From 6a4673b9ccbe821f4e531f53a66175168e798ad3 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:28:21 +0200 Subject: [PATCH 001/108] Test: Show Deleted Users as Deleted See: https://mantis.ilias.de/view.php?id=47978 --- components/ILIAS/Test/src/Participants/Participant.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Test/src/Participants/Participant.php b/components/ILIAS/Test/src/Participants/Participant.php index 21bf4258b985..d51eaf4f4b93 100644 --- a/components/ILIAS/Test/src/Participants/Participant.php +++ b/components/ILIAS/Test/src/Participants/Participant.php @@ -253,12 +253,16 @@ public function getDisplayName(Language $language, bool $anonymous_test = false) return $language->txt('anonymous'); } + if ($this->login === '' && $this->firstname === '' && $this->lastname === '') { + return $language->txt('user_deleted'); + } + $display_name = ''; - if ($this->firstname) { + if ($this->firstname !== '') { $display_name .= $this->firstname . ' '; } - if ($this->lastname) { + if ($this->lastname !== '') { $display_name .= $this->lastname; } From 81ee5a848531c6f818917ce4c53d9083bf80ebb2 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:29:16 +0200 Subject: [PATCH 002/108] Test: Retrieve Importname in Participant Repo --- .../ILIAS/Test/src/Participants/ParticipantRepository.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ILIAS/Test/src/Participants/ParticipantRepository.php b/components/ILIAS/Test/src/Participants/ParticipantRepository.php index 553b99256877..ac3dda5140d0 100755 --- a/components/ILIAS/Test/src/Participants/ParticipantRepository.php +++ b/components/ILIAS/Test/src/Participants/ParticipantRepository.php @@ -362,6 +362,7 @@ private function getActiveParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, + ta.importname, ud.firstname, ud.lastname, ud.login, @@ -401,6 +402,7 @@ private function getInvitedParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, + ta.importname, ud.firstname, ud.lastname, ud.login, From 0923cd57a73cafffdc058c67eaff132aaa943af7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 24 Jun 2026 11:53:32 +0200 Subject: [PATCH 003/108] Test: Retrieve Importname in Participant Repo This reverts commit b7dc09a5961f960299d1e9872f43392811aaf635. It should not have been picked to ILIAS 11. --- .../ILIAS/Test/src/Participants/ParticipantRepository.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/ILIAS/Test/src/Participants/ParticipantRepository.php b/components/ILIAS/Test/src/Participants/ParticipantRepository.php index ac3dda5140d0..553b99256877 100755 --- a/components/ILIAS/Test/src/Participants/ParticipantRepository.php +++ b/components/ILIAS/Test/src/Participants/ParticipantRepository.php @@ -362,7 +362,6 @@ private function getActiveParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, - ta.importname, ud.firstname, ud.lastname, ud.login, @@ -402,7 +401,6 @@ private function getInvitedParticipantsQuery(): string ta.last_finished_pass, ta.last_started_pass, COALESCE(ta.last_started_pass, -1) <> COALESCE(ta.last_finished_pass, -1) as unfinished_attempts, - ta.importname, ud.firstname, ud.lastname, ud.login, From f62fa2cbd25050982bc40a25f797f54e637ae468 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 25 Jun 2026 08:46:30 +0200 Subject: [PATCH 004/108] Test: Set Original Id on Test Copy See: https://mantis.ilias.de/view.php?id=47983 --- .../class.ilTestFixedQuestionSetConfig.php | 19 ++++++++++----- .../classes/class.assQuestion.php | 23 +++++++++++++++++-- .../tests/assQuestionTest.php | 2 +- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php b/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php index 2f429f9fcb02..92db879f27f7 100755 --- a/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php +++ b/components/ILIAS/Test/classes/class.ilTestFixedQuestionSetConfig.php @@ -71,12 +71,19 @@ public function cloneQuestionSetRelatedData(ilObjTest $clone_test_obj): void foreach ($this->test_obj->questions as $key => $question_id) { $question_orig = assQuestion::instantiateQuestion($question_id); - $clone_test_obj->questions[$key] = $question_orig->duplicate(true, '', '', -1, $clone_test_obj->getId()); - - $original_id = $this->questionrepository->getForQuestionId($question_id)->getOriginalId(); - - $question_clone = assQuestion::instantiateQuestion($clone_test_obj->questions[$key]); - $question_clone->saveToDb($original_id); + $clone_test_obj->questions[$key] = $question_orig->duplicate( + false, + '', + '', + -1, + $clone_test_obj->getId() + ); + $question_clone = assQuestion::instantiateQuestion( + $clone_test_obj->questions[$key] + ); + $question_clone->updateOriginalId( + $question_orig->getOriginalId() + ); // Save the mapping of old question id <-> new question id // This will be used in class.ilObjCourse::cloneDependencies to copy learning objectives diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 8a8cf44b2daf..7f3e20ad0df0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -1228,8 +1228,27 @@ public function saveQuestionDataToDb(?int $original_id = null): void ]); } + /** + * + * @deprecated This is a momentary helper function to update the original id, + * when cloning a test. Do not use anywhere else. 2026-06-25, sk + */ + public function updateOriginalId(?int $original_id = null): void + { + $this->original_id = $original_id; + $this->db->update( + 'qpl_questions', + [ + 'original_id' => [ilDBConstants::T_INTEGER, $original_id], + ], + [ + 'question_id' => [ilDBConstants::T_INTEGER, $this->getId()] + ] + ); + } + public function duplicate( - bool $for_test = true, + bool $set_original_id = true, string $title = '', string $author = '', int $owner = -1, @@ -1256,7 +1275,7 @@ public function duplicate( if ($owner) { $clone->setOwner($owner); } - if ($for_test) { + if ($set_original_id) { $clone->saveToDb($this->id); } else { $clone->saveToDb(); diff --git a/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php b/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php index 96e8a43ff484..99ecf3210d54 100644 --- a/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php +++ b/components/ILIAS/TestQuestionPool/tests/assQuestionTest.php @@ -69,7 +69,7 @@ public function getQuestionType(): string return ''; } - public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int + public function duplicate(bool $set_original_id = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int { return 0; } From 4ca7cef30a470b2939fdc87ce18190e342b66677 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 20 Jan 2026 13:33:46 +0100 Subject: [PATCH 005/108] BookingPool: Remove reservations on pool deletion See: https://mantis.ilias.de/view.php?id=21705 Deleting or trashing a booking pool left reservations and calendar entries behind. The Booking Manager module now listens for ILIAS object `delete` and `toTrash` events; `ObjectEvent::handleDeletion` resolves each affected pool and calls `ilBookingObject::deleteReservationsAndCalEntries` for every booking object. --- .../BookingManager/Objects/ObjectEvent.php | 48 +++++++++++++++++++ .../Service/class.InternalDomainService.php | 6 +++ ...class.ilBookingManagerAppEventListener.php | 8 ++++ components/ILIAS/BookingManager/module.xml | 3 +- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 components/ILIAS/BookingManager/Objects/ObjectEvent.php diff --git a/components/ILIAS/BookingManager/Objects/ObjectEvent.php b/components/ILIAS/BookingManager/Objects/ObjectEvent.php new file mode 100644 index 000000000000..f51b65947bf1 --- /dev/null +++ b/components/ILIAS/BookingManager/Objects/ObjectEvent.php @@ -0,0 +1,48 @@ +getId(); + } catch (ilObjectTypeMismatchException) { + continue; + } + + foreach (ilBookingObject::getList($pool_id) as $booking_object) { + $booking_object_id = $booking_object['booking_object_id'] ?? null; + if ($booking_object_id === null) { + continue; + } + + (new ilBookingObject($booking_object_id))->deleteReservationsAndCalEntries($booking_object_id); + } + } + } +} diff --git a/components/ILIAS/BookingManager/Service/class.InternalDomainService.php b/components/ILIAS/BookingManager/Service/class.InternalDomainService.php index 2b59b3ad4a11..ab2fb7defe18 100755 --- a/components/ILIAS/BookingManager/Service/class.InternalDomainService.php +++ b/components/ILIAS/BookingManager/Service/class.InternalDomainService.php @@ -20,6 +20,7 @@ namespace ILIAS\BookingManager; +use ILIAS\BookingManager\Objects\ObjectEvent; use ILIAS\DI\Container; use ILIAS\Repository\GlobalDICDomainServices; use ILIAS\BookingManager\BookingProcess\BookingProcessManager; @@ -128,6 +129,11 @@ public function userEvent(): UserEvent return self::$instances["user_event"] ??= new UserEvent($this); } + public function objectEvent(): ObjectEvent + { + return new ObjectEvent(); + } + public function bookingSettings(): SettingsManager { return self::$instances["settings"] ??= new SettingsManager( diff --git a/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php b/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php index de3c29c33729..39344fef5569 100644 --- a/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php +++ b/components/ILIAS/BookingManager/classes/class.ilBookingManagerAppEventListener.php @@ -42,6 +42,14 @@ public static function handleEvent( break; } break; + case "components/ILIAS/ILIASObject": + switch ($a_event) { + case "toTrash": + case "delete": + $DIC->bookingManager()->internal()->domain()->objectEvent()->handleDeletion([$a_parameter["ref_id"]]); + break; + } + break; } } } diff --git a/components/ILIAS/BookingManager/module.xml b/components/ILIAS/BookingManager/module.xml index a5f775ba7db2..a981b186622d 100755 --- a/components/ILIAS/BookingManager/module.xml +++ b/components/ILIAS/BookingManager/module.xml @@ -5,7 +5,7 @@ cat crs @@ -17,6 +17,7 @@ + From 3e76fa31ac56e7651b1350431db3aee039e42101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Jou=C3=9Fen?= Date: Wed, 3 Dec 2025 12:20:30 +0100 Subject: [PATCH 006/108] T&A Streamline usage of getAdditionalTableName to never expect a array return value --- .../classes/class.assQuestion.php | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 7f3e20ad0df0..5fe7dd438d73 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -816,20 +816,14 @@ public function deleteAnswers(int $question_id): void public function deleteAdditionalTableData(int $question_id): void { - $additional_table_name = $this->getAdditionalTableName(); + $table_name = $this->getAdditionalTableName(); - if (!is_array($additional_table_name)) { - $additional_table_name = [$additional_table_name]; - } - - foreach ($additional_table_name as $table) { - if (strlen($table)) { - $this->db->manipulateF( - "DELETE FROM $table WHERE question_fi = %s", - ['integer'], - [$question_id] - ); - } + if (strlen($table_name)) { + $this->db->manipulateF( + "DELETE FROM $table_name WHERE question_fi = %s", + [ilDBConstants::T_INTEGER], + [$question_id] + ); } } From 097b2a32b4aeab4b72795589150180e3a7d0dfa3 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 20 Jan 2026 08:44:23 +0100 Subject: [PATCH 007/108] BookingPool: Render Pool Without Booking Info See: https://mantis.ilias.de/view.php?id=46909 `ProcessUtilGUI::handleBookingSuccess()` only redirects to `displayPostInfo` when the bookable object has post text or a booking info file; otherwise, after the success message (and async return-URL handling when needed), it redirects to `ilObjBookingPoolGUI::render` so the pool view opens instead of stopping on an empty info step. Added an optional custom success message, split redirect logic into helpers, and aligned with/without-schedule booking flows (including `void` `confirmedBooking()` and translated text for multiple bookings). --- .../BookingProcess/class.ProcessUtilGUI.php | 70 ++++++++++++------- .../class.ilBookingProcessWithScheduleGUI.php | 11 +-- ...ass.ilBookingProcessWithoutScheduleGUI.php | 33 +++++---- 3 files changed, 68 insertions(+), 46 deletions(-) diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php index 584281ab09f1..1ddab4e7782f 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ProcessUtilGUI.php @@ -37,6 +37,8 @@ class ProcessUtilGUI protected InternalGUIService $gui; protected InternalDomainService $domain; protected object $parent_gui; + protected \ilLanguage $lng; + protected \ilGlobalTemplateInterface $tpl; public function __construct( InternalDomainService $domain_service, @@ -53,6 +55,8 @@ public function __construct( $this->ctrl = $this->gui->ctrl(); $this->request = $this->gui->standardRequest(); $this->objects_manager = $domain_service->objects($pool->getId()); + $this->lng = $domain_service->lng(); + $this->tpl = $this->gui->mainTemplate(); } // Back to parent @@ -126,38 +130,50 @@ protected function checkPermission(string $a_perm) : void public function handleBookingSuccess( int $a_obj_id, string $post_info_cmd, - ?array $a_rsv_ids = null + ?array $a_rsv_ids = null, + ?string $message = null ): void { - $this->log->debug("handleBookingSuccess"); - $main_tpl = $this->gui->mainTemplate(); - $ctrl = $this->gui->ctrl(); - $lng = $this->domain->lng(); - $request = $this->gui->standardRequest(); - - $main_tpl->setOnScreenMessage('success', $lng->txt('book_reservation_confirmed'), true); + $this->log->debug('handleBookingSuccess'); + $this->tpl->setOnScreenMessage( + 'success', + $message ?: $this->lng->txt('book_reservation_confirmed'), + true + ); + + $this->redirectToReturnCmdIfAsynchronous(); + $this->redirectToBookingInformationIfAvailable($a_obj_id, $post_info_cmd, $a_rsv_ids); + $this->redirectToRender(); + } - // show post booking information? + public function redirectToBookingInformationIfAvailable(int $a_obj_id, string $post_info_cmd, ?array $a_rsv_ids = null): void + { $obj = new \ilBookingObject($a_obj_id); - $pfile = $this->objects_manager->getBookingInfoFilename($a_obj_id); - $ptext = $obj->getPostText(); + if (trim($obj->getPostText()) === '' && $this->objects_manager->getBookingInfoFilename($a_obj_id) === '') { + return; + } - if (trim($ptext) || $pfile) { - if (count($a_rsv_ids)) { - $ctrl->setParameter( - $this->parent_gui, - 'rsv_ids', - implode(";", $a_rsv_ids) - ); - } - $ctrl->redirect($this->parent_gui, $post_info_cmd); - } else { - if ($ctrl->isAsynch()) { - $this->gui->send(""); - } else { - $this->back(); - } + $a_rsv_ids ??= []; + if ($a_rsv_ids !== []) { + $this->ctrl->setParameter($this->parent_gui, 'rsv_ids', implode(';', $a_rsv_ids)); } + $this->ctrl->redirect($this->parent_gui, $post_info_cmd); + } + + private function redirectToReturnCmdIfAsynchronous(): void + { + if (!$this->ctrl->isAsynch()) { + return; + } + + $this->gui->send( + "" + ); + } + + private function redirectToRender(): void + { + $this->ctrl->saveParameterByClass(\ilObjBookingPoolGUI::class, 'ref_id'); + $this->ctrl->redirectByClass(\ilObjBookingPoolGUI::class, 'render'); } /** diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php index 79fe039b102f..696d42e5edc0 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithScheduleGUI.php @@ -514,12 +514,13 @@ protected function bookAvailableItems(?int $recurrence = null, ?ilDateTime $unti $until, $message ); - if (count($booked) > 0) { - $this->util_gui->handleBookingSuccess($obj_id, "displayPostInfo", $booked); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); - $this->util_gui->back(); + if ($booked !== []) { + $this->util_gui->handleBookingSuccess($obj_id, 'displayPostInfo', $booked); + return; } + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); + $this->util_gui->back(); } protected function getBookAvailableTarget( diff --git a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php index 384cb89f4dbc..1922cdee8e80 100755 --- a/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php +++ b/components/ILIAS/BookingManager/BookingProcess/class.ilBookingProcessWithoutScheduleGUI.php @@ -285,12 +285,12 @@ public function saveMultipleBookings(): void { $participants = $this->book_request->getParticipants(); $object_id = $this->book_request->getObjectId(); - if (count($participants) > 0 && $object_id > 0) { + if ($participants !== [] && $object_id > 0) { $this->book_obj_id = $object_id; } else { $this->util_gui->back(); } - $rsv_ids = array(); + $rsv_ids = []; foreach ($participants as $id) { if (!$this->access->canManageReservationForUser($this->pool->getRefId(), $id)) { $this->showNoPermission(); @@ -305,24 +305,29 @@ public function saveMultipleBookings(): void ); } - if (count($rsv_ids)) { - $this->tpl->setOnScreenMessage('success', "booking_multiple_succesfully"); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed_overbooked'), true); + if ($rsv_ids !== []) { + $this->util_gui->handleBookingSuccess( + $object_id, + 'displayPostInfo', + $rsv_ids, + $this->lng->txt('booking_multiple_succesfully') + ); + return; } + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed_overbooked'), true); $this->util_gui->back(); } - // // Step 2: Confirmation // // Book object - either of type or specific - for given dates - public function confirmedBooking($message = ""): bool + public function confirmedBooking($message = ''): void { $success = false; - $rsv_ids = array(); + $rsv_ids = []; if ($this->book_obj_id > 0) { $object_id = $this->book_obj_id; @@ -349,12 +354,12 @@ public function confirmedBooking($message = ""): bool } if ($success) { - $this->util_gui->handleBookingSuccess($success, "displayPostInfo", $rsv_ids); - } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); - $this->ctrl->redirect($this, 'book'); + $this->util_gui->handleBookingSuccess($success, 'displayPostInfo', $rsv_ids); + return; } - return true; + + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('book_reservation_failed'), true); + $this->ctrl->redirect($this, 'book'); } public function displayPostInfo(): void From 829fe9cf26a6318d65cb83ed46273bef466541e5 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 16 Jan 2026 12:14:27 +0100 Subject: [PATCH 008/108] BookingPool: Allow Pool Filter in Course Resources See: https://mantis.ilias.de/view.php?id=46889 From the course resources tab, `ilBookingGatewayGUI` passed the course object id as reservation context, which pre-filtered the list and hid the booking pool filter. For courses, `context_obj_id` is now `null`, so `ilBookingReservationsGUI` does not restrict by context and the pool filter can be used. The constructor accepts `?int $context_obj_id` instead of defaulting to `0`. --- .../BookingService/class.ilBookingGatewayGUI.php | 6 +++++- .../Reservations/class.ilBookingReservationsGUI.php | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php b/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php index c6ad0f1f3255..ee95ae9ed325 100755 --- a/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php +++ b/components/ILIAS/BookingManager/BookingService/class.ilBookingGatewayGUI.php @@ -174,7 +174,11 @@ public function executeCommand(): void case "ilbookingreservationsgui": $this->showPoolSelector("ilbookingreservationsgui"); $this->setSubTabs("reservations"); - $res_gui = new ilBookingReservationsGUI($this->pool, $this->help, $this->obj_id); + $res_gui = new ilBookingReservationsGUI( + $this->pool, + $this->help, + ilObject::_lookupType($this->obj_id) !== 'crs' ? $this->obj_id : null + ); $this->ctrl->forwardCommand($res_gui); break; diff --git a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php index a633795ca9cb..adacb94570a2 100755 --- a/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php +++ b/components/ILIAS/BookingManager/Reservations/class.ilBookingReservationsGUI.php @@ -40,7 +40,7 @@ class ilBookingReservationsGUI protected array $raw_post_data; protected StandardGUIRequest $book_request; protected ilBookingHelpAdapter $help; - protected int $context_obj_id; + protected ?int $context_obj_id; protected ilCtrl $ctrl; protected ilGlobalTemplateInterface $tpl; protected ilLanguage $lng; @@ -53,7 +53,7 @@ class ilBookingReservationsGUI protected int $booked_user; protected ilUIService $ui_service; - public function __construct(ilObjBookingPool $pool, ilBookingHelpAdapter $help, int $context_obj_id = 0) + public function __construct(ilObjBookingPool $pool, ilBookingHelpAdapter $help, ?int $context_obj_id = null) { global $DIC; @@ -202,7 +202,7 @@ public function log(): void $this->access->canManageAllReservations($this->ref_id) || $this->pool->hasPublicLog(), $this->ui_service->filter()->getData($bookings_table->getFilter()) ?? [], null, - $this->context_obj_id > 0 ? [$this->context_obj_id] : null + $this->context_obj_id !== null ? [$this->context_obj_id] : null ); $reservations_table->getExportMode() > 0 && $reservations_table->exportData($reservations_table->getExportMode(), true); } From b49dc7263b59f9cce7bddc2991849482764a5fb4 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 17 Apr 2026 08:18:26 +0200 Subject: [PATCH 009/108] BookingPool: Disambiguate Info Stakeholders See: https://mantis.ilias.de/view.php?id=47260 Both `ilBookBookingInfoStakeholder` and `ilBookObjectInfoStakeholder` inherited the same `getConsumerNameForPresentation()` label from the parent, which made the two resource consumers hard to tell apart. Each class now appends a suffix (`/BookingInfo` or `/ObjectInfo`) to the parent name. --- .../Objects/class.ilBookBookingInfoStakeholder.php | 5 +++++ .../Objects/class.ilBookObjectInfoStakeholder.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php b/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php index 54413bf58529..bae67fbdfaf3 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookBookingInfoStakeholder.php @@ -33,6 +33,11 @@ public function getOwnerOfNewResources(): int return $this->default_owner; } + public function getConsumerNameForPresentation(): string + { + return parent::getConsumerNameForPresentation() . '/BookingInfo'; + } + public function canBeAccessedByCurrentUser(ResourceIdentification $identification): bool { global $DIC; diff --git a/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php b/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php index bcc4b6fac7d3..3b4efb35302f 100755 --- a/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php +++ b/components/ILIAS/BookingManager/Objects/class.ilBookObjectInfoStakeholder.php @@ -34,6 +34,11 @@ public function getOwnerOfNewResources(): int return $this->default_owner; } + public function getConsumerNameForPresentation(): string + { + return parent::getConsumerNameForPresentation() . '/ObjectInfo'; + } + public function canBeAccessedByCurrentUser(ResourceIdentification $identification): bool { global $DIC; From a290618c4c38429a9c9a57c0cc8b402eebd0b9b7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 27 Jan 2026 11:11:44 +0100 Subject: [PATCH 010/108] fix: missing logger dependency --- .../ILIAS/News/classes/class.ilNewsForContextBlockGUI.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php index 2d698667fb70..98149d3c2ab3 100755 --- a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php +++ b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php @@ -51,6 +51,7 @@ class ilNewsForContextBlockGUI extends ilBlockGUI protected ilHelpGUI $help; protected ilSetting $settings; protected ilTabsGUI $tabs; + protected ilLogger $logger; protected StandardGUIRequest $std_request; protected InternalDomainService $domain; @@ -70,6 +71,7 @@ public function __construct() $this->help = $DIC["ilHelp"]; $this->settings = $DIC->settings(); $this->tabs = $DIC->tabs(); + $this->logger = $DIC->logger()->news(); $locator = $DIC->news()->internal(); $this->std_request = $locator->gui()->standardRequest(); From 6bddf9499dae59ba58f8c6b99f8d57e64c9ac919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Jou=C3=9Fen?= Date: Fri, 26 Jun 2026 12:28:36 +0200 Subject: [PATCH 011/108] News: Fix duplicated property declaration in ContextBlockGUI --- components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php | 1 - 1 file changed, 1 deletion(-) diff --git a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php index 98149d3c2ab3..da2d994301f9 100755 --- a/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php +++ b/components/ILIAS/News/classes/class.ilNewsForContextBlockGUI.php @@ -51,7 +51,6 @@ class ilNewsForContextBlockGUI extends ilBlockGUI protected ilHelpGUI $help; protected ilSetting $settings; protected ilTabsGUI $tabs; - protected ilLogger $logger; protected StandardGUIRequest $std_request; protected InternalDomainService $domain; From 9bec14a049dce3db64a39bf2185eab165f82753f Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Fri, 20 Feb 2026 12:53:46 +0100 Subject: [PATCH 012/108] BackgroundTasks: Add grp to folder-type whitelist See: https://mantis.ilias.de/view.php?id=42358 `ilCollectFilesJob` only treated `fold` and `crs` as folder-like objects, so selecting a group when collecting files skipped its contents. The job now recurses `grp` the same way, skips non-`ilObject` instances safely, and keeps single-file handling separate after folder traversal. --- .../classes/Jobs/class.ilCollectFilesJob.php | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php b/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php index 3dae62a64972..140b8fced1f6 100755 --- a/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php +++ b/components/ILIAS/BackgroundTasks_/classes/Jobs/class.ilCollectFilesJob.php @@ -97,21 +97,37 @@ public function run(array $input, Observer $observer): Value foreach ($object_ref_ids as $object_ref_id) { $object = ilObjectFactory::getInstanceByRefId($object_ref_id); + if (!$object instanceof ilObject) { + continue; + } + $object_type = $object->getType(); $object_name = $object->getTitle(); - $object_temp_dir = ""; // empty as content will be added in recurseFolder and getFileDirs + $object_temp_dir = ''; // empty as content will be added in recurseFolder and getFileDirs - if ($object_type === "fold" || $object_type === "crs") { + if (in_array($object_type, ['fold', 'crs', 'grp'], true)) { $num_recursions = 0; - $files_from_folder = $this->recurseFolder($object_ref_id, $object_name, $object_temp_dir, $num_recursions, $initiated_by_folder_action); + $files_from_folder = $this->recurseFolder( + $object_ref_id, + $object_name, + $object_temp_dir, + $num_recursions, + $initiated_by_folder_action + ); $files = array_merge($files, $files_from_folder); - } elseif (($object_type === "file") && (($file_dirs = $this->getFileDirs( - $object_ref_id, - $object_name, - $object_temp_dir - )) !== false)) { - $files[] = $file_dirs; + continue; } + + if ($object_type !== 'file') { + continue; + } + + $file_dirs = $this->getFileDirs($object_ref_id, $object_name, $object_temp_dir); + if (!is_array($file_dirs)) { + continue; + } + + $files[] = $file_dirs; } $this->logger->debug('Collected files:'); $this->logger->dump($files); From 241d08f47d8132a03871e9b976d1513de5aac3cf Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Wed, 24 Jun 2026 13:51:40 +0200 Subject: [PATCH 013/108] ItemGroup: Add migration for display fields See: https://mantis.ilias.de/view.php?id=31861 Item groups still stored presentation in legacy `hide_title` and `behaviour` columns after the new display model was introduced. `ilItemGroupDisplayMigration` maps those values to `display` and `toggleable_initially`, marks rows as migrated, and is registered on the ItemGroup setup `Agent` after `ilItemGroupDBUpdateSteps`. --- .../ItemGroup/classes/Setup/class.Agent.php | 7 + .../class.ilItemGroupDisplayMigration.php | 123 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php diff --git a/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php b/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php index 40a3967dc68d..2c3737c26f70 100755 --- a/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php +++ b/components/ILIAS/ItemGroup/classes/Setup/class.Agent.php @@ -29,4 +29,11 @@ public function getUpdateObjective(?Setup\Config $config = null): Setup\Objectiv { return new \ilDatabaseUpdateStepsExecutedObjective(new ilItemGroupDBUpdateSteps()); } + + public function getMigrations(): array + { + return [ + new ilItemGroupDisplayMigration() + ]; + } } diff --git a/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php b/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php new file mode 100644 index 000000000000..f22fabe7cf1d --- /dev/null +++ b/components/ILIAS/ItemGroup/classes/Setup/class.ilItemGroupDisplayMigration.php @@ -0,0 +1,123 @@ +db = $environment->getResource(Environment::RESOURCE_DATABASE); + } + + public function step(Environment $environment): void + { + $result = $this->db->queryF( + 'SELECT id, hide_title, behaviour FROM itgr_data WHERE hide_title <> %s AND behaviour <> %s LIMIT 1', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [self::MIGRATED_MARKER, self::MIGRATED_MARKER] + ); + + $row = $this->db->fetchAssoc($result); + if ($row === null) { + return; + } + + [$display, $toggleable_initially] = $this->mapLegacyValues((int) $row['hide_title'], (int) $row['behaviour']); + + $this->db->update( + 'itgr_data', + [ + 'display' => [ilDBConstants::T_TEXT, $display], + 'toggleable_initially' => [ilDBConstants::T_TEXT, $toggleable_initially], + 'hide_title' => [ilDBConstants::T_INTEGER, self::MIGRATED_MARKER], + 'behaviour' => [ilDBConstants::T_INTEGER, self::MIGRATED_MARKER], + ], + [ + 'id' => [ilDBConstants::T_INTEGER, (int) $row['id']], + ] + ); + } + + public function getRemainingAmountOfSteps(): int + { + $result = $this->db->queryF( + 'SELECT COUNT(id) AS cnt FROM itgr_data WHERE hide_title <> %s AND behaviour <> %s', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER], + [self::MIGRATED_MARKER, self::MIGRATED_MARKER] + ); + + return (int) ($this->db->fetchObject($result)?->cnt ?? 0); + } + + /** + * @return array{0: string, 1: string} + */ + private function mapLegacyValues(int $hide_title, int $behaviour): array + { + return match (true) { + $hide_title === 1 => [ + ilItemGroupAR::DISPLAY_WITHOUT_TITLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ], + $behaviour === ilItemGroupBehaviour::EXPANDABLE_CLOSED => [ + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_CLOSED, + ], + $behaviour === ilItemGroupBehaviour::EXPANDABLE_OPEN => [ + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ], + default => [ + ilItemGroupAR::DISPLAY_WITH_TITLE, + ilItemGroupAR::DISPLAY_WITH_TITLE_AND_TOGGLEABLE_INITIALLY_OPEN, + ] + }; + } +} From 1f31266bb291973b09a856e5fb7763b502ff8536 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Thu, 12 Mar 2026 08:03:33 +0100 Subject: [PATCH 014/108] Test: Full Width for Cloze Inputs in Scoring See: https://mantis.ilias.de/view.php?id=47413 Manual scoring cloze gaps render narrow text fields by default. Added a stylesheet rule for `.ilAssClozeTest` answer areas so `input[type=text]` spans the full row width, in both `_component_test.scss` and compiled `delos.css`. --- .../legacy/Modules/_component_test.scss | 12 +++++++++--- templates/default/delos.css | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/templates/default/070-components/legacy/Modules/_component_test.scss b/templates/default/070-components/legacy/Modules/_component_test.scss index ddb96549b42e..3d082718e745 100755 --- a/templates/default/070-components/legacy/Modules/_component_test.scss +++ b/templates/default/070-components/legacy/Modules/_component_test.scss @@ -172,9 +172,15 @@ $cons-scoring-bottom-fade-height: $il-padding-xxxlarge-vertical * 2; display: none; } - // fix for ordering question - .ilc_qanswer_Answer.solutionbox { - width: auto; + .ilc_qanswer_Answer { + // fix for ordering question + .solutionbox { + width: auto; + } + + .ilc_answers.answers.ilAssClozeTest div input[type=text] { + width: 100%; + } } } diff --git a/templates/default/delos.css b/templates/default/delos.css index 0b838ddd9024..9beac26ca5e9 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -16905,6 +16905,9 @@ div.ilc_Page.readonly textarea[disabled] { .c-consecutive-scoring__answer__body .ilc_qanswer_Answer.solutionbox { width: auto; } +.c-consecutive-scoring__answer__body .ilc_qanswer_Answer.ilc_answers.answers.ilAssClozeTest div input[type=text] { + width: 100%; +} .c-consecutive-scoring__answer__grade-card { width: 40%; border-left: 1px solid #dddddd; From a981ee45bd5f28b6ffc5b3363c9b80595c6cc965 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Mon, 16 Mar 2026 12:06:44 +0100 Subject: [PATCH 015/108] Test: Resize Mark Icon and Unify Summary Icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test player mark-question icon was 12×12px; it is now 28×28px in `_component_test_legacy.scss` and `delos.css`. In `QuestionsOfAttemptTable`, the marked column uses the standard checked/unchecked icons like the answered column instead of `marked.svg`. --- .../src/Questions/Presentation/QuestionsOfAttemptTable.php | 3 +-- .../070-components/legacy/Modules/_component_test_legacy.scss | 4 ++-- templates/default/delos.css | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php b/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php index 7ba58cbdaf2a..de061939dc99 100644 --- a/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php +++ b/components/ILIAS/Test/src/Questions/Presentation/QuestionsOfAttemptTable.php @@ -139,7 +139,6 @@ protected function getColumns(): array $icon_factory = $this->ui_factory->symbol()->icon(); $icon_checked = $icon_factory->custom('assets/images/standard/icon_checked.svg', $this->lng->txt('yes')); $icon_unchecked = $icon_factory->custom('assets/images/standard/icon_unchecked.svg', $this->lng->txt('no')); - $icon_marked = $icon_factory->custom('assets/images/object/marked.svg', $this->lng->txt('tst_question_marked')); $columns = [ 'order' => $column_factory->number($this->lng->txt('tst_qst_order')), @@ -148,7 +147,7 @@ protected function getColumns(): array 'postponed' => $column_factory->boolean(ucfirst($this->lng->txt('postponed')), $this->lng->txt('yes'), ''), 'points' => $column_factory->number($this->lng->txt('tst_maximum_points'))->withUnit($this->lng->txt('points_short')), 'answered' => $column_factory->boolean($this->lng->txt('answered'), $icon_checked, $icon_unchecked), - 'marked' => $column_factory->boolean($this->lng->txt('tst_question_marker'), $icon_marked, ''), + 'marked' => $column_factory->boolean($this->lng->txt('tst_question_marker'), $icon_checked, $icon_unchecked), ]; $optional_columns = [ diff --git a/templates/default/070-components/legacy/Modules/_component_test_legacy.scss b/templates/default/070-components/legacy/Modules/_component_test_legacy.scss index fc89928f1838..5d0f70e7bd21 100755 --- a/templates/default/070-components/legacy/Modules/_component_test_legacy.scss +++ b/templates/default/070-components/legacy/Modules/_component_test_legacy.scss @@ -538,8 +538,8 @@ td.ilc_Page { } .ilTestMarkQuestionIcon { - width: 12px; - height: 12px; + width: 28px; + height: 28px; } .ilTestAnswerStatusIcon { diff --git a/templates/default/delos.css b/templates/default/delos.css index 9beac26ca5e9..c23d5b29f046 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -16623,8 +16623,8 @@ td.ilc_Page { } .ilTestMarkQuestionIcon { - width: 12px; - height: 12px; + width: 28px; + height: 28px; } .ilTestAnswerStatusIcon { From f127cd02c02938e1a8f3efe24fe34a8a19c6a3a0 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 26 Jun 2026 14:11:02 +0200 Subject: [PATCH 016/108] [Fix] glossary: make copying glossary against broken terms --- components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php | 7 +++++-- components/ILIAS/Glossary/classes/class.ilObjGlossary.php | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php b/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php index bd85e6f407da..411e3ab9a0a1 100755 --- a/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php +++ b/components/ILIAS/Glossary/Term/class.ilGlossaryTerm.php @@ -642,9 +642,12 @@ public static function _copyTerm( int $a_term_id, int $a_glossary_id ): int { - $old_term = new ilGlossaryTerm($a_term_id); - // copy the term + try { + $old_term = new ilGlossaryTerm($a_term_id); + } catch (Exception $e) { + return 0; + } $new_term = new ilGlossaryTerm(); $new_term->setTerm($old_term->getTerm()); $new_term->setLanguage($old_term->getLanguage()); diff --git a/components/ILIAS/Glossary/classes/class.ilObjGlossary.php b/components/ILIAS/Glossary/classes/class.ilObjGlossary.php index 8a1fb18b603c..e836f7c79a41 100755 --- a/components/ILIAS/Glossary/classes/class.ilObjGlossary.php +++ b/components/ILIAS/Glossary/classes/class.ilObjGlossary.php @@ -590,6 +590,9 @@ public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = $term_mappings = array(); foreach (ilGlossaryTerm::getTermList([$this->getRefId()]) as $term) { $new_term_id = ilGlossaryTerm::_copyTerm($term["id"], $new_obj->getId()); + if ($new_term_id === 0) { + continue; + } $term_mappings[$term["id"]] = $new_term_id; // copy tax node assignments From 3c55bf12dcaf177b1211b1d675e3bc905459e6a2 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Fri, 26 Jun 2026 14:50:23 +0200 Subject: [PATCH 017/108] copage: drag/drop array fix --- components/ILIAS/COPage/Page/class.PageCommandActionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php b/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php index 018d7fdc46a1..feb1801a63ca 100755 --- a/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php +++ b/components/ILIAS/COPage/Page/class.PageCommandActionHandler.php @@ -184,7 +184,7 @@ protected function dragDropCommand(array $body): Server\Response $source = explode(":", $source); $target = explode(":", $target); - $updated = $page->moveContentAfter($source[0], $target[0], $source[1], $target[1]); + $updated = $page->moveContentAfter($source[0], $target[0], $source[1] ?? '', $target[1] ?? ''); return $this->sendPage($updated); } From 1fd99078a1792ae958cefb310562461b5a8ed8fc Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 09:55:24 +0200 Subject: [PATCH 018/108] blog: removed static calls --- .../ILIAS/Blog/Export/class.ilBlogDataSet.php | 2 +- .../Blog/Export/class.ilBlogImporter.php | 4 +++- .../Blog/Posting/class.ilBlogPosting.php | 21 ------------------- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 5 ++--- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php index b3aa993583a5..7e688a01008b 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php +++ b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php @@ -285,7 +285,7 @@ public function readData( // keywords foreach ($this->data as $idx => $item) { - $blog_id = ilBlogPosting::lookupBlogId($item["Id"]); + $blog_id = $this->service->domain()->posting()->lookupBlogId((int) $item["Id"]); $keywords = $this->service->domain()->posting()->getKeywords((int) $blog_id, (int) $item["Id"]); if ($keywords) { foreach ($keywords as $kidx => $keyword) { diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index 4481de258d39..60f8f93b4a01 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -61,10 +61,12 @@ public function importXmlRepresentation( public function finalProcessing( ilImportMapping $a_mapping ): void { + global $DIC; + $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = ilBlogPosting::lookupBlogId($blp_id); + $blog_id = $DIC->blog()->internal()->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php index 9e50d67ba854..86475aceafb7 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php @@ -254,27 +254,6 @@ public function unpublish(): void ); } - public static function lookupBlogId( - int $a_posting_id - ): ?int { - global $DIC; - return $DIC->blog()->internal()->domain()->posting()->lookupBlogId($a_posting_id); - } - - /** - * Checks whether a posting exists - */ - public static function exists( - int $a_blog_id, - int $a_posting_id - ): bool { - global $DIC; - return $DIC->blog()->internal()->domain()->posting()->exists( - $a_blog_id, - $a_posting_id - ); - } - /** * Set blog node id (needed for notification) */ diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 08e5be07df37..9ea6bf02de9c 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -103,6 +103,7 @@ public function __construct( $this->rbac_review = $domain->rbac()->review(); $this->rbacadmin = $domain->rbac()->admin(); $this->lng = $domain->lng(); + $this->posting_manager = $domain->posting(); $gui = $service->gui(); $this->gui = $gui; @@ -134,12 +135,10 @@ public function __construct( $blog_page = $this->blog_request->getBlogPage(); if ($blog_page > 0 && - ilBlogPosting::lookupBlogId($blog_page) !== $this->object->getId()) { + $this->posting_manager->lookupBlogId($blog_page) !== $this->object->getId()) { throw new ilException("Posting ID does not match blog."); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); - $blog_id = 0; if ($this->object) { // gather postings by month From a1ee21d87ec668fb33f5a2bd0fdeebdbfc641070 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 14:22:02 +0200 Subject: [PATCH 019/108] blog: removed static calls --- .../ILIAS/Blog/News/class.NewsManager.php | 6 +- .../Blog/Posting/Service/class.GUIService.php | 69 +++++++++++++++++++ .../Blog/Posting/class.ilBlogPosting.php | 5 +- .../Blog/Posting/class.ilBlogPostingGUI.php | 21 +++--- components/ILIAS/Blog/RSS/RSSGUI.php | 2 +- .../Blog/Service/class.InternalGUIService.php | 10 +++ .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 2 +- 7 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 components/ILIAS/Blog/Posting/Service/class.GUIService.php diff --git a/components/ILIAS/Blog/News/class.NewsManager.php b/components/ILIAS/Blog/News/class.NewsManager.php index b6b863237105..cc3c2d6b68ae 100644 --- a/components/ILIAS/Blog/News/class.NewsManager.php +++ b/components/ILIAS/Blog/News/class.NewsManager.php @@ -29,11 +29,15 @@ */ class NewsManager { + protected \ILIAS\Blog\Posting\Service\GUIService $posting_gui; + public function __construct( protected InternalDataService $data, protected InternalRepoService $repo, protected InternalDomainService $domain ) { + global $DIC; + $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); } /** @@ -110,7 +114,7 @@ public function handle(\ilBlogPosting $page, bool $update = false): void $news_item->setContentTextIsLangVar(false); $news_item->setContent($content); - $snippet = \ilBlogPostingGUI::getSnippet($page->getId()); + $snippet = $this->posting_gui->getSnippet($page->getId()); $news_item->setContentLong($snippet); if (!$news_item->getId()) { diff --git a/components/ILIAS/Blog/Posting/Service/class.GUIService.php b/components/ILIAS/Blog/Posting/Service/class.GUIService.php new file mode 100644 index 000000000000..7fcd9217f95e --- /dev/null +++ b/components/ILIAS/Blog/Posting/Service/class.GUIService.php @@ -0,0 +1,69 @@ +postingGUI(0, null, $a_id); + return $bpgui->getSnippet( + $a_truncate, + $a_truncate_length, + $a_truncate_sign, + $a_include_picture, + $a_picture_width, + $a_picture_height, + $a_export_directory + ); + } +} diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php index 86475aceafb7..e30778d3d707 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPosting.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPosting.php @@ -28,6 +28,7 @@ */ class ilBlogPosting extends ilPageObject { + protected \ILIAS\Blog\Posting\Service\GUIService $posting_gui; protected string $title = ""; protected ?ilDateTime $created = null; protected int $blog_node_id = 0; @@ -45,6 +46,8 @@ public function afterConstructor(): void $this->internal_data = $DIC->blog()->internal()->data(); $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); $this->blog_domain = $DIC->blog()->internal()->domain(); + $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); + } public function getParentType(): string @@ -267,7 +270,7 @@ public function setBlogNodeId( public function getNotificationAbstract(): string { - $snippet = ilBlogPostingGUI::getSnippet($this->getId(), true); + $snippet = $this->posting_gui->getSnippet($this->getId(), true); // making things more readable $snippet = str_replace(array('
', '
', '

', ''), "\n", $snippet); diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index 967bf110d875..997bf8b97e62 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -735,8 +735,7 @@ public function saveKeywordsForm(): void /** * Get first text paragraph of page */ - public static function getSnippet( - int $a_id, + public function getSnippet( bool $a_truncate = false, int $a_truncate_length = 500, string $a_truncate_sign = "...", @@ -745,25 +744,23 @@ public static function getSnippet( int $a_picture_height = 144, ?string $a_export_directory = null ): string { - $bpgui = new self(0, null, $a_id); - // scan the full page for media objects $img = ""; if ($a_include_picture) { - $img = $bpgui->getFirstMediaObjectAsTag($a_picture_width, $a_picture_height, $a_export_directory); + $img = $this->getFirstMediaObjectAsTag($a_picture_width, $a_picture_height, $a_export_directory); } - $bpgui->setRawPageContent(true); - $bpgui->setAbstractOnly(true); + $this->setRawPageContent(true); + $this->setAbstractOnly(true); // #8627: export won't work - should we set offline mode? - $bpgui->setFileDownloadLink("."); - $bpgui->setFullscreenLink("."); - $bpgui->setSourcecodeDownloadScript("."); - $bpgui->setProfileBackUrl("."); + $this->setFileDownloadLink("."); + $this->setFullscreenLink("."); + $this->setSourcecodeDownloadScript("."); + $this->setProfileBackUrl("."); // render without title - $page = $bpgui->showPage(); + $page = $this->showPage(); if ($a_truncate) { $page = ilPageObject::truncateHTML($page, $a_truncate_length, $a_truncate_sign); diff --git a/components/ILIAS/Blog/RSS/RSSGUI.php b/components/ILIAS/Blog/RSS/RSSGUI.php index 8ed3896f0d09..646f492afb06 100644 --- a/components/ILIAS/Blog/RSS/RSSGUI.php +++ b/components/ILIAS/Blog/RSS/RSSGUI.php @@ -75,7 +75,7 @@ public function deliver(string $a_wsp_id): void } // #16434 - $snippet = strip_tags(\ilBlogPostingGUI::getSnippet($id), "

"); + $snippet = strip_tags($this->gui->posting()->getSnippet($id), "

"); $snippet = str_replace("&", "&", $snippet); $snippet = ""; diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index 3d616e8ef350..fad63aa0f514 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -25,6 +25,7 @@ use ILIAS\PermanentLink\PermanentLinkManager; use ILIAS\Blog\ReadingTime\GUIService; use ILIAS\Blog\RSS\RSSGUI; +use ILIAS\Blog\Posting\Service\GUIService as PostingGUIService; class InternalGUIService { @@ -114,6 +115,15 @@ public function readingTime(): GUIService ); } + public function posting(): PostingGUIService + { + return self::$instance["posting"] ??= new PostingGUIService( + $this->data_service, + $this->domain_service, + $this + ); + } + public function rss(): RSSGUI { return self::$instance["rss"] ??= new RSSGUI( diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 9ea6bf02de9c..099336228a0b 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -1254,7 +1254,7 @@ public function renderList( $wtpl->parseCurrentBlock(); } - $snippet = ilBlogPostingGUI::getSnippet( + $snippet = $this->gui->posting()->getSnippet( $item_id, $this->blog_settings->getAbstractShorten(), $this->blog_settings->getAbstractShortenLength(), From c6c4f54392a576f899710d43a9250e7184752ef9 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 14:25:03 +0200 Subject: [PATCH 020/108] blog: removed static calls --- components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index 997bf8b97e62..a5a648b4a515 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -805,7 +805,7 @@ protected function getFirstMediaObjectAsTag( // see ilCOPageHTMLExport::exportHTMLMOB() $mob_dir = "./mobs/mm_" . $mob_obj->getId(); } - $mob_res = self::parseImage( + $mob_res = $this->parseImage( $mob_size["width"], $mob_size["height"], $a_width, @@ -828,7 +828,7 @@ protected function getFirstMediaObjectAsTag( return ""; } - protected static function parseImage( + protected function parseImage( int $src_width, int $src_height, int $tgt_width, From d5815fcf71fd2711cd0191359d46723a589e9901 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 17:10:53 +0200 Subject: [PATCH 021/108] blog: improve DIC handling --- .../Blog/Posting/class.ilBlogPostingGUI.php | 21 +++++++------------ .../class.ilBlogDraftsDerivedTaskProvider.php | 5 +++-- .../classes/class.ilBlogNewsRendererGUI.php | 16 ++++++++------ .../ILIAS/Blog/classes/class.ilObjBlog.php | 5 +++-- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 6 ++---- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index a5a648b4a515..af3fc1ae46a3 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -44,6 +44,7 @@ class ilBlogPostingGUI extends ilPageObjectGUI protected ilLocatorGUI $locator; protected ilSetting $settings; protected LOMServices $lom_services; + protected ilToolbarGUI $toolbar; protected int $node_id; protected PostingManager $posting_manager; protected ?object $access_handler = null; @@ -114,6 +115,7 @@ public function __construct( $this->notes = $DIC->notes(); $this->profile_gui = $DIC->blog()->internal()->gui()->profile(); $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->toolbar = $DIC->toolbar(); } public function executeCommand(): string @@ -179,12 +181,11 @@ protected function checkAccess(string $a_cmd): bool public function preview( ?string $a_mode = null ): string { - global $DIC; $ilCtrl = $this->ctrl; $tpl = $this->tpl; $ilSetting = $this->settings; - $toolbar = $DIC->toolbar(); + $toolbar = $this->toolbar; $append = ""; $this->getBlogPosting()->increaseViewCnt(); @@ -628,9 +629,7 @@ public function activatePage(bool $a_to_list = false): void */ public function editKeywords(): void { - global $DIC; - - $renderer = $DIC->ui()->renderer(); + $renderer = $this->blog_gui->ui()->renderer(); $ilTabs = $this->tabs; $tpl = $this->tpl; @@ -652,9 +651,7 @@ public function editKeywords(): void */ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\Standard { - global $DIC; - - $ui_factory = $DIC->ui()->factory(); + $ui_factory = $this->blog_gui->ui()->factory(); $keywords = $this->posting_manager->getKeywords( $this->getBlogPosting()->getBlogId(), @@ -681,7 +678,7 @@ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\ $input_tag = $input_tag->withValue($keywords); } - $DIC->ctrl()->setParameter( + $this->ctrl->setParameter( $this, 'tags', 'tags_processing' @@ -689,7 +686,7 @@ protected function initKeywordsForm(): \ILIAS\UI\Component\Input\Container\Form\ $section = $ui_factory->input()->field()->section([$input_tag], $this->lng->txt("blog_edit_keywords"), ""); - $form_action = $DIC->ctrl()->getFormAction($this, "saveKeywordsForm"); + $form_action = $this->ctrl->getFormAction($this, "saveKeywordsForm"); return $ui_factory->input()->container()->form()->standard($form_action, ["tags" => $section]); } @@ -707,9 +704,7 @@ protected function getParentObjId(): int public function saveKeywordsForm(): void { - global $DIC; - - $request = $DIC->http()->request(); + $request = $this->blog_gui->http()->request(); $form = $this->initKeywordsForm(); if ($request->getMethod() === "POST" diff --git a/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php b/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php index 800be9b2ff13..49f0c0a3c3aa 100755 --- a/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php +++ b/components/ILIAS/Blog/Tasks/classes/class.ilBlogDraftsDerivedTaskProvider.php @@ -37,8 +37,9 @@ public function __construct( ) { global $DIC; - $this->gui = $DIC->blog()->internal()->gui(); - $this->domain = $DIC->blog()->internal()->domain(); + $service = $DIC->blog()->internal(); + $this->gui = $service->gui(); + $this->domain = $service->domain(); $this->taskService = $taskService; $this->accessHandler = $accessHandler; $this->lng = $lng; diff --git a/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php b/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php index 3275e896e556..32dfa9f6ddc2 100755 --- a/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php +++ b/components/ILIAS/Blog/classes/class.ilBlogNewsRendererGUI.php @@ -18,17 +18,21 @@ declare(strict_types=1); -/** - * Blog news renderer - * @author Alexander Killing - */ class ilBlogNewsRendererGUI extends ilNewsDefaultRendererGUI { - public function getObjectLink(): string + protected \ILIAS\Blog\InternalGUIService $blog_gui; + + public function __construct() { global $DIC; + parent::__construct(); + $service = $DIC->blog()->internal(); + $this->blog_gui = $service->gui(); + } - $pl = $DIC->blog()->internal()->gui()->permanentLink($this->getNewsRefId()); + public function getObjectLink(): string + { + $pl = $this->blog_gui->permanentLink($this->getNewsRefId()); $n = $this->getNewsItem(); $posting_id = 0; diff --git a/components/ILIAS/Blog/classes/class.ilObjBlog.php b/components/ILIAS/Blog/classes/class.ilObjBlog.php index 27da54a1f0b0..90092326f992 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlog.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlog.php @@ -44,8 +44,9 @@ public function __construct( ) { global $DIC; + $service = $DIC->blog()->internal(); $this->notes_service = $DIC->notes(); - $this->settings_manager = $DIC->blog()->internal()->domain()->blogSettings(); + $this->settings_manager = $service->domain()->blogSettings(); parent::__construct($a_id, $a_reference); $this->rbac_review = $DIC->rbac()->review(); @@ -57,7 +58,7 @@ public function __construct( if ($this->getId() > 0) { $this->blog_settings = $this->settings_manager->getByObjId($this->getId()); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $service->domain()->posting(); } protected function initType(): void diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 099336228a0b..49b0f4b51f14 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -1810,10 +1810,8 @@ public function renderNavigation( } if (count($blocks)) { - global $DIC; - - $ui_factory = $DIC->ui()->factory(); - $ui_renderer = $DIC->ui()->renderer(); + $ui_factory = $this->ui->factory(); + $ui_renderer = $this->ui->renderer(); ksort($blocks); foreach ($blocks as $block) { From 4c20b8d80ccf22184ad28043210253155bdefc17 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 17:23:45 +0200 Subject: [PATCH 022/108] blog: improve DIC handling --- components/ILIAS/Blog/Export/BlogHtmlExport.php | 17 ++++++++++------- .../ILIAS/Blog/Export/class.ilBlogDataSet.php | 8 +++++--- .../ILIAS/Blog/Export/class.ilBlogExporter.php | 5 ++++- .../ILIAS/Blog/Export/class.ilBlogImporter.php | 3 ++- .../ILIAS/Blog/News/class.NewsManager.php | 6 +++--- .../Blog/PermanentLink/StaticUrlHandler.php | 3 ++- .../Service/class.InternalDomainService.php | 3 ++- .../Blog/classes/class.ilObjBlogListGUI.php | 5 ++--- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Blog/Export/BlogHtmlExport.php b/components/ILIAS/Blog/Export/BlogHtmlExport.php index b94f698f5158..e23f6447c2da 100755 --- a/components/ILIAS/Blog/Export/BlogHtmlExport.php +++ b/components/ILIAS/Blog/Export/BlogHtmlExport.php @@ -56,11 +56,13 @@ public function __construct( $this->blog_gui = $blog_gui; /** @var \ilObjBlog $blog */ $blog = $blog_gui->getObject(); + $this->blog = $blog; + + $blog_service = $DIC->blog()->internal(); + $this->collector = $DIC->export()->domain()->html()->collector($blog->getId()); $this->collector->init(); - $this->blog = $blog; - //$this->export_dir = $exp_dir; $this->sub_dir = $sub_dir; $this->target_dir = $exp_dir . "/" . $sub_dir; @@ -86,7 +88,7 @@ public function __construct( } else { $this->content_style_domain = $cs->domain()->styleForObjId($this->blog->getId()); } - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $blog_service->domain()->posting(); } protected function init(): void { @@ -339,8 +341,6 @@ public function buildExportLink( protected function getInitialisedTemplate( string $a_back_url = "" ): \ilGlobalPageTemplate { - global $DIC; - $this->export_util->resetGlobalScreen(); $location_stylesheet = \ilUtil::getStyleSheetLocation(); @@ -350,13 +350,16 @@ protected function getInitialisedTemplate( ); \ilPCQuestion::resetInitialState(); - $tabs = $DIC->tabs(); + $tabs = $this->tabs; $tabs->clearTargets(); $tabs->clearSubTabs(); if ($a_back_url) { $tabs->setBackTarget($this->lng->txt("back"), $a_back_url); } - $tpl = new \ilGlobalPageTemplate($DIC->globalScreen(), $DIC->ui(), $DIC->http()); + + /** @var \ILIAS\DI\Container $DIC */ + global $DIC; + $tpl = new \ilGlobalPageTemplate($this->global_screen, $DIC->ui(), $DIC->http()); $this->co_page_html_export->getPreparedMainTemplate($tpl); diff --git a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php index 7e688a01008b..b8f8e9195cbe 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogDataSet.php +++ b/components/ILIAS/Blog/Export/class.ilBlogDataSet.php @@ -44,13 +44,15 @@ public function __construct() { global $DIC; parent::__construct(); + + $blog_service = $DIC->blog()->internal(); + $this->content_style_domain = $DIC ->contentStyle() ->domain(); $this->notes = $DIC->notes(); - $this->reading_time = $DIC->blog()->internal()->domain()->readingTime(); - $this->blog_settings = $DIC->blog()->internal()->domain()->blogSettings(); - $this->service = $DIC->blog()->internal(); + $this->reading_time = $blog_service->domain()->readingTime(); + $this->blog_settings = $blog_service->domain()->blogSettings(); } public function getSupportedVersions(): array diff --git a/components/ILIAS/Blog/Export/class.ilBlogExporter.php b/components/ILIAS/Blog/Export/class.ilBlogExporter.php index ca20e3be90f0..71b49d3c5e4a 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogExporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogExporter.php @@ -34,10 +34,13 @@ public function init(): void $this->ds = new ilBlogDataSet(); $this->ds->setDSPrefix("ds"); + + $blog_service = $DIC->blog()->internal(); + $this->content_style_domain = $DIC ->contentStyle() ->domain(); - $this->posting_manager = $DIC->blog()->internal()->domain()->posting(); + $this->posting_manager = $blog_service->domain()->posting(); } public function getXmlExportTailDependencies( diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index 60f8f93b4a01..d5b1e2b0ed29 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -62,11 +62,12 @@ public function finalProcessing( ilImportMapping $a_mapping ): void { global $DIC; + $blog_service = $DIC->blog()->internal(); $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = $DIC->blog()->internal()->domain()->posting()->lookupBlogId($blp_id); + $blog_id = $blog_service->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/News/class.NewsManager.php b/components/ILIAS/Blog/News/class.NewsManager.php index cc3c2d6b68ae..59227a090de8 100644 --- a/components/ILIAS/Blog/News/class.NewsManager.php +++ b/components/ILIAS/Blog/News/class.NewsManager.php @@ -34,10 +34,10 @@ class NewsManager public function __construct( protected InternalDataService $data, protected InternalRepoService $repo, - protected InternalDomainService $domain + protected InternalDomainService $domain, + \ILIAS\Blog\InternalGUIService $gui ) { - global $DIC; - $this->posting_gui = $DIC->blog()->internal()->gui()->posting(); + $this->posting_gui = $gui->posting(); } /** diff --git a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php index f2e3ea3b9f5c..5589c26e47e2 100644 --- a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php +++ b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php @@ -37,11 +37,12 @@ public function getNamespace(): string public function handle(Request $request, Context $context, Factory $response_factory): Response { global $DIC; + $blog_service = $DIC->blog()->internal(); $ctrl = $DIC->ctrl(); $access = $DIC->access(); $uri = null; - $blog_domain = $DIC->blog()->internal()->domain(); + $blog_domain = $blog_service->domain(); $id = $request->getReferenceId()?->toInt() ?? 0; $additional_params = $request->getAdditionalParameters() ?? []; diff --git a/components/ILIAS/Blog/Service/class.InternalDomainService.php b/components/ILIAS/Blog/Service/class.InternalDomainService.php index 55b2141439c3..8aa784d42057 100755 --- a/components/ILIAS/Blog/Service/class.InternalDomainService.php +++ b/components/ILIAS/Blog/Service/class.InternalDomainService.php @@ -119,7 +119,8 @@ public function news(): NewsManager return self::$instance["news"] ??= new NewsManager( $this->data, $this->repo, - $this + $this, + $this->dic->blog()->internal()->gui() ); } diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php index 6212950298f3..ffd04737cbe0 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php @@ -70,11 +70,10 @@ public function insertCommand( string $onclick = "" ): void { global $DIC; + $blog_service = $DIC->blog()->internal(); $tpl = $this->ui->mainTemplate(); - $export_possible = $DIC->blog() - ->internal() - ->domain() + $export_possible = $blog_service->domain() ->export() ->isCommentsExportPossible($this->obj_id); if ($cmd === "export" From 391b0683627f0b4b2ce27d827aaeae2bbc0ad82f Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 19:17:27 +0200 Subject: [PATCH 023/108] blog: improve DIC handling --- .../ILIAS/Blog/Export/class.ilBlogExporter.php | 14 +++++++++----- .../ILIAS/Blog/Export/class.ilBlogImporter.php | 17 +++++++++-------- .../Blog/PermanentLink/StaticUrlHandler.php | 17 ++++++++++++----- .../Blog/classes/class.ilObjBlogListGUI.php | 11 +++++++++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Blog/Export/class.ilBlogExporter.php b/components/ILIAS/Blog/Export/class.ilBlogExporter.php index 71b49d3c5e4a..4132a223fb5f 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogExporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogExporter.php @@ -28,13 +28,10 @@ class ilBlogExporter extends ilXmlExporter protected ilBlogDataSet $ds; protected \ILIAS\Style\Content\DomainService $content_style_domain; - public function init(): void + public function __construct() { global $DIC; - - $this->ds = new ilBlogDataSet(); - $this->ds->setDSPrefix("ds"); - + parent::__construct(); $blog_service = $DIC->blog()->internal(); $this->content_style_domain = $DIC @@ -43,6 +40,13 @@ public function init(): void $this->posting_manager = $blog_service->domain()->posting(); } + + public function init(): void + { + $this->ds = new ilBlogDataSet(); + $this->ds->setDSPrefix("ds"); + } + public function getXmlExportTailDependencies( string $a_entity, string $a_target_release, diff --git a/components/ILIAS/Blog/Export/class.ilBlogImporter.php b/components/ILIAS/Blog/Export/class.ilBlogImporter.php index d5b1e2b0ed29..d0c21290b127 100755 --- a/components/ILIAS/Blog/Export/class.ilBlogImporter.php +++ b/components/ILIAS/Blog/Export/class.ilBlogImporter.php @@ -25,19 +25,23 @@ */ class ilBlogImporter extends ilXmlImporter { + protected \ILIAS\Blog\InternalService $blog_service; protected ilBlogDataSet $ds; protected \ILIAS\Style\Content\DomainService $content_style_domain; - public function init(): void + public function __construct() { global $DIC; - - $this->ds = new ilBlogDataSet(); - $this->ds->setDSPrefix("ds"); $this->content_style_domain = $DIC ->contentStyle() ->domain(); + $this->blog_service = $DIC->blog()->internal(); + } + public function init(): void + { + $this->ds = new ilBlogDataSet(); + $this->ds->setDSPrefix("ds"); $cop_config = $this->getImport()->getConfig("components/ILIAS/COPage"); $cop_config->setUpdateIfExists(true); } @@ -61,13 +65,10 @@ public function importXmlRepresentation( public function finalProcessing( ilImportMapping $a_mapping ): void { - global $DIC; - $blog_service = $DIC->blog()->internal(); - $blp_map = $a_mapping->getMappingsOfEntity("components/ILIAS/COPage", "pg"); foreach ($blp_map as $blp_id) { $blp_id = (int) substr($blp_id, 4); - $blog_id = $blog_service->domain()->posting()->lookupBlogId($blp_id); + $blog_id = $this->blog_service->domain()->posting()->lookupBlogId($blp_id); ilBlogPosting::_writeParentId("blp", $blp_id, (int) $blog_id); } diff --git a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php index 5589c26e47e2..2a0261e113f7 100644 --- a/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php +++ b/components/ILIAS/Blog/PermanentLink/StaticUrlHandler.php @@ -29,6 +29,15 @@ class StaticURLHandler extends BaseHandler implements Handler { + protected \ILIAS\Blog\InternalService $blog_service; + + public function __construct() + { + global $DIC; + $this->blog_service = $DIC->blog()->internal(); + parent::__construct(); + } + public function getNamespace(): string { return 'blog'; @@ -36,11 +45,9 @@ public function getNamespace(): string public function handle(Request $request, Context $context, Factory $response_factory): Response { - global $DIC; - $blog_service = $DIC->blog()->internal(); - - $ctrl = $DIC->ctrl(); - $access = $DIC->access(); + $blog_service = $this->blog_service; + $ctrl = $blog_service->gui()->ctrl(); + $access = $blog_service->domain()->access(); $uri = null; $blog_domain = $blog_service->domain(); diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php index ffd04737cbe0..d0437edddf09 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogListGUI.php @@ -27,6 +27,14 @@ class ilObjBlogListGUI extends ilObjectListGUI { private ?Modal $comment_modal = null; + protected \ILIAS\Blog\InternalService $blog_service; + + public function __construct(int $context = self::CONTEXT_REPOSITORY) + { + global $DIC; + parent::__construct($context); + $this->blog_service = $DIC->blog()->internal(); + } public function init(): void { @@ -69,8 +77,7 @@ public function insertCommand( string $cmd = "", string $onclick = "" ): void { - global $DIC; - $blog_service = $DIC->blog()->internal(); + $blog_service = $this->blog_service; $tpl = $this->ui->mainTemplate(); $export_possible = $blog_service->domain() From 42f193e1470a7ebf9ba4aaaaac46e7be576a556e Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sat, 27 Jun 2026 19:51:52 +0200 Subject: [PATCH 024/108] fixed copyright --- .../Blog/Posting/Service/class.GUIService.php | 16 +++++++++++++++- components/ILIAS/Blog/RSS/RSSGUI.php | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Blog/Posting/Service/class.GUIService.php b/components/ILIAS/Blog/Posting/Service/class.GUIService.php index 7fcd9217f95e..8e90ca73064a 100644 --- a/components/ILIAS/Blog/Posting/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Posting/Service/class.GUIService.php @@ -1,6 +1,20 @@ Date: Sat, 27 Jun 2026 21:54:29 +0200 Subject: [PATCH 025/108] blog: moved contributor code to subservice --- .../ILIAS/Blog/Contributor/ContributorGUI.php | 262 ++++++++++++++++++ .../Contributor/Service/class.GUIService.php | 11 + .../Blog/Service/class.InternalGUIService.php | 11 +- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 229 ++------------- 4 files changed, 297 insertions(+), 216 deletions(-) create mode 100644 components/ILIAS/Blog/Contributor/ContributorGUI.php diff --git a/components/ILIAS/Blog/Contributor/ContributorGUI.php b/components/ILIAS/Blog/Contributor/ContributorGUI.php new file mode 100644 index 000000000000..c2a277e0581e --- /dev/null +++ b/components/ILIAS/Blog/Contributor/ContributorGUI.php @@ -0,0 +1,262 @@ +gui->ctrl(); + $next_class = $ctrl->getNextClass($this); + $cmd = $ctrl->getCmd("contributors"); + + switch ($next_class) { + case strtolower(\ilRepositorySearchGUI::class): + $rep_search = new \ilRepositorySearchGUI(); + $rep_search->setTitle($this->domain->lng()->txt("blog_add_contributor")); + $rep_search->setCallback($this, 'addContributor', $this->blog->getAllLocalRoles($this->node_id)); + $ctrl->setReturn($this, 'contributors'); + $ctrl->forwardCommand($rep_search); + break; + + default: + if (method_exists($this, $cmd)) { + $this->$cmd(); + } + break; + } + } + + public function contributors(): void + { + $ilTabs = $this->gui->tabs(); + $ilToolbar = $this->gui->toolbar(); + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $tpl = $this->gui->ui()->mainTemplate(); + + $ilTabs->activateTab("contributors"); + + $local_roles = $this->blog->getAllLocalRoles($this->node_id); + + // add member + ilRepositorySearchGUI::fillAutoCompleteToolbar( + $this, + $ilToolbar, + array( + 'auto_complete_name' => $lng->txt('user'), + 'submit_name' => $lng->txt('add'), + 'add_search' => true, + 'add_from_container' => $this->node_id, + 'user_type' => $local_roles + ), + true + ); + + $other_roles = $this->blog->getRolesWithContributeOrRedact($this->node_id); + if ($other_roles) { + $tpl->setOnScreenMessage('info', sprintf($lng->txt("blog_contribute_other_roles"), implode(", ", $other_roles))); + } + + $table = $this->gui->contributor()->contributorTableBuilder( + $this->blog->getAllLocalRoles($this->node_id), + $this, + "contributors" + )->getTable(); + + if ($table->handleCommand()) { + return; + } + + $tpl->setContent($table->render()); + } + + /** + * Autocomplete submit + */ + public function addUserFromAutoComplete(): void + { + $lng = $this->domain->lng(); + $req = $this->gui->standardRequest(); + + $user_login = $req->getUserLogin(); + $user_type = $req->getUserType(); + + if (trim($user_login) === '') { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt('msg_no_search_string')); + $this->contributors(); + return; + } + $users = explode(',', $user_login); + + $user_ids = array(); + foreach ($users as $user) { + $user_id = ilObjUser::_lookupId($user); + + if (!$user_id) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt('user_not_known')); + $this->contributors(); + return; + } + + $user_ids[] = (int) $user_id; + } + + $this->addContributor($user_ids, $user_type); + } + + /** + * Centralized method to add contributors + */ + public function addContributor( + array $a_user_ids = array(), + ?string $a_user_type = null + ): void { + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $rbacreview = $this->domain->rbac()->review(); + $rbacadmin = $this->domain->rbac()->admin(); + $a_user_type = (int) $a_user_type; + + if (empty($a_user_ids)) { + $a_user_ids = $this->gui->standardRequest()->getIds(); + } + + if (!count($a_user_ids) || !$a_user_type) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("no_checkbox")); + $this->contributors(); + return; + } + + // get contributor role + $local_roles = array_keys($this->blog->getAllLocalRoles($this->node_id)); + if (!in_array($a_user_type, $local_roles)) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("missing_perm")); + $this->contributors(); + return; + } + + foreach ($a_user_ids as $user_id) { + $user_id = (int) $user_id; + $a_user_type = (int) $a_user_type; + if (!$rbacreview->isAssigned($user_id, $a_user_type)) { + $rbacadmin->assignUser($a_user_type, $user_id); + } + } + + $this->gui->ui()->mainTemplate()->setOnScreenMessage('success', $lng->txt("settings_saved"), true); + $ilCtrl->redirect($this, "contributors"); + } + + /** + * Used in ContributorTableBuilder + */ + public function confirmRemoveContributor(array $ids = []): void + { + if (empty($ids)) { + $ids = $this->gui->standardRequest()->getIds(); + } + if (count($ids) === 0) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $this->domain->lng()->txt("select_one"), true); + $this->gui->ctrl()->redirect($this, "contributors"); + } + + $confirm = new ilConfirmationGUI(); + $confirm->setHeaderText($this->domain->lng()->txt('blog_confirm_delete_contributors')); + $confirm->setFormAction($this->gui->ctrl()->getFormAction($this, 'removeContributor')); + $confirm->setConfirm($this->domain->lng()->txt('delete'), 'removeContributor'); + $confirm->setCancel($this->domain->lng()->txt('cancel'), 'contributors'); + + foreach ($ids as $user_id) { + $confirm->addItem( + 'id[]', + (string) $user_id, + \ilUserUtil::getNamePresentation($user_id, false, false, "", true) + ); + } + + $this->gui->ui()->mainTemplate()->setContent($confirm->getHTML()); + } + + public function removeContributor(): void + { + $ilCtrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $rbacadmin = $this->domain->rbac()->admin(); + + $ids = $this->gui->standardRequest()->getIds(); + + if (count($ids) === 0) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("select_one"), true); + $ilCtrl->redirect($this, "contributors"); + } + + // get contributor role + $local_roles = array_keys($this->blog->getAllLocalRoles($this->node_id)); + if (!$local_roles) { + $this->gui->ui()->mainTemplate()->setOnScreenMessage('failure', $lng->txt("missing_perm")); + $this->contributors(); + return; + } + + foreach ($ids as $user_id) { + foreach ($local_roles as $role_id) { + $rbacadmin->deassignUser($role_id, $user_id); + } + } + + $this->gui->ui()->mainTemplate()->setOnScreenMessage('success', $lng->txt("settings_saved"), true); + $this->gui->ctrl()->redirect($this, "contributors"); + } + + /** + * Used in ContributorTableBuilder + */ + public function addContributorContainerAction(array $ids = []): void + { + if (empty($ids)) { + $ids = $this->gui->standardRequest()->getIds(); + } + + // This would typically add contributors from a container + // For now, redirecting back to contributors as this seems to be a placeholder action + $this->gui->ctrl()->redirect($this, "contributors"); + } +} diff --git a/components/ILIAS/Blog/Contributor/Service/class.GUIService.php b/components/ILIAS/Blog/Contributor/Service/class.GUIService.php index ef8de2f00f64..8007e5d6d2d8 100755 --- a/components/ILIAS/Blog/Contributor/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Contributor/Service/class.GUIService.php @@ -57,4 +57,15 @@ public function contributorTableBuilder( $parent_cmd ); } + + public function contributorGUI(int $node_id, \ilObjBlog $blog): ContributorGUI + { + return new ContributorGUI( + $this->data_service, + $this->domain_service, + $this->gui, + $node_id, + $blog + ); + } } diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index fad63aa0f514..7fad2a4afa9c 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -67,11 +67,12 @@ public function standardRequest(): StandardGUIRequest public function contributor(): Contributor\GUIService { - return new Contributor\GUIService( - $this->data_service, - $this->domain_service, - $this - ); + return self::$instance["contributor"] ?? + self::$instance["contributor"] = new Contributor\GUIService( + $this->data_service, + $this->domain_service, + $this + ); } public function exercise(): Exercise\GUIService diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 49b0f4b51f14..6938914a27d5 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -30,15 +30,17 @@ use ILIAS\Blog\Settings\Settings; use ILIAS\Blog\ReadingTime\ReadingTimeManager; use ILIAS\Blog\Posting\PostingManager; +use ILIAS\Blog\Contributor\ContributorGUI; /** * @ilCtrl_Calls ilObjBlogGUI: ilBlogPostingGUI, ilWorkspaceAccessGUI * @ilCtrl_Calls ilObjBlogGUI: ilInfoScreenGUI, ilNoteGUI, ilCommonActionDispatcherGUI - * @ilCtrl_Calls ilObjBlogGUI: ilPermissionGUI, ilObjectCopyGUI, ilRepositorySearchGUI + * @ilCtrl_Calls ilObjBlogGUI: ilPermissionGUI, ilObjectCopyGUI * @ilCtrl_Calls ilObjBlogGUI: ilExportGUI, ilObjectContentStyleSettingsGUI, ilBlogExerciseGUI, ilObjNotificationSettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ilObjectMetaDataGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\SettingsGUI * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Settings\BlockSettingsGUI + * @ilCtrl_Calls ilObjBlogGUI: ILIAS\Blog\Contributor\ContributorGUI */ class ilObjBlogGUI extends ilObject2GUI implements ilDesktopItemHandling { @@ -295,7 +297,7 @@ protected function setTabs(): void $this->tabs_gui->addTab( "contributors", $lng->txt("blog_contributors"), - $this->ctrl->getLinkTarget($this, "contributors") + $this->ctrl->getLinkTargetByClass(ContributorGUI::class, "contributors") ); } @@ -508,14 +510,11 @@ public function executeCommand(): void $this->ctrl->forwardCommand($cp); break; - case 'ilrepositorysearchgui': + case strtolower(\ilRepositorySearchGUI::class): + $this->checkPermission("write"); $this->prepareOutput(); $ilTabs->activateTab("contributors"); - $rep_search = new ilRepositorySearchGUI(); - $rep_search->setTitle($this->lng->txt("blog_add_contributor")); - $rep_search->setCallback($this, 'addContributor', $this->object->getAllLocalRoles($this->node_id)); - $this->ctrl->setReturn($this, 'contributors'); - $this->ctrl->forwardCommand($rep_search); + $this->ctrl->forwardCommand($this->gui->contributor()->contributorGUI($this->node_id, $this->object)); break; case 'ilexportgui': @@ -581,6 +580,17 @@ public function executeCommand(): void $this->ctrl->forwardCommand($gui); break; + case strtolower(ContributorGUI::class): + $this->checkPermission("write"); + $this->prepareOutput(); + $ilTabs->activateTab("contributors"); + $gui = $this->gui->contributor()->contributorGUI( + $this->node_id, + $this->object + ); + $this->ctrl->forwardCommand($gui); + break; + case strtolower(\ILIAS\Blog\Settings\BlockSettingsGUI::class): $this->checkPermission("write"); $this->prepareOutput(); @@ -2127,209 +2137,6 @@ public function approve(): void } - // - // contributors - // - - public function contributors(): void - { - $ilTabs = $this->tabs; - $ilToolbar = $this->toolbar; - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $tpl = $this->tpl; - - if (!$this->checkPermissionBool("write")) { - return; - } - - $ilTabs->activateTab("contributors"); - - $local_roles = $this->object->getAllLocalRoles($this->node_id); - - // add member - ilRepositorySearchGUI::fillAutoCompleteToolbar( - $this, - $ilToolbar, - array( - 'auto_complete_name' => $lng->txt('user'), - 'submit_name' => $lng->txt('add'), - 'add_search' => true, - 'add_from_container' => $this->node_id, - 'user_type' => $local_roles - ), - true - ); - - $other_roles = $this->object->getRolesWithContributeOrRedact($this->node_id); - if ($other_roles) { - $this->tpl->setOnScreenMessage('info', sprintf($lng->txt("blog_contribute_other_roles"), implode(", ", $other_roles))); - } - - $table = $this->gui->contributor()->contributorTableBuilder( - $this->object->getAllLocalRoles($this->node_id), - $this, - "contributors" - )->getTable(); - - if ($table->handleCommand()) { - return; - } - - $tpl->setContent($table->render()); - } - - /** - * Autocomplete submit - */ - public function addUserFromAutoComplete(): void - { - $lng = $this->lng; - - $user_login = $this->blog_request->getUserLogin(); - $user_type = $this->blog_request->getUserType(); - - if (trim($user_login) === '') { - $this->tpl->setOnScreenMessage('failure', $lng->txt('msg_no_search_string')); - $this->contributors(); - return; - } - $users = explode(',', $user_login); - - $user_ids = array(); - foreach ($users as $user) { - $user_id = ilObjUser::_lookupId($user); - - if (!$user_id) { - $this->tpl->setOnScreenMessage('failure', $lng->txt('user_not_known')); - $this->contributors(); - return; - } - - $user_ids[] = (int) $user_id; - } - - $this->addContributor($user_ids, $user_type); - } - - /** - * Centralized method to add contributors - */ - public function addContributor( - array $a_user_ids = array(), - ?string $a_user_type = null - ): void { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $rbacreview = $this->rbac_review; - $rbacadmin = $this->rbacadmin; - $a_user_type = (int) $a_user_type; - - if (!$this->checkPermissionBool("write")) { - return; - } - - if (!count($a_user_ids) || !$a_user_type) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("no_checkbox")); - $this->contributors(); - return; - } - - // get contributor role - $local_roles = array_keys($this->object->getAllLocalRoles($this->node_id)); - if (!in_array($a_user_type, $local_roles)) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("missing_perm")); - $this->contributors(); - return; - } - - foreach ($a_user_ids as $user_id) { - $user_id = (int) $user_id; - $a_user_type = (int) $a_user_type; - if (!$rbacreview->isAssigned($user_id, $a_user_type)) { - $rbacadmin->assignUser($a_user_type, $user_id); - } - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("settings_saved"), true); - $ilCtrl->redirect($this, "contributors"); - } - - /** - * Used in ContributorTableBuilder - */ - public function confirmRemoveContributor(array $ids = []): void - { - if (empty($ids)) { - $ids = $this->blog_request->getIds(); - } - if (count($ids) === 0) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt("select_one"), true); - $this->ctrl->redirect($this, "contributors"); - } - - $confirm = new ilConfirmationGUI(); - $confirm->setHeaderText($this->lng->txt('blog_confirm_delete_contributors')); - $confirm->setFormAction($this->ctrl->getFormAction($this, 'removeContributor')); - $confirm->setConfirm($this->lng->txt('delete'), 'removeContributor'); - $confirm->setCancel($this->lng->txt('cancel'), 'contributors'); - - foreach ($ids as $user_id) { - $confirm->addItem( - 'id[]', - (string) $user_id, - $this->profile_gui->getNamePresentation($user_id, false, "", true) - ); - } - - $this->tpl->setContent($confirm->getHTML()); - } - - public function removeContributor(): void - { - $ilCtrl = $this->ctrl; - $lng = $this->lng; - $rbacadmin = $this->rbacadmin; - - $ids = $this->blog_request->getIds(); - - if (count($ids) === 0) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("select_one"), true); - $ilCtrl->redirect($this, "contributors"); - } - - // get contributor role - $local_roles = array_keys($this->object->getAllLocalRoles($this->node_id)); - if (!$local_roles) { - $this->tpl->setOnScreenMessage('failure', $lng->txt("missing_perm")); - $this->contributors(); - return; - } - - foreach ($ids as $user_id) { - foreach ($local_roles as $role_id) { - $rbacadmin->deassignUser($role_id, $user_id); - } - } - - $this->tpl->setOnScreenMessage('success', $lng->txt("settings_saved"), true); - $this->ctrl->redirect($this, "contributors"); - } - - /** - * Used in ContributorTableBuilder - */ - public function addContributorContainerAction(array $ids = []): void - { - if (empty($ids)) { - $ids = $this->blog_request->getIds(); - } - - // This would typically add contributors from a container - // For now, redirecting back to contributors as this seems to be a placeholder action - $this->ctrl->redirect($this, "contributors"); - } - public function deactivateAdmin(): void { if ($this->checkPermissionBool("write") && $this->apid > 0) { From dd7e52ff3981157dbd2747393a81bde2911a4bd3 Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 28 Jun 2026 09:58:31 +0200 Subject: [PATCH 026/108] blog: moved side block rendering to navigation subservice --- .../Navigation/Service/class.GUIService.php | 24 ++ .../Blog/Service/class.InternalGUIService.php | 9 +- .../ILIAS/Blog/classes/class.ilObjBlogGUI.php | 369 ++---------------- 3 files changed, 53 insertions(+), 349 deletions(-) diff --git a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php index e89279158f08..d420f56023c6 100755 --- a/components/ILIAS/Blog/Navigation/Service/class.GUIService.php +++ b/components/ILIAS/Blog/Navigation/Service/class.GUIService.php @@ -43,4 +43,28 @@ public function toolbarNavigationRenderer( $this->gui ); } + + public function monthBlock(): MonthBlockGUI + { + return new MonthBlockGUI( + $this->domain, + $this->gui + ); + } + + public function authorBlock(): AuthorBlockGUI + { + return new AuthorBlockGUI( + $this->domain, + $this->gui + ); + } + + public function keywordBlock(): KeywordBlockGUI + { + return new KeywordBlockGUI( + $this->domain, + $this->gui + ); + } } diff --git a/components/ILIAS/Blog/Service/class.InternalGUIService.php b/components/ILIAS/Blog/Service/class.InternalGUIService.php index 7fad2a4afa9c..ddc5461168db 100755 --- a/components/ILIAS/Blog/Service/class.InternalGUIService.php +++ b/components/ILIAS/Blog/Service/class.InternalGUIService.php @@ -43,10 +43,11 @@ public function __construct( public function navigation(): Navigation\GUIService { - return new Navigation\GUIService( - $this->domain_service, - $this - ); + return self::$instance["navigation"] ?? + self::$instance["navigation"] = new Navigation\GUIService( + $this->domain_service, + $this + ); } public function presentation(): Presentation\GUIService diff --git a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php index 6938914a27d5..11e4381f6043 100755 --- a/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php +++ b/components/ILIAS/Blog/classes/class.ilObjBlogGUI.php @@ -518,7 +518,10 @@ public function executeCommand(): void break; case 'ilexportgui': - $this->showExportGUI(); + $this->prepareOutput(); + $this->tabs->activateTab("export"); + $exp_gui = new ilExportGUI($this); + $this->ctrl->forwardCommand($exp_gui); break; case "ilobjectcontentstylesettingsgui": @@ -621,14 +624,6 @@ public function executeCommand(): void } } - protected function showExportGUI(): void - { - $this->prepareOutput(); - $this->tabs->activateTab("export"); - $exp_gui = new ilExportGUI($this); - $this->ctrl->forwardCommand($exp_gui); - } - protected function createExportFileWithComments(): void { $this->buildExportFile(true); @@ -1380,339 +1375,6 @@ protected function buildExportLink( } - /** - * Build navigation by date block - */ - protected function renderNavigationByDate( - array $a_items, - string $a_list_cmd = "render", - string $a_posting_cmd = "preview", - ?string $a_link_template = null, - bool $a_show_inactive = false, - int $a_blpg = 0 - ): string { - $ilCtrl = $this->ctrl; - - $blpg = ($a_blpg > 0) - ? $a_blpg - : $this->blpg; - - - // gather page active status - foreach ($a_items as $month => $postings) { - foreach (array_keys($postings) as $id) { - $active = ilBlogPosting::_lookupActive($id, "blp"); - if (!$a_show_inactive && !$active) { - unset($a_items[$month][$id]); - } - } - if (!count($a_items[$month])) { - unset($a_items[$month]); - } - } - - // list month (incl. postings) - if ($this->blog_settings->getNavMode() === ilObjBlog::NAV_MODE_LIST || $a_link_template) { - $max_months = $this->blog_settings->getNavModeListMonths(); - - $wtpl = new ilTemplate("tpl.blog_list_navigation_by_date.html", true, true, "components/ILIAS/Blog"); - - $ilCtrl->setParameter($this, "blpg", ""); - - $counter = $mon_counter = $last_year = 0; - foreach ($a_items as $month => $postings) { - if (!$a_link_template && $max_months && $mon_counter >= $max_months) { - break; - } - - $add_year = false; - $year = substr($month, 0, 4); - if (!$last_year || $year != $last_year) { - // #13562 - $add_year = true; - $last_year = $year; - } - - $mon_counter++; - - $month_name = ilCalendarUtil::_numericMonthToString((int) substr($month, 5)); - if (!$a_link_template) { - $ilCtrl->setParameter($this, "bmn", $month); - $month_url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - } else { - $month_url = $this->buildExportLink($a_link_template, "list", (string) $month); - } - - // list postings for month - //if($counter < $max_detail_postings) - if ($mon_counter <= $this->blog_settings->getNavModeListMonthsWithPostings()) { - if ($add_year) { - $wtpl->setCurrentBlock("navigation_year_details"); - $wtpl->setVariable("YEAR", $year); - $wtpl->parseCurrentBlock(); - } - - foreach ($postings as $id => $posting) { - //if($max_detail_postings && $counter >= $max_detail_postings) - //{ - // break; - //} - - $counter++; - - $caption = /* ilDatePresentation::formatDate($posting["created"], IL_CAL_DATETIME). - ", ".*/ $posting->getTitle(); - - if (!$a_link_template) { - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $id); - $url = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_posting_cmd); - } else { - $url = $this->buildExportLink($a_link_template, "posting", (string) $id); - } - - if (!$posting->isActive()) { - $wtpl->setVariable("NAV_ITEM_DRAFT", $this->lng->txt("blog_draft")); - } elseif ($this->blog_settings->getApproval() && !$posting->isApproved()) { - $wtpl->setVariable("NAV_ITEM_APPROVAL", $this->lng->txt("blog_needs_approval")); - } - - $wtpl->setCurrentBlock("navigation_item"); - $wtpl->setVariable("NAV_ITEM_URL", $url); - $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month_details"); - $wtpl->setVariable("NAV_MONTH", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - } - // summarized month - else { - if ($add_year) { - $wtpl->setCurrentBlock("navigation_year"); - $wtpl->setVariable("YEAR", $year); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month"); - $wtpl->setVariable("MONTH_NAME", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - $wtpl->setVariable("MONTH_COUNT", count($postings)); - } - $wtpl->parseCurrentBlock(); - } - if (!$a_link_template) { - $this->ctrl->setParameterByClass(self::class, "bmn", null); - $url = $this->ctrl->getLinkTargetByClass(self::class, $a_list_cmd); - } else { - $url = "index.html"; - } - - $wtpl->setVariable( - "STARTING_PAGE", - $this->ui->renderer()->render( - $this->ui->factory()->link()->standard( - $this->lng->txt("blog_starting_page"), - $url - ) - ) - ); - } - // single month - else { - $wtpl = new ilTemplate("tpl.blog_list_navigation_month.html", true, true, "components/ILIAS/Blog"); - - $ilCtrl->setParameter($this, "blpg", ""); - - $month_options = array(); - foreach ($a_items as $month => $postings) { - $month_name = $this->gui->presentation()->util()->getMonthPresentation($month); - - $month_options[$month] = $month_name; - - if ($month == $this->month) { - if (!$a_link_template) { - $ilCtrl->setParameter($this, "bmn", $month); - $month_url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - } else { - $month_url = $this->buildExportLink($a_link_template, "list", (string) $month); - } - - foreach ($postings as $id => $posting) { - $caption = /* ilDatePresentation::formatDate($posting["created"], IL_CAL_DATETIME). - ", ".*/ $posting["title"]; - - if (!$a_link_template) { - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", $month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "blpg", $id); - $url = $ilCtrl->getLinkTargetByClass("ilblogpostinggui", $a_posting_cmd); - } else { - $url = $this->buildExportLink($a_link_template, "posting", (string) $id); - } - - if (!$posting->isActive()) { - $wtpl->setVariable("NAV_ITEM_DRAFT", $this->lng->txt("blog_draft")); - } elseif ($this->blog_settings->getApproval() && !$posting["approved"]) { - $wtpl->setVariable("NAV_ITEM_APPROVAL", $this->lng->txt("blog_needs_approval")); - } - - $wtpl->setCurrentBlock("navigation_item"); - $wtpl->setVariable("NAV_ITEM_URL", $url); - $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); - $wtpl->parseCurrentBlock(); - } - - $wtpl->setCurrentBlock("navigation_month_details"); - if ($blpg > 0) { - $wtpl->setVariable("NAV_MONTH", $month_name); - $wtpl->setVariable("URL_MONTH", $month_url); - } - $wtpl->parseCurrentBlock(); - } - } - - if ($blpg === 0) { - $wtpl->setCurrentBlock("option_bl"); - foreach ($month_options as $value => $caption) { - $wtpl->setVariable("OPTION_VALUE", $value); - $wtpl->setVariable("OPTION_CAPTION", $caption); - if ($value == $this->month) { - $wtpl->setVariable("OPTION_SEL", ' selected="selected"'); - } - $wtpl->parseCurrentBlock(); - } - - $wtpl->setVariable("FORM_ACTION", $ilCtrl->getFormAction($this, $a_list_cmd)); - } - } - $ilCtrl->setParameter($this, "bmn", $this->month); - $ilCtrl->setParameterByClass("ilblogpostinggui", "bmn", ""); - return $wtpl->get(); - } - - /** - * Build navigation by keywords block - */ - protected function renderNavigationByKeywords( - string $a_list_cmd = "render", - bool $a_show_inactive = false, - string $a_link_template = "", - int $a_blpg = 0 - ): string { - $ilCtrl = $this->ctrl; - - $blpg = ($a_blpg > 0) - ? $a_blpg - : $this->blpg; - - $keywords = $this->getKeywords($a_show_inactive, $blpg); - if ($keywords) { - $wtpl = new ilTemplate("tpl.blog_list_navigation_keywords.html", true, true, "components/ILIAS/Blog"); - - $max = max($keywords); - - $wtpl->setCurrentBlock("keyword"); - foreach ($keywords as $keyword => $counter) { - if (!$a_link_template) { - $ilCtrl->setParameter($this, "kwd", urlencode((string) $keyword)); // #15885 - $url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - $ilCtrl->setParameter($this, "kwd", ""); - } else { - $url = $this->buildExportLink($a_link_template, "keyword", (string) $keyword); - } - - $wtpl->setVariable("TXT_KEYWORD", $keyword); - $wtpl->setVariable("CLASS_KEYWORD", ilTagging::getRelevanceClass($counter, $max)); - $wtpl->setVariable("URL_KEYWORD", $url); - $wtpl->parseCurrentBlock(); - } - - return $wtpl->get(); - } - return ""; - } - - protected function renderNavigationByAuthors( - array $a_items, - string $a_list_cmd = "render", - bool $a_show_inactive = false - ): string { - $ilCtrl = $this->ctrl; - - $authors = array(); - foreach ($a_items as $month => $items) { - foreach ($items as $item) { - /** @var \ILIAS\Blog\Posting\Posting $item */ - $item_id = $item->getId(); - if (($a_show_inactive || ilBlogPosting::_lookupActive($item_id, "blp"))) { - $author_id = $item->getAuthor(); - if ($author_id) { - $authors[] = $author_id; - } - foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { - $editor_id = (int) $editor["user_id"]; - if ($editor_id !== $author_id) { - $authors[] = $editor_id; - } - } - } - } - } - - $authors = array_unique($authors); - - // filter out deleted users - $authors = array_filter($authors, function ($id) { - return ilObject::_lookupType($id) == "usr"; - }); - - if (count($authors) > 1) { - $list = array(); - foreach ($authors as $user_id) { - if ($user_id) { - $ilCtrl->setParameter($this, "ath", $user_id); - $url = $ilCtrl->getLinkTarget($this, $a_list_cmd); - $ilCtrl->setParameter($this, "ath", ""); - - $base_name = ilUserUtil::getNamePresentation($user_id); - if (str_starts_with($base_name, "[")) { - $name = ilUserUtil::getNamePresentation($user_id, true); - $sort = $name; - } else { - $name = ilUserUtil::getNamePresentation( - $user_id, - true, - false, - "", - false, - true, - false - ); - $name_arr = ilObjUser::_lookupName($user_id); - $sort = $name_arr["lastname"] . " " . $name_arr["firstname"]; - } - - $idx = trim(strip_tags($sort)) . "///" . $user_id; // #10934 - $list[$idx] = array($name, $url); - } - } - ksort($list); - - $wtpl = new ilTemplate("tpl.blog_list_navigation_authors.html", true, true, "components/ILIAS/Blog"); - - $wtpl->setCurrentBlock("author"); - foreach ($list as $author) { - $wtpl->setVariable("TXT_AUTHOR", $author[0]); - $wtpl->setVariable("URL_AUTHOR", $author[1]); - $wtpl->parseCurrentBlock(); - } - - return $wtpl->get(); - } - return ""; - } - /** * Toolbar navigation */ @@ -1765,7 +1427,14 @@ public function renderNavigation( if (count($a_items)) { $blocks[$order["navigation"] ?? 0] = array( $this->lng->txt("blog_navigation"), - $this->renderNavigationByDate($a_items, $a_list_cmd, $a_posting_cmd, $a_link_template, $a_show_inactive, $a_blpg) + $this->gui->navigation()->monthBlock()->render( + $a_items, + $a_list_cmd, + $a_posting_cmd, + $a_link_template, + $a_show_inactive, + $a_blpg + ) ); } @@ -1776,7 +1445,13 @@ public function renderNavigation( $a_list_cmd !== "preview" && $a_list_cmd !== "gethtml" && !$a_link_template); - $keywords = $this->renderNavigationByKeywords($a_list_cmd, $a_show_inactive, (string) $a_link_template, $a_blpg); + $keywords = $this->gui->navigation()->keywordBlock()->render( + $a_items, + $a_list_cmd, + $a_show_inactive, + (string) $a_link_template, + $a_blpg + ); if ($keywords || $may_edit_keywords) { if (!$keywords) { $keywords = $this->lng->txt("blog_no_keywords"); @@ -1797,7 +1472,11 @@ public function renderNavigation( // authors if ($this->id_type === self::REPOSITORY_NODE_ID && $this->blog_settings->getAuthors()) { - $authors = $this->renderNavigationByAuthors($a_items, $a_list_cmd, $a_show_inactive); + $authors = $this->gui->navigation()->authorBlock()->render( + $a_items, + $a_list_cmd, + $a_show_inactive + ); if ($authors) { $blocks[$order["authors"] ?? 1] = array($this->lng->txt("blog_authors"), $authors); } From 9856b104c4254f624ee41c08182c2262cea7f50d Mon Sep 17 00:00:00 2001 From: Alexander Killing Date: Sun, 28 Jun 2026 09:59:09 +0200 Subject: [PATCH 027/108] blog: moved side block rendering to navigation subservice --- .../ILIAS/Blog/Navigation/AuthorBlockGUI.php | 122 +++++++++ .../ILIAS/Blog/Navigation/KeywordBlockGUI.php | 138 ++++++++++ .../ILIAS/Blog/Navigation/MonthBlockGUI.php | 253 ++++++++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 components/ILIAS/Blog/Navigation/AuthorBlockGUI.php create mode 100644 components/ILIAS/Blog/Navigation/KeywordBlockGUI.php create mode 100644 components/ILIAS/Blog/Navigation/MonthBlockGUI.php diff --git a/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php new file mode 100644 index 000000000000..6a88ce305357 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/AuthorBlockGUI.php @@ -0,0 +1,122 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + bool $show_inactive = false + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + + $authors = array(); + foreach ($items as $month => $month_items) { + foreach ($month_items as $item) { + $item_id = $item->getId(); + if (($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp"))) { + $author_id = $item->getAuthor(); + if ($author_id) { + $authors[] = $author_id; + } + foreach (\ilPageObject::getPageContributors("blp", $item_id) as $editor) { + $editor_id = (int) $editor["user_id"]; + if ($editor_id !== $author_id) { + $authors[] = $editor_id; + } + } + } + } + } + + $authors = array_unique($authors); + + // filter out deleted users + $authors = array_filter($authors, function ($id) { + return \ilObject::_lookupType($id) == "usr"; + }); + + if (count($authors) > 1) { + $list = array(); + foreach ($authors as $user_id) { + if ($user_id) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", (string) $user_id); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "ath", ""); + + $base_name = \ilUserUtil::getNamePresentation($user_id); + if (str_starts_with($base_name, "[")) { + $name = \ilUserUtil::getNamePresentation($user_id, true); + $sort = $name; + } else { + $name = \ilUserUtil::getNamePresentation( + $user_id, + true, + false, + "", + false, + true, + false + ); + $name_arr = \ilObjUser::_lookupName($user_id); + $sort = $name_arr["lastname"] . " " . $name_arr["firstname"]; + } + + $idx = trim(strip_tags((string) $sort)) . "///" . $user_id; + $list[$idx] = array($name, $url); + } + } + ksort($list); + + $wtpl = new \ilTemplate("tpl.blog_list_navigation_authors.html", true, true, "components/ILIAS/Blog"); + + $wtpl->setCurrentBlock("author"); + foreach ($list as $author) { + $wtpl->setVariable("TXT_AUTHOR", $author[0]); + $wtpl->setVariable("URL_AUTHOR", $author[1]); + $wtpl->parseCurrentBlock(); + } + + return $wtpl->get(); + } + return ""; + } +} diff --git a/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php new file mode 100644 index 000000000000..7a177ae023da --- /dev/null +++ b/components/ILIAS/Blog/Navigation/KeywordBlockGUI.php @@ -0,0 +1,138 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + bool $show_inactive = false, + string $link_template = "", + int $blpg = 0 + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + + $keywords = $this->getKeywords($items, $show_inactive, $blpg); + if ($keywords) { + $wtpl = new \ilTemplate("tpl.blog_list_navigation_keywords.html", true, true, "components/ILIAS/Blog"); + + $max = max($keywords); + + $wtpl->setCurrentBlock("keyword"); + foreach ($keywords as $keyword => $counter) { + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", urlencode((string) $keyword)); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "kwd", ""); + } else { + $url = $this->buildExportLink($link_template, "keyword", (string) $keyword); + } + + $wtpl->setVariable("TXT_KEYWORD", (string) $keyword); + $wtpl->setVariable("CLASS_KEYWORD", \ilTagging::getRelevanceClass((int) $counter, (int) $max)); + $wtpl->setVariable("URL_KEYWORD", $url); + $wtpl->parseCurrentBlock(); + } + + return $wtpl->get(); + } + return ""; + } + + /** + * @param Posting[][] $items + */ + protected function getKeywords( + array $items, + bool $show_inactive, + ?int $posting_id = null + ): array { + $keywords = array(); + $posting_manager = $this->domain->posting(); + $obj_id = \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()); + + if ($posting_id) { + foreach ($posting_manager->getKeywords($obj_id, $posting_id) as $keyword) { + if (isset($keywords[$keyword])) { + $keywords[$keyword]++; + } else { + $keywords[$keyword] = 1; + } + } + } else { + foreach ($items as $month => $month_items) { + foreach ($month_items as $item) { + $item_id = $item->getId(); + if ($show_inactive || \ilBlogPosting::_lookupActive($item_id, "blp")) { + foreach ($posting_manager->getKeywords($obj_id, $item_id) as $keyword) { + if (isset($keywords[$keyword])) { + $keywords[$keyword]++; + } else { + $keywords[$keyword] = 1; + } + } + } + } + } + } + + $tmp = array(); + foreach ($keywords as $keyword => $counter) { + $tmp[] = array("keyword" => $keyword, "counter" => $counter); + } + $tmp = \ilArrayUtil::sortArray($tmp, "keyword", "ASC"); + + $keywords = array(); + foreach ($tmp as $item) { + $keywords[(string) $item["keyword"]] = $item["counter"]; + } + return $keywords; + } + + protected function buildExportLink( + string $template, + string $type, + string $id + ): string { + $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); + return $blog_export->buildExportLink($template, $type, $id, []); + } +} diff --git a/components/ILIAS/Blog/Navigation/MonthBlockGUI.php b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php new file mode 100644 index 000000000000..1c948d4031b7 --- /dev/null +++ b/components/ILIAS/Blog/Navigation/MonthBlockGUI.php @@ -0,0 +1,253 @@ +domain = $domain; + $this->gui = $gui; + } + + /** + * @param Posting[][] $items + */ + public function render( + array $items, + string $list_cmd = "render", + string $posting_cmd = "preview", + ?string $link_template = null, + bool $show_inactive = false, + int $blpg = 0 + ): string { + $ctrl = $this->gui->ctrl(); + $lng = $this->domain->lng(); + $settings = $this->domain->blogSettings()->getByObjId( + \ilObject::_lookupObjId($this->gui->standardRequest()->getRefId()) + ); + + // gather page active status + foreach ($items as $month => $postings) { + foreach (array_keys($postings) as $id) { + $active = \ilBlogPosting::_lookupActive($id, "blp"); + if (!$show_inactive && !$active) { + unset($items[$month][$id]); + } + } + if (!count($items[$month])) { + unset($items[$month]); + } + } + + // list month (incl. postings) + if ($settings->getNavMode() === \ilObjBlog::NAV_MODE_LIST || $link_template) { + $max_months = $settings->getNavModeListMonths(); + + $wtpl = new \ilTemplate("tpl.blog_list_navigation_by_date.html", true, true, "components/ILIAS/Blog"); + + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", ""); + + $counter = $mon_counter = $last_year = 0; + foreach ($items as $month => $postings) { + if (!$link_template && $max_months && $mon_counter >= $max_months) { + break; + } + + $add_year = false; + $year = substr((string) $month, 0, 4); + if (!$last_year || $year != $last_year) { + $add_year = true; + $last_year = $year; + } + + $mon_counter++; + + $month_name = \ilCalendarUtil::_numericMonthToString((int) substr((string) $month, 5)); + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $month); + $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $month_url = $this->buildExportLink($link_template, "list", (string) $month); + } + + if ($mon_counter <= $settings->getNavModeListMonthsWithPostings()) { + if ($add_year) { + $wtpl->setCurrentBlock("navigation_year_details"); + $wtpl->setVariable("YEAR", $year); + $wtpl->parseCurrentBlock(); + } + + foreach ($postings as $id => $posting) { + $counter++; + $caption = $posting->getTitle(); + + if (!$link_template) { + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", $month); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); + $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); + } else { + $url = $this->buildExportLink($link_template, "posting", (string) $id); + } + + if (!$posting->isActive()) { + $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); + } elseif ($settings->getApproval() && !$posting->isApproved()) { + $wtpl->setVariable("NAV_ITEM_APPROVAL", $lng->txt("blog_needs_approval")); + } + + $wtpl->setCurrentBlock("navigation_item"); + $wtpl->setVariable("NAV_ITEM_URL", $url); + $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month_details"); + $wtpl->setVariable("NAV_MONTH", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + $wtpl->parseCurrentBlock(); + } + // summarized month + else { + if ($add_year) { + $wtpl->setCurrentBlock("navigation_year"); + $wtpl->setVariable("YEAR", $year); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month"); + $wtpl->setVariable("MONTH_NAME", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + $wtpl->setVariable("MONTH_COUNT", (string) count($postings)); + $wtpl->parseCurrentBlock(); + } + } + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", null); + $url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $url = "index.html"; + } + + $wtpl->setVariable( + "STARTING_PAGE", + $this->gui->ui()->renderer()->render( + $this->gui->ui()->factory()->link()->standard( + $lng->txt("blog_starting_page"), + $url + ) + ) + ); + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->gui->standardRequest()->getMonth()); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", ""); + return $wtpl->get(); + } + // single month + else { + $wtpl = new \ilTemplate("tpl.blog_list_navigation_month.html", true, true, "components/ILIAS/Blog"); + + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "blpg", ""); + + $month_options = array(); + foreach ($items as $month => $postings) { + $month_name = $this->gui->presentation()->util()->getMonthPresentation((string) $month); + + $month_options[(string) $month] = $month_name; + + if ($month == $this->gui->standardRequest()->getMonth()) { + if (!$link_template) { + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", (string) $month); + $month_url = $ctrl->getLinkTargetByClass(\ilObjBlogGUI::class, $list_cmd); + } else { + $month_url = $this->buildExportLink($link_template, "list", (string) $month); + } + + foreach ($postings as $id => $posting) { + $caption = $posting->getTitle(); + + if (!$link_template) { + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", (string) $month); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "blpg", (string) $id); + $url = $ctrl->getLinkTargetByClass(\ilBlogPostingGUI::class, $posting_cmd); + } else { + $url = $this->buildExportLink($link_template, "posting", (string) $id); + } + + if (!$posting->isActive()) { + $wtpl->setVariable("NAV_ITEM_DRAFT", $lng->txt("blog_draft")); + } elseif ($settings->getApproval() && !$posting->isApproved()) { + $wtpl->setVariable("NAV_ITEM_APPROVAL", $lng->txt("blog_needs_approval")); + } + + $wtpl->setCurrentBlock("navigation_item"); + $wtpl->setVariable("NAV_ITEM_URL", $url); + $wtpl->setVariable("NAV_ITEM_CAPTION", $caption); + $wtpl->parseCurrentBlock(); + } + + $wtpl->setCurrentBlock("navigation_month_details"); + if ($blpg > 0) { + $wtpl->setVariable("NAV_MONTH", $month_name); + $wtpl->setVariable("URL_MONTH", $month_url); + } + $wtpl->parseCurrentBlock(); + } + } + + if ($blpg === 0) { + $wtpl->setCurrentBlock("option_bl"); + foreach ($month_options as $value => $caption) { + $wtpl->setVariable("OPTION_VALUE", $value); + $wtpl->setVariable("OPTION_CAPTION", $caption); + if ($value == $this->gui->standardRequest()->getMonth()) { + $wtpl->setVariable("OPTION_SEL", ' selected="selected"'); + } + $wtpl->parseCurrentBlock(); + } + + $wtpl->setVariable("FORM_ACTION", $ctrl->getFormActionByClass(\ilObjBlogGUI::class, $list_cmd)); + } + } + $ctrl->setParameterByClass(\ilObjBlogGUI::class, "bmn", $this->gui->standardRequest()->getMonth()); + $ctrl->setParameterByClass(\ilBlogPostingGUI::class, "bmn", ""); + return $wtpl->get(); + } + + protected function buildExportLink( + string $template, + string $type, + string $id + ): string { + $blog_export = new \ILIAS\Blog\Export\BlogHtmlExport($this->gui->standardRequest()->getRefId()); + // Note: this might need adjustment since the original used $this->getKeywords(false) + // For now we assume keywords are not needed for these links or handled elsewhere + return $blog_export->buildExportLink($template, $type, $id, []); + } +} From bbb17a76f1657e1e5a02a8fe632c331b4f6b2df4 Mon Sep 17 00:00:00 2001 From: Ahmed Hamouda Date: Fri, 26 Jun 2026 10:47:02 +0200 Subject: [PATCH 028/108] add Http StatusCode::HTTP_UNPROCESSABLE_ENTITY --- components/ILIAS/HTTP/src/StatusCode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/components/ILIAS/HTTP/src/StatusCode.php b/components/ILIAS/HTTP/src/StatusCode.php index cdac27a2a57f..0d46f3a347a5 100755 --- a/components/ILIAS/HTTP/src/StatusCode.php +++ b/components/ILIAS/HTTP/src/StatusCode.php @@ -71,6 +71,7 @@ interface StatusCode public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; public const HTTP_EXPECTATION_FAILED = 417; + public const HTTP_UNPROCESSABLE_ENTITY = 422; public const HTTP_TOO_MANY_REQUESTS = 429; // [Server Error 5xx] From 9721802d190100be95ed26e5dbd7caff3dce7499 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 4 Jun 2026 11:26:12 +0200 Subject: [PATCH 029/108] Questions: Build General Structure --- .../classes/AdministrationMainBarProvider.php | 2 +- .../COPage/classes/class.ilPageObject.php | 2 +- components/ILIAS/COPage/xsl/page.xsl | 4 + components/ILIAS/Export/xml/ilias_pg_12.dtd | 566 ++++++++++++++++++ .../Administration/class.ilObjQuestions.php | 28 + .../class.ilObjQuestionsAccess.php | 27 + .../class.ilObjQuestionsGUI.php | 138 +++++ .../ILIAS/Questions/Legacy/LocalDIC.php | 152 +++++ .../Legacy/PageEditor/QstsQuestionPage.php | 27 + .../PageEditor/QstsQuestionPageConfig.php | 30 + .../PageEditor/class.QstsQuestionPageGUI.php | 42 ++ .../PageEditor/class.ilPCAnswerForm.php | 460 ++++++++++++++ .../PageEditor/class.ilPCAnswerFormGUI.php | 96 +++ components/ILIAS/Questions/Questions.php | 46 ++ components/ILIAS/Questions/README.md | 1 + components/ILIAS/Questions/maintenance.json | 14 + components/ILIAS/Questions/service.xml | 20 + .../AnswerForm/Capabilities/Capability.php | 26 + .../src/AnswerForm/Capabilities/Feedback.php | 29 + .../src/AnswerForm/Capabilities/Marking.php | 29 + .../src/AnswerForm/Capabilities/Skills.php | 28 + .../Questions/src/AnswerForm/Definition.php | 40 ++ .../Questions/src/AnswerForm/Factory.php | 86 +++ .../Questions/src/AnswerForm/Persistence.php | 36 ++ .../Questions/src/AnswerForm/Properties.php | 26 + .../src/AnswerForm/TypeGenericProperties.php | 72 +++ .../Questions/src/AnswerForm/Views/Edit.php | 46 ++ .../src/AnswerForm/Views/Participant.php | 31 + .../Cloze/Capabilities/Feedback.php | 42 ++ .../Cloze/Capabilities/Marking.php | 43 ++ .../Cloze/Capabilities/Skills.php | 37 ++ .../src/AnswerFormTypes/Cloze/Definition.php | 87 +++ .../src/AnswerFormTypes/Cloze/Persistence.php | 141 +++++ .../Cloze/Properties/AnswerForm/Factory.php | 76 +++ .../Properties/AnswerForm/Properties.php | 224 +++++++ .../Cloze/Properties/ClozeText/Factory.php | 57 ++ .../Cloze/Properties/ClozeText/Text.php | 157 +++++ .../Definitions/ScoringIdentical.php | 61 ++ .../Cloze/Properties/Gaps/Factory.php | 90 +++ .../Cloze/Properties/Gaps/Gap.php | 196 ++++++ .../Cloze/Properties/Gaps/Gaps.php | 238 ++++++++ .../Cloze/Properties/Gaps/LongMenu.php | 158 +++++ .../Cloze/Properties/Gaps/Numeric.php | 129 ++++ .../Gaps/Properties/AnswerOption.php | 137 +++++ .../Gaps/Properties/AnswerOptions.php | 223 +++++++ .../Properties/Gaps/Properties/Factory.php | 86 +++ .../Properties/Gaps/Properties/Properties.php | 267 +++++++++ .../Cloze/Properties/Gaps/Select.php | 108 ++++ .../Cloze/Properties/Gaps/Text.php | 122 ++++ .../Cloze/Properties/Gaps/Type.php | 56 ++ .../Gaps/class.UploadAnswerOptionsGUI.php | 97 +++ .../src/AnswerFormTypes/Cloze/Views/Edit.php | 280 +++++++++ .../Cloze/Views/Participant.php | 39 ++ components/ILIAS/Questions/src/Collector.php | 57 ++ .../Presentation/Definitions/CarryWrapper.php | 65 ++ .../src/Presentation/Definitions/EditForm.php | 555 +++++++++++++++++ .../Definitions/EditFormFactory.php | 555 +++++++++++++++++ .../Presentation/Definitions/EditOverview.php | 555 +++++++++++++++++ .../Presentation/Definitions/Editability.php | 28 + .../src/Presentation/Definitions/Leaf.php | 65 ++ .../Definitions/QuestionsTable.php | 123 ++++ .../Presentation/Layout/LayoutProvider.php | 154 +++++ .../Questions/src/Presentation/Views/Edit.php | 555 +++++++++++++++++ .../ILIAS/Questions/src/PublicInterface.php | 43 ++ .../src/Question/Definitions/Lifecycle.php | 31 + .../Definitions/TextMatchingOptions.php | 54 ++ .../src/Question/Persistence/Column.php | 40 ++ .../src/Question/Persistence/Join.php | 51 ++ .../src/Question/Persistence/JoinType.php | 27 + .../src/Question/Persistence/Junctor.php | 27 + .../Question/Persistence/ManipulateQuery.php | 66 ++ .../src/Question/Persistence/Operator.php | 50 ++ .../src/Question/Persistence/Order.php | 35 ++ .../Question/Persistence/OrderDirection.php | 27 + .../src/Question/Persistence/Repository.php | 219 +++++++ .../src/Question/Persistence/Select.php | 38 ++ .../src/Question/Persistence/SelectQuery.php | 165 +++++ .../Question/Persistence/TableNameBuilder.php | 57 ++ .../Question/Persistence/TableNameSpace.php | 44 ++ .../Persistence/TableNameSpaceCore.php | 34 ++ .../src/Question/Persistence/Value.php | 49 ++ .../src/Question/Persistence/Where.php | 52 ++ .../ILIAS/Questions/src/Question/Question.php | 26 + .../src/Question/QuestionImplementation.php | 255 ++++++++ .../ILIAS/Questions/src/Question/Response.php | 25 + .../Questions/src/Question/Views/Edit.php | 180 ++++++ .../src/Question/Views/Participant.php | 81 +++ .../Questions/src/Response/Repository.php | 28 + .../ILIAS/Questions/src/Response/Response.php | 30 + .../ILIAS/Questions/src/Setup/Agent.php | 74 +++ .../src/Setup/ClozeQuestionTables.php | 249 ++++++++ .../src/Setup/OverarchingQuestionTables.php | 206 +++++++ lang/ilias_de.lang | 27 +- lang/ilias_en.lang | 27 +- 94 files changed, 10266 insertions(+), 18 deletions(-) create mode 100755 components/ILIAS/Export/xml/ilias_pg_12.dtd create mode 100755 components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php create mode 100755 components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php create mode 100755 components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php create mode 100755 components/ILIAS/Questions/Legacy/LocalDIC.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php create mode 100644 components/ILIAS/Questions/Questions.php create mode 100644 components/ILIAS/Questions/README.md create mode 100644 components/ILIAS/Questions/maintenance.json create mode 100644 components/ILIAS/Questions/service.xml create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Definition.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Persistence.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Properties.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Views/Edit.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Views/Participant.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php create mode 100755 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php create mode 100644 components/ILIAS/Questions/src/Collector.php create mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/Editability.php create mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php create mode 100755 components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php create mode 100644 components/ILIAS/Questions/src/Presentation/Views/Edit.php create mode 100644 components/ILIAS/Questions/src/PublicInterface.php create mode 100644 components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php create mode 100644 components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Column.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Join.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/JoinType.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Junctor.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/ManipulateQuery.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Operator.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Order.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/OrderDirection.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Repository.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Select.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Value.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Where.php create mode 100644 components/ILIAS/Questions/src/Question/Question.php create mode 100644 components/ILIAS/Questions/src/Question/QuestionImplementation.php create mode 100644 components/ILIAS/Questions/src/Question/Response.php create mode 100644 components/ILIAS/Questions/src/Question/Views/Edit.php create mode 100644 components/ILIAS/Questions/src/Question/Views/Participant.php create mode 100644 components/ILIAS/Questions/src/Response/Repository.php create mode 100644 components/ILIAS/Questions/src/Response/Response.php create mode 100644 components/ILIAS/Questions/src/Setup/Agent.php create mode 100644 components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php create mode 100644 components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php diff --git a/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php b/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php index 3f9baaa54ae2..660af1b3f27f 100755 --- a/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php +++ b/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php @@ -225,7 +225,7 @@ private function getGroups(): array // admin menu layout $layout = array( "maintenance" => - array("adma", "serv", "cron", "bnmk", "lngf", "hlps", "wfe", 'fils', 'logs', 'sysc', "recf", "root"), + array("adma", "serv", "cron", "bnmk", "lngf", "hlps", "wfe", 'fils', 'qsts', 'logs', 'sysc', "recf", "root"), "layout_and_navigation" => array("mme", "gsfo", "dshs", "stys", "adve", "stus"), "repository_and_objects" => diff --git a/components/ILIAS/COPage/classes/class.ilPageObject.php b/components/ILIAS/COPage/classes/class.ilPageObject.php index 9bdd0fb89a41..92a0d77988ce 100755 --- a/components/ILIAS/COPage/classes/class.ilPageObject.php +++ b/components/ILIAS/COPage/classes/class.ilPageObject.php @@ -75,7 +75,7 @@ abstract class ilPageObject public string $xml = ""; public string $encoding = ""; public DomNode $node; - public string $cur_dtd = "ilias_pg_9.dtd"; + public string $cur_dtd = "ilias_pg_12.dtd"; public bool $contains_int_link = false; public bool $needs_parsing = false; public string $parent_type = ""; diff --git a/components/ILIAS/COPage/xsl/page.xsl b/components/ILIAS/COPage/xsl/page.xsl index 5d368b5851b4..bcdd7a8af4a1 100755 --- a/components/ILIAS/COPage/xsl/page.xsl +++ b/components/ILIAS/COPage/xsl/page.xsl @@ -3721,6 +3721,10 @@

+ + [[[ANSWER_FORM_]]] + + diff --git a/components/ILIAS/Export/xml/ilias_pg_12.dtd b/components/ILIAS/Export/xml/ilias_pg_12.dtd new file mode 100755 index 000000000000..0071822931b3 --- /dev/null +++ b/components/ILIAS/Export/xml/ilias_pg_12.dtd @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php new file mode 100755 index 000000000000..cb2e27c8333f --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php @@ -0,0 +1,28 @@ +type = 'qsts'; + parent::__construct($id, $referenced); + } +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php new file mode 100755 index 000000000000..4ea07f76b5d0 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php @@ -0,0 +1,27 @@ + + * @ingroup components\ILIASTest + */ +class ilObjQuestionsAccess extends ilObjectAccess +{ +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php new file mode 100755 index 000000000000..1eeea1615f0b --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -0,0 +1,138 @@ +data_factory = new DataFactory(); + + $this->edit_view = LocalDIC::dic()[Edit::class]; + + $this->type = 'qsts'; + + parent::__construct($a_data, $a_id, $a_call_by_reference, false); + + $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); + + if (!$this->access->checkAccess('read', '', $this->object->getRefId())) { + $this->ilias->raiseError($this->lng->txt("msg_no_perm_read_assf"), $this->ilias->error_obj->WARNING); + } + } + + public function executeCommand(): void + { + $next_class = $this->ctrl->getNextClass($this); + $cmd = $this->ctrl->getCmd(); + $this->prepareOutput(); + + switch ($next_class) { + case strtolower(UploadAnswerOptionsGUI::class): + $this->ctrl->forwardCommand(new UploadAnswerOptionsGUI()); + break; + + case strtolower(ilPermissionGUI::class): + $this->tabs_gui->activateTab('perm_settings'); + $this->ctrl->forwardCommand(new \ilPermissionGUI($this)); + break; + + case strtolower(QstsQuestionPageGUI::class): + $this->edit_view->forwardPageCmds( + $this->tpl, + $this->buildEditQuestionsBaseUri() + ); + break; + + default: + if ($cmd === null || $cmd === '' || $cmd === 'view') { + $cmd = 'viewQuestions'; + } + $cmd .= 'Object'; + $this->$cmd(); + break; + } + } + + public function viewQuestionsObject(): void + { + $this->tabs_gui->activateTab('questions'); + + $this->tpl->setContent( + $this->ui_renderer->render( + $this->edit_view->view( + $this->toolbar, + $this->buildEditQuestionsBaseUri() + ) + ) + ); + } + + public function getAdminTabs(): void + { + $this->getTabs(); + } + + protected function getTabs(): void + { + if ($this->rbac_system->checkAccess('read', $this->object->getRefId())) { + $this->tabs_gui->addTab( + 'questions', + $this->lng->txt('questions'), + $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + ); + } + + if ($this->rbac_system->checkAccess('edit_permission', $this->object->getRefId())) { + $this->tabs_gui->addTab( + 'perm_settings', + $this->lng->txt('perm_settings'), + $this->ctrl->getLinkTargetByClass([self::class, ilPermissionGUI::class], 'perm') + ); + } + } + + private function buildEditQuestionsBaseUri(): URI + { + return $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php new file mode 100755 index 000000000000..59a70f626023 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -0,0 +1,152 @@ + new DataFactory(); + $dic[UuidFactory::class] = static fn($c): UuidFactory => new UuidFactory(); + + $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory + => new AnswerFormFactory([ + $c[Cloze\Type::class] + ]); + $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository => + new QuestionsRepository( + $DIC['ilDB'], + new UuidFactory(), + ); + $dic[Edit::class] = static fn($c): Edit => new Edit( + $DIC['lng'], + $DIC['ilUser'], + $DIC['refinery'], + $DIC['ui.factory'], + $DIC['ui.renderer'], + $DIC['global_screen'], + $DIC['ilCtrl'], + $DIC['http'], + $DIC->uiService(), + $c[DataFactory::class], + $c[UuidFactory::class], + $c[AnswerFormFactory::class], + $c[QuestionsRepository::class] + ); + + $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory + => new Cloze\Properties\ClozeText\Factory( + $DIC['refinery'], + (new \ilMustacheFactory())->getBasicEngine(), + $c[UuidFactory::class], + $c[DataFactory::class]->text() + ); + $dic[Cloze\Properties\Gaps\Data\Factory::class] = static fn($c): Cloze\Properties\Gaps\Data\Factory + => new Cloze\Properties\Gaps\Data\Factory( + $c[UuidFactory::class], + $DIC['refinery'] + ); + $dic[Cloze\Properties\Gaps\Factory::class] = static fn($c): Cloze\Properties\Gaps\Factory + => new Cloze\Properties\Gaps\Factory( + $c[UuidFactory::class], + $c[Cloze\Properties\Gaps\Data\Factory::class], + [ + new Cloze\Properties\Gaps\Text( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\Numeric( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\Select( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\LongMenu( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ) + ] + ); + $dic[Cloze\Properties\AnswerForm\Factory::class] = static fn($c): Cloze\Properties\AnswerForm\Factory + => new Cloze\Properties\AnswerForm\Factory( + $c[UuidFactory::class], + $c[Cloze\Properties\ClozeText\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class] + ); + $dic[Cloze\Persistence::class] = static fn($c): Cloze\Persistence + => new Cloze\Persistence( + new TableNameSpaceCore('cloze') + ); + $dic[Cloze\Views\Edit::class] = static fn($c): Cloze\Views\Edit + => new Cloze\Views\Edit( + $DIC['lng'], + $DIC['ui.factory'], + $DIC['refinery'], + $DIC['http'], + $c[Cloze\Properties\AnswerForm\Factory::class], + $c[Cloze\Properties\ClozeText\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class] + ); + $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant + => new Cloze\Views\Participant(); + $dic[Cloze\Type::class] = static fn($c): Cloze\Type => new Cloze\Type( + $c[Cloze\Properties\AnswerForm\Factory::class], + $c[Cloze\Persistence::class], + [ + Cloze\Capabilities\Marking::class => new Cloze\Capabilities\Marking(), + Cloze\Capabilities\Feedback::class => new Cloze\Capabilities\Feedback(), + Cloze\Capabilities\Skills::class => new Cloze\Capabilities\Skills() + ], + $c[Cloze\Views\Edit::class], + $c[Cloze\Views\Participant::class] + ); + + return $dic; + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php new file mode 100644 index 000000000000..23644751d771 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -0,0 +1,27 @@ +setEnablePCType('Tabs', true); + $this->setEnablePCType('AnswerForm', true); + $this->setEnableInternalLinks(false); + $this->setEnablePageToc(true); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php new file mode 100644 index 000000000000..ae44c63ecae1 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -0,0 +1,42 @@ +setEnabledPageFocus(false); + } + + public function finishEditing(): void + { + $this->ctrl->redirectToURL($this->return_uri->__toString()); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php new file mode 100644 index 000000000000..86f8961dad6e --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -0,0 +1,460 @@ +setType('answf'); + } + + public function setQuestionReference(string $a_questionreference): void + { + if (is_object($this->getChildNode())) { + $this->getChildNode()->setAttribute("QRef", $a_questionreference); + } + } + + public function getQuestionReference(): ?string + { + if (is_object($this->getChildNode())) { + return $this->getChildNode()->getAttribute("QRef"); + } + return null; + } + + public function create( + ilPageObject $a_pg_obj, + string $a_hier_id, + string $a_pc_id = "" + ): void { + $this->createInitialChildNode( + $a_hier_id, + $a_pc_id, + "Question", + ["QRef" => ""] + ); + } + + /** + * Copy question from pool into page + */ + public function copyPoolQuestionIntoPage( + string $a_q_id, + string $a_hier_id + ): void { + $question = assQuestion::instantiateQuestion($a_q_id); + $duplicate_id = $question->copyObject(0, $question->getTitle()); + $duplicate = assQuestion::instantiateQuestion($duplicate_id); + $duplicate->setObjId(0); + + ilAssSelfAssessmentQuestionFormatter::prepareQuestionForLearningModule($duplicate); + + $this->getChildNode()->setAttribute("QRef", "il__qst_" . $duplicate_id); + } + + public static function getLangVars(): array + { + return ['ed_insert_pcqst', 'empty_question', 'pc_qst']; + } + + /** + * After page has been updated (or created) + */ + public static function afterPageUpdate( + ilPageObject $a_page, + DOMDocument $a_domdoc, + string $a_xml, + bool $a_creation + ): void { + global $DIC; + + $ilDB = $DIC->database(); + + $ilDB->manipulateF( + "DELETE FROM page_question WHERE page_parent_type = %s " . + " AND page_id = %s AND page_lang = %s", + array("text", "integer", "text"), + array($a_page->getParentType(), $a_page->getId(), $a_page->getLanguage()) + ); + + $xpath = new DOMXPath($a_domdoc); + $nodes = $xpath->query('//Question'); + $q_ids = array(); + foreach ($nodes as $node) { + $q_ref = $node->getAttribute("QRef"); + + $inst_id = ilInternalLink::_extractInstOfTarget($q_ref); + if (!($inst_id > 0)) { + $q_id = ilInternalLink::_extractObjIdOfTarget($q_ref); + if ($q_id > 0) { + $q_ids[$q_id] = $q_id; + } + } + } + foreach ($q_ids as $qid) { + $ilDB->replace( + "page_question", + [ + "page_parent_type" => ["text", $a_page->getParentType()], + "page_id" => ["integer", $a_page->getId()], + "page_lang" => ["text", $a_page->getLanguage()], + "question_id" => ["integer", $qid] + ], + [] + ); + } + } + + public static function beforePageDelete( + ilPageObject $a_page + ): void { + global $DIC; + + $ilDB = $DIC->database(); + + $ilDB->manipulateF( + "DELETE FROM page_question WHERE page_parent_type = %s " . + " AND page_id = %s AND page_lang = %s", + array("text", "integer", "text"), + array($a_page->getParentType(), $a_page->getId(), $a_page->getLanguage()) + ); + } + + public static function _getQuestionIdsForPage( + string $a_parent_type, + int $a_page_id, + string $a_lang = "-" + ): array { + global $DIC; + + $ilDB = $DIC->database(); + + $parent_type_array = explode(':', $a_parent_type); + + $res = $ilDB->queryF( + "SELECT * FROM page_question WHERE page_parent_type = %s " . + " AND page_id = %s AND page_lang = %s", + array("text", "integer", "text"), + array($parent_type_array[0], $a_page_id, $a_lang) + ); + $q_ids = array(); + while ($rec = $ilDB->fetchAssoc($res)) { + $q_ids[] = $rec["question_id"]; + } + + return $q_ids; + } + + public static function _getPageForQuestionId( + int $a_q_id, + string $a_parent_type = "" + ): ?array { + global $DIC; + + $ilDB = $DIC->database(); + + $set = $ilDB->query( + "SELECT * FROM page_question " . + " WHERE question_id = " . $ilDB->quote($a_q_id, "integer") + ); + while ($rec = $ilDB->fetchAssoc($set)) { + if ($a_parent_type == "" || $rec["page_parent_type"] == $a_parent_type) { + return array("page_id" => $rec["page_id"], "parent_type" => $rec["page_parent_type"]); + } + } + return null; + } + + public function modifyPageContentPostXsl( + string $a_output, + string $a_mode, + bool $a_abstract_only = false + ): string { + $qhtml = ""; + + if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { + // #14154 + $q_ids = $this->getQuestionIds(); + if (count($q_ids)) { + foreach ($q_ids as $q_id) { + $q_gui = assQuestionGUI::_getQuestionGUI("", $q_id); + // object check due to #16557 + if (is_object($q_gui->getObject()) && !$q_gui->getObject()->isComplete()) { + $a_output = str_replace( + "{{{{{Question;il__qst_" . $q_id . "}}}}}", + "" . $lng->txt("cont_empty_question") . "", + $a_output + ); + } + } + + // this exports the questions which is needed below + $qhtml = $this->getQuestionJsOfPage($a_mode == "edit", $a_mode); + + $a_output = "" . $a_output; + if (!self::$initial_done) { + $a_output = "" . $a_output; + self::$initial_done = true; + } + } + } else { + // set by T&A components + $qhtml = $this->getPage()->getPageConfig()->getQuestionHTML(); + + // address #19788 + if (!is_array($qhtml) || count($qhtml) == 0) { + // #14154 + $q_ids = $this->getQuestionIds(); + if (count($q_ids)) { + foreach ($q_ids as $k) { + $a_output = str_replace("{{{{{Question;il__qst_$k" . "}}}}}", " " . $lng->txt("copg_questions_not_supported_here"), $a_output); + } + } + } + } + + if (is_array($qhtml)) { + foreach ($qhtml as $k => $h) { + $a_output = str_replace("{{{{{Question;il__qst_$k" . "}}}}}", " " . $h, $a_output); + } + } + + return $a_output; + } + + /** + * Reset initial state (for exports) + */ + public static function resetInitialState(): void + { + self::$initial_done = false; + } + + public function getJavascriptFiles(string $a_mode): array + { + $js_files = array(); + + if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { + $js_files[] = 'assets/js/pure_rendering.js'; + $js_files[] = 'assets/js/question_handling.js'; + $js_files[] = 'assets/js/matchinginput.js'; + $js_files[] = 'assets/js/orderinghorizontal.js'; + $js_files[] = 'assets/js/orderingvertical.js'; + $js_files[] = 'assets/js/matching.js'; + + foreach ($this->getQuestionIds() as $qId) { + $qstGui = assQuestionGUI::_getQuestionGUI('', $qId); + $js_files = array_merge($js_files, $qstGui->getPresentationJavascripts()); + } + } + + if (!$this->getPage()->getPageConfig()->getEnableSelfAssessmentScorm() && $a_mode != ilPageObjectGUI::PREVIEW + && $a_mode != "offline") { + $js_files[] = "./components/ILIAS/COPage/js/ilCOPageQuestionHandler.js"; + } + + return $js_files; + } + + public function getCssFiles(string $a_mode): array + { + if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { + return array("./components/ILIAS/TestQuestionPool/resources/js/dist/question_handling.css", + "components/ILIAS/TestQuestionPool/templates/default/test_javascript.css"); + } + return array(); + } + + public function getOnloadCode(string $a_mode): array + { + $code = array(); + + if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { + if (!$this->getPage()->getPageConfig()->getEnableSelfAssessmentScorm() && $a_mode != ilPageObjectGUI::PREVIEW + && $a_mode != "offline" && $a_mode !== "edit") { + $ilCtrl->setParameterByClass(strtolower(get_class($this->getPage())) . "gui", "page_id", $this->getPage()->getId()); + $url = $ilCtrl->getLinkTargetByClass(strtolower(get_class($this->getPage())) . "gui", "processAnswer", "", true, false); + $code[] = "ilCOPageQuestionHandler.initCallback('" . $url . "');"; + } + + if ($this->getPage()->getPageConfig()->getDisableDefaultQuestionFeedback()) { + $code[] = "ilias.questions.default_feedback = false;"; + } + + $code[] = self::getJSTextInitCode($this->getPage()->getPageConfig()->getLocalizationLanguage()) . ' il.COPagePres.updateQuestionOverviews();'; + } + + $q_ids = $this->getQuestionIds(); + + // call renderers + foreach ($q_ids as $q_id) { + $code[] = "if (typeof renderILQuestion$q_id === 'function') {renderILQuestion$q_id();}"; + } + + // init answer status + $get_stored_tries = $this->getPage()->getPageConfig()->getUseStoredQuestionTries(); + if ($get_stored_tries) { + if (count($q_ids) > 0) { + foreach ($q_ids as $q_id) { + $as = ilPageQuestionProcessor::getAnswerStatus($q_id, $ilUser->getId()); + $code[] = "ilias.questions.initAnswer(" . $q_id . ", " . (int) ($as["try"] ?? 0) . ", " . ($as["passed"] ? "true" : "null") . ");"; + } + } + } + return $code; + } + + /** + * Get js txt init code + */ + public static function getJSTextInitCode(string $a_lang): string + { + global $DIC; + + $lng = $DIC->language(); + $ilUser = $DIC->user(); + + if ($a_lang == "") { + $a_lang = $ilUser->getLanguage(); + } + + return + ' + ilias.questions.txt.wrong_answers = "' . $lng->txtlng("content", "cont_wrong_answers", $a_lang) . '"; + ilias.questions.txt.wrong_answers_single = "' . $lng->txtlng("content", "cont_wrong_answers_single", $a_lang) . '"; + ilias.questions.txt.tries_remaining = "' . $lng->txtlng("content", "cont_tries_remaining", $a_lang) . '"; + ilias.questions.txt.please_try_again = "' . $lng->txtlng("content", "cont_please_try_again", $a_lang) . '"; + ilias.questions.txt.all_answers_correct = "' . $lng->txtlng("content", "cont_all_answers_correct", $a_lang) . '"; + ilias.questions.txt.enough_answers_correct = "' . $lng->txtlng("content", "cont_enough_answers_correct", $a_lang) . '"; + ilias.questions.txt.nr_of_tries_exceeded = "' . $lng->txtlng("content", "cont_nr_of_tries_exceeded", $a_lang) . '"; + ilias.questions.txt.correct_answers_separator = "' . $lng->txtlng("assessment", "or", $a_lang) . '"; + ilias.questions.txt.correct_answers_shown = "' . $lng->txtlng("content", "cont_correct_answers_shown", $a_lang) . '"; + ilias.questions.txt.correct_answers_also = "' . $lng->txtlng("content", "cont_correct_answers_also", $a_lang) . '"; + ilias.questions.txt.correct_answer_also = "' . $lng->txtlng("content", "cont_correct_answer_also", $a_lang) . '"; + ilias.questions.txt.ov_all_correct = "' . $lng->txtlng("content", "cont_ov_all_correct", $a_lang) . '"; + ilias.questions.txt.ov_some_correct = "' . $lng->txtlng("content", "cont_ov_some_correct", $a_lang) . '"; + ilias.questions.txt.ov_wrong_answered = "' . $lng->txtlng("content", "cont_ov_wrong_answered", $a_lang) . '"; + ilias.questions.txt.please_select = "' . $lng->txtlng("content", "cont_please_select", $a_lang) . '"; + ilias.questions.txt.ov_preview = "' . $lng->txtlng("content", "cont_ov_preview", $a_lang) . '"; + ilias.questions.txt.submit_answers = "' . $lng->txtlng("content", "cont_submit_answers", $a_lang) . '"; + ilias.questions.refresh_lang(); + '; + } + + public function getQuestionJsOfPage( + bool $a_no_interaction, + string $a_mode + ): array { + $q_ids = $this->getQuestionIds(); + $js = array(); + if (count($q_ids) > 0) { + foreach ($q_ids as $q_id) { + $q_exporter = new ilQuestionExporter($a_no_interaction); + $image_path = ""; + if ($a_mode == "offline") { + if ($this->getPage()->getParentType() == "sahs") { + $image_path = "./objects/"; + } + if ($this->getPage()->getParentType() == "lm") { + $image_path = "./assessment/0/" . $q_id . "/images/"; + } + } + $js[$q_id] = $q_exporter->exportQuestion($q_id, $image_path, $a_mode); + } + } + return $js; + } + + protected function getQuestionIds(): array + { + $dom = $this->getPage()->getDomDoc(); + $q_ids = []; + $nodes = $this->dom_util->path($dom, "//Question"); + foreach ($nodes as $node) { + $qref = $node->getAttribute("QRef"); + $inst_id = ilInternalLink::_extractInstOfTarget($qref); + $obj_id = ilInternalLink::_extractObjIdOfTarget($qref); + + if (!($inst_id > 0)) { + if ($obj_id > 0) { + $q_ids[] = $obj_id; + } + } + } + return $q_ids; + } + + public static function handleCopiedContent( + DOMDocument $a_domdoc, + bool $a_self_ass = true, + bool $a_clone_mobs = false, + int $new_parent_id = 0, + int $obj_copy_id = 0 + ): void { + global $DIC; + + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + // handle question elements + if ($a_self_ass) { + // copy questions + $path = "//Question"; + $nodes = $dom_util->path($a_domdoc, $path); + foreach ($nodes as $node) { + $qref = $node->getAttribute("QRef"); + + $inst_id = ilInternalLink::_extractInstOfTarget($qref); + $q_id = ilInternalLink::_extractObjIdOfTarget($qref); + + if (!($inst_id > 0)) { + if ($q_id > 0) { + $question = null; + try { + $question = assQuestion::instantiateQuestion($q_id); + } catch (Exception $e) { + } + // check due to #16557 + if (is_object($question) && $question->isComplete()) { + // check if page for question exists + // due to a bug in early 4.2.x version this is possible + if (!ilPageObject::_exists("qpl", $q_id)) { + $question->createPageObject(); + } + + // now copy this question and change reference to + // new question id + $duplicate_id = $question->duplicate(false); + $node->setAttribute("QRef", "il__qst_" . $duplicate_id); + } + } + } + } + } else { + // remove question + $path = "//Question"; + $nodes = $dom_util->path($a_domdoc, $path); + foreach ($nodes as $node) { + $parent = $node->parentNode; + $parent->parentNode->removeChild($parent); + } + } + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php new file mode 100644 index 000000000000..e97a2c11526d --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -0,0 +1,96 @@ +tabs = $DIC['ilTabs']; + $this->ui_renderer = $DIC['ui.renderer']; + $this->data_factory = new DataFactory(); + $this->edit_view = LocalDIC::dic()[Edit::class]; + + parent::__construct($a_pg_obj, $a_content_obj, $a_hier_id, $a_pc_id); + } + + public function executeCommand() + { + $cmd = $this->ctrl->getCmd() . 'Cmd'; + $this->$cmd(); + } + + public function insertCmd(): void + { + $this->setInsertTabs(); + $this->tpl->setContent( + $this->ui_renderer->render( + $this->edit_view->createAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ) + ) + ) + ); + } + + public function editCmd(): void + { + $this->setInsertTabs(); + $this->tpl->setContent( + $this->ui_renderer->render( + $this->edit_view->editAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ) + ) + ) + ); + } + + private function setInsertTabs(): void + { + $this->tabs->setBackTarget( + $this->lng->txt('cancel'), + $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') + ); + } + + public function setEditTabs(): void + { + } +} diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php new file mode 100644 index 000000000000..32633b7e0ae1 --- /dev/null +++ b/components/ILIAS/Questions/Questions.php @@ -0,0 +1,46 @@ + + new Agent( + $pull[Refinery::class] + ); + } +} diff --git a/components/ILIAS/Questions/README.md b/components/ILIAS/Questions/README.md new file mode 100644 index 000000000000..6b2a3b9fda2b --- /dev/null +++ b/components/ILIAS/Questions/README.md @@ -0,0 +1 @@ +# Questions diff --git a/components/ILIAS/Questions/maintenance.json b/components/ILIAS/Questions/maintenance.json new file mode 100644 index 000000000000..462ad3856cc3 --- /dev/null +++ b/components/ILIAS/Questions/maintenance.json @@ -0,0 +1,14 @@ +{ + "maintenance_model": "Classic", + "first_maintainer": "dstrassner(48931)", + "second_maintainer": "mbecker(27266)", + "implicit_maintainers": [], + "coordinator": [ + "" + ], + "tester": "SIG EA", + "testcase_writer": "Fabian(27631)", + "path": "Modules/Test", + "belong_to_component": "Test & Assessment", + "used_in_components": [] +} \ No newline at end of file diff --git a/components/ILIAS/Questions/service.xml b/components/ILIAS/Questions/service.xml new file mode 100644 index 000000000000..4f310194a537 --- /dev/null +++ b/components/ILIAS/Questions/service.xml @@ -0,0 +1,20 @@ + + + + + adm + + + + + + + + + + + + + + diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php new file mode 100644 index 000000000000..18f055fc1589 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -0,0 +1,26 @@ + $available_answer_form_types + */ + private readonly array $available_answer_form_types; + + /** + * @param array<\ILIAS\Questions\AnswerForm\Definition> $available_answer_form_types + */ + public function __construct( + private readonly UuidFactory $uuid_factory, + array $available_answer_form_types + ) { + $this->available_answer_form_types = array_reduce( + $available_answer_form_types, + function (array $c, Type $v) { + $c[$this->getHashedClass($v::class)] = $v; + return $c; + }, + [] + ); + } + + /** + * @return array + */ + public function getAnswerFormTypesArrayForSelect(): array + { + return array_reduce( + $this->available_answer_form_types, + function (array $c, Definition $v): array { + $c[$this->getHashedClass($v::class)] = $v->getLabel($this->lng); + return $c; + }, + [] + ); + } + + public function getHashedClass(string $class): string + { + return md5($class); + } + + public function buildTypeDefinitionFromSelectValue(string $value): Definition + { + $type = $this->available_answer_form_types[$value] ?? null; + if ($type === null) { + throw new InvalidArgumentException('This type of answer form does not exist.'); + } + return $type; + } + + public function getDefaultTypeGenericProperties(Uuid $question_id): TypeGenericProperties + { + return new TypeGenericProperties( + $this->uuid_factory->uuid4(), + $question_id + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php new file mode 100644 index 000000000000..9b046e733725 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -0,0 +1,36 @@ +answer_form_id; + } + + public function getQuestionId(): Uuid + { + return $this->question_id; + } + + public function getAvailablePoints(): ?float + { + return $this->available_points; + } + + public function getImageSize(): ?int + { + return $this->image_size; + } + + public function getShuffleAnswerOptions(): ?bool + { + return $this->shuffle_answer_options; + } + + public function getAdditionalText(): string + { + return $this->additional_text; + } + + public function getAdditionalTextLegacy(): string + { + return $this->additional_text_legacy; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php new file mode 100644 index 000000000000..2dd5fbb85e41 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -0,0 +1,46 @@ + $available_capabilities + */ + public function __construct( + private readonly PropertiesFactory $properties_factory, + private readonly Persistence $persistence, + private readonly array $available_capabilities, + private readonly Edit $edit_view, + private readonly Participant $participant_view + ) { + } + + public function getLabel(Language $lng): string + { + return $lng->txt('assClozeTest'); + } + + public function buildProperties( + TypeGenericData $type_generic_data, + array $type_specific_data + ): Properties { + return $this->properties_factory->fromData($type_generic_data, $type_specific_data); + } + + public function getProperties(): Properties + { + return $this->properties; + } + + public function getPersistence(): Persistence + { + return $this->persistence; + } + + public function hasCapability(string $capability_class_name): bool + { + return array_key_exists($capability_class_name, $this->available_capabilities); + } + + public function getCapability(string $capability_class_name): ?Capability + { + return $this->available_capabilities[$capability_class_name]; + } + + public function getEditView(): Edit + { + return $this->edit_view; + } + + public function getParticipantView(): Participant + { + return $this->participant_view; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php new file mode 100644 index 000000000000..80f03cba1607 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -0,0 +1,141 @@ +table_namespace; + } + + public function completeSelectQuery( + TableNameBuilder $table_name_builder, + SelectQuery $select_query, + Column $base_table_id_column + ): SelectQuery { + return $select_query->withAdditionalJoin( + new Join( + $base_table_id_column, + new Column( + $table_name_builder->getTypeSpecificAnswerFormsTableName(), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ) + ) + )->withAdditionalSelect( + new Select( + $table_name_builder->getTypeSpecificAnswerFormsTableName(), + self::ANSWER_FORM_TABLE_COLUMNS + ) + )->withAdditionalJoin( + new Join( + $base_table_id_column, + new Column( + $table_name_builder->getAnswerInputsTableName(), + self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN + ), + JoinType::Left + ) + )->withAdditionalSelect( + new Select( + $table_name_builder->getAnswerInputsTableName(), + self::ANSWER_INPUTS_TABLE_COLUMNS + ) + )->withAdditionalJoin( + new Join( + new Column( + $table_name_builder->getAnswerInputsTableName(), + self::ANSWER_INPUTS_TABLE_ID_COLUMN + ), + new Column( + $table_name_builder->getAnswerOptionsTableName(), + self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN + ), + JoinType::Left + ) + )->withAdditionalSelect( + new Select( + $table_name_builder->getAnswerOptionsTableName(), + self::ANSWER_OPTIONS_TABLE_COLUMNS + ) + )->withAdditionalJoin( + new Join( + $base_table_id_column, + new Column( + $table_name_builder->getAdditionalTableName('combinations'), + self::COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN + ), + JoinType::Left + ) + )->withAdditionalSelect( + new Select( + $table_name_builder->getAdditionalTableName('combinations'), + self::COMBINATIONS_TABLE_COLUMNS + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php new file mode 100644 index 000000000000..6d9e68728760 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php @@ -0,0 +1,76 @@ +getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $this->cloze_text_factory->buildFromTextString($type_generic_properties->getAdditionalText()), + $type_generic_properties->getAdditionalTextLegacy() + ); + } + + public function fromForm( + Properties $properties, + ClozeText $cloze_text, + ScoringIdentical $scoring_of_identical_responses, + bool $combinations_enabled + ): Properties { + $updated_gaps = $cloze_text->updateGapsFromMarkdown($properties->getGaps()); + return $properties + ->withClozeText( + $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) + )->withGaps($updated_gaps) + ->withScoringOfIdenticalResponses($scoring_of_identical_responses) + ->withCombinationsEnabled($combinations_enabled); + } + + public function buildDefault(): Properties + { + return new Properties( + null, + $this->cloze_text_factory->buildFromTextString(''), + $this->gaps_factory->getEmptyGapsObject() + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php new file mode 100644 index 000000000000..8b40e4edbfce --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php @@ -0,0 +1,224 @@ + $gaps + */ + public function __construct( + private readonly ?Uuid $answer_form_id, + private readonly ?Uuid $question_id, + private Text $cloze_text, + private Gaps $gaps, + private ScoringIdentical $scoring_identical = ScoringIdentical::ScoreAll, + private bool $combinations_enabled = false, + private readonly string $legacy_cloze_text = '' + ) { + } + + public function getTypeGenericData(): TypeGenericProperties + { + return new TypeGenericProperties( + $this->answer_form_id, + $this->question_id, + null, + null, + null, + $this->cloze_text->getRawRepresentationForPersistence(), + $this->legacy_cloze_text + ); + } + + public function getAnswerFormId(): ?Uuid + { + return $this->answer_form_id; + } + + public function getClozeText(): Text + { + return $this->cloze_text; + } + + public function withClozeText(Text $cloze_text): self + { + $clone = clone $this; + $clone->cloze_text = $cloze_text; + return $clone; + } + + public function getLegacyClozeText(): string + { + return $this->legacy_cloze_text; + } + + public function getScoringOfIdenticalResponses(): ScoringIdentical + { + return $this->scoring_identical; + } + + public function withScoringOfIdenticalResponses(ScoringIdentical $scoring_identical): self + { + $clone = clone $this; + $clone->scoring_identical = $scoring_identical; + return $clone; + } + + public function areCombinationsEnabled(): bool + { + return $this->combinations_enabled; + } + + public function withCombinationsEnabled(bool $combinations_enabled): self + { + $clone = clone $this; + $clone->combinations_enabled = $combinations_enabled; + return $clone; + } + + public function getGaps(): Gaps + { + return $this->gaps; + } + + public function withGaps(Gaps $gaps): self + { + $clone = clone $this; + $clone->gaps = $gaps; + return $clone; + } + + public function buildBasicEditingInputs( + Language $lng, + FieldFactory $ff, + Refinery $refinery, + Factory $propteries_factory, + ClozeTextFactory $cloze_text_factory + ): Section { + return $ff->section( + [ + self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getInput( + $lng, + $ff, + $cloze_text_factory + ), + self::FORM_KEY_IDENTICAL_SCORING => ScoringIdentical::buildInput( + $lng, + $ff, + $refinery, + $this->scoring_identical + )->withValue($this->getScoringOfIdenticalResponses()->value), + self::FORM_KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) + ->withValue($this->areCombinationsEnabled()) + ], + $lng->txt('create_answer_form') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $vs): self => $propteries_factory->fromForm( + $this, + $vs[self::FORM_KEY_CLOZE_TEXT], + $vs[self::FORM_KEY_IDENTICAL_SCORING], + $vs[self::FORM_KEY_ENABLE_COMBINATIONS] + ) + ) + ); + } + + public function buildBasicEditingInputsHidden( + FieldFactory $ff + ): Group { + return $ff->group( + [ + self::FORM_KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()), + self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getHiddenInput($ff) + ->withDedicatedName(self::FORM_KEY_CLOZE_TEXT), + self::FORM_KEY_GAPS_TO_EDIT => $this->gaps->getHiddenInput($ff) + ->withDedicatedName(self::FORM_KEY_GAPS_TO_EDIT), + self::FORM_KEY_IDENTICAL_SCORING => $ff->hidden() + ->withDedicatedName(self::FORM_KEY_IDENTICAL_SCORING) + ->withValue($this->getScoringOfIdenticalResponses()->value), + self::FORM_KEY_ENABLE_COMBINATIONS => $ff->hidden() + ->withDedicatedName(self::FORM_KEY_ENABLE_COMBINATIONS) + ->withValue($this->areCombinationsEnabled() ? 1 : 0) + ] + ); + } + + public function withValuesFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): Properties { + $cloze_text = $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_CLOZE_TEXT, + $refinery->custom()->transformation( + fn(string $v): ClozeText => $this->cloze_text_factory->buildFromHiddenInputString($v) + ) + ); + + $scoring_of_identical_responses = $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_IDENTICAL_SCORING, + $refinery->custom()->transformation( + static fn(string $v): ScoringIdentical => ScoringIdentical::tryFrom($v) + ?? $properties->getScoringOfIdenticalResponses() + ) + ); + + $combinations_enabled = $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_ENABLE_COMBINATIONS, + $refinery->kindlyTo()->bool() + ); + + $gaps = $cloze_text->updateGapsFromMarkdown($properties->getGaps()) + ->withValuesFromPost( + $refinery, + $post_wrapper, + $this->gaps_factory, + $form_input_path . '/' . self::FORM_KEY_GAPS_TO_EDIT + ); + + return $properties + ->withClozeText($cloze_text) + ->withScoringOfIdenticalResponses($scoring_of_identical_responses) + ->withCombinationsEnabled($combinations_enabled) + ->withGaps($gaps); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php new file mode 100644 index 000000000000..38a11609da62 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php @@ -0,0 +1,57 @@ +refinery, + $this->mustache_engine, + $this->text_factory, + $this->text_factory->markdown($text) + ); + } + + public function buildFromHiddenInputString(string $text): Text + { + return $this->buildFromTextString($this->unmaskTextFromOutputInHiddenInput($text)); + } + + private function unmaskTextFromOutputInHiddenInput(string $text): string + { + return str_replace(['{{', '}}'], ['{{', '}}'], $text); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php new file mode 100644 index 000000000000..5903fa486fc0 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -0,0 +1,157 @@ +markdown( + new \ilUIMarkdownPreviewGUI(), + $lng->txt('cloze_text') + )->withMustacheVariables([ + Gap::GAP_PLACEHOLDER_NAME => $lng->txt('gap') + ])->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): self => $cloze_text_factory->buildFromTextString($v) + ) + )->withAdditionalTransformation( + $this->refinery->custom()->constraint( + fn(self $v): bool => $v->hasAtLeastOneGap(), + $lng->txt('no_gaps') + ) + )->withRequired(true) + ->withValue($this->cloze_text->getRawRepresentation()); + } + + public function getHiddenInput(FieldFactory $ff): HiddenInput + { + return $ff->hidden()->withValue($this->getTextForOutputInHiddenInput()); + } + + public function getRawRepresentationForPersistence(): string + { + return $this->cloze_text->getRawRepresentation(); + } + + public function getRenderedMarkdown( + Gaps $gaps + ): string { + return $this->mustache_engine->render( + $this->cloze_text->getRawRepresentation(), + $gaps->getPlaceholderArrayForPreview() + ); + } + + public function updateGapsFromMarkdown( + Gaps $pre_existing_gaps + ): Gaps { + if ($this->cloze_text->getRawRepresentation() === '') { + return $pre_existing_gaps->withResetGaps(); + } + + $position = 0; + return array_reduce( + $this->mustache_engine->getTokenizer()->scan($this->cloze_text->getRawRepresentation()), + function (Gaps $c, array $v) use (&$position): Gaps { + if ($v['type'] !== '_v' + || !str_starts_with($v['name'], Gap::GAP_PLACEHOLDER_NAME)) { + return $c; + } + + if ($v['name'] === Gap::GAP_PLACEHOLDER_NAME) { + return $c->withNewGap($position++); + } + + $gap = $c->getGapByTagName($v['name']); + if ($gap !== null) { + return $c->withPosition($position++); + } + + return $c->withAdditionalGapFromTagName($v['name'], $position++); + }, + $pre_existing_gaps + ); + } + + public function withIdsOfNewGapsInClozeText(array $new_gaps): self + { + if ($new_gaps === []) { + return self; + } + + $clone = clone $this; + $clone->cloze_text = $this->text_factory->markdown( + mb_ereg_replace_callback( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '}}', + function (array $matches) use (&$new_gaps): string { + return array_shift($new_gaps)->getGapPlaceholder(); + }, + $this->cloze_text->getRawRepresentation() + ) + ); + return $clone; + } + + private function hasAtLeastOneGap(): bool + { + if ($this->cloze_text->getRawRepresentation() === '') { + return false; + } + + foreach ($this->mustache_engine->getTokenizer() + ->scan($this->cloze_text->getRawRepresentation()) as $token) { + if ($token['type'] === '_v' + && str_starts_with($token['name'], Gap::GAP_PLACEHOLDER_NAME)) { + return true; + } + } + + return false; + } + + private function getTextForOutputInHiddenInput(): string + { + return str_replace(['{{', '}}'], ['{{', '}}'], $this->cloze_text->getRawRepresentation()); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php new file mode 100644 index 000000000000..19e3c1f9a30f --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php @@ -0,0 +1,61 @@ +select( + $lng->txt('scoring_of_identical_responses'), + self::buildOptionsList($lng) + )->withRequired(true) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(string $v): self => self::tryFrom($v) ?? $default_value + ) + ); + } + + private static function buildOptionsList(Language $lng): array + { + return array_reduce( + self::cases(), + function (array $c, self $v) use ($lng): array { + $c[$v->value] = $lng->txt($v->value); + return $c; + }, + [] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php new file mode 100644 index 000000000000..06b38b1f56dd --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -0,0 +1,90 @@ +available_gap_types[$type->getIdentifier()] = $type; + } + } + + public function getAvailableGapTypes(): array + { + return $this->available_gap_types; + } + + public function getAvailableGapTypesOptionsArray( + Language $lng + ): array { + return array_map( + fn(Type $v) => $lng->txt("{$v->getIdentifier()}_gap"), + $this->available_gap_types + ); + } + + public function getNewGap( + int $position, + string $id = '' + ): Gap { + $answer_input_id = $id !== '' + ? $this->uuid_factory->fromString($id) + : $this->uuid_factory->uuid4(); + return new Gap( + $answer_input_id, + $position, + $this->data_factory->getDefaultDataObject($answer_input_id) + ); + } + + public function getEmptyGapsObject(): Gaps + { + return new Gaps( + $this, + [] + ); + } + + public function getGapTypeByIdentifier(string $identifier): Type + { + if (!array_key_exists($identifier, $this->available_gap_types)) { + throw new \InvalidArgumentException('Gap type does not exist.'); + } + return $this->available_gap_types[$identifier]; + } + + public function fromDatabase( + array $data + ): Gaps { + + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php new file mode 100644 index 000000000000..dc0be37e81e3 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -0,0 +1,196 @@ +answer_input_id; + } + + public function withAnswerInputId(Uuid $answer_input_id): self + { + $clone = clone $this; + $clone->answer_input_id = $answer_input_id; + return $clone; + } + + public function getPosition(): int + { + return $this->position; + } + + public function withPosition(int $position): self + { + $clone = clone $this; + $clone->position = $position; + return $clone; + } + + public function withType(Type $type): self + { + $clone = clone $this; + $clone->type = $type; + return $clone; + } + + public function getData(): Data + { + return $this->data; + } + + public function withData(Data $data): self + { + $clone = clone $this; + $clone->data = $data; + return $clone; + } + + public function isUndefined(): bool + { + return $this->type === null; + } + + public function getGapPlaceholder(): string + { + return "{{{$this->buildGapPlaceholderNameWithId()}}}"; + } + + public function buildShortenedGapName(): string + { + return self::GAP_PLACEHOLDER_NAME . '_' . $this->getShortenedAnswerInputId(); + } + + public function buildShortenedGapRepresentation(): string + { + return "[{$this->buildShortenedGapName()}]"; + } + + public function buildGapPlaceholderNameWithId(): string + { + return self::GAP_PLACEHOLDER_NAME . '_' . $this->answer_input_id->toString(); + } + + public function getEditAnswerOptionsSection( + Language $lng, + FieldFactory $ff + ): Section { + $section = $ff->section( + $this->type->getEditAnswerOptionsInputs($this->data), + "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" + ); + + $edit_section_constraint = $this->type->getEditAnswerOptionsSectionConstraint(); + if ($edit_section_constraint !== null) { + $section = $section->withAdditionalTransformation($edit_section_constraint); + } + + + return $section->withAdditionalTransformation( + $this->type->getBuildGapTransformation($this) + ); + } + + public function getEditPointsSection( + Language $lng, + FieldFactory $ff + ): Section { + $section = $ff->section( + $this->type->getEditPointsInputs($this->data->getAnswerOptions()), + "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" + ); + + $edit_section_constraint = $this->type->getEditPointsSectionConstraint(); + if ($edit_section_constraint !== null) { + $section = $section->withAdditionalTransformation($edit_section_constraint); + } + + + return $section->withAdditionalTransformation( + $this->type->getAddPointsTransformation($this) + ); + } + + public function getHiddenInput( + FieldFactory $ff + ): Group { + return $ff->group([ + self::FORM_KEY_TYPE => $ff->hidden()->withValue($this->type?->getIdentifier() ?? '') + ->withDedicatedName(self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId()), + self::FORM_KEY_DATA => $this->data->getHiddenInput($ff) + ->withDedicatedName(self::FORM_KEY_DATA . $this->getShortenedAnswerInputId()) + ]); + } + + public function withValuesFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + Factory $gaps_factory, + string $form_input_path + ): self { + $available_gap_types = $gaps_factory->getAvailableGapTypes(); + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->custom()->transformation( + fn(?string $v): self => $available_gap_types[$v] + ? $this->withType($available_gap_types[$v]) + : $this + ), + $refinery->always($this) + ]) + )->withData( + $this->data->withValuesFromPost( + $refinery, + $post_wrapper, + $form_input_path . '/' . self::FORM_KEY_DATA . $this->getShortenedAnswerInputId() + ) + ); + } + + private function getShortenedAnswerInputId(): string + { + return mb_substr($this->answer_input_id->toString(), 0, 4); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php new file mode 100644 index 000000000000..1e93f6b0b341 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -0,0 +1,238 @@ +gaps[$gap_id->toString()] ?? null; + } + + public function getGapByTagName(string $tag_name): ?Gap + { + return $this->gaps[$this->extractIdFromTagName($tag_name)] ?? null; + } + + public function hasAtLeastOneGap(): bool + { + return $this->gaps !== []; + } + + public function withGap(Gap $gap): self + { + $clone = clone $this; + $clone->gaps[$gap->getAnswerInputId()->toString()] = $gap; + return $clone; + } + + public function withNewGap(int $position): self + { + $new_gap = $this->factory->getNewGap($position); + $clone = clone $this; + $clone->gaps[$new_gap->getAnswerInputId()->toString()] = $new_gap; + return $clone; + } + + public function withAdditionalGapFromTagName(string $tag_name, $position): self + { + $id = $this->extractIdFromTagName($tag_name); + $clone = clone $this; + $clone->gaps[$id] = $this->factory->getNewGap($position, $id); + return $clone; + } + + public function withResetGaps(): self + { + if ($this->gaps === []) { + return $this; + } + + $clone = clone $this; + $clone->gaps = []; + return $clone; + } + + public function getUndefinedGaps(): array + { + return array_filter( + $this->gaps, + fn(Gap $v): bool => $v->isUndefined() + ); + } + + public function getPlaceholderArrayForPreview(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildShortenedGapRepresentation($v); + return $c; + }, + [] + ); + } + + public function buildGapsTypeInputs( + Language $lng, + FieldFactory $ff, + Refinery $refinery, + array $available_gap_types + ): Section { + return $ff->section( + array_reduce( + $this->getUndefinedGaps(), + function (array $c, Gap $v) use ($ff, $available_gap_types): array { + $c[$v->getAnswerInputId()->toString()] = $ff->select( + $v->buildShortenedGapName(), + $available_gap_types + )->withRequired(true); + return $c; + }, + [] + ), + $lng->txt('select_gap_types') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $vs): self => array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap( + $c->gaps[$v]->withType( + $this->factory->getGapTypeByIdentifier($vs[$v]) + ) + ), + $this + ) + ) + ); + } + + public function buildAnswerOptionsInputs( + Language $lng, + FieldFactory $ff, + Refinery $refinery + ): Section { + return $ff->section( + array_reduce( + $this->gaps, + function (array $c, Gap $v) use ($lng, $ff): array { + $c[$v->getAnswerInputId()->toString()] = $v->getEditAnswerOptionsSection( + $lng, + $ff + ); + return $c; + }, + [] + ), + $lng->txt('add_answer_options') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $vs): self => array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) + ) + ); + } + + public function buildPointInputs( + Language $lng, + FieldFactory $ff, + Refinery $refinery + ): Section { + return $ff->section( + array_reduce( + $this->gaps, + function (array $c, Gap $v) use ($lng, $ff): array { + $c[$v->getAnswerInputId()->toString()] = $v->getEditPointsSection( + $lng, + $ff + ); + return $c; + }, + [] + ), + $lng->txt('add_points') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $vs): self => array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) + ) + ); + } + + public function getHiddenInput(FieldFactory $ff): Group + { + return $ff->group( + array_reduce( + $this->gaps, + function (array $c, Gap $v) use ($ff): array { + $c[$v->getAnswerInputId()->toString()] = $v->getHiddenInput($ff) + ->withDedicatedName($v->getAnswerInputId()->toString()); + return $c; + }, + [] + ) + ); + } + + public function withValuesFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + Factory $gaps_factory, + string $form_input_path + ): self { + $clone = clone $this; + $clone->gaps = array_map( + fn(Gap $v): Gap => $v->withValuesFromPost( + $refinery, + $post_wrapper, + $gaps_factory, + $form_input_path . '/' . $v->getAnswerInputId()->toString() + ), + $this->gaps + ); + return $clone; + } + + private function extractIdFromTagName(string $tag_name): string + { + return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php new file mode 100644 index 000000000000..9295f0129a63 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -0,0 +1,158 @@ +ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'upload_answer_options' => $ff->file( + new UploadAnswerOptionsGUI(), + $this->lng->txt('upload_answer_options'), + $this->lng->txt('upload_answer_options_info') + )->withAcceptedMimeTypes(self::ACCEPTED_MIME_TYPES), + 'min_autocomplete' => $ff->numeric( + $this->lng->txt('min_autocomplete') + )->withRequired(true) + ->withValue($data->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), + 'options_awarding_points' => $ff->tag( + $this->lng->txt('answer_options'), + $data->getAnswerOptions()->getTagsArrayFromAnswerOptions() + ) + ->withRequired(true) + ->withValue( + array_map( + fn(AnswerOption $v): string => $v->getTextValue(), + $data->getAnswerOptions()->getAnswerOptionsAwardingPoints() + ) + ) + ]; + } + + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + $values = array_merge( + $vs['answer_options'], + $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) + ); + + return $values !== [] && array_filter( + $vs['options_awarding_points'], + fn(string $v): bool => !in_array($v, $values) + ) === []; + }, + $this->lng->txt('error') + ); + } + + public function getEditPointsInputs(AnswerOptions $answer_options): array + { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue(), + $answer_options->getAnswerOptionsAwardingPoints() + ); + } + + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + public function getBuildGapTransformation(Gap $gap): Transformation + { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withData( + $gap->getData() + ->withMinAutocomplete($vs['min_autocomplete']) + ->withAnswerOptions( + $gap->getData()->getAnswerOptions()->withAnswerOptionsFromTags( + array_merge( + $vs['answer_options'], + $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) + ) + )->withAnswerOptionsAwardingPoints($vs['options_awarding_points']) + ) + ) + ); + } + + public function getAnswerInput(): \ilFormPropertyGUI + { + ; + } + + private function retrieveAnswerOptionsArrayFromUpload(?array $upload_value): array + { + if ($upload_value === null + || ($decoded_value = base64_decode($upload_value[0] ?? '')) === '') { + return []; + } + + return array_filter( + mb_split('\R', $decoded_value) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php new file mode 100644 index 000000000000..52302d45edfd --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -0,0 +1,129 @@ +getAnswerOptions()->getAnswerOptionForPositionOrNew(0); + $ff = $this->ui_factory->input()->field(); + return [ + 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) + ->withStepSize($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withRequired(true) + ->withValue($answer_option->getLowerLimit()), + 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) + ->withStepSize($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withValue($answer_option->getUpperLimit()), + 'step_size' => $ff->numeric($this->lng->txt('step_size')) + ->withStepSize(0.000001) + ->withRequired(true) + ->withValue($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ]; + } + + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + fn(array $vs): bool => $vs['upper_limit'] === null + || $vs['upper_limit'] >= $vs['lower_limit'] + && $vs['upper_limit'] >= $vs['step_size'], + $this->lng->txt('upper_limit_bigger_than_lower') + ); + } + + public function getEditPointsInputs(AnswerOptions $answer_options): array + { + $inputs = $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + function (AnswerOption $v): string { + if ($v->getUpperLimit() === null) { + return sprintf( + $this->lng->txt('equal'), + $v->getLowerLimit() + ); + } + + return sprintf( + $this->lng->txt('between'), + $v->getLowerLimit(), + $v->getUpperLimit() + ); + } + ); + return array_map( + fn(NumericInput $v): NumericInput => $v->withRequired(true), + $inputs + ); + } + + public function getEditPointsSectionConstraint(): ?Constraint + { + return null; + } + + public function getBuildGapTransformation(Gap $gap): Transformation + { + $data = $gap->getData(); + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withData( + $data->withAnswerOptions( + $data->getAnswerOptions()->withAnswerOptions([ + $data->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) + ->withLowerLimit($vs['lower_limit']) + ->withUpperLimit($vs['upper_limit']) + ]) + )->withStepSize($vs['step_size']) + ) + ); + } + + public function getAnswerInput(): \ilFormPropertyGUI + { + ; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php new file mode 100644 index 000000000000..655760c8418c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php @@ -0,0 +1,137 @@ +answer_option_id; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function withPosition(int $position): self + { + $clone = clone $this; + $clone->position = $position; + return $clone; + } + + public function getTextValue(): string + { + return $this->text_value; + } + + public function withTextValue(string $text_value): self + { + $clone = clone $this; + $clone->text_value = $text_value; + return $clone; + } + + public function getLowerLimit(): ?float + { + return $this->lower_limit; + } + + public function withLowerLimit(float $lower_limit): self + { + $clone = clone $this; + $clone->lower_limit = $lower_limit; + return $clone; + } + + public function getUpperLimit(): ?float + { + return $this->upper_limit; + } + + public function withUpperLimit(?float $upper_limit): self + { + $clone = clone $this; + $clone->upper_limit = $upper_limit; + return $clone; + } + + public function getAvailablePoints(): ?float + { + return $this->available_points; + } + + public function withAvailablePoints(?float $available_points): self + { + $clone = clone $this; + $clone->available_points = $available_points; + return $clone; + } + + public function buildArrayForHiddenInput(): array + { + $values = [ + self::FORM_KEY_ID => $this->getAnswerOptionId()->toString(), + self::FORM_KEY_POSITION => $this->getPosition(), + self::FORM_KEY_TEXT_VALUE => $this->getTextValue() + ]; + + if ($this->getUpperLimit() !== null) { + $values[self::FORM_KEY_LOWER_LIMIT] = (string) $this->getLowerLimit(); + } + + if ($this->getUpperLimit() !== null) { + $values[self::FORM_KEY_UPPER_LIMIT] = (string) $this->getUpperLimit(); + } + + if ($this->getUpperLimit() !== null) { + $values[self::FORM_KEY_AVAILABLE_POINTS] = (string) $this->getAvailablePoints(); + } + + return $values; + } + + public function toPersistence(ManipulateQuery $query): ManipulateQuery + { + + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php new file mode 100644 index 000000000000..73622d6f9bd4 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php @@ -0,0 +1,223 @@ +answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); + } + + public function getAnswerOptionForPositionOrNew(int $position): AnswerOption + { + return $this->answer_options[$position] + ?? $this->factory->getDefaultAnswerOptionForPosition($position); + } + + public function getTagsArrayFromAnswerOptions(): array + { + return array_map( + fn(AnswerOption $v): string => $v->getTextValue(), + $this->answer_options + ); + } + + public function getAnswerOptionsAwardingPoints(): array + { + return $this->answer_options_awarding_points; + } + + public function withAnswerOptionsAwardingPoints(array $options): self + { + $clone = clone $this; + $clone->answer_options_awarding_points = array_reduce( + $options, + function (array $c, string $v): array { + $answer_option = $this->retrieveAnswerOptionByTextValue($v); + if ($answer_option !== null) { + $c[] = $answer_option; + } + return $c; + }, + [] + ); + return $clone; + } + + public function withAnswerOptions(array $answer_options): self + { + $clone = clone $this; + $clone->answer_options = $answer_options; + return $clone; + } + + public function withValuesFromHiddenInputValue( + ?string $value + ): self { + if ($value === null + || !is_array( + ($decoded_value = json_decode($value, true)) + ) + ) { + return $this; + } + + $clone = clone $this; + $clone->answer_options = array_map( + fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( + $vs[AnswerOption::FORM_KEY_ID], + $vs[AnswerOption::FORM_KEY_POSITION], + $vs[AnswerOption::FORM_KEY_TEXT_VALUE], + $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_UPPER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_AVAILABLE_POINTS] ?? null + ), + $decoded_value['answer_options'] ?? [] + ); + + $answer_inputs_awarding_points = $decoded_value['answer_options_awarding_points'] ?? []; + $clone->answer_options_awarding_points = array_filter( + $clone->answer_options, + fn(AnswerOption $v): bool => in_array( + $v->getAnswerOptionId()->toString(), + $answer_inputs_awarding_points + ) + ); + return $clone; + } + + public function withAnswerOptionsFromTags( + array $tags + ): self { + $clone = clone $this; + $position = 0; + $clone->answer_options = array_map( + function (string $v) use (&$position): AnswerOption { + return $this->buildAnswerOptionFromTag( + $position++, + $v + ); + }, + $tags + ); + return $clone; + } + + public function withAnswerOptionsWithAddedPointsFromForm( + Refinery $refinery, + array $values_from_form + ): self { + $clone = clone $this; + $clone->answer_options = array_map( + function (AnswerOption $v) use ($refinery, $values_from_form): AnswerOption { + $answer_option_id = $v->getAnswerOptionId()->toString(); + if (array_key_exists($answer_option_id, $values_from_form)) { + return $v->withAvailablePoints( + $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always(null) + ])->transform($values_from_form[$answer_option_id]) + ); + } + + return $v; + }, + $this->answer_options + ); + $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($this->answer_options); + return $clone; + } + + public function buildHiddenInputValue(): string + { + return json_encode([ + 'answer_options' => array_map( + fn(AnswerOption $v): array => $v->buildArrayForHiddenInput(), + $this->answer_options + ), + 'answer_options_awarding_points' => array_map( + fn(AnswerOption $v): string => $v->getAnswerOptionId()->toString(), + $this->answer_options_awarding_points + ) + ]); + } + + public function getEditPointsInputs( + FieldFactory $ff, + \Closure $build_label, + ?array $answer_options_awarding_points = null + ): array { + return array_reduce( + $answer_options_awarding_points ?? $this->answer_options, + function (array $c, AnswerOption $v) use ($ff, $build_label): array { + $c[$v->getAnswerOptionId()->toString()] = $ff->numeric($build_label($v)) + ->withStepSize(0.0001) + ->withValue($v->getAvailablePoints()); + return $c; + }, + [] + ); + } + + public function toPersistence(ManipulateQuery $query): ManipulateQuery + { + + } + + private function buildAnswerOptionsAwardingPointsFromAnswerOptions( + array $answer_options + ): array { + return array_filter( + $answer_options, + fn(AnswerOption $v): bool => $v->getAvailablePoints() > 0.0 + ); + } + + private function buildAnswerOptionFromTag( + int $position, + string $text_value + ): AnswerOption { + $answer_option = $this->retrieveAnswerOptionByTextValue($text_value) + ?? $this->factory->getDefaultAnswerOptionForPosition($position); + return $answer_option + ->withPosition($position) + ->withTextValue($text_value); + } + + private function retrieveAnswerOptionByTextValue( + string $value + ): ?AnswerOption { + $filtered_array = array_filter( + $this->answer_options, + fn(AnswerOption $v): bool => $v->getTextValue() === $value + ); + return array_shift($filtered_array) ?? null; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php new file mode 100644 index 000000000000..d3a7b4ead468 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php @@ -0,0 +1,86 @@ +uuid_factory->uuid4(), + $position + ); + } + + public function buildAnswerOption( + string $answer_option_id, + int $position, + string $text_value, + ?string $lower_limit, + ?string $upper_limit, + ?string $points + ): AnswerOption { + return new AnswerOption( + $this->uuid_factory->fromString($answer_option_id), + $position, + $text_value, + $this->convertToFloatOrNull($lower_limit), + $this->convertToFloatOrNull($upper_limit), + $this->convertToFloatOrNull($points) + ); + } + + public function fromDatabase( + array $data + ): Gaps { + + } + + private function convertToFloatOrNull(?string $value): ?float + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->float(), + $this->refinery->always(null) + ])->transform($value); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php new file mode 100644 index 000000000000..b4fe36d1a9d6 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php @@ -0,0 +1,267 @@ + $answer_options + */ + public function __construct( + private readonly Uuid $answer_input_id, + private AnswerOptions $answer_options, + private ?int $max_chars = null, + private ?float $step_size = null, + private ?TextMatchingOptions $text_matching_method = null, + private ?int $min_autocomplete = null, + private ?bool $shuffle_answer_options = null + ) { + } + + public function getAnswerInputId(): Uuid + { + return $this->answer_input_id; + } + + public function getMaxChars(): ?int + { + return $this->max_chars; + } + + public function withMaxChars(?int $max_chars): self + { + $clone = clone $this; + $clone->max_chars = $max_chars; + return $clone; + } + + public function getStepSize(): ?float + { + return $this->step_size; + } + + public function withStepSize(float $step_size): self + { + $clone = clone $this; + $clone->step_size = $step_size; + return $clone; + } + + public function getTextMatchingMethod(): ?TextMatchingOptions + { + return $this->text_matching_method; + } + + public function withTextMatchingMethod(TextMatchingOptions $matching_method): self + { + $clone = clone $this; + $clone->text_matching_method = $matching_method; + return $clone; + } + + public function getMinAutocomplete(): ?int + { + return $this->min_autocomplete; + } + + public function withMinAutocomplete(int $min_autocomplete): self + { + $clone = clone $this; + $clone->min_autocomplete = $min_autocomplete; + return $clone; + } + + public function getShuffleAnswerOptions(): ?bool + { + return $this->shuffle_answer_options; + } + + public function withShuffleAnswerOptions(bool $shuffle_answer_options): self + { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + + public function getAnswerOptions(): AnswerOptions + { + return $this->answer_options; + } + + public function withAnswerOptions(AnswerOptions $answer_options): self + { + $clone = clone $this; + $clone->answer_options = $answer_options; + return $clone; + } + + public function getHiddenInput(FieldFactory $ff): Group + { + $inputs = []; + if ($this->max_chars !== null) { + $inputs[self::FORM_KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()) + ->withDedicatedName(self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId()); + } + + if ($this->step_size !== null) { + $inputs[self::FORM_KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()) + ->withDedicatedName(self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId()); + } + + if ($this->text_matching_method !== null) { + $inputs[self::FORM_KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value) + ->withDedicatedName(self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId()); + } + + if ($this->min_autocomplete !== null) { + $inputs[self::FORM_KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()) + ->withDedicatedName(self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId()); + } + + if ($this->shuffle_answer_options !== null) { + $inputs[self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0') + ->withDedicatedName(self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + } + + $inputs[self::FORM_KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()) + ->withDedicatedName(self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + + return $ff->group($inputs); + } + + public function withValuesFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): Data { + $clone = clone $this; + $clone->max_chars = $this->retrieveMaxCharsFromPost($refinery, $post_wrapper, $form_input_path); + $clone->step_size = $this->retrieveStepSizeFromPost($refinery, $post_wrapper, $form_input_path); + $clone->text_matching_method = $this->retrieveTextMatchingMethodFromPost($refinery, $post_wrapper, $form_input_path); + $clone->min_autocomplete = $this->retrieveMinAutocompleteFromPost($refinery, $post_wrapper, $form_input_path); + $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromPost($refinery, $post_wrapper, $form_input_path); + $clone->answer_options = $this->retrieveAnswerOptionsFromPost($refinery, $post_wrapper, $form_input_path); + return $clone; + } + + private function retrieveMaxCharsFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): ?int { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMaxChars()) + ]) + ); + } + + private function retrieveStepSizeFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): ?float { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always($this->getStepSize()) + ]) + ); + } + + private function retrieveTextMatchingMethodFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): ?TextMatchingOptions { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), + $refinery->custom()->transformation( + fn(?string $v): ?TextMatchingOptions => $v !== null + ? TextMatchingOptions::tryFrom($v) + : $this->getTextMatchingMethod() + ) + ); + } + + private function retrieveMinAutocompleteFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): ?int { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMinAutocomplete()) + ]) + ); + } + + private function retrieveShuffleAnswerOptionsFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): ?bool { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->bool(), + $refinery->always($this->getShuffleAnswerOptions()) + ]) + ); + } + + private function retrieveAnswerOptionsFromPost( + Refinery $refinery, + ArrayBasedRequestWrapper $post_wrapper, + string $form_input_path + ): AnswerOptions { + return $post_wrapper->retrieve( + $form_input_path . '/' . self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + $refinery->custom()->transformation( + fn(?string $v): AnswerOptions => $this->answer_options->withValuesFromHiddenInputValue($v) + ) + ); + } + + private function getShortenedAnswerInputId(): string + { + return mb_substr($this->answer_input_id->toString(), 0, 4); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php new file mode 100644 index 000000000000..220902079652 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -0,0 +1,108 @@ +ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withRequired(true) + ->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'shuffle_answer_options' => $ff->checkbox( + $this->lng->txt('shuffle_answers') + )->withValue($data?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) + ]; + } + + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return null; + } + + public function getEditPointsInputs(AnswerOptions $answer_options): array + { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue() + ); + } + + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + public function getBuildGapTransformation(Gap $gap): Transformation + { + $data = $gap->getData(); + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withData( + $data->withAnswerOptions( + $data->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + ) + ) + ); + } + + public function getAnswerInput(): \ilFormPropertyGUI + { + ; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php new file mode 100644 index 000000000000..1e0bcb93c136 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -0,0 +1,122 @@ +ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withRequired(true) + ->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'matching_method' => $ff->select( + $this->lng->txt('matching_method'), + TextMatchingOptions::buildOptionsList($this->lng) + )->withRequired(true) + ->withValue($data->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), + 'max_chars' => $ff->numeric( + $this->lng->txt('max_chars'), + )->withValue($data->getMaxChars()) + ]; + } + + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + fn(array $vs): bool => array_filter( + $vs['answer_options'], + fn(string $v): bool => in_array($v, $vs['answer_options']) + ) !== [], + $this->lng->txt('answer_options_must_be_unique') + ); + } + + public function getEditPointsInputs(AnswerOptions $answer_options): array + { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue() + ); + } + + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + public function getBuildGapTransformation(Gap $gap): Transformation + { + $data = $gap->getData(); + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withData( + $data->withMaxChars($vs['max_chars']) + ->withTextMatchingMethod(TextMatchingOptions::tryFrom($vs['matching_method']) ?? self::DEFAULT_TECT_MATCHING_METHOD) + ->withAnswerOptions( + $data->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + ) + ) + ); + } + + public function getAnswerInput(): \ilFormPropertyGUI + { + ; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php new file mode 100644 index 000000000000..181649f153ca --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -0,0 +1,56 @@ +getData(); + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withData( + $data->withAnswerOptions( + $data->getAnswerOptions() + ->withAnswerOptionsWithAddedPointsFromForm($this->refinery, $vs) + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php new file mode 100755 index 000000000000..99ab4d033dc2 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php @@ -0,0 +1,97 @@ + + * @ilCtrl_isCalledBy ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI: ilObjQuestionsGUI + */ +class UploadAnswerOptionsGUI extends AbstractCtrlAwareUploadHandler +{ + protected function getUploadResult(): HandlerResult + { + $this->upload->process(); + + $result_array = $this->upload->getResults(); + $result = end($result_array); + + if (!($result instanceof UploadResult) || !$result->isOK()) { + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_FAILED, + '', + $result->getStatus()->getMessage() + ); + } + + $content = base64_encode(file_get_contents($result->getPath())); + unlink($result->getPath()); + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $content, + 'file upload OK' + ); + } + + protected function getRemoveResult(string $identifier): HandlerResult + { + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $identifier, + 'We just don\'t do anything here.' + ); + } + + public function getInfoResult(string $identifier): ?FileInfoResult + { + return new BasicFileInfoResult( + $this->getFileIdentifierParameterName(), + $identifier, + 'unknown', + 0, + 'unknown' + ); + } + + /** + * @return \ILIAS\FileUpload\Handler\BasicFileInfoResult[] + */ + public function getInfoForExistingFiles(array $file_ids): array + { + $info_results = []; + foreach ($file_ids as $identifier) { + $info_results[] = $this->getInfoResult($identifier); + } + + return $info_results; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php new file mode 100644 index 000000000000..cefb14c14553 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -0,0 +1,280 @@ + $this->processBasicEditingForm($url_builder, $step_token), + self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm($url_builder, $step_token), + self::STEP_SET_POINTS => $this->processAnswerOptionsForm($url_builder, $step_token), + self::STEP_SAVE => $this->processAssignPointsForm($url_builder, $step_token), + default => [$this->buildBasicEditingForm($url_builder, $step_token)] + }; + } + + public function edit( + URLBuilder $url_builder, + URLBuilderToken $step_token, + string $step + ): array|ManipulateQuery { + + } + + public function other( + URLBuilder $url_builder, + URLBuilderToken $step_token, + string $step + ): array|ManipulateQuery { + + } + + private function buildBasicEditingForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): StandardForm { + return $this->ui_factory->input()->container()->form()->standard( + $url_builder->withParameter($step_token, self::STEP_SET_GAP_TYPES)->buildURI()->__toString(), + [ + self::MAIN_SECTION_NAME => $this->type->getProperties()->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory + ) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processBasicEditingForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + $form = $this->buildBasicEditingForm($url_builder, $step_token)->withRequest($this->http->request()); + $data = $form->getData(); + if ($data === null) { + return [$form]; + } + + return $this->buildOutputWithPanel( + $this->buildGapTypesForm( + $url_builder, + $step_token, + $data[self::MAIN_SECTION_NAME] + ), + $data[self::MAIN_SECTION_NAME] + ); + } + + private function buildGapTypesForm( + URLBuilder $url_builder, + URLBuilderToken $step_token, + Properties $properties + ): StandardForm { + $ff = $this->ui_factory->input()->field(); + return $this->ui_factory->input()->container()->form()->standard( + $url_builder->withParameter($step_token, self::STEP_SET_ANSWER_OPTIONS)->buildURI()->__toString(), + [ + self::MAIN_SECTION_NAME => $properties->getGaps()->buildGapsTypeInputs( + $this->lng, + $ff, + $this->refinery, + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng) + ), + self::PROPERTIES_SECTION_NAME => $properties + ->withClozeText($properties->getClozeText()) + ->buildBasicEditingInputsHidden($ff) + ->withDedicatedName(self::PROPERTIES_SECTION_NAME) + + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processGapTypesForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + $properties = $this->retrievePropertiesFromPost(); + $form = $this->buildGapTypesForm( + $url_builder, + $step_token, + $properties + )->withRequest($this->http->request()); + + $data = $form->getData(); + return $this->buildOutputWithPanel( + $data === null + ? $form + : $this->buildAnswerOptionsForm( + $url_builder, + $step_token, + $properties->withGaps($data[self::MAIN_SECTION_NAME]) + ), + $properties + ); + } + + private function buildAnswerOptionsForm( + URLBuilder $url_builder, + URLBuilderToken $step_token, + Properties $properties + ): StandardForm { + $ff = $this->ui_factory->input()->field(); + return $this->ui_factory->input()->container()->form()->standard( + $url_builder->withParameter($step_token, self::STEP_SET_POINTS)->buildURI()->__toString(), + [ + self::MAIN_SECTION_NAME => $properties->getGaps()->buildAnswerOptionsInputs($this->lng, $ff, $this->refinery), + self::PROPERTIES_SECTION_NAME => $properties->buildBasicEditingInputsHidden($ff) + ->withDedicatedName(self::PROPERTIES_SECTION_NAME) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processAnswerOptionsForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + $properties = $this->retrievePropertiesFromPost(); + $form = $this->buildAnswerOptionsForm( + $url_builder, + $step_token, + $properties + )->withRequest($this->http->request()); + + $data = $form->getData(); + return $this->buildOutputWithPanel( + $data === null + ? $form + : $this->buildAssignPointsForm( + $url_builder, + $step_token, + $properties->withGaps($data[self::MAIN_SECTION_NAME]) + ), + $properties + ); + } + + private function buildAssignPointsForm( + URLBuilder $url_builder, + URLBuilderToken $step_token, + Properties $properties + ): StandardForm { + $ff = $this->ui_factory->input()->field(); + return $this->ui_factory->input()->container()->form()->standard( + $url_builder->withParameter($step_token, self::STEP_SAVE)->buildURI()->__toString(), + [ + self::MAIN_SECTION_NAME => $properties->getGaps()->buildPointInputs($this->lng, $ff, $this->refinery), + self::PROPERTIES_SECTION_NAME => $properties->buildBasicEditingInputsHidden($ff) + ->withDedicatedName(self::PROPERTIES_SECTION_NAME) + ] + )->withSubmitLabel($this->lng->txt('save')); + } + + private function processAssignPointsForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array|ManipulateQuery { + $properties = $this->retrievePropertiesFromPost(); + $form = $this->buildAssignPointsForm( + $url_builder, + $step_token, + $properties + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + return $this->buildOutputWithPanel($form, $properties); + } + + return $this->save($properties->withGaps($data[self::MAIN_SECTION_NAME])); + } + + private function buildOutputWithPanel( + StandardForm $form, + Properties $properties + ): array { + return [ + $this->ui_factory->panel()->standard( + $this->lng->txt('cloze_text'), + $this->ui_factory->legacy()->content( + $properties->getClozeText()->getRenderedMarkdown( + $properties->getGaps() + ) + ) + ), + $form + ]; + } + + private function save(Properties $properties): ManipulateQuery + { + return $properties->toPersistence(); + } + + private function retrievePropertiesFromPost(): Properties + { + + return $this->type->getProperties()->withValuesFromPost( + $this->refinery, + $this->http->wrapper()->post(), + 'form/' . self::PROPERTIES_SECTION_NAME + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php new file mode 100644 index 000000000000..135596235c55 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php @@ -0,0 +1,39 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function getQuestionsForId(int $id): Question|null + { + return $this->repository->getForQuestionId($id); + } + + /** + * + * @param array $ids + * @return \Generator + */ + public function getQuestionsForIds(array $ids): \Generator + { + yield from $this->repository->getForQuestionIds($ids); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php new file mode 100755 index 000000000000..e23e4217a684 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php @@ -0,0 +1,65 @@ + + */ +class ArrayBasedRequestWrapper implements RequestWrapper +{ + /** + * GetRequestWrapper constructor. + * @param mixed[] $raw_values + */ + public function __construct(private array $raw_values) + { + } + + + /** + * @inheritDoc + */ + public function retrieve(string $key, Transformation $transformation) + { + return $transformation->transform($this->raw_values[$key] ?? null); + } + + + /** + * @inheritDoc + */ + public function has(string $key): bool + { + return isset($this->raw_values[$key]); + } + + /** + * @inheritDoc + */ + public function keys(): array + { + return array_keys($this->raw_values); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php new file mode 100644 index 000000000000..14a16b4c6037 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php @@ -0,0 +1,555 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function withEditable(Editability $editability): self + { + $clone = clone $this; + $clone->editability = $editability; + return $clone; + } + + public function withOrderingEnabled(bool $enable): self + { + $clone = clone $this; + $clone->ordering_enabled = $enable; + return $clone; + } + + public function view( + \ilToolbarGUI $toolbar, + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_QUESTION => $this->createQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + self::CMD_EDIT_QUESTION => $this->editQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + default => $this->showTable( + $toolbar, + $url_builder, + $action_token, + $question_id_token + ) + }; + } + + public function forwardPageCmds( + \ilGlobalTemplateInterface $tpl, + URI $base_uri, + ): void { + [ + 0 => $url_builder, + 1 => $action_token, + 3 => $question_id_token, + 4 => $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + $page_id = $this->retrievePageId($page_id_token); + $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); + + $tpl->setContent( + $this->ctrl->forwardCommand( + new \QstsQuestionPageGUI( + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) + ->withParameter($question_id_token, $question_id->toString()) + ->buildURI(), + $page_id + ) + ) + ); + } + + public function createAnswerForm( + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token, + $type_hash_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + $question_id = $this->retrieveQuestionId($question_id_token); + $url_builder_with_params = $url_builder + ->withParameter($question_id_token, $question_id->toString()) + ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) + ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); + + $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); + if ($answer_form_type_class_hash !== '') { + return $this->forwardCreateAnswerFormCmd( + $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), + $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), + $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), + $step_token + ); + } + + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + $url_builder_with_params, + $action_token, + $step_token, + $type_hash_token + ), + default => [$this->buildCreateAnswerForm( + $url_builder_with_params, + $action_token + )] + }; + } + + public function editAnswerForm( + URI $base_uri + ): array { + [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], + default => [$this->buildCreateAnswerForm($url_builder, $action_token)] + }; + } + + private function createQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $create = (new QuestionImplementation())->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->create( + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($create)) { + return $create; + } + + $this->questions_repository->store($create); + return $this->buildEditStartView( + $url_builder->withParameter($question_id_token, $create->getId()), + $step_token, + $page_id_token, + $create + ); + + } + + private function editQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + + $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); + + $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), + $step_token, + $page_id_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($edit)) { + return $edit; + } + + $this->questions_repository->store($edit); + return $this->buildEditStartView( + $url_builder_with_row_id, + $step_token, + $page_id_token, + $edit + ); + } + + private function showTable( + \ilToolbarGUI $toolbar, + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): array { + $toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('create'), + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() + ) + ); + + $table = new QuestionsTable( + $this->ui_factory, + $this->ui_services, + $this->lng, + $this->answer_form_types_factory, + $this->questions_repository, + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $action_token, + $question_id_token + ); + return [ + $table->getFilter($url_builder->buildURI()->__toString()), + $table->getTable()->withRequest($this->http->request()) + + ]; + } + + private function processCreateAnswerForm( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $type_hash_token + ): array { + $form = $this->buildCreateAnswerForm( + $url_builder, + $action_token + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null || $data['form_type'] === null) { + return [$form]; + } + + return $this->forwardCreateAnswerFormCmd( + $data['form_type'], + $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), + $step_token + ); + } + + private function forwardCreateAnswerFormCmd( + Definition $type, + TypeGenericProperties $type_generic_properties, + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + return $type->getEditView()->create( + $type->buildProperties($type_generic_properties, []), + $url_builder, + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + } + + private function acquireURLBuilderAndParameters(URI $base_uri): array + { + return (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_PAGE_ID, + self::TOKEN_TYPE_HASH + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function retrieveQuestionId( + URLBuilderToken $question_id_token + ): ?Uuid { + return $this->http->wrapper()->query()->retrieve( + $question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): Uuid => $this->uuid_factory->fromString($v) + ), + $this->refinery->always(null) + ]) + ); + } + + public function retrievePageId( + URLBuilderToken $page_id_token + ): ?int { + return $this->http->wrapper()->query()->retrieve( + $page_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->always(null) + ]) + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function initializeEditMode( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): void { + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::MODE_ENABLED, + true + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::QUESTIONLIST_ENTRY, + $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CLOSE_MODE_INFO, + $url_builder->buildURI() + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CREATE_QUESTION, + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() + ); + } + + private function setParametersForQuestionCmds( + URLBuilderToken $question_id_token, + string $question_id, + URLBuilderToken $page_id_token, + int $page_id + ): void { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $question_id_token->getName(), + $question_id + ); + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $page_id_token->getName(), + $page_id + ); + } + + private function buildQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): LegacySlate { + return $this->ui_factory->mainControls()->slate()->legacy( + $this->lng->txt('mainbar_button_label_questionlist'), + $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), + $this->ui_factory->legacy()->content( + $this->ui_renderer->render( + $this->ui_factory->panel()->secondary()->listing( + $this->lng->txt('mainbar_button_label_questionlist'), + [ + $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) + ] + ) + ) + ) + ); + } + + private function buildItemGroupForQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): ItemGroup { + return $this->ui_factory->item()->group( + '', + array_map( + fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( + $v->toEditLink( + $this->ui_factory->link(), + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $question_id_token + ) + ), + iterator_to_array($this->questions_repository->getAllQuestions()) + ) + ); + } + + private function buildEditStartView( + URLBuilder $url_builder, + URLBuilderToken $step_token, + URLBuilderToken $page_id_token, + QuestionImplementation $question + ): array { + return $question->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder, + $step_token, + $page_id_token, + '' + ); + } + + private function buildCreateAnswerForm( + URLBuilder $url_builder + ): StandardForm { + $if = $this->ui_factory->input(); + return $if->container()->form()->standard( + $url_builder->buildURI()->__toString(), + [ + 'form_type' => $if->field()->section( + [ + $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect() + )->withRequired(true) + ], + $this->lng->txt('create_answer_form') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) + ) + ) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function checkCapabilities(array $capabilities): void + { + foreach ($capabilities as $capability) { + if (!$this->questions_repository->capabilityExists($capability)) { + throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); + } + } + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php new file mode 100644 index 000000000000..14a16b4c6037 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php @@ -0,0 +1,555 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function withEditable(Editability $editability): self + { + $clone = clone $this; + $clone->editability = $editability; + return $clone; + } + + public function withOrderingEnabled(bool $enable): self + { + $clone = clone $this; + $clone->ordering_enabled = $enable; + return $clone; + } + + public function view( + \ilToolbarGUI $toolbar, + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_QUESTION => $this->createQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + self::CMD_EDIT_QUESTION => $this->editQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + default => $this->showTable( + $toolbar, + $url_builder, + $action_token, + $question_id_token + ) + }; + } + + public function forwardPageCmds( + \ilGlobalTemplateInterface $tpl, + URI $base_uri, + ): void { + [ + 0 => $url_builder, + 1 => $action_token, + 3 => $question_id_token, + 4 => $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + $page_id = $this->retrievePageId($page_id_token); + $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); + + $tpl->setContent( + $this->ctrl->forwardCommand( + new \QstsQuestionPageGUI( + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) + ->withParameter($question_id_token, $question_id->toString()) + ->buildURI(), + $page_id + ) + ) + ); + } + + public function createAnswerForm( + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token, + $type_hash_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + $question_id = $this->retrieveQuestionId($question_id_token); + $url_builder_with_params = $url_builder + ->withParameter($question_id_token, $question_id->toString()) + ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) + ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); + + $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); + if ($answer_form_type_class_hash !== '') { + return $this->forwardCreateAnswerFormCmd( + $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), + $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), + $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), + $step_token + ); + } + + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + $url_builder_with_params, + $action_token, + $step_token, + $type_hash_token + ), + default => [$this->buildCreateAnswerForm( + $url_builder_with_params, + $action_token + )] + }; + } + + public function editAnswerForm( + URI $base_uri + ): array { + [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], + default => [$this->buildCreateAnswerForm($url_builder, $action_token)] + }; + } + + private function createQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $create = (new QuestionImplementation())->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->create( + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($create)) { + return $create; + } + + $this->questions_repository->store($create); + return $this->buildEditStartView( + $url_builder->withParameter($question_id_token, $create->getId()), + $step_token, + $page_id_token, + $create + ); + + } + + private function editQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + + $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); + + $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), + $step_token, + $page_id_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($edit)) { + return $edit; + } + + $this->questions_repository->store($edit); + return $this->buildEditStartView( + $url_builder_with_row_id, + $step_token, + $page_id_token, + $edit + ); + } + + private function showTable( + \ilToolbarGUI $toolbar, + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): array { + $toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('create'), + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() + ) + ); + + $table = new QuestionsTable( + $this->ui_factory, + $this->ui_services, + $this->lng, + $this->answer_form_types_factory, + $this->questions_repository, + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $action_token, + $question_id_token + ); + return [ + $table->getFilter($url_builder->buildURI()->__toString()), + $table->getTable()->withRequest($this->http->request()) + + ]; + } + + private function processCreateAnswerForm( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $type_hash_token + ): array { + $form = $this->buildCreateAnswerForm( + $url_builder, + $action_token + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null || $data['form_type'] === null) { + return [$form]; + } + + return $this->forwardCreateAnswerFormCmd( + $data['form_type'], + $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), + $step_token + ); + } + + private function forwardCreateAnswerFormCmd( + Definition $type, + TypeGenericProperties $type_generic_properties, + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + return $type->getEditView()->create( + $type->buildProperties($type_generic_properties, []), + $url_builder, + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + } + + private function acquireURLBuilderAndParameters(URI $base_uri): array + { + return (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_PAGE_ID, + self::TOKEN_TYPE_HASH + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function retrieveQuestionId( + URLBuilderToken $question_id_token + ): ?Uuid { + return $this->http->wrapper()->query()->retrieve( + $question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): Uuid => $this->uuid_factory->fromString($v) + ), + $this->refinery->always(null) + ]) + ); + } + + public function retrievePageId( + URLBuilderToken $page_id_token + ): ?int { + return $this->http->wrapper()->query()->retrieve( + $page_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->always(null) + ]) + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function initializeEditMode( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): void { + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::MODE_ENABLED, + true + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::QUESTIONLIST_ENTRY, + $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CLOSE_MODE_INFO, + $url_builder->buildURI() + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CREATE_QUESTION, + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() + ); + } + + private function setParametersForQuestionCmds( + URLBuilderToken $question_id_token, + string $question_id, + URLBuilderToken $page_id_token, + int $page_id + ): void { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $question_id_token->getName(), + $question_id + ); + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $page_id_token->getName(), + $page_id + ); + } + + private function buildQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): LegacySlate { + return $this->ui_factory->mainControls()->slate()->legacy( + $this->lng->txt('mainbar_button_label_questionlist'), + $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), + $this->ui_factory->legacy()->content( + $this->ui_renderer->render( + $this->ui_factory->panel()->secondary()->listing( + $this->lng->txt('mainbar_button_label_questionlist'), + [ + $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) + ] + ) + ) + ) + ); + } + + private function buildItemGroupForQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): ItemGroup { + return $this->ui_factory->item()->group( + '', + array_map( + fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( + $v->toEditLink( + $this->ui_factory->link(), + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $question_id_token + ) + ), + iterator_to_array($this->questions_repository->getAllQuestions()) + ) + ); + } + + private function buildEditStartView( + URLBuilder $url_builder, + URLBuilderToken $step_token, + URLBuilderToken $page_id_token, + QuestionImplementation $question + ): array { + return $question->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder, + $step_token, + $page_id_token, + '' + ); + } + + private function buildCreateAnswerForm( + URLBuilder $url_builder + ): StandardForm { + $if = $this->ui_factory->input(); + return $if->container()->form()->standard( + $url_builder->buildURI()->__toString(), + [ + 'form_type' => $if->field()->section( + [ + $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect() + )->withRequired(true) + ], + $this->lng->txt('create_answer_form') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) + ) + ) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function checkCapabilities(array $capabilities): void + { + foreach ($capabilities as $capability) { + if (!$this->questions_repository->capabilityExists($capability)) { + throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); + } + } + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php new file mode 100644 index 000000000000..14a16b4c6037 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php @@ -0,0 +1,555 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function withEditable(Editability $editability): self + { + $clone = clone $this; + $clone->editability = $editability; + return $clone; + } + + public function withOrderingEnabled(bool $enable): self + { + $clone = clone $this; + $clone->ordering_enabled = $enable; + return $clone; + } + + public function view( + \ilToolbarGUI $toolbar, + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_QUESTION => $this->createQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + self::CMD_EDIT_QUESTION => $this->editQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + default => $this->showTable( + $toolbar, + $url_builder, + $action_token, + $question_id_token + ) + }; + } + + public function forwardPageCmds( + \ilGlobalTemplateInterface $tpl, + URI $base_uri, + ): void { + [ + 0 => $url_builder, + 1 => $action_token, + 3 => $question_id_token, + 4 => $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + $page_id = $this->retrievePageId($page_id_token); + $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); + + $tpl->setContent( + $this->ctrl->forwardCommand( + new \QstsQuestionPageGUI( + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) + ->withParameter($question_id_token, $question_id->toString()) + ->buildURI(), + $page_id + ) + ) + ); + } + + public function createAnswerForm( + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token, + $type_hash_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + $question_id = $this->retrieveQuestionId($question_id_token); + $url_builder_with_params = $url_builder + ->withParameter($question_id_token, $question_id->toString()) + ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) + ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); + + $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); + if ($answer_form_type_class_hash !== '') { + return $this->forwardCreateAnswerFormCmd( + $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), + $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), + $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), + $step_token + ); + } + + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + $url_builder_with_params, + $action_token, + $step_token, + $type_hash_token + ), + default => [$this->buildCreateAnswerForm( + $url_builder_with_params, + $action_token + )] + }; + } + + public function editAnswerForm( + URI $base_uri + ): array { + [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], + default => [$this->buildCreateAnswerForm($url_builder, $action_token)] + }; + } + + private function createQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $create = (new QuestionImplementation())->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->create( + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($create)) { + return $create; + } + + $this->questions_repository->store($create); + return $this->buildEditStartView( + $url_builder->withParameter($question_id_token, $create->getId()), + $step_token, + $page_id_token, + $create + ); + + } + + private function editQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + + $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); + + $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), + $step_token, + $page_id_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($edit)) { + return $edit; + } + + $this->questions_repository->store($edit); + return $this->buildEditStartView( + $url_builder_with_row_id, + $step_token, + $page_id_token, + $edit + ); + } + + private function showTable( + \ilToolbarGUI $toolbar, + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): array { + $toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('create'), + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() + ) + ); + + $table = new QuestionsTable( + $this->ui_factory, + $this->ui_services, + $this->lng, + $this->answer_form_types_factory, + $this->questions_repository, + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $action_token, + $question_id_token + ); + return [ + $table->getFilter($url_builder->buildURI()->__toString()), + $table->getTable()->withRequest($this->http->request()) + + ]; + } + + private function processCreateAnswerForm( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $type_hash_token + ): array { + $form = $this->buildCreateAnswerForm( + $url_builder, + $action_token + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null || $data['form_type'] === null) { + return [$form]; + } + + return $this->forwardCreateAnswerFormCmd( + $data['form_type'], + $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), + $step_token + ); + } + + private function forwardCreateAnswerFormCmd( + Definition $type, + TypeGenericProperties $type_generic_properties, + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + return $type->getEditView()->create( + $type->buildProperties($type_generic_properties, []), + $url_builder, + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + } + + private function acquireURLBuilderAndParameters(URI $base_uri): array + { + return (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_PAGE_ID, + self::TOKEN_TYPE_HASH + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function retrieveQuestionId( + URLBuilderToken $question_id_token + ): ?Uuid { + return $this->http->wrapper()->query()->retrieve( + $question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): Uuid => $this->uuid_factory->fromString($v) + ), + $this->refinery->always(null) + ]) + ); + } + + public function retrievePageId( + URLBuilderToken $page_id_token + ): ?int { + return $this->http->wrapper()->query()->retrieve( + $page_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->always(null) + ]) + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function initializeEditMode( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): void { + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::MODE_ENABLED, + true + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::QUESTIONLIST_ENTRY, + $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CLOSE_MODE_INFO, + $url_builder->buildURI() + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CREATE_QUESTION, + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() + ); + } + + private function setParametersForQuestionCmds( + URLBuilderToken $question_id_token, + string $question_id, + URLBuilderToken $page_id_token, + int $page_id + ): void { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $question_id_token->getName(), + $question_id + ); + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $page_id_token->getName(), + $page_id + ); + } + + private function buildQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): LegacySlate { + return $this->ui_factory->mainControls()->slate()->legacy( + $this->lng->txt('mainbar_button_label_questionlist'), + $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), + $this->ui_factory->legacy()->content( + $this->ui_renderer->render( + $this->ui_factory->panel()->secondary()->listing( + $this->lng->txt('mainbar_button_label_questionlist'), + [ + $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) + ] + ) + ) + ) + ); + } + + private function buildItemGroupForQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): ItemGroup { + return $this->ui_factory->item()->group( + '', + array_map( + fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( + $v->toEditLink( + $this->ui_factory->link(), + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $question_id_token + ) + ), + iterator_to_array($this->questions_repository->getAllQuestions()) + ) + ); + } + + private function buildEditStartView( + URLBuilder $url_builder, + URLBuilderToken $step_token, + URLBuilderToken $page_id_token, + QuestionImplementation $question + ): array { + return $question->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder, + $step_token, + $page_id_token, + '' + ); + } + + private function buildCreateAnswerForm( + URLBuilder $url_builder + ): StandardForm { + $if = $this->ui_factory->input(); + return $if->container()->form()->standard( + $url_builder->buildURI()->__toString(), + [ + 'form_type' => $if->field()->section( + [ + $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect() + )->withRequired(true) + ], + $this->lng->txt('create_answer_form') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) + ) + ) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function checkCapabilities(array $capabilities): void + { + foreach ($capabilities as $capability) { + if (!$this->questions_repository->capabilityExists($capability)) { + throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); + } + } + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php new file mode 100644 index 000000000000..b8e8ce6a99d8 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php @@ -0,0 +1,28 @@ + + */ +class ArrayBasedRequestWrapper implements RequestWrapper +{ + /** + * GetRequestWrapper constructor. + * @param mixed[] $raw_values + */ + public function __construct(private array $raw_values) + { + } + + + /** + * @inheritDoc + */ + public function retrieve(string $key, Transformation $transformation) + { + return $transformation->transform($this->raw_values[$key] ?? null); + } + + + /** + * @inheritDoc + */ + public function has(string $key): bool + { + return isset($this->raw_values[$key]); + } + + /** + * @inheritDoc + */ + public function keys(): array + { + return array_keys($this->raw_values); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php new file mode 100644 index 000000000000..39bb97834356 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php @@ -0,0 +1,123 @@ +loadLanguageModule('qpl'); + } + + public function getTable(): Table\Data + { + return $this->ui_factory->table()->data( + $this, + $this->lng->txt('questions'), + $this->getColums(), + ); + } + + public function getFilter(string $action): Filter + { + $question_type_options = [ + '' => $this->lng->txt('filter_all_question_types') + ]; + + foreach ($this->answer_form_type_factory->getAvailableAnswerFormTypes() as $class => $type) { + $question_type_options[$class] = $type->getLabel($this->lng); + } + + $field_factory = $this->ui_factory->input()->field(); + $filter_inputs = [ + 'title' => $field_factory->text($this->lng->txt('title')), + 'contains_type' => $field_factory->select($this->lng->txt('contains_type'), $question_type_options), + ]; + + $active = array_fill(0, count($filter_inputs), true); + + $filter = $this->ui_service->filter()->standard( + 'question_table_filter_id', + $action, + $filter_inputs, + $active, + true, + true + ); + return $filter; + } + + + public function getColums(): array + { + $f = $this->ui_factory->table()->column(); + + return [ + 'title' => $f->link($this->lng->txt('title')), + 'type' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true), + ]; + } + + public function getRows( + Table\DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + foreach ($this->questions_repository->getAllQuestions() as $question) { + yield $question->toTableRow( + $row_builder, + $this->ui_factory, + $this->url_builder, + $this->row_id_token + ); + } + } + + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return 0; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php b/components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php new file mode 100755 index 000000000000..6c48a4496a59 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php @@ -0,0 +1,154 @@ +context_collection->main(); + } + + protected function isModeEnabled(CalledContexts $called_contexts): bool + { + return $called_contexts->current()->getAdditionalData() + ->is(self::MODE_ENABLED, true); + } + + public function getBreadCrumbsModification(CalledContexts $called_contexts): ?BreadCrumbsModification + { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $this->globalScreen()->layout()->factory()->breadcrumbs() + ->withModification( + function (?Breadcrumbs $current): ?Breadcrumbs { + return null; + } + )->withPriority(self::MODIFICATION_PRIORITY); + } + + public function getMainBarModification(CalledContexts $called_contexts): ?MainBarModification + { + $mainbar = $this->globalScreen()->layout()->factory()->mainbar(); + + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $mainbar + ->withModification( + $this->buildMainBarModification($called_contexts) + )->withPriority(self::MODIFICATION_PRIORITY); + } + + public function getMetaBarModification(CalledContexts $called_contexts): ?MetaBarModification + { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $this->globalScreen()->layout()->factory()->metabar() + ->withModification( + function (?MetaBar $current): ?MetaBar { + return null; + } + )->withPriority(self::MODIFICATION_PRIORITY); + } + + public function getPageBuilderDecorator(CalledContexts $called_contexts): ?PageBuilderModification + { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + $mode_info = $this->dic['ui.factory']->mainControls()->modeInfo( + $this->dic->language()->txt('edit_questions'), + $called_contexts->current()->getAdditionalData()->get(self::URL_CLOSE_MODE_INFO) + ); + + return $this->factory->page() + ->withLowPriority() + ->withModification( + static function (PagePartProvider $parts) use ($mode_info): Page { + $p = new StandardPageBuilder(); + $page = $p->build($parts); + return $page->withModeInfo($mode_info); + } + ); + } + + private function buildMainbarModification(CalledContexts $called_contexts): \Closure + { + return function (?MainBar $mainbar) use ($called_contexts): ?MainBar { + if ($mainbar === null) { + return null; + } + + $tools = $mainbar->getToolEntries(); + $new_mainbar = array_reduce( + array_keys($tools), + static fn(MainBar $c, string $v): MainBar => $c->withAdditionalToolEntry($v, $tools[$v]), + $mainbar + ->withClearedEntries() + ->withAdditionalEntry( + 'create_question', + $this->dic['ui.factory']->button()->bulky( + $this->dic['ui.factory']->symbol()->icon()->standard('', '')->withAbbreviation('CQ'), + $this->dic['lng']->txt('create_question'), + $called_contexts->current()->getAdditionalData()->get(self::URL_CREATE_QUESTION)->__toString() + ) + ) + ); + + if ($called_contexts->current()->getAdditionalData()->exists(self::QUESTIONLIST_ENTRY)) { + return $new_mainbar->withAdditionalEntry( + 'question_list', + $called_contexts->current()->getAdditionalData()->get(self::QUESTIONLIST_ENTRY) + ); + } + + return $new_mainbar; + }; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php new file mode 100644 index 000000000000..14a16b4c6037 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -0,0 +1,555 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function withEditable(Editability $editability): self + { + $clone = clone $this; + $clone->editability = $editability; + return $clone; + } + + public function withOrderingEnabled(bool $enable): self + { + $clone = clone $this; + $clone->ordering_enabled = $enable; + return $clone; + } + + public function view( + \ilToolbarGUI $toolbar, + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_QUESTION => $this->createQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + self::CMD_EDIT_QUESTION => $this->editQuestion( + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token + ), + default => $this->showTable( + $toolbar, + $url_builder, + $action_token, + $question_id_token + ) + }; + } + + public function forwardPageCmds( + \ilGlobalTemplateInterface $tpl, + URI $base_uri, + ): void { + [ + 0 => $url_builder, + 1 => $action_token, + 3 => $question_id_token, + 4 => $page_id_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + $page_id = $this->retrievePageId($page_id_token); + $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); + + $tpl->setContent( + $this->ctrl->forwardCommand( + new \QstsQuestionPageGUI( + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) + ->withParameter($question_id_token, $question_id->toString()) + ->buildURI(), + $page_id + ) + ) + ); + } + + public function createAnswerForm( + URI $base_uri + ): array { + [ + $url_builder, + $action_token, + $step_token, + $question_id_token, + $page_id_token, + $type_hash_token + ] = $this->acquireURLBuilderAndParameters($base_uri); + $question_id = $this->retrieveQuestionId($question_id_token); + $url_builder_with_params = $url_builder + ->withParameter($question_id_token, $question_id->toString()) + ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) + ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); + + $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); + if ($answer_form_type_class_hash !== '') { + return $this->forwardCreateAnswerFormCmd( + $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), + $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), + $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), + $step_token + ); + } + + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + $url_builder_with_params, + $action_token, + $step_token, + $type_hash_token + ), + default => [$this->buildCreateAnswerForm( + $url_builder_with_params, + $action_token + )] + }; + } + + public function editAnswerForm( + URI $base_uri + ): array { + [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); + return match($this->retrieveStringValueForToken($action_token)) { + self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], + default => [$this->buildCreateAnswerForm($url_builder, $action_token)] + }; + } + + private function createQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $create = (new QuestionImplementation())->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->create( + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($create)) { + return $create; + } + + $this->questions_repository->store($create); + return $this->buildEditStartView( + $url_builder->withParameter($question_id_token, $create->getId()), + $step_token, + $page_id_token, + $create + ); + + } + + private function editQuestion( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $question_id_token, + URLBuilderToken $page_id_token + ): array { + $this->initializeEditMode($url_builder, $action_token, $question_id_token); + + $question_id = $this->retrieveQuestionId($question_id_token); + + $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); + + $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), + $step_token, + $page_id_token, + $this->retrieveStringValueForToken($step_token) + ); + + if (is_array($edit)) { + return $edit; + } + + $this->questions_repository->store($edit); + return $this->buildEditStartView( + $url_builder_with_row_id, + $step_token, + $page_id_token, + $edit + ); + } + + private function showTable( + \ilToolbarGUI $toolbar, + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): array { + $toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('create'), + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() + ) + ); + + $table = new QuestionsTable( + $this->ui_factory, + $this->ui_services, + $this->lng, + $this->answer_form_types_factory, + $this->questions_repository, + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $action_token, + $question_id_token + ); + return [ + $table->getFilter($url_builder->buildURI()->__toString()), + $table->getTable()->withRequest($this->http->request()) + + ]; + } + + private function processCreateAnswerForm( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $step_token, + URLBuilderToken $type_hash_token + ): array { + $form = $this->buildCreateAnswerForm( + $url_builder, + $action_token + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null || $data['form_type'] === null) { + return [$form]; + } + + return $this->forwardCreateAnswerFormCmd( + $data['form_type'], + $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), + $step_token + ); + } + + private function forwardCreateAnswerFormCmd( + Definition $type, + TypeGenericProperties $type_generic_properties, + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array { + return $type->getEditView()->create( + $type->buildProperties($type_generic_properties, []), + $url_builder, + $step_token, + $this->retrieveStringValueForToken($step_token) + ); + } + + private function acquireURLBuilderAndParameters(URI $base_uri): array + { + return (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_PAGE_ID, + self::TOKEN_TYPE_HASH + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function retrieveQuestionId( + URLBuilderToken $question_id_token + ): ?Uuid { + return $this->http->wrapper()->query()->retrieve( + $question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): Uuid => $this->uuid_factory->fromString($v) + ), + $this->refinery->always(null) + ]) + ); + } + + public function retrievePageId( + URLBuilderToken $page_id_token + ): ?int { + return $this->http->wrapper()->query()->retrieve( + $page_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->always(null) + ]) + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function initializeEditMode( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): void { + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::MODE_ENABLED, + true + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::QUESTIONLIST_ENTRY, + $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CLOSE_MODE_INFO, + $url_builder->buildURI() + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CREATE_QUESTION, + $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() + ); + } + + private function setParametersForQuestionCmds( + URLBuilderToken $question_id_token, + string $question_id, + URLBuilderToken $page_id_token, + int $page_id + ): void { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $question_id_token->getName(), + $question_id + ); + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $page_id_token->getName(), + $page_id + ); + } + + private function buildQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): LegacySlate { + return $this->ui_factory->mainControls()->slate()->legacy( + $this->lng->txt('mainbar_button_label_questionlist'), + $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), + $this->ui_factory->legacy()->content( + $this->ui_renderer->render( + $this->ui_factory->panel()->secondary()->listing( + $this->lng->txt('mainbar_button_label_questionlist'), + [ + $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) + ] + ) + ) + ) + ); + } + + private function buildItemGroupForQuestionListSlate( + URLBuilder $url_builder, + URLBuilderToken $action_token, + URLBuilderToken $question_id_token + ): ItemGroup { + return $this->ui_factory->item()->group( + '', + array_map( + fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( + $v->toEditLink( + $this->ui_factory->link(), + $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), + $question_id_token + ) + ), + iterator_to_array($this->questions_repository->getAllQuestions()) + ) + ); + } + + private function buildEditStartView( + URLBuilder $url_builder, + URLBuilderToken $step_token, + URLBuilderToken $page_id_token, + QuestionImplementation $question + ): array { + return $question->getEditView( + $this->lng, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl, + $this->data_factory + )->edit( + $url_builder, + $step_token, + $page_id_token, + '' + ); + } + + private function buildCreateAnswerForm( + URLBuilder $url_builder + ): StandardForm { + $if = $this->ui_factory->input(); + return $if->container()->form()->standard( + $url_builder->buildURI()->__toString(), + [ + 'form_type' => $if->field()->section( + [ + $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect() + )->withRequired(true) + ], + $this->lng->txt('create_answer_form') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) + ) + ) + ] + )->withSubmitLabel($this->lng->txt('next')); + } + + private function checkCapabilities(array $capabilities): void + { + foreach ($capabilities as $capability) { + if (!$this->questions_repository->capabilityExists($capability)) { + throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); + } + } + } +} diff --git a/components/ILIAS/Questions/src/PublicInterface.php b/components/ILIAS/Questions/src/PublicInterface.php new file mode 100644 index 000000000000..6b089127b8ff --- /dev/null +++ b/components/ILIAS/Questions/src/PublicInterface.php @@ -0,0 +1,43 @@ +collector; + } + + public function getEditPresentation(): Edit + { + return $this->edit_presentation; + } +} diff --git a/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php b/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php new file mode 100644 index 000000000000..43184f62a9f7 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php @@ -0,0 +1,31 @@ + $lng->txt($this->value), + default => sprintf($lng->txt('cloze_textgap_levenshtein_of'), $this->value) + }; + } + + public static function buildOptionsList(Language $lng): array + { + return array_reduce( + self::cases(), + function (array $c, self $v) use ($lng): array { + $c[$v->value] = $v->getLabel($lng); + return $c; + }, + [] + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Column.php b/components/ILIAS/Questions/src/Question/Persistence/Column.php new file mode 100644 index 000000000000..1565f3f35e82 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Column.php @@ -0,0 +1,40 @@ +table; + } + + public function toColumnString(): string + { + return "{$this->table}.{$this->column}"; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Join.php b/components/ILIAS/Questions/src/Question/Persistence/Join.php new file mode 100644 index 000000000000..e4848d4d5d47 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Join.php @@ -0,0 +1,51 @@ +type->value} JOIN {$this->right->getTable()} ON {$this->left->toColumnString()} = {$this->right->toColumnString()}"; + } + + public function getLeft(): Column + { + return $this->left; + } + + public function getRight(): Column + { + return $this->right; + } + + public function getType(): JoinType + { + return $this->type; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/JoinType.php b/components/ILIAS/Questions/src/Question/Persistence/JoinType.php new file mode 100644 index 000000000000..4e071ddfe573 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/JoinType.php @@ -0,0 +1,27 @@ +available_points = $available_points; + return $clone; + } + + public function withImageSize(int $image_size): self + { + $clone = clone $this; + $clone->image_size = $image_size; + return $clone; + } + + public function withShuffleAnswerOptions(bool $shuffle_answer_options): self + { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + public function withAddtionalText(string $additional_text): self + { + $clone = clone $this; + $clone->additional_text = $additional_text; + return $clone; + } + +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Operator.php b/components/ILIAS/Questions/src/Question/Persistence/Operator.php new file mode 100644 index 000000000000..70a6124f4a48 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Operator.php @@ -0,0 +1,50 @@ +'; + case Greater = '>'; + case Less = '>'; + case GreaterOrEqual = '>='; + case LessOrEqual = '<='; + case In = 'IN'; + case Like = 'LIKE'; + case Between = 'BETWEEN'; + + public function toSql(Column $left, int $nr_of_values): string + { + if ($nr_of_values > 1) { + $placeholders = '%s'; + for ($i = 1; $i < $nr_of_values; $i++) { + $placeholders .= ', %s'; + } + } + + return match($this) { + self::In => "{$left->toColumnString()} {$this->value} ({$placeholders})", + self::Between => "{$left->toColumnString()} {$this->value} %s AND %s", + default => "{$left->toColumnString()} {$this->value} %s" + }; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Order.php b/components/ILIAS/Questions/src/Question/Persistence/Order.php new file mode 100644 index 000000000000..c0b4d27d2ea1 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Order.php @@ -0,0 +1,35 @@ +column->toColumnString()} {$this->direction->value}"; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/OrderDirection.php b/components/ILIAS/Questions/src/Question/Persistence/OrderDirection.php new file mode 100644 index 000000000000..079dd1279555 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/OrderDirection.php @@ -0,0 +1,27 @@ + + */ + public function getAllQuestions(): \Generator + { + yield from $this->getForWhereClause(''); + } + + public function getForQuestionId(Uuid $question_id): ?QuestionImplementation + { + return $this->getForWhereClause( + "q.id={$this->db->quote($question_id->toString(), \ilDBConstants::T_TEXT)}" + )->current(); + } + + /** + * + * @param array<\ILIAS\Data\Uuid> $question_ids + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getForQuestionIds(array $question_ids): \Generator + { + yield from $this->getForWhereClause( + $this->db->in( + 'q.question_id', + $question_ids, + false, + \ilDBConstants::T_INTEGER + ) + ); + } + + private function buildQuestionFromDBRecords(\stdClass $db_record): QuestionImplementation + { + return new QuestionImplementation( + $this->uuid_factory->fromString($db_record->id), + $db_record->page_id, + $db_record->title, + $db_record->author, + Lifecycle::from($db_record->lifecycle), + $db_record->remarks, + $db_record->original_id === null ? null : $this->uuid_factory->fromString($db_record->original_id), + new \DateTimeImmutable('@' . $db_record->last_update, new \DateTimeZone('UTC')), + new \DateTimeImmutable('@' . $db_record->created, new \DateTimeZone('UTC')) + ); + } + + /** + * @return \Generator + */ + private function getForWhereClause(string $where): \Generator + { + $query_result = $this->db->query( + 'SELECT q.id, q.page_id, q.title, q.author, q.lifecycle, q.remarks,' . PHP_EOL + . 'q.original_id, q.last_update, q.created' . PHP_EOL + . 'FROM ' . self::QUESTION_TABLE . ' q' . PHP_EOL + . ($where === '' ? '' : 'WHERE ' . $where) + ); + + while (($db_record = $this->db->fetchObject($query_result)) !== null) { + yield $this->buildQuestionFromDBRecords($db_record); + } + } + + /** + * + * @param ILIAS\Questions\Question\Question|array $questions + * @return array + */ + public function store( + QuestionImplementation|array $questions + ): array { + if ($questions instanceof QuestionImplementation) { + return [$this->storeQuestion($questions)]; + } + + return array_map( + fn(QuestionImplementation $v) => $this->storeQuestion($v), + $questions + ); + } + + private function storeQuestion(QuestionImplementation $question): Uuid + { + if ($question->getPageId() === null) { + $question = $question->withPageId($this->buildQuestionPage()); + } + + if ($question->getId() === null) { + $question = $question->withQuestionId($this->buildAvailableUuid()); + $this->db->insert( + self::QUESTION_TABLE, + $question->toStorage() + ); + return $question->getId(); + } + $this->db->update( + self::QUESTION_TABLE, + $question->toStorage(), + [ + 'id' => [ + \ilDBConstants::T_TEXT, + $question->getId()->toString() + ] + ] + ); + return $question->getId(); + } + + private function buildAvailableUuid(): Uuid + { + do { + $uuid = $this->uuid_factory->uuid4(); + if ($this->checkAvailabilityOfId($uuid)) { + return $uuid; + } + } while (true); + } + + private function checkAvailabilityOfId(Uuid $uuid): bool + { + return $this->db->fetchObject( + $this->db->query( + 'SELECT COUNT(*) as cnt FROM ' . self::QUESTION_TABLE + . " WHERE id='{$uuid->toString()}'" + ) + )->cnt === 0; + } + + private function buildQuestionPage(): int + { + $page = new \QstsQuestionPage(); + $page->setId($this->getNextAvailableQuestionPageId()); + $page->createFromXML(); + return $page->getId(); + } + + private function getNextAvailableQuestionPageId(): int + { + + $last_id = $this->db->fetchObject( + $this->db->query( + 'SELECT MAX(page_id) AS last FROM ' . self::PAGE_EDITOR_TABLE + . ' WHERE parent_type = "qsts"' + ) + )->last; + if ($last_id === null) { + return 1; + } + + return $last_id + 1; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Select.php b/components/ILIAS/Questions/src/Question/Persistence/Select.php new file mode 100644 index 000000000000..ab166ef0201f --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Select.php @@ -0,0 +1,38 @@ + "{$this->table}.{$v}", + $this->columns + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php b/components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php new file mode 100644 index 000000000000..aad33592fdc7 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php @@ -0,0 +1,165 @@ +select[] = new Select( + Repository::QUESTION_TABLE, + Repository::QUESTION_TABLE_COLUMNS + ); + + $this->select[] = new Select( + Repository::ANSWER_FORM_TABLE, + Repository::ANSWER_FORM_TABLE_COLUMNS + ); + + $left = new Column( + Repository::QUESTION_TABLE, + Repository::QUESTION_TABLE_ID_COLUMN + ); + + $this->joins[] = new Join( + $left, + new Column( + Repository::ANSWER_FORM_TABLE, + Repository::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ) + ); + + $this->where[] = new Where( + $left, + new Value( + \ilDBConstants::T_INTEGER, + $answer_form_ids + ), + Operator::In + ); + + $this->order[] = new Order( + Repository::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ); + + $this->order[] = new Order( + Repository::ANSWER_FORM_TABLE_ID_COLUMN + ); + } + + public function withAdditionalSelect(Select $select): self + { + $clone = clone $this; + $clone->select[] = $select; + return $clone; + } + + public function withAdditionalJoin(Join $join): self + { + $clone = clone $this; + $clone->joins[] = $join; + return $clone; + } + + public function withAdditionalWhere(Where $where): self + { + $clone = clone $this; + $clone->where[] = $where; + return $clone; + } + + public function withAdditionalOrder(Order $order): self + { + $clone = clone $this; + $clone->order[] = $order; + return $clone; + } + + public function withLimit(int $limit): self + { + $clone = clone $this; + $clone->limit = $limit; + return $clone; + } + + public function toSql(): string + { + return $this->db->queryF( + 'SELECT ' . implode( + ', ', + array_reduce( + $this->select, + static fn(array $c, Select $v): array => [...$c, ...$v->toColumnsArray()], + [] + ) + ) . ' FROM ' . self::QUESTION_TABLE + . array_reduce( + $this->joins, + static fn(string $c, Join $v): string => $c . PHP_EOL . $v->toSql(), + '' + ) . PHP_EOL + . $this->buildWhereString() + . $this->limit !== null ? "LIMIT = {$this->limit}" : '', + $this->binding_types, + $this->binding_values + ); + } + + private function buildWhereString(): string + { + return array_reduce( + $this->where, + function (?string $c, Where $v): string { + $this->addValueToBinding($v->getRight()); + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + } + ); + } + + private function addValueToBinding(Value $value): void + { + if (!is_array($value)) { + $this->binding_types[] = $value->getType(); + $this->binding_values[] = $value->getValue(); + return; + } + + foreach ($value->getValue() as $v) { + $this->binding_types[] = $value->getType(); + $this->binding_values[] = $v; + } + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php b/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php new file mode 100644 index 000000000000..f65b225418e9 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php @@ -0,0 +1,57 @@ +type_specific_part = $table_name_space->getTypeSpecificTableNamePart(); + } + + public function getTypeSpecificAnswerFormsTableName(): string + { + return "qsts_answer_forms_{$this->type_specific_part}"; + } + + public function getAnswerInputsTableName(): string + { + return "qsts_answer_inputs_{$this->type_specific_part}"; + } + + public function getAnswerOptionsTableName(): string + { + return "qsts_answer_options_{$this->type_specific_part}"; + } + + public function getResponsesTableName(): string + { + return "qsts_responses_{$this->type_specific_part}"; + } + + public function getAdditionalTableName(string $table_identifier): string + { + return "qsts_{$this->type_specific_part}_{$table_identifier}"; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php new file mode 100644 index 000000000000..d6779d1692ce --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php @@ -0,0 +1,44 @@ + 4 || mb_strlen($answer_form_id) > 8) { + throw new \InvalidArgumentException( + 'Neither $vendor nor $answer_form_id can be longer then 4 characters.' + ); + } + } + + public function getTypeSpecificTableNamePart(): string + { + return "{$this->vendor}_{$this->answer_form_id}"; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php new file mode 100644 index 000000000000..7d207a4b297b --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php @@ -0,0 +1,34 @@ +answer_form_id; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Value.php b/components/ILIAS/Questions/src/Question/Persistence/Value.php new file mode 100644 index 000000000000..3fca04d0d4f2 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Value.php @@ -0,0 +1,49 @@ +type; + } + + public function getValue(): string|int|array + { + return $this->value; + } + + public function getNumberOfElements(): int + { + if (is_array($this->value)) { + return count($this->value); + } + + return 1; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Where.php b/components/ILIAS/Questions/src/Question/Persistence/Where.php new file mode 100644 index 000000000000..d155b7f96612 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Where.php @@ -0,0 +1,52 @@ +negate ? 'NOT ' : '' + . $this->comparison->toSql( + $this->left, + $this->right->getNumberOfElements() + ); + } + + public function getRight(): int|string + { + return $this->right; + } + + public function getLogicalOperator(): Junctor + { + return $this->junctor; + } +} diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php new file mode 100644 index 000000000000..a57d35e274f6 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -0,0 +1,26 @@ +id; + } + + public function withQuestionId(Uuid $question_id): self + { + $clone = clone $this; + $clone->id = $question_id; + return $clone; + } + + public function getPageId(): ?int + { + return $this->page_id; + } + + public function withPageId(int $page_id): self + { + $clone = clone $this; + $clone->page_id = $page_id; + return $clone; + } + + public function getTitle(): string + { + return $this->title; + } + + public function withTitle(string $title): self + { + $clone = clone $this; + $clone->title = $title; + return $clone; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function withAuthor(string $author): self + { + $clone = clone $this; + $clone->author = $author; + return $clone; + } + + public function getLifecycle(): Lifecycle + { + return $this->lifecycle; + } + + public function withLifecycle(Lifecycle $lifecycle): self + { + $clone = clone $this; + $clone->lifecycle = $lifecycle; + return $clone; + } + + public function getRemarks(): string + { + return $this->remarks; + } + + public function withRemarks(string $remarks): self + { + $clone = clone $this; + $clone->remarks = $remarks; + return $clone; + } + + public function getOriginalId(): ?Uuid + { + return $this->original_id; + } + + public function withOriginalId(Uuid $original_id): self + { + $clone = clone $this; + $clone->original_id = $original_id; + return $clone; + } + + public function getLastUpdate(): ?\DateTimeImmutable + { + return $this->last_update; + } + + public function getCreated(): ?\DateTimeImmutable + { + return $this->created; + } + + public function withCreated(\DateTimeImmutable $created): self + { + $clone = clone $this; + $clone->created = $created; + return $clone; + } + + public function getAnswerForms(): array + { + return $this->answer_forms; + } + + public function getAnswerForm(Uuid $form_id): ?Form + { + return $this->answer_forms[$form_id->toString()] ?? null; + } + + public function withAnswerForm(Form $answer_form): self + { + $this->answer_forms[$answer_from->getId()->toString()] = $answer_form; + } + + /** + * Checks whether the question is a clone of another question or not + */ + public function isClone(): bool + { + return $this->original_id !== null; + } + + public function getEditView( + Language $lng, + \ilObjUser $current_user, + UIFactory $ui_factory, + Refinery $refinery, + RequestInterface $request, + \ilCtrl $ctrl, + DataFactory $data_factory + ): Views\Edit { + return new Views\Edit($lng, $current_user, $ui_factory, $refinery, $request, $ctrl, $data_factory, $this); + } + + public function getParticipantView(): Views\Participant + { + return new Views\Participant( + new \QstsQuestionPageGUI($this), + $this->answer_forms + ); + } + + public function toEditLink( + LinkFactory $link_factory, + URLBuilder $url_builder, + URLBuilderToken $row_id_token + ): StandardLink { + return $link_factory->standard( + $this->title, + $url_builder->withParameter( + $row_id_token, + $this->id->toString() + )->buildURI()->__toString() + ); + } + + public function toTableRow( + DataRowBuilder $row_builder, + UIFactory $ui_factory, + URLBuilder $url_builder, + URLBuilderToken $row_id_token + ): DataRow { + return $row_builder->buildDataRow( + $this->id->toString(), + [ + 'title' => $ui_factory->link()->standard( + $this->title, + $url_builder->withParameter( + $row_id_token, + $this->id->toString() + )->buildURI()->__toString() + ) + ] + ); + } + + /** + * @return array + */ + public function toStorage(): array + { + return [ + 'id' => [\ilDBConstants::T_TEXT, $this->id->toString()], + 'page_id' => [\ilDBConstants::T_INTEGER, $this->page_id], + 'title' => [\ilDBConstants::T_TEXT, $this->title], + 'author' => [\ilDBConstants::T_TEXT, $this->author], + 'lifecycle' => [\ilDBConstants::T_TEXT, $this->lifecycle->value], + 'remarks' => [\ilDBConstants::T_TEXT, $this->remarks], + 'original_id' => [\ilDBConstants::T_TEXT, $this->original_id?->toString()], + 'last_update' => [\ilDBConstants::T_INTEGER, time()], + 'created' => [\ilDBConstants::T_INTEGER, $this->created?->getTimestamp() ?? time()] + ]; + } + +} diff --git a/components/ILIAS/Questions/src/Question/Response.php b/components/ILIAS/Questions/src/Question/Response.php new file mode 100644 index 000000000000..3bba6d1e8643 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Response.php @@ -0,0 +1,25 @@ + $this->onBasicPropertiesFormSubmission($url_builder, $step_token), + default => [$this->buildBasicPropertiesForm($url_builder, $step_token)] + }; + } + + public function edit( + URLBuilder $url_builder, + URLBuilderToken $step_token, + URLBuilderToken $page_id_token, + string $step + ): array|Question { + return match ($step) { + self::CMD_SAVE_QUESTION => $this->onBasicPropertiesFormSubmission($url_builder, $step_token), + default => [ + $this->buildBasicPropertiesForm($url_builder, $step_token), + $this->buildPreviewPanel($url_builder, $page_id_token) + ] + }; + } + + private function buildBasicPropertiesForm( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): StandardForm { + return $this->ui_factory->input()->container()->form()->standard( + $url_builder->withParameter($step_token, self::CMD_SAVE_QUESTION) + ->buildURI()->__toString(), + $this->buildBasicPropertiesInputs() + ); + } + + private function onBasicPropertiesFormSubmission( + URLBuilder $url_builder, + URLBuilderToken $step_token + ): array|Question { + $form = $this->buildBasicPropertiesForm($url_builder, $step_token) + ->withRequest($this->request); + $data = $form->getData(); + if ($data === null) { + return [$form]; + } + + return $data['question']; + } + + private function buildBasicPropertiesInputs(): array + { + $ff = $this->ui_factory->input()->field(); + $section = $ff->section( + [ + 'title' => $ff->text($this->lng->txt('title')) + ->withRequired(true), + 'author' => $ff->text($this->lng->txt('author')) + ->withValue($this->current_user->getFullname()), + 'lifecycle' => $ff->select( + $this->lng->txt('qst_lifecycle'), + array_reduce( + Lifecycle::cases(), + function (array $c, Lifecycle $v): array { + $c[$v->value] = $this->lng->txt("qst_lifecycle_{$v->value}"); + return $c; + }, + [] + ) + )->withRequired(true), + 'remarks' => $ff->textarea($this->lng->txt('qst_remarks')) + ], + $this->lng->txt('edit_basic_form_properties') + )->withAdditionalTransformation($this->buildAddBasicPropertiesToQuestionTrafo()); + + return [ + 'question' => $section->withValue([ + 'title' => $this->question->getTitle(), + 'author' => $this->question->getAuthor(), + 'lifecycle' => $this->question->getLifecycle()->value, + 'remarks' => $this->question->getRemarks() + ]) + ]; + } + + private function buildAddBasicPropertiesToQuestionTrafo(): Transformation + { + return $this->refinery->custom()->transformation( + fn(array $vs): QuestionImplementation => new QuestionImplementation( + $this->question?->getId(), + $this->question?->getPageId(), + $vs['title'], + $vs['author'], + Lifecycle::tryFrom($vs['lifecycle']) ?? Lifecycle::Draft, + $vs['remarks'], + $this->question?->getOriginalId(), + $this->question?->getLastUpdate(), + $this->question?->getCreated(), + $this->question?->getAnswerForms() ?? [] + ) + ); + } + + private function buildPreviewPanel( + URLBuilder $url_builder, + URLBuilderToken $page_id_token + ): StandardPanel { + return $this->ui_factory->panel()->standard( + $this->lng->txt('preview'), + $this->ui_factory->legacy()->content($this->question->getTitle()) + )->withActions( + $this->ui_factory->dropdown()->standard([ + $this->ui_factory->link()->standard( + $this->lng->txt('edit'), + $url_builder + ->withURI( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') + ) + )->withParameter($page_id_token, (string) $this->question->getPageId()) + ->buildURI()->__toString() + ) + ]) + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php new file mode 100644 index 000000000000..186f8477de0c --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -0,0 +1,81 @@ +question->getAnswerForms() as $form) { + if (!$form->getType()->isAsyncPresentationAvailable()) { + throw \Exception('This QuestionType has no async presentation.'); + } + } + $clone = clone $this; + $clone->async = $async; + return $clone; + } + + public function withIsInteractive(bool $interactive): self + { + $clone = clone $this; + $clone->interactive = $interactive; + return $clone; + } + + public function withShowMarks(bool $show_marks): self + { + foreach ($this->question->getAnswerForms() as $form) { + if (!$form->getType()->isMarkable()) { + throw \Exception('This QuestionType cannot be marked.'); + } + } + + $clone = clone $this; + $clone->show_marks = $show_marks; + return $clone; + } + + public function withShowCorrectSolution(bool $show_correct_solution): self + { + foreach ($this->question->getAnswerForms() as $form) { + if (!$form->getType()->isMarkable()) { + throw \Exception('This QuestionType cannot be marked.'); + } + } + + $clone = clone $this; + $clone->show_correct_solution = $show_correct_solution; + return $clone; + } +} diff --git a/components/ILIAS/Questions/src/Response/Repository.php b/components/ILIAS/Questions/src/Response/Repository.php new file mode 100644 index 000000000000..62d40c84fdd4 --- /dev/null +++ b/components/ILIAS/Questions/src/Response/Repository.php @@ -0,0 +1,28 @@ +db = $db; + } + + public function step_1(): void + { + $table_name = $this->table_name_builder->getTypeSpecificAnswerFormsTableName(); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'scoring_identical_responses' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => true + ], + 'combinations_activated' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addPrimaryKey($table_name, ['answer_form_id']); + } + } + + public function step_2(): void + { + $table_name = $this->table_name_builder->getAnswerInputsTableName(); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => true + ], + 'gap_type' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => true + ], + 'max_chars' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'step_size' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'text_matching_method' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => false + ], + 'min_autocomplete' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'shuffle_answer_options' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addIndex($table_name, ['answer_form_id'], 'af'); + } + } + + public function step_3(): void + { + $table_name = $this->table_name_builder->getAnswerOptionsTableName(); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_input_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => true + ], + 'text_value' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => false + ], + 'lower_limit' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'upper_limit' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_input_id'])) { + $this->db->addIndex($table_name, ['answer_input_id'], 'ai'); + } + } + + public function step_4(): void + { + $table_name = $this->table_name_builder->getAdditionalTableName('combinations'); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_options' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => true + ], + 'points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addIndex($table_name, ['answer_form_id'], 'ai'); + } + } + + public function step_5(): void + { + $table_name = $this->table_name_builder->getResponsesTableName(); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_input_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'selected_answer_option' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => false + ], + 'text' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_input_id'])) { + $this->db->addIndex($table_name, ['answer_input_id'], 'ai'); + } + } +} diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php new file mode 100644 index 000000000000..b4d007a48138 --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -0,0 +1,206 @@ +db = $db; + } + + public function step_1(): void + { + $table_name = 'qsts_questions'; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'page_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => true + ], + 'title' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 512, + 'notnull' => true + ], + 'author' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 512, + 'notnull' => false + ], + 'lifecycle' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 16, + 'notnull' => true + ], + 'remarks' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => false + ], + 'original_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => false + ], + 'last_update' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], + 'created' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + } + + public function step_2(): void + { + $table_name = 'qsts_answer_forms'; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'type' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => true + ], + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'available_points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'image_size' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'shuffle_answer_options' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => true + ], + 'additional_text' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'additional_text_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['question_id'])) { + $this->db->addIndex($table_name, ['question_id'], 'q'); + } + } + + public function step_3(): void + { + $table_name = 'qsts_responses'; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'reached_points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['question_id'])) { + $this->db->addIndex($table_name, ['question_id'], 'q'); + } + } + + public function step_4(): void + { + $table_name = 'qsts_linking'; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'obj_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['question_id'])) { + $this->db->addPrimaryKey($table_name, ['question_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['obj_id'])) { + $this->db->addIndex($table_name, ['obj_id'], 'o'); + } + } +} diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 476a103a5fe0..db0654f2f2e0 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -1136,14 +1136,6 @@ assessment#:#qst_essay_chars_remaining#:#Noch übrige Zeichen: assessment#:#qst_essay_wordcounter_enabled#:#Wörter zählen assessment#:#qst_essay_wordcounter_enabled_info#:#Die eingegebenen Wörter werden gezählt. Die Anzahl der Wörter wird den Teilnehmern unterhalb des Texteingabefeldes angezeigt. assessment#:#qst_essay_written_words#:#Anzahl der eingegebenen Wörter: -assessment#:#qst_lifecycle#:#Lebenszyklus -assessment#:#qst_lifecycle_draft#:#Entwurf -assessment#:#qst_lifecycle_filter_all#:#Alle Lebenszyklen -assessment#:#qst_lifecycle_final#:#Endgültig -assessment#:#qst_lifecycle_outdated#:#Veraltet -assessment#:#qst_lifecycle_rejected#:#Abgelehnt -assessment#:#qst_lifecycle_review#:#Überarbeitung notwendig -assessment#:#qst_lifecycle_sharable#:#Verteilbar assessment#:#qst_nested_nested_answers_off#:#Ohne Einrückung assessment#:#qst_nested_nested_answers_on#:#Mit Einrückung assessment#:#qst_nr_of_tries#:#Anzahl der Versuche @@ -5107,6 +5099,8 @@ common#:#obj_ps_desc#:#Globale Einstellungen für Datenschutz und Sicherheit common#:#obj_qpl#:#Fragenpool für Tests common#:#obj_qpl_duplicate#:#Fragenpool für Tests kopieren common#:#obj_qpl_select#:#-- Bitte wählen Sie einen Fragenpool für Tests aus -- +common#:#obj_qsts#:#Fragen +common#:#obj_qsts_desc#:#Globale Einstellungen für Fragen common#:#obj_rcat#:#ECS-Kategorie common#:#obj_rcrs#:#ECS-Kurs common#:#obj_recf#:#Wiederhergestellte Objekte @@ -14132,6 +14126,23 @@ qpl#:#qpl_page_type_qfbg#:#Generelles Feedback qpl#:#qpl_page_type_qfbs#:#Spezielles Feedback qpl#:#qpl_page_type_qht#:#Hinweis qpl#:#qpl_page_type_qpl#:#Fragenseite +qsts#:#cloze_enable_combinations#:#Enable Gap Combination +qsts#:#cont_ed_insert_answf#:#Antwortformular einfügen +qsts#:#create_answer_form#:#Antwortformular erstellen +qsts#:#edit_basic_form_properties#:#Grundeinstellungen der Frage bearbeiten +qsts#:#qst_lifecycle#:#Lebenszyklus +qsts#:#qst_lifecycle_draft#:#Entwurf +qsts#:#qst_lifecycle_filter_all#:#Alle Lebenszyklen +qsts#:#qst_lifecycle_final#:#Endgültig +qsts#:#qst_lifecycle_outdated#:#Veraltet +qsts#:#qst_lifecycle_rejected#:#Abgelehnt +qsts#:#qst_lifecycle_review#:#Überarbeitung notwendig +qsts#:#qst_lifecycle_sharable#:#Verteilbar +qsts#:#qst_remarks#:#Bemerkungen +qsts#:#score_all#:#Alle Antworten bewerten +qsts#:#score_distinct#:#Gleiche Antworten nur einmal bewerten +qsts#:#scoring_of_identical_responses#:#Bewertung gleicher Antworten +qsts#:#select_answer_form_type#:#Art des Antwortformulars auswählen rating#:#rat_not_rated_yet#:#Noch nicht bewertet rating#:#rat_nr_ratings#:#%s Bewertungen rating#:#rat_one_rating#:#Eine Bewertung diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 512dd28c41af..fe226ed2e22a 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -1136,14 +1136,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters: assessment#:#qst_essay_wordcounter_enabled#:#Count Words assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field. assessment#:#qst_essay_written_words#:#Number of entered words: -assessment#:#qst_lifecycle#:#Lifecycle -assessment#:#qst_lifecycle_draft#:#Draft -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles -assessment#:#qst_lifecycle_final#:#Final -assessment#:#qst_lifecycle_outdated#:#Outdated -assessment#:#qst_lifecycle_rejected#:#Rejected -assessment#:#qst_lifecycle_review#:#To be Reviewed -assessment#:#qst_lifecycle_sharable#:#Shareable assessment#:#qst_nested_nested_answers_off#:#No indents, just order assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers assessment#:#qst_nr_of_tries#:#Number of Tries @@ -5109,6 +5101,8 @@ common#:#obj_ps_desc#:#Configure global privacy and security settings here. common#:#obj_qpl#:#Question Pool for Tests common#:#obj_qpl_duplicate#:#Copy Question Pool for Tests common#:#obj_qpl_select#:#-- Please select one question pool for tests -- +common#:#obj_qsts#:#Questions +common#:#obj_qsts_desc#:#Global Settings For Questions common#:#obj_rcat#:#ECS Category common#:#obj_rcrs#:#ECS Course common#:#obj_recf#:#Restored Objects @@ -14103,6 +14097,23 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback qpl#:#qpl_page_type_qfbs#:#Special Feedback qpl#:#qpl_page_type_qht#:#Hint qpl#:#qpl_page_type_qpl#:#Question Page +qsts#:#cloze_enable_combinations#:#Lückenkombinationen aktivieren +qsts#:#cont_ed_insert_answf#:#Insert Answer Form +qsts#:#create_answer_form#:#Create Answer Form +qsts#:#edit_basic_form_properties#:#Edit Basic Form Properties +qsts#:#qst_lifecycle#:#Lifecycle +qsts#:#qst_lifecycle_draft#:#Draft +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles +qsts#:#qst_lifecycle_final#:#Final +qsts#:#qst_lifecycle_outdated#:#Outdated +qsts#:#qst_lifecycle_rejected#:#Rejected +qsts#:#qst_lifecycle_review#:#To be Reviewed +qsts#:#qst_lifecycle_sharable#:#Shareable +qsts#:#qst_remarks#:#Remarks +qsts#:#score_all#:#All Responses are Scored +qsts#:#score_distinct#:#Identical Responses are Only Scored Once +qsts#:#scoring_of_identical_responses#:#Scoring of Identical Responses +qsts#:#select_answer_form_type#:#Select Type of Answer Form rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating From 0ca7f960b2b947513e5f26f1649f22d7c8563d12 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 15 Dec 2025 07:52:08 +0100 Subject: [PATCH 030/108] Questions: Start Implementing Cloze Question --- .../class.ilObjQuestionsGUI.php | 12 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 31 +- .../Legacy/PageEditor/QstsQuestionPage.php | 26 + .../PageEditor/class.QstsQuestionPageGUI.php | 9 +- .../PageEditor/class.ilPCAnswerForm.php | 391 ++---------- .../PageEditor/class.ilPCAnswerFormGUI.php | 47 +- .../Questions/src/AnswerForm/Definition.php | 2 +- .../Questions/src/AnswerForm/Factory.php | 15 +- .../Questions/src/AnswerForm/Persistence.php | 8 +- .../Questions/src/AnswerForm/Properties.php | 10 +- .../Questions/src/AnswerForm/Views/Edit.php | 25 +- .../src/AnswerFormTypes/Cloze/Definition.php | 9 +- .../src/AnswerFormTypes/Cloze/Persistence.php | 59 +- .../Cloze/Properties/AnswerForm/Factory.php | 34 +- .../Properties/AnswerForm/Properties.php | 116 ++-- .../Cloze/Properties/ClozeText/Factory.php | 4 +- .../Cloze/Properties/ClozeText/Text.php | 10 +- .../Definitions/ScoringIdentical.php | 6 + .../Cloze/Properties/Gaps/Factory.php | 6 +- .../Cloze/Properties/Gaps/Gap.php | 71 +-- .../Cloze/Properties/Gaps/Gaps.php | 43 +- .../Cloze/Properties/Gaps/LongMenu.php | 22 +- .../Cloze/Properties/Gaps/Numeric.php | 26 +- .../Gaps/Properties/AnswerOption.php | 14 +- .../Gaps/Properties/AnswerOptions.php | 2 +- .../Properties/Gaps/Properties/Factory.php | 8 +- .../Properties/Gaps/Properties/Properties.php | 93 ++- .../Cloze/Properties/Gaps/Select.php | 20 +- .../Cloze/Properties/Gaps/Text.php | 22 +- .../Cloze/Properties/Gaps/Type.php | 14 +- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 351 +++++------ .../Presentation/Definitions/CarryWrapper.php | 65 -- .../src/Presentation/Definitions/EditForm.php | 555 ------------------ .../Definitions/EditFormFactory.php | 555 ------------------ .../Presentation/Definitions/EditOverview.php | 555 ------------------ .../Presentation/Definitions/Editability.php | 2 +- .../src/Presentation/Definitions/Leaf.php | 65 -- .../Layout/Definitions/CarryWrapper.php | 58 ++ .../Layout/Definitions/EditForm.php | 132 +++++ .../Layout/Definitions/EditOverview.php | 94 +++ .../Layout/Definitions/Environment.php | 37 ++ .../Definitions/EnvironmentImplementation.php | 200 +++++++ .../Layout/Definitions/Factory.php | 94 +++ .../Presentation/Layout/Definitions/Leaf.php | 34 ++ .../Definitions/QuestionsTable.php | 104 ++-- .../{ => GlobalScreen}/LayoutProvider.php | 2 +- .../Questions/src/Presentation/Views/Edit.php | 438 +++++--------- .../ILIAS/Questions/src/PublicInterface.php | 2 +- .../src/Question/Persistence/Column.php | 10 +- .../src/Question/Persistence/CoreTables.php | 93 +++ .../src/Question/Persistence/Insert.php | 103 ++++ .../src/Question/Persistence/Join.php | 2 +- .../src/Question/Persistence/Manipulate.php | 58 ++ .../Question/Persistence/ManipulateQuery.php | 66 --- .../src/Question/Persistence/Operator.php | 8 +- .../src/Question/Persistence/Order.php | 2 +- .../{SelectQuery.php => Query.php} | 52 +- .../src/Question/Persistence/Repository.php | 189 +++--- .../src/Question/Persistence/Select.php | 4 +- .../src/Question/Persistence/Storable.php | 26 + .../src/Question/Persistence/Table.php | 43 ++ .../Question/Persistence/TableNameBuilder.php | 39 +- .../Question/Persistence/TableNameSpace.php | 2 +- .../src/Question/Persistence/TableTypes.php | 47 ++ .../src/Question/Persistence/Update.php | 91 +++ .../src/Question/Persistence/Value.php | 5 + .../src/Question/Persistence/Where.php | 2 +- .../ILIAS/Questions/src/Question/Question.php | 2 +- .../src/Question/QuestionImplementation.php | 75 ++- .../Questions/src/Question/Views/Edit.php | 124 ++-- .../src/Setup/ClozeQuestionTables.php | 11 +- 71 files changed, 2218 insertions(+), 3334 deletions(-) delete mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php delete mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php delete mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php delete mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php delete mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php create mode 100755 components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php create mode 100755 components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php rename components/ILIAS/Questions/src/Presentation/{ => Layout}/Definitions/QuestionsTable.php (65%) rename components/ILIAS/Questions/src/Presentation/Layout/{ => GlobalScreen}/LayoutProvider.php (98%) create mode 100644 components/ILIAS/Questions/src/Question/Persistence/CoreTables.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Insert.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Manipulate.php delete mode 100644 components/ILIAS/Questions/src/Question/Persistence/ManipulateQuery.php rename components/ILIAS/Questions/src/Question/Persistence/{SelectQuery.php => Query.php} (76%) create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Storable.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Table.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableTypes.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/Update.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 1eeea1615f0b..880b35b4543b 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -19,7 +19,7 @@ declare(strict_types=1); use ILIAS\Questions\Legacy\LocalDIC; -use ILIAS\Questions\Presentation\Edit; +use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; @@ -96,12 +96,10 @@ public function viewQuestionsObject(): void $this->tabs_gui->activateTab('questions'); $this->tpl->setContent( - $this->ui_renderer->render( - $this->edit_view->view( - $this->toolbar, - $this->buildEditQuestionsBaseUri() - ) - ) + $this->edit_view->view( + $this->toolbar, + $this->buildEditQuestionsBaseUri() + )->render($this->ui_renderer) ); } diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 59a70f626023..bb6fdf7339f2 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -24,7 +24,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Question\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\Presentation\Edit; +use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\DI\Container as ILIASContainer; @@ -50,13 +51,22 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[UuidFactory::class] = static fn($c): UuidFactory => new UuidFactory(); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory - => new AnswerFormFactory([ - $c[Cloze\Type::class] - ]); + => new AnswerFormFactory( + $c[UuidFactory::class], + [ + $c[Cloze\Definition::class] + ] + ); $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository => new QuestionsRepository( $DIC['ilDB'], new UuidFactory(), + $c[AnswerFormFactory::class] + ); + $dic[DefinitionsFactory::class] = static fn($c): DefinitionsFactory => + new DefinitionsFactory( + $DIC['ui.factory'], + $DIC['lng'] ); $dic[Edit::class] = static fn($c): Edit => new Edit( $DIC['lng'], @@ -71,25 +81,25 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[DataFactory::class], $c[UuidFactory::class], $c[AnswerFormFactory::class], - $c[QuestionsRepository::class] + $c[QuestionsRepository::class], + $c[DefinitionsFactory::class] ); $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory => new Cloze\Properties\ClozeText\Factory( $DIC['refinery'], (new \ilMustacheFactory())->getBasicEngine(), - $c[UuidFactory::class], $c[DataFactory::class]->text() ); - $dic[Cloze\Properties\Gaps\Data\Factory::class] = static fn($c): Cloze\Properties\Gaps\Data\Factory - => new Cloze\Properties\Gaps\Data\Factory( + $dic[Cloze\Properties\Gaps\Properties\Factory::class] = static fn($c): Cloze\Properties\Gaps\Properties\Factory + => new Cloze\Properties\Gaps\Properties\Factory( $c[UuidFactory::class], $DIC['refinery'] ); $dic[Cloze\Properties\Gaps\Factory::class] = static fn($c): Cloze\Properties\Gaps\Factory => new Cloze\Properties\Gaps\Factory( $c[UuidFactory::class], - $c[Cloze\Properties\Gaps\Data\Factory::class], + $c[Cloze\Properties\Gaps\Properties\Factory::class], [ new Cloze\Properties\Gaps\Text( $DIC['refinery'], @@ -115,7 +125,6 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Properties\AnswerForm\Factory::class] = static fn($c): Cloze\Properties\AnswerForm\Factory => new Cloze\Properties\AnswerForm\Factory( - $c[UuidFactory::class], $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] ); @@ -135,7 +144,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant => new Cloze\Views\Participant(); - $dic[Cloze\Type::class] = static fn($c): Cloze\Type => new Cloze\Type( + $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( $c[Cloze\Properties\AnswerForm\Factory::class], $c[Cloze\Persistence::class], [ diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 23644751d771..b600e179ed02 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -18,10 +18,36 @@ declare(strict_types=1); +use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\Question\QuestionImplementation; + class QstsQuestionPage extends ilPageObject { + private readonly Edit $edit_view; + private readonly QuestionImplementation $question; + public function getParentType(): string { return 'qsts'; } + + public function getEditView(): Edit + { + return $this->edit_view; + } + + public function setEditView(Edit $edit_view): void + { + $this->edit_view = $edit_view; + } + + public function getQuestion(): QuestionImplementation + { + return $this->question; + } + + public function setQuestion(QuestionImplementation $question): void + { + $this->question = $question; + } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index ae44c63ecae1..ee196302261c 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\URI; /** @@ -29,9 +31,12 @@ class QstsQuestionPageGUI extends ilPageObjectGUI { public function __construct( private readonly URI $return_uri, - int $id = 0 + Edit $edit_view, + QuestionImplementation $question ) { - parent::__construct('qsts', $id); + parent::__construct('qsts', $question->getPageId()); + $this->obj->setQuestion($question); + $this->obj->setEditView($edit_view); $this->setEnabledPageFocus(false); } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 86f8961dad6e..8881d2f112aa 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -18,260 +18,43 @@ declare(strict_types=1); +use ILIAS\Data\UUID\Uuid; + class ilPCAnswerForm extends ilPageContent { + private const string ANSWER_FORM_ELEMENT_TAG = 'AnswerForm'; + private const string ANSWER_FORM_ID_ATTRIBUT = 'Uuid'; + private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_(.*)\]\]\]'; + public function init(): void { $this->setType('answf'); } - public function setQuestionReference(string $a_questionreference): void - { - if (is_object($this->getChildNode())) { - $this->getChildNode()->setAttribute("QRef", $a_questionreference); - } - } - - public function getQuestionReference(): ?string - { - if (is_object($this->getChildNode())) { - return $this->getChildNode()->getAttribute("QRef"); - } - return null; - } - - public function create( - ilPageObject $a_pg_obj, - string $a_hier_id, - string $a_pc_id = "" - ): void { - $this->createInitialChildNode( - $a_hier_id, - $a_pc_id, - "Question", - ["QRef" => ""] - ); - } - - /** - * Copy question from pool into page - */ - public function copyPoolQuestionIntoPage( - string $a_q_id, - string $a_hier_id - ): void { - $question = assQuestion::instantiateQuestion($a_q_id); - $duplicate_id = $question->copyObject(0, $question->getTitle()); - $duplicate = assQuestion::instantiateQuestion($duplicate_id); - $duplicate->setObjId(0); - - ilAssSelfAssessmentQuestionFormatter::prepareQuestionForLearningModule($duplicate); - - $this->getChildNode()->setAttribute("QRef", "il__qst_" . $duplicate_id); - } - public static function getLangVars(): array { return ['ed_insert_pcqst', 'empty_question', 'pc_qst']; } - /** - * After page has been updated (or created) - */ - public static function afterPageUpdate( - ilPageObject $a_page, - DOMDocument $a_domdoc, - string $a_xml, - bool $a_creation - ): void { - global $DIC; - - $ilDB = $DIC->database(); - - $ilDB->manipulateF( - "DELETE FROM page_question WHERE page_parent_type = %s " . - " AND page_id = %s AND page_lang = %s", - array("text", "integer", "text"), - array($a_page->getParentType(), $a_page->getId(), $a_page->getLanguage()) - ); - - $xpath = new DOMXPath($a_domdoc); - $nodes = $xpath->query('//Question'); - $q_ids = array(); - foreach ($nodes as $node) { - $q_ref = $node->getAttribute("QRef"); - - $inst_id = ilInternalLink::_extractInstOfTarget($q_ref); - if (!($inst_id > 0)) { - $q_id = ilInternalLink::_extractObjIdOfTarget($q_ref); - if ($q_id > 0) { - $q_ids[$q_id] = $q_id; - } - } - } - foreach ($q_ids as $qid) { - $ilDB->replace( - "page_question", - [ - "page_parent_type" => ["text", $a_page->getParentType()], - "page_id" => ["integer", $a_page->getId()], - "page_lang" => ["text", $a_page->getLanguage()], - "question_id" => ["integer", $qid] - ], - [] - ); - } - } - - public static function beforePageDelete( - ilPageObject $a_page - ): void { - global $DIC; - - $ilDB = $DIC->database(); - - $ilDB->manipulateF( - "DELETE FROM page_question WHERE page_parent_type = %s " . - " AND page_id = %s AND page_lang = %s", - array("text", "integer", "text"), - array($a_page->getParentType(), $a_page->getId(), $a_page->getLanguage()) - ); - } - - public static function _getQuestionIdsForPage( - string $a_parent_type, - int $a_page_id, - string $a_lang = "-" - ): array { - global $DIC; - - $ilDB = $DIC->database(); - - $parent_type_array = explode(':', $a_parent_type); - - $res = $ilDB->queryF( - "SELECT * FROM page_question WHERE page_parent_type = %s " . - " AND page_id = %s AND page_lang = %s", - array("text", "integer", "text"), - array($parent_type_array[0], $a_page_id, $a_lang) - ); - $q_ids = array(); - while ($rec = $ilDB->fetchAssoc($res)) { - $q_ids[] = $rec["question_id"]; - } - - return $q_ids; - } - - public static function _getPageForQuestionId( - int $a_q_id, - string $a_parent_type = "" - ): ?array { - global $DIC; - - $ilDB = $DIC->database(); - - $set = $ilDB->query( - "SELECT * FROM page_question " . - " WHERE question_id = " . $ilDB->quote($a_q_id, "integer") - ); - while ($rec = $ilDB->fetchAssoc($set)) { - if ($a_parent_type == "" || $rec["page_parent_type"] == $a_parent_type) { - return array("page_id" => $rec["page_id"], "parent_type" => $rec["page_parent_type"]); - } - } - return null; - } - public function modifyPageContentPostXsl( - string $a_output, - string $a_mode, - bool $a_abstract_only = false + string $output, + string $mode, + bool $abstract_only = false ): string { - $qhtml = ""; - - if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { - // #14154 - $q_ids = $this->getQuestionIds(); - if (count($q_ids)) { - foreach ($q_ids as $q_id) { - $q_gui = assQuestionGUI::_getQuestionGUI("", $q_id); - // object check due to #16557 - if (is_object($q_gui->getObject()) && !$q_gui->getObject()->isComplete()) { - $a_output = str_replace( - "{{{{{Question;il__qst_" . $q_id . "}}}}}", - "" . $lng->txt("cont_empty_question") . "", - $a_output - ); - } - } - - // this exports the questions which is needed below - $qhtml = $this->getQuestionJsOfPage($a_mode == "edit", $a_mode); - - $a_output = "" . $a_output; - if (!self::$initial_done) { - $a_output = "" . $a_output; - self::$initial_done = true; - } - } - } else { - // set by T&A components - $qhtml = $this->getPage()->getPageConfig()->getQuestionHTML(); - - // address #19788 - if (!is_array($qhtml) || count($qhtml) == 0) { - // #14154 - $q_ids = $this->getQuestionIds(); - if (count($q_ids)) { - foreach ($q_ids as $k) { - $a_output = str_replace("{{{{{Question;il__qst_$k" . "}}}}}", " " . $lng->txt("copg_questions_not_supported_here"), $a_output); - } - } - } - } - - if (is_array($qhtml)) { - foreach ($qhtml as $k => $h) { - $a_output = str_replace("{{{{{Question;il__qst_$k" . "}}}}}", " " . $h, $a_output); - } - } - - return $a_output; - } - - /** - * Reset initial state (for exports) - */ - public static function resetInitialState(): void - { - self::$initial_done = false; - } - - public function getJavascriptFiles(string $a_mode): array - { - $js_files = array(); - - if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { - $js_files[] = 'assets/js/pure_rendering.js'; - $js_files[] = 'assets/js/question_handling.js'; - $js_files[] = 'assets/js/matchinginput.js'; - $js_files[] = 'assets/js/orderinghorizontal.js'; - $js_files[] = 'assets/js/orderingvertical.js'; - $js_files[] = 'assets/js/matching.js'; - - foreach ($this->getQuestionIds() as $qId) { - $qstGui = assQuestionGUI::_getQuestionGUI('', $qId); - $js_files = array_merge($js_files, $qstGui->getPresentationJavascripts()); - } + if ($this->pg_obj::class !== QstsQuestionPage::class) { + return $output; } - if (!$this->getPage()->getPageConfig()->getEnableSelfAssessmentScorm() && $a_mode != ilPageObjectGUI::PREVIEW - && $a_mode != "offline") { - $js_files[] = "./components/ILIAS/COPage/js/ilCOPageQuestionHandler.js"; - } + /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + $question = $this->pg_obj->getQuestion(); - return $js_files; + return mb_ereg_replace_callback( + self::ANSWER_FORM_PLACEHOLDER, + fn(array $matches): string => $question + ->getAnswerFormByIdString($matches[1])?->getTypeGenericProperties() + ->getAdditionalText() ?? '', + $output + ); } public function getCssFiles(string $a_mode): array @@ -283,123 +66,29 @@ public function getCssFiles(string $a_mode): array return array(); } - public function getOnloadCode(string $a_mode): array - { - $code = array(); - - if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { - if (!$this->getPage()->getPageConfig()->getEnableSelfAssessmentScorm() && $a_mode != ilPageObjectGUI::PREVIEW - && $a_mode != "offline" && $a_mode !== "edit") { - $ilCtrl->setParameterByClass(strtolower(get_class($this->getPage())) . "gui", "page_id", $this->getPage()->getId()); - $url = $ilCtrl->getLinkTargetByClass(strtolower(get_class($this->getPage())) . "gui", "processAnswer", "", true, false); - $code[] = "ilCOPageQuestionHandler.initCallback('" . $url . "');"; - } - - if ($this->getPage()->getPageConfig()->getDisableDefaultQuestionFeedback()) { - $code[] = "ilias.questions.default_feedback = false;"; - } - - $code[] = self::getJSTextInitCode($this->getPage()->getPageConfig()->getLocalizationLanguage()) . ' il.COPagePres.updateQuestionOverviews();'; - } - - $q_ids = $this->getQuestionIds(); - - // call renderers - foreach ($q_ids as $q_id) { - $code[] = "if (typeof renderILQuestion$q_id === 'function') {renderILQuestion$q_id();}"; - } - - // init answer status - $get_stored_tries = $this->getPage()->getPageConfig()->getUseStoredQuestionTries(); - if ($get_stored_tries) { - if (count($q_ids) > 0) { - foreach ($q_ids as $q_id) { - $as = ilPageQuestionProcessor::getAnswerStatus($q_id, $ilUser->getId()); - $code[] = "ilias.questions.initAnswer(" . $q_id . ", " . (int) ($as["try"] ?? 0) . ", " . ($as["passed"] ? "true" : "null") . ");"; - } - } - } - return $code; - } - - /** - * Get js txt init code - */ - public static function getJSTextInitCode(string $a_lang): string - { - global $DIC; - - $lng = $DIC->language(); - $ilUser = $DIC->user(); - - if ($a_lang == "") { - $a_lang = $ilUser->getLanguage(); - } - - return - ' - ilias.questions.txt.wrong_answers = "' . $lng->txtlng("content", "cont_wrong_answers", $a_lang) . '"; - ilias.questions.txt.wrong_answers_single = "' . $lng->txtlng("content", "cont_wrong_answers_single", $a_lang) . '"; - ilias.questions.txt.tries_remaining = "' . $lng->txtlng("content", "cont_tries_remaining", $a_lang) . '"; - ilias.questions.txt.please_try_again = "' . $lng->txtlng("content", "cont_please_try_again", $a_lang) . '"; - ilias.questions.txt.all_answers_correct = "' . $lng->txtlng("content", "cont_all_answers_correct", $a_lang) . '"; - ilias.questions.txt.enough_answers_correct = "' . $lng->txtlng("content", "cont_enough_answers_correct", $a_lang) . '"; - ilias.questions.txt.nr_of_tries_exceeded = "' . $lng->txtlng("content", "cont_nr_of_tries_exceeded", $a_lang) . '"; - ilias.questions.txt.correct_answers_separator = "' . $lng->txtlng("assessment", "or", $a_lang) . '"; - ilias.questions.txt.correct_answers_shown = "' . $lng->txtlng("content", "cont_correct_answers_shown", $a_lang) . '"; - ilias.questions.txt.correct_answers_also = "' . $lng->txtlng("content", "cont_correct_answers_also", $a_lang) . '"; - ilias.questions.txt.correct_answer_also = "' . $lng->txtlng("content", "cont_correct_answer_also", $a_lang) . '"; - ilias.questions.txt.ov_all_correct = "' . $lng->txtlng("content", "cont_ov_all_correct", $a_lang) . '"; - ilias.questions.txt.ov_some_correct = "' . $lng->txtlng("content", "cont_ov_some_correct", $a_lang) . '"; - ilias.questions.txt.ov_wrong_answered = "' . $lng->txtlng("content", "cont_ov_wrong_answered", $a_lang) . '"; - ilias.questions.txt.please_select = "' . $lng->txtlng("content", "cont_please_select", $a_lang) . '"; - ilias.questions.txt.ov_preview = "' . $lng->txtlng("content", "cont_ov_preview", $a_lang) . '"; - ilias.questions.txt.submit_answers = "' . $lng->txtlng("content", "cont_submit_answers", $a_lang) . '"; - ilias.questions.refresh_lang(); - '; - } - - public function getQuestionJsOfPage( - bool $a_no_interaction, - string $a_mode - ): array { - $q_ids = $this->getQuestionIds(); - $js = array(); - if (count($q_ids) > 0) { - foreach ($q_ids as $q_id) { - $q_exporter = new ilQuestionExporter($a_no_interaction); - $image_path = ""; - if ($a_mode == "offline") { - if ($this->getPage()->getParentType() == "sahs") { - $image_path = "./objects/"; - } - if ($this->getPage()->getParentType() == "lm") { - $image_path = "./assessment/0/" . $q_id . "/images/"; - } - } - $js[$q_id] = $q_exporter->exportQuestion($q_id, $image_path, $a_mode); - } - } - return $js; + public function create( + Uuid $answer_form_id + ): void { + $this->createInitialChildNode( + $this->hier_id, + '', + self::ANSWER_FORM_ELEMENT_TAG, + [self::ANSWER_FORM_ID_ATTRIBUT => $answer_form_id->toString()] + ); } - protected function getQuestionIds(): array + private function getAnswerFormIds(): array { - $dom = $this->getPage()->getDomDoc(); - $q_ids = []; - $nodes = $this->dom_util->path($dom, "//Question"); - foreach ($nodes as $node) { - $qref = $node->getAttribute("QRef"); - $inst_id = ilInternalLink::_extractInstOfTarget($qref); - $obj_id = ilInternalLink::_extractObjIdOfTarget($qref); - - if (!($inst_id > 0)) { - if ($obj_id > 0) { - $q_ids[] = $obj_id; - } - } - } - return $q_ids; + return array_reduce( + $this->dom_util->path( + $this->getPage()->getDomDoc(), + '//' . self::ANSWER_FORM_ELEMENT_TAG + ), + function (array $c, \DomNode $node): Uuid { + $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUT); + }, + [] + ); } public static function handleCopiedContent( diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index e97a2c11526d..d46608aab8b1 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -18,8 +18,7 @@ declare(strict_types=1); -use ILIAS\Questions\Legacy\LocalDIC; -use ILIAS\Questions\Presentation\Edit; +use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Data\Factory as DataFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -34,18 +33,18 @@ class ilPCAnswerFormGUI extends ilPageContentGUI private readonly Edit $edit_view; public function __construct( - ilPageObject $a_pg_obj, - ?ilPageContent $a_content_obj, - string $a_hier_id, - string $a_pc_id = "" + ilPageObject $pg_obj, + ?ilPageContent $content_obj, + string $hier_id, + string $pc_id = '' ) { global $DIC; $this->tabs = $DIC['ilTabs']; $this->ui_renderer = $DIC['ui.renderer']; $this->data_factory = new DataFactory(); - $this->edit_view = LocalDIC::dic()[Edit::class]; - parent::__construct($a_pg_obj, $a_content_obj, $a_hier_id, $a_pc_id); + parent::__construct($pg_obj, $content_obj, $hier_id, $pc_id); + $this->edit_view = $this->pg_obj->getEditView(); } public function executeCommand() @@ -57,28 +56,32 @@ public function executeCommand() public function insertCmd(): void { $this->setInsertTabs(); + $content_obj = new ilPCAnswerForm($this->pg_obj); + $content_obj->setHierId($this->hier_id); $this->tpl->setContent( - $this->ui_renderer->render( - $this->edit_view->createAnswerForm( - $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') - ) - ) - ) + $this->edit_view->createAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ), + $this->pg_obj->getQuestion(), + $content_obj + )->render($this->ui_renderer) ); } public function editCmd(): void { $this->setInsertTabs(); + $content_obj = new ilPCAnswerForm($this->pg_obj); + $content_obj->setHierId($this->hier_id); $this->tpl->setContent( - $this->ui_renderer->render( - $this->edit_view->editAnswerForm( - $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') - ) - ) - ) + $this->edit_view->editAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ), + $this->pg_obj->getQuestion(), + $content_obj + )->render($this->ui_renderer) ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 094c6c7931eb..2c44dd759687 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -29,7 +29,7 @@ interface Definition { public function getLabel(Language $lng): string; public function buildProperties( - TypeGenericData $type_generic_properties, + TypeGenericProperties $type_generic_properties, array $type_specific_data ): Properties; public function getPersistence(): Persistence; diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index ae0fe0e8853e..a281dde81f12 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -22,6 +22,8 @@ use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; class Factory { @@ -39,7 +41,7 @@ public function __construct( ) { $this->available_answer_form_types = array_reduce( $available_answer_form_types, - function (array $c, Type $v) { + function (array $c, Definition $v) { $c[$this->getHashedClass($v::class)] = $v; return $c; }, @@ -47,15 +49,20 @@ function (array $c, Type $v) { ); } + public function getAvailableDefinitions(): array + { + return array_values($this->available_answer_form_types); + } + /** * @return array */ - public function getAnswerFormTypesArrayForSelect(): array + public function getAnswerFormTypesArrayForSelect(Language $lng): array { return array_reduce( $this->available_answer_form_types, - function (array $c, Definition $v): array { - $c[$this->getHashedClass($v::class)] = $v->getLabel($this->lng); + function (array $c, Definition $v) use ($lng): array { + $c[$this->getHashedClass($v::class)] = $v->getLabel($lng); return $c; }, [] diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php index 9b046e733725..6d97fbe24c5e 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -21,16 +21,16 @@ namespace ILIAS\Questions\AnswerForm; use ILIAS\Questions\Question\Persistence\Column; -use ILIAS\Questions\Question\Persistence\SelectQuery; +use ILIAS\Questions\Question\Persistence\Query; use ILIAS\Questions\Question\Persistence\TableNameBuilder; use ILIAS\Questions\Question\Persistence\TableNameSpace; interface Persistence { public function getPublicNameSpace(): TableNameSpace; - public function completeSelectQuery( + public function completeQuery( TableNameBuilder $table_name_builder, - SelectQuery $select_query, + Query $query, Column $base_table_id_column, - ): SelectQuery; + ): Query; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index 28385aee6d83..2a558d7e79ef 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -20,7 +20,15 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; + interface Properties { - public function getTypeGenericData(): TypeGenericData; + public function getAnswerFormId(): ?Uuid; + public function getQuestionId(): ?Uuid; + public function getTypeGenericProperties(): TypeGenericProperties; + public function getBasicPropertiesForListing( + Language $lng + ): array; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index 2dd5fbb85e41..4d76d2c80a98 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -20,27 +20,22 @@ namespace ILIAS\Questions\AnswerForm\Views; -use ILIAS\Questions\Question\Persistence\ManipulateQuery; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; +use ILIAS\Questions\Presentation\Layout\Definitions\EditOverview; +use ILIAS\Questions\Presentation\Layout\Definitions\Environment; interface Edit { public function create( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step - ): array|ManipulateQuery; + Environment $environment + ): EditForm|Properties; public function edit( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step - ): array|ManipulateQuery; + Environment $environment + ): EditOverview|EditForm|Properties; public function other( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $cmd - ): array|ManipulateQuery; + Environment $environment + ): EditForm|Properties; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index c2bc3f878064..e6a9655e911b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -22,7 +22,7 @@ use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\TypeGenericData; +use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; @@ -49,17 +49,12 @@ public function getLabel(Language $lng): string } public function buildProperties( - TypeGenericData $type_generic_data, + TypeGenericProperties $type_generic_data, array $type_specific_data ): Properties { return $this->properties_factory->fromData($type_generic_data, $type_specific_data); } - public function getProperties(): Properties - { - return $this->properties; - } - public function getPersistence(): Persistence { return $this->persistence; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php index 80f03cba1607..8789adabaa70 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -21,7 +21,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\AnswerForm\Persistence as PersistenceInterface; -use ILIAS\Questions\Question\Persistence\SelectQuery; +use ILIAS\Questions\Question\Persistence\TableTypes; +use ILIAS\Questions\Question\Persistence\Query; use ILIAS\Questions\Question\Persistence\Join; use ILIAS\Questions\Question\Persistence\Column; use ILIAS\Questions\Question\Persistence\JoinType; @@ -34,26 +35,30 @@ class Persistence implements PersistenceInterface { private const string ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array ANSWER_FORM_TABLE_COLUMNS = [ - 'case_sensitive', - 'identical_responses_valid', - 'max_chars', - 'min_auto_complete' + 'scoring_identical_responses', + 'combinations_activated' ]; private const string ANSWER_INPUTS_TABLE_ID_COLUMN = 'id'; private const string ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array ANSWER_INPUTS_TABLE_COLUMNS = [ 'id AS answer_input_id', + 'position', 'gap_type', - 'max_chars' + 'max_chars', + 'step_size', + 'text_matching_method', + 'shuffle_answer_options' ]; private const string ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_input_id'; private const array ANSWER_OPTIONS_TABLE_COLUMNS = [ 'id AS answer_option_id', 'position', - 'value', - 'points' + 'text_value', + 'points', + 'lower_limit', + 'upper_limit' ]; private const string COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; @@ -73,67 +78,77 @@ public function getPublicNameSpace(): TableNameSpace return $this->table_namespace; } - public function completeSelectQuery( + public function completeQuery( TableNameBuilder $table_name_builder, - SelectQuery $select_query, + Query $query, Column $base_table_id_column - ): SelectQuery { - return $select_query->withAdditionalJoin( + ): Query { + $answer_form_specific_table = TableTypes::TypeSpecificAnswerForms + ->getTable($table_name_builder); + $answer_input_table = TableTypes::AnswerInputs + ->getTable($table_name_builder); + $answer_options_table = TableTypes::AnswerOptions + ->getTable($table_name_builder); + $combinations_table = TableTypes::Additional + ->getTable($table_name_builder, 'combinations'); + + return $query->withAdditionalJoin( new Join( $base_table_id_column, new Column( - $table_name_builder->getTypeSpecificAnswerFormsTableName(), + $answer_form_specific_table, self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN - ) + ), + JoinType::Left ) )->withAdditionalSelect( new Select( - $table_name_builder->getTypeSpecificAnswerFormsTableName(), + $answer_form_specific_table, self::ANSWER_FORM_TABLE_COLUMNS ) )->withAdditionalJoin( new Join( $base_table_id_column, new Column( - $table_name_builder->getAnswerInputsTableName(), + $answer_input_table, self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN ), JoinType::Left ) )->withAdditionalSelect( new Select( - $table_name_builder->getAnswerInputsTableName(), + $answer_input_table, self::ANSWER_INPUTS_TABLE_COLUMNS ) )->withAdditionalJoin( new Join( new Column( - $table_name_builder->getAnswerInputsTableName(), + $answer_input_table, self::ANSWER_INPUTS_TABLE_ID_COLUMN ), new Column( - $table_name_builder->getAnswerOptionsTableName(), + $answer_options_table, self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ), JoinType::Left ) )->withAdditionalSelect( new Select( - $table_name_builder->getAnswerOptionsTableName(), + $answer_options_table, self::ANSWER_OPTIONS_TABLE_COLUMNS ) )->withAdditionalJoin( new Join( $base_table_id_column, new Column( - $table_name_builder->getAdditionalTableName('combinations'), + $combinations_table, self::COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN ), JoinType::Left ) )->withAdditionalSelect( new Select( - $table_name_builder->getAdditionalTableName('combinations'), + $combinations_table, self::COMBINATIONS_TABLE_COLUMNS ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php index 6d9e68728760..1bde6f2050c1 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php @@ -25,14 +25,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; -use ILIAS\Data\UUID\Factory as UuidFactory; -use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; -use ILIAS\Refinery\Factory as Refinery; class Factory { public function __construct( - private readonly UuidFactory $uuid_factory, private readonly ClozeTextFactory $cloze_text_factory, private readonly GapsFactory $gaps_factory ) { @@ -42,11 +38,28 @@ public function fromData( TypeGenericProperties $type_generic_properties, array $type_specific_data ): Properties { + if ($type_specific_data === []) { + return new Properties( + $type_generic_properties->getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + $this->gaps_factory->getEmptyGapsObject() + ); + } + return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), - $this->cloze_text_factory->buildFromTextString($type_generic_properties->getAdditionalText()), - $type_generic_properties->getAdditionalTextLegacy() + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + $type_specific_data['gaps'], + $type_specific_data['identical_scoring'], + $type_specific_data['combinations_enabled'] ); } @@ -64,13 +77,4 @@ public function fromForm( ->withScoringOfIdenticalResponses($scoring_of_identical_responses) ->withCombinationsEnabled($combinations_enabled); } - - public function buildDefault(): Properties - { - return new Properties( - null, - $this->cloze_text_factory->buildFromTextString(''), - $this->gaps_factory->getEmptyGapsObject() - ); - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php index 8b40e4edbfce..b62ea994ed55 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php @@ -26,6 +26,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -35,7 +37,7 @@ class Properties implements PropertiesInterface { - private const string FORM_KEY_ID = 'id'; + private const string FORM_KEY_ID = 'form_id'; private const string FORM_KEY_CLOZE_TEXT = 'cloze_text'; private const string FORM_KEY_IDENTICAL_SCORING = 'identical_scoring'; private const string FORM_KEY_ENABLE_COMBINATIONS = 'enable_combinations'; @@ -45,17 +47,27 @@ class Properties implements PropertiesInterface * @param array $gaps */ public function __construct( - private readonly ?Uuid $answer_form_id, - private readonly ?Uuid $question_id, + private readonly Uuid $answer_form_id, + private readonly Uuid $question_id, private Text $cloze_text, + private readonly string $legacy_cloze_text, private Gaps $gaps, private ScoringIdentical $scoring_identical = ScoringIdentical::ScoreAll, - private bool $combinations_enabled = false, - private readonly string $legacy_cloze_text = '' + private bool $combinations_enabled = false ) { } - public function getTypeGenericData(): TypeGenericProperties + public function getAnswerFormId(): ?Uuid + { + return $this->answer_form_id; + } + + public function getQuestionId(): Uuid + { + return $this->question_id; + } + + public function getTypeGenericProperties(): TypeGenericProperties { return new TypeGenericProperties( $this->answer_form_id, @@ -68,11 +80,6 @@ public function getTypeGenericData(): TypeGenericProperties ); } - public function getAnswerFormId(): ?Uuid - { - return $this->answer_form_id; - } - public function getClozeText(): Text { return $this->cloze_text; @@ -126,6 +133,16 @@ public function withGaps(Gaps $gaps): self return $clone; } + public function getBasicPropertiesForListing(Language $lng): array + { + return [ + $lng->txt('cloze_text') => $this->cloze_text + ->getRenderedMarkdownForEditingPresentation($this->gaps), + $lng->txt('score_identical') => $this->scoring_identical + ->getTranslatedOptionName($lng) + ]; + } + public function buildBasicEditingInputs( Language $lng, FieldFactory $ff, @@ -162,15 +179,15 @@ public function buildBasicEditingInputs( ); } - public function buildBasicEditingInputsHidden( + public function buildCarryInputs( FieldFactory $ff ): Group { return $ff->group( [ self::FORM_KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()), - self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getHiddenInput($ff) + self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getCarryInputs($ff) ->withDedicatedName(self::FORM_KEY_CLOZE_TEXT), - self::FORM_KEY_GAPS_TO_EDIT => $this->gaps->getHiddenInput($ff) + self::FORM_KEY_GAPS_TO_EDIT => $this->gaps->getCarryInputs($ff) ->withDedicatedName(self::FORM_KEY_GAPS_TO_EDIT), self::FORM_KEY_IDENTICAL_SCORING => $ff->hidden() ->withDedicatedName(self::FORM_KEY_IDENTICAL_SCORING) @@ -182,43 +199,54 @@ public function buildBasicEditingInputsHidden( ); } - public function withValuesFromPost( + public function withValuesFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + ClozeTextFactory $cloze_text_factory, + GapsFactory $gaps_factory, + CarryWrapper $carry ): Properties { - $cloze_text = $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_CLOZE_TEXT, - $refinery->custom()->transformation( - fn(string $v): ClozeText => $this->cloze_text_factory->buildFromHiddenInputString($v) - ) + $clone = clone $this; + $clone->cloze_text = $carry->retrieve( + self::FORM_KEY_CLOZE_TEXT, + $refinery->byTrying([ + $refinery->custom()->transformation( + fn(?string $v): Text => $v === null + ? throw new \InvalidArgumentException() + : $cloze_text_factory->buildFromHiddenInputString($v) + ), + $refinery->always($clone->cloze_text) + ]) ); - $scoring_of_identical_responses = $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_IDENTICAL_SCORING, - $refinery->custom()->transformation( - static fn(string $v): ScoringIdentical => ScoringIdentical::tryFrom($v) - ?? $properties->getScoringOfIdenticalResponses() - ) + $clone->scoring_identical = $carry->retrieve( + self::FORM_KEY_IDENTICAL_SCORING, + $refinery->byTrying([ + $refinery->custom()->transformation( + fn(?string $v): ScoringIdentical => $v === null + ? throw new \InvalidArgumentException() + : ScoringIdentical::tryFrom($v) ?? $clone->scoring_identical + ), + $refinery->always($clone->scoring_identical) + ]) ); - $combinations_enabled = $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_ENABLE_COMBINATIONS, - $refinery->kindlyTo()->bool() + $clone->combinations_enabled = $carry->retrieve( + self::FORM_KEY_ENABLE_COMBINATIONS, + $refinery->byTrying([ + $refinery->kindlyTo()->bool(), + $refinery->always($clone->combinations_enabled) + ]) ); - $gaps = $cloze_text->updateGapsFromMarkdown($properties->getGaps()) - ->withValuesFromPost( - $refinery, - $post_wrapper, - $this->gaps_factory, - $form_input_path . '/' . self::FORM_KEY_GAPS_TO_EDIT - ); - - return $properties - ->withClozeText($cloze_text) - ->withScoringOfIdenticalResponses($scoring_of_identical_responses) - ->withCombinationsEnabled($combinations_enabled) - ->withGaps($gaps); + $clone->gaps = $carry->retrieve( + self::FORM_KEY_GAPS_TO_EDIT, + $clone->cloze_text->updateGapsFromMarkdown($this->getGaps()) + ->getFromCarryTransformation( + $refinery, + $gaps_factory + ) + ); + + return $clone; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php index 38a11609da62..1d722fbdfc64 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php @@ -21,7 +21,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText; use ILIAS\Data\Text\Factory as TextFactory; -use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Refinery\Factory as Refinery; use Mustache\Engine; @@ -30,7 +29,6 @@ class Factory public function __construct( private readonly Refinery $refinery, private readonly Engine $mustache_engine, - private readonly UuidFactory $uuid_factory, private readonly TextFactory $text_factory ) { } @@ -52,6 +50,6 @@ public function buildFromHiddenInputString(string $text): Text private function unmaskTextFromOutputInHiddenInput(string $text): string { - return str_replace(['{{', '}}'], ['{{', '}}'], $text); + return str_replace(['\{\{', '\}\}'], ['{{', '}}'], $text); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 5903fa486fc0..3bb2bed3321b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -64,7 +64,7 @@ public function getInput( ->withValue($this->cloze_text->getRawRepresentation()); } - public function getHiddenInput(FieldFactory $ff): HiddenInput + public function getCarryInputs(FieldFactory $ff): HiddenInput { return $ff->hidden()->withValue($this->getTextForOutputInHiddenInput()); } @@ -74,7 +74,7 @@ public function getRawRepresentationForPersistence(): string return $this->cloze_text->getRawRepresentation(); } - public function getRenderedMarkdown( + public function getRenderedMarkdownForEditingPresentation( Gaps $gaps ): string { return $this->mustache_engine->render( @@ -152,6 +152,10 @@ private function hasAtLeastOneGap(): bool private function getTextForOutputInHiddenInput(): string { - return str_replace(['{{', '}}'], ['{{', '}}'], $this->cloze_text->getRawRepresentation()); + return str_replace( + ['{{', '}}'], + ['\{\{', '\}\}'], + $this->cloze_text->getRawRepresentation() + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php index 19e3c1f9a30f..a67115f323e3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php @@ -30,6 +30,12 @@ enum ScoringIdentical: string case ScoreAll = 'score_all'; case OnlyScoreDistinct = 'score_distinct'; + public function getTranslatedOptionName( + Language $lng + ): string { + return $lng->txt($this->value); + } + public static function buildInput( Language $lng, FieldFactory $ff, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 06b38b1f56dd..996257b5e236 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Factory as DataFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Factory as PropertiesFactory; use ILIAS\Language\Language; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -30,7 +30,7 @@ class Factory public function __construct( private readonly UuidFactory $uuid_factory, - private readonly DataFactory $data_factory, + private readonly PropertiesFactory $data_factory, array $available_gap_types ) { foreach ($available_gap_types as $type) { @@ -62,7 +62,7 @@ public function getNewGap( return new Gap( $answer_input_id, $position, - $this->data_factory->getDefaultDataObject($answer_input_id) + $this->data_factory->getDefaultProperties($answer_input_id) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index dc0be37e81e3..5cbc2b73cd3c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -20,11 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; -use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; @@ -39,7 +40,7 @@ class Gap public function __construct( private Uuid $answer_input_id, private int $position, - private Data $data, + private Properties $properties, private ?Type $type = null ) { } @@ -75,15 +76,15 @@ public function withType(Type $type): self return $clone; } - public function getData(): Data + public function getProperties(): Properties { - return $this->data; + return $this->properties; } - public function withData(Data $data): self + public function withProperties(Properties $properties): self { $clone = clone $this; - $clone->data = $data; + $clone->properties = $properties; return $clone; } @@ -117,7 +118,7 @@ public function getEditAnswerOptionsSection( FieldFactory $ff ): Section { $section = $ff->section( - $this->type->getEditAnswerOptionsInputs($this->data), + $this->type->getEditAnswerOptionsInputs($this->properties), "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" ); @@ -137,7 +138,7 @@ public function getEditPointsSection( FieldFactory $ff ): Section { $section = $ff->section( - $this->type->getEditPointsInputs($this->data->getAnswerOptions()), + $this->type->getEditPointsInputs($this->properties->getAnswerOptions()), "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" ); @@ -152,40 +153,44 @@ public function getEditPointsSection( ); } - public function getHiddenInput( + public function getCarryInputs( FieldFactory $ff ): Group { return $ff->group([ self::FORM_KEY_TYPE => $ff->hidden()->withValue($this->type?->getIdentifier() ?? '') ->withDedicatedName(self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId()), - self::FORM_KEY_DATA => $this->data->getHiddenInput($ff) + self::FORM_KEY_DATA => $this->properties->getCarryInputs($ff) ->withDedicatedName(self::FORM_KEY_DATA . $this->getShortenedAnswerInputId()) ]); } - public function withValuesFromPost( + public function getFromCarryTransformation( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - Factory $gaps_factory, - string $form_input_path - ): self { - $available_gap_types = $gaps_factory->getAvailableGapTypes(); - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->custom()->transformation( - fn(?string $v): self => $available_gap_types[$v] - ? $this->withType($available_gap_types[$v]) - : $this - ), - $refinery->always($this) - ]) - )->withData( - $this->data->withValuesFromPost( - $refinery, - $post_wrapper, - $form_input_path . '/' . self::FORM_KEY_DATA . $this->getShortenedAnswerInputId() - ) + Factory $gaps_factory + ): Transformation { + return $refinery->custom()->transformation( + function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { + if ($v === null) { + return $this; + } + $available_gap_types = $gaps_factory->getAvailableGapTypes(); + return $v->retrieve( + self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->custom()->transformation( + fn(?string $v): self => $available_gap_types[$v] + ? $this->withType($available_gap_types[$v]) + : $this + ), + $refinery->always($this) + ]) + )->withProperties( + $v->retrieve( + self::FORM_KEY_DATA . $this->getShortenedAnswerInputId(), + $this->properties->getFromCarryTransformation($refinery) + ) + ); + } ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 1e93f6b0b341..6585d277ae40 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -20,10 +20,11 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; -use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; @@ -197,13 +198,13 @@ function (array $c, Gap $v) use ($lng, $ff): array { ); } - public function getHiddenInput(FieldFactory $ff): Group + public function getCarryInputs(FieldFactory $ff): Group { return $ff->group( array_reduce( $this->gaps, function (array $c, Gap $v) use ($ff): array { - $c[$v->getAnswerInputId()->toString()] = $v->getHiddenInput($ff) + $c[$v->getAnswerInputId()->toString()] = $v->getCarryInputs($ff) ->withDedicatedName($v->getAnswerInputId()->toString()); return $c; }, @@ -212,23 +213,29 @@ function (array $c, Gap $v) use ($ff): array { ); } - public function withValuesFromPost( + public function getFromCarryTransformation( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - Factory $gaps_factory, - string $form_input_path - ): self { - $clone = clone $this; - $clone->gaps = array_map( - fn(Gap $v): Gap => $v->withValuesFromPost( - $refinery, - $post_wrapper, - $gaps_factory, - $form_input_path . '/' . $v->getAnswerInputId()->toString() - ), - $this->gaps + Factory $gaps_factory + ): Transformation { + return $refinery->custom()->transformation( + function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { + if ($v === null) { + return $this; + } + $clone = clone $this; + $clone->gaps = array_map( + fn(Gap $gap): Gap => $v->retrieve( + $gap->getAnswerInputId()->toString(), + $gap->getFromCarryTransformation( + $refinery, + $gaps_factory + ) + ), + $this->gaps + ); + return $clone; + } ); - return $clone; } private function extractIdFromTagName(string $tag_name): string diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index 9295f0129a63..7d23ee8c2c66 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; use ILIAS\FileUpload\MimeType; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -48,14 +48,14 @@ public function getIdentifier(): string return 'long_menu'; } - public function getEditAnswerOptionsInputs(Data $data): array + public function getEditAnswerOptionsInputs(Properties $properties): array { $ff = $this->ui_factory->input()->field(); return [ 'answer_options' => $ff->tag( $this->lng->txt('answer_options'), [] - )->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + )->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'upload_answer_options' => $ff->file( new UploadAnswerOptionsGUI(), $this->lng->txt('upload_answer_options'), @@ -64,16 +64,16 @@ public function getEditAnswerOptionsInputs(Data $data): array 'min_autocomplete' => $ff->numeric( $this->lng->txt('min_autocomplete') )->withRequired(true) - ->withValue($data->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), + ->withValue($properties->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), 'options_awarding_points' => $ff->tag( $this->lng->txt('answer_options'), - $data->getAnswerOptions()->getTagsArrayFromAnswerOptions() + $properties->getAnswerOptions()->getTagsArrayFromAnswerOptions() ) ->withRequired(true) ->withValue( array_map( fn(AnswerOption $v): string => $v->getTextValue(), - $data->getAnswerOptions()->getAnswerOptionsAwardingPoints() + $properties->getAnswerOptions()->getAnswerOptionsAwardingPoints() ) ) ]; @@ -124,11 +124,11 @@ function (array $vs): bool { public function getBuildGapTransformation(Gap $gap): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withData( - $gap->getData() + fn(array $vs): Gap => $gap->withProperties( + $gap->getProperties() ->withMinAutocomplete($vs['min_autocomplete']) ->withAnswerOptions( - $gap->getData()->getAnswerOptions()->withAnswerOptionsFromTags( + $gap->getProperties()->getAnswerOptions()->withAnswerOptionsFromTags( array_merge( $vs['answer_options'], $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index 52302d45edfd..d58ba257a21b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -47,22 +47,22 @@ public function getIdentifier(): string return 'numeric'; } - public function getEditAnswerOptionsInputs(Data $data): array + public function getEditAnswerOptionsInputs(Properties $properties): array { - $answer_option = $data->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); + $answer_option = $properties->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); $ff = $this->ui_factory->input()->field(); return [ 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) - ->withStepSize($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) ->withRequired(true) ->withValue($answer_option->getLowerLimit()), 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) - ->withStepSize($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) ->withValue($answer_option->getUpperLimit()), 'step_size' => $ff->numeric($this->lng->txt('step_size')) ->withStepSize(0.000001) ->withRequired(true) - ->withValue($data->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withValue($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) ]; } @@ -108,12 +108,12 @@ public function getEditPointsSectionConstraint(): ?Constraint public function getBuildGapTransformation(Gap $gap): Transformation { - $data = $gap->getData(); + $properties = $gap->getProperties(); return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withData( - $data->withAnswerOptions( - $data->getAnswerOptions()->withAnswerOptions([ - $data->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) + fn(array $vs): Gap => $gap->withProperties( + $properties->withAnswerOptions( + $properties->getAnswerOptions()->withAnswerOptions([ + $properties->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) ->withLowerLimit($vs['lower_limit']) ->withUpperLimit($vs['upper_limit']) ]) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php index 655760c8418c..69140125a5f5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php @@ -18,19 +18,19 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; use ILIAS\Questions\Question\Persistence\ManipulateQuery; use ILIAS\Data\UUID\Uuid; class AnswerOption { - public const string KEY_ID = 'id'; - public const string KEY_POSITION = 'position'; - public const string KEY_TEXT_VALUE = 'text_value'; - public const string KEY_LOWER_LIMIT = 'lower_limit'; - public const string KEY_UPPER_LIMIT = 'upper_limit'; - public const string KEY_AVAILABLE_POINTS = 'points'; + public const string FORM_KEY_ID = 'id'; + public const string FORM_KEY_POSITION = 'position'; + public const string FORM_KEY_TEXT_VALUE = 'text_value'; + public const string FORM_KEY_LOWER_LIMIT = 'lower_limit'; + public const string FORM_KEY_UPPER_LIMIT = 'upper_limit'; + public const string FORM_KEY_AVAILABLE_POINTS = 'points'; public function __construct( private readonly Uuid $answer_option_id, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php index 73622d6f9bd4..4bf7df0f3f20 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; use ILIAS\Questions\Question\Persistence\ManipulateQuery; use ILIAS\Refinery\Factory as Refinery; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php index d3a7b4ead468..09e2cbd9a0de 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; @@ -32,10 +32,10 @@ public function __construct( ) { } - public function getDefaultDataObject( + public function getDefaultProperties( Uuid $answer_input_id - ): Data { - return new Data( + ): Properties { + return new Properties( $answer_input_id, new AnswerOptions( $this, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php index b4fe36d1a9d6..3c7d8ebfad70 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php @@ -18,16 +18,17 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; use ILIAS\Questions\Question\Definitions\TextMatchingOptions; -use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; +use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Implementation\Component\Input\Field\Group; -class Data +class Properties { private const string FORM_KEY_MAX_CHARS = 'max_chars'; private const string FORM_KEY_STEP_SIZE = 'step_size'; @@ -37,7 +38,7 @@ class Data private const string FORM_KEY_ANSWER_OPTIONS = 'answer_options'; /** - * @param array $answer_options + * @param array $answer_options */ public function __construct( private readonly Uuid $answer_input_id, @@ -127,7 +128,7 @@ public function withAnswerOptions(AnswerOptions $answer_options): self return $clone; } - public function getHiddenInput(FieldFactory $ff): Group + public function getCarryInputs(FieldFactory $ff): Group { $inputs = []; if ($this->max_chars !== null) { @@ -161,28 +162,29 @@ public function getHiddenInput(FieldFactory $ff): Group return $ff->group($inputs); } - public function withValuesFromPost( - Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path - ): Data { - $clone = clone $this; - $clone->max_chars = $this->retrieveMaxCharsFromPost($refinery, $post_wrapper, $form_input_path); - $clone->step_size = $this->retrieveStepSizeFromPost($refinery, $post_wrapper, $form_input_path); - $clone->text_matching_method = $this->retrieveTextMatchingMethodFromPost($refinery, $post_wrapper, $form_input_path); - $clone->min_autocomplete = $this->retrieveMinAutocompleteFromPost($refinery, $post_wrapper, $form_input_path); - $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromPost($refinery, $post_wrapper, $form_input_path); - $clone->answer_options = $this->retrieveAnswerOptionsFromPost($refinery, $post_wrapper, $form_input_path); - return $clone; + public function getFromCarryTransformation( + Refinery $refinery + ): Transformation { + return $refinery->custom()->transformation( + function (CarryWrapper $v) use ($refinery): self { + $clone = clone $this; + $clone->max_chars = $this->retrieveMaxCharsFromCarry($refinery, $v); + $clone->step_size = $this->retrieveStepSizeFromCarry($refinery, $v); + $clone->text_matching_method = $this->retrieveTextMatchingMethodFromCarry($refinery, $v); + $clone->min_autocomplete = $this->retrieveMinAutocompleteFromCarry($refinery, $v); + $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromCarry($refinery, $v); + $clone->answer_options = $this->retrieveAnswerOptionsFromCarry($refinery, $v); + return $clone; + } + ); } - private function retrieveMaxCharsFromPost( + private function retrieveMaxCharsFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): ?int { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->int(), $refinery->always($this->getMaxChars()) @@ -190,13 +192,12 @@ private function retrieveMaxCharsFromPost( ); } - private function retrieveStepSizeFromPost( + private function retrieveStepSizeFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): ?float { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->float(), $refinery->always($this->getStepSize()) @@ -204,13 +205,12 @@ private function retrieveStepSizeFromPost( ); } - private function retrieveTextMatchingMethodFromPost( + private function retrieveTextMatchingMethodFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): ?TextMatchingOptions { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( fn(?string $v): ?TextMatchingOptions => $v !== null ? TextMatchingOptions::tryFrom($v) @@ -219,13 +219,12 @@ private function retrieveTextMatchingMethodFromPost( ); } - private function retrieveMinAutocompleteFromPost( + private function retrieveMinAutocompleteFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): ?int { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->int(), $refinery->always($this->getMinAutocomplete()) @@ -233,13 +232,12 @@ private function retrieveMinAutocompleteFromPost( ); } - private function retrieveShuffleAnswerOptionsFromPost( + private function retrieveShuffleAnswerOptionsFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): ?bool { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->bool(), $refinery->always($this->getShuffleAnswerOptions()) @@ -247,13 +245,12 @@ private function retrieveShuffleAnswerOptionsFromPost( ); } - private function retrieveAnswerOptionsFromPost( + private function retrieveAnswerOptionsFromCarry( Refinery $refinery, - ArrayBasedRequestWrapper $post_wrapper, - string $form_input_path + CarryWrapper $carry ): AnswerOptions { - return $post_wrapper->retrieve( - $form_input_path . '/' . self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + return $carry->retrieve( + self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( fn(?string $v): AnswerOptions => $this->answer_options->withValuesFromHiddenInputValue($v) ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 220902079652..54847e55d77e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -46,7 +46,7 @@ public function getIdentifier(): string return 'select'; } - public function getEditAnswerOptionsInputs(Data $data): array + public function getEditAnswerOptionsInputs(Properties $properties): array { $ff = $this->ui_factory->input()->field(); return [ @@ -54,10 +54,10 @@ public function getEditAnswerOptionsInputs(Data $data): array $this->lng->txt('answer_options'), [] )->withRequired(true) - ->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + ->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'shuffle_answer_options' => $ff->checkbox( $this->lng->txt('shuffle_answers') - )->withValue($data?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) + )->withValue($properties?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) ]; } @@ -91,11 +91,11 @@ function (array $vs): bool { public function getBuildGapTransformation(Gap $gap): Transformation { - $data = $gap->getData(); + $properties = $gap->getProperties(); return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withData( - $data->withAnswerOptions( - $data->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + fn(array $vs): Gap => $gap->withProperties( + $properties->withAnswerOptions( + $properties->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) ) ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 1e0bcb93c136..4f99af8a7b10 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -47,7 +47,7 @@ public function getIdentifier(): string return 'text'; } - public function getEditAnswerOptionsInputs(Data $data): array + public function getEditAnswerOptionsInputs(Properties $properties): array { $ff = $this->ui_factory->input()->field(); return [ @@ -55,15 +55,15 @@ public function getEditAnswerOptionsInputs(Data $data): array $this->lng->txt('answer_options'), [] )->withRequired(true) - ->withValue($data->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + ->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'matching_method' => $ff->select( $this->lng->txt('matching_method'), TextMatchingOptions::buildOptionsList($this->lng) )->withRequired(true) - ->withValue($data->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), + ->withValue($properties->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), 'max_chars' => $ff->numeric( $this->lng->txt('max_chars'), - )->withValue($data->getMaxChars()) + )->withValue($properties->getMaxChars()) ]; } @@ -103,13 +103,13 @@ function (array $vs): bool { public function getBuildGapTransformation(Gap $gap): Transformation { - $data = $gap->getData(); + $properties = $gap->getProperties(); return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withData( - $data->withMaxChars($vs['max_chars']) + fn(array $vs): Gap => $gap->withProperties( + $properties->withMaxChars($vs['max_chars']) ->withTextMatchingMethod(TextMatchingOptions::tryFrom($vs['matching_method']) ?? self::DEFAULT_TECT_MATCHING_METHOD) ->withAnswerOptions( - $data->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + $properties->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) ) ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index 181649f153ca..6bcb0a700e6a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Data\Data; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; @@ -34,7 +34,7 @@ public function __construct( } abstract public function getIdentifier(): string; - abstract public function getEditAnswerOptionsInputs(Data $data): array; + abstract public function getEditAnswerOptionsInputs(Properties $data): array; abstract public function getEditAnswerOptionsSectionConstraint(): ?Constraint; abstract public function getEditPointsInputs(AnswerOptions $answer_options): array; abstract public function getEditPointsSectionConstraint(): ?Constraint; @@ -43,11 +43,11 @@ abstract public function getAnswerInput(): \ilFormPropertyGUI; public function getAddPointsTransformation(Gap $gap): Transformation { - $data = $gap->getData(); + $properties = $gap->getProperties(); return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withData( - $data->withAnswerOptions( - $data->getAnswerOptions() + fn(array $vs): Gap => $gap->withProperties( + $properties->withAnswerOptions( + $properties->getAnswerOptions() ->withAnswerOptionsWithAddedPointsFromForm($this->refinery, $vs) ) ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index cefb14c14553..76e1aa301b34 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -25,25 +25,23 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; -use ILIAS\Questions\Question\Persistence\ManipulateQuery; +use ILIAS\Questions\Presentation\Layout\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; +use ILIAS\Questions\Presentation\Layout\Definitions\EditOverview; use ILIAS\HTTP\Services as HTTPServices; use ILIAS\Language\Language; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; +use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\Refinery\Factory as Refinery; class Edit implements EditViewInterface { + private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; private const string STEP_SET_GAP_TYPES = 'sgt'; private const string STEP_SET_ANSWER_OPTIONS = 'sao'; private const string STEP_SET_POINTS = 'sap'; private const string STEP_SAVE = 's'; - private const string MAIN_SECTION_NAME = 'form'; - private const string PROPERTIES_SECTION_NAME = 'properties'; - public function __construct( private readonly Language $lng, private readonly UIFactory $ui_factory, @@ -56,225 +54,230 @@ public function __construct( } public function create( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step, - ): array|ManipulateQuery { - return match($step) { - self::STEP_SET_GAP_TYPES => $this->processBasicEditingForm($url_builder, $step_token), - self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm($url_builder, $step_token), - self::STEP_SET_POINTS => $this->processAnswerOptionsForm($url_builder, $step_token), - self::STEP_SAVE => $this->processAssignPointsForm($url_builder, $step_token), - default => [$this->buildBasicEditingForm($url_builder, $step_token)] + Environment $environment + ): EditForm|Properties { + return match($environment->getStep()) { + self::STEP_SET_GAP_TYPES => $this->processBasicEditingForm( + $environment->withProperties( + $environment->getProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getDefinitionsFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ) + ), + self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( + $environment->withProperties( + $environment->getProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getDefinitionsFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ) + ), + self::STEP_SET_POINTS => $this->processAnswerOptionsForm( + $environment->withProperties( + $environment->getProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getDefinitionsFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ) + ), + self::STEP_SAVE => $this->processAssignPointsForm( + $environment->withProperties( + $environment->getProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getDefinitionsFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ) + ), + default => $this->buildBasicEditingForm($environment) }; } public function edit( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step - ): array|ManipulateQuery { - + Environment $environment + ): EditOverview|EditForm|Properties { + return match ($step) { + default => $this->buildEditingOverview($environment) + }; } public function other( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step - ): array|ManipulateQuery { + Environment $environment + ): EditForm|Properties { } + private function buildEditingOverview( + Environment $environment + ): EditOverview { + return $environment->getDefinitionsFactory()->getEditOverview( + $environment->getEditability(), + $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES), + $environment->getProperties() + ); + } + private function buildBasicEditingForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): StandardForm { - return $this->ui_factory->input()->container()->form()->standard( - $url_builder->withParameter($step_token, self::STEP_SET_GAP_TYPES)->buildURI()->__toString(), - [ - self::MAIN_SECTION_NAME => $this->type->getProperties()->buildBasicEditingInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->refinery, - $this->properties_factory, - $this->cloze_text_factory - ) - ] - )->withSubmitLabel($this->lng->txt('next')); + Environment $environment + ): EditForm { + return $environment->getDefinitionsFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES), + $environment->getProperties()->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory + ), + false + ); } private function processBasicEditingForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - $form = $this->buildBasicEditingForm($url_builder, $step_token)->withRequest($this->http->request()); - $data = $form->getData(); - if ($data === null) { - return [$form]; - } + Environment $environment + ): EditForm { + $form = $this->buildBasicEditingForm( + $environment + )->withRequest($this->http->request()); - return $this->buildOutputWithPanel( - $this->buildGapTypesForm( - $url_builder, - $step_token, - $data[self::MAIN_SECTION_NAME] - ), - $data[self::MAIN_SECTION_NAME] - ); + $data = $form->getData(); + return $data === null + ? $form + : $this->buildGapTypesForm( + $environment->withProperties($data) + ); } private function buildGapTypesForm( - URLBuilder $url_builder, - URLBuilderToken $step_token, - Properties $properties - ): StandardForm { + Environment $environment + ): EditForm { + $properties = $environment->getProperties(); $ff = $this->ui_factory->input()->field(); - return $this->ui_factory->input()->container()->form()->standard( - $url_builder->withParameter($step_token, self::STEP_SET_ANSWER_OPTIONS)->buildURI()->__toString(), - [ - self::MAIN_SECTION_NAME => $properties->getGaps()->buildGapsTypeInputs( - $this->lng, - $ff, - $this->refinery, - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng) - ), - self::PROPERTIES_SECTION_NAME => $properties - ->withClozeText($properties->getClozeText()) - ->buildBasicEditingInputsHidden($ff) - ->withDedicatedName(self::PROPERTIES_SECTION_NAME) - - ] - )->withSubmitLabel($this->lng->txt('next')); + return $environment->getDefinitionsFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS), + $properties->getGaps()->buildGapsTypeInputs( + $this->lng, + $ff, + $this->refinery, + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng) + ), + false, + $properties->withClozeText($properties->getClozeText()) + ->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); } private function processGapTypesForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - $properties = $this->retrievePropertiesFromPost(); + Environment $environment + ): EditForm { $form = $this->buildGapTypesForm( - $url_builder, - $step_token, - $properties + $environment )->withRequest($this->http->request()); $data = $form->getData(); - return $this->buildOutputWithPanel( - $data === null - ? $form - : $this->buildAnswerOptionsForm( - $url_builder, - $step_token, - $properties->withGaps($data[self::MAIN_SECTION_NAME]) - ), - $properties - ); + return $data === null + ? $form + : $this->buildAnswerOptionsForm( + $environment->withProperties( + $environment->getProperties()->withGaps($data) + ) + ); } private function buildAnswerOptionsForm( - URLBuilder $url_builder, - URLBuilderToken $step_token, - Properties $properties - ): StandardForm { + Environment $environment + ): EditForm { + $properties = $environment->getProperties(); $ff = $this->ui_factory->input()->field(); - return $this->ui_factory->input()->container()->form()->standard( - $url_builder->withParameter($step_token, self::STEP_SET_POINTS)->buildURI()->__toString(), - [ - self::MAIN_SECTION_NAME => $properties->getGaps()->buildAnswerOptionsInputs($this->lng, $ff, $this->refinery), - self::PROPERTIES_SECTION_NAME => $properties->buildBasicEditingInputsHidden($ff) - ->withDedicatedName(self::PROPERTIES_SECTION_NAME) - ] - )->withSubmitLabel($this->lng->txt('next')); + return $environment->getDefinitionsFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS), + $properties->getGaps()->buildAnswerOptionsInputs($this->lng, $ff, $this->refinery), + false, + $properties->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); } private function processAnswerOptionsForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - $properties = $this->retrievePropertiesFromPost(); + Environment $environment + ): EditForm { $form = $this->buildAnswerOptionsForm( - $url_builder, - $step_token, - $properties + $environment )->withRequest($this->http->request()); $data = $form->getData(); - return $this->buildOutputWithPanel( - $data === null - ? $form - : $this->buildAssignPointsForm( - $url_builder, - $step_token, - $properties->withGaps($data[self::MAIN_SECTION_NAME]) - ), - $properties - ); + return $data === null + ? $form + : $this->buildAssignPointsForm( + $environment->withProperties( + $environment->getProperties()->withGaps($data) + ) + ); } private function buildAssignPointsForm( - URLBuilder $url_builder, - URLBuilderToken $step_token, - Properties $properties - ): StandardForm { + Environment $environment + ): EditForm { + $properties = $environment->getProperties(); $ff = $this->ui_factory->input()->field(); - return $this->ui_factory->input()->container()->form()->standard( - $url_builder->withParameter($step_token, self::STEP_SAVE)->buildURI()->__toString(), - [ - self::MAIN_SECTION_NAME => $properties->getGaps()->buildPointInputs($this->lng, $ff, $this->refinery), - self::PROPERTIES_SECTION_NAME => $properties->buildBasicEditingInputsHidden($ff) - ->withDedicatedName(self::PROPERTIES_SECTION_NAME) - ] - )->withSubmitLabel($this->lng->txt('save')); + return $environment->getDefinitionsFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SAVE), + $properties->getGaps()->buildPointInputs($this->lng, $ff, $this->refinery), + true, + $properties->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); } private function processAssignPointsForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array|ManipulateQuery { - $properties = $this->retrievePropertiesFromPost(); + Environment $environment + ): EditForm|Properties { $form = $this->buildAssignPointsForm( - $url_builder, - $step_token, - $properties + $environment )->withRequest($this->http->request()); + $properties = $environment->getProperties(); $data = $form->getData(); - if ($data === null) { - return $this->buildOutputWithPanel($form, $properties); - } - - return $this->save($properties->withGaps($data[self::MAIN_SECTION_NAME])); + return $data === null + ? $form->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ) : $properties->withGaps($data); } - private function buildOutputWithPanel( - StandardForm $form, + private function buildClozeTextPanel( Properties $properties - ): array { - return [ - $this->ui_factory->panel()->standard( - $this->lng->txt('cloze_text'), - $this->ui_factory->legacy()->content( - $properties->getClozeText()->getRenderedMarkdown( - $properties->getGaps() - ) + ): StandardPanel { + return $this->ui_factory->panel()->standard( + $this->lng->txt('cloze_text'), + $this->ui_factory->legacy()->content( + $properties->getClozeText()->getRenderedMarkdownForEditingPresentation( + $properties->getGaps() ) - ), - $form - ]; - } - - private function save(Properties $properties): ManipulateQuery - { - return $properties->toPersistence(); - } - - private function retrievePropertiesFromPost(): Properties - { - - return $this->type->getProperties()->withValuesFromPost( - $this->refinery, - $this->http->wrapper()->post(), - 'form/' . self::PROPERTIES_SECTION_NAME + ) ); } } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php deleted file mode 100755 index e23e4217a684..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ -class ArrayBasedRequestWrapper implements RequestWrapper -{ - /** - * GetRequestWrapper constructor. - * @param mixed[] $raw_values - */ - public function __construct(private array $raw_values) - { - } - - - /** - * @inheritDoc - */ - public function retrieve(string $key, Transformation $transformation) - { - return $transformation->transform($this->raw_values[$key] ?? null); - } - - - /** - * @inheritDoc - */ - public function has(string $key): bool - { - return isset($this->raw_values[$key]); - } - - /** - * @inheritDoc - */ - public function keys(): array - { - return array_keys($this->raw_values); - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php deleted file mode 100644 index 14a16b4c6037..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EditForm.php +++ /dev/null @@ -1,555 +0,0 @@ -checkCapabilities($capability_class_names); - $clone = clone $this; - $clone->required_capabilities = $capability_class_names; - return $clone; - } - - public function withEditable(Editability $editability): self - { - $clone = clone $this; - $clone->editability = $editability; - return $clone; - } - - public function withOrderingEnabled(bool $enable): self - { - $clone = clone $this; - $clone->ordering_enabled = $enable; - return $clone; - } - - public function view( - \ilToolbarGUI $toolbar, - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_QUESTION => $this->createQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - self::CMD_EDIT_QUESTION => $this->editQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - default => $this->showTable( - $toolbar, - $url_builder, - $action_token, - $question_id_token - ) - }; - } - - public function forwardPageCmds( - \ilGlobalTemplateInterface $tpl, - URI $base_uri, - ): void { - [ - 0 => $url_builder, - 1 => $action_token, - 3 => $question_id_token, - 4 => $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - $page_id = $this->retrievePageId($page_id_token); - $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); - - $tpl->setContent( - $this->ctrl->forwardCommand( - new \QstsQuestionPageGUI( - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) - ->withParameter($question_id_token, $question_id->toString()) - ->buildURI(), - $page_id - ) - ) - ); - } - - public function createAnswerForm( - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token, - $type_hash_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - $question_id = $this->retrieveQuestionId($question_id_token); - $url_builder_with_params = $url_builder - ->withParameter($question_id_token, $question_id->toString()) - ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) - ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); - - $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); - if ($answer_form_type_class_hash !== '') { - return $this->forwardCreateAnswerFormCmd( - $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), - $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), - $step_token - ); - } - - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( - $url_builder_with_params, - $action_token, - $step_token, - $type_hash_token - ), - default => [$this->buildCreateAnswerForm( - $url_builder_with_params, - $action_token - )] - }; - } - - public function editAnswerForm( - URI $base_uri - ): array { - [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], - default => [$this->buildCreateAnswerForm($url_builder, $action_token)] - }; - } - - private function createQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $create = (new QuestionImplementation())->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->create( - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($create)) { - return $create; - } - - $this->questions_repository->store($create); - return $this->buildEditStartView( - $url_builder->withParameter($question_id_token, $create->getId()), - $step_token, - $page_id_token, - $create - ); - - } - - private function editQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - - $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); - - $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), - $step_token, - $page_id_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($edit)) { - return $edit; - } - - $this->questions_repository->store($edit); - return $this->buildEditStartView( - $url_builder_with_row_id, - $step_token, - $page_id_token, - $edit - ); - } - - private function showTable( - \ilToolbarGUI $toolbar, - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): array { - $toolbar->addComponent( - $this->ui_factory->button()->standard( - $this->lng->txt('create'), - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() - ) - ); - - $table = new QuestionsTable( - $this->ui_factory, - $this->ui_services, - $this->lng, - $this->answer_form_types_factory, - $this->questions_repository, - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $action_token, - $question_id_token - ); - return [ - $table->getFilter($url_builder->buildURI()->__toString()), - $table->getTable()->withRequest($this->http->request()) - - ]; - } - - private function processCreateAnswerForm( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $type_hash_token - ): array { - $form = $this->buildCreateAnswerForm( - $url_builder, - $action_token - )->withRequest($this->http->request()); - - $data = $form->getData(); - if ($data === null || $data['form_type'] === null) { - return [$form]; - } - - return $this->forwardCreateAnswerFormCmd( - $data['form_type'], - $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), - $step_token - ); - } - - private function forwardCreateAnswerFormCmd( - Definition $type, - TypeGenericProperties $type_generic_properties, - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - return $type->getEditView()->create( - $type->buildProperties($type_generic_properties, []), - $url_builder, - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - } - - private function acquireURLBuilderAndParameters(URI $base_uri): array - { - return (new URLBuilder($base_uri)) - ->acquireParameters( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_PAGE_ID, - self::TOKEN_TYPE_HASH - ); - } - - private function retrieveStringValueForToken( - URLBuilderToken $token - ): string { - return $this->http->wrapper()->query()->retrieve( - $token->getName(), - $this->buildStringTrafo() - ); - } - - private function retrieveQuestionId( - URLBuilderToken $question_id_token - ): ?Uuid { - return $this->http->wrapper()->query()->retrieve( - $question_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->custom()->transformation( - fn($v): Uuid => $this->uuid_factory->fromString($v) - ), - $this->refinery->always(null) - ]) - ); - } - - public function retrievePageId( - URLBuilderToken $page_id_token - ): ?int { - return $this->http->wrapper()->query()->retrieve( - $page_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->kindlyTo()->int(), - $this->refinery->always(null) - ]) - ); - } - - private function buildStringTrafo(): Transformation - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->string(), - $this->refinery->always('') - ]); - } - - private function initializeEditMode( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): void { - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::MODE_ENABLED, - true - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::QUESTIONLIST_ENTRY, - $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CLOSE_MODE_INFO, - $url_builder->buildURI() - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CREATE_QUESTION, - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() - ); - } - - private function setParametersForQuestionCmds( - URLBuilderToken $question_id_token, - string $question_id, - URLBuilderToken $page_id_token, - int $page_id - ): void { - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $question_id_token->getName(), - $question_id - ); - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $page_id_token->getName(), - $page_id - ); - } - - private function buildQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): LegacySlate { - return $this->ui_factory->mainControls()->slate()->legacy( - $this->lng->txt('mainbar_button_label_questionlist'), - $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), - $this->ui_factory->legacy()->content( - $this->ui_renderer->render( - $this->ui_factory->panel()->secondary()->listing( - $this->lng->txt('mainbar_button_label_questionlist'), - [ - $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) - ] - ) - ) - ) - ); - } - - private function buildItemGroupForQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): ItemGroup { - return $this->ui_factory->item()->group( - '', - array_map( - fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( - $v->toEditLink( - $this->ui_factory->link(), - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $question_id_token - ) - ), - iterator_to_array($this->questions_repository->getAllQuestions()) - ) - ); - } - - private function buildEditStartView( - URLBuilder $url_builder, - URLBuilderToken $step_token, - URLBuilderToken $page_id_token, - QuestionImplementation $question - ): array { - return $question->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder, - $step_token, - $page_id_token, - '' - ); - } - - private function buildCreateAnswerForm( - URLBuilder $url_builder - ): StandardForm { - $if = $this->ui_factory->input(); - return $if->container()->form()->standard( - $url_builder->buildURI()->__toString(), - [ - 'form_type' => $if->field()->section( - [ - $if->field()->select( - $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect() - )->withRequired(true) - ], - $this->lng->txt('create_answer_form') - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) - ) - ) - ] - )->withSubmitLabel($this->lng->txt('next')); - } - - private function checkCapabilities(array $capabilities): void - { - foreach ($capabilities as $capability) { - if (!$this->questions_repository->capabilityExists($capability)) { - throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); - } - } - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php deleted file mode 100644 index 14a16b4c6037..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EditFormFactory.php +++ /dev/null @@ -1,555 +0,0 @@ -checkCapabilities($capability_class_names); - $clone = clone $this; - $clone->required_capabilities = $capability_class_names; - return $clone; - } - - public function withEditable(Editability $editability): self - { - $clone = clone $this; - $clone->editability = $editability; - return $clone; - } - - public function withOrderingEnabled(bool $enable): self - { - $clone = clone $this; - $clone->ordering_enabled = $enable; - return $clone; - } - - public function view( - \ilToolbarGUI $toolbar, - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_QUESTION => $this->createQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - self::CMD_EDIT_QUESTION => $this->editQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - default => $this->showTable( - $toolbar, - $url_builder, - $action_token, - $question_id_token - ) - }; - } - - public function forwardPageCmds( - \ilGlobalTemplateInterface $tpl, - URI $base_uri, - ): void { - [ - 0 => $url_builder, - 1 => $action_token, - 3 => $question_id_token, - 4 => $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - $page_id = $this->retrievePageId($page_id_token); - $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); - - $tpl->setContent( - $this->ctrl->forwardCommand( - new \QstsQuestionPageGUI( - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) - ->withParameter($question_id_token, $question_id->toString()) - ->buildURI(), - $page_id - ) - ) - ); - } - - public function createAnswerForm( - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token, - $type_hash_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - $question_id = $this->retrieveQuestionId($question_id_token); - $url_builder_with_params = $url_builder - ->withParameter($question_id_token, $question_id->toString()) - ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) - ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); - - $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); - if ($answer_form_type_class_hash !== '') { - return $this->forwardCreateAnswerFormCmd( - $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), - $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), - $step_token - ); - } - - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( - $url_builder_with_params, - $action_token, - $step_token, - $type_hash_token - ), - default => [$this->buildCreateAnswerForm( - $url_builder_with_params, - $action_token - )] - }; - } - - public function editAnswerForm( - URI $base_uri - ): array { - [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], - default => [$this->buildCreateAnswerForm($url_builder, $action_token)] - }; - } - - private function createQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $create = (new QuestionImplementation())->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->create( - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($create)) { - return $create; - } - - $this->questions_repository->store($create); - return $this->buildEditStartView( - $url_builder->withParameter($question_id_token, $create->getId()), - $step_token, - $page_id_token, - $create - ); - - } - - private function editQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - - $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); - - $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), - $step_token, - $page_id_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($edit)) { - return $edit; - } - - $this->questions_repository->store($edit); - return $this->buildEditStartView( - $url_builder_with_row_id, - $step_token, - $page_id_token, - $edit - ); - } - - private function showTable( - \ilToolbarGUI $toolbar, - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): array { - $toolbar->addComponent( - $this->ui_factory->button()->standard( - $this->lng->txt('create'), - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() - ) - ); - - $table = new QuestionsTable( - $this->ui_factory, - $this->ui_services, - $this->lng, - $this->answer_form_types_factory, - $this->questions_repository, - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $action_token, - $question_id_token - ); - return [ - $table->getFilter($url_builder->buildURI()->__toString()), - $table->getTable()->withRequest($this->http->request()) - - ]; - } - - private function processCreateAnswerForm( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $type_hash_token - ): array { - $form = $this->buildCreateAnswerForm( - $url_builder, - $action_token - )->withRequest($this->http->request()); - - $data = $form->getData(); - if ($data === null || $data['form_type'] === null) { - return [$form]; - } - - return $this->forwardCreateAnswerFormCmd( - $data['form_type'], - $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), - $step_token - ); - } - - private function forwardCreateAnswerFormCmd( - Definition $type, - TypeGenericProperties $type_generic_properties, - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - return $type->getEditView()->create( - $type->buildProperties($type_generic_properties, []), - $url_builder, - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - } - - private function acquireURLBuilderAndParameters(URI $base_uri): array - { - return (new URLBuilder($base_uri)) - ->acquireParameters( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_PAGE_ID, - self::TOKEN_TYPE_HASH - ); - } - - private function retrieveStringValueForToken( - URLBuilderToken $token - ): string { - return $this->http->wrapper()->query()->retrieve( - $token->getName(), - $this->buildStringTrafo() - ); - } - - private function retrieveQuestionId( - URLBuilderToken $question_id_token - ): ?Uuid { - return $this->http->wrapper()->query()->retrieve( - $question_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->custom()->transformation( - fn($v): Uuid => $this->uuid_factory->fromString($v) - ), - $this->refinery->always(null) - ]) - ); - } - - public function retrievePageId( - URLBuilderToken $page_id_token - ): ?int { - return $this->http->wrapper()->query()->retrieve( - $page_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->kindlyTo()->int(), - $this->refinery->always(null) - ]) - ); - } - - private function buildStringTrafo(): Transformation - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->string(), - $this->refinery->always('') - ]); - } - - private function initializeEditMode( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): void { - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::MODE_ENABLED, - true - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::QUESTIONLIST_ENTRY, - $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CLOSE_MODE_INFO, - $url_builder->buildURI() - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CREATE_QUESTION, - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() - ); - } - - private function setParametersForQuestionCmds( - URLBuilderToken $question_id_token, - string $question_id, - URLBuilderToken $page_id_token, - int $page_id - ): void { - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $question_id_token->getName(), - $question_id - ); - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $page_id_token->getName(), - $page_id - ); - } - - private function buildQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): LegacySlate { - return $this->ui_factory->mainControls()->slate()->legacy( - $this->lng->txt('mainbar_button_label_questionlist'), - $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), - $this->ui_factory->legacy()->content( - $this->ui_renderer->render( - $this->ui_factory->panel()->secondary()->listing( - $this->lng->txt('mainbar_button_label_questionlist'), - [ - $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) - ] - ) - ) - ) - ); - } - - private function buildItemGroupForQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): ItemGroup { - return $this->ui_factory->item()->group( - '', - array_map( - fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( - $v->toEditLink( - $this->ui_factory->link(), - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $question_id_token - ) - ), - iterator_to_array($this->questions_repository->getAllQuestions()) - ) - ); - } - - private function buildEditStartView( - URLBuilder $url_builder, - URLBuilderToken $step_token, - URLBuilderToken $page_id_token, - QuestionImplementation $question - ): array { - return $question->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder, - $step_token, - $page_id_token, - '' - ); - } - - private function buildCreateAnswerForm( - URLBuilder $url_builder - ): StandardForm { - $if = $this->ui_factory->input(); - return $if->container()->form()->standard( - $url_builder->buildURI()->__toString(), - [ - 'form_type' => $if->field()->section( - [ - $if->field()->select( - $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect() - )->withRequired(true) - ], - $this->lng->txt('create_answer_form') - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) - ) - ) - ] - )->withSubmitLabel($this->lng->txt('next')); - } - - private function checkCapabilities(array $capabilities): void - { - foreach ($capabilities as $capability) { - if (!$this->questions_repository->capabilityExists($capability)) { - throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); - } - } - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php deleted file mode 100644 index 14a16b4c6037..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EditOverview.php +++ /dev/null @@ -1,555 +0,0 @@ -checkCapabilities($capability_class_names); - $clone = clone $this; - $clone->required_capabilities = $capability_class_names; - return $clone; - } - - public function withEditable(Editability $editability): self - { - $clone = clone $this; - $clone->editability = $editability; - return $clone; - } - - public function withOrderingEnabled(bool $enable): self - { - $clone = clone $this; - $clone->ordering_enabled = $enable; - return $clone; - } - - public function view( - \ilToolbarGUI $toolbar, - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_QUESTION => $this->createQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - self::CMD_EDIT_QUESTION => $this->editQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - default => $this->showTable( - $toolbar, - $url_builder, - $action_token, - $question_id_token - ) - }; - } - - public function forwardPageCmds( - \ilGlobalTemplateInterface $tpl, - URI $base_uri, - ): void { - [ - 0 => $url_builder, - 1 => $action_token, - 3 => $question_id_token, - 4 => $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - $page_id = $this->retrievePageId($page_id_token); - $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); - - $tpl->setContent( - $this->ctrl->forwardCommand( - new \QstsQuestionPageGUI( - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) - ->withParameter($question_id_token, $question_id->toString()) - ->buildURI(), - $page_id - ) - ) - ); - } - - public function createAnswerForm( - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token, - $type_hash_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - $question_id = $this->retrieveQuestionId($question_id_token); - $url_builder_with_params = $url_builder - ->withParameter($question_id_token, $question_id->toString()) - ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) - ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); - - $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); - if ($answer_form_type_class_hash !== '') { - return $this->forwardCreateAnswerFormCmd( - $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), - $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), - $step_token - ); - } - - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( - $url_builder_with_params, - $action_token, - $step_token, - $type_hash_token - ), - default => [$this->buildCreateAnswerForm( - $url_builder_with_params, - $action_token - )] - }; - } - - public function editAnswerForm( - URI $base_uri - ): array { - [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], - default => [$this->buildCreateAnswerForm($url_builder, $action_token)] - }; - } - - private function createQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $create = (new QuestionImplementation())->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->create( - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($create)) { - return $create; - } - - $this->questions_repository->store($create); - return $this->buildEditStartView( - $url_builder->withParameter($question_id_token, $create->getId()), - $step_token, - $page_id_token, - $create - ); - - } - - private function editQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - - $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); - - $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), - $step_token, - $page_id_token, - $this->retrieveStringValueForToken($step_token) - ); - - if (is_array($edit)) { - return $edit; - } - - $this->questions_repository->store($edit); - return $this->buildEditStartView( - $url_builder_with_row_id, - $step_token, - $page_id_token, - $edit - ); - } - - private function showTable( - \ilToolbarGUI $toolbar, - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): array { - $toolbar->addComponent( - $this->ui_factory->button()->standard( - $this->lng->txt('create'), - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() - ) - ); - - $table = new QuestionsTable( - $this->ui_factory, - $this->ui_services, - $this->lng, - $this->answer_form_types_factory, - $this->questions_repository, - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $action_token, - $question_id_token - ); - return [ - $table->getFilter($url_builder->buildURI()->__toString()), - $table->getTable()->withRequest($this->http->request()) - - ]; - } - - private function processCreateAnswerForm( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $type_hash_token - ): array { - $form = $this->buildCreateAnswerForm( - $url_builder, - $action_token - )->withRequest($this->http->request()); - - $data = $form->getData(); - if ($data === null || $data['form_type'] === null) { - return [$form]; - } - - return $this->forwardCreateAnswerFormCmd( - $data['form_type'], - $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), - $step_token - ); - } - - private function forwardCreateAnswerFormCmd( - Definition $type, - TypeGenericProperties $type_generic_properties, - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - return $type->getEditView()->create( - $type->buildProperties($type_generic_properties, []), - $url_builder, - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - } - - private function acquireURLBuilderAndParameters(URI $base_uri): array - { - return (new URLBuilder($base_uri)) - ->acquireParameters( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_PAGE_ID, - self::TOKEN_TYPE_HASH - ); - } - - private function retrieveStringValueForToken( - URLBuilderToken $token - ): string { - return $this->http->wrapper()->query()->retrieve( - $token->getName(), - $this->buildStringTrafo() - ); - } - - private function retrieveQuestionId( - URLBuilderToken $question_id_token - ): ?Uuid { - return $this->http->wrapper()->query()->retrieve( - $question_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->custom()->transformation( - fn($v): Uuid => $this->uuid_factory->fromString($v) - ), - $this->refinery->always(null) - ]) - ); - } - - public function retrievePageId( - URLBuilderToken $page_id_token - ): ?int { - return $this->http->wrapper()->query()->retrieve( - $page_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->kindlyTo()->int(), - $this->refinery->always(null) - ]) - ); - } - - private function buildStringTrafo(): Transformation - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->string(), - $this->refinery->always('') - ]); - } - - private function initializeEditMode( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): void { - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::MODE_ENABLED, - true - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::QUESTIONLIST_ENTRY, - $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CLOSE_MODE_INFO, - $url_builder->buildURI() - ); - $this->global_screen->tool()->context()->current()->addAdditionalData( - LayoutProvider::URL_CREATE_QUESTION, - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() - ); - } - - private function setParametersForQuestionCmds( - URLBuilderToken $question_id_token, - string $question_id, - URLBuilderToken $page_id_token, - int $page_id - ): void { - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $question_id_token->getName(), - $question_id - ); - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $page_id_token->getName(), - $page_id - ); - } - - private function buildQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): LegacySlate { - return $this->ui_factory->mainControls()->slate()->legacy( - $this->lng->txt('mainbar_button_label_questionlist'), - $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), - $this->ui_factory->legacy()->content( - $this->ui_renderer->render( - $this->ui_factory->panel()->secondary()->listing( - $this->lng->txt('mainbar_button_label_questionlist'), - [ - $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) - ] - ) - ) - ) - ); - } - - private function buildItemGroupForQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): ItemGroup { - return $this->ui_factory->item()->group( - '', - array_map( - fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( - $v->toEditLink( - $this->ui_factory->link(), - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $question_id_token - ) - ), - iterator_to_array($this->questions_repository->getAllQuestions()) - ) - ); - } - - private function buildEditStartView( - URLBuilder $url_builder, - URLBuilderToken $step_token, - URLBuilderToken $page_id_token, - QuestionImplementation $question - ): array { - return $question->getEditView( - $this->lng, - $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit( - $url_builder, - $step_token, - $page_id_token, - '' - ); - } - - private function buildCreateAnswerForm( - URLBuilder $url_builder - ): StandardForm { - $if = $this->ui_factory->input(); - return $if->container()->form()->standard( - $url_builder->buildURI()->__toString(), - [ - 'form_type' => $if->field()->section( - [ - $if->field()->select( - $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect() - )->withRequired(true) - ], - $this->lng->txt('create_answer_form') - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) - ) - ) - ] - )->withSubmitLabel($this->lng->txt('next')); - } - - private function checkCapabilities(array $capabilities): void - { - foreach ($capabilities as $capability) { - if (!$this->questions_repository->capabilityExists($capability)) { - throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); - } - } - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php index b8e8ce6a99d8..f7ca340fb0d8 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation; +namespace ILIAS\Questions\Presentation\Definitions; enum Editability { diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php b/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php deleted file mode 100755 index e23e4217a684..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ -class ArrayBasedRequestWrapper implements RequestWrapper -{ - /** - * GetRequestWrapper constructor. - * @param mixed[] $raw_values - */ - public function __construct(private array $raw_values) - { - } - - - /** - * @inheritDoc - */ - public function retrieve(string $key, Transformation $transformation) - { - return $transformation->transform($this->raw_values[$key] ?? null); - } - - - /** - * @inheritDoc - */ - public function has(string $key): bool - { - return isset($this->raw_values[$key]); - } - - /** - * @inheritDoc - */ - public function keys(): array - { - return array_keys($this->raw_values); - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php new file mode 100755 index 000000000000..87a4328128a4 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php @@ -0,0 +1,58 @@ +transform( + $this->retrieveValueFromArray($key) + ); + } + + private function retrieveValueFromArray(string $key): mixed + { + $value = $this->raw_values[$key] ?? null; + + if ($value === null) { + return null; + } + + if ($value instanceof Leaf) { + return $value->get(); + } + + return new self($value); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php new file mode 100644 index 000000000000..dfe92b2075b8 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php @@ -0,0 +1,132 @@ +form = $this->buildForm(); + } + + public function withContentBeforeForm(StandardPanel $content): self + { + $clone = clone $this; + $clone->content_before_form = $content; + return $clone; + } + + public function withContentAfterForm(StandardPanel $content): self + { + $clone = clone $this; + $clone->content_after_form = $content; + return $clone; + } + + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); + } + + public function withRequest( + ServerRequestInterface $request + ): self { + $clone = clone $this; + $clone->form = $clone->form->withRequest($request); + return $clone; + } + + public function getData(): mixed + { + $data = $this->form->getData(); + return $data[self::MAIN_SECTION_NAME] ?? null; + } + + private function buildContent(): array + { + $content = []; + + if ($this->content_before_form !== null) { + $content[] = $this->content_before_form; + } + + $content[] = $this->form; + + if ($this->content_after_form !== null) { + $content[] = $this->content_after_form; + } + + return $content; + } + + private function buildForm(): StandardForm + { + $form = $this->form_factory->standard( + $this->url_builder->buildURI()->__toString(), + $this->buildFormInputs() + ); + + if ($this->is_final_step) { + return $form->withSubmitLabel($this->lng->txt('save')); + } + + return $form->withSubmitLabel($this->lng->txt('next')); + } + + private function buildFormInputs(): array + { + $form_inputs = [ + self::MAIN_SECTION_NAME => $this->main_section_inputs + ]; + + if ($this->carry_inputs !== null) { + $form_inputs[self::CARRY_SECTION_NAME] = $this->carry_inputs + ->withDedicatedName(self::CARRY_SECTION_NAME); + } + + return $form_inputs; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php new file mode 100644 index 000000000000..eee4451940e2 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php @@ -0,0 +1,94 @@ +form = $this->buildForm(); + } + + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); + } + + public function withRequest( + ServerRequestInterface $request + ): self { + $clone = clone $this; + $clone->form = $clone->form->withRequest($request); + return $clone; + } + + public function withOrderable(bool $orderable): self + { + $clone = clone $this; + $clone->orderable = $orderable; + return $clone; + } + + private function buildContent(): array + { + return [ + $this->buildBasicAnswerFormPanel(), + $this->answer_form_properties->getOverviewTable() + ]; + } + + private function buildBasicAnswerFormPanel(): StandardPanel + { + $content = [ + $this->ui_factory->listing()->descriptive( + $this->answer_form_properties->getBasicPropertiesForListing($this->lng) + ) + ]; + + if ($this->editability === Editability::Full) { + $content[] = $this->ui_factory->button()->standard( + $this->lng->txt('edit_basic_answer_form_properties'), + $this->url_builder->buildURI()->__toString() + ); + } + + return $this->ui_factory->panel()->standard( + $this->lng->txt('basic_answer_form_properites'), + $content + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php new file mode 100644 index 000000000000..151bfd712cee --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php @@ -0,0 +1,37 @@ +acquireURLBuilderAndParameters($base_uri); + } + + public function getDefinitionsFactory(): Factory + { + return $this->definitions_factory; + } + + public function getUrlBuilder(): URLBuilder + { + return $this->url_builder; + } + + public function getUrlBuilderWithStepParameter(string $step): URLBuilder + { + return $this->getUrlBuilder()->withParameter($this->step_token, $step); + } + + public function withDefaultStep(): self + { + $clone = clone $this; + $clone->default_step = true; + return $clone; + } + + public function getStep(): string + { + return $this->default_step + ? '' + : $this->retrieveStringValueForToken($this->step_token, self::TOKEN_STRING_STEP); + } + + public function getEditability(): Editability + { + return $this->editability; + } + + public function getProperties(): ?Properties + { + return $this->properties; + } + + public function withProperties(Properties $properties): self + { + $clone = clone $this; + $clone->properties = $properties; + return $clone; + } + + public function getAction(): string + { + return $this->retrieveStringValueForToken($this->action_token); + } + + public function withActionParameter(string $action): self + { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->action_token, $action); + return $clone; + } + + public function withQuestionIdParameter(Uuid $question_id): self + { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->question_id_token, $question_id->toString()); + return $clone; + } + + public function withAnswerFormTypeHashParameter(string $type_hash): URLBuilder + { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->type_hash_token, $type_hash); + return $clone; + } + + public function getQuestionId(): ?Uuid + { + return $this->http->wrapper()->query()->retrieve( + $this->question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): Uuid => $this->uuid_factory->fromString($v) + ), + $this->refinery->always(null) + ]) + ); + } + + public function getTypeClassHast(): string + { + return $this->retrieveStringValueForToken($this->type_hash_token); + } + + public function setParametersForQuestionCmds(): void + { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $this->question_id_token->getName(), + $this->getQuestionId()->toString() + ); + } + + private function acquireURLBuilderAndParameters(URI $base_uri): void + { + [ + $this->url_builder, + $this->action_token, + $this->step_token, + $this->question_id_token, + $this->type_hash_token + ] = (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_TYPE_HASH + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php new file mode 100644 index 000000000000..d5ee413e8c5f --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php @@ -0,0 +1,94 @@ +ui_factory, + $this->lng, + $editability, + $url_builder, + $answer_elements_table, + $answer_form_properties + ); + } + + public function getEditForm( + URLBuilder $url_builder, + Section $main_section_inputs, + bool $is_final_step, + ?Group $carry_inputs = null + ): EditForm { + return new EditForm( + $this->ui_factory->input()->container()->form(), + $this->lng, + $url_builder, + $main_section_inputs, + $is_final_step, + $carry_inputs + ); + } + + public function getCarrySectionData( + ArrayBasedRequestWrapper $post_wrapper, + Refinery $refinery + ): CarryWrapper { + return new CarryWrapper( + array_reduce( + $post_wrapper->keys(), + function (array $c, string $v) use ($post_wrapper, $refinery): array { + $value = new Leaf( + $post_wrapper->retrieve($v, $refinery->identity()) + ); + foreach (array_reverse(explode('/', $v)) as $path_element) { + $value = [$path_element => $value]; + } + return array_merge_recursive($c, $value); + }, + [] + )['form'][EditForm::CARRY_SECTION_NAME] ?? [] + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php new file mode 100755 index 000000000000..66e8e0bf572d --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php @@ -0,0 +1,34 @@ +value; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php similarity index 65% rename from components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php rename to components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php index 39bb97834356..b099f2a16737 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php @@ -18,17 +18,19 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation; +namespace ILIAS\Questions\Presentation\Layout\Definitions; -use ILIAS\Questions\AnswerFormTypes\Factory as AnswerFormTypesFactory; +use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; use ILIAS\UI\Component\Table; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; +use Psr\Http\Message\ServerRequestInterface; class QuestionsTable implements Table\DataRetrieval { @@ -36,54 +38,20 @@ public function __construct( private readonly UIFactory $ui_factory, private readonly \ilUIService $ui_service, private readonly \ilLanguage $lng, - private readonly AnswerFormTypesFactory $answer_form_type_factory, + private readonly ServerRequestInterface $request, + private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, - private readonly URLBuilder $url_builder, - private readonly URLBuilderToken $action_token, - private readonly URLBuilderToken $row_id_token + private readonly EnvironmentImplementation $environment ) { $lng->loadLanguageModule('qpl'); } - public function getTable(): Table\Data - { - return $this->ui_factory->table()->data( - $this, - $this->lng->txt('questions'), - $this->getColums(), - ); + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); } - public function getFilter(string $action): Filter - { - $question_type_options = [ - '' => $this->lng->txt('filter_all_question_types') - ]; - - foreach ($this->answer_form_type_factory->getAvailableAnswerFormTypes() as $class => $type) { - $question_type_options[$class] = $type->getLabel($this->lng); - } - - $field_factory = $this->ui_factory->input()->field(); - $filter_inputs = [ - 'title' => $field_factory->text($this->lng->txt('title')), - 'contains_type' => $field_factory->select($this->lng->txt('contains_type'), $question_type_options), - ]; - - $active = array_fill(0, count($filter_inputs), true); - - $filter = $this->ui_service->filter()->standard( - 'question_table_filter_id', - $action, - $filter_inputs, - $active, - true, - true - ); - return $filter; - } - - public function getColums(): array { $f = $this->ui_factory->table()->column(); @@ -103,12 +71,14 @@ public function getRows( mixed $filter_data, mixed $additional_parameters ): \Generator { + $environment_with_action = $this->environment->withActionParameter( + Edit::CMD_EDIT_QUESTION + ); foreach ($this->questions_repository->getAllQuestions() as $question) { yield $question->toTableRow( $row_builder, $this->ui_factory, - $this->url_builder, - $this->row_id_token + $environment_with_action ); } } @@ -120,4 +90,44 @@ public function getTotalRowCount( ): ?int { return 0; } + + private function buildContent(): array + { + return [ + $this->buildFilter($this->environment->getUrlBuilder()->buildURI()->__toString()), + $this->ui_factory->table()->data( + $this, + $this->lng->txt('questions'), + $this->getColums(), + )->withRequest($this->request) + ]; + } + + private function buildFilter(string $action): Filter + { + $question_type_options = [ + '' => $this->lng->txt('filter_all_question_types') + ]; + + $field_factory = $this->ui_factory->input()->field(); + $filter_inputs = [ + 'title' => $field_factory->text($this->lng->txt('title')), + 'contains_type' => $field_factory->select( + $this->lng->txt('contains_type'), + $question_type_options + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + ), + ]; + + $active = array_fill(0, count($filter_inputs), true); + + $filter = $this->ui_service->filter()->standard( + 'question_table_filter_id', + $action, + $filter_inputs, + $active, + true, + true + ); + return $filter; + } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php similarity index 98% rename from components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php rename to components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php index 6c48a4496a59..e3a9cb948116 100755 --- a/components/ILIAS/Questions/src/Presentation/Layout/LayoutProvider.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation; +namespace ILIAS\Questions\Presentation\Layout\GlobalScreen; use ILIAS\GlobalScreen\Scope\Layout\Factory\BreadCrumbsModification; use ILIAS\GlobalScreen\Scope\Layout\Factory\MainBarModification; diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 14a16b4c6037..c6437358efbb 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -18,8 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation; - +namespace ILIAS\Questions\Presentation\Views; + +use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; +use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Definitions\Editability; +use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Layout\Definitions\QuestionsTable; +use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\TypeGenericProperties; @@ -28,31 +34,20 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; -use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Transformation; use ILIAS\HTTP\Services as HTTP; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; -use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Component\Item\Standard as StandardItem; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; use ILIAS\GlobalScreen\Services as GlobalScreen; class Edit { - private const array QUERY_PARAMETER_NAME_SPACE = ['q']; - private const string TOKEN_STRING_ACTION = 'a'; - private const string TOKEN_STRING_STEP = 's'; - private const string TOKEN_STRING_QUESTION_ID = 'q'; - private const string TOKEN_STRING_PAGE_ID = 'p'; - private const string TOKEN_TYPE_HASH = 't'; private const string CMD_CREATE_QUESTION = 'create'; - private const string CMD_EDIT_QUESTION = 'edit'; + public const string CMD_EDIT_QUESTION = 'edit'; private const string CMD_CREATE_ANSWER_FORM = 'create_af'; private const string CMD_EDIT_ANSWER_FORM = 'edit_af'; @@ -73,9 +68,9 @@ public function __construct( private readonly DataFactory $data_factory, private readonly UuidFactory $uuid_factory, private readonly AnswerFormFactory $answer_form_factory, - private readonly Repository $questions_repository + private readonly Repository $questions_repository, + private readonly DefinitionsFactory $definitions_factory ) { - } public function withRequiredCapabilities(array $capability_class_names): self @@ -103,35 +98,12 @@ public function withOrderingEnabled(bool $enable): self public function view( \ilToolbarGUI $toolbar, URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_CREATE_QUESTION => $this->createQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - self::CMD_EDIT_QUESTION => $this->editQuestion( - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token - ), - default => $this->showTable( - $toolbar, - $url_builder, - $action_token, - $question_id_token - ) + ): QuestionsTable|EditForm { + $environment = $this->buildEnvironment($base_uri); + return match($environment->getAction()) { + self::CMD_CREATE_QUESTION => $this->createQuestion($environment), + self::CMD_EDIT_QUESTION => $this->editQuestion($environment), + default => $this->showTable($toolbar, $environment) }; } @@ -139,92 +111,75 @@ public function forwardPageCmds( \ilGlobalTemplateInterface $tpl, URI $base_uri, ): void { - [ - 0 => $url_builder, - 1 => $action_token, - 3 => $question_id_token, - 4 => $page_id_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); - $page_id = $this->retrievePageId($page_id_token); - $this->setParametersForQuestionCmds($question_id_token, $question_id->toString(), $page_id_token, $page_id); + $environment = $this->buildEnvironment($base_uri); + $this->initializeEditMode($environment); + $environment->setParametersForQuestionCmds(); $tpl->setContent( $this->ctrl->forwardCommand( new \QstsQuestionPageGUI( - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION) - ->withParameter($question_id_token, $question_id->toString()) + $environment + ->withActionParameter(self::CMD_EDIT_QUESTION) + ->withQuestionIdParameter($environment->getQuestionId()) + ->getUrlBuilder() ->buildURI(), - $page_id + $this, + $this->questions_repository->getForQuestionId($environment->getQuestionId()) ) ) ); } public function createAnswerForm( - URI $base_uri - ): array { - [ - $url_builder, - $action_token, - $step_token, - $question_id_token, - $page_id_token, - $type_hash_token - ] = $this->acquireURLBuilderAndParameters($base_uri); - $question_id = $this->retrieveQuestionId($question_id_token); - $url_builder_with_params = $url_builder - ->withParameter($question_id_token, $question_id->toString()) - ->withParameter($page_id_token, (string) $this->retrievePageId($page_id_token)) - ->withParameter($action_token, self::CMD_CREATE_ANSWER_FORM); - - $answer_form_type_class_hash = $this->retrieveStringValueForToken($type_hash_token); + URI $base_uri, + QuestionImplementation $question, + \ilPCAnswerForm $content_object + ): EditForm { + $environment = $this->buildEnvironment($base_uri) + ->withActionParameter(self::CMD_CREATE_ANSWER_FORM) + ->withQuestionIdParameter($question->getId()); + + $answer_form_type_class_hash = $environment->getTypeClassHast(); if ($answer_form_type_class_hash !== '') { return $this->forwardCreateAnswerFormCmd( - $this->answer_form_types_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties($question_id), - $url_builder_with_params->withParameter($type_hash_token, $answer_form_type_class_hash), - $step_token + $environment->withAnswerFormTypeHashParameter($answer_form_type_class_hash), + $content_object, + $this->answer_form_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), + $this->answer_form_factory->getDefaultTypeGenericProperties($question->getId()) ); } - return match($this->retrieveStringValueForToken($action_token)) { + return match($environment->getAction()) { self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( - $url_builder_with_params, - $action_token, - $step_token, - $type_hash_token + $environment, + $content_object, + $this->answer_form_factory->getDefaultTypeGenericProperties($question->getId()) ), - default => [$this->buildCreateAnswerForm( - $url_builder_with_params, - $action_token - )] + default => $this->buildCreateAnswerForm($environment) }; } public function editAnswerForm( - URI $base_uri - ): array { - [$url_builder, $action_token] = $this->acquireURLBuilderAndParameters($base_uri); - return match($this->retrieveStringValueForToken($action_token)) { - self::CMD_EDIT_ANSWER_FORM => [$this->processCreateAnswerForm($url_builder, $action_token)], - default => [$this->buildCreateAnswerForm($url_builder, $action_token)] + URI $base_uri, + QuestionImplementation $question, + \ilPCAnswerForm $content_obj + ): EditForm|EditOverview { + $environment = $this->buildEnvironment($base_uri) + ->withActionParameter(self::CMD_EDIT_ANSWER_FORM) + ->withQuestionIdParameter($question->getId()); + + return match($environment->getAction()) { + self::CMD_EDIT_ANSWER_FORM => $this->processCreateAnswerForm($url_builder), + default => $this->forwardEditAnswerFormCmd($environment) }; } private function createQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $create = (new QuestionImplementation())->getEditView( + EnvironmentImplementation $environment + ): EditForm { + $this->initializeEditMode($environment); + + $create = $this->questions_repository->getNew()->getEditView( $this->lng, $this->current_user, $this->ui_factory, @@ -233,37 +188,30 @@ private function createQuestion( $this->ctrl, $this->data_factory )->create( - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION), - $step_token, - $this->retrieveStringValueForToken($step_token) + $environment->withActionParameter(self::CMD_CREATE_QUESTION) ); - if (is_array($create)) { + if ($create instanceof EditForm) { return $create; } $this->questions_repository->store($create); return $this->buildEditStartView( - $url_builder->withParameter($question_id_token, $create->getId()), - $step_token, - $page_id_token, + $environment + ->withDefaultStep() + ->withActionParameter(self::CMD_EDIT_QUESTION) + ->withQuestionIdParameter($create->getId()), $create ); } private function editQuestion( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $question_id_token, - URLBuilderToken $page_id_token - ): array { - $this->initializeEditMode($url_builder, $action_token, $question_id_token); - - $question_id = $this->retrieveQuestionId($question_id_token); + EnvironmentImplementation $environment + ): EditForm { + $this->initializeEditMode($environment); - $url_builder_with_row_id = $url_builder->withParameter($question_id_token, $question_id->toString()); + $question_id = $environment->getQuestionId(); $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( $this->lng, @@ -274,152 +222,92 @@ private function editQuestion( $this->ctrl, $this->data_factory )->edit( - $url_builder_with_row_id->withParameter($action_token, self::CMD_EDIT_QUESTION), - $step_token, - $page_id_token, - $this->retrieveStringValueForToken($step_token) + $environment + ->withActionParameter(self::CMD_EDIT_QUESTION) + ->withQuestionIdParameter($question_id) ); - if (is_array($edit)) { + if ($edit instanceof EditForm) { return $edit; } $this->questions_repository->store($edit); return $this->buildEditStartView( - $url_builder_with_row_id, - $step_token, - $page_id_token, + $environment->withQuestionIdParameter($question_id), $edit ); } private function showTable( \ilToolbarGUI $toolbar, - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token - ): array { + EnvironmentImplementation $environment + ): QuestionsTable { $toolbar->addComponent( $this->ui_factory->button()->standard( $this->lng->txt('create'), - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI()->__toString() + $environment->withActionParameter(self::CMD_CREATE_QUESTION) + ->getUrlBuilder() + ->buildURI() + ->__toString() ) ); - $table = new QuestionsTable( + return new QuestionsTable( $this->ui_factory, $this->ui_services, $this->lng, - $this->answer_form_types_factory, + $this->http->request(), + $this->answer_form_factory, $this->questions_repository, - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $action_token, - $question_id_token + $environment ); - return [ - $table->getFilter($url_builder->buildURI()->__toString()), - $table->getTable()->withRequest($this->http->request()) - - ]; } private function processCreateAnswerForm( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $step_token, - URLBuilderToken $type_hash_token - ): array { - $form = $this->buildCreateAnswerForm( - $url_builder, - $action_token - )->withRequest($this->http->request()); + EnvironmentImplementation $environment, + \ilPCAnswerForm $content_obj, + TypeGenericProperties $generic_answer_form_properties + ): EditForm { + $form = $this->buildCreateAnswerForm($environment)->withRequest($this->http->request()); $data = $form->getData(); - if ($data === null || $data['form_type'] === null) { - return [$form]; - } - - return $this->forwardCreateAnswerFormCmd( - $data['form_type'], - $url_builder->withParameter($type_hash_token, $this->answer_form_types_factory->getHashedClass($data['form_type']::class)), - $step_token - ); + return $data === null + ? $form + : $this->forwardCreateAnswerFormCmd( + $environment->withAnswerFormTypeHashParameterParameter( + $this->answer_form_factory->getHashedClass($data::class) + ), + $content_obj, + $data, + $generic_answer_form_properties + ); } private function forwardCreateAnswerFormCmd( + EnvironmentImplementation $environment, + \ilPCAnswerForm $content_obj, Definition $type, TypeGenericProperties $type_generic_properties, - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array { - return $type->getEditView()->create( - $type->buildProperties($type_generic_properties, []), - $url_builder, - $step_token, - $this->retrieveStringValueForToken($step_token) - ); - } - - private function acquireURLBuilderAndParameters(URI $base_uri): array - { - return (new URLBuilder($base_uri)) - ->acquireParameters( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_PAGE_ID, - self::TOKEN_TYPE_HASH - ); - } - - private function retrieveStringValueForToken( - URLBuilderToken $token - ): string { - return $this->http->wrapper()->query()->retrieve( - $token->getName(), - $this->buildStringTrafo() + ): ?EditForm { + $create = $type->getEditView()->create( + $environment->withProperties( + $type->buildProperties($type_generic_properties, []) + ) ); - } - private function retrieveQuestionId( - URLBuilderToken $question_id_token - ): ?Uuid { - return $this->http->wrapper()->query()->retrieve( - $question_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->custom()->transformation( - fn($v): Uuid => $this->uuid_factory->fromString($v) - ), - $this->refinery->always(null) - ]) - ); - } + if ($create instanceof EditForm) { + return $create; + } - public function retrievePageId( - URLBuilderToken $page_id_token - ): ?int { - return $this->http->wrapper()->query()->retrieve( - $page_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->kindlyTo()->int(), - $this->refinery->always(null) - ]) - ); - } + $this->questions_repository->store($create); + $content_obj->create($create->getAnswerFormId()); + $content_obj->getPage()->update(); - private function buildStringTrafo(): Transformation - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->string(), - $this->refinery->always('') - ]); + $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); } private function initializeEditMode( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token + EnvironmentImplementation $environment ): void { $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::MODE_ENABLED, @@ -427,40 +315,23 @@ private function initializeEditMode( ); $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::QUESTIONLIST_ENTRY, - $this->buildQuestionListSlate($url_builder, $action_token, $question_id_token) + $this->buildQuestionListSlate($environment) ); $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::URL_CLOSE_MODE_INFO, - $url_builder->buildURI() + $environment->getUrlBuilder()->buildURI() ); $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::URL_CREATE_QUESTION, - $url_builder->withParameter($action_token, self::CMD_CREATE_QUESTION)->buildURI() - ); - } - - private function setParametersForQuestionCmds( - URLBuilderToken $question_id_token, - string $question_id, - URLBuilderToken $page_id_token, - int $page_id - ): void { - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $question_id_token->getName(), - $question_id - ); - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $page_id_token->getName(), - $page_id + $environment + ->withActionParameter(self::CMD_CREATE_QUESTION) + ->getUrlBuilder() + ->buildURI() ); } private function buildQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token + EnvironmentImplementation $environment ): LegacySlate { return $this->ui_factory->mainControls()->slate()->legacy( $this->lng->txt('mainbar_button_label_questionlist'), @@ -470,7 +341,7 @@ private function buildQuestionListSlate( $this->ui_factory->panel()->secondary()->listing( $this->lng->txt('mainbar_button_label_questionlist'), [ - $this->buildItemGroupForQuestionListSlate($url_builder, $action_token, $question_id_token) + $this->buildItemGroupForQuestionListSlate($environment) ] ) ) @@ -479,9 +350,7 @@ private function buildQuestionListSlate( } private function buildItemGroupForQuestionListSlate( - URLBuilder $url_builder, - URLBuilderToken $action_token, - URLBuilderToken $question_id_token + EnvironmentImplementation $environment ): ItemGroup { return $this->ui_factory->item()->group( '', @@ -489,8 +358,7 @@ private function buildItemGroupForQuestionListSlate( fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( $v->toEditLink( $this->ui_factory->link(), - $url_builder->withParameter($action_token, self::CMD_EDIT_QUESTION), - $question_id_token + $environment->withActionParameter(self::CMD_EDIT_QUESTION) ) ), iterator_to_array($this->questions_repository->getAllQuestions()) @@ -499,11 +367,9 @@ private function buildItemGroupForQuestionListSlate( } private function buildEditStartView( - URLBuilder $url_builder, - URLBuilderToken $step_token, - URLBuilderToken $page_id_token, + EnvironmentImplementation $environment, QuestionImplementation $question - ): array { + ): EditForm { return $question->getEditView( $this->lng, $this->current_user, @@ -512,36 +378,30 @@ private function buildEditStartView( $this->http->request(), $this->ctrl, $this->data_factory - )->edit( - $url_builder, - $step_token, - $page_id_token, - '' - ); + )->edit($environment); } private function buildCreateAnswerForm( - URLBuilder $url_builder - ): StandardForm { + EnvironmentImplementation $environemt + ): EditForm { $if = $this->ui_factory->input(); - return $if->container()->form()->standard( - $url_builder->buildURI()->__toString(), - [ - 'form_type' => $if->field()->section( - [ + return $this->edit_form_factory->getEditForm( + $environemt->getUrlBuilder(), + $if->field()->section( + [ $if->field()->select( $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect() + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) )->withRequired(true) ], - $this->lng->txt('create_answer_form') - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $vs): ?Form => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) - ) + $this->lng->txt('create_answer_form') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): ?Definition => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) ) - ] - )->withSubmitLabel($this->lng->txt('next')); + ), + false + ); } private function checkCapabilities(array $capabilities): void @@ -552,4 +412,18 @@ private function checkCapabilities(array $capabilities): void } } } + + public function buildEnvironment( + URI $base_uri + ): EnvironmentImplementation { + return new EnvironmentImplementation( + $this->ctrl, + $this->http, + $this->refinery, + $this->uuid_factory, + $this->definitions_factory, + $this->editability, + $base_uri + ); + } } diff --git a/components/ILIAS/Questions/src/PublicInterface.php b/components/ILIAS/Questions/src/PublicInterface.php index 6b089127b8ff..85370e839b29 100644 --- a/components/ILIAS/Questions/src/PublicInterface.php +++ b/components/ILIAS/Questions/src/PublicInterface.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions; -use ILIAS\Questions\Presentation\Edit; +use ILIAS\Questions\Presentation\Views\Edit; class PublicInterface { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Column.php b/components/ILIAS/Questions/src/Question/Persistence/Column.php index 1565f3f35e82..f25e35cbe660 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Column.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Column.php @@ -23,18 +23,18 @@ class Column { public function __construct( - private readonly string $table, + private readonly Table $table, private readonly string $column ) { } - public function getTable(): string + public function getTableName(): string { - return $this->table; + return $this->table->getName(); } - public function toColumnString(): string + public function getColumnString(): string { - return "{$this->table}.{$this->column}"; + return "{$this->table->getName()}.{$this->column}"; } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Question/Persistence/CoreTables.php new file mode 100644 index 000000000000..a8778109cc7a --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/CoreTables.php @@ -0,0 +1,93 @@ + self::QUESTION_TABLE_COLUMNS, + self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS + }; + } + + public function getIdColumn(): Column + { + return match($this) { + self::Questions => new Column( + $this->getTable(), + self::QUESTION_TABLE_ID_COLUMN + ), + self::AnswerForms => new Column( + $this->getTable(), + self::ANSWER_FORM_TABLE_ID_COLUMN + ) + }; + } + + public function getForeignKeyColumn(): ?Column + { + return match($this) { + self::AnswerForms => new Column( + $this->getTable(), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ), + default => null + }; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Insert.php b/components/ILIAS/Questions/src/Question/Persistence/Insert.php new file mode 100644 index 000000000000..1d60dfddad15 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Insert.php @@ -0,0 +1,103 @@ + $columns + */ + public function __construct( + private readonly Table $table, + private readonly array $columns + ) { + foreach ($columns as $column) { + if ($column->getTableName() !== $this->table->getName()) { + throw new \InvalidArgumentException( + "You can only add Columns of the table {$this->table->getName()} to this Insert." + ); + } + } + } + + public function getTableName(): string + { + return $this->table->getName(); + } + + /** + * + * @param array<\ILIAS\Questions\Question\Persistence\Value> $values + * @return self + */ + public function withAdditionalDataSet( + array $values + ): self { + if (count($this->columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be the same amount of Values as there are Columns." + ); + } + + $clone = clone $this; + $clone->values_to_insert[] = $values; + return $clone; + } + + public function toManipulateString(\ilDBInterface $db): string + { + return "INSERT INTO {$this->table->getName()} " + . $this->buildColumnsString() . PHP_EOL + . $this->buildValuesString($db); + } + + private function buildColumnsString(): string + { + return '(' + . implode( + ', ', + array_map( + fn(Column $v): string => $v->getColumnString(), + $this->columns + ) + ) . ')'; + } + + private function buildValuesString(\ilDBInterface $db): string + { + $return = []; + foreach ($this->values_to_insert as $values) { + $return[] = '(' . implode( + ', ', + array_map( + fn(Value $v): string => $v->getQuotedValue($db), + $values + ) + ) . ')'; + } + + return 'VALUES ' . implode( + ',' . PHP_EOL, + $return + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Join.php b/components/ILIAS/Questions/src/Question/Persistence/Join.php index e4848d4d5d47..993fd58124f9 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Join.php @@ -31,7 +31,7 @@ public function __construct( public function toSql(): string { - return "{$this->type->value} JOIN {$this->right->getTable()} ON {$this->left->toColumnString()} = {$this->right->toColumnString()}"; + return "{$this->type->value} JOIN {$this->right->getTableName()} ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; } public function getLeft(): Column diff --git a/components/ILIAS/Questions/src/Question/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Question/Persistence/Manipulate.php new file mode 100644 index 000000000000..861cf97cc7a7 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Manipulate.php @@ -0,0 +1,58 @@ +statements[] = $statement; + return $clone; + } + + public function run(): void + { + $atom_query = $this->db->buildAtomQuery(); + + $manipulates = []; + foreach ($this->statements as $statement) { + $atom_query->addTableLock($statement->getTableName()); + $manipulates[] = $statement->toManipulateString($this->db); + } + $atom_query->addQueryCallable( + function () use ($manipulates): void { + foreach ($manipulates as $manipulate) { + $this->db->manipulate($manipulate); + } + } + ); + $atom_query->run(); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/ManipulateQuery.php b/components/ILIAS/Questions/src/Question/Persistence/ManipulateQuery.php deleted file mode 100644 index 66880fdc2096..000000000000 --- a/components/ILIAS/Questions/src/Question/Persistence/ManipulateQuery.php +++ /dev/null @@ -1,66 +0,0 @@ -available_points = $available_points; - return $clone; - } - - public function withImageSize(int $image_size): self - { - $clone = clone $this; - $clone->image_size = $image_size; - return $clone; - } - - public function withShuffleAnswerOptions(bool $shuffle_answer_options): self - { - $clone = clone $this; - $clone->shuffle_answer_options = $shuffle_answer_options; - return $clone; - } - public function withAddtionalText(string $additional_text): self - { - $clone = clone $this; - $clone->additional_text = $additional_text; - return $clone; - } - -} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Operator.php b/components/ILIAS/Questions/src/Question/Persistence/Operator.php index 70a6124f4a48..41c577f25b13 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Operator.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Operator.php @@ -25,7 +25,7 @@ enum Operator: string case Equal = '='; case Unequal = '<>'; case Greater = '>'; - case Less = '>'; + case Less = '<'; case GreaterOrEqual = '>='; case LessOrEqual = '<='; case In = 'IN'; @@ -42,9 +42,9 @@ public function toSql(Column $left, int $nr_of_values): string } return match($this) { - self::In => "{$left->toColumnString()} {$this->value} ({$placeholders})", - self::Between => "{$left->toColumnString()} {$this->value} %s AND %s", - default => "{$left->toColumnString()} {$this->value} %s" + self::In => "{$left->getColumnString()} {$this->value} ({$placeholders})", + self::Between => "{$left->getColumnString()} {$this->value} %s AND %s", + default => "{$left->getColumnString()} {$this->value} %s" }; } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Order.php b/components/ILIAS/Questions/src/Question/Persistence/Order.php index c0b4d27d2ea1..97e974e809fd 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Order.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Order.php @@ -30,6 +30,6 @@ public function __construct( public function toSql(): string { - return "{$this->column->toColumnString()} {$this->direction->value}"; + return "{$this->column->getColumnString()} {$this->direction->value}"; } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php b/components/ILIAS/Questions/src/Question/Persistence/Query.php similarity index 76% rename from components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php rename to components/ILIAS/Questions/src/Question/Persistence/Query.php index aad33592fdc7..d8baf6a558cf 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/SelectQuery.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Query.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Question\Persistence; -class SelectQuery +class Query { private array $select = []; private array $where = []; @@ -32,47 +32,35 @@ class SelectQuery private array $binding_values = []; public function __construct( - private readonly \ilDBInterface $db, - array $answer_form_ids + private readonly \ilDBInterface $db ) { - $this->select[] = new Select( - Repository::QUESTION_TABLE, - Repository::QUESTION_TABLE_COLUMNS - ); + + $questions_table_definition = CoreTables::Questions; + $answer_form_table_definition = CoreTables::AnswerForms; + $questions_id_column = $questions_table_definition->getIdColumn(); $this->select[] = new Select( - Repository::ANSWER_FORM_TABLE, - Repository::ANSWER_FORM_TABLE_COLUMNS + $questions_table_definition->getTable(), + $questions_table_definition->getColumnsForSelect() ); - $left = new Column( - Repository::QUESTION_TABLE, - Repository::QUESTION_TABLE_ID_COLUMN + $this->select[] = new Select( + $answer_form_table_definition->getTable(), + $answer_form_table_definition->getColumnsForSelect() ); $this->joins[] = new Join( - $left, - new Column( - Repository::ANSWER_FORM_TABLE, - Repository::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN - ) - ); - - $this->where[] = new Where( - $left, - new Value( - \ilDBConstants::T_INTEGER, - $answer_form_ids - ), - Operator::In + $questions_id_column, + $answer_form_table_definition->getForeignKeyColumn(), + JoinType::Left ); $this->order[] = new Order( - Repository::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + $questions_id_column ); $this->order[] = new Order( - Repository::ANSWER_FORM_TABLE_ID_COLUMN + $answer_form_table_definition->getIdColumn() ); } @@ -111,7 +99,7 @@ public function withLimit(int $limit): self return $clone; } - public function toSql(): string + public function toSql(): \ilDBStatement { return $this->db->queryF( 'SELECT ' . implode( @@ -121,14 +109,14 @@ public function toSql(): string static fn(array $c, Select $v): array => [...$c, ...$v->toColumnsArray()], [] ) - ) . ' FROM ' . self::QUESTION_TABLE + ) . ' FROM ' . CoreTables::Questions->value . array_reduce( $this->joins, static fn(string $c, Join $v): string => $c . PHP_EOL . $v->toSql(), '' ) . PHP_EOL . $this->buildWhereString() - . $this->limit !== null ? "LIMIT = {$this->limit}" : '', + . ($this->limit !== null ? "LIMIT = {$this->limit}" : ''), $this->binding_types, $this->binding_values ); @@ -146,7 +134,7 @@ function (?string $c, Where $v): string { return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; } - ); + ) ?? ''; } private function addValueToBinding(Value $value): void diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 94baea58a872..cfa7ee494150 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\Question\Persistence; +use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Definition; -use ILIAS\Questions\AnswerFormTypes\Factory as FormTypesFactory; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -29,53 +29,41 @@ class Repository { - public const string QUESTION_TABLE = 'qsts_questions'; - public const string QUESTION_TABLE_ID_COLUMN = 'id'; - public const array QUESTION_TABLE_COLUMNS = [ - 'id', - 'page_id', - 'title', - 'author', - 'lifecycle', - 'remarks', - 'original_id', - 'last_update', - 'created' - ]; - - public const string ANSWER_FORM_TABLE = 'qsts_answer_forms'; - public const string ANSWER_FORM_TABLE_ID_COLUMN = 'id'; - public const string ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN = 'question_id'; - public const array ANSWER_FORM_TABLE_COLUMNS = [ - 'id AS answer_form_id', - 'type', - 'available_points', - 'image_size', - 'shuffle_answer_options', - 'additional_text', - 'additional_text_legacy' - ]; - - private const string PAGE_EDITOR_TABLE = 'page_object'; - public function __construct( private readonly \ilDBInterface $db, - private readonly UuidFactory $uuid_factory + private readonly UuidFactory $uuid_factory, + private readonly AnswerFormFactory $answer_form_factory ) { } + public function getNew(): QuestionImplementation + { + return new QuestionImplementation( + $this->buildAvailableUuid() + ); + } + /** * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ public function getAllQuestions(): \Generator { - yield from $this->getForWhereClause(''); + yield from $this->getForBaseQuery(new Query($this->db)); } public function getForQuestionId(Uuid $question_id): ?QuestionImplementation { - return $this->getForWhereClause( - "q.id={$this->db->quote($question_id->toString(), \ilDBConstants::T_TEXT)}" + return $this->getForBaseQuery( + (new Query($this->db))->withAdditionalWhere( + new Where( + CoreTables::Questions->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $question_id->toString() + ), + Operator::Equal + ) + ) )->current(); } @@ -86,91 +74,84 @@ public function getForQuestionId(Uuid $question_id): ?QuestionImplementation */ public function getForQuestionIds(array $question_ids): \Generator { - yield from $this->getForWhereClause( - $this->db->in( - 'q.question_id', - $question_ids, - false, - \ilDBConstants::T_INTEGER + yield from $this->getForBaseQuery( + (new Query($this->db))->withAdditionalWhere( + new Where( + CoreTables::Questions->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + array_map( + fn(Uuid $v): string => $v->toString(), + $question_ids + ) + ), + Operator::In + ) ) ); } - private function buildQuestionFromDBRecords(\stdClass $db_record): QuestionImplementation + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + private function getForBaseQuery(Query $query): \Generator { - return new QuestionImplementation( - $this->uuid_factory->fromString($db_record->id), - $db_record->page_id, - $db_record->title, - $db_record->author, - Lifecycle::from($db_record->lifecycle), - $db_record->remarks, - $db_record->original_id === null ? null : $this->uuid_factory->fromString($db_record->original_id), - new \DateTimeImmutable('@' . $db_record->last_update, new \DateTimeZone('UTC')), - new \DateTimeImmutable('@' . $db_record->created, new \DateTimeZone('UTC')) - ); + $result = array_reduce( + $this->answer_form_factory->getAvailableDefinitions(), + fn(Query $c, Definition $v) => $v->getPersistence()->completeQuery( + new TableNameBuilder($v->getPersistence()->getPublicNameSpace()), + $c, + CoreTables::Questions->getIdColumn() + ), + $query + )->toSql(); + + $question_records = [$this->db->fetchObject($result)]; + if ($question_records[0] === null) { + return null; + } + while (($db_record = $this->db->fetchObject($result)) !== null) { + if ($db_record->id === $question_records[0]->id) { + $question_records[] = $db_record; + continue; + } + yield $this->buildQuestionFromDBRecords($question_records); + $question_records = [$db_record]; + } + yield $this->buildQuestionFromDBRecords($question_records); } - /** - * @return \Generator - */ - private function getForWhereClause(string $where): \Generator + private function buildQuestionFromDBRecords(array $db_record): QuestionImplementation { - $query_result = $this->db->query( - 'SELECT q.id, q.page_id, q.title, q.author, q.lifecycle, q.remarks,' . PHP_EOL - . 'q.original_id, q.last_update, q.created' . PHP_EOL - . 'FROM ' . self::QUESTION_TABLE . ' q' . PHP_EOL - . ($where === '' ? '' : 'WHERE ' . $where) + $basic_properties = $db_record[0]; + return new QuestionImplementation( + $this->uuid_factory->fromString($basic_properties->id), + $basic_properties->page_id, + $basic_properties->title, + $basic_properties->author, + Lifecycle::from($basic_properties->lifecycle), + $basic_properties->remarks, + $basic_properties->original_id === null + ? null + : $this->uuid_factory->fromString($basic_properties->original_id), + new \DateTimeImmutable('@' . $basic_properties->last_update, new \DateTimeZone('UTC')), + new \DateTimeImmutable('@' . $basic_properties->created, new \DateTimeZone('UTC')) ); - - while (($db_record = $this->db->fetchObject($query_result)) !== null) { - yield $this->buildQuestionFromDBRecords($db_record); - } } /** * - * @param ILIAS\Questions\Question\Question|array $questions + * @param array<\ILIAS\Questions\Question\Persistence\Storable> $storable * @return array */ public function store( - QuestionImplementation|array $questions - ): array { - if ($questions instanceof QuestionImplementation) { - return [$this->storeQuestion($questions)]; - } - - return array_map( - fn(QuestionImplementation $v) => $this->storeQuestion($v), - $questions - ); - } - - private function storeQuestion(QuestionImplementation $question): Uuid - { - if ($question->getPageId() === null) { - $question = $question->withPageId($this->buildQuestionPage()); - } - - if ($question->getId() === null) { - $question = $question->withQuestionId($this->buildAvailableUuid()); - $this->db->insert( - self::QUESTION_TABLE, - $question->toStorage() - ); - return $question->getId(); - } - $this->db->update( - self::QUESTION_TABLE, - $question->toStorage(), - [ - 'id' => [ - \ilDBConstants::T_TEXT, - $question->getId()->toString() - ] - ] - ); - return $question->getId(); + array $storable + ): void { + array_reduce( + $storable, + fn(Manipulate $c, Storable $v): Manipulate => $v->toStorage($c), + new Manipulate($this->db) + )->run(); } private function buildAvailableUuid(): Uuid @@ -206,7 +187,7 @@ private function getNextAvailableQuestionPageId(): int $last_id = $this->db->fetchObject( $this->db->query( - 'SELECT MAX(page_id) AS last FROM ' . self::PAGE_EDITOR_TABLE + 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value . ' WHERE parent_type = "qsts"' ) )->last; diff --git a/components/ILIAS/Questions/src/Question/Persistence/Select.php b/components/ILIAS/Questions/src/Question/Persistence/Select.php index ab166ef0201f..5e39990d2380 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Select.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Select.php @@ -23,7 +23,7 @@ class Select { public function __construct( - private readonly string $table, + private readonly Table $table, private readonly array $columns ) { } @@ -31,7 +31,7 @@ public function __construct( public function toColumnsArray(): array { return array_map( - fn(string $v): string => "{$this->table}.{$v}", + fn(string $v): string => "{$this->table->getName()}.{$v}", $this->columns ); } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Storable.php b/components/ILIAS/Questions/src/Question/Persistence/Storable.php new file mode 100644 index 000000000000..6f29a0ea7b8b --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Storable.php @@ -0,0 +1,26 @@ +table_definition instanceof CoreTables) { + return $this->table_definition->value; + } + + return $this->table_name_builder->getTableNameFor( + $this->table_definition, + $this->table_identifier + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php b/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php index f65b225418e9..f686cf99346d 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php +++ b/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php @@ -30,28 +30,21 @@ public function __construct( $this->type_specific_part = $table_name_space->getTypeSpecificTableNamePart(); } - public function getTypeSpecificAnswerFormsTableName(): string - { - return "qsts_answer_forms_{$this->type_specific_part}"; - } - - public function getAnswerInputsTableName(): string - { - return "qsts_answer_inputs_{$this->type_specific_part}"; - } - - public function getAnswerOptionsTableName(): string - { - return "qsts_answer_options_{$this->type_specific_part}"; - } - - public function getResponsesTableName(): string - { - return "qsts_responses_{$this->type_specific_part}"; - } - - public function getAdditionalTableName(string $table_identifier): string - { - return "qsts_{$this->type_specific_part}_{$table_identifier}"; + public function getTableNameFor( + TableTypes $type, + string $identifier = '' + ): string { + if ($type === TableTypes::Additional && $identifier === '') { + throw \InvalidArgumentException( + 'Identifier cannot be empty for type ' . TableTypes::Additional->name . '.' + ); + } + return match ($type) { + TableTypes::TypeSpecificAnswerForms => "qsts_answer_forms_{$this->type_specific_part}", + TableTypes::AnswerInputs => "qsts_answer_inputs_{$this->type_specific_part}", + TableTypes::AnswerOptions => "qsts_answer_options_{$this->type_specific_part}", + TableTypes::Responses => "qsts_responses_{$this->type_specific_part}", + TableTypes::Additional => "qsts_{$this->type_specific_part}_{$identifier}" + }; } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php index d6779d1692ce..fc7c0051d485 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php +++ b/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php @@ -32,7 +32,7 @@ public function __construct( ) { if (mb_strlen($vendor) > 4 || mb_strlen($answer_form_id) > 8) { throw new \InvalidArgumentException( - 'Neither $vendor nor $answer_form_id can be longer then 4 characters.' + '$vendor cannot be longer than 4, $answer_form_id can be longer then 8 characters.' ); } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php new file mode 100644 index 000000000000..4ab28142e239 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php @@ -0,0 +1,47 @@ + new Table( + $this, + $table_name_builder, + $table_identifier + ), + default => new Table( + $this, + $table_name_builder + ) + }; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Update.php b/components/ILIAS/Questions/src/Question/Persistence/Update.php new file mode 100644 index 000000000000..087b30d746e2 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/Update.php @@ -0,0 +1,91 @@ +getTableName() !== $this->table->getName()) { + throw new \InvalidArgumentException( + "You can only add Columns of the table {$this->table->getName()} to this Insert." + ); + } + } + + if (count(columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be the same amount of Values as there are Columns." + ); + } + } + + public function getTableName(): string + { + return $this->table->getName(); + } + + public function toManipulateString(\ilDBInterface $db): string + { + return "UPDATE {$this->table->getName()}" . PHP_EOL + . $this->buildSetterString($db) . PHP_EOL + . $this->buildWhereString($db); + } + + private function buildSetterString(\ilDBInterface $db): string + { + return trim( + array_reduce( + array_keys($this->columns), + fn(string $c, int $v): string => $c + . "{$this->columns[$v]->getColumnString()} = {$this->values[$v]->getQuotedValue()},", + 'SET ' + ), + ',' + ); + } + + private function buildWhereString(\ilDBInterface $db): string + { + $values = []; + + return sprintf( + array_reduce( + $this->where, + function (?string $c, Where $v) use ($db, &$values): string { + $values[] = $v->getRight()->getQuotedValue($db); + + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + } + ), + $values + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Value.php b/components/ILIAS/Questions/src/Question/Persistence/Value.php index 3fca04d0d4f2..c459a68367f9 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Value.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Value.php @@ -38,6 +38,11 @@ public function getValue(): string|int|array return $this->value; } + public function getQuotedValue(\ilDBInterface $db): string + { + return $db->quote($this->value, $this->type); + } + public function getNumberOfElements(): int { if (is_array($this->value)) { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Where.php b/components/ILIAS/Questions/src/Question/Persistence/Where.php index d155b7f96612..d14febb99cf1 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Where.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Where.php @@ -40,7 +40,7 @@ public function toSql(): string ); } - public function getRight(): int|string + public function getRight(): Value { return $this->right; } diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index a57d35e274f6..c5e8a96776bd 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Question; -interface Question +interface Question extends Persistence\Storable { public function getParticipantView(): Views\Participant; } diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 25a623087044..0beba1272155 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -20,7 +20,9 @@ namespace ILIAS\Questions\Question; -use ILIAS\Questions\AnswerForm\Form; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\Question\Persistence\Manipulate; +use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Uuid; @@ -30,18 +32,19 @@ use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Component\Table\DataRow; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; use ILIAS\Refinery\Factory as Refinery; use Psr\Http\Message\RequestInterface; class QuestionImplementation implements Question { + public bool $self_updated = false; + public array $updated_answer_forms = []; + /** * @param array{string, \ILIAS\Questions\AnswerForm\Form} $answer_forms */ public function __construct( - private ?Uuid $id = null, + private readonly Uuid $id, private ?int $page_id = null, private string $title = '', private string $author = '', @@ -49,7 +52,7 @@ public function __construct( private string $remarks = '', private ?Uuid $original_id = null, private ?\DateTimeImmutable $last_update = null, - private ?\DateTimeImmutable $created = null, + private readonly ?\DateTimeImmutable $created = null, private array $answer_forms = [], private ?Taxonomies $taxonomies = null, private ?ContentForRecapitulation $content_for_recapitulation = null @@ -61,13 +64,6 @@ public function getId(): ?Uuid return $this->id; } - public function withQuestionId(Uuid $question_id): self - { - $clone = clone $this; - $clone->id = $question_id; - return $clone; - } - public function getPageId(): ?int { return $this->page_id; @@ -77,6 +73,7 @@ public function withPageId(int $page_id): self { $clone = clone $this; $clone->page_id = $page_id; + $clone->self_updated = true; return $clone; } @@ -89,6 +86,7 @@ public function withTitle(string $title): self { $clone = clone $this; $clone->title = $title; + $clone->self_updated = true; return $clone; } @@ -101,6 +99,7 @@ public function withAuthor(string $author): self { $clone = clone $this; $clone->author = $author; + $clone->self_updated = true; return $clone; } @@ -113,6 +112,7 @@ public function withLifecycle(Lifecycle $lifecycle): self { $clone = clone $this; $clone->lifecycle = $lifecycle; + $clone->self_updated = true; return $clone; } @@ -125,6 +125,7 @@ public function withRemarks(string $remarks): self { $clone = clone $this; $clone->remarks = $remarks; + $clone->self_updated = true; return $clone; } @@ -137,6 +138,7 @@ public function withOriginalId(Uuid $original_id): self { $clone = clone $this; $clone->original_id = $original_id; + $clone->self_updated = true; return $clone; } @@ -150,26 +152,22 @@ public function getCreated(): ?\DateTimeImmutable return $this->created; } - public function withCreated(\DateTimeImmutable $created): self - { - $clone = clone $this; - $clone->created = $created; - return $clone; - } - public function getAnswerForms(): array { return $this->answer_forms; } - public function getAnswerForm(Uuid $form_id): ?Form + public function getAnswerFormByIdString(string $form_id): ?AnswerFormProperties { - return $this->answer_forms[$form_id->toString()] ?? null; + return $this->answer_forms[$form_id] ?? null; } - public function withAnswerForm(Form $answer_form): self + public function withAnswerForm(AnswerFormProperties $answer_form): self { - $this->answer_forms[$answer_from->getId()->toString()] = $answer_form; + $clone = clone $this; + $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; + $clone->updated_answer_forms[] = $answer_form->getAnswerFormId(); + return $clone; } /** @@ -202,43 +200,40 @@ public function getParticipantView(): Views\Participant public function toEditLink( LinkFactory $link_factory, - URLBuilder $url_builder, - URLBuilderToken $row_id_token + EnvironmentImplementation $environment ): StandardLink { return $link_factory->standard( $this->title, - $url_builder->withParameter( - $row_id_token, - $this->id->toString() - )->buildURI()->__toString() + $environment->withQuestionIdParameter($this->id) + ->getUrlBuilder() + ->buildURI() + ->__toString() ); } public function toTableRow( DataRowBuilder $row_builder, UIFactory $ui_factory, - URLBuilder $url_builder, - URLBuilderToken $row_id_token + EnvironmentImplementation $environment ): DataRow { return $row_builder->buildDataRow( $this->id->toString(), [ 'title' => $ui_factory->link()->standard( $this->title, - $url_builder->withParameter( - $row_id_token, - $this->id->toString() - )->buildURI()->__toString() + $environment->withQuestionIdParameter( + $this->id + )->getUrlBuilder() + ->buildURI() + ->__toString() ) ] ); } - /** - * @return array - */ - public function toStorage(): array - { + public function toStorage( + Manipulate $manipulate + ): Manipulate { return [ 'id' => [\ilDBConstants::T_TEXT, $this->id->toString()], 'page_id' => [\ilDBConstants::T_INTEGER, $this->page_id], diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index e1d9fcab1497..98632822c5a3 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -20,6 +20,9 @@ namespace ILIAS\Questions\Question\Views; +use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; +use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Layout\Definitions\Environment; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; @@ -28,8 +31,8 @@ use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; -use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Component\Panel\Standard as StandardPanel; +use ILIAS\UI\Component\Input\Field\Section; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use Psr\Http\Message\RequestInterface; @@ -52,57 +55,49 @@ public function __construct( } public function create( - URLBuilder $url_builder, - URLBuilderToken $step_token, - string $step - ): array|Question { - return match ($step) { - self::CMD_SAVE_QUESTION => $this->onBasicPropertiesFormSubmission($url_builder, $step_token), - default => [$this->buildBasicPropertiesForm($url_builder, $step_token)] + Environment $environment + ): EditForm|Question { + return match ($environment->getStep()) { + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), + default => $this->buildBasicPropertiesForm($environment) }; } public function edit( - URLBuilder $url_builder, - URLBuilderToken $step_token, - URLBuilderToken $page_id_token, - string $step - ): array|Question { - return match ($step) { - self::CMD_SAVE_QUESTION => $this->onBasicPropertiesFormSubmission($url_builder, $step_token), - default => [ - $this->buildBasicPropertiesForm($url_builder, $step_token), - $this->buildPreviewPanel($url_builder, $page_id_token) - ] + Environment $environment + ): EditForm|Question { + return match ($environment->getStep()) { + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), + default => $this->buildBasicPropertiesForm($environment)->withContentAfterForm( + $this->buildPreviewPanel($environment) + ) }; } private function buildBasicPropertiesForm( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): StandardForm { - return $this->ui_factory->input()->container()->form()->standard( - $url_builder->withParameter($step_token, self::CMD_SAVE_QUESTION) - ->buildURI()->__toString(), - $this->buildBasicPropertiesInputs() + Environment $environment + ): EditForm { + return $environment->getDefinitionsFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), + $this->buildBasicPropertiesInputs(), + true ); } - private function onBasicPropertiesFormSubmission( - URLBuilder $url_builder, - URLBuilderToken $step_token - ): array|Question { - $form = $this->buildBasicPropertiesForm($url_builder, $step_token) - ->withRequest($this->request); - $data = $form->getData(); - if ($data === null) { - return [$form]; - } + private function processBasicPropertiesForm( + Environment $environment + ): EditForm|Question { + $form = $this->buildBasicPropertiesForm( + $environment + )->withRequest($this->request); - return $data['question']; + $data = $form->getData(); + return $data === null + ? $form + : $data; } - private function buildBasicPropertiesInputs(): array + private function buildBasicPropertiesInputs(): Section { $ff = $this->ui_factory->input()->field(); $section = $ff->section( @@ -127,38 +122,35 @@ function (array $c, Lifecycle $v): array { $this->lng->txt('edit_basic_form_properties') )->withAdditionalTransformation($this->buildAddBasicPropertiesToQuestionTrafo()); - return [ - 'question' => $section->withValue([ - 'title' => $this->question->getTitle(), - 'author' => $this->question->getAuthor(), - 'lifecycle' => $this->question->getLifecycle()->value, - 'remarks' => $this->question->getRemarks() - ]) - ]; + return $section->withValue([ + 'title' => $this->question->getTitle(), + 'author' => $this->question->getAuthor(), + 'lifecycle' => $this->question->getLifecycle()->value, + 'remarks' => $this->question->getRemarks() + ]); } private function buildAddBasicPropertiesToQuestionTrafo(): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): QuestionImplementation => new QuestionImplementation( - $this->question?->getId(), - $this->question?->getPageId(), - $vs['title'], - $vs['author'], - Lifecycle::tryFrom($vs['lifecycle']) ?? Lifecycle::Draft, - $vs['remarks'], - $this->question?->getOriginalId(), - $this->question?->getLastUpdate(), - $this->question?->getCreated(), - $this->question?->getAnswerForms() ?? [] - ) + function (array $vs): QuestionImplementation { + $question = $this->question + ->withTitle($vs['title']) + ->withAuthor($vs['author']) + ->withRemarks($vs['remarks']); + + $lifecycle = Lifecycle::tryFrom($vs['lifecycle']); + if ($lifecycle !== null) { + return $question->withLifecycle($lifecycle); + } + + return $question; + } ); } - private function buildPreviewPanel( - URLBuilder $url_builder, - URLBuilderToken $page_id_token - ): StandardPanel { + private function buildPreviewPanel(): StandardPanel + { return $this->ui_factory->panel()->standard( $this->lng->txt('preview'), $this->ui_factory->legacy()->content($this->question->getTitle()) @@ -166,13 +158,7 @@ private function buildPreviewPanel( $this->ui_factory->dropdown()->standard([ $this->ui_factory->link()->standard( $this->lng->txt('edit'), - $url_builder - ->withURI( - $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') - ) - )->withParameter($page_id_token, (string) $this->question->getPageId()) - ->buildURI()->__toString() + $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') ) ]) ); diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index 1237fd2d08e9..b49a7a69e37b 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\Setup; use ILIAS\Questions\Question\Persistence\TableNameBuilder; +use ILIAS\Questions\Question\Persistence\TableTypes; class ClozeQuestionTables implements \ilDatabaseUpdateSteps { @@ -38,7 +39,7 @@ public function prepare(\ilDBInterface $db): void public function step_1(): void { - $table_name = $this->table_name_builder->getTypeSpecificAnswerFormsTableName(); + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::TypeSpecificAnswerForms); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ @@ -66,7 +67,7 @@ public function step_1(): void public function step_2(): void { - $table_name = $this->table_name_builder->getAnswerInputsTableName(); + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerInputs); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -127,7 +128,7 @@ public function step_2(): void public function step_3(): void { - $table_name = $this->table_name_builder->getAnswerOptionsTableName(); + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerOptions); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -176,7 +177,7 @@ public function step_3(): void public function step_4(): void { - $table_name = $this->table_name_builder->getAdditionalTableName('combinations'); + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Additional, 'combinations'); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -212,7 +213,7 @@ public function step_4(): void public function step_5(): void { - $table_name = $this->table_name_builder->getResponsesTableName(); + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Responses); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ From 7f33ed4cccd625416c5af8d5bda84a2faf8c6239 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 23 Dec 2025 14:38:50 +0100 Subject: [PATCH 031/108] Questions: Implementing Persistence --- .../ILIAS/Questions/Legacy/LocalDIC.php | 11 +- .../PageEditor/class.ilPCAnswerForm.php | 38 +- .../Questions/src/AnswerForm/Definition.php | 5 +- .../Questions/src/AnswerForm/Factory.php | 40 +- .../Questions/src/AnswerForm/Persistence.php | 26 +- .../Questions/src/AnswerForm/Properties.php | 3 +- .../src/AnswerForm/TypeGenericProperties.php | 86 +++- .../src/AnswerFormTypes/Cloze/Definition.php | 9 +- .../src/AnswerFormTypes/Cloze/Persistence.php | 174 +++++--- .../Cloze/Properties/AnswerForm/Factory.php | 34 +- .../Properties/AnswerForm/Properties.php | 116 +++++- .../Cloze/Properties/ClozeText/Text.php | 21 +- .../AnswerOption.php | 39 +- .../AnswerOptions.php | 51 ++- .../Properties/Gaps/AnswerOptions/Factory.php | 126 ++++++ .../Cloze/Properties/Gaps/Factory.php | 50 ++- .../Cloze/Properties/Gaps/Gap.php | 371 +++++++++++++++--- .../Cloze/Properties/Gaps/Gaps.php | 101 ++++- .../Cloze/Properties/Gaps/LongMenu.php | 57 +-- .../Cloze/Properties/Gaps/Numeric.php | 52 +-- .../Properties/Gaps/Properties/Factory.php | 86 ---- .../Properties/Gaps/Properties/Properties.php | 264 ------------- .../Cloze/Properties/Gaps/Select.php | 32 +- .../Cloze/Properties/Gaps/Text.php | 42 +- .../Cloze/Properties/Gaps/Type.php | 14 +- components/ILIAS/Questions/src/Collector.php | 2 +- .../src/{Question => }/Persistence/Column.php | 16 +- .../{Question => }/Persistence/CoreTables.php | 22 +- .../Questions/src/Persistence/Delete.php | 59 +++ .../src/{Question => }/Persistence/Insert.php | 57 +-- .../src/{Question => }/Persistence/Join.php | 2 +- .../{Question => }/Persistence/JoinType.php | 2 +- .../{Question => }/Persistence/Junctor.php | 2 +- .../{Question => }/Persistence/Manipulate.php | 41 +- .../src/Persistence/ManipulationType.php | 27 ++ .../{Question => }/Persistence/Operator.php | 2 +- .../src/{Question => }/Persistence/Order.php | 2 +- .../Persistence/OrderDirection.php | 2 +- .../src/{Question => }/Persistence/Query.php | 96 ++++- .../Questions/src/Persistence/Replace.php | 32 ++ .../Questions/src/Persistence/Repository.php | 274 +++++++++++++ .../src/{Question => }/Persistence/Select.php | 8 +- .../{Question => }/Persistence/Storable.php | 3 +- .../src/{Question => }/Persistence/Table.php | 2 +- .../Persistence/TableNameBuilder.php | 2 +- .../Persistence/TableNameSpace.php | 2 +- .../Persistence/TableNameSpaceCore.php | 2 +- .../{Question => }/Persistence/TableTypes.php | 2 +- .../src/{Question => }/Persistence/Update.php | 32 +- .../src/{Question => }/Persistence/Value.php | 4 +- .../src/{Question => }/Persistence/Where.php | 2 +- .../Definitions/EnvironmentImplementation.php | 2 +- .../Layout/Definitions/QuestionsTable.php | 2 +- .../Questions/src/Presentation/Views/Edit.php | 30 +- .../src/Question/Persistence/Repository.php | 200 ---------- .../ILIAS/Questions/src/Question/Question.php | 7 +- .../src/Question/QuestionImplementation.php | 223 +++++++++-- .../Questions/src/Question/Views/Edit.php | 19 +- .../ILIAS/Questions/src/Setup/Agent.php | 4 +- .../src/Setup/ClozeQuestionTables.php | 4 +- 60 files changed, 2043 insertions(+), 993 deletions(-) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/{Properties => AnswerOptions}/AnswerOption.php (71%) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/{Properties => AnswerOptions}/AnswerOptions.php (84%) create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php rename components/ILIAS/Questions/src/{Question => }/Persistence/Column.php (65%) rename components/ILIAS/Questions/src/{Question => }/Persistence/CoreTables.php (80%) create mode 100644 components/ILIAS/Questions/src/Persistence/Delete.php rename components/ILIAS/Questions/src/{Question => }/Persistence/Insert.php (55%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Join.php (96%) rename components/ILIAS/Questions/src/{Question => }/Persistence/JoinType.php (92%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Junctor.php (93%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Manipulate.php (51%) create mode 100644 components/ILIAS/Questions/src/Persistence/ManipulationType.php rename components/ILIAS/Questions/src/{Question => }/Persistence/Operator.php (96%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Order.php (94%) rename components/ILIAS/Questions/src/{Question => }/Persistence/OrderDirection.php (92%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Query.php (58%) create mode 100644 components/ILIAS/Questions/src/Persistence/Replace.php create mode 100644 components/ILIAS/Questions/src/Persistence/Repository.php rename components/ILIAS/Questions/src/{Question => }/Persistence/Select.php (81%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Storable.php (86%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Table.php (95%) rename components/ILIAS/Questions/src/{Question => }/Persistence/TableNameBuilder.php (97%) rename components/ILIAS/Questions/src/{Question => }/Persistence/TableNameSpace.php (96%) rename components/ILIAS/Questions/src/{Question => }/Persistence/TableNameSpaceCore.php (94%) rename components/ILIAS/Questions/src/{Question => }/Persistence/TableTypes.php (95%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Update.php (76%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Value.php (91%) rename components/ILIAS/Questions/src/{Question => }/Persistence/Where.php (96%) delete mode 100644 components/ILIAS/Questions/src/Question/Persistence/Repository.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index bb6fdf7339f2..4058c8d8616f 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\Legacy; -use ILIAS\Questions\Question\Persistence\Repository as QuestionsRepository; +use ILIAS\Questions\Persistence\Repository as QuestionsRepository; use ILIAS\Questions\AnswerFormTypes\Cloze; -use ILIAS\Questions\Question\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; @@ -60,6 +60,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository => new QuestionsRepository( $DIC['ilDB'], + $DIC['refinery'], new UuidFactory(), $c[AnswerFormFactory::class] ); @@ -91,15 +92,15 @@ protected static function buildDIC(ILIASContainer $DIC): self (new \ilMustacheFactory())->getBasicEngine(), $c[DataFactory::class]->text() ); - $dic[Cloze\Properties\Gaps\Properties\Factory::class] = static fn($c): Cloze\Properties\Gaps\Properties\Factory - => new Cloze\Properties\Gaps\Properties\Factory( + $dic[Cloze\Properties\Gaps\AnswerOptions\Factory::class] = static fn($c): Cloze\Properties\Gaps\AnswerOptions\Factory + => new Cloze\Properties\Gaps\AnswerOptions\Factory( $c[UuidFactory::class], $DIC['refinery'] ); $dic[Cloze\Properties\Gaps\Factory::class] = static fn($c): Cloze\Properties\Gaps\Factory => new Cloze\Properties\Gaps\Factory( $c[UuidFactory::class], - $c[Cloze\Properties\Gaps\Properties\Factory::class], + $c[Cloze\Properties\Gaps\AnswerOptions\Factory::class], [ new Cloze\Properties\Gaps\Text( $DIC['refinery'], diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 8881d2f112aa..b5be94a4d7f7 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -23,8 +23,8 @@ class ilPCAnswerForm extends ilPageContent { private const string ANSWER_FORM_ELEMENT_TAG = 'AnswerForm'; - private const string ANSWER_FORM_ID_ATTRIBUT = 'Uuid'; - private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_(.*)\]\]\]'; + private const string ANSWER_FORM_ID_ATTRIBUTE = 'Uuid'; + private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_([0-9a-f\-]+)\]\]\]'; public function init(): void { @@ -73,22 +73,30 @@ public function create( $this->hier_id, '', self::ANSWER_FORM_ELEMENT_TAG, - [self::ANSWER_FORM_ID_ATTRIBUT => $answer_form_id->toString()] + [self::ANSWER_FORM_ID_ATTRIBUTE => $answer_form_id->toString()] ); } - private function getAnswerFormIds(): array - { - return array_reduce( - $this->dom_util->path( - $this->getPage()->getDomDoc(), - '//' . self::ANSWER_FORM_ELEMENT_TAG - ), - function (array $c, \DomNode $node): Uuid { - $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUT); - }, - [] - ); + public static function afterPageUpdate( + ilPageObject $page, + DOMDocument $domdoc, + string $xml, + bool $creation + ): void { + if ($page::class !== QstsQuestionPage::class) { + return; + } + + global $DIC; + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + $question = $page->getQuestion(); + + $answer_forms = []; + foreach ($dom_util->path($domdoc, '//AnswerForm') as $node) { + $answer_forms[] = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); + } } public static function handleCopiedContent( diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 2c44dd759687..e623a00b8b98 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -23,6 +23,9 @@ use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Questions\AnswerForm\Persistence; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Language\Language; interface Definition @@ -30,7 +33,7 @@ interface Definition public function getLabel(Language $lng): string; public function buildProperties( TypeGenericProperties $type_generic_properties, - array $type_specific_data + ?Query $query ): Properties; public function getPersistence(): Persistence; public function hasCapability(string $capability_class_name): bool; diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index a281dde81f12..83bfa1b2ed1f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -54,11 +54,22 @@ public function getAvailableDefinitions(): array return array_values($this->available_answer_form_types); } + public function getDefinitionForClass( + string $class + ): Definition { + $definition = $this->available_answer_form_types[$this->getHashedClass($class)] ?? null; + if ($definition === null) { + throw new InvalidArgumentException('This type of answer form does not exist.'); + } + return $definition; + } + /** * @return array */ - public function getAnswerFormTypesArrayForSelect(Language $lng): array - { + public function getAnswerFormTypesArrayForSelect( + Language $lng + ): array { return array_reduce( $this->available_answer_form_types, function (array $c, Definition $v) use ($lng): array { @@ -74,8 +85,9 @@ public function getHashedClass(string $class): string return md5($class); } - public function buildTypeDefinitionFromSelectValue(string $value): Definition - { + public function buildTypeDefinitionFromSelectValue( + string $value + ): Definition { $type = $this->available_answer_form_types[$value] ?? null; if ($type === null) { throw new InvalidArgumentException('This type of answer form does not exist.'); @@ -83,11 +95,27 @@ public function buildTypeDefinitionFromSelectValue(string $value): Definition return $type; } - public function getDefaultTypeGenericProperties(Uuid $question_id): TypeGenericProperties - { + public function getDefaultTypeGenericProperties( + Uuid $question_id + ): TypeGenericProperties { return new TypeGenericProperties( $this->uuid_factory->uuid4(), $question_id ); } + + public function buildTypeGenericPropertiesFromDatabase( + array $db_values + ): TypeGenericProperties { + return new TypeGenericProperties( + $this->uuid_factory->fromString($db_values['id']), + $this->uuid_factory->fromString($db_values['question_id']), + $db_values['type'], + $db_values['available_points'], + $db_values['image_size'], + $db_values['shuffle_answer_options'] === 1, + $db_values['additional_text'], + $db_values['additional_text_legacy'] + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php index 6d97fbe24c5e..958aeeaa860b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -20,16 +20,32 @@ namespace ILIAS\Questions\AnswerForm; -use ILIAS\Questions\Question\Persistence\Column; -use ILIAS\Questions\Question\Persistence\Query; -use ILIAS\Questions\Question\Persistence\TableNameBuilder; -use ILIAS\Questions\Question\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableTypes; interface Persistence { public function getPublicNameSpace(): TableNameSpace; - public function completeQuery( + public function getColumns( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null, + array $columns_to_skip = [] + ): array; + public function getIdColumn( TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null + ): Column; + public function getForeignKeyColumn( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null + ): Column; + public function completeQuery( Query $query, Column $base_table_id_column, ): Query; diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index 2a558d7e79ef..6897d1d0b1ab 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -20,10 +20,11 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Questions\Persistence\Storable; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; -interface Properties +interface Properties extends Storable { public function getAnswerFormId(): ?Uuid; public function getQuestionId(): ?Uuid; diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 7a452ad0c979..da716c041d08 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -20,13 +20,23 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Insert; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; +use ILIAS\Questions\Persistence\Storable; +use ILIAS\Questions\Persistence\Update; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Persistence\Where; use ILIAS\Data\UUID\Uuid; -class TypeGenericProperties +class TypeGenericProperties implements Storable { public function __construct( private readonly Uuid $answer_form_id, private readonly Uuid $question_id, + private readonly ?string $definition_class = null, private ?float $available_points = null, private ?int $image_size = null, private ?bool $shuffle_answer_options = null, @@ -69,4 +79,78 @@ public function getAdditionalTextLegacy(): string { return $this->additional_text_legacy; } + + public function toStorage( + Manipulate $manipulate + ): Manipulate { + if ($this->definition_class === null) { + throw new \UnexpectedValueException( + 'You cannot save a Answer Form without a Type!' + ); + } + return $manipulate->withAdditionalStatement( + $manipulate->getManipulationType() === ManipulationType::Create + ? $this->buildInsertStatement() + : $this->buildUpdateStatement() + ); + } + + public function toDelete( + Manipulate $manipulate + ): Manipulate { + $answer_form_table_definition = CoreTables::AnswerForms; + return $manipulate->withAdditionalStatement( + new Delete( + $answer_form_table_definition->getTable(), + [ + new Where( + $answer_form_table_definition->getIdColumn(), + $this->answer_form_id->toString() + ) + ] + ) + ); + } + + private function buildInsertStatement(): Insert + { + return new Insert( + CoreTables::AnswerForms->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->definition_class), + new Value(\ilDBConstants::T_TEXT, $this->question_id->toString()), + new Value(\ilDBConstants::T_FLOAT, $this->available_points), + new Value(\ilDBConstants::T_INTEGER, $this->image_size), + new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0), + new Value(\ilDBConstants::T_TEXT, $this->additional_text), + new Value(\ilDBConstants::T_TEXT, $this->additional_text_legacy) + + ] + ); + } + + private function buildUpdateStatement(): Update + { + $answer_form_table_definition = CoreTables::AnswerForms; + return new Update( + $answer_form_table_definition->getColumns([ + $answer_form_table_definition->getIdColumn(), + 'type', + 'question_id', + 'additional_text_legacy' + ]), + [ + new Value(\ilDBConstants::T_FLOAT, $this->available_points), + new Value(\ilDBConstants::T_INT, $this->image_size), + new Value(\ilDBConstants::T_INT, $this->shuffle_answer_options ? 1 : 0), + new Value(\ilDBConstants::T_TEXT, $this->additional_text) + + ], + new Where( + $answer_form_table_definition->getIdColumn(), + $this->answer_form_id->toString() + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index e6a9655e911b..8242d8f66026 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -22,11 +22,13 @@ use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; use ILIAS\Questions\AnswerForm\Capabilities\Capability; +use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; +use ILIAS\Questions\Persistence\Query; use ILIAS\Language\Language; class Definition implements DefinitionInterface @@ -50,9 +52,12 @@ public function getLabel(Language $lng): string public function buildProperties( TypeGenericProperties $type_generic_data, - array $type_specific_data + ?Query $query ): Properties { - return $this->properties_factory->fromData($type_generic_data, $type_specific_data); + return $this->properties_factory->fromData( + $type_generic_data, + $query + ); } public function getPersistence(): Persistence diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php index 8789adabaa70..256706b6a3b7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -21,39 +21,45 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\AnswerForm\Persistence as PersistenceInterface; -use ILIAS\Questions\Question\Persistence\TableTypes; -use ILIAS\Questions\Question\Persistence\Query; -use ILIAS\Questions\Question\Persistence\Join; -use ILIAS\Questions\Question\Persistence\Column; -use ILIAS\Questions\Question\Persistence\JoinType; -use ILIAS\Questions\Question\Persistence\Select; -use ILIAS\Questions\Question\Persistence\TableNameBuilder; -use ILIAS\Questions\Question\Persistence\TableNameSpace; -use ILIAS\Questions\Question\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\Join; +use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\JoinType; +use ILIAS\Questions\Persistence\Select; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableNameSpaceCore; class Persistence implements PersistenceInterface { + private const string ID_COLUMN = 'id'; + + private const string ANSWER_FORM_TABLE_ID_COLUMN = 'answer_form_id'; private const string ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array ANSWER_FORM_TABLE_COLUMNS = [ + 'answer_form_id', 'scoring_identical_responses', 'combinations_activated' ]; - private const string ANSWER_INPUTS_TABLE_ID_COLUMN = 'id'; private const string ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array ANSWER_INPUTS_TABLE_COLUMNS = [ - 'id AS answer_input_id', + 'id', + 'answer_form_id', 'position', 'gap_type', 'max_chars', 'step_size', 'text_matching_method', + 'min_autocomplete', 'shuffle_answer_options' ]; private const string ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_input_id'; private const array ANSWER_OPTIONS_TABLE_COLUMNS = [ - 'id AS answer_option_id', + 'id', + 'answer_input_id', 'position', 'text_value', 'points', @@ -61,9 +67,11 @@ class Persistence implements PersistenceInterface 'upper_limit' ]; + private const string COMBINATIONS_TABLE_IDENTIFIER = 'combinations'; private const string COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array COMBINATIONS_TABLE_COLUMNS = [ - 'id AS combination_id', + 'id', + 'answer_form_id', 'answer_options', 'points' ]; @@ -78,78 +86,146 @@ public function getPublicNameSpace(): TableNameSpace return $this->table_namespace; } - public function completeQuery( + public function getColumns( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null, + array $columns_to_skip = [] + ): array { + $table = $table_type->getTable($table_name_builder, $table_identifier); + $column_identifiers = match($table_type) { + TableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, + TableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, + TableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, + TableTypes::Additional => self::COMBINATIONS_TABLE_COLUMNS + }; + return array_map( + fn(string $v): Column => new Column($table, $v), + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ); + } + + public function getIdColumn( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null + ): Column { + return match($table_type) { + TableTypes::TypeSpecificAnswerForms => new Column( + $table_type->getTable($table_name_builder), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ), + default => new Column( + $table_type->getTable($table_name_builder, $table_identifier), + self::ID_COLUMN + ) + }; + } + + public function getForeignKeyColumn( TableNameBuilder $table_name_builder, + TableTypes $table_type, + ?string $table_identifier = null + ): Column { + return match($table_type) { + TableTypes::TypeSpecificAnswerForms => new Column( + $table_type->getTable($table_name_builder), + self::ANSWER_FORM_TABLE_ID_COLUMN + ), + TableTypes::AnswerInputs => new Column( + $table_type->getTable($table_name_builder), + self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::AnswerOptions => new Column( + $table_type->getTable($table_name_builder), + self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::Additional => new Column( + $table_type->getTable($table_name_builder, $table_identifier), + self::COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN + ) + }; + } + + public function completeQuery( Query $query, - Column $base_table_id_column + Column $answer_form_id_column ): Query { - $answer_form_specific_table = TableTypes::TypeSpecificAnswerForms - ->getTable($table_name_builder); - $answer_input_table = TableTypes::AnswerInputs - ->getTable($table_name_builder); - $answer_options_table = TableTypes::AnswerOptions - ->getTable($table_name_builder); - $combinations_table = TableTypes::Additional - ->getTable($table_name_builder, 'combinations'); + $table_name_builder = $query->getTableNameBuilder(Definition::class); + + $answer_form_specific_table_definition = TableTypes::TypeSpecificAnswerForms; + $answer_input_table_definition = TableTypes::AnswerInputs; + $answer_options_table_definition = TableTypes::AnswerOptions; + $combinations_table_definition = TableTypes::Additional; return $query->withAdditionalJoin( new Join( - $base_table_id_column, - new Column( - $answer_form_specific_table, - self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + $answer_form_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + $answer_form_specific_table_definition ), JoinType::Left ) )->withAdditionalSelect( new Select( - $answer_form_specific_table, - self::ANSWER_FORM_TABLE_COLUMNS + $this->getColumns($table_name_builder, $answer_form_specific_table_definition) ) )->withAdditionalJoin( new Join( - $base_table_id_column, - new Column( - $answer_input_table, - self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN + $answer_form_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + $answer_input_table_definition ), JoinType::Left ) )->withAdditionalSelect( new Select( - $answer_input_table, - self::ANSWER_INPUTS_TABLE_COLUMNS + $this->getColumns( + $table_name_builder, + $answer_input_table_definition + ) ) )->withAdditionalJoin( new Join( - new Column( - $answer_input_table, - self::ANSWER_INPUTS_TABLE_ID_COLUMN + $this->getIdColumn( + $table_name_builder, + $answer_input_table_definition ), - new Column( - $answer_options_table, - self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN + $this->getForeignKeyColumn( + $table_name_builder, + $answer_options_table_definition ), JoinType::Left ) )->withAdditionalSelect( new Select( - $answer_options_table, - self::ANSWER_OPTIONS_TABLE_COLUMNS + $this->getColumns( + $table_name_builder, + $answer_options_table_definition + ) ) )->withAdditionalJoin( new Join( - $base_table_id_column, - new Column( - $combinations_table, - self::COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN + $answer_form_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + $combinations_table_definition, + self::COMBINATIONS_TABLE_IDENTIFIER ), JoinType::Left ) )->withAdditionalSelect( new Select( - $combinations_table, - self::COMBINATIONS_TABLE_COLUMNS + $this->getColumns( + $table_name_builder, + $combinations_table_definition, + self::COMBINATIONS_TABLE_IDENTIFIER + ) ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php index 1bde6f2050c1..1e0904d5872a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php @@ -21,10 +21,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm; use ILIAS\Questions\AnswerForm\TypeGenericProperties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableTypes; class Factory { @@ -36,9 +39,9 @@ public function __construct( public function fromData( TypeGenericProperties $type_generic_properties, - array $type_specific_data + ?Query $query ): Properties { - if ($type_specific_data === []) { + if ($query === null) { return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), @@ -50,6 +53,21 @@ public function fromData( ); } + [ + 'scoring_identical_responses' => $scoring_identical_responses, + 'combinations_activated' => $combinations_activated + ] = $query->retrieveCurrentRecord( + TableTypes::TypeSpecificAnswerForms->getTable( + $query->getTableNameBuilder(Definition::class) + ), + $query->getRefinery()->custom()->transformation( + fn(array $vs): array => [ + 'scoring_identical_responses' => ScoringIdentical::tryFrom($vs[0]['scoring_identical_responses']), + 'combinations_activated' => $vs[0]['combinations_activated'] === 1 + ] + ) + ); + return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), @@ -57,9 +75,9 @@ public function fromData( $type_generic_properties->getAdditionalText() ), $type_generic_properties->getAdditionalTextLegacy(), - $type_specific_data['gaps'], - $type_specific_data['identical_scoring'], - $type_specific_data['combinations_enabled'] + $this->gaps_factory->fromDatabase($query), + $scoring_identical_responses, + $combinations_activated ); } @@ -69,7 +87,11 @@ public function fromForm( ScoringIdentical $scoring_of_identical_responses, bool $combinations_enabled ): Properties { - $updated_gaps = $cloze_text->updateGapsFromMarkdown($properties->getGaps()); + $updated_gaps = $cloze_text->updateGapsFromMarkdown( + $properties->getAnswerFormId(), + $properties->getGaps() + ); + return $properties ->withClozeText( $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php index b62ea994ed55..845e6ba93c9f 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php @@ -20,13 +20,23 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm; +use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerForm\Properties as PropertiesInterface; use ILIAS\Questions\AnswerForm\TypeGenericProperties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\Persistence\Insert; +use ILIAS\Questions\Persistence\Update; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; @@ -72,6 +82,7 @@ public function getTypeGenericProperties(): TypeGenericProperties return new TypeGenericProperties( $this->answer_form_id, $this->question_id, + Definition::class, null, null, null, @@ -85,8 +96,9 @@ public function getClozeText(): Text return $this->cloze_text; } - public function withClozeText(Text $cloze_text): self - { + public function withClozeText( + Text $cloze_text + ): self { $clone = clone $this; $clone->cloze_text = $cloze_text; return $clone; @@ -102,8 +114,9 @@ public function getScoringOfIdenticalResponses(): ScoringIdentical return $this->scoring_identical; } - public function withScoringOfIdenticalResponses(ScoringIdentical $scoring_identical): self - { + public function withScoringOfIdenticalResponses( + ScoringIdentical $scoring_identical + ): self { $clone = clone $this; $clone->scoring_identical = $scoring_identical; return $clone; @@ -114,8 +127,9 @@ public function areCombinationsEnabled(): bool return $this->combinations_enabled; } - public function withCombinationsEnabled(bool $combinations_enabled): self - { + public function withCombinationsEnabled( + bool $combinations_enabled + ): self { $clone = clone $this; $clone->combinations_enabled = $combinations_enabled; return $clone; @@ -126,15 +140,17 @@ public function getGaps(): Gaps return $this->gaps; } - public function withGaps(Gaps $gaps): self - { + public function withGaps( + Gaps $gaps + ): self { $clone = clone $this; $clone->gaps = $gaps; return $clone; } - public function getBasicPropertiesForListing(Language $lng): array - { + public function getBasicPropertiesForListing( + Language $lng + ): array { return [ $lng->txt('cloze_text') => $this->cloze_text ->getRenderedMarkdownForEditingPresentation($this->gaps), @@ -240,13 +256,83 @@ public function withValuesFromCarry( $clone->gaps = $carry->retrieve( self::FORM_KEY_GAPS_TO_EDIT, - $clone->cloze_text->updateGapsFromMarkdown($this->getGaps()) - ->getFromCarryTransformation( - $refinery, - $gaps_factory - ) + $clone->cloze_text->updateGapsFromMarkdown( + $this->getAnswerFormId(), + $this->getGaps() + )->getFromCarryTransformation( + $refinery, + $gaps_factory + ) ); return $clone; } + + public function toStorage( + Manipulate $manipulate + ): Manipulate { + $persistence = $manipulate->getPersistenceForDefinitionClass(Definition::class); + $table_name_builder = $manipulate->getTableNameBuilder(Definition::class); + + $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create + ? $this->buildInsertAnswerFormStatement( + $persistence, + $table_name_builder + ) : $this->buildUpdateAnswerFormStatement( + $persistence, + $table_name_builder + ); + + return $this->gaps->toStorage( + $manipulate->withAdditionalStatement( + $answer_form_statement + ), + $persistence, + $table_name_builder + ); + } + + private function buildInsertAnswerFormStatement( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Insert { + $table_definition = TableTypes::TypeSpecificAnswerForms; + return new Insert( + $persistence->getColumns( + $table_name_builder, + $table_definition + ), + [ + new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), + new Value(\ilDBConstants::T_INTEGER, $this->combinations_enabled ? 1 : 0) + ] + ); + } + + private function buildUpdateAnswerFormStatement( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Update { + $table_definition = TableTypes::TypeSpecificAnswerForms; + return new Update( + $persistence->getColumns( + $table_name_builder, + $table_definition + ), + [ + new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), + new Value(\ilDBConstants::T_INTEGER, $this->combinations_enabled ? 1 : 0) + ], + [ + new Where( + $persistence->getIdColumn( + $table_name_builder, + $table_definition + ), + new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 3bb2bed3321b..49ca76b35e41 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -24,6 +24,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Data\Text\Markdown; use ILIAS\Data\Text\Factory as TextFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -84,6 +85,7 @@ public function getRenderedMarkdownForEditingPresentation( } public function updateGapsFromMarkdown( + Uuid $answer_form_id, Gaps $pre_existing_gaps ): Gaps { if ($this->cloze_text->getRawRepresentation() === '') { @@ -93,22 +95,33 @@ public function updateGapsFromMarkdown( $position = 0; return array_reduce( $this->mustache_engine->getTokenizer()->scan($this->cloze_text->getRawRepresentation()), - function (Gaps $c, array $v) use (&$position): Gaps { + function (Gaps $c, array $v) use ($answer_form_id, &$position): Gaps { if ($v['type'] !== '_v' || !str_starts_with($v['name'], Gap::GAP_PLACEHOLDER_NAME)) { return $c; } if ($v['name'] === Gap::GAP_PLACEHOLDER_NAME) { - return $c->withNewGap($position++); + return $c->withNewGap($answer_form_id, $position++); } $gap = $c->getGapByTagName($v['name']); if ($gap !== null) { - return $c->withPosition($position++); + return $c->withGap( + $gap->withProperties( + $gap->getProperties()->withPosition( + $answer_form_id, + $position++ + ) + ) + ); } - return $c->withAdditionalGapFromTagName($v['name'], $position++); + return $c->withAdditionalGapFromTagName( + $answer_form_id, + $v['name'], + $position++ + ); }, $pre_existing_gaps ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php similarity index 71% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index 69140125a5f5..0c8d1132bf44 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -18,9 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; -use ILIAS\Questions\Question\Persistence\ManipulateQuery; +use ILIAS\Questions\Persistence\Replace; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Data\UUID\Uuid; class AnswerOption @@ -34,6 +38,7 @@ class AnswerOption public function __construct( private readonly Uuid $answer_option_id, + private readonly Uuid $answer_input_id, private int $position, private string $text_value = '', private ?float $lower_limit = null, @@ -130,8 +135,36 @@ public function buildArrayForHiddenInput(): array return $values; } - public function toPersistence(ManipulateQuery $query): ManipulateQuery + public function buildReplace( + ?Replace $replace, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Replace { + $table_definition = TableTypes::AnswerOptions; + + if ($replace === null) { + return new Replace( + $persistence->getColumns($table_name_builder, $table_definition), + $this->buildValuesForGapReplace() + ); + } + + return $replace->withAdditionalValues( + $this->buildValuesForGapReplace() + ); + } + + private function buildValuesForGapReplace(): array { + return [ + new Value(\ilDBConstants::T_TEXT, $this->answer_option_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $this->position), + new Value(\ilDBConstants::T_TEXT, $this->text_value), + new Value(\ilDBConstants::T_FLOAT, $this->available_points), + new Value(\ilDBConstants::T_FLOAT, $this->lower_limit), + new Value(\ilDBConstants::T_FLOAT, $this->upper_limit) + ]; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php similarity index 84% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 4bf7df0f3f20..4c90762f832a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -18,9 +18,12 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; -use ILIAS\Questions\Question\Persistence\ManipulateQuery; +use ILIAS\Questions\Persistence\Replace; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -35,10 +38,12 @@ public function __construct( $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); } - public function getAnswerOptionForPositionOrNew(int $position): AnswerOption - { + public function getAnswerOptionForPositionOrNew( + Uuid $answer_input_id, + int $position + ): AnswerOption { return $this->answer_options[$position] - ?? $this->factory->getDefaultAnswerOptionForPosition($position); + ?? $this->factory->getDefaultAnswerOptionForPosition($answer_input_id, $position); } public function getTagsArrayFromAnswerOptions(): array @@ -54,8 +59,9 @@ public function getAnswerOptionsAwardingPoints(): array return $this->answer_options_awarding_points; } - public function withAnswerOptionsAwardingPoints(array $options): self - { + public function withAnswerOptionsAwardingPoints( + array $options + ): self { $clone = clone $this; $clone->answer_options_awarding_points = array_reduce( $options, @@ -71,14 +77,16 @@ function (array $c, string $v): array { return $clone; } - public function withAnswerOptions(array $answer_options): self - { + public function withAnswerOptions( + array $answer_options + ): self { $clone = clone $this; $clone->answer_options = $answer_options; return $clone; } public function withValuesFromHiddenInputValue( + Uuid $answer_input_id, ?string $value ): self { if ($value === null @@ -93,6 +101,7 @@ public function withValuesFromHiddenInputValue( $clone->answer_options = array_map( fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( $vs[AnswerOption::FORM_KEY_ID], + $answer_input_id, $vs[AnswerOption::FORM_KEY_POSITION], $vs[AnswerOption::FORM_KEY_TEXT_VALUE], $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, @@ -114,13 +123,15 @@ public function withValuesFromHiddenInputValue( } public function withAnswerOptionsFromTags( + Uuid $answer_input_id, array $tags ): self { $clone = clone $this; $position = 0; $clone->answer_options = array_map( - function (string $v) use (&$position): AnswerOption { + function (string $v) use ($answer_input_id, &$position): AnswerOption { return $this->buildAnswerOptionFromTag( + $answer_input_id, $position++, $v ); @@ -186,9 +197,20 @@ function (array $c, AnswerOption $v) use ($ff, $build_label): array { ); } - public function toPersistence(ManipulateQuery $query): ManipulateQuery - { - + public function buildReplace( + ?Replace $replace, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Replace { + return array_reduce( + $this->answer_options, + fn(?Replace $c, AnswerOption $v): Replace => $v->buildReplace( + $c, + $persistence, + $table_name_builder + ), + $replace + ); } private function buildAnswerOptionsAwardingPointsFromAnswerOptions( @@ -201,11 +223,12 @@ private function buildAnswerOptionsAwardingPointsFromAnswerOptions( } private function buildAnswerOptionFromTag( + Uuid $answer_input_id, int $position, string $text_value ): AnswerOption { $answer_option = $this->retrieveAnswerOptionByTextValue($text_value) - ?? $this->factory->getDefaultAnswerOptionForPosition($position); + ?? $this->factory->getDefaultAnswerOptionForPosition($answer_input_id, $position); return $answer_option ->withPosition($position) ->withTextValue($text_value); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php new file mode 100644 index 000000000000..9d31efe2526f --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -0,0 +1,126 @@ +uuid_factory->uuid4(), + $answer_input_id, + $position + ); + } + + public function buildAnswerOption( + string $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + ?string $lower_limit, + ?string $upper_limit, + ?string $points + ): AnswerOption { + return new AnswerOption( + $this->uuid_factory->fromString($answer_option_id), + $answer_input_id, + $position, + $text_value, + $this->convertToFloatOrNull($lower_limit), + $this->convertToFloatOrNull($upper_limit), + $this->convertToFloatOrNull($points) + ); + } + + public function fromDatabase( + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::AnswerOptions->getTable($query->getTableNameBuilder(Definition::class)), + $query->getRefinery()->custom()->transformation( + function (array $vs): array { + $previous_answer_input_id = null; + $return_array = []; + $answer_options = []; + foreach ($vs as $v) { + if ($previous_answer_input_id !== null + && $v['answer_input_id'] !== $previous_answer_input_id) { + $return_array[$previous_answer_input_id] = new AnswerOptions( + $this, + $answer_options + ); + $answer_options = []; + } + $previous_answer_input_id = $v['answer_input_id']; + $answer_options[] = new AnswerOption( + $this->uuid_factory->fromString($v['id']), + $this->uuid_factory->fromString($v['answer_input_id']), + $v['position'], + $v['text_value'], + $v['lower_limit'], + $v['upper_limit'], + $v['points'] + ); + } + + $return_array[$v['answer_input_id']] = new AnswerOptions( + $this, + $answer_options + ); + + return $return_array; + } + ) + ); + } + + private function convertToFloatOrNull(?string $value): ?float + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->float(), + $this->refinery->always(null) + ])->transform($value); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 996257b5e236..8a416f060ec9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -20,8 +20,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Factory as AnswerOptionsFactory; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Language\Language; +use ILIAS\Data\UUID\Uuid; use ILIAS\Data\UUID\Factory as UuidFactory; class Factory @@ -30,7 +35,7 @@ class Factory public function __construct( private readonly UuidFactory $uuid_factory, - private readonly PropertiesFactory $data_factory, + private readonly AnswerOptionsFactory $answer_options_factory, array $available_gap_types ) { foreach ($available_gap_types as $type) { @@ -53,16 +58,19 @@ public function getAvailableGapTypesOptionsArray( } public function getNewGap( + Uuid $answer_form_id, int $position, string $id = '' ): Gap { $answer_input_id = $id !== '' ? $this->uuid_factory->fromString($id) : $this->uuid_factory->uuid4(); + return new Gap( $answer_input_id, + $answer_form_id, $position, - $this->data_factory->getDefaultProperties($answer_input_id) + $this->answer_options_factory->getDefaultAnswerOptions() ); } @@ -83,8 +91,42 @@ public function getGapTypeByIdentifier(string $identifier): Type } public function fromDatabase( - array $data + Query $query ): Gaps { + $answer_options = $this->answer_options_factory->fromDatabase($query); + return $query->retrieveCurrentRecord( + TableTypes::AnswerInputs->getTable($query->getTableNameBuilder(Definition::class)), + $query->getRefinery()->custom()->transformation( + function (array $vs) use ($answer_options): Gaps { + $previous_answer_input_id = null; + $gaps = []; + foreach ($vs as $v) { + if ($previous_answer_input_id === $v['id']) { + continue; + } + $previous_answer_input_id = $v['id']; + $gaps[] = new Gap( + $this->uuid_factory->fromString($v['id']), + $this->uuid_factory->fromString($v['answer_form_id']), + $v['position'], + $answer_options[$v['id']], + $this->getGapTypeByIdentifier($v['gap_type']), + $v['max_chars'], + $v['step_size'], + $v['text_matching_method'] === null + ? null + : TextMatchingOptions::tryFrom($v['text_matching_method']), + $v['min_autocomplete'], + $v['shuffle_answer_options'] === 1 + ); + } + return new Gaps( + $this, + $gaps + ); + } + ) + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 5cbc2b73cd3c..c65b249a8298 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -20,77 +20,247 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerForm\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\Persistence\Replace; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; +use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\Refinery\Transformation; class Gap { public const string GAP_PLACEHOLDER_NAME = 'GAP'; private const string FORM_KEY_TYPE = 'type'; - private const string FORM_KEY_DATA = 'data'; + private const string FORM_KEY_MAX_CHARS = 'max_chars'; + private const string FORM_KEY_STEP_SIZE = 'step_size'; + private const string FORM_KEY_TEXT_MATCHING_METHOD = 'matching_method'; + private const string FORM_KEY_MIN_AUTOCOMPLETE = 'min_autocomplete'; + private const string FORM_KEY_SHUFFLE_ANSWER_OPTIONS = 'shuffle'; + private const string FORM_KEY_ANSWER_OPTIONS = 'answer_options'; + /** + * @param array $answer_options + */ public function __construct( - private Uuid $answer_input_id, + private readonly Uuid $answer_input_id, + private readonly Uuid $answer_form_id, private int $position, - private Properties $properties, - private ?Type $type = null + private AnswerOptions $answer_options, + private ?Type $type = null, + private ?int $max_chars = null, + private ?float $step_size = null, + private ?TextMatchingOptions $text_matching_method = null, + private ?int $min_autocomplete = null, + private ?bool $shuffle_answer_options = null ) { } - public function getAnswerInputId(): ?Uuid + public function getAnswerInputId(): Uuid { return $this->answer_input_id; } - public function withAnswerInputId(Uuid $answer_input_id): self + public function getPosition(): int { + return $this->position; + } + + public function withPosition( + int $position + ): self { $clone = clone $this; - $clone->answer_input_id = $answer_input_id; + $clone->position = $position; return $clone; } - public function getPosition(): int + public function isUndefined(): bool { - return $this->position; + return $this->type === null; } - public function withPosition(int $position): self + public function getType(): ?Type { + return $this->type; + } + + public function withType( + Type $type + ): self { $clone = clone $this; - $clone->position = $position; + $clone->type = $type; return $clone; } - public function withType(Type $type): self + public function getMaxChars(): ?int { + return $this->max_chars; + } + + public function withMaxChars( + ?int $max_chars + ): self { $clone = clone $this; - $clone->type = $type; + $clone->max_chars = $max_chars; return $clone; } - public function getProperties(): Properties + public function getStepSize(): ?float { - return $this->properties; + return $this->step_size; + } + + public function withStepSize( + float $step_size + ): self { + $clone = clone $this; + $clone->step_size = $step_size; + return $clone; } - public function withProperties(Properties $properties): self + public function getTextMatchingMethod(): ?TextMatchingOptions { + return $this->text_matching_method; + } + + public function withTextMatchingMethod( + TextMatchingOptions $matching_method + ): self { $clone = clone $this; - $clone->properties = $properties; + $clone->text_matching_method = $matching_method; return $clone; } - public function isUndefined(): bool + public function getMinAutocomplete(): ?int { - return $this->type === null; + return $this->min_autocomplete; + } + + public function withMinAutocomplete( + int $min_autocomplete + ): self { + $clone = clone $this; + $clone->min_autocomplete = $min_autocomplete; + return $clone; + } + + public function getShuffleAnswerOptions(): ?bool + { + return $this->shuffle_answer_options; + } + + public function withShuffleAnswerOptions( + bool $shuffle_answer_options + ): self { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + + public function getAnswerOptions(): AnswerOptions + { + return $this->answer_options; + } + + public function withAnswerOptions( + AnswerOptions $answer_options + ): self { + $clone = clone $this; + $clone->answer_options = $answer_options; + return $clone; + } + + public function getCarryInputs( + FieldFactory $ff + ): Group { + $inputs = []; + if ($this->type !== null) { + $inputs[self::FORM_KEY_TYPE] = $ff->hidden()->withValue($this->type?->getIdentifier() ?? '') + ->withDedicatedName(self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId()); + } + + if ($this->max_chars !== null) { + $inputs[self::FORM_KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()) + ->withDedicatedName(self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId()); + } + + if ($this->step_size !== null) { + $inputs[self::FORM_KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()) + ->withDedicatedName(self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId()); + } + + if ($this->text_matching_method !== null) { + $inputs[self::FORM_KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value) + ->withDedicatedName(self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId()); + } + + if ($this->min_autocomplete !== null) { + $inputs[self::FORM_KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()) + ->withDedicatedName(self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId()); + } + + if ($this->shuffle_answer_options !== null) { + $inputs[self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0') + ->withDedicatedName(self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + } + + $inputs[self::FORM_KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()) + ->withDedicatedName(self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + + return $ff->group($inputs); + } + + public function getFromCarryTransformation( + Refinery $refinery, + Factory $gaps_factory + ): Transformation { + return $refinery->custom()->transformation( + function (CarryWrapper $v) use ($refinery, $gaps_factory): self { + $clone = clone $this; + $clone->type = $this->retrieveTypeFromCarry($refinery, $v, $gaps_factory->getAvailableGapTypes()); + $clone->max_chars = $this->retrieveMaxCharsFromCarry($refinery, $v); + $clone->step_size = $this->retrieveStepSizeFromCarry($refinery, $v); + $clone->text_matching_method = $this->retrieveTextMatchingMethodFromCarry($refinery, $v); + $clone->min_autocomplete = $this->retrieveMinAutocompleteFromCarry($refinery, $v); + $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromCarry($refinery, $v); + $clone->answer_options = $this->retrieveAnswerOptionsFromCarry($refinery, $v); + return $clone; + } + ); + } + + public function buildReplace( + ?Replace $replace, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Replace { + if ($this->type === null) { + throw new \UnexpectedValueException( + 'A Gap without Type cannot be stored.' + ); + } + + $table_definition = TableTypes::AnswerInputs; + + if ($replace === null) { + return new Replace( + $persistence->getColumns($table_name_builder, $table_definition), + $this->buildValuesForGapReplace() + ); + } + + return $replace->withAdditionalValues( + $this->buildValuesForGapReplace() + ); } public function getGapPlaceholder(): string @@ -110,26 +280,27 @@ public function buildShortenedGapRepresentation(): string public function buildGapPlaceholderNameWithId(): string { - return self::GAP_PLACEHOLDER_NAME . '_' . $this->answer_input_id->toString(); + return self::GAP_PLACEHOLDER_NAME . '_' . $this->getAnswerInputId()->toString(); } public function getEditAnswerOptionsSection( Language $lng, FieldFactory $ff ): Section { + $type = $this->getType(); $section = $ff->section( - $this->type->getEditAnswerOptionsInputs($this->properties), - "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" + $type->getEditAnswerOptionsInputs($this), + "{$this->buildShortenedGapName()} ({$lng->txt("{$type->getIdentifier()}_gap")})" ); - $edit_section_constraint = $this->type->getEditAnswerOptionsSectionConstraint(); + $edit_section_constraint = $type->getEditAnswerOptionsSectionConstraint(); if ($edit_section_constraint !== null) { $section = $section->withAdditionalTransformation($edit_section_constraint); } return $section->withAdditionalTransformation( - $this->type->getBuildGapTransformation($this) + $type->getBuildGapTransformation($this) ); } @@ -137,60 +308,130 @@ public function getEditPointsSection( Language $lng, FieldFactory $ff ): Section { + $type = $this->getType(); $section = $ff->section( - $this->type->getEditPointsInputs($this->properties->getAnswerOptions()), - "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" + $type->getEditPointsInputs($this->getAnswerOptions()), + "{$this->buildShortenedGapName()} ({$lng->txt("{$type->getIdentifier()}_gap")})" ); - $edit_section_constraint = $this->type->getEditPointsSectionConstraint(); + $edit_section_constraint = $type->getEditPointsSectionConstraint(); if ($edit_section_constraint !== null) { $section = $section->withAdditionalTransformation($edit_section_constraint); } return $section->withAdditionalTransformation( - $this->type->getAddPointsTransformation($this) + $type->getAddPointsTransformation($this) ); } - public function getCarryInputs( - FieldFactory $ff - ): Group { - return $ff->group([ - self::FORM_KEY_TYPE => $ff->hidden()->withValue($this->type?->getIdentifier() ?? '') - ->withDedicatedName(self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId()), - self::FORM_KEY_DATA => $this->properties->getCarryInputs($ff) - ->withDedicatedName(self::FORM_KEY_DATA . $this->getShortenedAnswerInputId()) - ]); + private function buildValuesForGapReplace(): array + { + return [ + new Value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $this->position), + new Value(\ilDBConstants::T_TEXT, $this->type->getIdentifier()), + new Value(\ilDBConstants::T_INTEGER, $this->max_chars), + new Value(\ilDBConstants::T_FLOAT, $this->step_size), + new Value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), + new Value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), + new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0) + + ]; } - public function getFromCarryTransformation( + private function retrieveTypeFromCarry( Refinery $refinery, - Factory $gaps_factory - ): Transformation { - return $refinery->custom()->transformation( - function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { - if ($v === null) { - return $this; - } - $available_gap_types = $gaps_factory->getAvailableGapTypes(); - return $v->retrieve( - self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->custom()->transformation( - fn(?string $v): self => $available_gap_types[$v] - ? $this->withType($available_gap_types[$v]) - : $this - ), - $refinery->always($this) - ]) - )->withProperties( - $v->retrieve( - self::FORM_KEY_DATA . $this->getShortenedAnswerInputId(), - $this->properties->getFromCarryTransformation($refinery) - ) - ); - } + CarryWrapper $carry, + array $available_gap_types + ): ?Type { + return $carry->retrieve( + self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), + $refinery->custom()->transformation( + fn(?string $v): ?Type => $available_gap_types[$v] ?? $this->getType() + ) + ); + } + + private function retrieveMaxCharsFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): ?int { + return $carry->retrieve( + self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMaxChars()) + ]) + ); + } + + private function retrieveStepSizeFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): ?float { + return $carry->retrieve( + self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always($this->getStepSize()) + ]) + ); + } + + private function retrieveTextMatchingMethodFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): ?TextMatchingOptions { + return $carry->retrieve( + self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), + $refinery->custom()->transformation( + fn(?string $v): ?TextMatchingOptions => $v !== null + ? TextMatchingOptions::tryFrom($v) + : $this->getTextMatchingMethod() + ) + ); + } + + private function retrieveMinAutocompleteFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): ?int { + return $carry->retrieve( + self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMinAutocomplete()) + ]) + ); + } + + private function retrieveShuffleAnswerOptionsFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): ?bool { + return $carry->retrieve( + self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + $refinery->byTrying([ + $refinery->kindlyTo()->bool(), + $refinery->always($this->getShuffleAnswerOptions()) + ]) + ); + } + + private function retrieveAnswerOptionsFromCarry( + Refinery $refinery, + CarryWrapper $carry + ): AnswerOptions { + return $carry->retrieve( + self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + $refinery->custom()->transformation( + fn(?string $v): AnswerOptions => $this->answer_options->withValuesFromHiddenInputValue( + $this->answer_input_id, + $v + ) + ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 6585d277ae40..0df4837ec8bc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -20,6 +20,15 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Junctor; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\Operator; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Persistence\Where; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; @@ -37,13 +46,15 @@ public function __construct( ) { } - public function getGapById(Uuid $gap_id): ?Gap - { + public function getGapById( + Uuid $gap_id + ): ?Gap { return $this->gaps[$gap_id->toString()] ?? null; } - public function getGapByTagName(string $tag_name): ?Gap - { + public function getGapByTagName( + string $tag_name + ): ?Gap { return $this->gaps[$this->extractIdFromTagName($tag_name)] ?? null; } @@ -52,26 +63,36 @@ public function hasAtLeastOneGap(): bool return $this->gaps !== []; } - public function withGap(Gap $gap): self - { + public function withGap( + Gap $gap + ): self { $clone = clone $this; $clone->gaps[$gap->getAnswerInputId()->toString()] = $gap; return $clone; } - public function withNewGap(int $position): self - { - $new_gap = $this->factory->getNewGap($position); + public function withNewGap( + Uuid $answer_form_id, + int $position + ): self { + $new_gap = $this->factory->getNewGap($answer_form_id, $position); $clone = clone $this; $clone->gaps[$new_gap->getAnswerInputId()->toString()] = $new_gap; return $clone; } - public function withAdditionalGapFromTagName(string $tag_name, $position): self - { - $id = $this->extractIdFromTagName($tag_name); + public function withAdditionalGapFromTagName( + Uuid $answer_form_id, + string $tag_name, + int $position + ): self { + $answer_input_id = $this->extractIdFromTagName($tag_name); $clone = clone $this; - $clone->gaps[$id] = $this->factory->getNewGap($position, $id); + $clone->gaps[$answer_input_id] = $this->factory->getNewGap( + $answer_form_id, + $position, + $answer_input_id + ); return $clone; } @@ -238,6 +259,60 @@ function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { ); } + public function toStorage( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + $table_definition = TableTypes::AnswerInputs; + $manipulate->withAdditionalStatement( + new Delete( + $table_definition->getTable($table_name_builder), + [ + new Where( + $persistence->getIdColumn($table_name_builder, $table_definition), + new Value( + \ilDBConstants::T_TEXT, + array_map( + fn(Gap $v): string => $v->getAnswerInputId()->toString(), + $this->gaps + ) + ), + Operator::In, + Junctor::Conjunction, + true + ) + ] + ) + ); + + [ + 'gaps' => $replace_for_gaps, + 'answer_options' => $replace_for_answer_options + ] = array_reduce( + $this->gaps, + fn(array $c, Gap $v): array => [ + 'gaps' => $v->buildReplace( + $c['gaps'], + $persistence, + $table_name_builder + ), + 'answer_options' => $v->getAnswerOptions()->buildReplace( + $c['answer_options'], + $persistence, + $table_name_builder + ) + ], + [ + 'gaps' => null, + 'answer_options' => null + ] + ); + + return $manipulate->withAdditionalStatement($replace_for_gaps) + ->withAdditionalStatement($replace_for_answer_options); + } + private function extractIdFromTagName(string $tag_name): string { return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index 7d23ee8c2c66..1e5bb45de089 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Properties; use ILIAS\FileUpload\MimeType; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -48,14 +48,15 @@ public function getIdentifier(): string return 'long_menu'; } - public function getEditAnswerOptionsInputs(Properties $properties): array - { + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { $ff = $this->ui_factory->input()->field(); return [ 'answer_options' => $ff->tag( $this->lng->txt('answer_options'), [] - )->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + )->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'upload_answer_options' => $ff->file( new UploadAnswerOptionsGUI(), $this->lng->txt('upload_answer_options'), @@ -64,16 +65,16 @@ public function getEditAnswerOptionsInputs(Properties $properties): array 'min_autocomplete' => $ff->numeric( $this->lng->txt('min_autocomplete') )->withRequired(true) - ->withValue($properties->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), + ->withValue($gap->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), 'options_awarding_points' => $ff->tag( $this->lng->txt('answer_options'), - $properties->getAnswerOptions()->getTagsArrayFromAnswerOptions() + $gap->getAnswerOptions()->getTagsArrayFromAnswerOptions() ) ->withRequired(true) ->withValue( array_map( fn(AnswerOption $v): string => $v->getTextValue(), - $properties->getAnswerOptions()->getAnswerOptionsAwardingPoints() + $gap->getAnswerOptions()->getAnswerOptionsAwardingPoints() ) ) ]; @@ -97,8 +98,9 @@ function (array $vs): bool { ); } - public function getEditPointsInputs(AnswerOptions $answer_options): array - { + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { return $answer_options->getEditPointsInputs( $this->ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue(), @@ -121,21 +123,21 @@ function (array $vs): bool { ); } - public function getBuildGapTransformation(Gap $gap): Transformation - { + public function getBuildGapTransformation( + Gap $gap + ): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withProperties( - $gap->getProperties() - ->withMinAutocomplete($vs['min_autocomplete']) - ->withAnswerOptions( - $gap->getProperties()->getAnswerOptions()->withAnswerOptionsFromTags( - array_merge( - $vs['answer_options'], - $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) - ) - )->withAnswerOptionsAwardingPoints($vs['options_awarding_points']) - ) - ) + fn(array $vs): Gap => $gap + ->withMinAutocomplete($vs['min_autocomplete']) + ->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $gap->getAnswerInputId(), + array_merge( + $vs['answer_options'], + $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) + ) + )->withAnswerOptionsAwardingPoints($vs['options_awarding_points']) + ) ); } @@ -144,8 +146,9 @@ public function getAnswerInput(): \ilFormPropertyGUI ; } - private function retrieveAnswerOptionsArrayFromUpload(?array $upload_value): array - { + private function retrieveAnswerOptionsArrayFromUpload( + ?array $upload_value + ): array { if ($upload_value === null || ($decoded_value = base64_decode($upload_value[0] ?? '')) === '') { return []; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index d58ba257a21b..df1cd1bc5dfa 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Properties; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -47,22 +47,27 @@ public function getIdentifier(): string return 'numeric'; } - public function getEditAnswerOptionsInputs(Properties $properties): array - { - $answer_option = $properties->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { + $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew( + $gap->getAnswerInputId(), + 0 + ); + $ff = $this->ui_factory->input()->field(); return [ 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) - ->withStepSize($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) ->withRequired(true) ->withValue($answer_option->getLowerLimit()), 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) - ->withStepSize($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) ->withValue($answer_option->getUpperLimit()), 'step_size' => $ff->numeric($this->lng->txt('step_size')) ->withStepSize(0.000001) ->withRequired(true) - ->withValue($properties->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withValue($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) ]; } @@ -76,8 +81,9 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint ); } - public function getEditPointsInputs(AnswerOptions $answer_options): array - { + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { $inputs = $answer_options->getEditPointsInputs( $this->ui_factory->input()->field(), function (AnswerOption $v): string { @@ -106,19 +112,19 @@ public function getEditPointsSectionConstraint(): ?Constraint return null; } - public function getBuildGapTransformation(Gap $gap): Transformation - { - $properties = $gap->getProperties(); + public function getBuildGapTransformation( + Gap $gap + ): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withProperties( - $properties->withAnswerOptions( - $properties->getAnswerOptions()->withAnswerOptions([ - $properties->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) - ->withLowerLimit($vs['lower_limit']) - ->withUpperLimit($vs['upper_limit']) - ]) - )->withStepSize($vs['step_size']) - ) + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptions([ + $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew( + $gap->getAnswerInputId(), + 0 + )->withLowerLimit($vs['lower_limit']) + ->withUpperLimit($vs['upper_limit']) + ]) + )->withStepSize($vs['step_size']) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php deleted file mode 100644 index 09e2cbd9a0de..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Factory.php +++ /dev/null @@ -1,86 +0,0 @@ -uuid_factory->uuid4(), - $position - ); - } - - public function buildAnswerOption( - string $answer_option_id, - int $position, - string $text_value, - ?string $lower_limit, - ?string $upper_limit, - ?string $points - ): AnswerOption { - return new AnswerOption( - $this->uuid_factory->fromString($answer_option_id), - $position, - $text_value, - $this->convertToFloatOrNull($lower_limit), - $this->convertToFloatOrNull($upper_limit), - $this->convertToFloatOrNull($points) - ); - } - - public function fromDatabase( - array $data - ): Gaps { - - } - - private function convertToFloatOrNull(?string $value): ?float - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->float(), - $this->refinery->always(null) - ])->transform($value); - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php deleted file mode 100644 index 3c7d8ebfad70..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Properties/Properties.php +++ /dev/null @@ -1,264 +0,0 @@ - $answer_options - */ - public function __construct( - private readonly Uuid $answer_input_id, - private AnswerOptions $answer_options, - private ?int $max_chars = null, - private ?float $step_size = null, - private ?TextMatchingOptions $text_matching_method = null, - private ?int $min_autocomplete = null, - private ?bool $shuffle_answer_options = null - ) { - } - - public function getAnswerInputId(): Uuid - { - return $this->answer_input_id; - } - - public function getMaxChars(): ?int - { - return $this->max_chars; - } - - public function withMaxChars(?int $max_chars): self - { - $clone = clone $this; - $clone->max_chars = $max_chars; - return $clone; - } - - public function getStepSize(): ?float - { - return $this->step_size; - } - - public function withStepSize(float $step_size): self - { - $clone = clone $this; - $clone->step_size = $step_size; - return $clone; - } - - public function getTextMatchingMethod(): ?TextMatchingOptions - { - return $this->text_matching_method; - } - - public function withTextMatchingMethod(TextMatchingOptions $matching_method): self - { - $clone = clone $this; - $clone->text_matching_method = $matching_method; - return $clone; - } - - public function getMinAutocomplete(): ?int - { - return $this->min_autocomplete; - } - - public function withMinAutocomplete(int $min_autocomplete): self - { - $clone = clone $this; - $clone->min_autocomplete = $min_autocomplete; - return $clone; - } - - public function getShuffleAnswerOptions(): ?bool - { - return $this->shuffle_answer_options; - } - - public function withShuffleAnswerOptions(bool $shuffle_answer_options): self - { - $clone = clone $this; - $clone->shuffle_answer_options = $shuffle_answer_options; - return $clone; - } - - public function getAnswerOptions(): AnswerOptions - { - return $this->answer_options; - } - - public function withAnswerOptions(AnswerOptions $answer_options): self - { - $clone = clone $this; - $clone->answer_options = $answer_options; - return $clone; - } - - public function getCarryInputs(FieldFactory $ff): Group - { - $inputs = []; - if ($this->max_chars !== null) { - $inputs[self::FORM_KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()) - ->withDedicatedName(self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId()); - } - - if ($this->step_size !== null) { - $inputs[self::FORM_KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()) - ->withDedicatedName(self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId()); - } - - if ($this->text_matching_method !== null) { - $inputs[self::FORM_KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value) - ->withDedicatedName(self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId()); - } - - if ($this->min_autocomplete !== null) { - $inputs[self::FORM_KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()) - ->withDedicatedName(self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId()); - } - - if ($this->shuffle_answer_options !== null) { - $inputs[self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0') - ->withDedicatedName(self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); - } - - $inputs[self::FORM_KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()) - ->withDedicatedName(self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); - - return $ff->group($inputs); - } - - public function getFromCarryTransformation( - Refinery $refinery - ): Transformation { - return $refinery->custom()->transformation( - function (CarryWrapper $v) use ($refinery): self { - $clone = clone $this; - $clone->max_chars = $this->retrieveMaxCharsFromCarry($refinery, $v); - $clone->step_size = $this->retrieveStepSizeFromCarry($refinery, $v); - $clone->text_matching_method = $this->retrieveTextMatchingMethodFromCarry($refinery, $v); - $clone->min_autocomplete = $this->retrieveMinAutocompleteFromCarry($refinery, $v); - $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromCarry($refinery, $v); - $clone->answer_options = $this->retrieveAnswerOptionsFromCarry($refinery, $v); - return $clone; - } - ); - } - - private function retrieveMaxCharsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?int { - return $carry->retrieve( - self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->int(), - $refinery->always($this->getMaxChars()) - ]) - ); - } - - private function retrieveStepSizeFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?float { - return $carry->retrieve( - self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->float(), - $refinery->always($this->getStepSize()) - ]) - ); - } - - private function retrieveTextMatchingMethodFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?TextMatchingOptions { - return $carry->retrieve( - self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), - $refinery->custom()->transformation( - fn(?string $v): ?TextMatchingOptions => $v !== null - ? TextMatchingOptions::tryFrom($v) - : $this->getTextMatchingMethod() - ) - ); - } - - private function retrieveMinAutocompleteFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?int { - return $carry->retrieve( - self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->int(), - $refinery->always($this->getMinAutocomplete()) - ]) - ); - } - - private function retrieveShuffleAnswerOptionsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?bool { - return $carry->retrieve( - self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->bool(), - $refinery->always($this->getShuffleAnswerOptions()) - ]) - ); - } - - private function retrieveAnswerOptionsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): AnswerOptions { - return $carry->retrieve( - self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), - $refinery->custom()->transformation( - fn(?string $v): AnswerOptions => $this->answer_options->withValuesFromHiddenInputValue($v) - ) - ); - } - - private function getShortenedAnswerInputId(): string - { - return mb_substr($this->answer_input_id->toString(), 0, 4); - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 54847e55d77e..e2ccaf3a51da 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -20,9 +20,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -46,18 +45,19 @@ public function getIdentifier(): string return 'select'; } - public function getEditAnswerOptionsInputs(Properties $properties): array - { + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { $ff = $this->ui_factory->input()->field(); return [ 'answer_options' => $ff->tag( $this->lng->txt('answer_options'), [] )->withRequired(true) - ->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'shuffle_answer_options' => $ff->checkbox( $this->lng->txt('shuffle_answers') - )->withValue($properties?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) + )->withValue($gap?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) ]; } @@ -66,8 +66,9 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint return null; } - public function getEditPointsInputs(AnswerOptions $answer_options): array - { + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { return $answer_options->getEditPointsInputs( $this->ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue() @@ -89,13 +90,16 @@ function (array $vs): bool { ); } - public function getBuildGapTransformation(Gap $gap): Transformation - { - $properties = $gap->getProperties(); + public function getBuildGapTransformation( + Gap $gap + ): Transformation { return $this->refinery->custom()->transformation( fn(array $vs): Gap => $gap->withProperties( - $properties->withAnswerOptions( - $properties->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + $gap->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $gap->getAnswerInputId(), + $vs['answer_options'] + ) ) ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 4f99af8a7b10..ab2a08cd3c86 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -20,9 +20,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -47,23 +46,24 @@ public function getIdentifier(): string return 'text'; } - public function getEditAnswerOptionsInputs(Properties $properties): array - { + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { $ff = $this->ui_factory->input()->field(); return [ 'answer_options' => $ff->tag( $this->lng->txt('answer_options'), [] )->withRequired(true) - ->withValue($properties->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'matching_method' => $ff->select( $this->lng->txt('matching_method'), TextMatchingOptions::buildOptionsList($this->lng) )->withRequired(true) - ->withValue($properties->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), + ->withValue($gap->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), 'max_chars' => $ff->numeric( $this->lng->txt('max_chars'), - )->withValue($properties->getMaxChars()) + )->withValue($gap->getMaxChars()) ]; } @@ -78,8 +78,9 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint ); } - public function getEditPointsInputs(AnswerOptions $answer_options): array - { + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { return $answer_options->getEditPointsInputs( $this->ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue() @@ -101,17 +102,20 @@ function (array $vs): bool { ); } - public function getBuildGapTransformation(Gap $gap): Transformation - { - $properties = $gap->getProperties(); + public function getBuildGapTransformation( + Gap $gap + ): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withProperties( - $properties->withMaxChars($vs['max_chars']) - ->withTextMatchingMethod(TextMatchingOptions::tryFrom($vs['matching_method']) ?? self::DEFAULT_TECT_MATCHING_METHOD) - ->withAnswerOptions( - $properties->getAnswerOptions()->withAnswerOptionsFromTags($vs['answer_options']) + fn(array $vs): Gap => $gap->withMaxChars($vs['max_chars']) + ->withTextMatchingMethod( + TextMatchingOptions::tryFrom($vs['matching_method']) + ?? self::DEFAULT_TECT_MATCHING_METHOD + )->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $gap->getAnswerInputId(), + $vs['answer_options'] ) - ) + ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index 6bcb0a700e6a..e0c04bf318e8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -20,8 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; @@ -34,7 +33,7 @@ public function __construct( } abstract public function getIdentifier(): string; - abstract public function getEditAnswerOptionsInputs(Properties $data): array; + abstract public function getEditAnswerOptionsInputs(Gap $gap): array; abstract public function getEditAnswerOptionsSectionConstraint(): ?Constraint; abstract public function getEditPointsInputs(AnswerOptions $answer_options): array; abstract public function getEditPointsSectionConstraint(): ?Constraint; @@ -43,13 +42,10 @@ abstract public function getAnswerInput(): \ilFormPropertyGUI; public function getAddPointsTransformation(Gap $gap): Transformation { - $properties = $gap->getProperties(); return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withProperties( - $properties->withAnswerOptions( - $properties->getAnswerOptions() - ->withAnswerOptionsWithAddedPointsFromForm($this->refinery, $vs) - ) + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions() + ->withAnswerOptionsWithAddedPointsFromForm($this->refinery, $vs) ) ); } diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php index 8208cdb88258..39165d20e65d 100644 --- a/components/ILIAS/Questions/src/Collector.php +++ b/components/ILIAS/Questions/src/Collector.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions; -use ILIAS\Questions\Question\Persistence\Repository; +use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\Question; class Collector diff --git a/components/ILIAS/Questions/src/Question/Persistence/Column.php b/components/ILIAS/Questions/src/Persistence/Column.php similarity index 65% rename from components/ILIAS/Questions/src/Question/Persistence/Column.php rename to components/ILIAS/Questions/src/Persistence/Column.php index f25e35cbe660..63a1aa110f31 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Column.php +++ b/components/ILIAS/Questions/src/Persistence/Column.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Column { public function __construct( private readonly Table $table, - private readonly string $column + private readonly string $identifier ) { } @@ -33,8 +33,18 @@ public function getTableName(): string return $this->table->getName(); } + public function getColumnAlias(): string + { + return "{$this->table->getName()}_{$this->identifier}"; + } + public function getColumnString(): string { - return "{$this->table->getName()}.{$this->column}"; + return "{$this->table->getName()}.{$this->identifier}"; + } + + public function getAliasedColumnString(): string + { + return "{$this->table->getName()}.{$this->identifier} {$this->getColumnAlias()}"; } } diff --git a/components/ILIAS/Questions/src/Question/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php similarity index 80% rename from components/ILIAS/Questions/src/Question/Persistence/CoreTables.php rename to components/ILIAS/Questions/src/Persistence/CoreTables.php index a8778109cc7a..e1940c63ade2 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/CoreTables.php +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; enum CoreTables: string { @@ -35,11 +35,12 @@ enum CoreTables: string 'created' ]; - private const string ANSWER_FORM_TABLE_ID_COLUMN = 'id'; + public const string ANSWER_FORM_TABLE_ID_COLUMN = 'id'; private const string ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN = 'question_id'; private const array ANSWER_FORM_TABLE_COLUMNS = [ - 'id AS answer_form_id', + 'id', 'type', + 'question_id', 'available_points', 'image_size', 'shuffle_answer_options', @@ -58,12 +59,21 @@ public function getTable(): Table return new Table($this); } - public function getColumnsForSelect(): array - { - return match($this) { + public function getColumns( + array $columns_to_skip = [] + ): array { + $table = $this->getTable(); + $column_identifiers = match($this) { self::Questions => self::QUESTION_TABLE_COLUMNS, self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS }; + return array_map( + fn(string $v): Column => new Column($table, $v), + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ); } public function getIdColumn(): Column diff --git a/components/ILIAS/Questions/src/Persistence/Delete.php b/components/ILIAS/Questions/src/Persistence/Delete.php new file mode 100644 index 000000000000..be8173df325e --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Delete.php @@ -0,0 +1,59 @@ + $where + */ + public function __construct( + private readonly Table $table, + private readonly array $where + ) { + } + + public function toManipulateString(\ilDBInterface $db): string + { + return "DELETE FROM {$this->table->getName()}" . PHP_EOL + . $this->buildWhereString($db); + } + + private function buildWhereString(\ilDBInterface $db): string + { + $values = []; + + return sprintf( + array_reduce( + $this->where, + function (?string $c, Where $v) use ($db, &$values): string { + $values[] = $v->getRight()->getQuotedValue($db); + + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + } + ), + $values + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Insert.php b/components/ILIAS/Questions/src/Persistence/Insert.php similarity index 55% rename from components/ILIAS/Questions/src/Question/Persistence/Insert.php rename to components/ILIAS/Questions/src/Persistence/Insert.php index 1d60dfddad15..41a9ec9182b8 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Insert.php +++ b/components/ILIAS/Questions/src/Persistence/Insert.php @@ -15,62 +15,73 @@ * https://github.com/ILIAS-eLearning * * ******************************************************************* */ + declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Insert { - private array $values_to_insert = []; + protected array $value_sets = []; /** - * @param array<\ILIAS\Questions\Question\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values */ public function __construct( - private readonly Table $table, - private readonly array $columns + protected readonly array $columns, + array $values ) { + if ($columns === [] || count($columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be at least one Column and the same amount of Values as there are Columns." + ); + } + + $table_name = $columns[0]->getTableName(); foreach ($columns as $column) { - if ($column->getTableName() !== $this->table->getName()) { + if ($column->getTableName() !== $table_name) { throw new \InvalidArgumentException( - "You can only add Columns of the table {$this->table->getName()} to this Insert." + "All Columns MUST belong to the same Table." ); } } - } - public function getTableName(): string - { - return $this->table->getName(); + $this->value_sets[] = $values; } /** - * - * @param array<\ILIAS\Questions\Question\Persistence\Value> $values - * @return self + * @param array<\ILIAS\Questions\Persistence\Value> $values */ - public function withAdditionalDataSet( + public function withAdditionalValues( array $values ): self { - if (count($this->columns) !== count($values)) { + if (count($values) !== count($this->columns)) { throw new \InvalidArgumentException( "There MUST be the same amount of Values as there are Columns." ); } $clone = clone $this; - $clone->values_to_insert[] = $values; + $clone->value_sets[] = $values; return $clone; } - public function toManipulateString(\ilDBInterface $db): string - { - return "INSERT INTO {$this->table->getName()} " + public function lockTable( + \ilAtomQuery $atom_query + ): void { + $atom_query->addTableLock($this->columns[0]->getTableName()); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { + return "INSERT INTO {$this->columns[0]->getTableName()}" . PHP_EOL . $this->buildColumnsString() . PHP_EOL . $this->buildValuesString($db); } - private function buildColumnsString(): string + protected function buildColumnsString(): string { return '(' . implode( @@ -82,10 +93,10 @@ private function buildColumnsString(): string ) . ')'; } - private function buildValuesString(\ilDBInterface $db): string + protected function buildValuesString(\ilDBInterface $db): string { $return = []; - foreach ($this->values_to_insert as $values) { + foreach ($this->value_sets as $values) { $return[] = '(' . implode( ', ', array_map( diff --git a/components/ILIAS/Questions/src/Question/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php similarity index 96% rename from components/ILIAS/Questions/src/Question/Persistence/Join.php rename to components/ILIAS/Questions/src/Persistence/Join.php index 993fd58124f9..a107fd063115 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Join { diff --git a/components/ILIAS/Questions/src/Question/Persistence/JoinType.php b/components/ILIAS/Questions/src/Persistence/JoinType.php similarity index 92% rename from components/ILIAS/Questions/src/Question/Persistence/JoinType.php rename to components/ILIAS/Questions/src/Persistence/JoinType.php index 4e071ddfe573..e020447a0056 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/JoinType.php +++ b/components/ILIAS/Questions/src/Persistence/JoinType.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; enum JoinType: string { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Junctor.php b/components/ILIAS/Questions/src/Persistence/Junctor.php similarity index 93% rename from components/ILIAS/Questions/src/Question/Persistence/Junctor.php rename to components/ILIAS/Questions/src/Persistence/Junctor.php index 1d7ab4e22f01..6158e1229bce 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Junctor.php +++ b/components/ILIAS/Questions/src/Persistence/Junctor.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; enum Junctor: string { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php similarity index 51% rename from components/ILIAS/Questions/src/Question/Persistence/Manipulate.php rename to components/ILIAS/Questions/src/Persistence/Manipulate.php index 861cf97cc7a7..74cefe53b0b6 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -18,19 +18,48 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; + +use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\AnswerForm\Persistence; class Manipulate { private array $statements; public function __construct( - private readonly \ilDBInterface $db + private readonly \ilDBInterface $db, + private readonly AnswerFormFactory $answer_form_factory, + private readonly ManipulationType $type ) { } + public function getManipulationType(): ManipulationType + { + return $this->type; + } + + public function getPersistenceForDefinitionClass( + string $definition_class + ): Persistence { + return $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence(); + } + + public function getTableNameBuilder( + string $definition_class + ): TableNameBuilder { + return new TableNameBuilder( + $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence() + ->getPublicNameSpace() + ); + } + public function withAdditionalStatement( - Insert|Update $statement + Insert|Update|Replace|Delete $statement ): self { $clone = clone $this; $clone->statements[] = $statement; @@ -43,13 +72,13 @@ public function run(): void $manipulates = []; foreach ($this->statements as $statement) { - $atom_query->addTableLock($statement->getTableName()); + $statement->lockTable($atom_query); $manipulates[] = $statement->toManipulateString($this->db); } $atom_query->addQueryCallable( - function () use ($manipulates): void { + function (\ilDBInterface $db) use ($manipulates): void { foreach ($manipulates as $manipulate) { - $this->db->manipulate($manipulate); + $db->manipulate($manipulate); } } ); diff --git a/components/ILIAS/Questions/src/Persistence/ManipulationType.php b/components/ILIAS/Questions/src/Persistence/ManipulationType.php new file mode 100644 index 000000000000..b52c8c6b6f33 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/ManipulationType.php @@ -0,0 +1,27 @@ +getIdColumn(); $this->select[] = new Select( - $questions_table_definition->getTable(), - $questions_table_definition->getColumnsForSelect() + $questions_table_definition->getColumns() ); $this->select[] = new Select( - $answer_form_table_definition->getTable(), - $answer_form_table_definition->getColumnsForSelect() + $answer_form_table_definition->getColumns() ); $this->joins[] = new Join( @@ -64,6 +71,30 @@ public function __construct( ); } + public function getPersistenceForDefinitionClass( + string $definition_class + ): Persistence { + return $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence(); + } + + public function getTableNameBuilder( + string $definition_class + ): TableNameBuilder { + return new TableNameBuilder( + $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence() + ->getPublicNameSpace() + ); + } + + public function getRefinery(): Refinery + { + return $this->refinery; + } + public function withAdditionalSelect(Select $select): self { $clone = clone $this; @@ -99,7 +130,42 @@ public function withLimit(int $limit): self return $clone; } - public function toSql(): \ilDBStatement + public function loadNextRecord(): \Generator + { + $alias = CoreTables::Questions->getIdColumn()->getColumnAlias(); + + $result = $this->toSql(); + + $this->current_record = [$this->db->fetchAssoc($result)]; + if ($this->current_record[0] === null) { + return null; + } + + while (($db_record = $this->db->fetchAssoc($result)) !== null) { + if ($db_record[$alias] === $this->current_record[0][$alias]) { + $this->current_record[] = $db_record; + continue; + } + yield $this; + $this->current_record = [$db_record]; + } + yield $this; + } + + public function retrieveCurrentRecord( + Table $table, + Transformation $transformation + ): mixed { + $table_name = $table->getName(); + $filtered_record = []; + foreach ($this->current_record as $data_set) { + $filtered_record[] = $this->filterDataSetByTable($table_name, $data_set); + } + + return $transformation->transform($filtered_record); + } + + private function toSql(): \ilDBStatement { return $this->db->queryF( 'SELECT ' . implode( @@ -150,4 +216,20 @@ private function addValueToBinding(Value $value): void $this->binding_values[] = $v; } } + + public function filterDataSetByTable( + string $table_name, + array $data_set + ): array { + return array_reduce( + array_keys($data_set), + function (array $c, string $v) use ($table_name, $data_set): array { + if (str_starts_with($v, $table_name)) { + $c[mb_substr($v, mb_strlen($table_name) + 1)] = $data_set[$v]; + } + return $c; + }, + [] + ); + } } diff --git a/components/ILIAS/Questions/src/Persistence/Replace.php b/components/ILIAS/Questions/src/Persistence/Replace.php new file mode 100644 index 000000000000..3554ea426818 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Replace.php @@ -0,0 +1,32 @@ +columns[0]->getTableName()}" . PHP_EOL + . $this->buildColumnsString() . PHP_EOL + . $this->buildValuesString($db); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php new file mode 100644 index 000000000000..262d8a99d595 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -0,0 +1,274 @@ +buildAvailableUuid() + ); + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getAllQuestions(): \Generator + { + yield from $this->getForBaseQuery( + new Query( + $this->db, + $this->answer_form_factory, + $this->refinery + ) + ); + } + + public function getForQuestionId(Uuid $question_id): ?QuestionImplementation + { + return $this->getForBaseQuery( + (new Query( + $this->db, + $this->answer_form_factory, + $this->refinery + ))->withAdditionalWhere( + new Where( + CoreTables::Questions->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $question_id->toString() + ), + Operator::Equal + ) + ) + )->current(); + } + + /** + * + * @param array<\ILIAS\Data\Uuid> $question_ids + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getForQuestionIds(array $question_ids): \Generator + { + yield from $this->getForBaseQuery( + (new Query( + $this->db, + $this->answer_form_factory, + $this->refinery + ))->withAdditionalWhere( + new Where( + CoreTables::Questions->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + array_map( + fn(Uuid $v): string => $v->toString(), + $question_ids + ) + ), + Operator::In + ) + ) + ); + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + private function getForBaseQuery(Query $query): \Generator + { + $query_with_answer_forms = array_reduce( + $this->answer_form_factory->getAvailableDefinitions(), + fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( + $c, + CoreTables::AnswerForms->getIdColumn() + ), + $query + ); + + foreach ($query_with_answer_forms->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + $this->retrieveAnswerFormsFromQuery($query_with_record) + ); + } + } + + public function create( + array $storable + ): void { + $this->store( + $storable, + new Manipulate( + $this->db, + $this->answer_form_factory, + ManipulationType::Create + ) + ); + } + + public function update( + array $storable + ): void { + $this->store( + $storable, + new Manipulate( + $this->db, + $this->answer_form_factory, + ManipulationType::Update + ) + ); + } + + private function retrieveQuestionFromQuery( + Query $query, + array $answer_forms + ): QuestionImplementation { + return $query->retrieveCurrentRecord( + CoreTables::Questions->getTable(), + $this->refinery->custom()->transformation( + fn(array $vs): QuestionImplementation => new QuestionImplementation( + $this->uuid_factory->fromString($vs[0]['id']), + $vs[0]['page_id'], + $vs[0]['title'], + $vs[0]['author'], + Lifecycle::from($vs[0]['lifecycle']), + $vs[0]['remarks'], + $vs[0]['original_id'] === null + ? null + : $this->uuid_factory->fromString($vs[0]['original_id']), + new \DateTimeImmutable('@' . $vs[0]['last_update'], new \DateTimeZone('UTC')), + new \DateTimeImmutable('@' . $vs[0]['created'], new \DateTimeZone('UTC')), + $answer_forms + ) + ) + ); + } + + private function retrieveAnswerFormsFromQuery( + Query $query + ): array { + return $query->retrieveCurrentRecord( + CoreTables::AnswerForms->getTable(), + $this->refinery->custom()->transformation( + function (array $vs) use ($query): array { + if (count($vs) === 1 && $vs[0]['type'] === null) { + return []; + } + + $answer_forms = []; + $previous_answer_form_id = null; + foreach ($vs as $data_set) { + if ($data_set['id'] === $previous_answer_form_id) { + continue; + } + $previous_answer_form_id = $data_set['id']; + $definition = $this->answer_form_factory + ->getDefinitionForClass($data_set['type']); + $answer_forms[] = $definition->buildProperties( + $this->answer_form_factory->buildTypeGenericPropertiesFromDatabase($data_set), + $query + ); + } + return $answer_forms; + } + ) + ); + } + + /** + * + * @param array<\ILIAS\Questions\Persistence\Storable> $storable + * @return array + */ + private function store( + array $storable, + Manipulate $manipulate + ): void { + array_reduce( + $storable, + fn(Manipulate $c, Storable $v): Manipulate => $v->toStorage($c), + $manipulate + )->run(); + } + + private function buildAvailableUuid(): Uuid + { + do { + $uuid = $this->uuid_factory->uuid4(); + if ($this->checkAvailabilityOfId($uuid)) { + return $uuid; + } + } while (true); + } + + private function checkAvailabilityOfId(Uuid $uuid): bool + { + return $this->db->fetchObject( + $this->db->query( + 'SELECT COUNT(*) as cnt FROM ' . self::QUESTION_TABLE + . " WHERE id='{$uuid->toString()}'" + ) + )->cnt === 0; + } + + private function buildQuestionPage(): int + { + $page = new \QstsQuestionPage(); + $page->setId($this->getNextAvailableQuestionPageId()); + $page->createFromXML(); + return $page->getId(); + } + + private function getNextAvailableQuestionPageId(): int + { + + $last_id = $this->db->fetchObject( + $this->db->query( + 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value + . ' WHERE parent_type = "qsts"' + ) + )->last; + if ($last_id === null) { + return 1; + } + + return $last_id + 1; + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Select.php b/components/ILIAS/Questions/src/Persistence/Select.php similarity index 81% rename from components/ILIAS/Questions/src/Question/Persistence/Select.php rename to components/ILIAS/Questions/src/Persistence/Select.php index 5e39990d2380..ef4c4ad50258 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Select.php +++ b/components/ILIAS/Questions/src/Persistence/Select.php @@ -18,12 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; +/** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + */ class Select { public function __construct( - private readonly Table $table, private readonly array $columns ) { } @@ -31,7 +33,7 @@ public function __construct( public function toColumnsArray(): array { return array_map( - fn(string $v): string => "{$this->table->getName()}.{$v}", + fn(Column $v): string => $v->getAliasedColumnString(), $this->columns ); } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Storable.php b/components/ILIAS/Questions/src/Persistence/Storable.php similarity index 86% rename from components/ILIAS/Questions/src/Question/Persistence/Storable.php rename to components/ILIAS/Questions/src/Persistence/Storable.php index 6f29a0ea7b8b..429e65f8ef30 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Storable.php +++ b/components/ILIAS/Questions/src/Persistence/Storable.php @@ -18,9 +18,10 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; interface Storable { public function toStorage(Manipulate $manipulate): Manipulate; + public function toDelete(Manipulate $manipulate): Manipulate; } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Table.php b/components/ILIAS/Questions/src/Persistence/Table.php similarity index 95% rename from components/ILIAS/Questions/src/Question/Persistence/Table.php rename to components/ILIAS/Questions/src/Persistence/Table.php index facf9b3640de..c9ff6f5cba6e 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Table.php +++ b/components/ILIAS/Questions/src/Persistence/Table.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Table { diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php similarity index 97% rename from components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php rename to components/ILIAS/Questions/src/Persistence/TableNameBuilder.php index f686cf99346d..7d007559198f 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableNameBuilder.php +++ b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class TableNameBuilder { diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php b/components/ILIAS/Questions/src/Persistence/TableNameSpace.php similarity index 96% rename from components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php rename to components/ILIAS/Questions/src/Persistence/TableNameSpace.php index fc7c0051d485..2c8d735bfd20 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpace.php +++ b/components/ILIAS/Questions/src/Persistence/TableNameSpace.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class TableNameSpace { diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php similarity index 94% rename from components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php rename to components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php index 7d207a4b297b..9c8b747841e5 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableNameSpaceCore.php +++ b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class TableNameSpaceCore extends TableNameSpace { diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Persistence/TableTypes.php similarity index 95% rename from components/ILIAS/Questions/src/Question/Persistence/TableTypes.php rename to components/ILIAS/Questions/src/Persistence/TableTypes.php index 4ab28142e239..06116bfe7059 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php +++ b/components/ILIAS/Questions/src/Persistence/TableTypes.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; enum TableTypes { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Update.php b/components/ILIAS/Questions/src/Persistence/Update.php similarity index 76% rename from components/ILIAS/Questions/src/Question/Persistence/Update.php rename to components/ILIAS/Questions/src/Persistence/Update.php index 087b30d746e2..df4e1bd22cdf 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Update.php +++ b/components/ILIAS/Questions/src/Persistence/Update.php @@ -18,39 +18,39 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Update { + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ public function __construct( - private readonly Table $table, private readonly array $columns, private readonly array $values, private readonly array $where ) { + if ($columns === [] || count($columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be at least one Column and the same amount of Values as there are Columns." + ); + } + + $table_name = $columns[0]->getTableName(); foreach ($columns as $column) { - if ($column->getTableName() !== $this->table->getName()) { + if ($column->getTableName() !== $table_name) { throw new \InvalidArgumentException( - "You can only add Columns of the table {$this->table->getName()} to this Insert." + "All Columns MUST belong to the same Table." ); } } - - if (count(columns) !== count($values)) { - throw new \InvalidArgumentException( - "There MUST be the same amount of Values as there are Columns." - ); - } - } - - public function getTableName(): string - { - return $this->table->getName(); } public function toManipulateString(\ilDBInterface $db): string { - return "UPDATE {$this->table->getName()}" . PHP_EOL + return "UPDATE {$this->columns[0]->getTableName()}" . PHP_EOL . $this->buildSetterString($db) . PHP_EOL . $this->buildWhereString($db); } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Value.php b/components/ILIAS/Questions/src/Persistence/Value.php similarity index 91% rename from components/ILIAS/Questions/src/Question/Persistence/Value.php rename to components/ILIAS/Questions/src/Persistence/Value.php index c459a68367f9..9d7e48fa54fc 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Value.php +++ b/components/ILIAS/Questions/src/Persistence/Value.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Value { public function __construct( private readonly string $type, - private readonly string|int|array $value + private readonly null|string|int|float|array $value ) { } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Where.php b/components/ILIAS/Questions/src/Persistence/Where.php similarity index 96% rename from components/ILIAS/Questions/src/Question/Persistence/Where.php rename to components/ILIAS/Questions/src/Persistence/Where.php index d14febb99cf1..179345f5e6cc 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Where.php +++ b/components/ILIAS/Questions/src/Persistence/Where.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Persistence; +namespace ILIAS\Questions\Persistence; class Where { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php index ac538405df51..4d45ddd4c373 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php @@ -128,7 +128,7 @@ public function withQuestionIdParameter(Uuid $question_id): self return $clone; } - public function withAnswerFormTypeHashParameter(string $type_hash): URLBuilder + public function withAnswerFormTypeHashParameter(string $type_hash): self { $clone = clone $this; $clone->url_builder = $this->url_builder diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php index b099f2a16737..7be8ac24903a 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php @@ -23,7 +23,7 @@ use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; -use ILIAS\Questions\Question\Persistence\Repository; +use ILIAS\Questions\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; use ILIAS\UI\Component\Table; diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index c6437358efbb..e92128dad7c5 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -29,7 +29,7 @@ use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\TypeGenericProperties; -use ILIAS\Questions\Question\Persistence\Repository; +use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; @@ -143,15 +143,19 @@ public function createAnswerForm( if ($answer_form_type_class_hash !== '') { return $this->forwardCreateAnswerFormCmd( $environment->withAnswerFormTypeHashParameter($answer_form_type_class_hash), + $question, $content_object, $this->answer_form_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties($question->getId()) + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId() + ) ); } return match($environment->getAction()) { self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( $environment, + $question, $content_object, $this->answer_form_factory->getDefaultTypeGenericProperties($question->getId()) ), @@ -162,14 +166,14 @@ public function createAnswerForm( public function editAnswerForm( URI $base_uri, QuestionImplementation $question, - \ilPCAnswerForm $content_obj + \ilPCAnswerForm $content_object ): EditForm|EditOverview { $environment = $this->buildEnvironment($base_uri) ->withActionParameter(self::CMD_EDIT_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); return match($environment->getAction()) { - self::CMD_EDIT_ANSWER_FORM => $this->processCreateAnswerForm($url_builder), + self::CMD_EDIT_ANSWER_FORM => $this->processCreateAnswerForm($environment->getUrlBuilder()), default => $this->forwardEditAnswerFormCmd($environment) }; } @@ -195,7 +199,7 @@ private function createQuestion( return $create; } - $this->questions_repository->store($create); + $this->questions_repository->create([$create]); return $this->buildEditStartView( $environment ->withDefaultStep() @@ -231,7 +235,7 @@ private function editQuestion( return $edit; } - $this->questions_repository->store($edit); + $this->questions_repository->update([$edit]); return $this->buildEditStartView( $environment->withQuestionIdParameter($question_id), $edit @@ -265,6 +269,7 @@ private function showTable( private function processCreateAnswerForm( EnvironmentImplementation $environment, + QuestionImplementation $question, \ilPCAnswerForm $content_obj, TypeGenericProperties $generic_answer_form_properties ): EditForm { @@ -274,9 +279,10 @@ private function processCreateAnswerForm( return $data === null ? $form : $this->forwardCreateAnswerFormCmd( - $environment->withAnswerFormTypeHashParameterParameter( + $environment->withAnswerFormTypeHashParameter( $this->answer_form_factory->getHashedClass($data::class) ), + $question, $content_obj, $data, $generic_answer_form_properties @@ -285,13 +291,14 @@ private function processCreateAnswerForm( private function forwardCreateAnswerFormCmd( EnvironmentImplementation $environment, + QuestionImplementation $question, \ilPCAnswerForm $content_obj, Definition $type, TypeGenericProperties $type_generic_properties, ): ?EditForm { $create = $type->getEditView()->create( $environment->withProperties( - $type->buildProperties($type_generic_properties, []) + $type->buildProperties($type_generic_properties, null) ) ); @@ -299,7 +306,10 @@ private function forwardCreateAnswerFormCmd( return $create; } - $this->questions_repository->store($create); + $this->questions_repository->create( + [$question->withAnswerForm($create)] + ); + $content_obj->create($create->getAnswerFormId()); $content_obj->getPage()->update(); @@ -385,7 +395,7 @@ private function buildCreateAnswerForm( EnvironmentImplementation $environemt ): EditForm { $if = $this->ui_factory->input(); - return $this->edit_form_factory->getEditForm( + return $environemt->getDefinitionsFactory()->getEditForm( $environemt->getUrlBuilder(), $if->field()->section( [ diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php deleted file mode 100644 index cfa7ee494150..000000000000 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ /dev/null @@ -1,200 +0,0 @@ -buildAvailableUuid() - ); - } - - /** - * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> - */ - public function getAllQuestions(): \Generator - { - yield from $this->getForBaseQuery(new Query($this->db)); - } - - public function getForQuestionId(Uuid $question_id): ?QuestionImplementation - { - return $this->getForBaseQuery( - (new Query($this->db))->withAdditionalWhere( - new Where( - CoreTables::Questions->getIdColumn(), - new Value( - \ilDBConstants::T_TEXT, - $question_id->toString() - ), - Operator::Equal - ) - ) - )->current(); - } - - /** - * - * @param array<\ILIAS\Data\Uuid> $question_ids - * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> - */ - public function getForQuestionIds(array $question_ids): \Generator - { - yield from $this->getForBaseQuery( - (new Query($this->db))->withAdditionalWhere( - new Where( - CoreTables::Questions->getIdColumn(), - new Value( - \ilDBConstants::T_TEXT, - array_map( - fn(Uuid $v): string => $v->toString(), - $question_ids - ) - ), - Operator::In - ) - ) - ); - } - - /** - * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> - */ - private function getForBaseQuery(Query $query): \Generator - { - $result = array_reduce( - $this->answer_form_factory->getAvailableDefinitions(), - fn(Query $c, Definition $v) => $v->getPersistence()->completeQuery( - new TableNameBuilder($v->getPersistence()->getPublicNameSpace()), - $c, - CoreTables::Questions->getIdColumn() - ), - $query - )->toSql(); - - $question_records = [$this->db->fetchObject($result)]; - if ($question_records[0] === null) { - return null; - } - while (($db_record = $this->db->fetchObject($result)) !== null) { - if ($db_record->id === $question_records[0]->id) { - $question_records[] = $db_record; - continue; - } - yield $this->buildQuestionFromDBRecords($question_records); - $question_records = [$db_record]; - } - yield $this->buildQuestionFromDBRecords($question_records); - } - - private function buildQuestionFromDBRecords(array $db_record): QuestionImplementation - { - $basic_properties = $db_record[0]; - return new QuestionImplementation( - $this->uuid_factory->fromString($basic_properties->id), - $basic_properties->page_id, - $basic_properties->title, - $basic_properties->author, - Lifecycle::from($basic_properties->lifecycle), - $basic_properties->remarks, - $basic_properties->original_id === null - ? null - : $this->uuid_factory->fromString($basic_properties->original_id), - new \DateTimeImmutable('@' . $basic_properties->last_update, new \DateTimeZone('UTC')), - new \DateTimeImmutable('@' . $basic_properties->created, new \DateTimeZone('UTC')) - ); - } - - /** - * - * @param array<\ILIAS\Questions\Question\Persistence\Storable> $storable - * @return array - */ - public function store( - array $storable - ): void { - array_reduce( - $storable, - fn(Manipulate $c, Storable $v): Manipulate => $v->toStorage($c), - new Manipulate($this->db) - )->run(); - } - - private function buildAvailableUuid(): Uuid - { - do { - $uuid = $this->uuid_factory->uuid4(); - if ($this->checkAvailabilityOfId($uuid)) { - return $uuid; - } - } while (true); - } - - private function checkAvailabilityOfId(Uuid $uuid): bool - { - return $this->db->fetchObject( - $this->db->query( - 'SELECT COUNT(*) as cnt FROM ' . self::QUESTION_TABLE - . " WHERE id='{$uuid->toString()}'" - ) - )->cnt === 0; - } - - private function buildQuestionPage(): int - { - $page = new \QstsQuestionPage(); - $page->setId($this->getNextAvailableQuestionPageId()); - $page->createFromXML(); - return $page->getId(); - } - - private function getNextAvailableQuestionPageId(): int - { - - $last_id = $this->db->fetchObject( - $this->db->query( - 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value - . ' WHERE parent_type = "qsts"' - ) - )->last; - if ($last_id === null) { - return 1; - } - - return $last_id + 1; - } -} diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index c5e8a96776bd..3172fdf85186 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -20,7 +20,10 @@ namespace ILIAS\Questions\Question; -interface Question extends Persistence\Storable +use ILIAS\Questions\Question\Views\Participant; +use ILIAS\Questions\Persistence\Storable; + +interface Question { - public function getParticipantView(): Views\Participant; + public function getParticipantView(): Participant; } diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 0beba1272155..3620aea14457 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -21,7 +21,14 @@ namespace ILIAS\Questions\Question; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; -use ILIAS\Questions\Question\Persistence\Manipulate; +use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\Persistence\Insert; +use ILIAS\Questions\Persistence\Update; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; +use ILIAS\Questions\Persistence\Storable; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Data\Factory as DataFactory; @@ -35,13 +42,16 @@ use ILIAS\Refinery\Factory as Refinery; use Psr\Http\Message\RequestInterface; -class QuestionImplementation implements Question +class QuestionImplementation implements Question, Storable { - public bool $self_updated = false; - public array $updated_answer_forms = []; + private bool $self_updated = false; + private array $updated_answer_forms = []; + private array $deleted_answer_forms = []; + + private array $answer_forms; /** - * @param array{string, \ILIAS\Questions\AnswerForm\Form} $answer_forms + * @param array{string, \ILIAS\Questions\AnswerForm\Properties} $answer_forms */ public function __construct( private readonly Uuid $id, @@ -53,10 +63,18 @@ public function __construct( private ?Uuid $original_id = null, private ?\DateTimeImmutable $last_update = null, private readonly ?\DateTimeImmutable $created = null, - private array $answer_forms = [], + array $answer_forms = [], private ?Taxonomies $taxonomies = null, private ?ContentForRecapitulation $content_for_recapitulation = null ) { + $this->answer_forms = array_reduce( + $answer_forms, + function (array $c, AnswerFormProperties $v): array { + $c[$v->getAnswerFormId()->toString()] = $v; + return $c; + }, + [] + ); } public function getId(): ?Uuid @@ -69,8 +87,9 @@ public function getPageId(): ?int return $this->page_id; } - public function withPageId(int $page_id): self - { + public function withPageId( + int $page_id + ): self { $clone = clone $this; $clone->page_id = $page_id; $clone->self_updated = true; @@ -82,8 +101,9 @@ public function getTitle(): string return $this->title; } - public function withTitle(string $title): self - { + public function withTitle( + string $title + ): self { $clone = clone $this; $clone->title = $title; $clone->self_updated = true; @@ -95,8 +115,9 @@ public function getAuthor(): string return $this->author; } - public function withAuthor(string $author): self - { + public function withAuthor( + string $author + ): self { $clone = clone $this; $clone->author = $author; $clone->self_updated = true; @@ -108,8 +129,9 @@ public function getLifecycle(): Lifecycle return $this->lifecycle; } - public function withLifecycle(Lifecycle $lifecycle): self - { + public function withLifecycle( + Lifecycle $lifecycle + ): self { $clone = clone $this; $clone->lifecycle = $lifecycle; $clone->self_updated = true; @@ -121,8 +143,9 @@ public function getRemarks(): string return $this->remarks; } - public function withRemarks(string $remarks): self - { + public function withRemarks( + string $remarks + ): self { $clone = clone $this; $clone->remarks = $remarks; $clone->self_updated = true; @@ -134,8 +157,9 @@ public function getOriginalId(): ?Uuid return $this->original_id; } - public function withOriginalId(Uuid $original_id): self - { + public function withOriginalId( + Uuid $original_id + ): self { $clone = clone $this; $clone->original_id = $original_id; $clone->self_updated = true; @@ -157,16 +181,32 @@ public function getAnswerForms(): array return $this->answer_forms; } - public function getAnswerFormByIdString(string $form_id): ?AnswerFormProperties - { + public function getAnswerFormByIdString( + string $form_id + ): ?AnswerFormProperties { return $this->answer_forms[$form_id] ?? null; } - public function withAnswerForm(AnswerFormProperties $answer_form): self - { + public function withAnswerForm( + AnswerFormProperties $answer_form + ): self { $clone = clone $this; $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; - $clone->updated_answer_forms[] = $answer_form->getAnswerFormId(); + $clone->updated_answer_forms[] = $answer_form; + return $clone; + } + + public function withoutDeletedAnswerForms( + array $found_answer_form_ids + ): self { + $clone = clone $this; + foreach (array_keys($this->answer_forms) as $answer_form_id) { + if (!in_array($answer_form_id, $found_answer_form_ids)) { + $this->deleted_answer_forms = $clone->answer_forms[$answer_form_id]; + unset($clone->answer_forms[$answer_form_id]); + } + } + return $clone; } @@ -234,17 +274,132 @@ public function toTableRow( public function toStorage( Manipulate $manipulate ): Manipulate { - return [ - 'id' => [\ilDBConstants::T_TEXT, $this->id->toString()], - 'page_id' => [\ilDBConstants::T_INTEGER, $this->page_id], - 'title' => [\ilDBConstants::T_TEXT, $this->title], - 'author' => [\ilDBConstants::T_TEXT, $this->author], - 'lifecycle' => [\ilDBConstants::T_TEXT, $this->lifecycle->value], - 'remarks' => [\ilDBConstants::T_TEXT, $this->remarks], - 'original_id' => [\ilDBConstants::T_TEXT, $this->original_id?->toString()], - 'last_update' => [\ilDBConstants::T_INTEGER, time()], - 'created' => [\ilDBConstants::T_INTEGER, $this->created?->getTimestamp() ?? time()] - ]; + return $manipulate->getManipulationType() === ManipulationType::Create + ? $this->addInsertStatementsToManipulation($manipulate) + : $this->addUpdateStatementsToManipulation($manipulate); + } + + public function toDelete( + Manipulate $manipulate + ): Manipulate { + ; + } + + private function addInsertStatementsToManipulation( + Manipulate $manipulate + ): Manipulate { + if ($this->created === null) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildInsertQuestionStatement() + ); + } + + if ($this->updated_answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + if ($this->answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->answer_forms + ); + } + + return $manipulate; + } + + private function addUpdateStatementsToManipulation( + Manipulate $manipulate + ): Manipulate { + if ($this->self_updated) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdateQuestionStatement() + ); + } + + if ($this->deleted_answer_forms !== []) { + $manipulate = $this->addDeleteAnswerFormStatementsToManipulate( + $manipulate, + $this->deleted_answer_forms + ); + } + + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + private function buildInsertQuestionStatement(): Insert + { + return new Insert( + CoreTables::Questions->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $this->id->toString()), + new Value(\ilDBConstants::T_INTEGER, $this->page_id), + new Value(\ilDBConstants::T_TEXT, $this->title), + new Value(\ilDBConstants::T_TEXT, $this->author), + new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), + new Value(\ilDBConstants::T_TEXT, $this->remarks), + new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), + new Value(\ilDBConstants::T_INTEGER, time()), + new Value(\ilDBConstants::T_INTEGER, time()) + ] + ); } + private function addAnswerFormStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms + ): Manipulate { + return array_reduce( + $answer_forms, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toStorage( + $v->getTypeGenericProperties()->toStorage($c) + ), + $manipulate + ); + } + + private function addDeleteAnswerFormStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms_to_delete + ): Manipulate { + return array_reduce( + $answer_forms_to_delete, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( + $v->getTypeGenericProperties()->toDelete($c) + ), + $manipulate + ); + } + + private function buildUpdateQuestionStatement(): Update + { + $questions_table_definition = CoreTables::Questions; + return new Update( + $questions_table_definition->getColumns([ + CoreTables::ANSWER_FORM_TABLE_ID_COLUMN, + 'page_id', + 'created' + ]), + [ + new Value(\ilDBConstants::T_TEXT, $this->title), + new Value(\ilDBConstants::T_TEXT, $this->author), + new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), + new Value(\ilDBConstants::T_TEXT, $this->remarks), + new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), + new Value(\ilDBConstants::T_INTEGER, time()) + ], + [ + new Where( + $questions_table_definition->getIdColumn(), + new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 98632822c5a3..fc7667749e75 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -21,16 +21,13 @@ namespace ILIAS\Questions\Question\Views; use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; -use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; -use ILIAS\Questions\Presentation\Layout\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Data\Factory as DataFactory; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\Refinery\Factory as Refinery; @@ -55,7 +52,7 @@ public function __construct( } public function create( - Environment $environment + EnvironmentImplementation $environment ): EditForm|Question { return match ($environment->getStep()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), @@ -64,7 +61,7 @@ public function create( } public function edit( - Environment $environment + EnvironmentImplementation $environment ): EditForm|Question { return match ($environment->getStep()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), @@ -75,7 +72,7 @@ public function edit( } private function buildBasicPropertiesForm( - Environment $environment + EnvironmentImplementation $environment ): EditForm { return $environment->getDefinitionsFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), @@ -85,7 +82,7 @@ private function buildBasicPropertiesForm( } private function processBasicPropertiesForm( - Environment $environment + EnvironmentImplementation $environment ): EditForm|Question { $form = $this->buildBasicPropertiesForm( $environment @@ -149,8 +146,10 @@ function (array $vs): QuestionImplementation { ); } - private function buildPreviewPanel(): StandardPanel - { + private function buildPreviewPanel( + EnvironmentImplementation $environment + ): StandardPanel { + $environment->setParametersForQuestionCmds(); return $this->ui_factory->panel()->standard( $this->lng->txt('preview'), $this->ui_factory->legacy()->content($this->question->getTitle()) diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index e8e6b59f4224..82af39742651 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\Question\Persistence\TableNameBuilder; -use ILIAS\Questions\Question\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Setup\Agent\NullAgent; use ILIAS\Setup\Objective; use ILIAS\Setup\ObjectiveCollection; diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index b49a7a69e37b..f41dc833c0d8 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\Question\Persistence\TableNameBuilder; -use ILIAS\Questions\Question\Persistence\TableTypes; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; class ClozeQuestionTables implements \ilDatabaseUpdateSteps { From 238d91b8955a6307bfeabaeab65b037b0cf5d5f3 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 8 Jan 2026 11:22:18 +0100 Subject: [PATCH 032/108] Questions: Implement Answer Form Editing --- .../Administration/class.ilObjQuestions.php | 6 +- .../class.ilObjQuestionsGUI.php | 8 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 28 +- .../Legacy/PageEditor/QstsQuestionPage.php | 18 +- .../PageEditor/class.QstsQuestionPageGUI.php | 19 +- .../PageEditor/class.ilPCAnswerForm.php | 87 ++-- .../PageEditor/class.ilPCAnswerFormGUI.php | 27 +- components/ILIAS/Questions/Questions.php | 12 +- .../js/dist/ParticipantViewLongMenu.js | 115 ++++++ .../src/AnswerForm/Capabilities/Feedback.php | 10 +- .../src/AnswerForm/Capabilities/Marking.php | 5 +- .../src/AnswerForm/Capabilities/Skills.php | 4 +- .../Questions/src/AnswerForm/Definition.php | 14 +- .../Questions/src/AnswerForm/Factory.php | 2 +- .../Questions/src/AnswerForm/Migration.php | 36 ++ .../Questions/src/AnswerForm/Persistence.php | 4 + .../Questions/src/AnswerForm/Properties.php | 21 +- .../src/AnswerForm/TypeGenericProperties.php | 36 +- .../Questions/src/AnswerForm/Views/Edit.php | 6 +- .../src/AnswerForm/Views/Participant.php | 5 +- .../Cloze/Capabilities/Feedback.php | 14 +- .../Cloze/Capabilities/Marking.php | 10 +- .../Cloze/Capabilities/Skills.php | 7 +- .../src/AnswerFormTypes/Cloze/Definition.php | 26 +- .../Cloze/Layout/OverviewTable.php | 109 +++++ .../AnswerFormTypes/Cloze/MigrationCloze.php | 40 ++ .../Cloze/MigrationLongMenu.php | 40 ++ .../Cloze/MigrationNumeric.php | 40 ++ .../src/AnswerFormTypes/Cloze/Persistence.php | 28 +- .../Cloze/Properties/ClozeText/Factory.php | 15 +- .../Cloze/Properties/ClozeText/Text.php | 38 +- .../Definitions/ScoringIdentical.php | 5 +- .../Properties/{AnswerForm => }/Factory.php | 16 +- .../Gaps/AnswerOptions/AnswerOption.php | 39 +- .../Gaps/AnswerOptions/AnswerOptions.php | 88 +++- .../Properties/Gaps/AnswerOptions/Factory.php | 13 +- .../Cloze/Properties/Gaps/Factory.php | 55 ++- .../Cloze/Properties/Gaps/Gap.php | 52 ++- .../Cloze/Properties/Gaps/Gaps.php | 216 ++++++++-- .../Cloze/Properties/Gaps/LongMenu.php | 49 ++- .../Cloze/Properties/Gaps/Numeric.php | 40 +- .../Cloze/Properties/Gaps/Select.php | 60 ++- .../Cloze/Properties/Gaps/Text.php | 33 +- .../Cloze/Properties/Gaps/Type.php | 27 +- .../Gaps/class.UploadAnswerOptionsGUI.php | 19 +- .../{AnswerForm => }/Properties.php | 111 +++++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 251 +++++++----- .../Cloze/Views/Participant.php | 21 +- components/ILIAS/Questions/src/Collector.php | 17 +- .../Questions/src/Persistence/CoreTables.php | 25 +- .../Questions/src/Persistence/Delete.php | 30 +- .../Questions/src/Persistence/Insert.php | 12 +- .../ILIAS/Questions/src/Persistence/Join.php | 3 +- .../Questions/src/Persistence/Manipulate.php | 13 +- .../src/Persistence/ManipulationType.php | 1 + .../Questions/src/Persistence/Operator.php | 14 +- .../ILIAS/Questions/src/Persistence/Query.php | 57 ++- .../Questions/src/Persistence/Replace.php | 1 + .../Questions/src/Persistence/Repository.php | 126 ++++-- .../Questions/src/Persistence/Storable.php | 9 +- .../src/Persistence/TableNameSpaceCore.php | 1 + .../Questions/src/Persistence/Update.php | 37 +- .../ILIAS/Questions/src/Persistence/Value.php | 20 +- .../ILIAS/Questions/src/Persistence/Where.php | 2 +- .../{Layout => }/Definitions/CarryWrapper.php | 13 +- .../{Layout => }/Definitions/Environment.php | 38 +- .../Definitions/EnvironmentImplementation.php | 377 ++++++++++++++++++ .../{Layout => }/Definitions/Leaf.php | 2 +- .../src/Presentation/Layout/Async.php | 53 +++ .../Definitions/EnvironmentImplementation.php | 200 ---------- .../Layout/{Definitions => }/EditForm.php | 36 +- .../Layout/{Definitions => }/EditOverview.php | 44 +- .../Layout/{Definitions => }/Factory.php | 36 +- .../Layout/GlobalScreen/LayoutProvider.php | 35 +- .../{Definitions => }/QuestionsTable.php | 55 ++- .../Questions/src/Presentation/Views/Edit.php | 284 +++++++++---- .../Definitions/TextMatchingOptions.php | 10 +- .../ILIAS/Questions/src/Question/Question.php | 1 - .../src/Question/QuestionImplementation.php | 168 ++++++-- .../Questions/src/Question/Views/Edit.php | 27 +- .../src/Question/Views/Participant.php | 52 ++- .../Questions/src/Response/Repository.php | 15 +- .../ILIAS/Questions/src/Response/Response.php | 12 +- .../ILIAS/Questions/src/Setup/Agent.php | 65 ++- .../src/Setup/ClozeQuestionTables.php | 6 +- .../src/Setup/OverarchingQuestionTables.php | 36 +- .../src/Setup/QuestionsMigration.php | 279 +++++++++++++ 87 files changed, 3209 insertions(+), 928 deletions(-) create mode 100644 components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js create mode 100644 components/ILIAS/Questions/src/AnswerForm/Migration.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationLongMenu.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationNumeric.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/{AnswerForm => }/Factory.php (86%) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/{AnswerForm => }/Properties.php (78%) rename components/ILIAS/Questions/src/Presentation/{Layout => }/Definitions/CarryWrapper.php (84%) rename components/ILIAS/Questions/src/Presentation/{Layout => }/Definitions/Environment.php (51%) create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php rename components/ILIAS/Questions/src/Presentation/{Layout => }/Definitions/Leaf.php (92%) create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Async.php delete mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php rename components/ILIAS/Questions/src/Presentation/Layout/{Definitions => }/EditForm.php (74%) rename components/ILIAS/Questions/src/Presentation/Layout/{Definitions => }/EditOverview.php (62%) rename components/ILIAS/Questions/src/Presentation/Layout/{Definitions => }/Factory.php (72%) rename components/ILIAS/Questions/src/Presentation/Layout/{Definitions => }/QuestionsTable.php (79%) create mode 100644 components/ILIAS/Questions/src/Setup/QuestionsMigration.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php index cb2e27c8333f..b2cb613898d0 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php @@ -20,8 +20,10 @@ class ilObjQuestions extends ilObject { - public function __construct(int $id = 0, bool $referenced = true) - { + public function __construct( + int $id = 0, + bool $referenced = true + ) { $this->type = 'qsts'; parent::__construct($id, $referenced); } diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 880b35b4543b..982c8c284216 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -77,7 +77,9 @@ public function executeCommand(): void case strtolower(QstsQuestionPageGUI::class): $this->edit_view->forwardPageCmds( $this->tpl, - $this->buildEditQuestionsBaseUri() + $this->buildEditQuestionsBaseUri(), + $this->obj_id, + $this->ref_id ); break; @@ -98,7 +100,9 @@ public function viewQuestionsObject(): void $this->tpl->setContent( $this->edit_view->view( $this->toolbar, - $this->buildEditQuestionsBaseUri() + $this->buildEditQuestionsBaseUri(), + $this->object->getId(), + $this->object->getRefId() )->render($this->ui_renderer) ); } diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 4058c8d8616f..d48699cd70e3 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -25,10 +25,11 @@ use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Layout\Factory as DefinitionsFactory; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\DI\Container as ILIASContainer; +use Mustache\Engine as MustacheEngine; use Pimple\Container as PimpleContainer; class LocalDIC extends PimpleContainer @@ -49,6 +50,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic = new self(); $dic[DataFactory::class] = static fn($c): DataFactory => new DataFactory(); $dic[UuidFactory::class] = static fn($c): UuidFactory => new UuidFactory(); + $dic[MustacheEngine::class] = static fn($c): MustacheEngine + => new MustacheEngine(['escape' => static fn($v) => $v]); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( @@ -67,6 +70,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[DefinitionsFactory::class] = static fn($c): DefinitionsFactory => new DefinitionsFactory( $DIC['ui.factory'], + $DIC['http'], $DIC['lng'] ); $dic[Edit::class] = static fn($c): Edit => new Edit( @@ -76,10 +80,12 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ui.factory'], $DIC['ui.renderer'], $DIC['global_screen'], + $DIC['tpl'], + $DIC->contentStyle(), $DIC['ilCtrl'], $DIC['http'], + $DIC['ilTabs'], $DIC->uiService(), - $c[DataFactory::class], $c[UuidFactory::class], $c[AnswerFormFactory::class], $c[QuestionsRepository::class], @@ -89,7 +95,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory => new Cloze\Properties\ClozeText\Factory( $DIC['refinery'], - (new \ilMustacheFactory())->getBasicEngine(), + $c[MustacheEngine::class], $c[DataFactory::class]->text() ); $dic[Cloze\Properties\Gaps\AnswerOptions\Factory::class] = static fn($c): Cloze\Properties\Gaps\AnswerOptions\Factory @@ -120,12 +126,13 @@ protected static function buildDIC(ILIASContainer $DIC): self new Cloze\Properties\Gaps\LongMenu( $DIC['refinery'], $DIC['lng'], - $DIC['ui.factory'] + $DIC['ui.factory'], + $DIC['tpl'] ) ] ); - $dic[Cloze\Properties\AnswerForm\Factory::class] = static fn($c): Cloze\Properties\AnswerForm\Factory - => new Cloze\Properties\AnswerForm\Factory( + $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory + => new Cloze\Properties\Factory( $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] ); @@ -139,14 +146,17 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ui.factory'], $DIC['refinery'], $DIC['http'], - $c[Cloze\Properties\AnswerForm\Factory::class], + $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] ); $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant - => new Cloze\Views\Participant(); + => new Cloze\Views\Participant( + $DIC['tpl'], + $c[MustacheEngine::class] + ); $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( - $c[Cloze\Properties\AnswerForm\Factory::class], + $c[Cloze\Properties\Factory::class], $c[Cloze\Persistence::class], [ Cloze\Capabilities\Marking::class => new Cloze\Capabilities\Marking(), diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index b600e179ed02..9e23acb982f2 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -18,36 +18,26 @@ declare(strict_types=1); -use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\QuestionImplementation; class QstsQuestionPage extends ilPageObject { - private readonly Edit $edit_view; private readonly QuestionImplementation $question; + #[\Override] public function getParentType(): string { return 'qsts'; } - public function getEditView(): Edit - { - return $this->edit_view; - } - - public function setEditView(Edit $edit_view): void - { - $this->edit_view = $edit_view; - } - public function getQuestion(): QuestionImplementation { return $this->question; } - public function setQuestion(QuestionImplementation $question): void - { + public function setQuestion( + QuestionImplementation $question + ): void { $this->question = $question; } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index ee196302261c..dd338ed92682 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -18,7 +18,6 @@ declare(strict_types=1); -use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\URI; @@ -29,19 +28,29 @@ */ class QstsQuestionPageGUI extends ilPageObjectGUI { + private URI $return_uri; + public function __construct( - private readonly URI $return_uri, - Edit $edit_view, - QuestionImplementation $question + QuestionImplementation $question, + int $obj_id ) { parent::__construct('qsts', $question->getPageId()); + $this->obj->setParentId($obj_id); $this->obj->setQuestion($question); - $this->obj->setEditView($edit_view); $this->setEnabledPageFocus(false); } + #[\Override] public function finishEditing(): void { $this->ctrl->redirectToURL($this->return_uri->__toString()); } + + public function withReturnUri( + URI $return_uri + ): self { + $clone = clone $this; + $clone->return_uri = $return_uri; + return $clone; + } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index b5be94a4d7f7..0a747ad04580 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -18,7 +18,13 @@ declare(strict_types=1); +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\Legacy\LocalDIC; +use ILIAS\Questions\Persistence\Repository; use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; class ilPCAnswerForm extends ilPageContent { @@ -31,11 +37,13 @@ public function init(): void $this->setType('answf'); } + #[\Override] public static function getLangVars(): array { return ['ed_insert_pcqst', 'empty_question', 'pc_qst']; } + #[\Override] public function modifyPageContentPostXsl( string $output, string $mode, @@ -45,38 +53,25 @@ public function modifyPageContentPostXsl( return $output; } - /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + global $DIC; + $ui_factory = $DIC['ui.factory']; + $ui_renderer = $DIC['ui.renderer']; + $lng = $DIC['lng']; $question = $this->pg_obj->getQuestion(); return mb_ereg_replace_callback( self::ANSWER_FORM_PLACEHOLDER, - fn(array $matches): string => $question - ->getAnswerFormByIdString($matches[1])?->getTypeGenericProperties() - ->getAdditionalText() ?? '', + fn(array $matches): string => $this->renderAnswerForm( + $ui_factory, + $ui_renderer, + $lng, + $question->getAnswerFormPropertiesByIdString($matches[1]) + ), $output ); } - public function getCssFiles(string $a_mode): array - { - if ($this->getPage()->getPageConfig()->getEnableSelfAssessment()) { - return array("./components/ILIAS/TestQuestionPool/resources/js/dist/question_handling.css", - "components/ILIAS/TestQuestionPool/templates/default/test_javascript.css"); - } - return array(); - } - - public function create( - Uuid $answer_form_id - ): void { - $this->createInitialChildNode( - $this->hier_id, - '', - self::ANSWER_FORM_ELEMENT_TAG, - [self::ANSWER_FORM_ID_ATTRIBUTE => $answer_form_id->toString()] - ); - } - + #[\Override] public static function afterPageUpdate( ilPageObject $page, DOMDocument $domdoc, @@ -89,6 +84,7 @@ public static function afterPageUpdate( global $DIC; $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + $question_repository = LocalDIC::dic()[Repository::class]; /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ $question = $page->getQuestion(); @@ -97,8 +93,13 @@ public static function afterPageUpdate( foreach ($dom_util->path($domdoc, '//AnswerForm') as $node) { $answer_forms[] = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); } + + $question_repository->update( + [$question->withoutDeletedAnswerForms($answer_forms)] + ); } + #[\Override] public static function handleCopiedContent( DOMDocument $a_domdoc, bool $a_self_ass = true, @@ -154,4 +155,42 @@ public static function handleCopiedContent( } } } + + public function create( + Uuid $answer_form_id + ): void { + $this->createInitialChildNode( + $this->hier_id, + '', + self::ANSWER_FORM_ELEMENT_TAG, + [self::ANSWER_FORM_ID_ATTRIBUTE => $answer_form_id->toString()] + ); + } + + public function getAnswerFormIdStringFromAttribute(): string + { + return $this->getChildNode()->attributes + ->getNamedItem(self::ANSWER_FORM_ID_ATTRIBUTE)->nodeValue; + } + + private function renderAnswerForm( + UIFactory $ui_factory, + UIRenderer $ui_renderer, + Language $lng, + ?AnswerFormProperties $answer_form_properties, + ): string { + if ($answer_form_properties === null) { + return $lng->txt('broken_answer_form'); + } + + return $ui_renderer->render( + $ui_factory->legacy()->latexContent( + $answer_form_properties->getDefinition()->getParticipantView() + ->get( + $answer_form_properties, + null + ) + ) + ); + } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index d46608aab8b1..ebf306f5b42d 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Data\Factory as DataFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -43,8 +44,10 @@ public function __construct( $this->ui_renderer = $DIC['ui.renderer']; $this->data_factory = new DataFactory(); + $local_dic = LocalDIC::dic(); + $this->edit_view = $local_dic[Edit::class]; + parent::__construct($pg_obj, $content_obj, $hier_id, $pc_id); - $this->edit_view = $this->pg_obj->getEditView(); } public function executeCommand() @@ -63,6 +66,7 @@ public function insertCmd(): void $this->data_factory->uri( ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') ), + $this->pg_obj->getParentId(), $this->pg_obj->getQuestion(), $content_obj )->render($this->ui_renderer) @@ -71,16 +75,21 @@ public function insertCmd(): void public function editCmd(): void { - $this->setInsertTabs(); - $content_obj = new ilPCAnswerForm($this->pg_obj); - $content_obj->setHierId($this->hier_id); + /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + $question = $this->pg_obj->getQuestion(); + $answer_form_properties = $question->getAnswerFormPropertiesByIdString( + $this->getContentObject()->getAnswerFormIdStringFromAttribute() + ); + $this->tpl->setContent( $this->edit_view->editAnswerForm( $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'edit') ), - $this->pg_obj->getQuestion(), - $content_obj + $this->pg_obj->getParentId(), + $question, + $answer_form_properties, + $answer_form_properties->getDefinition() )->render($this->ui_renderer) ); } @@ -92,8 +101,4 @@ private function setInsertTabs(): void $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') ); } - - public function setEditTabs(): void - { - } } diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 32633b7e0ae1..050ce8cff30e 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -21,8 +21,11 @@ namespace ILIAS; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; +use ILIAS\Questions\AnswerForm\Migration as AnswerFormMigration; +use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationCloze; +use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationLongMenu; +use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationNumeric; use ILIAS\Questions\Setup\Agent; -use ILIAS\Refinery\Factory as Refinery; use ILIAS\Setup\Agent as AgentInterface; class Questions implements Component\Component @@ -40,7 +43,12 @@ public function init( $define[] = AnswerFormDefinition::class; $contribute[AgentInterface::class] = static fn() => new Agent( - $pull[Refinery::class] + $seek[AnswerFormMigration::class] ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze(); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu(); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric(); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); } } diff --git a/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js b/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js new file mode 100644 index 000000000000..47938f3bcc47 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js @@ -0,0 +1,115 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +(function () { + const longmenu = () => { + const init = (input, autocompleteLength, answerOptions) => { + if (input.nodeName === 'INPUT') { + let longest = answerOptions.reduce((a, b) => { + return a.length > b.length ? a : b; + }); + input.setAttribute('size', longest.length); + input.addEventListener( + 'keyup', + (e) => { keyHandler(autocompleteLength, answerOptions, e); } + ); + }; + }; + + const keyHandler = (autocompleteLength, answerOptions, e) => { + if (e.key === 'Enter' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + onSelectHandler(e); + return; + } + + if (e.key === 'ArrowDown') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.firstElementChild.focus(); + } + + if (e.target.nodeName === 'LI' && e.target.nextElementSibling !== null) { + e.target.nextElementSibling.focus(); + } + return; + } + + if (e.key === 'ArrowUp' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.previousElementSibling === null) { + e.target.parentElement.previousElementSibling.focus(); + } else { + e.target.previousElementSibling.focus(); + } + return; + } + + onChangeHandler(autocompleteLength, answerOptions, e); + }; + + const onChangeHandler = (autocompleteLength, answerOptions, e) => { + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.remove(); + } + + if (e.key === 'Tab' || e.target.value.length < autocompleteLength) { + return; + } + + const matchingAnswers = answerOptions.filter((answer) => { + return answer.toLowerCase().includes(e.target.value.toLowerCase()) + }); + + if (matchingAnswers.length === 0) { + return; + } + + let list = document.createElement('ul'); + matchingAnswers.forEach((answer) => { + let listElement = document.createElement('li'); + listElement.tabIndex = 0; + listElement.textContent = answer; + list.appendChild(listElement); + }); + list.addEventListener('click', onSelectHandler); + list.addEventListener('keyup', onSelectHandler); + e.target.parentNode.appendChild(list); + }; + + const onSelectHandler = (e) => { + if (e.type === 'keydown' && e.key !== 'Enter') { + return; + } + e.target.parentNode.previousElementSibling.value = e.target.textContent; + e.target.parentNode.previousElementSibling.focus(); + e.target.parentNode.remove(); + }; + + const public_interface = { + init + }; + return public_interface; + }; + + il = il || {}; + il.test = il.test || {}; + il.test.player = il.test.player || {}; + il.test.player.longmenu = longmenu(); +}()); diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php index b2268af3f4ae..8da8607347e7 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php @@ -24,6 +24,12 @@ interface Feedback extends Capability { - public function getGeneralFeedback(Response $response): array; - public function getSpecificFeedback(Response $response, string $answer_id): array; + public function getGeneralFeedback( + Response $response + ): array; + + public function getSpecificFeedback( + Response $response, + string $answer_id + ): array; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php index c01031fce58a..73f28e03c4d0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php @@ -24,6 +24,9 @@ interface Marking extends Capability { - public function addAchievedPointsToResponse(Response $response): Response; + public function addAchievedPointsToResponse( + Response $response + ): Response; + public function getBestResponse(): Response; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php index 9af7f1e5a4e9..4273cd894f10 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php @@ -24,5 +24,7 @@ interface Skills extends Capability { - public function getSkillPointsForResponse(Response $response): float; + public function getSkillPointsForResponse( + Response $response + ): float; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index e623a00b8b98..865ab114013f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -25,19 +25,27 @@ use ILIAS\Questions\AnswerForm\Views\Participant; use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Language\Language; interface Definition { public function getLabel(Language $lng): string; + public function buildProperties( TypeGenericProperties $type_generic_properties, ?Query $query ): Properties; public function getPersistence(): Persistence; - public function hasCapability(string $capability_class_name): bool; - public function getCapability(string $capability_class_name): ?Capability; + + public function hasCapability( + string $capability_class_name + ): bool; + + public function getCapability( + string $capability_class_name + ): ?Capability; + public function getEditView(): Edit; + public function getParticipantView(): Participant; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index 83bfa1b2ed1f..f47770b066ff 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -110,7 +110,7 @@ public function buildTypeGenericPropertiesFromDatabase( return new TypeGenericProperties( $this->uuid_factory->fromString($db_values['id']), $this->uuid_factory->fromString($db_values['question_id']), - $db_values['type'], + $this->getDefinitionForClass($db_values['type']), $db_values['available_points'], $db_values['image_size'], $db_values['shuffle_answer_options'] === 1, diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration.php new file mode 100644 index 000000000000..ae72b1be9ef7 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration.php @@ -0,0 +1,36 @@ +question_id; } + public function getDefinition(): Definition + { + return $this->definition; + } + public function getAvailablePoints(): ?float { return $this->available_points; @@ -83,7 +88,7 @@ public function getAdditionalTextLegacy(): string public function toStorage( Manipulate $manipulate ): Manipulate { - if ($this->definition_class === null) { + if ($this->definition === null) { throw new \UnexpectedValueException( 'You cannot save a Answer Form without a Type!' ); @@ -99,13 +104,17 @@ public function toDelete( Manipulate $manipulate ): Manipulate { $answer_form_table_definition = CoreTables::AnswerForms; + return $manipulate->withAdditionalStatement( new Delete( $answer_form_table_definition->getTable(), [ new Where( $answer_form_table_definition->getIdColumn(), - $this->answer_form_id->toString() + new Value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) ) ] ) @@ -118,7 +127,7 @@ private function buildInsertStatement(): Insert CoreTables::AnswerForms->getColumns(), [ new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->definition_class), + new Value(\ilDBConstants::T_TEXT, $this->definition::class), new Value(\ilDBConstants::T_TEXT, $this->question_id->toString()), new Value(\ilDBConstants::T_FLOAT, $this->available_points), new Value(\ilDBConstants::T_INTEGER, $this->image_size), @@ -135,22 +144,27 @@ private function buildUpdateStatement(): Update $answer_form_table_definition = CoreTables::AnswerForms; return new Update( $answer_form_table_definition->getColumns([ - $answer_form_table_definition->getIdColumn(), + 'id', 'type', 'question_id', 'additional_text_legacy' ]), [ new Value(\ilDBConstants::T_FLOAT, $this->available_points), - new Value(\ilDBConstants::T_INT, $this->image_size), - new Value(\ilDBConstants::T_INT, $this->shuffle_answer_options ? 1 : 0), + new Value(\ilDBConstants::T_INTEGER, $this->image_size), + new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0), new Value(\ilDBConstants::T_TEXT, $this->additional_text) ], - new Where( - $answer_form_table_definition->getIdColumn(), - $this->answer_form_id->toString() - ) + [ + new Where( + $answer_form_table_definition->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index 4d76d2c80a98..76e6705616a7 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -21,9 +21,9 @@ namespace ILIAS\Questions\AnswerForm\Views; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; -use ILIAS\Questions\Presentation\Layout\Definitions\EditOverview; -use ILIAS\Questions\Presentation\Layout\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\EditOverview; interface Edit { diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php index 9bd7e2c1d7bc..4f9811b388b2 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php @@ -20,12 +20,15 @@ namespace ILIAS\Questions\AnswerForm\Views; +use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Response\Response; interface Participant { public function isAsyncPresentationAvailable(): bool; + public function get( + Properties $properties, ?Response $response - ): array; + ): string; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php index f7e6c9176f32..2bedec07703c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php @@ -25,18 +25,24 @@ class Feedback implements FeedbackInterface { + #[\Override] public function isConfigured(): bool { return false; } - public function getGeneralFeedback(Response $response): array - { + #[\Override] + public function getGeneralFeedback( + Response $response + ): array { } - public function getSpecificFeedback(Response $response, string $answer_id): array - { + #[\Override] + public function getSpecificFeedback( + Response $response, + string $answer_id + ): array { } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php index 97a3a1ee3812..947979f5abfc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php @@ -22,20 +22,24 @@ use ILIAS\Questions\AnswerForm\Capabilities\Marking as MarkingInterface; use ILIAS\Questions\Response\Response; -use ILIAS\Questions\AnswerForm; class Marking implements MarkingInterface { + #[\Override] public function isConfigured(): bool { return false; } - public function addAchievedPointsToResponse(Response $response): Response - { + + #[\Override] + public function addAchievedPointsToResponse( + Response $response + ): Response { } + #[\Override] public function getBestResponse(): Response { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php index 0e3ff7fead41..eb7380fb3d32 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php @@ -25,13 +25,16 @@ class Skills implements SkillsInterface { + #[\Override] public function isConfigured(): bool { return false; } - public function getSkillPointsForResponse(Response $response): float - { + #[\Override] + public function getSkillPointsForResponse( + Response $response + ): float { } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index 8242d8f66026..18967e1e3c3a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -24,8 +24,8 @@ use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerForm\TypeGenericProperties; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Factory as PropertiesFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; use ILIAS\Questions\Persistence\Query; @@ -45,11 +45,14 @@ public function __construct( ) { } - public function getLabel(Language $lng): string - { + #[\Override] + public function getLabel( + Language $lng + ): string { return $lng->txt('assClozeTest'); } + #[\Override] public function buildProperties( TypeGenericProperties $type_generic_data, ?Query $query @@ -60,26 +63,33 @@ public function buildProperties( ); } + #[\Override] public function getPersistence(): Persistence { return $this->persistence; } - public function hasCapability(string $capability_class_name): bool - { + #[\Override] + public function hasCapability( + string $capability_class_name + ): bool { return array_key_exists($capability_class_name, $this->available_capabilities); } - public function getCapability(string $capability_class_name): ?Capability - { + #[\Override] + public function getCapability( + string $capability_class_name + ): ?Capability { return $this->available_capabilities[$capability_class_name]; } + #[\Override] public function getEditView(): Edit { return $this->edit_view; } + #[\Override] public function getParticipantView(): Participant { return $this->participant_view; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php new file mode 100644 index 000000000000..01d99c6ebbc6 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -0,0 +1,109 @@ +table_factory->data( + $this, + $this->lng->txt('gaps'), + $this->getColums() + )->withActions($this->getActions()) + ->withRequest($this->request); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + yield from $this->environment->getAnswerFormProperties()->getGaps() + ->toTableRows($row_builder, $this->lng); + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return 0; + } + + private function getColums(): array + { + $f = $this->table_factory->column(); + + return [ + 'gap' => $f->text($this->lng->txt('title')), + 'type' => $f->text($this->lng->txt('question_type')), + 'answers_options_awarding_points' => $f->text($this->lng->txt('answer_options_awarding_points')), + 'available_points' => $f->number($this->lng->txt('available_points')) + ]; + } + + private function getActions(): array + { + return [ + 'edit_gaps' => $this->table_factory->action()->standard( + $this->lng->txt('edit_gaps'), + $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_GAP_TYPES), + $this->environment->getTableRowIdToken() + ), + 'edit_answer_options' => $this->table_factory->action()->standard( + $this->lng->txt('edit_answer_options'), + $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS), + $this->environment->getTableRowIdToken() + ), + 'edit_points' => $this->table_factory->action()->standard( + $this->lng->txt('edit_available_points'), + $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_POINTS), + $this->environment->getTableRowIdToken() + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php new file mode 100644 index 000000000000..10d11263beac --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php @@ -0,0 +1,40 @@ +table_namespace; } + #[\Override] public function getColumns( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -101,13 +104,16 @@ public function getColumns( }; return array_map( fn(string $v): Column => new Column($table, $v), - array_filter( - $column_identifiers, - fn(string $v) => !in_array($v, $columns_to_skip) + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) ) ); } + #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -125,6 +131,7 @@ public function getIdColumn( }; } + #[\Override] public function getForeignKeyColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -150,6 +157,7 @@ public function getForeignKeyColumn( }; } + #[\Override] public function completeQuery( Query $query, Column $answer_form_id_column @@ -202,6 +210,13 @@ public function completeQuery( ), JoinType::Left ) + )->withAdditionalOrder( + new Order( + $this->getIdColumn( + $table_name_builder, + $answer_input_table_definition + ) + ) )->withAdditionalSelect( new Select( $this->getColumns( @@ -209,6 +224,13 @@ public function completeQuery( $answer_options_table_definition ) ) + )->withAdditionalOrder( + new Order( + new Column( + $answer_options_table_definition->getTable($table_name_builder), + 'position' + ) + ) )->withAdditionalJoin( new Join( $answer_form_id_column, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php index 1d722fbdfc64..55e2ac30a4c6 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php @@ -33,8 +33,9 @@ public function __construct( ) { } - public function buildFromTextString(string $text): Text - { + public function buildFromTextString( + string $text + ): Text { return new Text( $this->refinery, $this->mustache_engine, @@ -43,13 +44,15 @@ public function buildFromTextString(string $text): Text ); } - public function buildFromHiddenInputString(string $text): Text - { + public function buildFromHiddenInputString( + string $text + ): Text { return $this->buildFromTextString($this->unmaskTextFromOutputInHiddenInput($text)); } - private function unmaskTextFromOutputInHiddenInput(string $text): string - { + private function unmaskTextFromOutputInHiddenInput( + string $text + ): string { return str_replace(['\{\{', '\}\}'], ['{{', '}}'], $text); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 49ca76b35e41..59120c3fa759 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -65,8 +65,9 @@ public function getInput( ->withValue($this->cloze_text->getRawRepresentation()); } - public function getCarryInputs(FieldFactory $ff): HiddenInput - { + public function getCarryInputs( + FieldFactory $ff + ): HiddenInput { return $ff->hidden()->withValue($this->getTextForOutputInHiddenInput()); } @@ -75,12 +76,21 @@ public function getRawRepresentationForPersistence(): string return $this->cloze_text->getRawRepresentation(); } + public function getRenderedMarkdownForParticipantPresentation(): string + { + return $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ); + } + public function getRenderedMarkdownForEditingPresentation( Gaps $gaps ): string { return $this->mustache_engine->render( - $this->cloze_text->getRawRepresentation(), - $gaps->getPlaceholderArrayForPreview() + $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ), + $gaps->getPlaceholderArrayForEditFormPanel() ); } @@ -95,7 +105,7 @@ public function updateGapsFromMarkdown( $position = 0; return array_reduce( $this->mustache_engine->getTokenizer()->scan($this->cloze_text->getRawRepresentation()), - function (Gaps $c, array $v) use ($answer_form_id, &$position): Gaps { + function (Gaps $c, array $v) use ($answer_form_id, $pre_existing_gaps, &$position): Gaps { if ($v['type'] !== '_v' || !str_starts_with($v['name'], Gap::GAP_PLACEHOLDER_NAME)) { return $c; @@ -105,14 +115,11 @@ function (Gaps $c, array $v) use ($answer_form_id, &$position): Gaps { return $c->withNewGap($answer_form_id, $position++); } - $gap = $c->getGapByTagName($v['name']); + $gap = $pre_existing_gaps->getGapByTagName($v['name']); if ($gap !== null) { return $c->withGap( - $gap->withProperties( - $gap->getProperties()->withPosition( - $answer_form_id, - $position++ - ) + $gap->withPosition( + $position++ ) ); } @@ -123,14 +130,15 @@ function (Gaps $c, array $v) use ($answer_form_id, &$position): Gaps { $position++ ); }, - $pre_existing_gaps + $pre_existing_gaps->withResetGaps() ); } - public function withIdsOfNewGapsInClozeText(array $new_gaps): self - { + public function withIdsOfNewGapsInClozeText( + array $new_gaps + ): self { if ($new_gaps === []) { - return self; + return $this; } $clone = clone $this; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php index a67115f323e3..5a1903432530 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php @@ -53,8 +53,9 @@ public static function buildInput( ); } - private static function buildOptionsList(Language $lng): array - { + private static function buildOptionsList( + Language $lng + ): array { return array_reduce( self::cases(), function (array $c, self $v) use ($lng): array { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php similarity index 86% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 1e0904d5872a..103012a35f75 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -18,10 +18,9 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; use ILIAS\Questions\AnswerForm\TypeGenericProperties; -use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; @@ -45,11 +44,14 @@ public function fromData( return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), $this->cloze_text_factory->buildFromTextString( $type_generic_properties->getAdditionalText() ), $type_generic_properties->getAdditionalTextLegacy(), - $this->gaps_factory->getEmptyGapsObject() + $this->gaps_factory->getEmptyGapsObject( + $type_generic_properties->getAnswerFormId() + ) ); } @@ -58,7 +60,7 @@ public function fromData( 'combinations_activated' => $combinations_activated ] = $query->retrieveCurrentRecord( TableTypes::TypeSpecificAnswerForms->getTable( - $query->getTableNameBuilder(Definition::class) + $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) ), $query->getRefinery()->custom()->transformation( fn(array $vs): array => [ @@ -71,11 +73,15 @@ public function fromData( return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), $this->cloze_text_factory->buildFromTextString( $type_generic_properties->getAdditionalText() ), $type_generic_properties->getAdditionalTextLegacy(), - $this->gaps_factory->fromDatabase($query), + $this->gaps_factory->fromDatabase( + $type_generic_properties->getAnswerFormId(), + $query + ), $scoring_identical_responses, $combinations_activated ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index 0c8d1132bf44..4452b2a7d823 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -21,10 +21,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; use ILIAS\Questions\Persistence\Replace; -use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Data\UUID\Uuid; class AnswerOption @@ -57,8 +54,9 @@ public function getPosition(): ?int return $this->position; } - public function withPosition(int $position): self - { + public function withPosition( + int $position + ): self { $clone = clone $this; $clone->position = $position; return $clone; @@ -69,8 +67,9 @@ public function getTextValue(): string return $this->text_value; } - public function withTextValue(string $text_value): self - { + public function withTextValue( + string $text_value + ): self { $clone = clone $this; $clone->text_value = $text_value; return $clone; @@ -81,8 +80,9 @@ public function getLowerLimit(): ?float return $this->lower_limit; } - public function withLowerLimit(float $lower_limit): self - { + public function withLowerLimit( + float $lower_limit + ): self { $clone = clone $this; $clone->lower_limit = $lower_limit; return $clone; @@ -93,8 +93,9 @@ public function getUpperLimit(): ?float return $this->upper_limit; } - public function withUpperLimit(?float $upper_limit): self - { + public function withUpperLimit( + ?float $upper_limit + ): self { $clone = clone $this; $clone->upper_limit = $upper_limit; return $clone; @@ -105,8 +106,9 @@ public function getAvailablePoints(): ?float return $this->available_points; } - public function withAvailablePoints(?float $available_points): self - { + public function withAvailablePoints( + ?float $available_points + ): self { $clone = clone $this; $clone->available_points = $available_points; return $clone; @@ -120,7 +122,7 @@ public function buildArrayForHiddenInput(): array self::FORM_KEY_TEXT_VALUE => $this->getTextValue() ]; - if ($this->getUpperLimit() !== null) { + if ($this->getLowerLimit() !== null) { $values[self::FORM_KEY_LOWER_LIMIT] = (string) $this->getLowerLimit(); } @@ -128,7 +130,7 @@ public function buildArrayForHiddenInput(): array $values[self::FORM_KEY_UPPER_LIMIT] = (string) $this->getUpperLimit(); } - if ($this->getUpperLimit() !== null) { + if ($this->getAvailablePoints() !== null) { $values[self::FORM_KEY_AVAILABLE_POINTS] = (string) $this->getAvailablePoints(); } @@ -137,14 +139,11 @@ public function buildArrayForHiddenInput(): array public function buildReplace( ?Replace $replace, - Persistence $persistence, - TableNameBuilder $table_name_builder + array $columns ): Replace { - $table_definition = TableTypes::AnswerOptions; - if ($replace === null) { return new Replace( - $persistence->getColumns($table_name_builder, $table_definition), + $columns, $this->buildValuesForGapReplace() ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 4c90762f832a..8309f2c77eea 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -20,11 +20,16 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; +use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; class AnswerOptions @@ -33,24 +38,40 @@ class AnswerOptions public function __construct( private readonly Factory $factory, + private readonly Uuid $answer_input_id, private array $answer_options ) { $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); } + public function isIncomplete(): bool + { + return $this->answer_options === [] + || $this->answer_options_awarding_points === []; + } + public function getAnswerOptionForPositionOrNew( - Uuid $answer_input_id, int $position ): AnswerOption { return $this->answer_options[$position] - ?? $this->factory->getDefaultAnswerOptionForPosition($answer_input_id, $position); + ?? $this->factory->getDefaultAnswerOptionForPosition( + $this->answer_input_id, + $position + ); } public function getTagsArrayFromAnswerOptions(): array { - return array_map( - fn(AnswerOption $v): string => $v->getTextValue(), - $this->answer_options + return array_reduce( + $this->answer_options, + function (array $c, AnswerOption $v): array { + if ($v->getTextValue() === '') { + return $c; + } + $c[] = $v->getTextValue(); + return $c; + }, + [] ); } @@ -86,7 +107,6 @@ public function withAnswerOptions( } public function withValuesFromHiddenInputValue( - Uuid $answer_input_id, ?string $value ): self { if ($value === null @@ -101,7 +121,7 @@ public function withValuesFromHiddenInputValue( $clone->answer_options = array_map( fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( $vs[AnswerOption::FORM_KEY_ID], - $answer_input_id, + $this->answer_input_id, $vs[AnswerOption::FORM_KEY_POSITION], $vs[AnswerOption::FORM_KEY_TEXT_VALUE], $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, @@ -123,15 +143,13 @@ public function withValuesFromHiddenInputValue( } public function withAnswerOptionsFromTags( - Uuid $answer_input_id, array $tags ): self { $clone = clone $this; $position = 0; $clone->answer_options = array_map( - function (string $v) use ($answer_input_id, &$position): AnswerOption { + function (string $v) use (&$position): AnswerOption { return $this->buildAnswerOptionFromTag( - $answer_input_id, $position++, $v ); @@ -160,12 +178,26 @@ function (AnswerOption $v) use ($refinery, $values_from_form): AnswerOption { return $v; }, - $this->answer_options + $clone->answer_options ); - $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($this->answer_options); + $clone->answer_options_awarding_points = $clone + ->buildAnswerOptionsAwardingPointsFromAnswerOptions($clone->answer_options); return $clone; } + public function buildArrayForInput( + Transformation $shuffle_transformation + ): array { + return array_reduce( + $shuffle_transformation->transform($this->answer_options), + function (array $c, AnswerOption $v): array { + $c[$v->getAnswerOptionId()->toString()] = $v->getTextValue(); + return $c; + }, + [] + ); + } + public function buildHiddenInputValue(): string { return json_encode([ @@ -206,13 +238,36 @@ public function buildReplace( $this->answer_options, fn(?Replace $c, AnswerOption $v): Replace => $v->buildReplace( $c, - $persistence, + $persistence->getColumns($table_name_builder, TableTypes::AnswerOptions), $table_name_builder ), $replace ); } + public function buildDelete( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $answer_options_table_definition = TableTypes::AnswerOptions; + + return new Delete( + $answer_options_table_definition->getTable($table_name_builder), + [ + new Where( + $persistence->getForeignKeyColumn( + $table_name_builder, + $answer_options_table_definition + ), + new Value( + \ilDBConstants::T_TEXT, + $this->answer_input_id->toString() + ) + ) + ] + ); + } + private function buildAnswerOptionsAwardingPointsFromAnswerOptions( array $answer_options ): array { @@ -223,12 +278,15 @@ private function buildAnswerOptionsAwardingPointsFromAnswerOptions( } private function buildAnswerOptionFromTag( - Uuid $answer_input_id, int $position, string $text_value ): AnswerOption { $answer_option = $this->retrieveAnswerOptionByTextValue($text_value) - ?? $this->factory->getDefaultAnswerOptionForPosition($answer_input_id, $position); + ?? $this->factory->getDefaultAnswerOptionForPosition( + $this->answer_input_id, + $position + ); + return $answer_option ->withPosition($position) ->withTextValue($text_value); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php index 9d31efe2526f..bf71bfed14b5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -35,10 +35,12 @@ public function __construct( ) { } - public function getDefaultAnswerOptions(): AnswerOptions - { + public function getDefaultAnswerOptions( + Uuid $answer_input_id + ): AnswerOptions { return new AnswerOptions( $this, + $answer_input_id, [] ); } @@ -89,6 +91,7 @@ function (array $vs): array { && $v['answer_input_id'] !== $previous_answer_input_id) { $return_array[$previous_answer_input_id] = new AnswerOptions( $this, + $this->uuid_factory->fromString($previous_answer_input_id), $answer_options ); $answer_options = []; @@ -107,6 +110,7 @@ function (array $vs): array { $return_array[$v['answer_input_id']] = new AnswerOptions( $this, + $this->uuid_factory->fromString($v['answer_input_id']), $answer_options ); @@ -116,8 +120,9 @@ function (array $vs): array { ); } - private function convertToFloatOrNull(?string $value): ?float - { + private function convertToFloatOrNull( + ?string $value + ): ?float { return $this->refinery->byTrying([ $this->refinery->kindlyTo()->float(), $this->refinery->always(null) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 8a416f060ec9..cce432c55096 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -70,20 +70,23 @@ public function getNewGap( $answer_input_id, $answer_form_id, $position, - $this->answer_options_factory->getDefaultAnswerOptions() + $this->answer_options_factory->getDefaultAnswerOptions($answer_input_id) ); } - public function getEmptyGapsObject(): Gaps - { + public function getEmptyGapsObject( + Uuid $answer_form_id, + ): Gaps { return new Gaps( $this, + $answer_form_id, [] ); } - public function getGapTypeByIdentifier(string $identifier): Type - { + public function getGapTypeByIdentifier( + string $identifier + ): Type { if (!array_key_exists($identifier, $this->available_gap_types)) { throw new \InvalidArgumentException('Gap type does not exist.'); } @@ -91,6 +94,7 @@ public function getGapTypeByIdentifier(string $identifier): Type } public function fromDatabase( + Uuid $answer_form_id, Query $query ): Gaps { $answer_options = $this->answer_options_factory->fromDatabase($query); @@ -98,35 +102,44 @@ public function fromDatabase( return $query->retrieveCurrentRecord( TableTypes::AnswerInputs->getTable($query->getTableNameBuilder(Definition::class)), $query->getRefinery()->custom()->transformation( - function (array $vs) use ($answer_options): Gaps { + function (array $vs) use ($answer_form_id, $answer_options): Gaps { $previous_answer_input_id = null; $gaps = []; foreach ($vs as $v) { - if ($previous_answer_input_id === $v['id']) { + if ($v['answer_form_id'] !== $answer_form_id->toString() + || $previous_answer_input_id === $v['id']) { continue; } $previous_answer_input_id = $v['id']; - $gaps[] = new Gap( - $this->uuid_factory->fromString($v['id']), - $this->uuid_factory->fromString($v['answer_form_id']), - $v['position'], - $answer_options[$v['id']], - $this->getGapTypeByIdentifier($v['gap_type']), - $v['max_chars'], - $v['step_size'], - $v['text_matching_method'] === null - ? null - : TextMatchingOptions::tryFrom($v['text_matching_method']), - $v['min_autocomplete'], - $v['shuffle_answer_options'] === 1 - ); + $gaps[] = $this->buildGapFromDBValues($v, $answer_options); } return new Gaps( $this, + $answer_form_id, $gaps ); } ) ); } + + private function buildGapFromDBValues( + array $values, + array $answer_options + ): Gap { + return new Gap( + $this->uuid_factory->fromString($values['id']), + $this->uuid_factory->fromString($values['answer_form_id']), + $values['position'], + $answer_options[$values['id']], + $this->getGapTypeByIdentifier($values['gap_type']), + $values['max_chars'], + $values['step_size'], + $values['text_matching_method'] === null + ? null + : TextMatchingOptions::tryFrom($values['text_matching_method']), + $values['min_autocomplete'], + $values['shuffle_answer_options'] === 1 + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index c65b249a8298..5a2af5208b82 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -26,7 +26,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; +use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; @@ -34,6 +34,8 @@ use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Table\DataRow; +use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\Refinery\Transformation; class Gap @@ -283,24 +285,28 @@ public function buildGapPlaceholderNameWithId(): string return self::GAP_PLACEHOLDER_NAME . '_' . $this->getAnswerInputId()->toString(); } + public function buildParticipantViewLegacyInput(): string + { + return $this->type->getParticipantViewLegacyInput($this); + } + public function getEditAnswerOptionsSection( Language $lng, FieldFactory $ff ): Section { - $type = $this->getType(); $section = $ff->section( - $type->getEditAnswerOptionsInputs($this), - "{$this->buildShortenedGapName()} ({$lng->txt("{$type->getIdentifier()}_gap")})" + $this->getType()->getEditAnswerOptionsInputs($this), + "{$this->buildShortenedGapName()} ({$lng->txt("{$this->getType()->getIdentifier()}_gap")})" ); - $edit_section_constraint = $type->getEditAnswerOptionsSectionConstraint(); + $edit_section_constraint = $this->getType()->getEditAnswerOptionsSectionConstraint(); if ($edit_section_constraint !== null) { $section = $section->withAdditionalTransformation($edit_section_constraint); } return $section->withAdditionalTransformation( - $type->getBuildGapTransformation($this) + $this->getType()->getBuildGapTransformation($this) ); } @@ -325,6 +331,34 @@ public function getEditPointsSection( ); } + public function toTableRow( + DataRowBuilder $row_builder, + Language $lng + ): DataRow { + $total_points = 0; + $answer_options_list = ''; + foreach ($this->answer_options->getAnswerOptionsAwardingPoints() as $option) { + $total_points += $option->getAvailablePoints(); + + $gap_text = $option->getTextValue(); + if ($gap_text === '') { + $gap_text = $option->getLowerlimit(); + } + + $answer_options_list .= "{$gap_text} ({$option->getAvailablePoints()})
"; + } + + return $row_builder->buildDataRow( + $this->answer_input_id->toString(), + [ + 'gap' => $this->buildShortenedGapName(), + 'type' => $lng->txt("{$this->type->getIdentifier()}_gap"), + 'answers_options_awarding_points' => $answer_options_list, + 'available_points' => $total_points + ] + ); + } + private function buildValuesForGapReplace(): array { return [ @@ -427,10 +461,8 @@ private function retrieveAnswerOptionsFromCarry( return $carry->retrieve( self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( - fn(?string $v): AnswerOptions => $this->answer_options->withValuesFromHiddenInputValue( - $this->answer_input_id, - $v - ) + fn(?string $v): AnswerOptions => $this->answer_options + ->withValuesFromHiddenInputValue($v) ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 0df4837ec8bc..772fa189e2b7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -29,7 +29,7 @@ use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; -use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; +use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -37,13 +37,29 @@ use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Table\DataRowBuilder; +use ILIAS\UICore\GlobalTemplate; class Gaps { + /** + * @var array + */ + private array $gaps; + public function __construct( private readonly Factory $factory, - private array $gaps + private Uuid $answer_form_id, + array $gaps ) { + $this->gaps = array_reduce( + $gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v; + return $c; + }, + [] + ); } public function getGapById( @@ -115,12 +131,36 @@ public function getUndefinedGaps(): array ); } - public function getPlaceholderArrayForPreview(): array + public function getRemovedGaps( + self $old_gaps + ): array { + return array_diff_key($old_gaps->gaps, $this->gaps); + } + + public function getAddedGaps( + self $old_gaps + ): array { + return array_diff_key($this->gaps, $old_gaps->gaps); + } + + public function getPlaceholderArrayForParticipantView(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildParticipantViewLegacyInput(); + return $c; + }, + [] + ); + } + + public function getPlaceholderArrayForEditFormPanel(): array { return array_reduce( $this->gaps, function (array $c, Gap $v): array { - $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildShortenedGapRepresentation($v); + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildShortenedGapRepresentation(); return $c; }, [] @@ -131,11 +171,14 @@ public function buildGapsTypeInputs( Language $lng, FieldFactory $ff, Refinery $refinery, - array $available_gap_types + array $available_gap_types, + array $selected_gaps ): Section { return $ff->section( array_reduce( - $this->getUndefinedGaps(), + $selected_gaps !== [] + ? $this->filterGapsBySelected($selected_gaps) + : $this->getUndefinedGaps(), function (array $c, Gap $v) use ($ff, $available_gap_types): array { $c[$v->getAnswerInputId()->toString()] = $ff->select( $v->buildShortenedGapName(), @@ -164,11 +207,14 @@ function (array $c, Gap $v) use ($ff, $available_gap_types): array { public function buildAnswerOptionsInputs( Language $lng, FieldFactory $ff, - Refinery $refinery + Refinery $refinery, + array $selected_gaps ): Section { return $ff->section( array_reduce( - $this->gaps, + $selected_gaps !== [] + ? $this->filterGapsBySelected($selected_gaps) + : $this->getGapsWithIncompleteAnswerOptions(), function (array $c, Gap $v) use ($lng, $ff): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditAnswerOptionsSection( $lng, @@ -193,11 +239,14 @@ function (array $c, Gap $v) use ($lng, $ff): array { public function buildPointInputs( Language $lng, FieldFactory $ff, - Refinery $refinery + Refinery $refinery, + array $selected_gaps ): Section { return $ff->section( array_reduce( - $this->gaps, + $selected_gaps !== [] + ? $this->filterGapsBySelected($selected_gaps) + : $this->getGapsWithIncompleteAnswerOptions(), function (array $c, Gap $v) use ($lng, $ff): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditPointsSection( $lng, @@ -259,33 +308,23 @@ function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { ); } + public function toTableRows( + DataRowBuilder $row_builder, + Language $lng + ): \Generator { + foreach ($this->gaps as $gap) { + yield $gap->toTableRow( + $row_builder, + $lng + ); + } + } + public function toStorage( Manipulate $manipulate, Persistence $persistence, TableNameBuilder $table_name_builder ): Manipulate { - $table_definition = TableTypes::AnswerInputs; - $manipulate->withAdditionalStatement( - new Delete( - $table_definition->getTable($table_name_builder), - [ - new Where( - $persistence->getIdColumn($table_name_builder, $table_definition), - new Value( - \ilDBConstants::T_TEXT, - array_map( - fn(Gap $v): string => $v->getAnswerInputId()->toString(), - $this->gaps - ) - ), - Operator::In, - Junctor::Conjunction, - true - ) - ] - ) - ); - [ 'gaps' => $replace_for_gaps, 'answer_options' => $replace_for_answer_options @@ -309,12 +348,119 @@ public function toStorage( ] ); - return $manipulate->withAdditionalStatement($replace_for_gaps) - ->withAdditionalStatement($replace_for_answer_options); + return $manipulate->withAdditionalStatement( + $this->buildDeleteForRemovedGaps( + $persistence, + $table_name_builder + ) + )->withAdditionalStatement($replace_for_gaps) + ->withAdditionalStatement($replace_for_answer_options); + } + + public function toDelete( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->gaps, + fn(Manipulate $c, Gap $v): Manipulate => $c->withAdditionalStatement( + $v->getAnswerOptions()->buildDelete( + $persistence, + $table_name_builder + ) + ), + $manipulate->withAdditionalStatement( + $this->buildDeleteForDeletionOfAnswerForm( + $persistence, + $table_name_builder + ) + ) + ); + } + + private function buildDeleteForRemovedGaps( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::AnswerInputs; + return new Delete( + $table_definition->getTable($table_name_builder), + [ + new Where( + $persistence->getForeignKeyColumn( + $table_name_builder, + $table_definition + ), + new Value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ), + new Where( + $persistence->getIdColumn( + $table_name_builder, + $table_definition + ), + new Value( + \ilDBConstants::T_TEXT, + array_map( + fn(Gap $v): string => $v->getAnswerInputId()->toString(), + $this->gaps + ) + ), + Operator::In, + Junctor::Conjunction, + true + ) + ] + ); + } + + private function buildDeleteForDeletionOfAnswerForm( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::AnswerInputs; + + return new Delete( + $table_definition->getTable($table_name_builder), + [ + new Where( + $persistence->getForeignKeyColumn( + $table_name_builder, + $table_definition + ), + new Value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + ) + ] + ); } - private function extractIdFromTagName(string $tag_name): string + private function getGapsWithIncompleteAnswerOptions(): array { + return array_filter( + $this->gaps, + fn(Gap $v): bool => $v->getAnswerOptions()->isIncomplete() + ); + } + + private function extractIdFromTagName( + string $tag_name + ): string { return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); } + + private function filterGapsBySelected( + array $selected_gaps + ): array { + return array_filter( + $this->gaps, + fn(string $k): bool => in_array($k, $selected_gaps), + ARRAY_FILTER_USE_KEY + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index 1e5bb45de089..e2d3ee9e2852 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -22,13 +22,13 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Properties; use ILIAS\FileUpload\MimeType; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UICore\GlobalTemplate; class LongMenu extends Type { @@ -38,16 +38,49 @@ class LongMenu extends Type public function __construct( Refinery $refinery, private readonly Language $lng, - private readonly UIFactory $ui_factory + private readonly UIFactory $ui_factory, + private readonly GlobalTemplate $global_tpl ) { parent::__construct($refinery); } + #[\Override] public function getIdentifier(): string { return 'long_menu'; } + #[\Override] + public function getParticipantViewLegacyInput( + Gap $gap + ): string { + $answer_input_id = $gap->getAnswerInputId()->toString(); + $gaptemplate = new \ilTemplate( + 'tpl.il_as_qpl_longmenu_question_text_gap.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $gaptemplate->setVariable( + 'KEY', + $answer_input_id + ); + + $this->global_tpl->addOnLoadCode('il.test.player.longmenu.init(' + . "document.querySelector('input[name=\"answer[{$answer_input_id}]\"]'), " + . "{$gap->getMinAutocomplete()}, " + . json_encode( + array_values( + $gap->getAnswerOptions()->buildArrayForInput( + $this->refinery->random()->dontShuffle() + ) + ) + ) . ')'); + return $gaptemplate->get(); + } + + #[\Override] public function getEditAnswerOptionsInputs( Gap $gap ): array { @@ -72,9 +105,11 @@ public function getEditAnswerOptionsInputs( ) ->withRequired(true) ->withValue( - array_map( - fn(AnswerOption $v): string => $v->getTextValue(), - $gap->getAnswerOptions()->getAnswerOptionsAwardingPoints() + array_values( + array_map( + fn(AnswerOption $v): string => $v->getTextValue(), + $gap->getAnswerOptions()->getAnswerOptionsAwardingPoints() + ) ) ) ]; @@ -108,6 +143,7 @@ public function getEditPointsInputs( ); } + #[\Override] public function getEditPointsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( @@ -123,6 +159,7 @@ function (array $vs): bool { ); } + #[\Override] public function getBuildGapTransformation( Gap $gap ): Transformation { @@ -131,7 +168,6 @@ public function getBuildGapTransformation( ->withMinAutocomplete($vs['min_autocomplete']) ->withAnswerOptions( $gap->getAnswerOptions()->withAnswerOptionsFromTags( - $gap->getAnswerInputId(), array_merge( $vs['answer_options'], $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) @@ -141,6 +177,7 @@ public function getBuildGapTransformation( ); } + #[\Override] public function getAnswerInput(): \ilFormPropertyGUI { ; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index df1cd1bc5dfa..33349d9f33c8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -22,7 +22,6 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Properties; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -42,18 +41,36 @@ public function __construct( parent::__construct($refinery); } + #[\Override] public function getIdentifier(): string { return 'numeric'; } + #[\Override] + public function getParticipantViewLegacyInput( + Gap $gap + ): string { + $gaptemplate = new \ilTemplate( + 'tpl.il_as_qpl_cloze_question_gap_numeric.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $gaptemplate->setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] public function getEditAnswerOptionsInputs( Gap $gap ): array { - $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew( - $gap->getAnswerInputId(), - 0 - ); + $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); $ff = $this->ui_factory->input()->field(); return [ @@ -71,6 +88,7 @@ public function getEditAnswerOptionsInputs( ]; } + #[\Override] public function getEditAnswerOptionsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( @@ -81,6 +99,7 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint ); } + #[\Override] public function getEditPointsInputs( AnswerOptions $answer_options ): array { @@ -107,27 +126,28 @@ function (AnswerOption $v): string { ); } + #[\Override] public function getEditPointsSectionConstraint(): ?Constraint { return null; } + #[\Override] public function getBuildGapTransformation( Gap $gap ): Transformation { return $this->refinery->custom()->transformation( fn(array $vs): Gap => $gap->withAnswerOptions( $gap->getAnswerOptions()->withAnswerOptions([ - $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew( - $gap->getAnswerInputId(), - 0 - )->withLowerLimit($vs['lower_limit']) - ->withUpperLimit($vs['upper_limit']) + $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) + ->withLowerLimit($vs['lower_limit']) + ->withUpperLimit($vs['upper_limit']) ]) )->withStepSize($vs['step_size']) ); } + #[\Override] public function getAnswerInput(): \ilFormPropertyGUI { ; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index e2ccaf3a51da..6a1afbbf7671 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -25,6 +25,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; +use ILIAS\Refinery\Random\Seed\GivenSeed; use ILIAS\Refinery\Transformation; use ILIAS\UI\Factory as UIFactory; @@ -40,11 +41,54 @@ public function __construct( parent::__construct($refinery); } + #[\Override] public function getIdentifier(): string { return 'select'; } + #[\Override] + public function getParticipantViewLegacyInput( + Gap $gap + ): string { + $gaptemplate = new \ilTemplate( + 'tpl.il_as_qpl_cloze_question_gap_select.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $shuffler = $gap->getShuffleAnswerOptions() + ? $this->refinery->random()->shuffleArray(new GivenSeed(4)) + : $this->refinery->random()->dontShuffle(); + + foreach ($gap->getAnswerOptions()->buildArrayForInput($shuffler) as $key => $answer_option) { + $gaptemplate->setCurrentBlock('select_gap_option'); + $gaptemplate->setVariable( + 'SELECT_GAP_VALUE', + $key + ); + $gaptemplate->setVariable( + 'SELECT_GAP_TEXT', + \ilLegacyFormElementsUtil::prepareFormOutput($answer_option) + ); + $gaptemplate->parseCurrentBlock(); + } + + $gaptemplate->setVariable( + 'PLEASE_SELECT', + $this->lng->txt('please_select') + ); + + $gaptemplate->setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] public function getEditAnswerOptionsInputs( Gap $gap ): array { @@ -61,11 +105,13 @@ public function getEditAnswerOptionsInputs( ]; } + #[\Override] public function getEditAnswerOptionsSectionConstraint(): ?Constraint { return null; } + #[\Override] public function getEditPointsInputs( AnswerOptions $answer_options ): array { @@ -75,6 +121,7 @@ public function getEditPointsInputs( ); } + #[\Override] public function getEditPointsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( @@ -90,21 +137,20 @@ function (array $vs): bool { ); } + #[\Override] public function getBuildGapTransformation( Gap $gap ): Transformation { return $this->refinery->custom()->transformation( - fn(array $vs): Gap => $gap->withProperties( - $gap->withAnswerOptions( - $gap->getAnswerOptions()->withAnswerOptionsFromTags( - $gap->getAnswerInputId(), - $vs['answer_options'] - ) + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $vs['answer_options'] ) - ) + )->withShuffleAnswerOptions($vs['shuffle_answer_options']) ); } + #[\Override] public function getAnswerInput(): \ilFormPropertyGUI { ; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index ab2a08cd3c86..5fcf987685ac 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -41,11 +41,38 @@ public function __construct( parent::__construct($refinery); } + #[\Override] public function getIdentifier(): string { return 'text'; } + #[\Override] + public function getParticipantViewLegacyInput( + Gap $gap + ): string { + $gaptemplate = new \ilTemplate( + 'tpl.il_as_qpl_cloze_question_gap_text.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $gap_size = $gap->getMaxChars(); + if ($gap_size > 0) { + $gaptemplate->setCurrentBlock('size_and_maxlength'); + $gaptemplate->setVariable('TEXT_GAP_SIZE', $gap_size); + $gaptemplate->parseCurrentBlock(); + } + $gaptemplate->setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] public function getEditAnswerOptionsInputs( Gap $gap ): array { @@ -67,6 +94,7 @@ public function getEditAnswerOptionsInputs( ]; } + #[\Override] public function getEditAnswerOptionsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( @@ -78,6 +106,7 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint ); } + #[\Override] public function getEditPointsInputs( AnswerOptions $answer_options ): array { @@ -87,6 +116,7 @@ public function getEditPointsInputs( ); } + #[\Override] public function getEditPointsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( @@ -102,6 +132,7 @@ function (array $vs): bool { ); } + #[\Override] public function getBuildGapTransformation( Gap $gap ): Transformation { @@ -112,13 +143,13 @@ public function getBuildGapTransformation( ?? self::DEFAULT_TECT_MATCHING_METHOD )->withAnswerOptions( $gap->getAnswerOptions()->withAnswerOptionsFromTags( - $gap->getAnswerInputId(), $vs['answer_options'] ) ) ); } + #[\Override] public function getAnswerInput(): \ilFormPropertyGUI { ; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index e0c04bf318e8..becbef79ccf9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -33,15 +33,32 @@ public function __construct( } abstract public function getIdentifier(): string; - abstract public function getEditAnswerOptionsInputs(Gap $gap): array; + + abstract public function getParticipantViewLegacyInput( + Gap $gap + ): string; + + abstract public function getEditAnswerOptionsInputs( + Gap $gap + ): array; + abstract public function getEditAnswerOptionsSectionConstraint(): ?Constraint; - abstract public function getEditPointsInputs(AnswerOptions $answer_options): array; + + abstract public function getEditPointsInputs( + AnswerOptions $answer_options + ): array; + abstract public function getEditPointsSectionConstraint(): ?Constraint; - abstract public function getBuildGapTransformation(Gap $gap): Transformation; + + abstract public function getBuildGapTransformation( + Gap $gap + ): Transformation; + abstract public function getAnswerInput(): \ilFormPropertyGUI; - public function getAddPointsTransformation(Gap $gap): Transformation - { + public function getAddPointsTransformation( + Gap $gap + ): Transformation { return $this->refinery->custom()->transformation( fn(array $vs): Gap => $gap->withAnswerOptions( $gap->getAnswerOptions() diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php index 99ab4d033dc2..94c0f296d8a9 100755 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php @@ -34,6 +34,7 @@ */ class UploadAnswerOptionsGUI extends AbstractCtrlAwareUploadHandler { + #[\Override] protected function getUploadResult(): HandlerResult { $this->upload->process(); @@ -61,8 +62,10 @@ protected function getUploadResult(): HandlerResult ); } - protected function getRemoveResult(string $identifier): HandlerResult - { + #[\Override] + protected function getRemoveResult( + string $identifier + ): HandlerResult { return new BasicHandlerResult( $this->getFileIdentifierParameterName(), HandlerResult::STATUS_OK, @@ -71,8 +74,10 @@ protected function getRemoveResult(string $identifier): HandlerResult ); } - public function getInfoResult(string $identifier): ?FileInfoResult - { + #[\Override] + public function getInfoResult( + string $identifier + ): ?FileInfoResult { return new BasicFileInfoResult( $this->getFileIdentifierParameterName(), $identifier, @@ -82,11 +87,13 @@ public function getInfoResult(string $identifier): ?FileInfoResult ); } + #[\Override] /** * @return \ILIAS\FileUpload\Handler\BasicFileInfoResult[] */ - public function getInfoForExistingFiles(array $file_ids): array - { + public function getInfoForExistingFiles( + array $file_ids + ): array { $info_results = []; foreach ($file_ids as $identifier) { $info_results[] = $this->getInfoResult($identifier); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php similarity index 78% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 845e6ba93c9f..7db9cb5a3cdc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -18,17 +18,19 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerForm\Properties as PropertiesInterface; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; +use ILIAS\Questions\AnswerFormTypes\Cloze\Layout\OverviewTable; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; @@ -37,13 +39,17 @@ use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Persistence\Where; -use ILIAS\Questions\Presentation\Layout\Definitions\CarryWrapper; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Table\Factory as TableFactory; +use ILIAS\UI\Component\Table\Data as DataTable; +use Psr\Http\Message\ServerRequestInterface; class Properties implements PropertiesInterface { @@ -59,6 +65,7 @@ class Properties implements PropertiesInterface public function __construct( private readonly Uuid $answer_form_id, private readonly Uuid $question_id, + private readonly Definition $definition, private Text $cloze_text, private readonly string $legacy_cloze_text, private Gaps $gaps, @@ -67,22 +74,31 @@ public function __construct( ) { } - public function getAnswerFormId(): ?Uuid + #[\Override] + public function getAnswerFormId(): Uuid { return $this->answer_form_id; } + #[\Override] public function getQuestionId(): Uuid { return $this->question_id; } + #[\Override] + public function getDefinition(): Definition + { + return $this->definition; + } + + #[\Override] public function getTypeGenericProperties(): TypeGenericProperties { return new TypeGenericProperties( $this->answer_form_id, $this->question_id, - Definition::class, + $this->definition, null, null, null, @@ -109,6 +125,13 @@ public function getLegacyClozeText(): string return $this->legacy_cloze_text; } + public function getClozeTextForPresentation(): string + { + return $this->cloze_text === null + ? $this->legacy_cloze_text + : $this->cloze_text->getRenderedMarkdownForParticipantPresentation(); + } + public function getScoringOfIdenticalResponses(): ScoringIdentical { return $this->scoring_identical; @@ -148,6 +171,7 @@ public function withGaps( return $clone; } + #[\Override] public function getBasicPropertiesForListing( Language $lng ): array { @@ -159,6 +183,21 @@ public function getBasicPropertiesForListing( ]; } + #[\Override] + public function getOverviewTable( + TableFactory $table_factory, + Language $lng, + ServerRequestInterface $request, + Environment $environment + ): DataTable { + return new OverviewTable( + $table_factory, + $lng, + $request, + $environment + )->getTable(); + } + public function buildBasicEditingInputs( Language $lng, FieldFactory $ff, @@ -200,7 +239,8 @@ public function buildCarryInputs( ): Group { return $ff->group( [ - self::FORM_KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()), + self::FORM_KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()) + ->withDedicatedName(self::FORM_KEY_ID), self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getCarryInputs($ff) ->withDedicatedName(self::FORM_KEY_CLOZE_TEXT), self::FORM_KEY_GAPS_TO_EDIT => $this->gaps->getCarryInputs($ff) @@ -268,11 +308,17 @@ public function withValuesFromCarry( return $clone; } + #[\Override] public function toStorage( Manipulate $manipulate ): Manipulate { - $persistence = $manipulate->getPersistenceForDefinitionClass(Definition::class); - $table_name_builder = $manipulate->getTableNameBuilder(Definition::class); + $persistence = $manipulate->getPersistenceForDefinitionClass( + $this->definition::class + ); + + $table_name_builder = $manipulate->getTableNameBuilder( + $this->definition::class + ); $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create ? $this->buildInsertAnswerFormStatement( @@ -292,6 +338,30 @@ public function toStorage( ); } + #[\Override] + public function toDelete( + Manipulate $manipulate + ): Manipulate { + $persistence = $manipulate->getPersistenceForDefinitionClass( + $this->definition::class + ); + + $table_name_builder = $manipulate->getTableNameBuilder( + $this->definition::class + ); + + return $this->gaps->toDelete( + $manipulate->withAdditionalStatement( + $this->buildDeleteAnswerFormStatement( + $persistence, + $table_name_builder + ) + ), + $persistence, + $table_name_builder + ); + } + private function buildInsertAnswerFormStatement( Persistence $persistence, TableNameBuilder $table_name_builder @@ -318,7 +388,9 @@ private function buildUpdateAnswerFormStatement( return new Update( $persistence->getColumns( $table_name_builder, - $table_definition + $table_definition, + null, + ['answer_form_id'] ), [ new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), @@ -335,4 +407,27 @@ private function buildUpdateAnswerFormStatement( ] ); } + + private function buildDeleteAnswerFormStatement( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::TypeSpecificAnswerForms; + + return new Delete( + $table_definition->getTable($table_name_builder), + [ + new Where( + $persistence->getForeignKeyColumn( + $table_name_builder, + $table_definition + ), + new Value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 76e1aa301b34..64eb908e2555 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -21,25 +21,32 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; use ILIAS\Questions\AnswerForm\Views\Edit as EditViewInterface; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Factory as PropertiesFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\AnswerForm\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; -use ILIAS\Questions\Presentation\Layout\Definitions\Environment; -use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; -use ILIAS\Questions\Presentation\Layout\Definitions\EditOverview; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\HTTP\Services as HTTPServices; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; +use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\Refinery\Factory as Refinery; class Edit implements EditViewInterface { private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; + private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; private const string STEP_SET_GAP_TYPES = 'sgt'; + public const string STEP_JUMP_TO_SET_GAP_TYPES = 'jsgt'; private const string STEP_SET_ANSWER_OPTIONS = 'sao'; - private const string STEP_SET_POINTS = 'sap'; + public const string STEP_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; + private const string STEP_SET_POINTS = 'sp'; + public const string STEP_JUMP_TO_SET_POINTS = 'jsp'; private const string STEP_SAVE = 's'; public function __construct( @@ -53,96 +60,101 @@ public function __construct( ) { } + #[\Override] public function create( Environment $environment ): EditForm|Properties { - return match($environment->getStep()) { - self::STEP_SET_GAP_TYPES => $this->processBasicEditingForm( - $environment->withProperties( - $environment->getProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getDefinitionsFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery - ) - ) - ) - ), - self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( - $environment->withProperties( - $environment->getProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getDefinitionsFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery - ) - ) - ) - ), - self::STEP_SET_POINTS => $this->processAnswerOptionsForm( - $environment->withProperties( - $environment->getProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getDefinitionsFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery - ) - ) - ) - ), - self::STEP_SAVE => $this->processAssignPointsForm( - $environment->withProperties( - $environment->getProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getDefinitionsFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery - ) - ) - ) - ), - default => $this->buildBasicEditingForm($environment) + $step = $environment->getStep(); + + return match($step) { + '' => $this->buildBasicEditingForm($environment), + default => $this->callIntermediateStep($environment, $step) }; } + #[\Override] public function edit( Environment $environment ): EditOverview|EditForm|Properties { + $step = $environment->getStep(); + + if ($step === '') { + return $environment->getPresentationFactory()->getEditOverview( + $environment, + $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + ->buildURI() + ); + } + + $environment->setEditAnswerFormBackTarget(); + return match ($step) { - default => $this->buildEditingOverview($environment) + self::STEP_EDIT_BASIC_PROPERTIES => $this->buildBasicEditingForm($environment), + default => $this->callIntermediateStep($environment, $step) }; } + #[\Override] public function other( Environment $environment ): EditForm|Properties { } - private function buildEditingOverview( - Environment $environment - ): EditOverview { - return $environment->getDefinitionsFactory()->getEditOverview( - $environment->getEditability(), - $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES), - $environment->getProperties() - ); + private function callIntermediateStep( + Environment $environment, + string $step + ): EditForm|Properties { + $initialized_environment = $environment->withPreservedTableRowIdsParameter(); + + if ($step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + && $step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + && $step !== self::STEP_JUMP_TO_SET_POINTS) { + $initialized_environment = $initialized_environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getPresentationFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ); + } + + return match ($step) { + self::STEP_SET_GAP_TYPES, + self::STEP_CONFIRMED_GAP_REMOVAL => $this->processBasicEditingForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesForm( + $initialized_environment + ), + self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsForm( + $initialized_environment + ), + self::STEP_SET_POINTS => $this->processAnswerOptionsForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_POINTS => $this->buildAssignPointsForm( + $initialized_environment + ), + self::STEP_SAVE => $this->processAssignPointsForm( + $initialized_environment + ) + }; } private function buildBasicEditingForm( Environment $environment ): EditForm { - return $environment->getDefinitionsFactory()->getEditForm( + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES), - $environment->getProperties()->buildBasicEditingInputs( + $environment->getAnswerFormProperties()->buildBasicEditingInputs( $this->lng, $this->ui_factory->input()->field(), $this->refinery, @@ -155,31 +167,53 @@ private function buildBasicEditingForm( private function processBasicEditingForm( Environment $environment - ): EditForm { + ): EditForm|Properties { $form = $this->buildBasicEditingForm( $environment )->withRequest($this->http->request()); $data = $form->getData(); - return $data === null - ? $form - : $this->buildGapTypesForm( - $environment->withProperties($data) - ); + if ($data === null) { + return $form; + } + + $new_gaps = $data->getGaps(); + $old_gaps = $environment->getAnswerFormProperties()->getGaps(); + + if ($environment->getStep() !== self::STEP_CONFIRMED_GAP_REMOVAL) { + $removed_gaps = $new_gaps->getRemovedGaps($old_gaps); + if ($removed_gaps !== []) { + return $form->withConfirmation( + $this->buildRemovedGapsConfirmation( + $environment, + $removed_gaps + ) + ); + } + } + + if ($new_gaps->getAddedGaps($old_gaps) === []) { + return $data; + } + + return $this->buildGapTypesForm( + $environment->withAnswerFormProperties($data) + ); } private function buildGapTypesForm( Environment $environment ): EditForm { - $properties = $environment->getProperties(); + $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); - return $environment->getDefinitionsFactory()->getEditForm( + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS), $properties->getGaps()->buildGapsTypeInputs( $this->lng, $ff, $this->refinery, - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng) + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $environment->getTableRowIds() ), false, $properties->withClozeText($properties->getClozeText()) @@ -200,8 +234,8 @@ private function processGapTypesForm( return $data === null ? $form : $this->buildAnswerOptionsForm( - $environment->withProperties( - $environment->getProperties()->withGaps($data) + $environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withGaps($data) ) ); } @@ -209,11 +243,16 @@ private function processGapTypesForm( private function buildAnswerOptionsForm( Environment $environment ): EditForm { - $properties = $environment->getProperties(); + $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); - return $environment->getDefinitionsFactory()->getEditForm( + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS), - $properties->getGaps()->buildAnswerOptionsInputs($this->lng, $ff, $this->refinery), + $properties->getGaps()->buildAnswerOptionsInputs( + $this->lng, + $ff, + $this->refinery, + $environment->getTableRowIds() + ), false, $properties->buildCarryInputs($ff) )->withContentBeforeForm( @@ -232,8 +271,8 @@ private function processAnswerOptionsForm( return $data === null ? $form : $this->buildAssignPointsForm( - $environment->withProperties( - $environment->getProperties()->withGaps($data) + $environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withGaps($data) ) ); } @@ -241,11 +280,16 @@ private function processAnswerOptionsForm( private function buildAssignPointsForm( Environment $environment ): EditForm { - $properties = $environment->getProperties(); + $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); - return $environment->getDefinitionsFactory()->getEditForm( + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::STEP_SAVE), - $properties->getGaps()->buildPointInputs($this->lng, $ff, $this->refinery), + $properties->getGaps()->buildPointInputs( + $this->lng, + $ff, + $this->refinery, + $environment->getTableRowIds() + ), true, $properties->buildCarryInputs($ff) )->withContentBeforeForm( @@ -260,7 +304,7 @@ private function processAssignPointsForm( $environment )->withRequest($this->http->request()); - $properties = $environment->getProperties(); + $properties = $environment->getAnswerFormProperties(); $data = $form->getData(); return $data === null ? $form->withContentBeforeForm( @@ -280,4 +324,29 @@ private function buildClozeTextPanel( ) ); } + + /** + * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap> $removed_gaps + */ + private function buildRemovedGapsConfirmation( + Environment $environment, + array $removed_gaps + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('confirm_remove_gaps'), + $environment->getUrlBuilderWithStepParameter( + self::STEP_CONFIRMED_GAP_REMOVAL + )->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() + ->interruptiveItem()->standard( + $v->getAnswerInputId()->toString(), + $v->buildShortenedGapName() + ), + $removed_gaps + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php index 135596235c55..77579cd67a73 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php @@ -20,20 +20,35 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\AnswerForm\Views\Participant as ParticipantViewInterface; -use ILIAS\Questions\AnswerFormTypes\Cloze\Type; use ILIAS\Questions\Response\Response; +use ILIAS\UICore\GlobalTemplate; +use Mustache\Engine as MustacheEngine; class Participant implements ParticipantViewInterface { + public function __construct( + private readonly GlobalTemplate $global_tpl, + private readonly MustacheEngine $mustache_engine + ) { + } + + #[\Override] public function isAsyncPresentationAvailable(): bool { return true; } + #[\Override] public function get( + Properties $properties, ?Response $response - ): array { - + ): string { + $this->global_tpl->addJavaScript('assets/js/ParticipantViewLongMenu.js'); + return $this->mustache_engine->render( + $properties->getClozeTextForPresentation(), + $properties->getGaps()->getPlaceholderArrayForParticipantView() + ); } } diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php index 39165d20e65d..68df3dc761ef 100644 --- a/components/ILIAS/Questions/src/Collector.php +++ b/components/ILIAS/Questions/src/Collector.php @@ -32,26 +32,29 @@ public function __construct( ) { } - public function withRequiredCapabilities(array $capability_class_names): self - { + public function withRequiredCapabilities( + array $capability_class_names + ): self { $this->checkCapabilities($capability_class_names); $clone = clone $this; $clone->required_capabilities = $capability_class_names; return $clone; } - public function getQuestionsForId(int $id): Question|null - { + public function getQuestionsForId( + int $id + ): ?Question { return $this->repository->getForQuestionId($id); } /** * - * @param array $ids + * @param list $ids * @return \Generator */ - public function getQuestionsForIds(array $ids): \Generator - { + public function getQuestionsForIds( + array $ids + ): \Generator { yield from $this->repository->getForQuestionIds($ids); } } diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php index e1940c63ade2..81cb668be3c5 100644 --- a/components/ILIAS/Questions/src/Persistence/CoreTables.php +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -22,6 +22,14 @@ enum CoreTables: string { + public const string LINKING_TABLE_ID_COLUMN = 'question_id'; + private const string LINKING_TABLE_FOREIGN_KEY_COLUMN = 'obj_id'; + private const array LINKING_TABLE_COLUMNS = [ + 'question_id', + 'obj_id', + 'position' + ]; + private const string QUESTION_TABLE_ID_COLUMN = 'id'; private const array QUESTION_TABLE_COLUMNS = [ 'id', @@ -64,14 +72,17 @@ public function getColumns( ): array { $table = $this->getTable(); $column_identifiers = match($this) { + self::Linking => self::LINKING_TABLE_COLUMNS, self::Questions => self::QUESTION_TABLE_COLUMNS, self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS }; return array_map( fn(string $v): Column => new Column($table, $v), - array_filter( - $column_identifiers, - fn(string $v) => !in_array($v, $columns_to_skip) + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) ) ); } @@ -79,6 +90,10 @@ public function getColumns( public function getIdColumn(): Column { return match($this) { + self::Linking => new Column( + $this->getTable(), + self::LINKING_TABLE_ID_COLUMN + ), self::Questions => new Column( $this->getTable(), self::QUESTION_TABLE_ID_COLUMN @@ -93,6 +108,10 @@ public function getIdColumn(): Column public function getForeignKeyColumn(): ?Column { return match($this) { + self::Linking => new Column( + $this->getTable(), + self::LINKING_TABLE_FOREIGN_KEY_COLUMN + ), self::AnswerForms => new Column( $this->getTable(), self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN diff --git a/components/ILIAS/Questions/src/Persistence/Delete.php b/components/ILIAS/Questions/src/Persistence/Delete.php index be8173df325e..7abec2bc259e 100644 --- a/components/ILIAS/Questions/src/Persistence/Delete.php +++ b/components/ILIAS/Questions/src/Persistence/Delete.php @@ -30,30 +30,44 @@ public function __construct( ) { } - public function toManipulateString(\ilDBInterface $db): string + public function getTableToLock(): string { + return $this->table->getName(); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { return "DELETE FROM {$this->table->getName()}" . PHP_EOL . $this->buildWhereString($db); } - private function buildWhereString(\ilDBInterface $db): string - { + private function buildWhereString( + \ilDBInterface $db + ): string { $values = []; - return sprintf( array_reduce( $this->where, function (?string $c, Where $v) use ($db, &$values): string { - $values[] = $v->getRight()->getQuotedValue($db); + $quoted_value = $v->getRight()->getQuotedValue($db); + if (is_array($quoted_value)) { + $values = array_merge( + $values, + array_values($quoted_value) + ); + } else { + $values[] = $quoted_value; + } if ($c === null) { return "WHERE {$v->toSql()}" . PHP_EOL; } - return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + return "{$c}{$v->getLogicalOperator()->value} {$v->toSql()}" . PHP_EOL; } - ), - $values + ) ?? '', + ...$values ); } } diff --git a/components/ILIAS/Questions/src/Persistence/Insert.php b/components/ILIAS/Questions/src/Persistence/Insert.php index 41a9ec9182b8..bccf22ee793d 100644 --- a/components/ILIAS/Questions/src/Persistence/Insert.php +++ b/components/ILIAS/Questions/src/Persistence/Insert.php @@ -67,10 +67,9 @@ public function withAdditionalValues( return $clone; } - public function lockTable( - \ilAtomQuery $atom_query - ): void { - $atom_query->addTableLock($this->columns[0]->getTableName()); + public function getTableToLock(): string + { + return $this->columns[0]->getTableName(); } public function toManipulateString( @@ -93,8 +92,9 @@ protected function buildColumnsString(): string ) . ')'; } - protected function buildValuesString(\ilDBInterface $db): string - { + protected function buildValuesString( + \ilDBInterface $db + ): string { $return = []; foreach ($this->value_sets as $values) { $return[] = '(' . implode( diff --git a/components/ILIAS/Questions/src/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php index a107fd063115..4e1a0446b741 100644 --- a/components/ILIAS/Questions/src/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -31,7 +31,8 @@ public function __construct( public function toSql(): string { - return "{$this->type->value} JOIN {$this->right->getTableName()} ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; + return "{$this->type->value} JOIN {$this->right->getTableName()} " + . "ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; } public function getLeft(): Column diff --git a/components/ILIAS/Questions/src/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php index 74cefe53b0b6..6a9388d11183 100644 --- a/components/ILIAS/Questions/src/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -25,7 +25,7 @@ class Manipulate { - private array $statements; + private array $statements = []; public function __construct( private readonly \ilDBInterface $db, @@ -68,11 +68,20 @@ public function withAdditionalStatement( public function run(): void { + if ($this->statements === []) { + return; + } + $atom_query = $this->db->buildAtomQuery(); $manipulates = []; + $locked_tables = []; foreach ($this->statements as $statement) { - $statement->lockTable($atom_query); + $table_to_lock = $statement->getTableToLock(); + if (!in_array($table_to_lock, $locked_tables)) { + $atom_query->addTableLock($table_to_lock); + $locked_tables[] = $table_to_lock; + } $manipulates[] = $statement->toManipulateString($this->db); } $atom_query->addQueryCallable( diff --git a/components/ILIAS/Questions/src/Persistence/ManipulationType.php b/components/ILIAS/Questions/src/Persistence/ManipulationType.php index b52c8c6b6f33..069a58b51d5f 100644 --- a/components/ILIAS/Questions/src/Persistence/ManipulationType.php +++ b/components/ILIAS/Questions/src/Persistence/ManipulationType.php @@ -24,4 +24,5 @@ enum ManipulationType { case Create; case Update; + case Delete; } diff --git a/components/ILIAS/Questions/src/Persistence/Operator.php b/components/ILIAS/Questions/src/Persistence/Operator.php index 8f8a6a418ce9..d1e19d3eb882 100644 --- a/components/ILIAS/Questions/src/Persistence/Operator.php +++ b/components/ILIAS/Questions/src/Persistence/Operator.php @@ -32,13 +32,13 @@ enum Operator: string case Like = 'LIKE'; case Between = 'BETWEEN'; - public function toSql(Column $left, int $nr_of_values): string - { - if ($nr_of_values > 1) { - $placeholders = '%s'; - for ($i = 1; $i < $nr_of_values; $i++) { - $placeholders .= ', %s'; - } + public function toSql( + Column $left, + int $nr_of_values + ): string { + $placeholders = '%s'; + for ($i = 1; $i < $nr_of_values; $i++) { + $placeholders .= ', %s'; } return match($this) { diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 0dcda7f2a046..f03a1ca646bd 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -43,11 +43,15 @@ public function __construct( private readonly AnswerFormFactory $answer_form_factory, private readonly Refinery $refinery ) { - + $questions_linking_table_definition = CoreTables::Linking; $questions_table_definition = CoreTables::Questions; $answer_form_table_definition = CoreTables::AnswerForms; $questions_id_column = $questions_table_definition->getIdColumn(); + $this->select[] = new Select( + $questions_linking_table_definition->getColumns() + ); + $this->select[] = new Select( $questions_table_definition->getColumns() ); @@ -56,6 +60,12 @@ public function __construct( $answer_form_table_definition->getColumns() ); + $this->joins[] = new Join( + $questions_id_column, + $questions_table_definition->getIdColumn(), + JoinType::Inner + ); + $this->joins[] = new Join( $questions_id_column, $answer_form_table_definition->getForeignKeyColumn(), @@ -95,36 +105,41 @@ public function getRefinery(): Refinery return $this->refinery; } - public function withAdditionalSelect(Select $select): self - { + public function withAdditionalSelect( + Select $select + ): self { $clone = clone $this; $clone->select[] = $select; return $clone; } - public function withAdditionalJoin(Join $join): self - { + public function withAdditionalJoin( + Join $join + ): self { $clone = clone $this; $clone->joins[] = $join; return $clone; } - public function withAdditionalWhere(Where $where): self - { + public function withAdditionalWhere( + Where $where + ): self { $clone = clone $this; $clone->where[] = $where; return $clone; } - public function withAdditionalOrder(Order $order): self - { + public function withAdditionalOrder( + Order $order + ): self { $clone = clone $this; $clone->order[] = $order; return $clone; } - public function withLimit(int $limit): self - { + public function withLimit( + int $limit + ): self { $clone = clone $this; $clone->limit = $limit; return $clone; @@ -175,13 +190,24 @@ private function toSql(): \ilDBStatement static fn(array $c, Select $v): array => [...$c, ...$v->toColumnsArray()], [] ) - ) . ' FROM ' . CoreTables::Questions->value + ) . ' FROM ' . CoreTables::Linking->value . array_reduce( $this->joins, static fn(string $c, Join $v): string => $c . PHP_EOL . $v->toSql(), '' ) . PHP_EOL . $this->buildWhereString() + . 'ORDER BY ' . implode( + ', ', + array_reduce( + $this->order, + static function (array $c, Order $v): array { + $c[] = $v->toSql(); + return $c; + }, + [] + ) + ) . PHP_EOL . ($this->limit !== null ? "LIMIT = {$this->limit}" : ''), $this->binding_types, $this->binding_values @@ -203,9 +229,10 @@ function (?string $c, Where $v): string { ) ?? ''; } - private function addValueToBinding(Value $value): void - { - if (!is_array($value)) { + private function addValueToBinding( + Value $value + ): void { + if (!is_array($value->getValue())) { $this->binding_types[] = $value->getType(); $this->binding_values[] = $value->getValue(); return; diff --git a/components/ILIAS/Questions/src/Persistence/Replace.php b/components/ILIAS/Questions/src/Persistence/Replace.php index 3554ea426818..9b34db9f6d8c 100644 --- a/components/ILIAS/Questions/src/Persistence/Replace.php +++ b/components/ILIAS/Questions/src/Persistence/Replace.php @@ -22,6 +22,7 @@ class Replace extends Insert { + #[\Override] public function toManipulateString( \ilDBInterface $db ): string { diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 262d8a99d595..998fc739d62d 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -22,7 +22,6 @@ use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; -use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -39,10 +38,12 @@ public function __construct( ) { } - public function getNew(): QuestionImplementation - { + public function getNew( + int $parent_obj_id + ): QuestionImplementation { return new QuestionImplementation( - $this->buildAvailableUuid() + $this->buildAvailableUuid(), + $parent_obj_id ); } @@ -60,8 +61,9 @@ public function getAllQuestions(): \Generator ); } - public function getForQuestionId(Uuid $question_id): ?QuestionImplementation - { + public function getForQuestionId( + Uuid $question_id + ): ?QuestionImplementation { return $this->getForBaseQuery( (new Query( $this->db, @@ -85,8 +87,9 @@ public function getForQuestionId(Uuid $question_id): ?QuestionImplementation * @param array<\ILIAS\Data\Uuid> $question_ids * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ - public function getForQuestionIds(array $question_ids): \Generator - { + public function getForQuestionIds( + array $question_ids + ): \Generator { yield from $this->getForBaseQuery( (new Query( $this->db, @@ -109,32 +112,17 @@ public function getForQuestionIds(array $question_ids): \Generator } /** - * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions */ - private function getForBaseQuery(Query $query): \Generator - { - $query_with_answer_forms = array_reduce( - $this->answer_form_factory->getAvailableDefinitions(), - fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( - $c, - CoreTables::AnswerForms->getIdColumn() - ), - $query - ); - - foreach ($query_with_answer_forms->loadNextRecord() as $query_with_record) { - yield $this->retrieveQuestionFromQuery( - $query_with_record, - $this->retrieveAnswerFormsFromQuery($query_with_record) - ); - } - } - public function create( - array $storable + array $questions ): void { $this->store( - $storable, + array_map( + fn(QuestionImplementation $v): QuestionImplementation => $v + ->withPageId($this->buildQuestionPage($v->getParentObjId())), + $questions + ), new Manipulate( $this->db, $this->answer_form_factory, @@ -143,11 +131,14 @@ public function create( ); } + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ public function update( - array $storable + array $questions ): void { $this->store( - $storable, + $questions, new Manipulate( $this->db, $this->answer_form_factory, @@ -156,15 +147,63 @@ public function update( ); } + public function delete( + array $questions + ): void { + array_reduce( + $questions, + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), + new Manipulate( + $this->db, + $this->answer_form_factory, + ManipulationType::Delete + ) + )->run(); + + foreach ($questions as $question) { + (new \QstsQuestionPage($question->getPageId()))->delete(); + } + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + private function getForBaseQuery( + Query $query + ): \Generator { + $query_with_answer_forms = array_reduce( + $this->answer_form_factory->getAvailableDefinitions(), + fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( + $c, + CoreTables::AnswerForms->getIdColumn() + ), + $query + ); + + foreach ($query_with_answer_forms->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + $this->retrieveAnswerFormsFromQuery($query_with_record) + ); + } + } + private function retrieveQuestionFromQuery( Query $query, array $answer_forms ): QuestionImplementation { + $linking_info = $query->retrieveCurrentRecord( + CoreTables::Linking->getTable(), + $this->refinery->identity() + ); + return $query->retrieveCurrentRecord( CoreTables::Questions->getTable(), $this->refinery->custom()->transformation( fn(array $vs): QuestionImplementation => new QuestionImplementation( $this->uuid_factory->fromString($vs[0]['id']), + $linking_info[0]['obj_id'], + $linking_info[0]['position'], $vs[0]['page_id'], $vs[0]['title'], $vs[0]['author'], @@ -213,17 +252,15 @@ function (array $vs) use ($query): array { } /** - * - * @param array<\ILIAS\Questions\Persistence\Storable> $storable - * @return array + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions */ private function store( - array $storable, + array $questions, Manipulate $manipulate ): void { array_reduce( - $storable, - fn(Manipulate $c, Storable $v): Manipulate => $v->toStorage($c), + $questions, + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toStorage($c), $manipulate )->run(); } @@ -238,20 +275,23 @@ private function buildAvailableUuid(): Uuid } while (true); } - private function checkAvailabilityOfId(Uuid $uuid): bool - { + private function checkAvailabilityOfId( + Uuid $uuid + ): bool { return $this->db->fetchObject( $this->db->query( - 'SELECT COUNT(*) as cnt FROM ' . self::QUESTION_TABLE + 'SELECT COUNT(*) as cnt FROM ' . CoreTables::Questions->value . " WHERE id='{$uuid->toString()}'" ) )->cnt === 0; } - private function buildQuestionPage(): int - { + private function buildQuestionPage( + int $parent_obj_id + ): int { $page = new \QstsQuestionPage(); $page->setId($this->getNextAvailableQuestionPageId()); + $page->setParentId($parent_obj_id); $page->createFromXML(); return $page->getId(); } diff --git a/components/ILIAS/Questions/src/Persistence/Storable.php b/components/ILIAS/Questions/src/Persistence/Storable.php index 429e65f8ef30..82290134d3e6 100644 --- a/components/ILIAS/Questions/src/Persistence/Storable.php +++ b/components/ILIAS/Questions/src/Persistence/Storable.php @@ -22,6 +22,11 @@ interface Storable { - public function toStorage(Manipulate $manipulate): Manipulate; - public function toDelete(Manipulate $manipulate): Manipulate; + public function toStorage( + Manipulate $manipulate + ): Manipulate; + + public function toDelete( + Manipulate $manipulate + ): Manipulate; } diff --git a/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php index 9c8b747841e5..aa7815dabc80 100644 --- a/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php +++ b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php @@ -27,6 +27,7 @@ public function __construct( ) { } + #[\Override] public function getTypeSpecificTableNamePart(): string { return $this->answer_form_id; diff --git a/components/ILIAS/Questions/src/Persistence/Update.php b/components/ILIAS/Questions/src/Persistence/Update.php index df4e1bd22cdf..0631f6099b59 100644 --- a/components/ILIAS/Questions/src/Persistence/Update.php +++ b/components/ILIAS/Questions/src/Persistence/Update.php @@ -48,44 +48,59 @@ public function __construct( } } - public function toManipulateString(\ilDBInterface $db): string + public function getTableToLock(): string { + return $this->columns[0]->getTableName(); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { return "UPDATE {$this->columns[0]->getTableName()}" . PHP_EOL . $this->buildSetterString($db) . PHP_EOL . $this->buildWhereString($db); } - private function buildSetterString(\ilDBInterface $db): string - { + private function buildSetterString( + \ilDBInterface $db + ): string { return trim( array_reduce( array_keys($this->columns), fn(string $c, int $v): string => $c - . "{$this->columns[$v]->getColumnString()} = {$this->values[$v]->getQuotedValue()},", + . "{$this->columns[$v]->getColumnString()} = {$this->values[$v]->getQuotedValue($db)},", 'SET ' ), ',' ); } - private function buildWhereString(\ilDBInterface $db): string - { + private function buildWhereString( + \ilDBInterface $db + ): string { $values = []; - return sprintf( array_reduce( $this->where, function (?string $c, Where $v) use ($db, &$values): string { - $values[] = $v->getRight()->getQuotedValue($db); + $quoted_value = $v->getRight()->getQuotedValue($db); + if (is_array($quoted_value)) { + $values = array_merge( + $values, + array_values($quoted_value) + ); + } else { + $values[] = $quoted_value; + } if ($c === null) { return "WHERE {$v->toSql()}" . PHP_EOL; } - return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + return "{$c}{$v->getLogicalOperator()->value} {$v->toSql()}" . PHP_EOL; } - ), - $values + ) ?? '', + ...$values ); } } diff --git a/components/ILIAS/Questions/src/Persistence/Value.php b/components/ILIAS/Questions/src/Persistence/Value.php index 9d7e48fa54fc..bb986140fd4c 100644 --- a/components/ILIAS/Questions/src/Persistence/Value.php +++ b/components/ILIAS/Questions/src/Persistence/Value.php @@ -22,6 +22,9 @@ class Value { + /* + * @param $type Type definition as provided by \ilDBConstants + */ public function __construct( private readonly string $type, private readonly null|string|int|float|array $value @@ -38,9 +41,20 @@ public function getValue(): string|int|array return $this->value; } - public function getQuotedValue(\ilDBInterface $db): string - { - return $db->quote($this->value, $this->type); + public function getQuotedValue( + \ilDBInterface $db + ): array|string { + if (!is_array($this->value)) { + return $db->quote( + $this->value, + $this->type + ); + } + + return array_map( + fn(mixed $v): string => $db->quote($v, $this->type), + $this->value + ); } public function getNumberOfElements(): int diff --git a/components/ILIAS/Questions/src/Persistence/Where.php b/components/ILIAS/Questions/src/Persistence/Where.php index 179345f5e6cc..b57d202bdde3 100644 --- a/components/ILIAS/Questions/src/Persistence/Where.php +++ b/components/ILIAS/Questions/src/Persistence/Where.php @@ -33,7 +33,7 @@ public function __construct( public function toSql(): string { - return $this->negate ? 'NOT ' : '' + return ($this->negate ? 'NOT ' : '') . $this->comparison->toSql( $this->left, $this->right->getNumberOfElements() diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php similarity index 84% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php rename to components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php index 87a4328128a4..3733be691737 100755 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/CarryWrapper.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Definitions; use ILIAS\Refinery\Transformation; @@ -34,15 +34,18 @@ public function __construct( * of `CarrySectionData` containing the next nesting level if the value contains * values from multiple variables from $_POST. */ - public function retrieve(string $key, Transformation $transformation): mixed - { + public function retrieve( + string $key, + Transformation $transformation + ): mixed { return $transformation->transform( $this->retrieveValueFromArray($key) ); } - private function retrieveValueFromArray(string $key): mixed - { + private function retrieveValueFromArray( + string $key + ): mixed { $value = $this->raw_values[$key] ?? null; if ($value === null) { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php similarity index 51% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php rename to components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 151bfd712cee..9c112f518e33 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -18,20 +18,46 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Definitions; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\Presentation\Definitions\Editability; +use ILIAS\Questions\Presentation\Layout\Factory; use ILIAS\UI\URLBuilder; +use ILIAS\UI\URLBuilderToken; interface Environment { - public function getDefinitionsFactory(): Factory; + public function setEditAnswerFormBackTarget(): void; + + public function addEditAnswerFormSubTab( + string $id, + string $text, + string $step + ): void; + + public function getPresentationFactory(): Factory; + public function getUrlBuilder(): URLBuilder; - public function getUrlBuilderWithStepParameter(string $step): URLBuilder; + + public function getUrlBuilderWithStepParameter( + string $step + ): URLBuilder; + + public function getTableRowIdToken(): URLBuilderToken; + + public function getTableRowIds(): array; + public function getStep(): string; + public function withDefaultStep(): self; + public function getEditability(): Editability; - public function getProperties(): ?Properties; - public function withProperties(Properties $properties): self; + + public function getAnswerFormProperties(): ?Properties; + + public function withAnswerFormProperties( + Properties $properties + ): self; + + public function withPreservedTableRowIdsParameter(): self; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php new file mode 100644 index 000000000000..cc5adc752c91 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -0,0 +1,377 @@ +acquireURLBuilderAndParameters($base_uri); + } + + #[\Override] + public function setEditAnswerFormBackTarget(): void + { + $this->tabs_gui->clearTargets(); + $this->tabs_gui->setBackTarget( + $this->lng->txt('cancel'), + $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + } + + #[\Override] + public function addEditAnswerFormSubTab( + string $id, + string $text, + string $step + ): void { + $this->tabs_gui->addSubTab( + $id, + $text, + $this->getUrlBuilderWithStepParameter($step)->buildURI()->__toString() + ); + } + + #[\Override] + public function getPresentationFactory(): Factory + { + return $this->presentation_factory; + } + + #[\Override] + public function getUrlBuilder(): URLBuilder + { + return $this->url_builder; + } + + #[\Override] + public function getUrlBuilderWithStepParameter( + string $step + ): URLBuilder { + return $this->getUrlBuilder()->withParameter($this->step_token, $step); + } + + #[\Override] + public function withDefaultStep(): self + { + $clone = clone $this; + $clone->default_step = true; + return $clone; + } + + #[\Override] + public function getStep(): string + { + return $this->default_step + ? '' + : $this->retrieveStringValueForToken($this->step_token, self::TOKEN_STRING_STEP); + } + + #[\Override] + public function getEditability(): Editability + { + return $this->editability; + } + + #[\Override] + public function getAnswerFormProperties(): ?Properties + { + return $this->properties; + } + + #[\Override] + public function withAnswerFormProperties( + Properties $properties + ): self { + $clone = clone $this; + $clone->properties = $properties; + return $clone; + } + + #[\Override] + public function getTableRowIdToken(): URLBuilderToken + { + return $this->table_row_token; + } + + /** + * @return list + */ + #[\Override] + public function getTableRowIds(): array + { + if ($this->table_row_ids !== null) { + return $this->table_row_ids; + } + + return $this->table_row_ids = $this->http->wrapper()->query()->retrieve( + $this->table_row_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + fn($v): string => $v !== '' + ? $this->refinery->kindlyTo()->string()->transform($v) + : throw new \UnexpectedValueException() + ) + ), + $this->refinery->always([]) + ]) + ); + } + + #[\Override] + public function withPreservedTableRowIdsParameter(): self + { + $clone = clone $this; + $clone->table_row_ids = $clone->getTableRowIds(); + $clone->url_builder = $this->url_builder + ->withParameter($this->table_row_token, $clone->table_row_ids); + return $clone; + } + + public function getObjId(): int + { + return $this->obj_id; + } + + public function getQuestionIdsToken(): URLBuilderToken + { + return $this->question_ids_token; + } + + public function getAction(): string + { + return $this->retrieveStringValueForToken($this->action_token); + } + + public function withActionParameter( + string $action + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->action_token, $action); + return $clone; + } + + public function withQuestionIdParameter( + Uuid $question_id + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->question_id_token, $question_id->toString()); + return $clone; + } + + public function withAnswerFormTypeHashParameter( + string $type_hash + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->type_hash_token, $type_hash); + return $clone; + } + + public function getQuestionId(): ?Uuid + { + return $this->http->wrapper()->query()->retrieve( + $this->question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + $this->buildRetrieveQuestionIdClosure() + ), + $this->refinery->always(null) + ]) + ); + } + + /** + * This function will either return the QuestionIds from the corresponding + * $_GET parameter OR from an InterruptiveItems $_POST value. + * @return array<\ILIAS\Data\UUID\Uuid>|string|null + */ + public function getQuestionIds(): array|string|null + { + return $this->http->wrapper()->query()->retrieve( + $this->question_ids_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): string => $v === ['ALL_OBJECTS'] + ? 'ALL_OBJECTS' + : throw new \UnexpectedValueException() + ), + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + $this->buildRetrieveQuestionIdClosure() + ) + ), + $this->refinery->always(null) + ]) + ) ?? $this->http->wrapper()->post()->retrieve( + self::INTERRUPTIVE_ITEMS_KEY, + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + $this->buildRetrieveQuestionIdClosure() + ) + ), + $this->refinery->always(null) + ); + } + + public function getTypeClassHast(): string + { + return $this->retrieveStringValueForToken($this->type_hash_token); + } + + public function setEditAnswerFormTabs( + string $cmd_feedback, + string $cmd_content_for_repetition + ): void { + $this->tabs_gui->addTab( + self::TAB_ID_ANSWER_FORM, + $this->lng->txt('answer_form'), + $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + + $this->tabs_gui->addTab( + $cmd_feedback, + $this->lng->txt('feedback'), + $this->withActionParameter($cmd_feedback) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + + $this->tabs_gui->addTab( + $cmd_content_for_repetition, + $this->lng->txt('suggested_solution'), + $this->withActionParameter($cmd_content_for_repetition) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + + $this->tabs_gui->activateTab(self::TAB_ID_ANSWER_FORM); + } + + public function setParametersForQuestionCmds(): void + { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $this->question_id_token->getName(), + $this->getQuestionId()->toString() + ); + } + + private function acquireURLBuilderAndParameters( + URI $base_uri + ): void { + [ + $this->url_builder, + $this->action_token, + $this->step_token, + $this->question_id_token, + $this->question_ids_token, + $this->type_hash_token, + $this->table_row_token + ] = (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_QUESTION_IDS, + self::TOKEN_TYPE_HASH, + self::TOKEN_TABLE_ROW_ID + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function buildRetrieveQuestionIdClosure(): \Closure + { + return fn($v): Uuid => is_string($v) + ? $this->uuid_factory->fromString($v) + : throw new \UnexpectedValueException(); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php b/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php similarity index 92% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php rename to components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php index 66e8e0bf572d..55e80b2bc9a3 100755 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Leaf.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Definitions; class Leaf { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Async.php b/components/ILIAS/Questions/src/Presentation/Layout/Async.php new file mode 100644 index 000000000000..f1de349ba47c --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Async.php @@ -0,0 +1,53 @@ +http->saveResponse( + $this->http->response()->withBody( + Streams::ofString( + $ui_renderer->renderAsync($this->content) + ) + ) + ); + $this->http->sendResponse(); + $this->http->close(); + } + + +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php deleted file mode 100644 index 4d45ddd4c373..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EnvironmentImplementation.php +++ /dev/null @@ -1,200 +0,0 @@ -acquireURLBuilderAndParameters($base_uri); - } - - public function getDefinitionsFactory(): Factory - { - return $this->definitions_factory; - } - - public function getUrlBuilder(): URLBuilder - { - return $this->url_builder; - } - - public function getUrlBuilderWithStepParameter(string $step): URLBuilder - { - return $this->getUrlBuilder()->withParameter($this->step_token, $step); - } - - public function withDefaultStep(): self - { - $clone = clone $this; - $clone->default_step = true; - return $clone; - } - - public function getStep(): string - { - return $this->default_step - ? '' - : $this->retrieveStringValueForToken($this->step_token, self::TOKEN_STRING_STEP); - } - - public function getEditability(): Editability - { - return $this->editability; - } - - public function getProperties(): ?Properties - { - return $this->properties; - } - - public function withProperties(Properties $properties): self - { - $clone = clone $this; - $clone->properties = $properties; - return $clone; - } - - public function getAction(): string - { - return $this->retrieveStringValueForToken($this->action_token); - } - - public function withActionParameter(string $action): self - { - $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->action_token, $action); - return $clone; - } - - public function withQuestionIdParameter(Uuid $question_id): self - { - $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->question_id_token, $question_id->toString()); - return $clone; - } - - public function withAnswerFormTypeHashParameter(string $type_hash): self - { - $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->type_hash_token, $type_hash); - return $clone; - } - - public function getQuestionId(): ?Uuid - { - return $this->http->wrapper()->query()->retrieve( - $this->question_id_token->getName(), - $this->refinery->byTrying([ - $this->refinery->custom()->transformation( - fn($v): Uuid => $this->uuid_factory->fromString($v) - ), - $this->refinery->always(null) - ]) - ); - } - - public function getTypeClassHast(): string - { - return $this->retrieveStringValueForToken($this->type_hash_token); - } - - public function setParametersForQuestionCmds(): void - { - $this->ctrl->setParameterByClass( - \QstsQuestionPageGUI::class, - $this->question_id_token->getName(), - $this->getQuestionId()->toString() - ); - } - - private function acquireURLBuilderAndParameters(URI $base_uri): void - { - [ - $this->url_builder, - $this->action_token, - $this->step_token, - $this->question_id_token, - $this->type_hash_token - ] = (new URLBuilder($base_uri)) - ->acquireParameters( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_TYPE_HASH - ); - } - - private function retrieveStringValueForToken( - URLBuilderToken $token - ): string { - return $this->http->wrapper()->query()->retrieve( - $token->getName(), - $this->buildStringTrafo() - ); - } - - private function buildStringTrafo(): Transformation - { - return $this->refinery->byTrying([ - $this->refinery->kindlyTo()->string(), - $this->refinery->always('') - ]); - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php similarity index 74% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php rename to components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index dfe92b2075b8..cad255b94886 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -18,13 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Layout; use ILIAS\Language\Language; use ILIAS\UI\Component\Input\Container\Form\Factory as FormFactory; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\URLBuilder; use ILIAS\UI\Renderer as UIRenderer; @@ -39,6 +40,7 @@ class EditForm private ?StandardPanel $content_before_form = null; private ?StandardPanel $content_after_form = null; + private ?InterruptiveModal $confirmation = null; public function __construct( private readonly FormFactory $form_factory, @@ -51,20 +53,30 @@ public function __construct( $this->form = $this->buildForm(); } - public function withContentBeforeForm(StandardPanel $content): self - { + public function withContentBeforeForm( + StandardPanel $content + ): self { $clone = clone $this; $clone->content_before_form = $content; return $clone; } - public function withContentAfterForm(StandardPanel $content): self - { + public function withContentAfterForm( + StandardPanel $content + ): self { $clone = clone $this; $clone->content_after_form = $content; return $clone; } + public function withConfirmation( + InterruptiveModal $confirmation_modal + ): self { + $clone = clone $this; + $clone->confirmation = $confirmation_modal; + return $clone; + } + public function render( UIRenderer $ui_renderer ): string { @@ -93,6 +105,20 @@ private function buildContent(): array $content[] = $this->content_before_form; } + if ($this->confirmation !== null) { + $content[] = $this->confirmation->withOnLoad( + $this->confirmation->getShowSignal() + )->withAdditionalOnLoadCode( + function ($id) { + return "var button = {$id}.querySelector('input[type=\"submit\"]'); " + . "button.addEventListener('click', (e) => {e.preventDefault();" + . 'const form = button.closest("dialog").nextElementSibling;' + . "form.action = '{$this->confirmation->getFormAction()}';" + . 'form.submit();});'; + } + ); + } + $content[] = $this->form; if ($this->content_after_form !== null) { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php similarity index 62% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php rename to components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index eee4451940e2..440884a95599 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -18,11 +18,12 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Presentation\Definitions\Editability; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\URI; use ILIAS\Language\Language; -use ILIAS\UI\URLBuilder; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; @@ -30,16 +31,13 @@ class EditOverview { - private bool $orderable = false; - public function __construct( private readonly UIFactory $ui_factory, private readonly Language $lng, - private readonly Editability $editability, - private readonly URLBuilder $url_builder, - private readonly Properties $answer_form_properties + private readonly ServerRequestInterface $request, + private readonly Environment $environment, + private readonly URI $uri_to_edit_basic_answer_form_properties ) { - $this->form = $this->buildForm(); } public function render( @@ -48,26 +46,16 @@ public function render( return $ui_renderer->render($this->buildContent()); } - public function withRequest( - ServerRequestInterface $request - ): self { - $clone = clone $this; - $clone->form = $clone->form->withRequest($request); - return $clone; - } - - public function withOrderable(bool $orderable): self - { - $clone = clone $this; - $clone->orderable = $orderable; - return $clone; - } - private function buildContent(): array { return [ $this->buildBasicAnswerFormPanel(), - $this->answer_form_properties->getOverviewTable() + $this->environment->getAnswerFormProperties()->getOverviewTable( + $this->ui_factory->table(), + $this->lng, + $this->request, + $this->environment + ) ]; } @@ -75,14 +63,14 @@ private function buildBasicAnswerFormPanel(): StandardPanel { $content = [ $this->ui_factory->listing()->descriptive( - $this->answer_form_properties->getBasicPropertiesForListing($this->lng) + $this->environment->getAnswerFormProperties()->getBasicPropertiesForListing($this->lng) ) ]; - if ($this->editability === Editability::Full) { + if ($this->environment->getEditability() === Editability::Full) { $content[] = $this->ui_factory->button()->standard( $this->lng->txt('edit_basic_answer_form_properties'), - $this->url_builder->buildURI()->__toString() + $this->uri_to_edit_basic_answer_form_properties->__toString() ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php similarity index 72% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php rename to components/ILIAS/Questions/src/Presentation/Layout/Factory.php index d5ee413e8c5f..a2bc23fbc70d 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -18,9 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Presentation\Definitions\CarryWrapper; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\Leaf; +use ILIAS\Data\URI; +use ILIAS\HTTP\Services as HttpService; use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -28,30 +32,29 @@ use ILIAS\UI\URLBuilder; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; -use ILIAS\UI\Component\Table\Data as DataTable; -use ILIAS\UI\Component\Table\Ordering as OrderingTable; +use ILIAS\UI\Component\MessageBox\MessageBox; +use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; +use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; class Factory { public function __construct( private readonly UIFactory $ui_factory, + private readonly HttpService $http, private readonly Language $lng ) { } public function getEditOverview( - Editability $editability, - URLBuilder $url_builder, - DataTable|OrderingTable $answer_elements_table, - Properties $answer_form_properties + Environment $environment, + URI $uri_to_edit_basic_answer_form_properties ): EditOverview { return new EditOverview( $this->ui_factory, $this->lng, - $editability, - $url_builder, - $answer_elements_table, - $answer_form_properties + $this->http->request(), + $environment, + $uri_to_edit_basic_answer_form_properties ); } @@ -71,6 +74,15 @@ public function getEditForm( ); } + public function getAsync( + InterruptiveModal|RoundTripModal|MessageBox $content + ): Async { + return new Async( + $this->http, + $content + ); + } + public function getCarrySectionData( ArrayBasedRequestWrapper $post_wrapper, Refinery $refinery diff --git a/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php index e3a9cb948116..590231b8a9c0 100755 --- a/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php @@ -43,19 +43,23 @@ class LayoutProvider extends AbstractModificationProvider private const MODIFICATION_PRIORITY = 5; //slightly above "low" + #[\Override] public function isInterestedInContexts(): ContextCollection { return $this->context_collection->main(); } - protected function isModeEnabled(CalledContexts $called_contexts): bool - { + protected function isModeEnabled( + CalledContexts $called_contexts + ): bool { return $called_contexts->current()->getAdditionalData() ->is(self::MODE_ENABLED, true); } - public function getBreadCrumbsModification(CalledContexts $called_contexts): ?BreadCrumbsModification - { + #[\Override] + public function getBreadCrumbsModification( + CalledContexts $called_contexts + ): ?BreadCrumbsModification { if (!$this->isModeEnabled($called_contexts)) { return null; } @@ -68,8 +72,10 @@ function (?Breadcrumbs $current): ?Breadcrumbs { )->withPriority(self::MODIFICATION_PRIORITY); } - public function getMainBarModification(CalledContexts $called_contexts): ?MainBarModification - { + #[\Override] + public function getMainBarModification( + CalledContexts $called_contexts + ): ?MainBarModification { $mainbar = $this->globalScreen()->layout()->factory()->mainbar(); if (!$this->isModeEnabled($called_contexts)) { @@ -82,8 +88,10 @@ public function getMainBarModification(CalledContexts $called_contexts): ?MainBa )->withPriority(self::MODIFICATION_PRIORITY); } - public function getMetaBarModification(CalledContexts $called_contexts): ?MetaBarModification - { + #[\Override] + public function getMetaBarModification( + CalledContexts $called_contexts + ): ?MetaBarModification { if (!$this->isModeEnabled($called_contexts)) { return null; } @@ -96,8 +104,10 @@ function (?MetaBar $current): ?MetaBar { )->withPriority(self::MODIFICATION_PRIORITY); } - public function getPageBuilderDecorator(CalledContexts $called_contexts): ?PageBuilderModification - { + #[\Override] + public function getPageBuilderDecorator( + CalledContexts $called_contexts + ): ?PageBuilderModification { if (!$this->isModeEnabled($called_contexts)) { return null; } @@ -118,8 +128,9 @@ static function (PagePartProvider $parts) use ($mode_info): Page { ); } - private function buildMainbarModification(CalledContexts $called_contexts): \Closure - { + private function buildMainbarModification( + CalledContexts $called_contexts + ): \Closure { return function (?MainBar $mainbar) use ($called_contexts): ?MainBar { if ($mainbar === null) { return null; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php similarity index 79% rename from components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php rename to components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 7be8ac24903a..b6e87a2e7c11 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Definitions/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -18,26 +18,28 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout\Definitions; +namespace ILIAS\Questions\Presentation\Layout; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; use ILIAS\Questions\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; -use ILIAS\UI\Component\Table; +use ILIAS\Language\Language; +use ILIAS\UI\Component\Table\DataRetrieval; +use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; use Psr\Http\Message\ServerRequestInterface; -class QuestionsTable implements Table\DataRetrieval +class QuestionsTable implements DataRetrieval { public function __construct( private readonly UIFactory $ui_factory, private readonly \ilUIService $ui_service, - private readonly \ilLanguage $lng, + private readonly Language $lng, private readonly ServerRequestInterface $request, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, @@ -52,18 +54,9 @@ public function render( return $ui_renderer->render($this->buildContent()); } - public function getColums(): array - { - $f = $this->ui_factory->table()->column(); - - return [ - 'title' => $f->link($this->lng->txt('title')), - 'type' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true), - ]; - } - + #[\Override] public function getRows( - Table\DataRowBuilder $row_builder, + DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, @@ -83,6 +76,7 @@ public function getRows( } } + #[\Override] public function getTotalRowCount( mixed $additional_viewcontrol_data, mixed $filter_data, @@ -99,12 +93,15 @@ private function buildContent(): array $this, $this->lng->txt('questions'), $this->getColums(), + )->withActions( + $this->getActions() )->withRequest($this->request) ]; } - private function buildFilter(string $action): Filter - { + private function buildFilter( + string $action + ): Filter { $question_type_options = [ '' => $this->lng->txt('filter_all_question_types') ]; @@ -130,4 +127,26 @@ private function buildFilter(string $action): Filter ); return $filter; } + + private function getColums(): array + { + $f = $this->ui_factory->table()->column(); + + return [ + 'title' => $f->link($this->lng->txt('title')), + 'type' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true), + ]; + } + + private function getActions(): array + { + return [ + 'delete' => $this->ui_factory->table()->action()->standard( + $this->lng->txt('delete'), + $this->environment->withActionParameter(Edit::CMD_DELETE_QUESTION) + ->getUrlBuilder(), + $this->environment->getQuestionIdsToken() + )->withAsync(true) + ]; + } } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index e92128dad7c5..f4c1bb110701 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,20 +20,24 @@ namespace ILIAS\Questions\Presentation\Views; -use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; -use ILIAS\Questions\Presentation\Layout\Definitions\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\Factory as DefinitionsFactory; use ILIAS\Questions\Presentation\Definitions\Editability; -use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; -use ILIAS\Questions\Presentation\Layout\Definitions\QuestionsTable; +use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\TypeGenericProperties; +use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; -use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\UICore\GlobalTemplate; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\HTTP\Services as HTTP; @@ -42,14 +46,18 @@ use ILIAS\UI\Component\Item\Standard as StandardItem; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; +use ILIAS\Style\Content\Service as ContentStyle; use ILIAS\GlobalScreen\Services as GlobalScreen; class Edit { private const string CMD_CREATE_QUESTION = 'create'; public const string CMD_EDIT_QUESTION = 'edit'; + public const string CMD_DELETE_QUESTION = 'delete'; private const string CMD_CREATE_ANSWER_FORM = 'create_af'; private const string CMD_EDIT_ANSWER_FORM = 'edit_af'; + private const string CMD_EDIT_FEEDBACK = 'edit_f'; + private const string CMD_EDIT_CONTENT_FOR_REPETITION = 'edit_cfr'; private array $required_capabilities = []; private Editability $editability = Editability::Full; @@ -62,10 +70,12 @@ public function __construct( private readonly UIFactory $ui_factory, private readonly UIRenderer $ui_renderer, private readonly GlobalScreen $global_screen, + private readonly GlobalTemplate $global_tpl, + private readonly ContentStyle $content_style, private readonly \ilCtrl $ctrl, private readonly HTTP $http, + private readonly \ilTabsGUI $tabs_gui, private readonly \ilUIService $ui_services, - private readonly DataFactory $data_factory, private readonly UuidFactory $uuid_factory, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, @@ -73,23 +83,26 @@ public function __construct( ) { } - public function withRequiredCapabilities(array $capability_class_names): self - { + public function withRequiredCapabilities( + array $capability_class_names + ): self { $this->checkCapabilities($capability_class_names); $clone = clone $this; $clone->required_capabilities = $capability_class_names; return $clone; } - public function withEditable(Editability $editability): self - { + public function withEditable( + Editability $editability + ): self { $clone = clone $this; $clone->editability = $editability; return $clone; } - public function withOrderingEnabled(bool $enable): self - { + public function withOrderingEnabled( + bool $enable + ): self { $clone = clone $this; $clone->ordering_enabled = $enable; return $clone; @@ -97,12 +110,24 @@ public function withOrderingEnabled(bool $enable): self public function view( \ilToolbarGUI $toolbar, - URI $base_uri - ): QuestionsTable|EditForm { - $environment = $this->buildEnvironment($base_uri); + URI $base_uri, + int $obj_id, + int $ref_id + ): Async|QuestionsTable|EditForm { + $this->content_style->gui()->addCss( + $this->global_tpl, + $ref_id + ); + + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + ); + return match($environment->getAction()) { self::CMD_CREATE_QUESTION => $this->createQuestion($environment), self::CMD_EDIT_QUESTION => $this->editQuestion($environment), + self::CMD_DELETE_QUESTION => $this->deleteQuestion($environment), default => $this->showTable($toolbar, $environment) }; } @@ -110,21 +135,34 @@ public function view( public function forwardPageCmds( \ilGlobalTemplateInterface $tpl, URI $base_uri, + int $obj_id, + int $ref_id ): void { - $environment = $this->buildEnvironment($base_uri); + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + ); $this->initializeEditMode($environment); $environment->setParametersForQuestionCmds(); + $this->content_style->gui()->addCss( + $tpl, + $ref_id + ); + $tpl->setContent( $this->ctrl->forwardCommand( new \QstsQuestionPageGUI( + $this->questions_repository->getForQuestionId( + $environment->getQuestionId() + ), + $obj_id + )->withReturnURI( $environment - ->withActionParameter(self::CMD_EDIT_QUESTION) - ->withQuestionIdParameter($environment->getQuestionId()) - ->getUrlBuilder() - ->buildURI(), - $this, - $this->questions_repository->getForQuestionId($environment->getQuestionId()) + ->withActionParameter(self::CMD_EDIT_QUESTION) + ->withQuestionIdParameter($environment->getQuestionId()) + ->getUrlBuilder() + ->buildURI() ) ) ); @@ -132,23 +170,34 @@ public function forwardPageCmds( public function createAnswerForm( URI $base_uri, + int $obj_id, QuestionImplementation $question, \ilPCAnswerForm $content_object ): EditForm { - $environment = $this->buildEnvironment($base_uri) - ->withActionParameter(self::CMD_CREATE_ANSWER_FORM) - ->withQuestionIdParameter($question->getId()); + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + )->withActionParameter(self::CMD_CREATE_ANSWER_FORM) + ->withQuestionIdParameter($question->getId()); $answer_form_type_class_hash = $environment->getTypeClassHast(); + if ($answer_form_type_class_hash !== '') { + $type = $this->answer_form_factory + ->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash); + return $this->forwardCreateAnswerFormCmd( - $environment->withAnswerFormTypeHashParameter($answer_form_type_class_hash), + $environment->withAnswerFormProperties( + $type->buildProperties( + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId() + ), + null + ) + )->withAnswerFormTypeHashParameter($answer_form_type_class_hash), $question, $content_object, - $this->answer_form_factory->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash), - $this->answer_form_factory->getDefaultTypeGenericProperties( - $question->getId() - ) + $type->getEditView() ); } @@ -165,17 +214,34 @@ public function createAnswerForm( public function editAnswerForm( URI $base_uri, + int $obj_id, QuestionImplementation $question, - \ilPCAnswerForm $content_object - ): EditForm|EditOverview { - $environment = $this->buildEnvironment($base_uri) - ->withActionParameter(self::CMD_EDIT_ANSWER_FORM) - ->withQuestionIdParameter($question->getId()); + AnswerFormProperties $answer_form_properties, + Definition $type + ): Async|EditForm|EditOverview { + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + )->withAnswerFormProperties($answer_form_properties) + ->withActionParameter(self::CMD_EDIT_ANSWER_FORM) + ->withQuestionIdParameter($question->getId()); + + $environment->setEditAnswerFormTabs( + self::CMD_EDIT_FEEDBACK, + self::CMD_EDIT_CONTENT_FOR_REPETITION + ); - return match($environment->getAction()) { - self::CMD_EDIT_ANSWER_FORM => $this->processCreateAnswerForm($environment->getUrlBuilder()), - default => $this->forwardEditAnswerFormCmd($environment) - }; + $edit = $type->getEditView()->edit($environment); + + if (!($edit instanceof AnswerFormProperties)) { + return $edit; + } + + $this->questions_repository->update( + [$question->withAnswerForm($edit)] + ); + + $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); } private function createQuestion( @@ -183,14 +249,15 @@ private function createQuestion( ): EditForm { $this->initializeEditMode($environment); - $create = $this->questions_repository->getNew()->getEditView( + $create = $this->questions_repository->getNew( + $environment->getObjId() + )->getEditView( $this->lng, $this->current_user, $this->ui_factory, $this->refinery, $this->http->request(), - $this->ctrl, - $this->data_factory + $this->ctrl )->create( $environment->withActionParameter(self::CMD_CREATE_QUESTION) ); @@ -200,12 +267,14 @@ private function createQuestion( } $this->questions_repository->create([$create]); - return $this->buildEditStartView( + return $this->ctrl->redirectToURL( $environment ->withDefaultStep() ->withActionParameter(self::CMD_EDIT_QUESTION) - ->withQuestionIdParameter($create->getId()), - $create + ->withQuestionIdParameter($create->getId()) + ->getUrlBuilder() + ->buildURI() + ->__toString() ); } @@ -216,19 +285,21 @@ private function editQuestion( $this->initializeEditMode($environment); $question_id = $environment->getQuestionId(); + $question = $this->questions_repository->getForQuestionId($question_id); + $environment_with_question_parameter = $environment + ->withQuestionIdParameter($question_id); - $edit = $this->questions_repository->getForQuestionId($question_id)->getEditView( + $edit = $question->getEditView( $this->lng, $this->current_user, $this->ui_factory, $this->refinery, $this->http->request(), - $this->ctrl, - $this->data_factory + $this->ctrl )->edit( - $environment - ->withActionParameter(self::CMD_EDIT_QUESTION) - ->withQuestionIdParameter($question_id) + $environment_with_question_parameter + ->withActionParameter(self::CMD_EDIT_QUESTION), + $question->getParticipantView() ); if ($edit instanceof EditForm) { @@ -237,11 +308,52 @@ private function editQuestion( $this->questions_repository->update([$edit]); return $this->buildEditStartView( - $environment->withQuestionIdParameter($question_id), + $environment_with_question_parameter + ->withDefaultStep() + ->withActionParameter(self::CMD_EDIT_QUESTION), $edit ); } + private function deleteQuestion( + EnvironmentImplementation $environment + ): Async { + $question_ids = $environment->getQuestionIds(); + + if ($question_ids === null) { + return $environment->getPresentationFactory()->getAsync( + $this->ui_factory->messageBox()->failure( + $this->lng->txt('msg_no_questions_selected') + ) + ); + } + + if ($environment->getStep() === self::CMD_DELETE_QUESTION) { + $this->questions_repository->delete( + iterator_to_array( + $this->questions_repository->getForQuestionIds($question_ids) + ) + ); + $this->ctrl->redirectToURL( + $environment->getUrlBuilder()->buildURI()->__toString() + ); + } + + return $environment->getPresentationFactory()->getAsync( + $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('qpl_confirm_delete_questions'), + $environment->withActionParameter( + self::CMD_DELETE_QUESTION + )->getUrlBuilderWithStepParameter( + self::CMD_DELETE_QUESTION + )->buildURI()->__toString() + )->withAffectedItems( + $this->buildAffectedItems($question_ids) + ) + ); + } + private function showTable( \ilToolbarGUI $toolbar, EnvironmentImplementation $environment @@ -279,13 +391,17 @@ private function processCreateAnswerForm( return $data === null ? $form : $this->forwardCreateAnswerFormCmd( - $environment->withAnswerFormTypeHashParameter( + $environment->withAnswerFormProperties( + $data->buildProperties( + $generic_answer_form_properties, + null + ) + )->withAnswerFormTypeHashParameter( $this->answer_form_factory->getHashedClass($data::class) ), $question, $content_obj, - $data, - $generic_answer_form_properties + $data->getEditView() ); } @@ -293,14 +409,9 @@ private function forwardCreateAnswerFormCmd( EnvironmentImplementation $environment, QuestionImplementation $question, \ilPCAnswerForm $content_obj, - Definition $type, - TypeGenericProperties $type_generic_properties, + AnswerFormEditView $answer_form_edit_view ): ?EditForm { - $create = $type->getEditView()->create( - $environment->withProperties( - $type->buildProperties($type_generic_properties, null) - ) - ); + $create = $answer_form_edit_view->create($environment); if ($create instanceof EditForm) { return $create; @@ -386,16 +497,18 @@ private function buildEditStartView( $this->ui_factory, $this->refinery, $this->http->request(), - $this->ctrl, - $this->data_factory - )->edit($environment); + $this->ctrl + )->edit( + $environment, + $question->getParticipantView() + ); } private function buildCreateAnswerForm( EnvironmentImplementation $environemt ): EditForm { $if = $this->ui_factory->input(); - return $environemt->getDefinitionsFactory()->getEditForm( + return $environemt->getPresentationFactory()->getEditForm( $environemt->getUrlBuilder(), $if->field()->section( [ @@ -414,26 +527,55 @@ private function buildCreateAnswerForm( ); } - private function checkCapabilities(array $capabilities): void - { + /** + * + * @param string|array<\ILIAS\Data\UUID\Uuid> $question_ids + * @return array<\ILIAS\UI\Component\Modal\InterruptiveItem\Standard> + */ + private function buildAffectedItems( + string|array $question_ids + ): array { + $questions = $question_ids === 'ALL_OBJECTS' + ? $this->questions_repository->getAllQuestions() + : $this->questions_repository->getForQuestionIds($question_ids); + $affected_items = []; + foreach ($questions as $question) { + $affected_items[] = $this->ui_factory->modal()->interruptiveItem()->standard( + $question->getId()->toString(), + $question->getTitle() + ); + } + return $affected_items; + } + + private function checkCapabilities( + array $capabilities + ): void { foreach ($capabilities as $capability) { if (!$this->questions_repository->capabilityExists($capability)) { - throw new \InvalidArgumentException('All provided capabilities must implement ILIAS\Questions\AnswerForm\Capabilities\Capability.'); + throw new \InvalidArgumentException( + 'All provided capabilities must implement ' + . 'ILIAS\Questions\AnswerForm\Capabilities\Capability.' + ); } } } public function buildEnvironment( - URI $base_uri + URI $base_uri, + int $obj_id ): EnvironmentImplementation { return new EnvironmentImplementation( $this->ctrl, $this->http, $this->refinery, + $this->lng, + $this->tabs_gui, $this->uuid_factory, $this->definitions_factory, $this->editability, - $base_uri + $base_uri, + $obj_id ); } } diff --git a/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php b/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php index 004f8903c66a..f3debbb9aca0 100644 --- a/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php +++ b/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php @@ -32,16 +32,18 @@ enum TextMatchingOptions: string case Levenstein4 = '4'; case Levenstein5 = '5'; - public function getLabel(Language $lng): string - { + public function getLabel( + Language $lng + ): string { return match ($this) { self::CaseInsensitive, self::CaseSensitive => $lng->txt($this->value), default => sprintf($lng->txt('cloze_textgap_levenshtein_of'), $this->value) }; } - public static function buildOptionsList(Language $lng): array - { + public static function buildOptionsList( + Language $lng + ): array { return array_reduce( self::cases(), function (array $c, self $v) use ($lng): array { diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 3172fdf85186..8ffa5808e0ca 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -21,7 +21,6 @@ namespace ILIAS\Questions\Question; use ILIAS\Questions\Question\Views\Participant; -use ILIAS\Questions\Persistence\Storable; interface Question { diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 3620aea14457..42233dbe742b 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -22,16 +22,15 @@ use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; -use ILIAS\Questions\Persistence\Storable; use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Persistence\Where; -use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; @@ -42,8 +41,9 @@ use ILIAS\Refinery\Factory as Refinery; use Psr\Http\Message\RequestInterface; -class QuestionImplementation implements Question, Storable +class QuestionImplementation implements Question { + private bool $linking_information_updated = false; private bool $self_updated = false; private array $updated_answer_forms = []; private array $deleted_answer_forms = []; @@ -55,6 +55,8 @@ class QuestionImplementation implements Question, Storable */ public function __construct( private readonly Uuid $id, + private int $parent_obj_id, + private ?int $position = null, private ?int $page_id = null, private string $title = '', private string $author = '', @@ -82,6 +84,29 @@ public function getId(): ?Uuid return $this->id; } + public function getParentObjId(): int + { + return $this->parent_obj_id; + } + + public function withParentObjId( + int $parent_obj_id + ): self { + $clone = clone $this; + $clone->parent_obj_id = $parent_obj_id; + $clone->linking_information_updated = true; + return $clone; + } + + public function withPosition( + int $position + ): self { + $clone = clone $this; + $clone->position = $position; + $clone->linking_information_updated = true; + return $clone; + } + public function getPageId(): ?int { return $this->page_id; @@ -181,7 +206,7 @@ public function getAnswerForms(): array return $this->answer_forms; } - public function getAnswerFormByIdString( + public function getAnswerFormPropertiesByIdString( string $form_id ): ?AnswerFormProperties { return $this->answer_forms[$form_id] ?? null; @@ -202,7 +227,7 @@ public function withoutDeletedAnswerForms( $clone = clone $this; foreach (array_keys($this->answer_forms) as $answer_form_id) { if (!in_array($answer_form_id, $found_answer_form_ids)) { - $this->deleted_answer_forms = $clone->answer_forms[$answer_form_id]; + $clone->deleted_answer_forms[] = $clone->answer_forms[$answer_form_id]; unset($clone->answer_forms[$answer_form_id]); } } @@ -224,17 +249,24 @@ public function getEditView( UIFactory $ui_factory, Refinery $refinery, RequestInterface $request, - \ilCtrl $ctrl, - DataFactory $data_factory + \ilCtrl $ctrl ): Views\Edit { - return new Views\Edit($lng, $current_user, $ui_factory, $refinery, $request, $ctrl, $data_factory, $this); + return new Views\Edit( + $lng, + $current_user, + $ui_factory, + $refinery, + $request, + $ctrl, + $this + ); } + #[\Override] public function getParticipantView(): Views\Participant { return new Views\Participant( - new \QstsQuestionPageGUI($this), - $this->answer_forms + $this ); } @@ -282,16 +314,24 @@ public function toStorage( public function toDelete( Manipulate $manipulate ): Manipulate { - ; + return $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate->withAdditionalStatement( + $this->buildDeleteQuestionStatement() + ), + $this->answer_forms + ); } private function addInsertStatementsToManipulation( Manipulate $manipulate ): Manipulate { if ($this->created === null) { - $manipulate = $manipulate->withAdditionalStatement( - $this->buildInsertQuestionStatement() - ); + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildInsertLinkingStatement() + )->withAdditionalStatement( + $this->buildInsertQuestionStatement() + ); } if ($this->updated_answer_forms !== []) { @@ -314,6 +354,13 @@ private function addInsertStatementsToManipulation( private function addUpdateStatementsToManipulation( Manipulate $manipulate ): Manipulate { + if ($this->linking_information_updated) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildUpdateLinkingStatement() + ); + } + if ($this->self_updated) { $manipulate = $manipulate->withAdditionalStatement( $this->buildUpdateQuestionStatement() @@ -321,7 +368,7 @@ private function addUpdateStatementsToManipulation( } if ($this->deleted_answer_forms !== []) { - $manipulate = $this->addDeleteAnswerFormStatementsToManipulate( + $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate, $this->deleted_answer_forms ); @@ -333,24 +380,6 @@ private function addUpdateStatementsToManipulation( ); } - private function buildInsertQuestionStatement(): Insert - { - return new Insert( - CoreTables::Questions->getColumns(), - [ - new Value(\ilDBConstants::T_TEXT, $this->id->toString()), - new Value(\ilDBConstants::T_INTEGER, $this->page_id), - new Value(\ilDBConstants::T_TEXT, $this->title), - new Value(\ilDBConstants::T_TEXT, $this->author), - new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), - new Value(\ilDBConstants::T_TEXT, $this->remarks), - new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), - new Value(\ilDBConstants::T_INTEGER, time()), - new Value(\ilDBConstants::T_INTEGER, time()) - ] - ); - } - private function addAnswerFormStatementsToManipulate( Manipulate $manipulate, array $answer_forms @@ -364,7 +393,7 @@ private function addAnswerFormStatementsToManipulate( ); } - private function addDeleteAnswerFormStatementsToManipulate( + private function addDeleteAnswerFormsStatementsToManipulate( Manipulate $manipulate, array $answer_forms_to_delete ): Manipulate { @@ -377,6 +406,58 @@ private function addDeleteAnswerFormStatementsToManipulate( ); } + + + private function buildInsertLinkingStatement(): Insert + { + return new Insert( + CoreTables::Linking->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $this->id->toString()), + new Value(\ilDBConstants::T_INTEGER, $this->parent_obj_id), + new Value(\ilDBConstants::T_INTEGER, $this->position) + ] + ); + } + + private function buildInsertQuestionStatement(): Insert + { + return new Insert( + CoreTables::Questions->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $this->id->toString()), + new Value(\ilDBConstants::T_INTEGER, $this->page_id), + new Value(\ilDBConstants::T_TEXT, $this->title), + new Value(\ilDBConstants::T_TEXT, $this->author), + new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), + new Value(\ilDBConstants::T_TEXT, $this->remarks), + new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), + new Value(\ilDBConstants::T_INTEGER, time()), + new Value(\ilDBConstants::T_INTEGER, time()) + ] + ); + } + + private function buildUpdateLinkingStatement(): Update + { + $linking_table_definition = CoreTables::Linking; + return new Update( + $linking_table_definition->getColumns( + [CoreTables::LINKING_TABLE_ID_COLUMN] + ), + [ + new Value(\ilDBConstants::T_INTEGER, $this->parent_obj_id), + new Value(\ilDBConstants::T_INTEGER, $this->position) + ], + [ + new Where( + $linking_table_definition->getIdColumn(), + new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + ) + ] + ); + } + private function buildUpdateQuestionStatement(): Update { $questions_table_definition = CoreTables::Questions; @@ -402,4 +483,21 @@ private function buildUpdateQuestionStatement(): Update ] ); } + + private function buildDeleteQuestionStatement(): Delete + { + $table_definition = CoreTables::Questions; + return new Delete( + $table_definition->getTable(), + [ + new Where( + $table_definition->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index fc7667749e75..5a21f551dbc4 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -20,12 +20,11 @@ namespace ILIAS\Questions\Question\Views; -use ILIAS\Questions\Presentation\Layout\Definitions\EditForm; -use ILIAS\Questions\Presentation\Layout\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Data\Factory as DataFactory; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; @@ -45,7 +44,6 @@ public function __construct( private readonly Refinery $refinery, private readonly RequestInterface $request, private readonly \ilCtrl $ctrl, - private readonly DataFactory $data_factory, private readonly QuestionImplementation $question ) { @@ -61,12 +59,13 @@ public function create( } public function edit( - EnvironmentImplementation $environment + EnvironmentImplementation $environment, + Participant $participant_view ): EditForm|Question { return match ($environment->getStep()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), default => $this->buildBasicPropertiesForm($environment)->withContentAfterForm( - $this->buildPreviewPanel($environment) + $this->buildPreviewPanel($environment, $participant_view) ) }; } @@ -74,7 +73,7 @@ public function edit( private function buildBasicPropertiesForm( EnvironmentImplementation $environment ): EditForm { - return $environment->getDefinitionsFactory()->getEditForm( + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), $this->buildBasicPropertiesInputs(), true @@ -101,8 +100,7 @@ private function buildBasicPropertiesInputs(): Section [ 'title' => $ff->text($this->lng->txt('title')) ->withRequired(true), - 'author' => $ff->text($this->lng->txt('author')) - ->withValue($this->current_user->getFullname()), + 'author' => $ff->text($this->lng->txt('author')), 'lifecycle' => $ff->select( $this->lng->txt('qst_lifecycle'), array_reduce( @@ -121,7 +119,9 @@ function (array $c, Lifecycle $v): array { return $section->withValue([ 'title' => $this->question->getTitle(), - 'author' => $this->question->getAuthor(), + 'author' => $this->question->getAuthor() !== '' + ? $this->question->getAuthor() + : $this->current_user->getFullname(), 'lifecycle' => $this->question->getLifecycle()->value, 'remarks' => $this->question->getRemarks() ]); @@ -147,12 +147,15 @@ function (array $vs): QuestionImplementation { } private function buildPreviewPanel( - EnvironmentImplementation $environment + EnvironmentImplementation $environment, + Participant $participant_view ): StandardPanel { $environment->setParametersForQuestionCmds(); return $this->ui_factory->panel()->standard( $this->lng->txt('preview'), - $this->ui_factory->legacy()->content($this->question->getTitle()) + $this->ui_factory->legacy()->content( + $participant_view->get($environment->getObjId()) + ) )->withActions( $this->ui_factory->dropdown()->standard([ $this->ui_factory->link()->standard( diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index 186f8477de0c..775aff19fd8e 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Question\Views; -use ILIAS\Questions\Question\Question; +use ILIAS\Questions\Question\QuestionImplementation; class Participant { @@ -30,12 +30,13 @@ class Participant private bool $show_correct_solution = false; public function __construct( - private readonly Question $question + private readonly QuestionImplementation $question ) { } - public function withIsAsync(bool $async): self - { + public function withIsAsync( + bool $async + ): self { foreach ($this->question->getAnswerForms() as $form) { if (!$form->getType()->isAsyncPresentationAvailable()) { throw \Exception('This QuestionType has no async presentation.'); @@ -46,15 +47,17 @@ public function withIsAsync(bool $async): self return $clone; } - public function withIsInteractive(bool $interactive): self - { + public function withIsInteractive( + bool $interactive + ): self { $clone = clone $this; $clone->interactive = $interactive; return $clone; } - public function withShowMarks(bool $show_marks): self - { + public function withShowMarks( + bool $show_marks + ): self { foreach ($this->question->getAnswerForms() as $form) { if (!$form->getType()->isMarkable()) { throw \Exception('This QuestionType cannot be marked.'); @@ -66,8 +69,9 @@ public function withShowMarks(bool $show_marks): self return $clone; } - public function withShowCorrectSolution(bool $show_correct_solution): self - { + public function withShowCorrectSolution( + bool $show_correct_solution + ): self { foreach ($this->question->getAnswerForms() as $form) { if (!$form->getType()->isMarkable()) { throw \Exception('This QuestionType cannot be marked.'); @@ -78,4 +82,32 @@ public function withShowCorrectSolution(bool $show_correct_solution): self $clone->show_correct_solution = $show_correct_solution; return $clone; } + + public function get( + int $obj_id + ): string { + $tpl = new \ilTemplate( + 'tpl.qpl_question_preview.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $tpl->setVariable( + 'PREVIEW_FORMACTION', + '' + ); + + $question_page = new \QstsQuestionPageGUI( + $this->question, + $obj_id + ); + $question_page->setPresentationTitle($this->question->getTitle()); + + $tpl->setVariable( + 'QUESTION_OUTPUT', + $question_page->presentation() + ); + return $tpl->get(); + } } diff --git a/components/ILIAS/Questions/src/Response/Repository.php b/components/ILIAS/Questions/src/Response/Repository.php index 62d40c84fdd4..01e2ff350f1b 100644 --- a/components/ILIAS/Questions/src/Response/Repository.php +++ b/components/ILIAS/Questions/src/Response/Repository.php @@ -22,7 +22,16 @@ interface Repository { - public function getForUser(int $question_id, int $user_id): Result; - public function getAllForQuestion(int $question_id): Result; - public function storeResult(Result $result): void; + public function getForUser( + int $question_id, + int $user_id + ): Result; + + public function getAllForQuestion( + int $question_id + ): Result; + + public function storeResult( + Result $result + ): void; } diff --git a/components/ILIAS/Questions/src/Response/Response.php b/components/ILIAS/Questions/src/Response/Response.php index 5e5f849c3dc9..b2cffed5cb8c 100644 --- a/components/ILIAS/Questions/src/Response/Response.php +++ b/components/ILIAS/Questions/src/Response/Response.php @@ -23,8 +23,16 @@ interface Response { public function getCorrespondingQuestionType(): QuestionType; + public function getQuestionId(): ?int; - public function withQuestionId(int $question_id): self; + + public function withQuestionId( + int $question_id + ): self; + public function getParticipantId(): ?int; - public function withParticipantId(int $participant_id): self; + + public function withParticipantId( + int $participant_id + ): self; } diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index 82af39742651..a8708db82f2e 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -22,16 +22,38 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpaceCore; -use ILIAS\Setup\Agent\NullAgent; +use ILIAS\Refinery\Transformation; +use ILIAS\Setup\Agent as SetupAgent; +use ILIAS\Setup\Agent\HasNoNamedObjective; use ILIAS\Setup\Objective; use ILIAS\Setup\ObjectiveCollection; +use ILIAS\Setup\Objective\NullObjective; use ILIAS\Setup\Metrics\Storage; use ILIAS\Setup\Config; -class Agent extends NullAgent +class Agent implements SetupAgent { - public function getUpdateObjective(?Config $config = null): Objective + use HasNoNamedObjective; + + public function __construct( + private readonly array $answer_form_migrations + ) { + } + + public function hasConfig(): bool + { + return false; + } + + public function getArrayToConfigTransformation(): Transformation { + throw new LogicException(self::class . ' has no Config.'); + } + + #[\Override] + public function getUpdateObjective( + ?Config $config = null + ): Objective { return new ObjectiveCollection( 'Database is updated for ILIAS\Questions', false, @@ -52,8 +74,10 @@ public function getUpdateObjective(?Config $config = null): Objective ); } - public function getStatusObjective(Storage $storage): Objective - { + #[\Override] + public function getStatusObjective( + Storage $storage + ): Objective { return new ObjectiveCollection( 'ILIAS\Questions', true, @@ -71,4 +95,35 @@ public function getStatusObjective(Storage $storage): Objective ) ); } + + #[\Override] + public function getMigrations(): array + { + return [ + new QuestionsMigration( + $this->answer_form_migrations + ) + ]; + } + + #[\Override] + public function getBuildObjective(): Objective + { + return new NullObjective(); + } + + #[\Override] + public function getInstallObjective( + ?Config $config = null + ): Objective { + return new NullObjective(); + } + + + #[\Override] + public function getNamedObjectives( + ?Config $config = null + ): array { + return new NullObjective(); + } } diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index f41dc833c0d8..91a85ec837f4 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -32,8 +32,10 @@ public function __construct( ) { } - public function prepare(\ilDBInterface $db): void - { + #[\Override] + public function prepare( + \ilDBInterface $db + ): void { $this->db = $db; } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index b4d007a48138..b128a071aa04 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -24,8 +24,10 @@ class OverarchingQuestionTables implements \ilDatabaseUpdateSteps { protected \ilDBInterface $db; - public function prepare(\ilDBInterface $db): void - { + #[\Override] + public function prepare( + \ilDBInterface $db + ): void { $this->db = $db; } @@ -190,7 +192,7 @@ public function step_4(): void 'position' => [ 'type' => \ilDBConstants::T_INTEGER, 'length' => 2, - 'notnull' => true + 'notnull' => false ] ]); } @@ -203,4 +205,32 @@ public function step_4(): void $this->db->addIndex($table_name, ['obj_id'], 'o'); } } + + public function step_5(): void + { + if (!$this->db->tableExists(QuestionsMigration::MIGRATIONS_TABLE)) { + $this->db->createTable(QuestionsMigration::MIGRATIONS_TABLE, [ + 'old_question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'new_question_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields( + QuestionsMigration::MIGRATIONS_TABLE, + ['old_question_id'] + )) { + $this->db->addPrimaryKey( + QuestionsMigration::MIGRATIONS_TABLE, + ['old_question_id'] + ); + } + } } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php new file mode 100644 index 000000000000..9ffadd2ff32d --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -0,0 +1,279 @@ +db = $environment->getResource(Setup\Environment::RESOURCE_DATABASE); + $this->io = $environment->getResource(Setup\Environment::RESOURCE_ADMIN_INTERACTION); + $this->uuid_factory = new UuidFactory(); + } + + #[\Override] + public function step( + Environment $environment + ): void { + /** + * sk, 2026-01-14: Sadly this is necessary to clone the question pages + * without duplicating a humongous amount of code. It is mighty + * depressing, but the structure of `COPage` is yay stupid. + */ + if (!$this->ilias_is_initialized) { + \ilContext::init(\ilContext::CONTEXT_CRON); + entry_point('ILIAS Legacy Initialisation Adapter'); + $this->ilias_is_initialized = true; + } + + $db_values = $this->fetchValidRecord(); + + if ($db_values->obj_fi === 0) { + $question->obj_fi = $this->getObjIdFromMapping($db_values->question_id); + } + + if ($db_values->obj_fi === null) { + $this->io->error( + "The question with the id {$db_values->question_id} could not be " + . "migrated as it doesn't belong to any object." + ); + return; + } + + $question = new QuestionImplementation( + $this->uuid_factory->uuid4(), + $db_values->obj_fi, + $db_values->sequence, + $this->buildQuestionPage($db_values), + $db_values->title, + $db_values->author, + Lifecycle::tryFrom($db_values->lifecycle), + $db_values->description, + $db_values->original_id, + $db_values->created, + time() + ); + } + + #[\Override] + public function getRemainingAmountOfSteps(): int + { + return $this->db->fetchObject( + $this->db->query( + 'SELECT COUNT(question_id) cnt FROM ' . self::QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL + . 'LEFT JOIN ' . self::MIGRATIONS_TABLE . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'WHERE t.type_tag IN (' + . implode( + ', ', + array_map( + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + $this->answer_form_migrations + ) + ) . ')' . PHP_EOL + . 'AND q.complete = 1' . PHP_EOL + . 'AND m.old_question_id IS NULL' + ) + )->cnt; + } + + private function fetchValidRecord(): array + { + $query_string = 'SELECT q.*, t.sequence FROM ' . self::QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL + . 'LEFT JOIN ' . self::MIGRATIONS_TABLE . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' t ON q.question_id = t.question_fi' + . 'WHERE t.type_tag IN (' + . implode( + ', ', + array_map( + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + $this->answer_form_migrations + ) + ) . ')' . PHP_EOL + . 'AND q.complete = 1' . PHP_EOL + . 'AND m.old_question_id IS NULL' + . 'LIMIT 1'; + + do { + $db_values = $this->db->fetchObject( + $this->db->query($query_string) + ); + } while (!$this->areDbValuesValid($db_values)); + + $db_values->original_id = $this->getNewQuestionIdForOld($db_values->original_id); + return $db_values; + } + + private function areDbValuesValid( + array $db_values + ): bool { + if ($db_values->original_id === null) { + return true; + } + + if ($this->allready_migrated_questions === null) { + $this->allready_migrated_questions = $this->loadAlreadyMigratedQuestions(); + } + + if (isset($this->allready_migrated_questions[$db_values->original_id])) { + return true; + } + + return false; + } + + private function getNewQuestionIdForOld( + ?int $question_id + ): ?uuid { + if ($question_id === null) { + return null; + } + + if ($this->allready_migrated_questions === null) { + $this->allready_migrated_questions = $this->loadAlreadyMigratedQuestions(); + } + + if (!isset($this->allready_migrated_questions[$question_id])) { + return null; + } + + return $this->uuid_factory->fromString( + $this->allready_migrated_questions[$question_id] + ); + } + + private function loadAlreadyMigratedQuestions(): array + { + + $query = $this->db->query( + 'SELECT * FROM ' . self::MIGRATIONS_TABLE + ); + + $questions = []; + while (($row = $this->db->fetchObject($query)) !== null) { + $questions[$row->old_question_id] = $row->new_question_id; + } + return $questions; + } + + private function getObjIdFromMapping( + int $question_id + ): ?int { + if ($this->question_to_learning_module_mapping === null) { + $this->question_to_learning_module_mapping = $this->loadQuestionsToLearningModuleMapping(); + } + + $this->question_to_learning_module_mapping[$question_id] ?? null; + } + + private function loadQuestionsToLearningModuleMapping(): array + { + + $query = $this->db->query( + 'SELECT question_id, obj_id FROM page_question pq' . PHP_EOL + . 'JOIN page_object po ON pq.page_id = po.page_id' . PHP_EOL + . 'AND pq.page_parent_type = po.parent_type' . PHP_EOL + . 'JOIN object_data o ON po.parent_id = o.obj_id' . PHP_EOL + . 'WHERE page_parent_type = "lm"' + ); + + $mapping = []; + while (($row = $this->db->fetchObject($query)) !== null) { + $mapping[$row->question_id] = $row->obj_id; + } + return $mapping; + } + + private function buildQuestionPage( + + ): int { + $new_id = $this->getNextAvailableQuestionPageId(); + $page = new \ilAssQuestionPage(); + $page->copy( + $new_id, + 'qsts', + ); + return $new_id; + } + + private function getNextAvailableQuestionPageId(): int + { + + $last_id = $this->db->fetchObject( + $this->db->query( + 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value + . ' WHERE parent_type = "qsts"' + ) + )->last; + if ($last_id === null) { + return 1; + } + + return $last_id + 1; + } +} From abacb62ac92d78996314b947c8606d195e0f6ed1 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 20 Jan 2026 17:55:55 +0100 Subject: [PATCH 033/108] Questions: Implement Basic Migration --- .../class.ilObjQuestionsGUI.php | 2 +- .../Legacy/PageEditor/QstsQuestionPage.php | 20 + .../PageEditor/class.ilPCAnswerForm.php | 2 +- components/ILIAS/Questions/Questions.php | 29 +- .../Questions/src/AnswerForm/Factory.php | 6 +- .../AnswerForm/{ => Migration}/Migration.php | 14 +- .../AnswerForm/Migration/MigrationInsert.php | 177 +++++++++ .../Questions/src/AnswerForm/Persistence.php | 2 +- .../Questions/src/AnswerForm/Properties.php | 2 +- .../src/AnswerForm/TypeGenericProperties.php | 25 +- .../Cloze/Layout/CombinationsOverview.php | 82 ++++ .../AnswerFormTypes/Cloze/Layout/Factory.php | 106 ++++++ .../Migration/BasicMigrationFunctions.php | 153 ++++++++ .../Cloze/Migration/MigrationCloze.php | 163 ++++++++ .../Cloze/Migration/MigrationLongMenu.php | 178 +++++++++ .../Cloze/Migration/MigrationNumeric.php | 122 ++++++ .../AnswerFormTypes/Cloze/MigrationCloze.php | 40 -- .../Cloze/MigrationLongMenu.php | 40 -- .../Cloze/MigrationNumeric.php | 40 -- .../src/AnswerFormTypes/Cloze/Persistence.php | 2 +- .../Combinations/EditCombinations.php | 352 ++++++++++++++++++ .../Cloze/Properties/Combinations/Factory.php | 108 ++++++ .../Cloze/Properties/Factory.php | 30 +- .../Gaps/AnswerOptions/AnswerOption.php | 6 +- .../Cloze/Properties/Gaps/Factory.php | 2 +- .../Cloze/Properties/Gaps/Gap.php | 9 +- .../Cloze/Properties/Gaps/Gaps.php | 1 - .../Cloze/Properties/Gaps/Text.php | 2 +- .../Cloze/Properties/Properties.php | 9 +- .../Definitions/TextMatchingOptions.php | 8 +- .../Questions/src/Persistence/CoreTables.php | 50 ++- .../Questions/src/Persistence/Manipulate.php | 2 +- .../ILIAS/Questions/src/Persistence/Query.php | 2 +- .../Questions/src/Persistence/Repository.php | 39 +- .../src/Presentation/Layout/Renderable.php | 158 ++++++++ .../src/Question/QuestionImplementation.php | 41 +- .../src/Setup/ClozeQuestionTables.php | 4 +- .../src/Setup/OverarchingQuestionTables.php | 21 +- .../src/Setup/QuestionsMigration.php | 229 ++++++++---- .../classes/class.ilAssQuestionPage.php | 28 +- 40 files changed, 2019 insertions(+), 287 deletions(-) rename components/ILIAS/Questions/src/AnswerForm/{ => Migration}/Migration.php (70%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationLongMenu.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationNumeric.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php rename components/ILIAS/Questions/src/{Question => }/Definitions/TextMatchingOptions.php (88%) create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Renderable.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 982c8c284216..ff2f24adef02 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -98,7 +98,7 @@ public function viewQuestionsObject(): void $this->tabs_gui->activateTab('questions'); $this->tpl->setContent( - $this->edit_view->view( + $this->edit_view->show( $this->toolbar, $this->buildEditQuestionsBaseUri(), $this->object->getId(), diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 9e23acb982f2..09d5a51e28e5 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -40,4 +40,24 @@ public function setQuestion( ): void { $this->question = $question; } + + public function migrateQuestionElementToAnswerForm(): void + { + global $DIC; + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + $answer_forms = $this->question->getAnswerForms(); + + $answer_form_node = new ilPCAnswerForm($this); + $answer_form_node->createPageContentNode(); + $answer_form_node->writePCId($this->generatePCId()); + $answer_form_node->create( + array_shift($answer_forms)->getAnswerFormId() + ); + + $dom_util->path($this->getDomDoc(), '//Question') + ->item(0)->parentNode->replaceWith($answer_form_node->getDomNode()); + + $this->update(); + } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 0a747ad04580..9d19474ebeea 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -78,7 +78,7 @@ public static function afterPageUpdate( string $xml, bool $creation ): void { - if ($page::class !== QstsQuestionPage::class) { + if ($page::class !== QstsQuestionPage::class || $creation) { return; } diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 050ce8cff30e..66a3852d69c8 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -21,10 +21,12 @@ namespace ILIAS; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; -use ILIAS\Questions\AnswerForm\Migration as AnswerFormMigration; -use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationCloze; -use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationLongMenu; -use ILIAS\Questions\AnswerFormTypes\Cloze\MigrationNumeric; +use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; +use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationCloze; +use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationLongMenu; +use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationNumeric; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\Setup\Agent; use ILIAS\Setup\Agent as AgentInterface; @@ -45,10 +47,23 @@ public function init( new Agent( $seek[AnswerFormMigration::class] ); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze(); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu(); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric(); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze( + $internal[Persistence::class], + $internal[\EvalMath::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu( + $internal[Persistence::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric( + $internal[Persistence::class], + $internal[\EvalMath::class] + ); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); + + $internal[Persistence::class] = static fn() => new Persistence( + new TableNameSpaceCore('cloze') + ); + $internal[\EvalMath::class] = static fn() => new \EvalMath(); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index f47770b066ff..5836840dbca5 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -96,11 +96,13 @@ public function buildTypeDefinitionFromSelectValue( } public function getDefaultTypeGenericProperties( - Uuid $question_id + Uuid $question_id, + Definition $type ): TypeGenericProperties { return new TypeGenericProperties( $this->uuid_factory->uuid4(), - $question_id + $question_id, + $type ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php similarity index 70% rename from components/ILIAS/Questions/src/AnswerForm/Migration.php rename to components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php index ae72b1be9ef7..590824fb049c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -18,9 +18,9 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm; +namespace ILIAS\Questions\AnswerForm\Migration; -use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\TableNameSpace; interface Migration { @@ -30,7 +30,11 @@ interface Migration */ public function getOldQuestionIdentifier(): string; - public function toStorage( - Manipulate $manipulate - ): Manipulate; + public function getDefinitionClass(): string; + + public function getTableNameSpace(): TableNameSpace; + + public function buildInsertStatement( + MigrationInsert $migration_insert + ): MigrationInsert; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php new file mode 100644 index 000000000000..9c02ad13fc98 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -0,0 +1,177 @@ +db; + } + + public function getIO(): IOWrapper + { + return $this->io; + } + + public function getUuid(): Uuid + { + return $this->uuid_factory->uuid4(); + } + + public function getTableNameBuilder(): TableNameBuilder + { + return $this->table_name_builder; + } + + public function getOldQuestionId(): int + { + return $this->old_question_id; + } + + public function getAnswerFormId(): Uuid + { + return $this->answer_form_id; + } + + public function withAvailablePoints( + float $available_points + ): self { + $clone = clone $this; + $clone->available_points = $available_points; + return $clone; + } + + public function withImageSize( + int $image_size + ): self { + $clone = clone $this; + $clone->image_size = $image_size; + return $clone; + } + + public function withShuffleAnswerOptions( + bool $shuffle_answer_options + ): self { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + + public function withAdditionalText( + string $additional_text + ): self { + $clone = clone $this; + $clone->additional_text = $additional_text; + return $clone; + } + + public function withAdditionalTextLegacy( + string $additional_text_legacy + ): self { + $clone = clone $this; + $clone->additional_text_legacy = $additional_text_legacy; + return $clone; + } + + public function withAdditionalInsert( + Insert $insert + ): self { + $clone = clone $this; + $clone->inserts[] = $insert; + return $clone; + } + + public function run(): void + { + $this->inserts[] = $this->buildCoreAnswerFormInsertStatement(); + $atom_query = $this->db->buildAtomQuery(); + + $manipulates = []; + $locked_tables = []; + foreach ($this->inserts as $statement) { + $table_to_lock = $statement->getTableToLock(); + if (!in_array($table_to_lock, $locked_tables)) { + $atom_query->addTableLock($table_to_lock); + $locked_tables[] = $table_to_lock; + } + $manipulates[] = $statement->toManipulateString($this->db); + } + $atom_query->addQueryCallable( + function (\ilDBInterface $db) use ($manipulates): void { + foreach ($manipulates as $manipulate) { + $db->manipulate($manipulate); + } + } + ); + $atom_query->run(); + } + + private function buildCoreAnswerFormInsertStatement(): Insert + { + return new Insert( + CoreTables::AnswerForms->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->definition_class), + new Value(\ilDBConstants::T_TEXT, $this->new_question_id->toString()), + new Value(\ilDBConstants::T_FLOAT, $this->available_points), + new Value(\ilDBConstants::T_INTEGER, $this->image_size), + new Value( + \ilDBConstants::T_INTEGER, + $this->shuffle_answer_options === null + ? null + : ($this->shuffle_answer_options ? 1 : 0) + ), + new Value(\ilDBConstants::T_TEXT, $this->additional_text), + new Value(\ilDBConstants::T_TEXT, $this->additional_text_legacy) + + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php index c12d6eccaded..da1a89a9da46 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -28,7 +28,7 @@ interface Persistence { - public function getPublicNameSpace(): TableNameSpace; + public function getTableNameSpace(): TableNameSpace; public function getColumns( TableNameBuilder $table_name_builder, diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index 7f00a69637a8..16c5b5c5a19d 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -35,7 +35,7 @@ public function getAnswerFormId(): Uuid; public function getQuestionId(): Uuid; - public function getDefinition(): Definition; + public function getDefinition(): ?Definition; public function getTypeGenericProperties(): TypeGenericProperties; diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 2300fa1a5634..9f924b9b78d3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -37,11 +37,11 @@ public function __construct( private readonly Uuid $answer_form_id, private readonly Uuid $question_id, private readonly ?Definition $definition = null, - private ?float $available_points = null, - private ?int $image_size = null, - private ?bool $shuffle_answer_options = null, - private string $additional_text = '', - private string $additional_text_legacy = '' + private readonly ?float $available_points = null, + private readonly ?int $image_size = null, + private readonly ?bool $shuffle_answer_options = null, + private readonly string $additional_text = '', + private readonly string $additional_text_legacy = '' ) { } @@ -55,7 +55,7 @@ public function getQuestionId(): Uuid return $this->question_id; } - public function getDefinition(): Definition + public function getDefinition(): ?Definition { return $this->definition; } @@ -131,7 +131,7 @@ private function buildInsertStatement(): Insert new Value(\ilDBConstants::T_TEXT, $this->question_id->toString()), new Value(\ilDBConstants::T_FLOAT, $this->available_points), new Value(\ilDBConstants::T_INTEGER, $this->image_size), - new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0), + new Value(\ilDBConstants::T_INTEGER, $this->getShuffleAnswerOptionsForStorage()), new Value(\ilDBConstants::T_TEXT, $this->additional_text), new Value(\ilDBConstants::T_TEXT, $this->additional_text_legacy) @@ -152,7 +152,7 @@ private function buildUpdateStatement(): Update [ new Value(\ilDBConstants::T_FLOAT, $this->available_points), new Value(\ilDBConstants::T_INTEGER, $this->image_size), - new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0), + new Value(\ilDBConstants::T_INTEGER, $this->getShuffleAnswerOptionsForStorage()), new Value(\ilDBConstants::T_TEXT, $this->additional_text) ], @@ -167,4 +167,13 @@ private function buildUpdateStatement(): Update ] ); } + + private function getShuffleAnswerOptionsForStorage(): ?int + { + if ($this->shuffle_answer_options === null) { + return null; + } + + return $this->shuffle_answer_options ? 1 : 0; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php new file mode 100644 index 000000000000..440884a95599 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -0,0 +1,82 @@ +render($this->buildContent()); + } + + private function buildContent(): array + { + return [ + $this->buildBasicAnswerFormPanel(), + $this->environment->getAnswerFormProperties()->getOverviewTable( + $this->ui_factory->table(), + $this->lng, + $this->request, + $this->environment + ) + ]; + } + + private function buildBasicAnswerFormPanel(): StandardPanel + { + $content = [ + $this->ui_factory->listing()->descriptive( + $this->environment->getAnswerFormProperties()->getBasicPropertiesForListing($this->lng) + ) + ]; + + if ($this->environment->getEditability() === Editability::Full) { + $content[] = $this->ui_factory->button()->standard( + $this->lng->txt('edit_basic_answer_form_properties'), + $this->uri_to_edit_basic_answer_form_properties->__toString() + ); + } + + return $this->ui_factory->panel()->standard( + $this->lng->txt('basic_answer_form_properites'), + $content + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php new file mode 100644 index 000000000000..a2bc23fbc70d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php @@ -0,0 +1,106 @@ +ui_factory, + $this->lng, + $this->http->request(), + $environment, + $uri_to_edit_basic_answer_form_properties + ); + } + + public function getEditForm( + URLBuilder $url_builder, + Section $main_section_inputs, + bool $is_final_step, + ?Group $carry_inputs = null + ): EditForm { + return new EditForm( + $this->ui_factory->input()->container()->form(), + $this->lng, + $url_builder, + $main_section_inputs, + $is_final_step, + $carry_inputs + ); + } + + public function getAsync( + InterruptiveModal|RoundTripModal|MessageBox $content + ): Async { + return new Async( + $this->http, + $content + ); + } + + public function getCarrySectionData( + ArrayBasedRequestWrapper $post_wrapper, + Refinery $refinery + ): CarryWrapper { + return new CarryWrapper( + array_reduce( + $post_wrapper->keys(), + function (array $c, string $v) use ($post_wrapper, $refinery): array { + $value = new Leaf( + $post_wrapper->retrieve($v, $refinery->identity()) + ); + foreach (array_reverse(explode('/', $v)) as $path_element) { + $value = [$path_element => $value]; + } + return array_merge_recursive($c, $value); + }, + [] + )['form'][EditForm::CARRY_SECTION_NAME] ?? [] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php new file mode 100644 index 000000000000..c5b9d830b705 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -0,0 +1,153 @@ +getColumns( + $table_name_builder, + TableTypes::AnswerInputs + ), + [ + new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $position), + new Value(\ilDBConstants::T_TEXT, $gap_type), + new Value(\ilDBConstants::T_INTEGER, $max_chars), + new Value(\ilDBConstants::T_FLOAT, $step_size), + new Value(\ilDBConstants::T_INTEGER, $matching_options?->value), + new Value(\ilDBConstants::T_INTEGER, $min_autocomplete), + new Value(\ilDBConstants::T_INTEGER, $shuffle) + ] + ); + } + + private function buildAnswerOptionInsertStatement( + Persistence $persistence, + TableNameBuilder $table_name_builder, + Uuid $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + float $points, + ?float $lower_limit, + ?float $upper_limit + ): Insert { + + return new Insert( + $persistence->getColumns( + $table_name_builder, + TableTypes::AnswerOptions + ), + [ + new Value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), + new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $position), + new Value(\ilDBConstants::T_TEXT, $text_value), + new Value(\ilDBConstants::T_FLOAT, $points), + new Value(\ilDBConstants::T_FLOAT, $lower_limit), + new Value( + \ilDBConstants::T_FLOAT, + $lower_limit !== $upper_limit + ? $upper_limit + : null + ) + ] + ); + } + + private function buildAnswerFormInsertStatement( + Persistence $persistence, + TableNameBuilder $table_name_builder, + Uuid $answer_form_id, + ScoringIdentical $scoring_identical, + int $combinations_enabled + ): Insert { + return new Insert( + $persistence->getColumns( + $table_name_builder, + TableTypes::TypeSpecificAnswerForms + ), + [ + new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + new Value(\ilDBConstants::T_TEXT, $scoring_identical->value), + new Value(\ilDBConstants::T_INTEGER, $combinations_enabled) + ] + ); + } + + private function buildScoringIdenticalFromOld( + int $scoring_identical + ): ScoringIdentical { + if ($scoring_identical === '1') { + return ScoringIdentical::ScoreAll; + } + + return ScoringIdentical::OnlyScoreDistinct; + } + + private function replaceGapsAndSantizeLegacyClozeText( + string $gap_replace_regex, + string $text, + array $gaps_mapping + ): string { + ksort($gaps_mapping); + + return mb_ereg_replace_callback( + $gap_replace_regex, + function (array $matches) use (&$gaps_mapping): string { + return '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gaps_mapping) . '}}'; + }, + $text + ); + } + + private function limitToFloat( + \EvalMath $math, + string $limit + ): float { + return (float) $math->e($limit); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php new file mode 100644 index 000000000000..6c5adb72b8f9 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -0,0 +1,163 @@ +persistence->getTableNameSpace(); + } + + #[\Override] + public function buildInsertStatement( + MigrationInsert $migration_insert + ): MigrationInsert { + $answer_input_mapping = []; + foreach ($this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + $answer_form_id = $migration_insert->getAnswerFormId(); + if (!isset($answer_input_mapping[$db_row->gap_id])) { + $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); + $migration_insert = $migration_insert->withAdditionalInsert( + $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_input_mapping[$db_row->gap_id], + $answer_form_id, + $db_row->gap_id, + $this->buildNewGapTypeIdentifierFromOld((int) $db_row->cloze_type), + null, + null, + $this->buildNewTextRatingFromOld($db_row->textgap_rating), + null, + $db_row->shuffle === '1' ? 1 : 0 + ) + ); + } + + $migration_insert = $migration_insert->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $migration_insert->getUuid(), + $answer_input_mapping[$db_row->gap_id], + $db_row->aorder, + $db_row->answertext, + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) + ) + ); + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_form_id, + $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), + $db_row->combinations_enabled + ) + )->withAdditionalTextLegacy( + $this->replaceGapsAndSantizeLegacyClozeText( + '\[gap\].+?\[\/gap\]', + $db_row->cloze_text, + $answer_input_mapping + ) + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT *, EXISTS (' . PHP_EOL + . 'SELECT gap_fi FROM qpl_a_cloze_combi_res' . PHP_EOL + . 'WHERE question_fi = a.question_fi' . PHP_EOL + . ') combinations_enabled' . PHP_EOL + . 'FROM qpl_qst_cloze q' . PHP_EOL + . 'JOIN qpl_a_cloze a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY a.gap_id, a.aorder' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function buildNewGapTypeIdentifierFromOld( + int $old_gap_type + ): string { + return match($old_gap_type) { + \assClozeGap::TYPE_TEXT => 'text', + \assClozeGap::TYPE_SELECT => 'select', + \assClozeGap::TYPE_NUMERIC => 'numeric' + }; + } + + private function buildNewTextRatingFromOld( + string $old_text_rating + ): TextMatchingOptions { + return match($old_text_rating) { + \assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE => TextMatchingOptions::CaseInsensitive, + \assClozeGap::TEXTGAP_RATING_CASESENSITIVE => TextMatchingOptions::CaseSensitive, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1 => TextMatchingOptions::Levenstein1, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2 => TextMatchingOptions::Levenstein2, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3 => TextMatchingOptions::Levenstein3, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4 => TextMatchingOptions::Levenstein4, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => TextMatchingOptions::Levenstein5 + }; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php new file mode 100644 index 000000000000..59b622f36ed3 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -0,0 +1,178 @@ +persistence->getTableNameSpace(); + } + + #[\Override] + public function buildInsertStatement( + MigrationInsert $migration_insert + ): MigrationInsert { + $answer_input_mapping = []; + + foreach ($this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + $answer_form_id = $migration_insert->getAnswerFormId(); + if (!isset($answer_input_mapping[$db_row->gap_number])) { + $answer_input_mapping[$db_row->gap_number] = $migration_insert->getUuid(); + + $migration_insert = $migration_insert->withAdditionalInsert( + $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_input_mapping[$db_row->gap_number], + $answer_form_id, + $db_row->gap_number, + $this->buildNewGapTypeIdentifierFromOld($db_row->type), + null, + null, + null, + $db_row->min_autocomplete, + $db_row->shuffle_answers === '1' ? 1 : 0 + ) + ); + + $answers = array_map( + fn(string $v) => [ + 'answer_input_id' => $migration_insert->getUuid(), + 'text' => trim($v), + 'points' => 0.0 + ], + $this->loadAnswersFromFile( + $migration_insert->getOldQuestionId(), + $db_row->gap_number + ) + ); + } + + if ($answers[$db_row->position]['text'] === trim($db_row->answer_text)) { + $answers[$db_row->position]['points'] = $db_row->points; + } + } + + foreach ($answers as $position => $answer) { + $migration_insert = $migration_insert->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer['answer_input_id'], + $answer_input_mapping[$db_row->gap_number], + $position, + $answer['text'], + $answer['points'], + null, + null + ) + ); + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_form_id, + $this->buildScoringIdenticalFromOld($db_row->identical_scoring), + 0 + ) + )->withAdditionalTextLegacy( + $this->replaceGapsAndSantizeLegacyClozeText( + '\[Longmenu \d+\]', + $db_row->cloze_text, + $answer_input_mapping + ) + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT * FROM qpl_qst_lome q' . PHP_EOL + . 'JOIN qpl_a_lome a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY a.gap_number, a.position' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function loadAnswersFromFile( + int $old_question_id, + int $gap_id + ): array { + $file = ilFileUtils::getDataDir() . "/assessment/longMenuQuestion/{$old_question_id}/{$gap_id}.txt"; + + if (!file_exists($file)) { + return []; + } + + return exlode( + "\n", + file_get_contents($file) + ); + } + + private function buildNewGapTypeIdentifierFromOld( + int $old_gap_type + ): string { + return match($old_gap_type) { + \assLongMenu::ANSWER_TYPE_TEXT_VAL => 'long_menu', + \assLongMenu::ANSWER_TYPE_SELECT_VAL => 'select' + }; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php new file mode 100644 index 000000000000..ad9825006ed1 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -0,0 +1,122 @@ +math->suppress_errors = true; + } + + #[\Override] + public function getOldQuestionIdentifier(): string + { + return 'assNumeric'; + } + + #[\Override] + public function getDefinitionClass(): string + { + return Definition::class; + } + + #[\Override] + public function getTableNameSpace(): TableNameSpace + { + return $this->persistence->getTableNameSpace(); + } + + #[\Override] + public function buildInsertStatement( + MigrationInsert $migration_insert + ): MigrationInsert { + $db_row = $this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + )->current(); + + $answer_form_id = $migration_insert->getAnswerFormId(); + $gap_id = $migration_insert->getUuid(); + return $migration_insert->withAdditionalInsert( + $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $gap_id, + $answer_form_id, + 0, + 'numeric', + null, + 0.0001, + null, + null, + null + ) + )->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $migration_insert->getUuid(), + $gap_id, + 0, + '', + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) + ) + )->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_form_id, + ScoringIdentical::ScoreAll, + 0 + ) + )->withAdditionalTextLegacy( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gap_id->toString()) . '}}' + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT points, lowerlimit, upperlimit FROM qpl_num_range' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php deleted file mode 100644 index 10d11263beac..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/MigrationCloze.php +++ /dev/null @@ -1,40 +0,0 @@ -table_namespace; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php new file mode 100644 index 000000000000..64eb908e2555 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -0,0 +1,352 @@ +getStep(); + + return match($step) { + '' => $this->buildBasicEditingForm($environment), + default => $this->callIntermediateStep($environment, $step) + }; + } + + #[\Override] + public function edit( + Environment $environment + ): EditOverview|EditForm|Properties { + $step = $environment->getStep(); + + if ($step === '') { + return $environment->getPresentationFactory()->getEditOverview( + $environment, + $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + ->buildURI() + ); + } + + $environment->setEditAnswerFormBackTarget(); + + return match ($step) { + self::STEP_EDIT_BASIC_PROPERTIES => $this->buildBasicEditingForm($environment), + default => $this->callIntermediateStep($environment, $step) + }; + } + + #[\Override] + public function other( + Environment $environment + ): EditForm|Properties { + + } + + private function callIntermediateStep( + Environment $environment, + string $step + ): EditForm|Properties { + $initialized_environment = $environment->withPreservedTableRowIdsParameter(); + + if ($step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + && $step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + && $step !== self::STEP_JUMP_TO_SET_POINTS) { + $initialized_environment = $initialized_environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withValuesFromCarry( + $this->refinery, + $this->cloze_text_factory, + $this->gap_factory, + $environment->getPresentationFactory()->getCarrySectionData( + $this->http->wrapper()->post(), + $this->refinery + ) + ) + ); + } + + return match ($step) { + self::STEP_SET_GAP_TYPES, + self::STEP_CONFIRMED_GAP_REMOVAL => $this->processBasicEditingForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesForm( + $initialized_environment + ), + self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsForm( + $initialized_environment + ), + self::STEP_SET_POINTS => $this->processAnswerOptionsForm( + $initialized_environment + ), + self::STEP_JUMP_TO_SET_POINTS => $this->buildAssignPointsForm( + $initialized_environment + ), + self::STEP_SAVE => $this->processAssignPointsForm( + $initialized_environment + ) + }; + } + + private function buildBasicEditingForm( + Environment $environment + ): EditForm { + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES), + $environment->getAnswerFormProperties()->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory + ), + false + ); + } + + private function processBasicEditingForm( + Environment $environment + ): EditForm|Properties { + $form = $this->buildBasicEditingForm( + $environment + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + return $form; + } + + $new_gaps = $data->getGaps(); + $old_gaps = $environment->getAnswerFormProperties()->getGaps(); + + if ($environment->getStep() !== self::STEP_CONFIRMED_GAP_REMOVAL) { + $removed_gaps = $new_gaps->getRemovedGaps($old_gaps); + if ($removed_gaps !== []) { + return $form->withConfirmation( + $this->buildRemovedGapsConfirmation( + $environment, + $removed_gaps + ) + ); + } + } + + if ($new_gaps->getAddedGaps($old_gaps) === []) { + return $data; + } + + return $this->buildGapTypesForm( + $environment->withAnswerFormProperties($data) + ); + } + + private function buildGapTypesForm( + Environment $environment + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + $ff = $this->ui_factory->input()->field(); + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS), + $properties->getGaps()->buildGapsTypeInputs( + $this->lng, + $ff, + $this->refinery, + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $environment->getTableRowIds() + ), + false, + $properties->withClozeText($properties->getClozeText()) + ->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); + } + + private function processGapTypesForm( + Environment $environment + ): EditForm { + $form = $this->buildGapTypesForm( + $environment + )->withRequest($this->http->request()); + + $data = $form->getData(); + return $data === null + ? $form + : $this->buildAnswerOptionsForm( + $environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withGaps($data) + ) + ); + } + + private function buildAnswerOptionsForm( + Environment $environment + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + $ff = $this->ui_factory->input()->field(); + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS), + $properties->getGaps()->buildAnswerOptionsInputs( + $this->lng, + $ff, + $this->refinery, + $environment->getTableRowIds() + ), + false, + $properties->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); + } + + private function processAnswerOptionsForm( + Environment $environment + ): EditForm { + $form = $this->buildAnswerOptionsForm( + $environment + )->withRequest($this->http->request()); + + $data = $form->getData(); + return $data === null + ? $form + : $this->buildAssignPointsForm( + $environment->withAnswerFormProperties( + $environment->getAnswerFormProperties()->withGaps($data) + ) + ); + } + + private function buildAssignPointsForm( + Environment $environment + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + $ff = $this->ui_factory->input()->field(); + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::STEP_SAVE), + $properties->getGaps()->buildPointInputs( + $this->lng, + $ff, + $this->refinery, + $environment->getTableRowIds() + ), + true, + $properties->buildCarryInputs($ff) + )->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ); + } + + private function processAssignPointsForm( + Environment $environment + ): EditForm|Properties { + $form = $this->buildAssignPointsForm( + $environment + )->withRequest($this->http->request()); + + $properties = $environment->getAnswerFormProperties(); + $data = $form->getData(); + return $data === null + ? $form->withContentBeforeForm( + $this->buildClozeTextPanel($properties) + ) : $properties->withGaps($data); + } + + private function buildClozeTextPanel( + Properties $properties + ): StandardPanel { + return $this->ui_factory->panel()->standard( + $this->lng->txt('cloze_text'), + $this->ui_factory->legacy()->content( + $properties->getClozeText()->getRenderedMarkdownForEditingPresentation( + $properties->getGaps() + ) + ) + ); + } + + /** + * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap> $removed_gaps + */ + private function buildRemovedGapsConfirmation( + Environment $environment, + array $removed_gaps + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('confirm_remove_gaps'), + $environment->getUrlBuilderWithStepParameter( + self::STEP_CONFIRMED_GAP_REMOVAL + )->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() + ->interruptiveItem()->standard( + $v->getAnswerInputId()->toString(), + $v->buildShortenedGapName() + ), + $removed_gaps + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php new file mode 100644 index 000000000000..103012a35f75 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -0,0 +1,108 @@ +getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + $this->gaps_factory->getEmptyGapsObject( + $type_generic_properties->getAnswerFormId() + ) + ); + } + + [ + 'scoring_identical_responses' => $scoring_identical_responses, + 'combinations_activated' => $combinations_activated + ] = $query->retrieveCurrentRecord( + TableTypes::TypeSpecificAnswerForms->getTable( + $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) + ), + $query->getRefinery()->custom()->transformation( + fn(array $vs): array => [ + 'scoring_identical_responses' => ScoringIdentical::tryFrom($vs[0]['scoring_identical_responses']), + 'combinations_activated' => $vs[0]['combinations_activated'] === 1 + ] + ) + ); + + return new Properties( + $type_generic_properties->getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + $this->gaps_factory->fromDatabase( + $type_generic_properties->getAnswerFormId(), + $query + ), + $scoring_identical_responses, + $combinations_activated + ); + } + + public function fromForm( + Properties $properties, + ClozeText $cloze_text, + ScoringIdentical $scoring_of_identical_responses, + bool $combinations_enabled + ): Properties { + $updated_gaps = $cloze_text->updateGapsFromMarkdown( + $properties->getAnswerFormId(), + $properties->getGaps() + ); + + return $properties + ->withClozeText( + $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) + )->withGaps($updated_gaps) + ->withScoringOfIdenticalResponses($scoring_of_identical_responses) + ->withCombinationsEnabled($combinations_enabled); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 103012a35f75..9df088c63851 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -57,16 +57,22 @@ public function fromData( [ 'scoring_identical_responses' => $scoring_identical_responses, - 'combinations_activated' => $combinations_activated + 'combinations_enabled' => $combinations_enabled ] = $query->retrieveCurrentRecord( TableTypes::TypeSpecificAnswerForms->getTable( $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) ), $query->getRefinery()->custom()->transformation( - fn(array $vs): array => [ - 'scoring_identical_responses' => ScoringIdentical::tryFrom($vs[0]['scoring_identical_responses']), - 'combinations_activated' => $vs[0]['combinations_activated'] === 1 - ] + function (array $vs) use ($type_generic_properties): array { + $values = $this->retrieveFirstMatchingRowFromDBRecords( + $type_generic_properties->getAnswerFormId()->toString(), + $vs + ); + return [ + 'scoring_identical_responses' => ScoringIdentical::tryFrom($values['scoring_identical_responses']), + 'combinations_enabled' => $values['combinations_enabled'] === 1 + ]; + } ) ); @@ -83,7 +89,7 @@ public function fromData( $query ), $scoring_identical_responses, - $combinations_activated + $combinations_enabled ); } @@ -105,4 +111,16 @@ public function fromForm( ->withScoringOfIdenticalResponses($scoring_of_identical_responses) ->withCombinationsEnabled($combinations_enabled); } + + private function retrieveFirstMatchingRowFromDBRecords( + string $answer_form_id, + array $vs + ): ?array { + foreach ($vs as $row) { + if ($row['answer_form_id'] === $answer_form_id) { + return $row; + } + } + return null; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index 4452b2a7d823..ff0000cdc243 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -40,7 +40,7 @@ public function __construct( private string $text_value = '', private ?float $lower_limit = null, private ?float $upper_limit = null, - private ?float $available_points = null + private float $available_points = 0.0 ) { } @@ -101,13 +101,13 @@ public function withUpperLimit( return $clone; } - public function getAvailablePoints(): ?float + public function getAvailablePoints(): float { return $this->available_points; } public function withAvailablePoints( - ?float $available_points + float $available_points ): self { $clone = clone $this; $clone->available_points = $available_points; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index cce432c55096..2dcfeae21079 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -24,7 +24,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Factory as AnswerOptionsFactory; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Question\Definitions\TextMatchingOptions; +use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; use ILIAS\Data\UUID\Factory as UuidFactory; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 5a2af5208b82..b8ab11b85caa 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -22,12 +22,12 @@ use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Presentation\Definitions\CarryWrapper; -use ILIAS\Questions\Question\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -370,7 +370,12 @@ private function buildValuesForGapReplace(): array new Value(\ilDBConstants::T_FLOAT, $this->step_size), new Value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), new Value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), - new Value(\ilDBConstants::T_INTEGER, $this->shuffle_answer_options ? 1 : 0) + new Value( + \ilDBConstants::T_INTEGER, + $this->shuffle_answer_options === null + ? null + : ($this->shuffle_answer_options ? 1 : 0) + ) ]; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 772fa189e2b7..8aa75827cdf8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -38,7 +38,6 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Table\DataRowBuilder; -use ILIAS\UICore\GlobalTemplate; class Gaps { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 5fcf987685ac..7ef1fc75d296 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -22,7 +22,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\Question\Definitions\TextMatchingOptions; +use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 7db9cb5a3cdc..caefc207870f 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -87,7 +87,7 @@ public function getQuestionId(): Uuid } #[\Override] - public function getDefinition(): Definition + public function getDefinition(): ?Definition { return $this->definition; } @@ -127,7 +127,7 @@ public function getLegacyClozeText(): string public function getClozeTextForPresentation(): string { - return $this->cloze_text === null + return $this->cloze_text->getRawRepresentationForPersistence() === '' ? $this->legacy_cloze_text : $this->cloze_text->getRenderedMarkdownForParticipantPresentation(); } @@ -179,7 +179,10 @@ public function getBasicPropertiesForListing( $lng->txt('cloze_text') => $this->cloze_text ->getRenderedMarkdownForEditingPresentation($this->gaps), $lng->txt('score_identical') => $this->scoring_identical - ->getTranslatedOptionName($lng) + ->getTranslatedOptionName($lng), + $lng->txt('gap_combinations') => $this->combinations_enabled + ? $lng->txt('enabled') + : $lng->txt('disabled') ]; } diff --git a/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php b/components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php similarity index 88% rename from components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php rename to components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php index f3debbb9aca0..c4c76919e765 100644 --- a/components/ILIAS/Questions/src/Question/Definitions/TextMatchingOptions.php +++ b/components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php @@ -18,14 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question\Definitions; +namespace ILIAS\Questions\Definitions; use ILIAS\Language\Language; enum TextMatchingOptions: string { - case CaseInsensitive = 'cloze_textgap_case_insensitive'; - case CaseSensitive = 'cloze_textgap_case_sensitive'; + case CaseInsensitive = 'case_insensitive'; + case CaseSensitive = 'case_sensitive'; case Levenstein1 = '1'; case Levenstein2 = '2'; case Levenstein3 = '3'; @@ -36,7 +36,7 @@ public function getLabel( Language $lng ): string { return match ($this) { - self::CaseInsensitive, self::CaseSensitive => $lng->txt($this->value), + self::CaseInsensitive, self::CaseSensitive => $lng->txt("cloze_textgap_{$this->value}"), default => sprintf($lng->txt('cloze_textgap_levenshtein_of'), $this->value) }; } diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php index 81cb668be3c5..d06a76b8a4b5 100644 --- a/components/ILIAS/Questions/src/Persistence/CoreTables.php +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -22,14 +22,6 @@ enum CoreTables: string { - public const string LINKING_TABLE_ID_COLUMN = 'question_id'; - private const string LINKING_TABLE_FOREIGN_KEY_COLUMN = 'obj_id'; - private const array LINKING_TABLE_COLUMNS = [ - 'question_id', - 'obj_id', - 'position' - ]; - private const string QUESTION_TABLE_ID_COLUMN = 'id'; private const array QUESTION_TABLE_COLUMNS = [ 'id', @@ -56,11 +48,26 @@ enum CoreTables: string 'additional_text_legacy' ]; + public const string LINKING_TABLE_ID_COLUMN = 'question_id'; + private const string LINKING_TABLE_FOREIGN_KEY_COLUMN = 'obj_id'; + private const array LINKING_TABLE_COLUMNS = [ + 'question_id', + 'obj_id', + 'position' + ]; + + public const string MIGRATIONS_TABLE_ID_COLUMN = 'new_question_id'; + private const string MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN = 'old_question_id'; + private const array MIGRATIONS_TABLE_COLUMNS = [ + 'old_question_id', + 'new_question_id' + ]; + case Questions = 'qsts_questions'; case AnswerForms = 'qsts_answer_forms'; case Responses = 'qsts_responses'; case Linking = 'qsts_linking'; - case PageEditor = 'page_object'; + case MigrationsTable = 'qsts_migrations'; public function getTable(): Table { @@ -72,9 +79,10 @@ public function getColumns( ): array { $table = $this->getTable(); $column_identifiers = match($this) { - self::Linking => self::LINKING_TABLE_COLUMNS, self::Questions => self::QUESTION_TABLE_COLUMNS, - self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS + self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, + self::Linking => self::LINKING_TABLE_COLUMNS, + self::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS }; return array_map( fn(string $v): Column => new Column($table, $v), @@ -90,10 +98,6 @@ public function getColumns( public function getIdColumn(): Column { return match($this) { - self::Linking => new Column( - $this->getTable(), - self::LINKING_TABLE_ID_COLUMN - ), self::Questions => new Column( $this->getTable(), self::QUESTION_TABLE_ID_COLUMN @@ -101,6 +105,14 @@ public function getIdColumn(): Column self::AnswerForms => new Column( $this->getTable(), self::ANSWER_FORM_TABLE_ID_COLUMN + ), + self::Linking => new Column( + $this->getTable(), + self::LINKING_TABLE_ID_COLUMN + ), + self::MigrationsTable => new Column( + $this->getTable(), + self::MIGRATIONS_TABLE_ID_COLUMN ) }; } @@ -108,13 +120,17 @@ public function getIdColumn(): Column public function getForeignKeyColumn(): ?Column { return match($this) { + self::AnswerForms => new Column( + $this->getTable(), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ), self::Linking => new Column( $this->getTable(), self::LINKING_TABLE_FOREIGN_KEY_COLUMN ), - self::AnswerForms => new Column( + self::MigrationsTable => new Column( $this->getTable(), - self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN ), default => null }; diff --git a/components/ILIAS/Questions/src/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php index 6a9388d11183..baaf5340e61b 100644 --- a/components/ILIAS/Questions/src/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -54,7 +54,7 @@ public function getTableNameBuilder( $this->answer_form_factory ->getDefinitionForClass($definition_class) ->getPersistence() - ->getPublicNameSpace() + ->getTableNameSpace() ); } diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index f03a1ca646bd..54dc4463ee86 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -96,7 +96,7 @@ public function getTableNameBuilder( $this->answer_form_factory ->getDefinitionForClass($definition_class) ->getPersistence() - ->getPublicNameSpace() + ->getTableNameSpace() ); } diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 998fc739d62d..1b9f1f8a2bd0 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -24,6 +24,7 @@ use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Setup\QuestionsMigration; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; @@ -197,7 +198,7 @@ private function retrieveQuestionFromQuery( $this->refinery->identity() ); - return $query->retrieveCurrentRecord( + $question = $query->retrieveCurrentRecord( CoreTables::Questions->getTable(), $this->refinery->custom()->transformation( fn(array $vs): QuestionImplementation => new QuestionImplementation( @@ -218,6 +219,12 @@ private function retrieveQuestionFromQuery( ) ) ); + + if ($question->getPageId() !== 0) { + return $question; + } + + return $this->migrateQuestionPage($question); } private function retrieveAnswerFormsFromQuery( @@ -301,8 +308,8 @@ private function getNextAvailableQuestionPageId(): int $last_id = $this->db->fetchObject( $this->db->query( - 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value - . ' WHERE parent_type = "qsts"' + 'SELECT MAX(page_id) AS last FROM page_object ' + . 'WHERE parent_type = "qsts"' ) )->last; if ($last_id === null) { @@ -311,4 +318,30 @@ private function getNextAvailableQuestionPageId(): int return $last_id + 1; } + + private function migrateQuestionPage( + QuestionImplementation $question + ): QuestionImplementation { + $old_page_id = $this->db->fetchObject( + $this->db->query( + 'SELECT old_question_id FROM ' . CoreTables::MigrationsTable->value . PHP_EOL + . "WHERE new_question_id = {$this->db->quote($question->getId(), \ilDBConstants::T_TEXT)}" + ) + )->old_question_id; + + $new_page_id = $this->getNextAvailableQuestionPageId(); + $old_qsts_page = new \ilAssQuestionPage($old_page_id); + $old_qsts_page->copyToAnswerForm($new_page_id, $question); + + $new_qsts_page = new \QstsQuestionPage($new_page_id); + $new_qsts_page->setQuestion($question); + $new_qsts_page->buildDom(); + $new_qsts_page->migrateQuestionElementToAnswerForm(); + + $new_question = $question->withPageId($new_page_id); + + $this->update([$new_question]); + + return $new_question; + } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php new file mode 100644 index 000000000000..cad255b94886 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php @@ -0,0 +1,158 @@ +form = $this->buildForm(); + } + + public function withContentBeforeForm( + StandardPanel $content + ): self { + $clone = clone $this; + $clone->content_before_form = $content; + return $clone; + } + + public function withContentAfterForm( + StandardPanel $content + ): self { + $clone = clone $this; + $clone->content_after_form = $content; + return $clone; + } + + public function withConfirmation( + InterruptiveModal $confirmation_modal + ): self { + $clone = clone $this; + $clone->confirmation = $confirmation_modal; + return $clone; + } + + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); + } + + public function withRequest( + ServerRequestInterface $request + ): self { + $clone = clone $this; + $clone->form = $clone->form->withRequest($request); + return $clone; + } + + public function getData(): mixed + { + $data = $this->form->getData(); + return $data[self::MAIN_SECTION_NAME] ?? null; + } + + private function buildContent(): array + { + $content = []; + + if ($this->content_before_form !== null) { + $content[] = $this->content_before_form; + } + + if ($this->confirmation !== null) { + $content[] = $this->confirmation->withOnLoad( + $this->confirmation->getShowSignal() + )->withAdditionalOnLoadCode( + function ($id) { + return "var button = {$id}.querySelector('input[type=\"submit\"]'); " + . "button.addEventListener('click', (e) => {e.preventDefault();" + . 'const form = button.closest("dialog").nextElementSibling;' + . "form.action = '{$this->confirmation->getFormAction()}';" + . 'form.submit();});'; + } + ); + } + + $content[] = $this->form; + + if ($this->content_after_form !== null) { + $content[] = $this->content_after_form; + } + + return $content; + } + + private function buildForm(): StandardForm + { + $form = $this->form_factory->standard( + $this->url_builder->buildURI()->__toString(), + $this->buildFormInputs() + ); + + if ($this->is_final_step) { + return $form->withSubmitLabel($this->lng->txt('save')); + } + + return $form->withSubmitLabel($this->lng->txt('next')); + } + + private function buildFormInputs(): array + { + $form_inputs = [ + self::MAIN_SECTION_NAME => $this->main_section_inputs + ]; + + if ($this->carry_inputs !== null) { + $form_inputs[self::CARRY_SECTION_NAME] = $this->carry_inputs + ->withDedicatedName(self::CARRY_SECTION_NAME); + } + + return $form_inputs; + } +} diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 42233dbe742b..5eba94dd88b1 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; @@ -45,6 +46,7 @@ class QuestionImplementation implements Question { private bool $linking_information_updated = false; private bool $self_updated = false; + private bool $page_id_updated = false; private array $updated_answer_forms = []; private array $deleted_answer_forms = []; @@ -117,7 +119,7 @@ public function withPageId( ): self { $clone = clone $this; $clone->page_id = $page_id; - $clone->self_updated = true; + $clone->page_id_updated = true; return $clone; } @@ -367,6 +369,12 @@ private function addUpdateStatementsToManipulation( ); } + if ($this->page_id) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdatePageIdStatement() + ); + } + if ($this->deleted_answer_forms !== []) { $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate, @@ -500,4 +508,35 @@ private function buildDeleteQuestionStatement(): Delete ] ); } + + /** + * skergomard, 2026-01-26: This we only need while the migrations exist, after + * this a question MUST never change the page assigned to it after its creation! + */ + private function buildUpdatePageIdStatement(): Update + { + $questions_table_definition = CoreTables::Questions; + return new Update( + [ + new Column( + $questions_table_definition->getTable(), + 'page_id' + ), + new Column( + $questions_table_definition->getTable(), + 'last_update' + ) + ], + [ + new Value(\ilDBConstants::T_TEXT, $this->page_id), + new Value(\ilDBConstants::T_INTEGER, time()) + ], + [ + new Where( + $questions_table_definition->getIdColumn(), + new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index 91a85ec837f4..f9079f62090f 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -54,7 +54,7 @@ public function step_1(): void 'length' => 32, 'notnull' => true ], - 'combinations_activated' => [ + 'combinations_enabled' => [ 'type' => \ilDBConstants::T_INTEGER, 'length' => 1, 'notnull' => true @@ -163,7 +163,7 @@ public function step_3(): void ], 'points' => [ 'type' => \ilDBConstants::T_FLOAT, - 'notnull' => false + 'notnull' => true ] ]); } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index b128a071aa04..8f045de511d0 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -20,6 +20,8 @@ namespace ILIAS\Questions\Setup; +use ILIAS\Questions\Persistence\CoreTables; + class OverarchingQuestionTables implements \ilDatabaseUpdateSteps { protected \ilDBInterface $db; @@ -33,7 +35,7 @@ public function prepare( public function step_1(): void { - $table_name = 'qsts_questions'; + $table_name = CoreTables::Questions->value; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -91,7 +93,7 @@ public function step_1(): void public function step_2(): void { - $table_name = 'qsts_answer_forms'; + $table_name = CoreTables::AnswerForms->value; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -121,7 +123,7 @@ public function step_2(): void 'shuffle_answer_options' => [ 'type' => \ilDBConstants::T_INTEGER, 'length' => 1, - 'notnull' => true + 'notnull' => false ], 'additional_text' => [ 'type' => \ilDBConstants::T_CLOB, @@ -145,7 +147,7 @@ public function step_2(): void public function step_3(): void { - $table_name = 'qsts_responses'; + $table_name = CoreTables::Responses->value; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -176,7 +178,7 @@ public function step_3(): void public function step_4(): void { - $table_name = 'qsts_linking'; + $table_name = CoreTables::Linking->value; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'question_id' => [ @@ -208,8 +210,9 @@ public function step_4(): void public function step_5(): void { - if (!$this->db->tableExists(QuestionsMigration::MIGRATIONS_TABLE)) { - $this->db->createTable(QuestionsMigration::MIGRATIONS_TABLE, [ + $table_name = CoreTables::MigrationsTable->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ 'old_question_id' => [ 'type' => \ilDBConstants::T_TEXT, 'length' => 64, @@ -224,11 +227,11 @@ public function step_5(): void } if (!$this->db->primaryExistsByFields( - QuestionsMigration::MIGRATIONS_TABLE, + $table_name, ['old_question_id'] )) { $this->db->addPrimaryKey( - QuestionsMigration::MIGRATIONS_TABLE, + $table_name, ['old_question_id'] ); } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 9ffadd2ff32d..a07550d2838b 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -20,33 +20,46 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\AnswerForm\Migration as AnswerFormMigration; +use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; +use ILIAS\Questions\AnswerForm\Migration\MigrationInsert as AnswerFormMigrationInsert; +use ILIAS\Questions\Persistence\CoreTables; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Persistence\Insert; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\Value; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\Setup; use ILIAS\Setup\CLI\IOWrapper; use ILIAS\Setup\Environment; use ILIAS\Setup\Migration; -use ILIAS\DI\Container; class QuestionsMigration implements Migration { - private const string QUESTIONS_TABLE = 'qpl_questions'; + private const string OLD_QUESTIONS_TABLE = 'qpl_questions'; private const string TEST_QUESTIONS_SEQUENCE_TABLE = 'tst_test_question'; - public const string MIGRATIONS_TABLE = 'qsts_migrations'; private \ilDBInterface $db; private IOWrapper $io; private UuidFactory $uuid_factory; + private readonly array $answer_form_migrations; private bool $ilias_is_initialized = false; private ?array $question_to_learning_module_mapping = null; private ?array $allready_migrated_questions = null; + private ?array $allready_migrated_questions_in_qpls = null; public function __construct( - private readonly array $answer_form_migrations + array $answer_form_migrations ) { + $this->answer_form_migrations = array_reduce( + $answer_form_migrations, + function (array $c, AnswerFormMigration $v): array { + $c[$v->getOldQuestionIdentifier()] = $v; + return $c; + }, + [] + ); } #[\Override] @@ -93,7 +106,7 @@ public function step( $db_values = $this->fetchValidRecord(); if ($db_values->obj_fi === 0) { - $question->obj_fi = $this->getObjIdFromMapping($db_values->question_id); + $db_values->obj_fi = $this->getObjIdFromLearningModulMapping($db_values->question_id); } if ($db_values->obj_fi === null) { @@ -104,19 +117,40 @@ public function step( return; } - $question = new QuestionImplementation( - $this->uuid_factory->uuid4(), - $db_values->obj_fi, - $db_values->sequence, - $this->buildQuestionPage($db_values), - $db_values->title, - $db_values->author, - Lifecycle::tryFrom($db_values->lifecycle), - $db_values->description, - $db_values->original_id, - $db_values->created, - time() - ); + /** @var \ILIAS\Questions\AnswerForm\Migration\Migration $answer_form_migration */ + $answer_form_migration = $this->answer_form_migrations[$db_values->type_tag]; + + $new_question_id = $this->uuid_factory->uuid4(); + + $answer_form_migration->buildInsertStatement( + $this->buildMigrationInsert( + $answer_form_migration, + [ + $this->buildInsertLinkingStatement( + $new_question_id, + $db_values->obj_fi, + $db_values->sequence + ), + $this->buildInsertQuestionStatement( + $new_question_id, + $db_values->title, + $db_values->author, + Lifecycle::tryFrom($db_values->lifecycle), + $db_values->description, + $db_values->original_id, + $db_values->created + ), + $this->buildInsertMigrationStatement( + $db_values->question_id, + $new_question_id + ) + ], + $new_question_id, + $db_values + ) + )->run(); + + $this->io->inform($new_question_id->toString()); } #[\Override] @@ -124,9 +158,9 @@ public function getRemainingAmountOfSteps(): int { return $this->db->fetchObject( $this->db->query( - 'SELECT COUNT(question_id) cnt FROM ' . self::QUESTIONS_TABLE . ' q' . PHP_EOL + 'SELECT COUNT(question_id) cnt FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL - . 'LEFT JOIN ' . self::MIGRATIONS_TABLE . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL . 'WHERE t.type_tag IN (' . implode( ', ', @@ -141,12 +175,12 @@ public function getRemainingAmountOfSteps(): int )->cnt; } - private function fetchValidRecord(): array + private function fetchValidRecord(): \stdClass { - $query_string = 'SELECT q.*, t.sequence FROM ' . self::QUESTIONS_TABLE . ' q' . PHP_EOL + $query_string = 'SELECT q.*, t.type_tag, s.sequence FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL - . 'LEFT JOIN ' . self::MIGRATIONS_TABLE . ' m ON q.question_id = m.old_question_id' . PHP_EOL - . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' t ON q.question_id = t.question_fi' + . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' s ON q.question_id = s.question_fi' . PHP_EOL . 'WHERE t.type_tag IN (' . implode( ', ', @@ -156,7 +190,7 @@ private function fetchValidRecord(): array ) ) . ')' . PHP_EOL . 'AND q.complete = 1' . PHP_EOL - . 'AND m.old_question_id IS NULL' + . 'AND m.old_question_id IS NULL' . PHP_EOL . 'LIMIT 1'; do { @@ -165,19 +199,19 @@ private function fetchValidRecord(): array ); } while (!$this->areDbValuesValid($db_values)); - $db_values->original_id = $this->getNewQuestionIdForOld($db_values->original_id); + $db_values->original_id = $this->cleanupAndMigrateOriginalId($db_values->original_id); return $db_values; } private function areDbValuesValid( - array $db_values + \stdClass $db_values ): bool { if ($db_values->original_id === null) { return true; } if ($this->allready_migrated_questions === null) { - $this->allready_migrated_questions = $this->loadAlreadyMigratedQuestions(); + $this->loadAlreadyMigratedQuestions(); } if (isset($this->allready_migrated_questions[$db_values->original_id])) { @@ -187,51 +221,49 @@ private function areDbValuesValid( return false; } - private function getNewQuestionIdForOld( - ?int $question_id - ): ?uuid { - if ($question_id === null) { - return null; - } - - if ($this->allready_migrated_questions === null) { - $this->allready_migrated_questions = $this->loadAlreadyMigratedQuestions(); - } - - if (!isset($this->allready_migrated_questions[$question_id])) { + private function cleanupAndMigrateOriginalId( + ?int $original_id + ): ?Uuid { + if ($original_id === null + || in_array($this->allready_migrated_questions_in_qpls, $original_id)) { return null; } - return $this->uuid_factory->fromString( - $this->allready_migrated_questions[$question_id] + $this->allready_migrated_questions[$original_id] ); } - private function loadAlreadyMigratedQuestions(): array + private function loadAlreadyMigratedQuestions(): void { $query = $this->db->query( - 'SELECT * FROM ' . self::MIGRATIONS_TABLE + 'SELECT m.*, o.type FROM ' . CoreTables::MigrationsTable->value . ' m' . PHP_EOL + . 'JOIN ' . CoreTables::Linking->value . 'l' . PHP_EOL + . 'ON m.new_question_id = l.question_id' . PHP_EOL + . 'JOIN object_data o ON l.obj_id = o.obj_id' . PHP_EOL ); - $questions = []; + $this->allready_migrated_questions = []; + $this->allready_migrated_questions_in_qpls = []; while (($row = $this->db->fetchObject($query)) !== null) { - $questions[$row->old_question_id] = $row->new_question_id; + $this->allready_migrated_questions[$row->old_question_id] = $row->new_question_id; + if ($row->type === 'qpl') { + $this->allready_migrated_questions_in_qpls[] = $row->new_question_id; + } } - return $questions; } - private function getObjIdFromMapping( + private function getObjIdFromLearningModulMapping( int $question_id ): ?int { if ($this->question_to_learning_module_mapping === null) { - $this->question_to_learning_module_mapping = $this->loadQuestionsToLearningModuleMapping(); + $this->loadQuestionsToLearningModuleMapping(); } $this->question_to_learning_module_mapping[$question_id] ?? null; } - private function loadQuestionsToLearningModuleMapping(): array + private function loadQuestionsToLearningModuleMapping(): void { $query = $this->db->query( @@ -242,38 +274,83 @@ private function loadQuestionsToLearningModuleMapping(): array . 'WHERE page_parent_type = "lm"' ); - $mapping = []; + $this->question_to_learning_module_mapping = []; while (($row = $this->db->fetchObject($query)) !== null) { - $mapping[$row->question_id] = $row->obj_id; + $this->question_to_learning_module_mapping[$row->question_id] = $row->obj_id; } - return $mapping; } - private function buildQuestionPage( - - ): int { - $new_id = $this->getNextAvailableQuestionPageId(); - $page = new \ilAssQuestionPage(); - $page->copy( - $new_id, - 'qsts', + private function buildInsertLinkingStatement( + Uuid $new_question_id, + int $obj_id, + ?int $position + ): Insert { + return new Insert( + CoreTables::Linking->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $new_question_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $obj_id), + new Value(\ilDBConstants::T_INTEGER, $position) + ] ); - return $new_id; } - private function getNextAvailableQuestionPageId(): int - { + private function buildInsertQuestionStatement( + Uuid $id, + string $title, + string $author, + Lifecycle $lifecycle, + string $remarks, + ?Uuid $original_id, + int $create_date + ): Insert { + return new Insert( + CoreTables::Questions->getColumns(), + [ + new Value(\ilDBConstants::T_TEXT, $id->toString()), + new Value(\ilDBConstants::T_INTEGER, 0), + new Value(\ilDBConstants::T_TEXT, $title), + new Value(\ilDBConstants::T_TEXT, $author), + new Value(\ilDBConstants::T_TEXT, $lifecycle->value), + new Value(\ilDBConstants::T_TEXT, $remarks), + new Value(\ilDBConstants::T_TEXT, $original_id?->toString()), + new Value(\ilDBConstants::T_INTEGER, time()), + new Value(\ilDBConstants::T_INTEGER, $create_date) + ] + ); + } - $last_id = $this->db->fetchObject( - $this->db->query( - 'SELECT MAX(page_id) AS last FROM ' . CoreTables::PageEditor->value - . ' WHERE parent_type = "qsts"' - ) - )->last; - if ($last_id === null) { - return 1; - } + private function buildInsertMigrationStatement( + int $old_question_id, + Uuid $new_question_id + ): Insert { + return new Insert( + CoreTables::MigrationsTable->getColumns(), + [ + new Value(\ilDBConstants::T_INTEGER, $old_question_id), + new Value(\ilDBConstants::T_TEXT, $new_question_id->toString()) + ] + ); + } - return $last_id + 1; + private function buildMigrationInsert( + AnswerFormMigration $answer_form_migration, + array $question_inserts, + Uuid $new_question_id, + \stdClass $db_values + ): AnswerFormMigrationInsert { + return new AnswerFormMigrationInsert( + $this->db, + $this->io, + $this->uuid_factory, + new TableNameBuilder( + $answer_form_migration->getTableNameSpace() + ), + $question_inserts, + $db_values->question_id, + $new_question_id, + $this->uuid_factory->uuid4(), + $answer_form_migration->getDefinitionClass() + ); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index ba7b5c1ef0c2..78787cd9dbc1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -16,15 +16,10 @@ * *********************************************************************/ -/** - * Question page object - * - * @author Alex Killing - * - * @version $Id$ - * - * @ingroup components\ILIASTestQuestionPool - */ +declare(strict_types=1); + +use ILIAS\Questions\Question\QuestionImplementation; + class ilAssQuestionPage extends ilPageObject { /** @@ -35,4 +30,19 @@ public function getParentType(): string { return "qpl"; } + + public function copyToAnswerForm( + int $new_id, + QuestionImplementation $question + ): void { + $new_page_object = new QstsQuestionPage(); + $new_page_object->setParentId($this->getParentId()); + $new_page_object->setId($new_id); + $new_page_object->setXMLContent($this->copyXMLContent(false, $this->getParentId())); + $new_page_object->setActive($this->getActive()); + $new_page_object->setActivationStart($this->getActivationStart()); + $new_page_object->setActivationEnd($this->getActivationEnd()); + $new_page_object->setQuestion($question); + $new_page_object->create(false); + } } From 14eddda14e06b6684ad699ae8e718f5727823aa7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 28 Jan 2026 12:37:59 +0100 Subject: [PATCH 034/108] Questions: Implement Combinations --- .../ILIAS/Questions/Legacy/LocalDIC.php | 15 +- .../Questions/src/AnswerForm/Persistence.php | 6 +- .../Questions/src/AnswerForm/Views/Edit.php | 9 +- .../Cloze/Layout/CombinationsOverview.php | 320 +++++++++++++++-- .../AnswerFormTypes/Cloze/Layout/Factory.php | 106 ------ .../Cloze/Layout/OverviewTable.php | 2 +- .../Cloze/Migration/MigrationCloze.php | 121 ++++++- .../Cloze/Migration/MigrationTextSubset.php | 122 +++++++ .../src/AnswerFormTypes/Cloze/Persistence.php | 109 ++++-- .../Cloze/Properties/ClozeText/Text.php | 17 + .../Properties/Combinations/Combination.php | 313 ++++++++++++++++ .../Properties/Combinations/Combinations.php | 176 +++++++++ .../Combinations/EditCombinations.php | 340 +++--------------- .../Cloze/Properties/Combinations/Factory.php | 304 ++++++++++++---- .../Cloze/Properties/Combinations/InRange.php | 38 ++ .../Properties/Combinations/MatchingValue.php | 100 ++++++ .../Cloze/Properties/Factory.php | 32 +- .../Gaps/AnswerOptions/AnswerOptions.php | 13 +- .../Properties/Gaps/AnswerOptions/Factory.php | 6 +- .../Cloze/Properties/Gaps/Gaps.php | 30 +- .../Cloze/Properties/Gaps/LongMenu.php | 8 +- .../Cloze/Properties/Gaps/Numeric.php | 12 +- .../Cloze/Properties/Gaps/Select.php | 8 +- .../Cloze/Properties/Gaps/Text.php | 6 - .../Cloze/Properties/Gaps/Type.php | 8 +- .../Cloze/Properties/Properties.php | 69 +++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 73 ++-- .../Questions/src/Persistence/Repository.php | 1 - .../Questions/src/Persistence/TableTypes.php | 2 +- .../Presentation/Definitions/Environment.php | 7 +- .../Definitions/EnvironmentImplementation.php | 60 +++- .../src/Presentation/Layout/EditForm.php | 2 +- .../src/Presentation/Layout/EditOverview.php | 2 +- .../src/Presentation/Layout/Factory.php | 28 +- .../src/Presentation/Layout/InputsBuilder.php | 90 +++++ .../Presentation/Layout/QuestionsTable.php | 2 +- .../src/Presentation/Layout/Renderable.php | 132 +------ .../Questions/src/Presentation/Views/Edit.php | 57 +-- .../src/Setup/ClozeQuestionTables.php | 51 ++- 39 files changed, 2011 insertions(+), 786 deletions(-) delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index d48699cd70e3..31d693bc6c15 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -25,7 +25,7 @@ use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Presentation\Layout\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\DI\Container as ILIASContainer; @@ -67,9 +67,10 @@ protected static function buildDIC(ILIASContainer $DIC): self new UuidFactory(), $c[AnswerFormFactory::class] ); - $dic[DefinitionsFactory::class] = static fn($c): DefinitionsFactory => - new DefinitionsFactory( + $dic[LayoutFactory::class] = static fn($c): LayoutFactory => + new LayoutFactory( $DIC['ui.factory'], + $DIC['refinery'], $DIC['http'], $DIC['lng'] ); @@ -89,7 +90,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[UuidFactory::class], $c[AnswerFormFactory::class], $c[QuestionsRepository::class], - $c[DefinitionsFactory::class] + $c[LayoutFactory::class] ); $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory @@ -134,7 +135,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory => new Cloze\Properties\Factory( $c[Cloze\Properties\ClozeText\Factory::class], - $c[Cloze\Properties\Gaps\Factory::class] + $c[Cloze\Properties\Gaps\Factory::class], + $c[Cloze\Properties\Combinations\Factory::class] ); $dic[Cloze\Persistence::class] = static fn($c): Cloze\Persistence => new Cloze\Persistence( @@ -144,6 +146,7 @@ protected static function buildDIC(ILIASContainer $DIC): self => new Cloze\Views\Edit( $DIC['lng'], $DIC['ui.factory'], + $DIC['ilToolbar'], $DIC['refinery'], $DIC['http'], $c[Cloze\Properties\Factory::class], @@ -166,6 +169,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Views\Edit::class], $c[Cloze\Views\Participant::class] ); + $dic[Cloze\Properties\Combinations\Factory::class] = static fn($c): Cloze\Properties\Combinations\Factory + => new Cloze\Properties\Combinations\Factory($c[UuidFactory::class]); return $dic; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php index da1a89a9da46..6b0484912f80 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -33,20 +33,20 @@ public function getTableNameSpace(): TableNameSpace; public function getColumns( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null, + string $table_identifier = '', array $columns_to_skip = [] ): array; public function getIdColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null + string $table_identifier = '' ): Column; public function getForeignKeyColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null + string $table_identifier = '' ): Column; public function completeQuery( diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index 76e6705616a7..aa4106783d8c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -22,8 +22,11 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\UI\URLBuilder; interface Edit { @@ -37,5 +40,9 @@ public function edit( public function other( Environment $environment - ): EditForm|Properties; + ): Async|Renderable|Properties; + + public function getFinishEditingUrl( + Environment $environment + ): URLBuilder; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index 440884a95599..a0f344715a59 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -18,65 +18,327 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Layout; -use ILIAS\Questions\Presentation\Definitions\Editability; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combination; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Factory as CombinationsFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Data\URI; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\InputsBuilder; +use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Data\Range; +use ILIAS\Data\Order; +use ILIAS\HTTP\Services as Http; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Panel\Standard as StandardPanel; +use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; +use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; +use ILIAS\UI\Component\Table\Data as DataTable; +use ILIAS\UI\Component\Table\DataRetrieval; +use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Renderer as UIRenderer; -use Psr\Http\Message\ServerRequestInterface; -class EditOverview +class CombinationsOverview implements DataRetrieval, Renderable { + private const string STEP_SAVE = 's'; + private const string STEP_SET_COMBINATION_VALUES = 'scv'; + private const string STEP_JUMP_TO_SET_COMBINATION_VALUES = 'jscv'; + private const string STEP_DELETE_COMBINATION = 'dc'; + private const string STEP_CONFIRM_DELETE_COMBINATION = 'cdc'; + + private ?RoundTripModal $modal = null; + public function __construct( private readonly UIFactory $ui_factory, + private readonly \ilToolbarGUI $toolbar, + private readonly Refinery $refinery, private readonly Language $lng, - private readonly ServerRequestInterface $request, + private readonly Http $http, private readonly Environment $environment, - private readonly URI $uri_to_edit_basic_answer_form_properties + private readonly CombinationsFactory $combinations_factory ) { } + #[\Override] public function render( UIRenderer $ui_renderer ): string { - return $ui_renderer->render($this->buildContent()); + $content = [ + $this->initializeModal($this->buildSetCombinationGapsModal()), + $this->buildTable() + ]; + if ($this->modal !== null) { + $content[] = $this->modal; + } + return $ui_renderer->render($content); } - private function buildContent(): array + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + yield from $this->environment->getAnswerFormProperties() + ->getCombinations()->toTableRows($this->lng, $row_builder); + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return $this->environment->getAnswerFormProperties() + ->getCombinations()->getNumberOfCombinations(); + } + + public function doAction(): Async|self|Properties { - return [ - $this->buildBasicAnswerFormPanel(), - $this->environment->getAnswerFormProperties()->getOverviewTable( - $this->ui_factory->table(), - $this->lng, - $this->request, - $this->environment + return match ($this->environment->getStep()) { + self::STEP_SET_COMBINATION_VALUES => $this->processSetCombinationGapsModal(), + self::STEP_DELETE_COMBINATION => $this->deleteCombination(), + self::STEP_SAVE => $this->processSetCombinationValues(), + default => $this->buildAction() + }; + } + + private function buildTable(): DataTable + { + return $this->ui_factory->table()->data( + $this, + $this->lng->txt('combinations'), + $this->getColumns() + )->withActions($this->getActions()) + ->withRequest($this->http->request()); + } + + private function initializeModal( + RoundTripModal $modal + ): RoundTripModal { + $this->toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('add_combination'), + $modal->getShowSignal() ) + ); + return $modal; + } + + private function getColumns(): array + { + $cf = $this->ui_factory->table()->column(); + return [ + 'gaps' => $cf->text($this->lng->txt('gaps')), + 'values' => $cf->text($this->lng->txt('values')), + 'available_points' => $cf->number($this->lng->txt('points'))->withDecimals(2) ]; } - private function buildBasicAnswerFormPanel(): StandardPanel + private function getActions(): array { - $content = [ - $this->ui_factory->listing()->descriptive( - $this->environment->getAnswerFormProperties()->getBasicPropertiesForListing($this->lng) - ) + $af = $this->ui_factory->table()->action(); + return [ + $af->single( + $this->lng->txt('edit'), + $this->environment->getUrlBuilderWithStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES), + $this->environment->getTableRowIdToken() + )->withAsync(true), + $af->single( + $this->lng->txt('delete'), + $this->environment->getUrlBuilderWithStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION), + $this->environment->getTableRowIdToken() + )->withAsync(true) ]; + } - if ($this->environment->getEditability() === Editability::Full) { - $content[] = $this->ui_factory->button()->standard( - $this->lng->txt('edit_basic_answer_form_properties'), - $this->uri_to_edit_basic_answer_form_properties->__toString() + private function buildAction(): Async + { + $affected_item = $this->environment->getAnswerFormProperties() + ->getCombinations()->getCombinationById( + $this->environment->getTableRowIds()[0] ); + + if ($affected_item === null) { + return $this->buildNoItemsSelectedAsync(); + } + + return $this->environment->getPresentationFactory()->getAsync( + match ($this->environment->getStep()) { + self::STEP_JUMP_TO_SET_COMBINATION_VALUES => + $this->buildSetCombinationValuesModal($affected_item), + self::STEP_CONFIRM_DELETE_COMBINATION => + $this->confirmDeleteCombination($affected_item) + } + ); + } + + private function buildNoItemsSelectedAsync(): Async + { + return new Async( + $this->http, + $this->ui_factory->messageBox()->failure('no_combination_selected') + ); + } + + private function buildSetCombinationGapsModal(): RoundTripModal + { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('add_combination'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps + ), + [ + 'combination' => $gaps->buildGapsMultiSelect( + $this->lng->txt('select_gaps_for_combinations'), + $this->ui_factory->input()->field() + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->constraint( + fn(array $v): bool => count($v) > 1, + $this->lng->txt('combination_needs_more_than_one') + ) + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $v): Combination => $this->combinations_factory + ->buildNewCombination($gaps, $v) + ) + ) + ], + $this->environment + ->getUrlBuilderWithStepParameter(self::STEP_SET_COMBINATION_VALUES) + ->buildURI() + ->__toString() + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processSetCombinationGapsModal(): self + { + $clone = clone $this; + + $set_gaps_modal = $clone->buildSetCombinationGapsModal() + ->withRequest($clone->http->request()); + $data = $set_gaps_modal->getData(); + + if ($data === null) { + $clone->modal = $set_gaps_modal->withOnLoad($set_gaps_modal->getShowSignal()); + return $clone; + } + + $set_values_modal = $clone->buildSetCombinationValuesModal($data['combination']); + $clone->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); + return $clone; + } + + private function buildSetCombinationValuesModal( + ?Combination $combination = null + ): RoundTripModal { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + + $inputs_builder = $this->buildInputsBuilder($combination); + $inputs = $inputs_builder->getInputs( + $this->environment + ); + + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('edit'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps + ), + [ + 'values_awarding_points' => $inputs + ], + $inputs_builder->addCarryToEnvironment( + $this->environment + )->getUrlBuilderWithStepParameter(self::STEP_SAVE) + ->buildURI() + ->__toString() + ); + } + + private function processSetCombinationValues(): self|Properties + { + $set_values_modal = $this->buildSetCombinationValuesModal() + ->withRequest($this->http->request()); + $data = $set_values_modal->getData(); + if ($data === null) { + $this->modal = $this->initializeModal($set_values_modal) + ->withOnLoad($set_values_modal->getShowSignal()); + return $this; } - return $this->ui_factory->panel()->standard( - $this->lng->txt('basic_answer_form_properites'), - $content + return $data['values_awarding_points']; + } + + private function confirmDeleteCombination( + Combination $affected_item + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('delete_combination'), + $this->environment->getUrlBuilderWithStepParameter( + self::STEP_DELETE_COMBINATION + )->withParameter( + $this->environment->getTableRowIdToken(), + [$affected_item->getId()->toString()] + )->buildURI()->__toString() + ); + } + + private function deleteCombination(): Properties + { + $combination_identifier = $this->environment->getTableRowIds(); + if ($combination_identifier === []) { + return $this->environment->getAnswerFormProperties(); + } + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ + $answer_form_properties = $this->environment->getAnswerFormProperties(); + + return $answer_form_properties->withCombinations( + $answer_form_properties->getCombinations()->withoutCombination( + $combination_identifier[0] + ) ); } + + private function buildInputsBuilder( + ?Combination $combination, + ): InputsBuilder { + $properties = $this->environment->getAnswerFormProperties(); + $inputs_builder = $this->environment->getPresentationFactory()->getInputsBuilder( + $this->combinations_factory->getToCombinationTransformation( + $this->ui_factory->input()->field(), + $this->refinery, + $this->lng, + $properties + ), + $combination?->buildPointsInputs( + $this->ui_factory->input()->field(), + $this->refinery, + $this->lng, + $this->combinations_factory, + $properties + ) + ); + + if ($combination === null) { + return $inputs_builder; + } + + return $inputs_builder->withCarry($combination->buildCarryString()); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php deleted file mode 100644 index a2bc23fbc70d..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/Factory.php +++ /dev/null @@ -1,106 +0,0 @@ -ui_factory, - $this->lng, - $this->http->request(), - $environment, - $uri_to_edit_basic_answer_form_properties - ); - } - - public function getEditForm( - URLBuilder $url_builder, - Section $main_section_inputs, - bool $is_final_step, - ?Group $carry_inputs = null - ): EditForm { - return new EditForm( - $this->ui_factory->input()->container()->form(), - $this->lng, - $url_builder, - $main_section_inputs, - $is_final_step, - $carry_inputs - ); - } - - public function getAsync( - InterruptiveModal|RoundTripModal|MessageBox $content - ): Async { - return new Async( - $this->http, - $content - ); - } - - public function getCarrySectionData( - ArrayBasedRequestWrapper $post_wrapper, - Refinery $refinery - ): CarryWrapper { - return new CarryWrapper( - array_reduce( - $post_wrapper->keys(), - function (array $c, string $v) use ($post_wrapper, $refinery): array { - $value = new Leaf( - $post_wrapper->retrieve($v, $refinery->identity()) - ); - foreach (array_reverse(explode('/', $v)) as $path_element) { - $value = [$path_element => $value]; - } - return array_merge_recursive($c, $value); - }, - [] - )['form'][EditForm::CARRY_SECTION_NAME] ?? [] - ); - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index 01d99c6ebbc6..039947f97b54 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -71,7 +71,7 @@ public function getTotalRowCount( mixed $filter_data, mixed $additional_parameters ): ?int { - return 0; + return $this->environment->getAnswerFormProperties()->getGaps()->getNumberOfGaps(); } private function getColums(): array diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 6c5adb72b8f9..99133c7f92c3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -24,8 +24,11 @@ use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; use ILIAS\Questions\Definitions\TextMatchingOptions; +use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableTypes; class MigrationCloze implements Migration { @@ -60,6 +63,7 @@ public function buildInsertStatement( MigrationInsert $migration_insert ): MigrationInsert { $answer_input_mapping = []; + $answer_options_mapping = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() @@ -67,6 +71,7 @@ public function buildInsertStatement( $answer_form_id = $migration_insert->getAnswerFormId(); if (!isset($answer_input_mapping[$db_row->gap_id])) { $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); + $answer_options_mapping[$db_row->gap_id] = []; $migration_insert = $migration_insert->withAdditionalInsert( $this->buildGapInsertStatement( $this->persistence, @@ -84,11 +89,17 @@ public function buildInsertStatement( ); } + $answer_option_id = $migration_insert->getUuid(); + $answer_options_mapping[$db_row->gap_id][$db_row->answertext] = [ + 'is_numeric' => $db_row->cloze_type == \assClozeGap::TYPE_NUMERIC, + 'answer_option_id' => $answer_option_id + ]; + $migration_insert = $migration_insert->withAdditionalInsert( $this->buildAnswerOptionInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), - $migration_insert->getUuid(), + $answer_option_id, $answer_input_mapping[$db_row->gap_id], $db_row->aorder, $db_row->answertext, @@ -99,6 +110,14 @@ public function buildInsertStatement( ); } + if ($db_row->combinations_enabled) { + $migration_insert = $this->addCombinationInserStatements( + $migration_insert, + $answer_input_mapping, + $answer_options_mapping + ); + } + return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( @@ -137,6 +156,91 @@ private function fetchDBValues( } } + private function fetchCombinationsDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT combination_id, gap_fi, answer, points FROM qpl_a_cloze_combi_res' . PHP_EOL + . "WHERE question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY combination_id, row_id' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function addCombinationInserStatements( + MigrationInsert $migration_insert, + array $answer_input_mapping, + array $answer_options_mapping + ): MigrationInsert { + $combination_mapping = []; + foreach ($this->fetchCombinationsDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { + $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); + $migration_insert = $migration_insert->withAdditionalInsert( + new Insert( + $this->persistence->getColumns( + $this->persistence->getTableNameSpace(), + TableTypes::Additional, + $this->persistence->getCombinationsTableIdentifier() + ), + [ + new Value( + \ilDBConstants::T_TEXT, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString() + ), + new Value( + \ilDBConstants::T_TEXT, + $migration_insert->getAnswerFormId()->toString() + ), + new Value(\ilDBConstants::T_FLOAT, $db_row->points), + ] + ) + ); + } + + $answer_option = $db_row->answer === 'out_of_bound' + ? reset($answer_options_mapping[$db_row->gap_fi]) + : $answer_options_mapping[$db_row->gap_fi][$db_row->answer]; + + $migration_insert = $migration_insert->withAdditionalInsert( + new Insert( + $this->persistence->getColumns( + $this->persistence->getTableNameSpace(), + TableTypes::Additional, + $this->persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + new Value( + \ilDBConstants::T_TEXT, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString() + ), + new Value( + \ilDBConstants::T_TEXT, + $answer_input_mapping[$db_row->gap_fi] + ), + new Value( + \ilDBConstants::T_TEXT, + $answer_option['answer_option_id'] + ), + new Value( + \ilDBConstants::T_TEXT, + $this->buildRangeValue($answer_option['is_numeric'], $db_row->answer) + ), + ] + ) + ); + } + + return $migration_insert; + } + private function buildNewGapTypeIdentifierFromOld( int $old_gap_type ): string { @@ -160,4 +264,19 @@ private function buildNewTextRatingFromOld( \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => TextMatchingOptions::Levenstein5 }; } + + private function buildRangeValue( + bool $is_numeric, + string $value + ): ?string { + if ($is_numeric === null) { + return null; + } + + if ($value === 'out_of_bounds') { + return InRange::OutOfRange->value; + } + + return InRange::InRange->value; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php new file mode 100644 index 000000000000..ad9825006ed1 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -0,0 +1,122 @@ +math->suppress_errors = true; + } + + #[\Override] + public function getOldQuestionIdentifier(): string + { + return 'assNumeric'; + } + + #[\Override] + public function getDefinitionClass(): string + { + return Definition::class; + } + + #[\Override] + public function getTableNameSpace(): TableNameSpace + { + return $this->persistence->getTableNameSpace(); + } + + #[\Override] + public function buildInsertStatement( + MigrationInsert $migration_insert + ): MigrationInsert { + $db_row = $this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + )->current(); + + $answer_form_id = $migration_insert->getAnswerFormId(); + $gap_id = $migration_insert->getUuid(); + return $migration_insert->withAdditionalInsert( + $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $gap_id, + $answer_form_id, + 0, + 'numeric', + null, + 0.0001, + null, + null, + null + ) + )->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $migration_insert->getUuid(), + $gap_id, + 0, + '', + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) + ) + )->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_form_id, + ScoringIdentical::ScoreAll, + 0 + ) + )->withAdditionalTextLegacy( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gap_id->toString()) . '}}' + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT points, lowerlimit, upperlimit FROM qpl_num_range' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php index 6d835d8bce23..3fa8bb631adf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -41,7 +41,7 @@ class Persistence implements PersistenceInterface private const array ANSWER_FORM_TABLE_COLUMNS = [ 'answer_form_id', 'scoring_identical_responses', - 'combinations_activated' + 'combinations_enabled' ]; private const string ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; @@ -68,15 +68,24 @@ class Persistence implements PersistenceInterface 'upper_limit' ]; - private const string COMBINATIONS_TABLE_IDENTIFIER = 'combinations'; - private const string COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; - private const array COMBINATIONS_TABLE_COLUMNS = [ + public const string COMBINATION_TABLE_IDENTIFIER = 'combinations'; + private const string COMBINATION_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; + private const array COMBINATION_TABLE_COLUMNS = [ 'id', 'answer_form_id', - 'answer_options', 'points' ]; + public const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER = 'combinations_to_answer_options'; + private const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_ID_COLUMN = 'combination_id'; + private const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN = 'combination_id'; + private const array COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS = [ + 'combination_id', + 'gap_id', + 'answer_option_id', + 'in_range' + ]; + public function __construct( private readonly TableNameSpaceCore $table_namespace ) { @@ -92,7 +101,7 @@ public function getTableNameSpace(): TableNameSpace public function getColumns( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null, + string $table_identifier = '', array $columns_to_skip = [] ): array { $table = $table_type->getTable($table_name_builder, $table_identifier); @@ -100,7 +109,10 @@ public function getColumns( TableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, TableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, TableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, - TableTypes::Additional => self::COMBINATIONS_TABLE_COLUMNS + TableTypes::Additional => match($table_identifier) { + self::COMBINATION_TABLE_IDENTIFIER => self::COMBINATION_TABLE_COLUMNS, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER => self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS + } }; return array_map( fn(string $v): Column => new Column($table, $v), @@ -117,25 +129,36 @@ public function getColumns( public function getIdColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null + string $table_identifier = '' ): Column { - return match($table_type) { - TableTypes::TypeSpecificAnswerForms => new Column( + if ($table_type === TableTypes::TypeSpecificAnswerForms) { + return new Column( $table_type->getTable($table_name_builder), self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN - ), - default => new Column( - $table_type->getTable($table_name_builder, $table_identifier), - self::ID_COLUMN - ) - }; + ); + } + + if ($table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { + return new Column( + $table_type->getTable( + $table_name_builder, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_ID_COLUMN + ); + } + + return new Column( + $table_type->getTable($table_name_builder, $table_identifier), + self::ID_COLUMN + ); } #[\Override] public function getForeignKeyColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, - ?string $table_identifier = null + string $table_identifier = '' ): Column { return match($table_type) { TableTypes::TypeSpecificAnswerForms => new Column( @@ -152,7 +175,9 @@ public function getForeignKeyColumn( ), TableTypes::Additional => new Column( $table_type->getTable($table_name_builder, $table_identifier), - self::COMBINATIONS_TABLE_FOREIGN_KEY_COLUMN + $table_identifier === self::COMBINATION_TABLE_IDENTIFIER + ? self::COMBINATION_TABLE_FOREIGN_KEY_COLUMN + : self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ) }; } @@ -167,7 +192,13 @@ public function completeQuery( $answer_form_specific_table_definition = TableTypes::TypeSpecificAnswerForms; $answer_input_table_definition = TableTypes::AnswerInputs; $answer_options_table_definition = TableTypes::AnswerOptions; - $combinations_table_definition = TableTypes::Additional; + $additional_table_definition = TableTypes::Additional; + + $combinations_id_column = $this->getIdColumn( + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER + ); return $query->withAdditionalJoin( new Join( @@ -236,8 +267,8 @@ public function completeQuery( $answer_form_id_column, $this->getForeignKeyColumn( $table_name_builder, - $combinations_table_definition, - self::COMBINATIONS_TABLE_IDENTIFIER + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER ), JoinType::Left ) @@ -245,10 +276,42 @@ public function completeQuery( new Select( $this->getColumns( $table_name_builder, - $combinations_table_definition, - self::COMBINATIONS_TABLE_IDENTIFIER + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER ) ) + )->withAdditionalJoin( + new Join( + $combinations_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + JoinType::Left + ) + )->withAdditionalSelect( + new Select( + $this->getColumns( + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ) + ) + )->withAdditionalOrder( + new Order( + $combinations_id_column + ) ); } + + public function getCombinationsTableIdentifier(): string + { + return self::COMBINATION_TABLE_IDENTIFIER; + } + + public function getCombinationToAnswerOptionsTableIdentifier(): string + { + return self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 59120c3fa759..d6e9cfb092b9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -27,9 +27,11 @@ use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Markdown as MarkdownInput; use ILIAS\UI\Component\Input\Field\Hidden as HiddenInput; +use ILIAS\UI\Component\Panel\Standard as StandardPanel; use Mustache\Engine; class Text @@ -83,6 +85,21 @@ public function getRenderedMarkdownForParticipantPresentation(): string ); } + public function buildPanelForEditing( + UIFactory $ui_factory, + Language $lng, + Gaps $gaps + ): StandardPanel { + return $ui_factory->panel()->standard( + $lng->txt('cloze_text'), + $ui_factory->legacy()->content( + $this->getRenderedMarkdownForEditingPresentation( + $gaps + ) + ) + ); + } + public function getRenderedMarkdownForEditingPresentation( Gaps $gaps ): string { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php new file mode 100644 index 000000000000..8ba82d26f080 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -0,0 +1,313 @@ + $matching_values + */ + public function __construct( + private readonly Uuid $id, + private ?float $available_points, + private array $matching_values = [] + ) { + } + + public function getId(): Uuid + { + return $this->id; + } + + public function getAvailablePoints(): ?float + { + return $this->available_points; + } + + public function withAdditionalMatchingValue( + MatchingValue $matching_value + ): self { + $clone = clone $this; + $clone->matching_values[] = $matching_value; + return $clone; + } + + public function getValuePresentation( + Language $lng + ): string { + return implode( + '
', + array_map( + fn(MatchingValue $v): string => $v->buildPresentationString($lng), + $this->matching_values + ) + ); + } + + public function containsAnswerOptionsExactly( + array $vs + ): bool { + return array_diff( + $vs, + array_map( + fn(MatchingValue $v): string => $v->getAnswerOption()->getAnswerOptionId()->toString(), + $this->matching_values + ) + ) === []; + } + + public function toStorage( + Uuid $answer_form_id, + Persistence $persistence, + TableNameBuilder $table_name_builder, + Manipulate $manipulate + ): Manipulate { + if ($this->matching_values === []) { + throw new \UnexpectedValueException( + 'A Combination without MatchingValues cannot be stored.' + ); + } + + return array_reduce( + $this->matching_values, + fn(Manipulate $c, MatchingValue $v): Manipulate => $c->withAdditionalStatement( + $v->toStorage( + $persistence, + $table_name_builder + ) + ), + $manipulate->withAdditionalStatement( + $this->buildReplace( + $answer_form_id, + $persistence, + $table_name_builder + ) + ) + ); + } + + public function toDelete( + Persistence $persistence, + TableNameBuilder $table_name_builder, + Manipulate $manipulate + ): Manipulate { + return $manipulate->withAdditionalStatement( + $this->buildDelete($persistence, $table_name_builder) + )->withAdditionalStatement( + $this->buildDeleteForLinkedValues( + $persistence, + $table_name_builder + ) + ); + } + + private function buildReplace( + Uuid $answer_form_id, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Replace { + $table_definition = TableTypes::Additional; + return new Replace( + $persistence->getColumns( + $table_name_builder, + $table_definition, + $persistence->getCombinationsTableIdentifier() + ), + [ + new Value(\ilDBConstants::T_TEXT, $this->id->toString()), + new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + new Value(\ilDBConstants::T_FLOAT, $this->available_points) + ] + ); + } + + private function buildDelete( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::Additional; + return new Delete( + new Table( + $table_definition, + $table_name_builder, + $persistence->getCombinationsTableIdentifier() + ), + [ + new Where( + $persistence->getIdColumn( + $table_name_builder, + $table_definition, + $persistence->getCombinationsTableIdentifier() + ), + new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + ) + ] + ); + } + + private function buildDeleteForLinkedValues( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::Additional; + return new Delete( + new Table( + $table_definition, + $table_name_builder, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + new Where( + $persistence->getIdColumn( + $table_name_builder, + $table_definition, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + ) + ] + ); + } + + public function toTableRow( + Language $lng, + DataRowBuilder $data_row_builder + ): DataRow { + return $data_row_builder->buildDataRow( + $this->id->toString(), + [ + 'gaps' => $this->buildGapsString(), + 'values' => $this->getValuePresentation($lng), + 'available_points' => $this->getAvailablePoints() + ] + ); + } + + public function buildGapsString(): string + { + return implode( + '
', + array_map( + fn(MatchingValue $v): string => $v->getGap()->buildShortenedGapRepresentation(), + $this->matching_values + ) + ); + } + + public function buildPointsInputs( + FieldFactory $field_factory, + Refinery $refinery, + Language $lng, + Factory $combinations_factory, + Properties $properties + ): Group { + return $field_factory->section( + [ + 'values' => $this->buildValuesInputs( + $field_factory, + $properties + ), + 'points' => $field_factory->numeric( + $lng->txt('points') + )->withStepSize(0.01) + ->withRequired(true) + ->withValue($this->getAvailablePoints()) + ], + $lng->txt('values') + )->withAdditionalTransformation( + $refinery->custom()->constraint( + fn(array $vs): bool => !$properties->getCombinations() + ->hasMatchingCombinationForAnswerOptionIds($vs['values']), + $lng->txt('combination_already_exists') + ) + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $v): Properties => $properties->withCombinations( + $properties->getCombinations()->withAdditionalCombination( + $combinations_factory->buildCombination( + $this->id, + $v['points'], + $combinations_factory->buildMatchingValuesFromForm( + $properties, + $this->id, + $v['values'] + ) + ) + ) + ) + ) + ); + } + + public function buildCarryString(): string + { + return json_encode([ + $this->id->toString() => array_map( + fn(MatchingValue $v) => $v->getGap()->getAnswerInputId()->toString(), + $this->matching_values + ) + ]); + } + + private function buildValuesInputs( + FieldFactory $field_factory, + Properties $properties + ): Group { + return $field_factory->group( + array_reduce( + $this->matching_values, + function (array $c, MatchingValue $v) use ($field_factory, $properties): array { + $gap_id = $v->getGap()->getAnswerInputId(); + $gap = $properties->getGaps()->getGapById($gap_id); + $c[$gap_id] = $field_factory->select( + $gap->buildShortenedGapName(), + $gap->getType()->getCombinationsSelectValues($gap) + )->withRequired(true) + ->withValue( + $v->getAnswerOption()?->getAnswerOptionId()->toString() + ); + return $c; + }, + [] + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php new file mode 100644 index 000000000000..90c6540c8a3e --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -0,0 +1,176 @@ +combinations = array_reduce( + $combinations, + function (array $c, Combination $v): array { + $c[$v->getId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function areCombinationsEnabled(): bool + { + return $this->enabled; + } + + public function withCombinationsEnabled( + bool $combinations_enabled + ): self { + $clone = clone $this; + $clone->enabled = $combinations_enabled; + return $clone; + } + + public function getCombinationById( + string $id + ): ?Combination { + return $this->combinations[$id] ?? null; + } + + public function withAdditionalCombination( + Combination $combination + ): self { + $clone = clone $this; + $clone->combinations[$combination->getId()->toString()] = $combination; + return $clone; + } + + public function withoutCombination( + string $id + ): self { + $clone = clone $this; + $clone->deleted_combinations[] = $clone->combinations[$id]; + unset($clone->combinations[$id]); + return $clone; + } + + public function hasMatchingCombinationForAnswerOptionIds( + array $vs + ): bool { + foreach ($this->combinations as $combination) { + if ($combination->containsAnswerOptionsExactly($vs)) { + return true; + } + return false; + } + } + + public function getEditView( + UIFactory $ui_factory, + \ilToolbarGUI $toolbar, + Refinery $refinery, + Language $lng, + HttpServices $http + ): EditCombinations { + return new EditCombinations( + $ui_factory, + $toolbar, + $refinery, + $lng, + $http, + $this->combinations_factory + ); + } + + public function toStorage( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->combinations, + fn(Manipulate $c, Combination $v): Manipulate => $v->toStorage( + $this->answer_form_id, + $persistence, + $table_name_builder, + $c + ), + array_reduce( + $this->deleted_combinations, + fn(Manipulate $c, Combination $v): Manipulate => $v->toDelete( + $persistence, + $table_name_builder, + $manipulate + ), + $manipulate + ) + ); + } + + public function toDelete( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->combinations, + fn(Manipulate $c, Combination $v): Manipulate => $c->withAdditionalStatement( + $v->toDelete( + $this->answer_form_id, + $manipulate, + $persistence, + $table_name_builder + ) + ), + $manipulate + ); + } + + public function toTableRows( + Language $lng, + DataRowBuilder $row_builder, + ): \Generator { + foreach ($this->combinations as $combination) { + yield $combination->toTableRow($lng, $row_builder); + } + } + + public function getNumberOfCombinations(): int + { + return count($this->combinations); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php index 64eb908e2555..031189a70094 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -18,335 +18,77 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; -use ILIAS\Questions\AnswerForm\Views\Edit as EditViewInterface; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Layout\CombinationsOverview; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\HTTP\Services as HTTPServices; use ILIAS\Language\Language; -use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; -use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; -use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; -class Edit implements EditViewInterface +class EditCombinations { - private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; - private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; - private const string STEP_SET_GAP_TYPES = 'sgt'; - public const string STEP_JUMP_TO_SET_GAP_TYPES = 'jsgt'; - private const string STEP_SET_ANSWER_OPTIONS = 'sao'; - public const string STEP_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; - private const string STEP_SET_POINTS = 'sp'; - public const string STEP_JUMP_TO_SET_POINTS = 'jsp'; - private const string STEP_SAVE = 's'; + private const string STEP_EDIT_COMBINATIONS_OVERVIEW = 'eco'; + + private const string LANG_VAR_EDIT_COMBINATIONS = 'edit_combinations'; public function __construct( - private readonly Language $lng, private readonly UIFactory $ui_factory, + private readonly \ilToolbarGUI $toolbar, private readonly Refinery $refinery, + private readonly Language $lng, private readonly HTTPServices $http, - private readonly PropertiesFactory $properties_factory, - private readonly ClozeTextFactory $cloze_text_factory, - private readonly GapFactory $gap_factory + private readonly Factory $combinations_factory ) { } - #[\Override] - public function create( - Environment $environment - ): EditForm|Properties { - $step = $environment->getStep(); - - return match($step) { - '' => $this->buildBasicEditingForm($environment), - default => $this->callIntermediateStep($environment, $step) - }; - } - - #[\Override] - public function edit( - Environment $environment - ): EditOverview|EditForm|Properties { - $step = $environment->getStep(); - - if ($step === '') { - return $environment->getPresentationFactory()->getEditOverview( - $environment, - $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) - ->buildURI() - ); - } - - $environment->setEditAnswerFormBackTarget(); - - return match ($step) { - self::STEP_EDIT_BASIC_PROPERTIES => $this->buildBasicEditingForm($environment), - default => $this->callIntermediateStep($environment, $step) - }; - } - - #[\Override] - public function other( - Environment $environment - ): EditForm|Properties { - - } - - private function callIntermediateStep( - Environment $environment, - string $step - ): EditForm|Properties { - $initialized_environment = $environment->withPreservedTableRowIdsParameter(); - - if ($step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS - && $step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS - && $step !== self::STEP_JUMP_TO_SET_POINTS) { - $initialized_environment = $initialized_environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getPresentationFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery - ) - ) - ); - } - - return match ($step) { - self::STEP_SET_GAP_TYPES, - self::STEP_CONFIRMED_GAP_REMOVAL => $this->processBasicEditingForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesForm( - $initialized_environment - ), - self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsForm( - $initialized_environment - ), - self::STEP_SET_POINTS => $this->processAnswerOptionsForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_POINTS => $this->buildAssignPointsForm( - $initialized_environment - ), - self::STEP_SAVE => $this->processAssignPointsForm( - $initialized_environment - ) - }; - } - - private function buildBasicEditingForm( - Environment $environment - ): EditForm { - return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES), - $environment->getAnswerFormProperties()->buildBasicEditingInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->refinery, - $this->properties_factory, - $this->cloze_text_factory - ), - false - ); - } - - private function processBasicEditingForm( + public function addCombinationsSubTab( Environment $environment - ): EditForm|Properties { - $form = $this->buildBasicEditingForm( - $environment - )->withRequest($this->http->request()); - - $data = $form->getData(); - if ($data === null) { - return $form; - } - - $new_gaps = $data->getGaps(); - $old_gaps = $environment->getAnswerFormProperties()->getGaps(); - - if ($environment->getStep() !== self::STEP_CONFIRMED_GAP_REMOVAL) { - $removed_gaps = $new_gaps->getRemovedGaps($old_gaps); - if ($removed_gaps !== []) { - return $form->withConfirmation( - $this->buildRemovedGapsConfirmation( - $environment, - $removed_gaps - ) - ); - } - } - - if ($new_gaps->getAddedGaps($old_gaps) === []) { - return $data; - } - - return $this->buildGapTypesForm( - $environment->withAnswerFormProperties($data) + ): void { + $environment->addEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::LANG_VAR_EDIT_COMBINATIONS ); } - private function buildGapTypesForm( + public function show( Environment $environment - ): EditForm { - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); - return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS), - $properties->getGaps()->buildGapsTypeInputs( - $this->lng, - $ff, - $this->refinery, - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), - $environment->getTableRowIds() - ), - false, - $properties->withClozeText($properties->getClozeText()) - ->buildCarryInputs($ff) - )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) + ): Async|Renderable|Properties { + $environment->addEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::LANG_VAR_EDIT_COMBINATIONS ); - } - - private function processGapTypesForm( - Environment $environment - ): EditForm { - $form = $this->buildGapTypesForm( - $environment - )->withRequest($this->http->request()); - - $data = $form->getData(); - return $data === null - ? $form - : $this->buildAnswerOptionsForm( - $environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withGaps($data) - ) - ); - } - private function buildAnswerOptionsForm( - Environment $environment - ): EditForm { - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); - return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS), - $properties->getGaps()->buildAnswerOptionsInputs( - $this->lng, - $ff, - $this->refinery, - $environment->getTableRowIds() - ), - false, - $properties->buildCarryInputs($ff) - )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) + $environment->activateEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW ); - } - private function processAnswerOptionsForm( - Environment $environment - ): EditForm { - $form = $this->buildAnswerOptionsForm( - $environment - )->withRequest($this->http->request()); + $combinations_overview = $this->buildCombinationsOverview($environment); - $data = $form->getData(); - return $data === null - ? $form - : $this->buildAssignPointsForm( - $environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withGaps($data) - ) - ); - } + $step = $environment->getStep(); + if ($step === self::STEP_EDIT_COMBINATIONS_OVERVIEW + || $step === '') { + return $combinations_overview; + } - private function buildAssignPointsForm( - Environment $environment - ): EditForm { - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); - return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SAVE), - $properties->getGaps()->buildPointInputs( - $this->lng, - $ff, - $this->refinery, - $environment->getTableRowIds() - ), - true, - $properties->buildCarryInputs($ff) - )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) - ); + return $combinations_overview->doAction(); } - private function processAssignPointsForm( + private function buildCombinationsOverview( Environment $environment - ): EditForm|Properties { - $form = $this->buildAssignPointsForm( - $environment - )->withRequest($this->http->request()); - - $properties = $environment->getAnswerFormProperties(); - $data = $form->getData(); - return $data === null - ? $form->withContentBeforeForm( - $this->buildClozeTextPanel($properties) - ) : $properties->withGaps($data); - } - - private function buildClozeTextPanel( - Properties $properties - ): StandardPanel { - return $this->ui_factory->panel()->standard( - $this->lng->txt('cloze_text'), - $this->ui_factory->legacy()->content( - $properties->getClozeText()->getRenderedMarkdownForEditingPresentation( - $properties->getGaps() - ) - ) - ); - } - - /** - * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap> $removed_gaps - */ - private function buildRemovedGapsConfirmation( - Environment $environment, - array $removed_gaps - ): InterruptiveModal { - return $this->ui_factory->modal()->interruptive( - $this->lng->txt('confirm'), - $this->lng->txt('confirm_remove_gaps'), - $environment->getUrlBuilderWithStepParameter( - self::STEP_CONFIRMED_GAP_REMOVAL - )->buildURI()->__toString() - )->withAffectedItems( - array_map( - fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() - ->interruptiveItem()->standard( - $v->getAnswerInputId()->toString(), - $v->buildShortenedGapName() - ), - $removed_gaps - ) + ): CombinationsOverview { + return new CombinationsOverview( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http, + $environment, + $this->combinations_factory ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index 103012a35f75..609c3141ac2a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -18,91 +18,269 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; use ILIAS\Questions\AnswerForm\TypeGenericProperties; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Custom\Transformation as CustomTransformation; +use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; +use ILIAS\UI\Component\Input\Field\Group; class Factory { public function __construct( - private readonly ClozeTextFactory $cloze_text_factory, - private readonly GapsFactory $gaps_factory + private readonly UuidFactory $uuid_factory ) { } - public function fromData( + public function getCombinations( TypeGenericProperties $type_generic_properties, - ?Query $query - ): Properties { - if ($query === null) { - return new Properties( - $type_generic_properties->getAnswerFormId(), - $type_generic_properties->getQuestionId(), - $type_generic_properties->getDefinition(), - $this->cloze_text_factory->buildFromTextString( - $type_generic_properties->getAdditionalText() - ), - $type_generic_properties->getAdditionalTextLegacy(), - $this->gaps_factory->getEmptyGapsObject( - $type_generic_properties->getAnswerFormId() + bool $combinations_enabled, + ?Gaps $gaps = null, + ?Query $query = null + ): Combinations { + return new Combinations( + $this, + $type_generic_properties->getAnswerFormId(), + $combinations_enabled, + !$combinations_enabled || $gaps === null || $query === null + ? [] + : $this->retrieveMatchingValuesFromQuery( + $type_generic_properties, + $gaps, + $this->retrieveCombinationsFromQuery( + $type_generic_properties, + $query + ), + $query ) - ); - } + ); + } - [ - 'scoring_identical_responses' => $scoring_identical_responses, - 'combinations_activated' => $combinations_activated - ] = $query->retrieveCurrentRecord( - TableTypes::TypeSpecificAnswerForms->getTable( - $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) - ), - $query->getRefinery()->custom()->transformation( - fn(array $vs): array => [ - 'scoring_identical_responses' => ScoringIdentical::tryFrom($vs[0]['scoring_identical_responses']), - 'combinations_activated' => $vs[0]['combinations_activated'] === 1 - ] + /** + * @param array $gap_ids + */ + public function buildNewCombination( + Gaps $gaps, + array $gap_ids, + ): Combination { + $combination_id = $this->uuid_factory->uuid4(); + return $this->buildCombination( + $combination_id, + null, + array_map( + fn(string $v): MatchingValue => new MatchingValue( + $combination_id, + $gaps->getGapById( + $this->uuid_factory->fromString($v) + ) + ), + $gap_ids ) ); + } - return new Properties( - $type_generic_properties->getAnswerFormId(), - $type_generic_properties->getQuestionId(), - $type_generic_properties->getDefinition(), - $this->cloze_text_factory->buildFromTextString( - $type_generic_properties->getAdditionalText() - ), - $type_generic_properties->getAdditionalTextLegacy(), - $this->gaps_factory->fromDatabase( - $type_generic_properties->getAnswerFormId(), - $query - ), - $scoring_identical_responses, - $combinations_activated + /** + * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\MatchingValue> $matching_values + */ + public function buildCombination( + Uuid $combination_id, + ?float $points, + array $matching_values + ): Combination { + return new Combination( + $combination_id, + $points, + $matching_values ); } - public function fromForm( + /** + * @param array $values_array + */ + public function buildMatchingValuesFromForm( Properties $properties, - ClozeText $cloze_text, - ScoringIdentical $scoring_of_identical_responses, - bool $combinations_enabled - ): Properties { - $updated_gaps = $cloze_text->updateGapsFromMarkdown( - $properties->getAnswerFormId(), - $properties->getGaps() + Uuid $combination_id, + array $values_array + ): array { + return array_reduce( + array_keys($values_array), + function (array $c, string $v) use ( + $properties, + $values_array, + $combination_id + ): array { + $gap = $properties->getGaps()->getGapById( + $this->uuid_factory->fromString($v) + ); + $answer_option = + $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($values_array[$v]) + ); + + if ($answer_option === null) { + return $c; + } + + $c[] = new MatchingValue( + $combination_id, + $gap, + $answer_option, + null + ); + return $c; + }, + [] + ); + } + + public function getToCombinationTransformation( + FieldFactory $field_factory, + Refinery $refinery, + Language $lng, + Properties $properties + ): CustomTransformation { + return $refinery->custom()->transformation( + function (string $v) use ( + $field_factory, + $refinery, + $lng, + $properties + ): Group { + $values_array = json_decode($v, true); + $combination_id = $this->uuid_factory->fromString( + array_key_first($values_array) + ); + return new Combination( + $combination_id, + null, + array_map( + fn(string $v): MatchingValue => new MatchingValue( + $combination_id, + $properties->getGaps()->getGapById( + $this->uuid_factory->fromString($v) + ) + ), + $values_array[$combination_id->toString()] + ) + )->buildPointsInputs( + $field_factory, + $refinery, + $lng, + $this, + $properties + ); + } + ); + } + + private function retrieveCombinationsFromQuery( + TypeGenericProperties $type_generic_properties, + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::Additional->getTable( + $query->getTableNameBuilder( + $type_generic_properties->getDefinition()::class + ), + Persistence::COMBINATION_TABLE_IDENTIFIER + ), + $query->getRefinery()->custom()->transformation( + fn(array $vs): array => $this->buildCombinationsFromQuery( + array_filter( + $vs, + fn(array $v): bool => $v['answer_form_id'] !== null + ) + ) + ) + ); + } + + private function buildCombinationsFromQuery( + array $values + ): array { + if ($values === []) { + return []; + } + + return array_reduce( + $values, + function (array $c, array $v): array { + if (array_key_exists($v['id'], $c)) { + return $c; + } + + $c[$v['id']] = new Combination( + $this->uuid_factory->fromString($v['id']), + $v['points'] + ); + + return $c; + }, + [] ); + } + + private function retrieveMatchingValuesFromQuery( + TypeGenericProperties $type_generic_properties, + Gaps $gaps, + array $combinations, + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::Additional->getTable( + $query->getTableNameBuilder( + $type_generic_properties->getDefinition()::class + ), + Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + $query->getRefinery()->custom()->transformation( + function (array $vs) use ( + $gaps, + $combinations + ): array { + $already_added = []; + foreach ($vs as $v) { + if (!array_key_exists($v['combination_id'], $combinations) + || in_array( + $v['combination_id'] . $v['gap_id'], + $already_added + ) + ) { + continue; + } - return $properties - ->withClozeText( - $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) - )->withGaps($updated_gaps) - ->withScoringOfIdenticalResponses($scoring_of_identical_responses) - ->withCombinationsEnabled($combinations_enabled); + $already_added[] = $v['combination_id'] . $v['gap_id']; + + $gap = $gaps->getGapById( + $this->uuid_factory->fromString($v['gap_id']) + ); + + $combinations[$v['combination_id']] = $combinations[$v['combination_id']] + ->withAdditionalMatchingValue( + new MatchingValue( + $this->uuid_factory->fromString($v['combination_id']), + $gap, + $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($v['answer_option_id']) + ) + ) + ); + } + + return array_values($combinations); + } + ) + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php new file mode 100644 index 000000000000..a32b62eff39c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php @@ -0,0 +1,38 @@ + $lng->txt('in_range'), + self::OutOfRange => $lng->txt('out_of_range') + }; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php new file mode 100644 index 000000000000..6f7a97fbc520 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -0,0 +1,100 @@ +gap; + } + + public function getAnswerOption(): ?AnswerOption + { + return $this->answer_option; + } + + public function buildPresentationString( + Language $lng + ): string { + if ($this->answer_option === null) { + return ''; + } + + if ($this->in_range !== null) { + return $this->in_range->getLabel($lng); + } + + $value = $this->answer_option->getTextValue(); + if (strlen($value) < 11) { + return $value; + } + + return mb_substr($value, 0, 10) . '...'; + } + + public function toStorage( + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Replace { + if ($this->answer_option === null) { + throw new \UnexpectedValueException( + 'A MatchingValue without AnswerOption cannot be stored.' + ); + } + + $table_definition = TableTypes::Additional; + return new Replace( + $persistence->getColumns( + $table_name_builder, + $table_definition, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + new Value(\ilDBConstants::T_TEXT, $this->combination_id->toString()), + new Value(\ilDBConstants::T_TEXT, $this->gap->getAnswerInputId()->toString()), + new Value(\ilDBConstants::T_TEXT, $this->answer_option->getAnswerOptionId()->toString()), + new Value(\ilDBConstants::T_TEXT, $this->in_range?->value) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 9df088c63851..44dab3e82a0e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Factory as CombinationsFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; use ILIAS\Questions\Persistence\Query; @@ -32,7 +33,8 @@ class Factory { public function __construct( private readonly ClozeTextFactory $cloze_text_factory, - private readonly GapsFactory $gaps_factory + private readonly GapsFactory $gaps_factory, + private readonly CombinationsFactory $combinations_factory ) { } @@ -49,8 +51,13 @@ public function fromData( $type_generic_properties->getAdditionalText() ), $type_generic_properties->getAdditionalTextLegacy(), + ScoringIdentical::ScoreAll, $this->gaps_factory->getEmptyGapsObject( $type_generic_properties->getAnswerFormId() + ), + $this->combinations_factory->getCombinations( + $type_generic_properties, + false ) ); } @@ -76,6 +83,11 @@ function (array $vs) use ($type_generic_properties): array { ) ); + $gaps = $this->gaps_factory->fromDatabase( + $type_generic_properties->getAnswerFormId(), + $query + ); + return new Properties( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), @@ -84,12 +96,14 @@ function (array $vs) use ($type_generic_properties): array { $type_generic_properties->getAdditionalText() ), $type_generic_properties->getAdditionalTextLegacy(), - $this->gaps_factory->fromDatabase( - $type_generic_properties->getAnswerFormId(), - $query - ), $scoring_identical_responses, - $combinations_enabled + $gaps, + $this->combinations_factory->getCombinations( + $type_generic_properties, + $combinations_enabled, + $gaps, + $query + ) ); } @@ -109,7 +123,11 @@ public function fromForm( $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) )->withGaps($updated_gaps) ->withScoringOfIdenticalResponses($scoring_of_identical_responses) - ->withCombinationsEnabled($combinations_enabled); + ->withCombinations( + $properties->getCombinations()->withCombinationsEnabled( + $combinations_enabled + ) + ); } private function retrieveFirstMatchingRowFromDBRecords( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 8309f2c77eea..00bf3b3b704a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -50,6 +50,15 @@ public function isIncomplete(): bool || $this->answer_options_awarding_points === []; } + public function getAnswerOptionById( + Uuid $answer_option_id + ): ?AnswerOption { + return array_find( + $this->answer_options, + fn(AnswerOption $v): bool => $v->getAnswerOptionId()->toString() === $answer_option_id->toString() + ); + } + public function getAnswerOptionForPositionOrNew( int $position ): AnswerOption { @@ -185,7 +194,7 @@ function (AnswerOption $v) use ($refinery, $values_from_form): AnswerOption { return $clone; } - public function buildArrayForInput( + public function buildArrayForSelectInput( Transformation $shuffle_transformation ): array { return array_reduce( @@ -221,7 +230,7 @@ public function getEditPointsInputs( $answer_options_awarding_points ?? $this->answer_options, function (array $c, AnswerOption $v) use ($ff, $build_label): array { $c[$v->getAnswerOptionId()->toString()] = $ff->numeric($build_label($v)) - ->withStepSize(0.0001) + ->withStepSize(0.01) ->withValue($v->getAvailablePoints()); return $c; }, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php index bf71bfed14b5..fde7139f60e7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -87,6 +87,10 @@ function (array $vs): array { $return_array = []; $answer_options = []; foreach ($vs as $v) { + if (array_key_exists($v['id'], $answer_options)) { + continue; + } + if ($previous_answer_input_id !== null && $v['answer_input_id'] !== $previous_answer_input_id) { $return_array[$previous_answer_input_id] = new AnswerOptions( @@ -97,7 +101,7 @@ function (array $vs): array { $answer_options = []; } $previous_answer_input_id = $v['answer_input_id']; - $answer_options[] = new AnswerOption( + $answer_options[$v['id']] = new AnswerOption( $this->uuid_factory->fromString($v['id']), $this->uuid_factory->fromString($v['answer_input_id']), $v['position'], diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 8aa75827cdf8..20c9305e66a0 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -35,8 +35,9 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Input\Field\Section; +use ILIAS\UI\Component\Input\Field\MultiSelect; use ILIAS\UI\Component\Table\DataRowBuilder; class Gaps @@ -73,6 +74,11 @@ public function getGapByTagName( return $this->gaps[$this->extractIdFromTagName($tag_name)] ?? null; } + public function getNumberOfGaps( + ): int { + return count($this->gaps); + } + public function hasAtLeastOneGap(): bool { return $this->gaps !== []; @@ -267,8 +273,26 @@ function (array $c, Gap $v) use ($lng, $ff): array { ); } - public function getCarryInputs(FieldFactory $ff): Group - { + public function buildGapsMultiSelect( + string $label, + FieldFactory $ff + ): MultiSelect { + return $ff->multiSelect( + $label, + array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->buildShortenedGapName(); + return $c; + }, + [] + ) + ); + } + + public function getCarryInputs( + FieldFactory $ff + ): Group { return $ff->group( array_reduce( $this->gaps, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index e2d3ee9e2852..773e17b34c8b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -72,7 +72,7 @@ public function getParticipantViewLegacyInput( . "{$gap->getMinAutocomplete()}, " . json_encode( array_values( - $gap->getAnswerOptions()->buildArrayForInput( + $gap->getAnswerOptions()->buildArrayForSelectInput( $this->refinery->random()->dontShuffle() ) ) @@ -177,12 +177,6 @@ public function getBuildGapTransformation( ); } - #[\Override] - public function getAnswerInput(): \ilFormPropertyGUI - { - ; - } - private function retrieveAnswerOptionsArrayFromUpload( ?array $upload_value ): array { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index 33349d9f33c8..540a6f45b2f5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Language\Language; @@ -148,8 +149,13 @@ public function getBuildGapTransformation( } #[\Override] - public function getAnswerInput(): \ilFormPropertyGUI - { - ; + public function getCombinationsSelectValues( + Gap $gap + ): array { + $values = []; + foreach (InRange::cases() as $in_range) { + $values[$in_range->value] = $in_range->getLabel($this->lng); + } + return $values; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 6a1afbbf7671..bef4eeadf8f4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -62,7 +62,7 @@ public function getParticipantViewLegacyInput( ? $this->refinery->random()->shuffleArray(new GivenSeed(4)) : $this->refinery->random()->dontShuffle(); - foreach ($gap->getAnswerOptions()->buildArrayForInput($shuffler) as $key => $answer_option) { + foreach ($gap->getAnswerOptions()->buildArrayForSelectInput($shuffler) as $key => $answer_option) { $gaptemplate->setCurrentBlock('select_gap_option'); $gaptemplate->setVariable( 'SELECT_GAP_VALUE', @@ -149,10 +149,4 @@ public function getBuildGapTransformation( )->withShuffleAnswerOptions($vs['shuffle_answer_options']) ); } - - #[\Override] - public function getAnswerInput(): \ilFormPropertyGUI - { - ; - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 7ef1fc75d296..851b45ffc422 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -148,10 +148,4 @@ public function getBuildGapTransformation( ) ); } - - #[\Override] - public function getAnswerInput(): \ilFormPropertyGUI - { - ; - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index becbef79ccf9..e9344e48b384 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -54,7 +54,13 @@ abstract public function getBuildGapTransformation( Gap $gap ): Transformation; - abstract public function getAnswerInput(): \ilFormPropertyGUI; + public function getCombinationsSelectValues( + Gap $gap + ): array { + return $gap->getAnswerOptions()->buildArrayForSelectInput( + $this->refinery->random()->dontShuffle() + ); + } public function getAddPointsTransformation( Gap $gap diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index caefc207870f..6a4fb96cd06f 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -27,6 +27,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Layout\OverviewTable; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; @@ -59,6 +60,8 @@ class Properties implements PropertiesInterface private const string FORM_KEY_ENABLE_COMBINATIONS = 'enable_combinations'; private const string FORM_KEY_GAPS_TO_EDIT = 'gaps'; + private bool $updated_combinations = false; + /** * @param array $gaps */ @@ -68,9 +71,9 @@ public function __construct( private readonly Definition $definition, private Text $cloze_text, private readonly string $legacy_cloze_text, + private ScoringIdentical $scoring_identical, private Gaps $gaps, - private ScoringIdentical $scoring_identical = ScoringIdentical::ScoreAll, - private bool $combinations_enabled = false + private Combinations $combinations ) { } @@ -145,16 +148,17 @@ public function withScoringOfIdenticalResponses( return $clone; } - public function areCombinationsEnabled(): bool + public function getCombinations(): Combinations { - return $this->combinations_enabled; + return $this->combinations; } - public function withCombinationsEnabled( - bool $combinations_enabled + public function withCombinations( + Combinations $combinations ): self { $clone = clone $this; - $clone->combinations_enabled = $combinations_enabled; + $clone->combinations = $combinations; + $clone->updated_combinations = true; return $clone; } @@ -180,7 +184,7 @@ public function getBasicPropertiesForListing( ->getRenderedMarkdownForEditingPresentation($this->gaps), $lng->txt('score_identical') => $this->scoring_identical ->getTranslatedOptionName($lng), - $lng->txt('gap_combinations') => $this->combinations_enabled + $lng->txt('gap_combinations') => $this->combinations->areCombinationsEnabled() ? $lng->txt('enabled') : $lng->txt('disabled') ]; @@ -222,7 +226,7 @@ public function buildBasicEditingInputs( $this->scoring_identical )->withValue($this->getScoringOfIdenticalResponses()->value), self::FORM_KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) - ->withValue($this->areCombinationsEnabled()) + ->withValue($this->combinations->areCombinationsEnabled()) ], $lng->txt('create_answer_form') )->withAdditionalTransformation( @@ -253,7 +257,7 @@ public function buildCarryInputs( ->withValue($this->getScoringOfIdenticalResponses()->value), self::FORM_KEY_ENABLE_COMBINATIONS => $ff->hidden() ->withDedicatedName(self::FORM_KEY_ENABLE_COMBINATIONS) - ->withValue($this->areCombinationsEnabled() ? 1 : 0) + ->withValue($this->combinations->areCombinationsEnabled() ? 1 : 0) ] ); } @@ -289,11 +293,15 @@ public function withValuesFromCarry( ]) ); - $clone->combinations_enabled = $carry->retrieve( + $clone->combinations = $carry->retrieve( self::FORM_KEY_ENABLE_COMBINATIONS, $refinery->byTrying([ - $refinery->kindlyTo()->bool(), - $refinery->always($clone->combinations_enabled) + $refinery->custom()->transformation( + fn($v): Combinations => $clone->combinations->withCombinationsEnabled( + $refinery->kindlyTo()->bool()->transform($v) + ) + ), + $refinery->always($clone->combinations) ]) ); @@ -333,7 +341,11 @@ public function toStorage( ); return $this->gaps->toStorage( - $manipulate->withAdditionalStatement( + $this->addReplaceCombinationsStatements( + $manipulate, + $persistence, + $table_name_builder + )->withAdditionalStatement( $answer_form_statement ), $persistence, @@ -378,7 +390,10 @@ private function buildInsertAnswerFormStatement( [ new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), - new Value(\ilDBConstants::T_INTEGER, $this->combinations_enabled ? 1 : 0) + new Value( + \ilDBConstants::T_INTEGER, + $this->combinations->areCombinationsEnabled() ? 1 : 0 + ) ] ); } @@ -392,12 +407,15 @@ private function buildUpdateAnswerFormStatement( $persistence->getColumns( $table_name_builder, $table_definition, - null, + '', ['answer_form_id'] ), [ new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), - new Value(\ilDBConstants::T_INTEGER, $this->combinations_enabled ? 1 : 0) + new Value( + \ilDBConstants::T_INTEGER, + $this->combinations->areCombinationsEnabled() ? 1 : 0 + ) ], [ new Where( @@ -411,6 +429,23 @@ private function buildUpdateAnswerFormStatement( ); } + private function addReplaceCombinationsStatements( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + if (!$this->combinations->areCombinationsEnabled() + || !$this->updated_combinations) { + return $manipulate; + } + + return $this->combinations->toStorage( + $manipulate, + $persistence, + $table_name_builder + ); + } + private function buildDeleteAnswerFormStatement( Persistence $persistence, TableNameBuilder $table_name_builder diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 64eb908e2555..7f4acf5ed02e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -27,15 +27,17 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\HTTP\Services as HTTPServices; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; -use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\URLBuilder; class Edit implements EditViewInterface { @@ -52,6 +54,7 @@ class Edit implements EditViewInterface public function __construct( private readonly Language $lng, private readonly UIFactory $ui_factory, + private readonly \ilToolbarGUI $toolbar, private readonly Refinery $refinery, private readonly HTTPServices $http, private readonly PropertiesFactory $properties_factory, @@ -78,6 +81,17 @@ public function edit( ): EditOverview|EditForm|Properties { $step = $environment->getStep(); + $combinations = $environment->getAnswerFormProperties()->getCombinations(); + if ($combinations->areCombinationsEnabled()) { + $combinations->getEditView( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http + )->addCombinationsSubTab($environment); + } + if ($step === '') { return $environment->getPresentationFactory()->getEditOverview( $environment, @@ -97,8 +111,23 @@ public function edit( #[\Override] public function other( Environment $environment - ): EditForm|Properties { + ): Async|Renderable|Properties { + return $environment + ->getAnswerFormProperties() + ->getCombinations()->getEditView( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http + )->show($environment); + } + #[\Override] + public function getFinishEditingUrl( + Environment $environment + ): URLBuilder { + return $environment->getUrlBuilder(); } private function callIntermediateStep( @@ -116,8 +145,7 @@ private function callIntermediateStep( $this->cloze_text_factory, $this->gap_factory, $environment->getPresentationFactory()->getCarrySectionData( - $this->http->wrapper()->post(), - $this->refinery + $this->http->wrapper()->post() ) ) ); @@ -219,7 +247,11 @@ private function buildGapTypesForm( $properties->withClozeText($properties->getClozeText()) ->buildCarryInputs($ff) )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps() + ) ); } @@ -256,7 +288,11 @@ private function buildAnswerOptionsForm( false, $properties->buildCarryInputs($ff) )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps() + ) ); } @@ -293,7 +329,11 @@ private function buildAssignPointsForm( true, $properties->buildCarryInputs($ff) )->withContentBeforeForm( - $this->buildClozeTextPanel($properties) + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps() + ) ); } @@ -308,21 +348,12 @@ private function processAssignPointsForm( $data = $form->getData(); return $data === null ? $form->withContentBeforeForm( - $this->buildClozeTextPanel($properties) - ) : $properties->withGaps($data); - } - - private function buildClozeTextPanel( - Properties $properties - ): StandardPanel { - return $this->ui_factory->panel()->standard( - $this->lng->txt('cloze_text'), - $this->ui_factory->legacy()->content( - $properties->getClozeText()->getRenderedMarkdownForEditingPresentation( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, $properties->getGaps() ) - ) - ); + ) : $properties->withGaps($data); } /** diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 1b9f1f8a2bd0..156a9ea93283 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -24,7 +24,6 @@ use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; -use ILIAS\Questions\Setup\QuestionsMigration; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; diff --git a/components/ILIAS/Questions/src/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Persistence/TableTypes.php index 06116bfe7059..a12804600aa5 100644 --- a/components/ILIAS/Questions/src/Persistence/TableTypes.php +++ b/components/ILIAS/Questions/src/Persistence/TableTypes.php @@ -30,7 +30,7 @@ enum TableTypes public function getTable( TableNameBuilder $table_name_builder, - ?string $table_identifier = null + string $table_identifier = '' ): Table { return match($this) { self::Additional => new Table( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 9c112f518e33..0ef27b1057ce 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -30,8 +30,11 @@ interface Environment public function setEditAnswerFormBackTarget(): void; public function addEditAnswerFormSubTab( - string $id, - string $text, + string $step, + string $text + ): void; + + public function activateEditAnswerFormSubTab( string $step ): void; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index cc5adc752c91..f4baf4d425e7 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; +use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; @@ -29,6 +30,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Component\Input\Input; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -41,6 +43,7 @@ class EnvironmentImplementation implements Environment private const string TOKEN_STRING_QUESTION_IDS = 'qs'; private const string TOKEN_TYPE_HASH = 't'; private const string TOKEN_TABLE_ROW_ID = 'r'; + private const string TOKEN_CARRY_ID = 'c'; private const string INTERRUPTIVE_ITEMS_KEY = 'interruptive_items'; @@ -57,6 +60,7 @@ class EnvironmentImplementation implements Environment private readonly URLBuilderToken $question_ids_token; private readonly URLBuilderToken $type_hash_token; private readonly URLBuilderToken $table_row_token; + private readonly URLBuilderToken $carry_token; private ?array $table_row_ids = null; @@ -87,17 +91,28 @@ public function setEditAnswerFormBackTarget(): void #[\Override] public function addEditAnswerFormSubTab( - string $id, - string $text, - string $step + string $step, + string $language_variable ): void { $this->tabs_gui->addSubTab( - $id, - $text, - $this->getUrlBuilderWithStepParameter($step)->buildURI()->__toString() + $step, + $this->lng->txt($language_variable), + $this->getUrlBuilderWithStepParameter($step) + ->withParameter( + $this->action_token, + Edit::CMD_OTHER_ANSWER_FORM + )->buildURI() + ->__toString() ); } + #[\Override] + public function activateEditAnswerFormSubTab( + string $step + ): void { + $this->tabs_gui->activateSubTab($step); + } + #[\Override] public function getPresentationFactory(): Factory { @@ -237,6 +252,15 @@ public function withAnswerFormTypeHashParameter( return $clone; } + public function withCarryParameter( + string $carry + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->carry_token, $carry); + return $clone; + } + public function getQuestionId(): ?Uuid { return $this->http->wrapper()->query()->retrieve( @@ -283,11 +307,20 @@ public function getQuestionIds(): array|string|null ); } - public function getTypeClassHast(): string + public function getTypeClassHash(): string { return $this->retrieveStringValueForToken($this->type_hash_token); } + public function getCarry( + Transformation $to_form_transformation + ): Input|array|string|null { + return $this->http->wrapper()->query()->retrieve( + $this->carry_token->getName(), + $to_form_transformation + ); + } + public function setEditAnswerFormTabs( string $cmd_feedback, string $cmd_content_for_repetition @@ -316,7 +349,14 @@ public function setEditAnswerFormTabs( ->__toString() ); + $this->tabs_gui->addSubTab( + self::TAB_ID_ANSWER_FORM, + $this->lng->txt('overview'), + $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + $this->tabs_gui->activateTab(self::TAB_ID_ANSWER_FORM); + $this->tabs_gui->activateSubTab(self::TAB_ID_ANSWER_FORM); } public function setParametersForQuestionCmds(): void @@ -338,7 +378,8 @@ private function acquireURLBuilderAndParameters( $this->question_id_token, $this->question_ids_token, $this->type_hash_token, - $this->table_row_token + $this->table_row_token, + $this->carry_token ] = (new URLBuilder($base_uri)) ->acquireParameters( self::QUERY_PARAMETER_NAME_SPACE, @@ -347,7 +388,8 @@ private function acquireURLBuilderAndParameters( self::TOKEN_STRING_QUESTION_ID, self::TOKEN_STRING_QUESTION_IDS, self::TOKEN_TYPE_HASH, - self::TOKEN_TABLE_ROW_ID + self::TOKEN_TABLE_ROW_ID, + self::TOKEN_CARRY_ID ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index cad255b94886..3b6dfbd7c8b1 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -31,7 +31,7 @@ use ILIAS\UI\Renderer as UIRenderer; use Psr\Http\Message\ServerRequestInterface; -class EditForm +class EditForm implements Renderable { private const string MAIN_SECTION_NAME = 'form'; public const string CARRY_SECTION_NAME = 'carry'; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index 440884a95599..fe795f394183 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -29,7 +29,7 @@ use ILIAS\UI\Renderer as UIRenderer; use Psr\Http\Message\ServerRequestInterface; -class EditOverview +class EditOverview implements Renderable { public function __construct( private readonly UIFactory $ui_factory, diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index a2bc23fbc70d..77a0b02922a8 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -27,11 +27,13 @@ use ILIAS\HTTP\Services as HttpService; use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; use ILIAS\Language\Language; +use ILIAS\Refinery\Custom\Transformation as CustomTransformation; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Input\Input; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; @@ -40,6 +42,7 @@ class Factory { public function __construct( private readonly UIFactory $ui_factory, + private readonly Refinery $refinery, private readonly HttpService $http, private readonly Language $lng ) { @@ -83,16 +86,33 @@ public function getAsync( ); } + /** + * @param CustomTransformation $to_inputs This MUST return an `array` of + * inputs that will then be used in the form. The transformation will receive + * the string produced by `$to_carry` as parameter. + * @param Input|array|null $inputs If you provide inputs it is assumed that no + * carry is present and you want to use them directly. + */ + public function getInputsBuilder( + CustomTransformation $to_inputs, + Input|array|null $inputs = null + ): InputsBuilder { + return new InputsBuilder( + $this->refinery, + $to_inputs, + $inputs + ); + } + public function getCarrySectionData( - ArrayBasedRequestWrapper $post_wrapper, - Refinery $refinery + ArrayBasedRequestWrapper $post_wrapper ): CarryWrapper { return new CarryWrapper( array_reduce( $post_wrapper->keys(), - function (array $c, string $v) use ($post_wrapper, $refinery): array { + function (array $c, string $v) use ($post_wrapper): array { $value = new Leaf( - $post_wrapper->retrieve($v, $refinery->identity()) + $post_wrapper->retrieve($v, $this->refinery->identity()) ); foreach (array_reverse(explode('/', $v)) as $path_element) { $value = [$path_element => $value]; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php new file mode 100644 index 000000000000..6089cf295a80 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php @@ -0,0 +1,90 @@ +carry = $carry; + return $clone; + } + + public function addCarryToEnvironment( + EnvironmentImplementation $environment + ): Environment { + return $environment->withCarryParameter( + $this->carry === null + ? $environment->getCarry($this->refinery->identity()) + : base64_encode($this->carry) + ); + } + + public function getInputs( + EnvironmentImplementation $environment + ): Input|array { + if ($this->inputs === null) { + $this->buildInputs($environment); + } + + return $this->inputs; + } + + private function buildInputs( + EnvironmentImplementation $environment + ): void { + $this->inputs = $environment->getCarry( + $this->refinery->custom()->transformation( + fn(string $v): Input|array => $this->to_inputs->transform( + base64_decode($v) + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index b6e87a2e7c11..8653b2a97b42 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -34,7 +34,7 @@ use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; use Psr\Http\Message\ServerRequestInterface; -class QuestionsTable implements DataRetrieval +class QuestionsTable implements Renderable, DataRetrieval { public function __construct( private readonly UIFactory $ui_factory, diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php index cad255b94886..522b6d0def13 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php @@ -20,139 +20,11 @@ namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Language\Language; -use ILIAS\UI\Component\Input\Container\Form\Factory as FormFactory; -use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; -use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Input\Field\Group; -use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; -use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\UI\URLBuilder; use ILIAS\UI\Renderer as UIRenderer; -use Psr\Http\Message\ServerRequestInterface; -class EditForm +interface Renderable { - private const string MAIN_SECTION_NAME = 'form'; - public const string CARRY_SECTION_NAME = 'carry'; - - private StandardForm $form; - - private ?StandardPanel $content_before_form = null; - private ?StandardPanel $content_after_form = null; - private ?InterruptiveModal $confirmation = null; - - public function __construct( - private readonly FormFactory $form_factory, - private readonly Language $lng, - private readonly URLBuilder $url_builder, - private readonly Section $main_section_inputs, - private readonly bool $is_final_step, - private readonly ?Group $carry_inputs - ) { - $this->form = $this->buildForm(); - } - - public function withContentBeforeForm( - StandardPanel $content - ): self { - $clone = clone $this; - $clone->content_before_form = $content; - return $clone; - } - - public function withContentAfterForm( - StandardPanel $content - ): self { - $clone = clone $this; - $clone->content_after_form = $content; - return $clone; - } - - public function withConfirmation( - InterruptiveModal $confirmation_modal - ): self { - $clone = clone $this; - $clone->confirmation = $confirmation_modal; - return $clone; - } - public function render( UIRenderer $ui_renderer - ): string { - return $ui_renderer->render($this->buildContent()); - } - - public function withRequest( - ServerRequestInterface $request - ): self { - $clone = clone $this; - $clone->form = $clone->form->withRequest($request); - return $clone; - } - - public function getData(): mixed - { - $data = $this->form->getData(); - return $data[self::MAIN_SECTION_NAME] ?? null; - } - - private function buildContent(): array - { - $content = []; - - if ($this->content_before_form !== null) { - $content[] = $this->content_before_form; - } - - if ($this->confirmation !== null) { - $content[] = $this->confirmation->withOnLoad( - $this->confirmation->getShowSignal() - )->withAdditionalOnLoadCode( - function ($id) { - return "var button = {$id}.querySelector('input[type=\"submit\"]'); " - . "button.addEventListener('click', (e) => {e.preventDefault();" - . 'const form = button.closest("dialog").nextElementSibling;' - . "form.action = '{$this->confirmation->getFormAction()}';" - . 'form.submit();});'; - } - ); - } - - $content[] = $this->form; - - if ($this->content_after_form !== null) { - $content[] = $this->content_after_form; - } - - return $content; - } - - private function buildForm(): StandardForm - { - $form = $this->form_factory->standard( - $this->url_builder->buildURI()->__toString(), - $this->buildFormInputs() - ); - - if ($this->is_final_step) { - return $form->withSubmitLabel($this->lng->txt('save')); - } - - return $form->withSubmitLabel($this->lng->txt('next')); - } - - private function buildFormInputs(): array - { - $form_inputs = [ - self::MAIN_SECTION_NAME => $this->main_section_inputs - ]; - - if ($this->carry_inputs !== null) { - $form_inputs[self::CARRY_SECTION_NAME] = $this->carry_inputs - ->withDedicatedName(self::CARRY_SECTION_NAME); - } - - return $form_inputs; - } + ): string; } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index f4c1bb110701..5ed3121d0034 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -22,8 +22,8 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\EditOverview; -use ILIAS\Questions\Presentation\Layout\Factory as DefinitionsFactory; +use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; +use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Layout\QuestionsTable; @@ -31,7 +31,6 @@ use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; -use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; @@ -55,7 +54,7 @@ class Edit public const string CMD_EDIT_QUESTION = 'edit'; public const string CMD_DELETE_QUESTION = 'delete'; private const string CMD_CREATE_ANSWER_FORM = 'create_af'; - private const string CMD_EDIT_ANSWER_FORM = 'edit_af'; + public const string CMD_OTHER_ANSWER_FORM = 'other_af'; private const string CMD_EDIT_FEEDBACK = 'edit_f'; private const string CMD_EDIT_CONTENT_FOR_REPETITION = 'edit_cfr'; @@ -79,7 +78,7 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, - private readonly DefinitionsFactory $definitions_factory + private readonly LayoutFactory $definitions_factory ) { } @@ -108,7 +107,7 @@ public function withOrderingEnabled( return $clone; } - public function view( + public function show( \ilToolbarGUI $toolbar, URI $base_uri, int $obj_id, @@ -180,7 +179,7 @@ public function createAnswerForm( )->withActionParameter(self::CMD_CREATE_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); - $answer_form_type_class_hash = $environment->getTypeClassHast(); + $answer_form_type_class_hash = $environment->getTypeClassHash(); if ($answer_form_type_class_hash !== '') { $type = $this->answer_form_factory @@ -190,7 +189,8 @@ public function createAnswerForm( $environment->withAnswerFormProperties( $type->buildProperties( $this->answer_form_factory->getDefaultTypeGenericProperties( - $question->getId() + $question->getId(), + $type ), null ) @@ -205,8 +205,7 @@ public function createAnswerForm( self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( $environment, $question, - $content_object, - $this->answer_form_factory->getDefaultTypeGenericProperties($question->getId()) + $content_object ), default => $this->buildCreateAnswerForm($environment) }; @@ -218,12 +217,11 @@ public function editAnswerForm( QuestionImplementation $question, AnswerFormProperties $answer_form_properties, Definition $type - ): Async|EditForm|EditOverview { + ): Async|Renderable { $environment = $this->buildEnvironment( $base_uri, $obj_id )->withAnswerFormProperties($answer_form_properties) - ->withActionParameter(self::CMD_EDIT_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); $environment->setEditAnswerFormTabs( @@ -231,17 +229,26 @@ public function editAnswerForm( self::CMD_EDIT_CONTENT_FOR_REPETITION ); - $edit = $type->getEditView()->edit($environment); + $edit_view = $type->getEditView(); - if (!($edit instanceof AnswerFormProperties)) { - return $edit; + if ($environment->getAction() === self::CMD_OTHER_ANSWER_FORM) { + $environment = $environment->withActionParameter(self::CMD_OTHER_ANSWER_FORM); + $next = $edit_view->other($environment); + } else { + $next = $edit_view->edit($environment); + } + + if (!($next instanceof AnswerFormProperties)) { + return $next; } $this->questions_repository->update( - [$question->withAnswerForm($edit)] + [$question->withAnswerForm($next)] ); - $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); + $this->ctrl->redirectToURL( + $edit_view->getFinishEditingUrl($environment)->buildURI()->__toString() + ); } private function createQuestion( @@ -382,8 +389,7 @@ private function showTable( private function processCreateAnswerForm( EnvironmentImplementation $environment, QuestionImplementation $question, - \ilPCAnswerForm $content_obj, - TypeGenericProperties $generic_answer_form_properties + \ilPCAnswerForm $content_object ): EditForm { $form = $this->buildCreateAnswerForm($environment)->withRequest($this->http->request()); @@ -393,14 +399,17 @@ private function processCreateAnswerForm( : $this->forwardCreateAnswerFormCmd( $environment->withAnswerFormProperties( $data->buildProperties( - $generic_answer_form_properties, + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId(), + $data + ), null ) )->withAnswerFormTypeHashParameter( $this->answer_form_factory->getHashedClass($data::class) ), $question, - $content_obj, + $content_object, $data->getEditView() ); } @@ -408,7 +417,7 @@ private function processCreateAnswerForm( private function forwardCreateAnswerFormCmd( EnvironmentImplementation $environment, QuestionImplementation $question, - \ilPCAnswerForm $content_obj, + \ilPCAnswerForm $content_object, AnswerFormEditView $answer_form_edit_view ): ?EditForm { $create = $answer_form_edit_view->create($environment); @@ -421,8 +430,8 @@ private function forwardCreateAnswerFormCmd( [$question->withAnswerForm($create)] ); - $content_obj->create($create->getAnswerFormId()); - $content_obj->getPage()->update(); + $content_object->create($create->getAnswerFormId()); + $content_object->getPage()->update(); $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); } diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index f9079f62090f..2ca3f8714441 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Setup; +use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; @@ -179,7 +180,10 @@ public function step_3(): void public function step_4(): void { - $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Additional, 'combinations'); + $table_name = $this->table_name_builder->getTableNameFor( + TableTypes::Additional, + Persistence::COMBINATION_TABLE_IDENTIFIER + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -192,11 +196,6 @@ public function step_4(): void 'length' => 64, 'notnull' => true ], - 'answer_options' => [ - 'type' => \ilDBConstants::T_TEXT, - 'length' => 4000, - 'notnull' => true - ], 'points' => [ 'type' => \ilDBConstants::T_FLOAT, 'notnull' => false @@ -214,6 +213,46 @@ public function step_4(): void } public function step_5(): void + { + $table_name = $this->table_name_builder->getTableNameFor( + TableTypes::Additional, + Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'combination_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'gap_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_option_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'in_range' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 16, + 'notnull' => false + ] + ]); + } + + if (!$this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id'])) { + $this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['combination_id'])) { + $this->db->addIndex($table_name, ['combination_id'], 'ci'); + } + } + + public function step_6(): void { $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Responses); if (!$this->db->tableExists($table_name)) { From 323016d35f1c3e52bd912cad3f390290bfda11cd Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 28 Jan 2026 15:59:05 +0100 Subject: [PATCH 035/108] Questions: Add LangVar For AnswerForm --- lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 2 files changed, 2 insertions(+) diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index db0654f2f2e0..fac960933e89 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -14126,6 +14126,7 @@ qpl#:#qpl_page_type_qfbg#:#Generelles Feedback qpl#:#qpl_page_type_qfbs#:#Spezielles Feedback qpl#:#qpl_page_type_qht#:#Hinweis qpl#:#qpl_page_type_qpl#:#Fragenseite +qsts#:#answer_form#:#Antwort Formular qsts#:#cloze_enable_combinations#:#Enable Gap Combination qsts#:#cont_ed_insert_answf#:#Antwortformular einfügen qsts#:#create_answer_form#:#Antwortformular erstellen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index fe226ed2e22a..306f683e8b6e 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -14097,6 +14097,7 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback qpl#:#qpl_page_type_qfbs#:#Special Feedback qpl#:#qpl_page_type_qht#:#Hint qpl#:#qpl_page_type_qpl#:#Question Page +qsts#:#answer_form#:#Answer Form qsts#:#cloze_enable_combinations#:#Lückenkombinationen aktivieren qsts#:#cont_ed_insert_answf#:#Insert Answer Form qsts#:#create_answer_form#:#Create Answer Form From b2aa5065e88023bb42d8c648176e77b412bbfff2 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 28 Jan 2026 16:19:57 +0100 Subject: [PATCH 036/108] Questions: Implement Migration For TextSubset --- components/ILIAS/Questions/Questions.php | 4 + .../Cloze/Migration/MigrationTextSubset.php | 100 +++++++++--------- 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 66a3852d69c8..0354c1e3430b 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -25,6 +25,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationCloze; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationLongMenu; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationNumeric; +use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationTextSubset; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\Setup\Agent; @@ -58,6 +59,9 @@ public function init( $internal[Persistence::class], $internal[\EvalMath::class] ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationTextSubset( + $internal[Persistence::class] + ); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index ad9825006ed1..2b5af74ffdeb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -26,22 +26,21 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Data\UUID\Uuid; -class MigrationNumeric implements Migration +class MigrationTextSubset implements Migration { use BasicMigrationFunctions; public function __construct( - private readonly Persistence $persistence, - private readonly \EvalMath $math + private readonly Persistence $persistence ) { - $this->math->suppress_errors = true; } #[\Override] public function getOldQuestionIdentifier(): string { - return 'assNumeric'; + return 'assTextSubset'; } #[\Override] @@ -60,50 +59,54 @@ public function getTableNameSpace(): TableNameSpace public function buildInsertStatement( MigrationInsert $migration_insert ): MigrationInsert { - $db_row = $this->fetchDBValues( + $gaps = []; + foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() - )->current(); + ) as $db_row) { + $answer_form_id = $migration_insert->getAnswerFormId(); - $answer_form_id = $migration_insert->getAnswerFormId(); - $gap_id = $migration_insert->getUuid(); - return $migration_insert->withAdditionalInsert( - $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $gap_id, - $answer_form_id, - 0, - 'numeric', - null, - 0.0001, - null, - null, - null - ) - )->withAdditionalInsert( - $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $migration_insert->getUuid(), - $gap_id, - 0, - '', - $db_row->points, - $this->limitToFloat($this->math, $db_row->lowerlimit), - $this->limitToFloat($this->math, $db_row->upperlimit) - ) - )->withAdditionalInsert( - $this->buildAnswerFormInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $answer_form_id, - ScoringIdentical::ScoreAll, - 0 - ) - )->withAdditionalTextLegacy( - '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gap_id->toString()) . '}}' - ); + if ($gaps === []) { + for ($i = 0; $i < $db_row->correctanswers; $i++) { + $gaps[] = $migration_insert->getUuid(); + } + } + + foreach ($gaps as $gap_id) { + $migration_insert = $migration_insert->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $migration_insert->getUuid(), + $gap_id, + $db_row->aorder, + $db_row->answertext, + $db_row->points, + null, + null + ) + ); + } + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_form_id, + ScoringIdentical::OnlyScoreDistinct, + false + ) + )->withAdditionalText( + implode( + "\n\n", + array_map( + fn(Uuid $v) => "{{GAP_{$v->toString()}}}", + $gaps, + ) + ) + ); } private function fetchDBValues( @@ -111,8 +114,9 @@ private function fetchDBValues( int $old_question_id ): \Generator { $query = $db->query( - 'SELECT points, lowerlimit, upperlimit FROM qpl_num_range' . PHP_EOL - . "WHERE q.question_fi = {$db->quote($old_question_id)}" + 'SELECT * FROM qpl_qst_textsubset q' . PHP_EOL + . 'JOIN qpl_a_textsubset a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL ); while (($row = $db->fetchObject($query)) !== null) { From b43d6c5179cd2530a5784adba56ea13570bdfc62 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 28 Jan 2026 18:36:06 +0100 Subject: [PATCH 037/108] Questions: Speedup Loading Lists --- .../Questions/src/Persistence/Repository.php | 49 ++++++++++++++++--- .../Presentation/Layout/QuestionsTable.php | 2 +- .../Questions/src/Presentation/Views/Edit.php | 6 +-- .../src/Question/QuestionImplementation.php | 42 ++++++++++++++++ 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 156a9ea93283..aa9eda087481 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -50,15 +50,48 @@ public function getNew( /** * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ - public function getAllQuestions(): \Generator + public function getQuestionDataOnlyForAllQuestions(): \Generator { - yield from $this->getForBaseQuery( - new Query( - $this->db, - $this->answer_form_factory, - $this->refinery + foreach ((new Query( + $this->db, + $this->answer_form_factory, + $this->refinery + ))->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + [] + ); + } + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getQuestionDataOnlyForQuestionIds( + array $question_ids + ): \Generator { + foreach ((new Query( + $this->db, + $this->answer_form_factory, + $this->refinery + )->withAdditionalWhere( + new Where( + CoreTables::Questions->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + array_map( + fn(Uuid $v): string => $v->toString(), + $question_ids + ) + ), + Operator::In ) - ); + ))->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + [] + ); + } } public function getForQuestionId( @@ -219,7 +252,7 @@ private function retrieveQuestionFromQuery( ) ); - if ($question->getPageId() !== 0) { + if ($answer_forms === [] || $question->getPageId() !== 0) { return $question; } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 8653b2a97b42..ce58b1f44cc9 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -67,7 +67,7 @@ public function getRows( $environment_with_action = $this->environment->withActionParameter( Edit::CMD_EDIT_QUESTION ); - foreach ($this->questions_repository->getAllQuestions() as $question) { + foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { yield $question->toTableRow( $row_builder, $this->ui_factory, diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 5ed3121d0034..76e95d60b572 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -491,7 +491,7 @@ private function buildItemGroupForQuestionListSlate( $environment->withActionParameter(self::CMD_EDIT_QUESTION) ) ), - iterator_to_array($this->questions_repository->getAllQuestions()) + iterator_to_array($this->questions_repository->getQuestionDataOnlyForAllQuestions()) ) ); } @@ -545,8 +545,8 @@ private function buildAffectedItems( string|array $question_ids ): array { $questions = $question_ids === 'ALL_OBJECTS' - ? $this->questions_repository->getAllQuestions() - : $this->questions_repository->getForQuestionIds($question_ids); + ? $this->questions_repository->getQuestionDataOnlyForAllQuestions() + : $this->questions_repository->getQuestionDataOnlyForQuestionIds($question_ids); $affected_items = []; foreach ($questions as $question) { $affected_items[] = $this->ui_factory->modal()->interruptiveItem()->standard( diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 5eba94dd88b1..dc53fde12789 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -319,6 +319,10 @@ public function toDelete( return $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate->withAdditionalStatement( $this->buildDeleteQuestionStatement() + )->withAdditionalStatement( + $this->buildDeleteLinkingStatement() + )->withAdditionalStatement( + $this->buildDeleteMigrationStatement() ), $this->answer_forms ); @@ -509,6 +513,44 @@ private function buildDeleteQuestionStatement(): Delete ); } + private function buildDeleteLinkingStatement(): Delete + { + $table_definition = CoreTables::Linking; + return new Delete( + $table_definition->getTable(), + [ + new Where( + $table_definition->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + /** + * skergomard, 2026-01-86: This we only need while the migrations exist, after + * this MUST go! + */ + private function buildDeleteMigrationStatement(): Delete + { + $table_definition = CoreTables::MigrationsTable; + return new Delete( + $table_definition->getTable(), + [ + new Where( + $table_definition->getIdColumn(), + new Value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + /** * skergomard, 2026-01-26: This we only need while the migrations exist, after * this a question MUST never change the page assigned to it after its creation! From 0e48eeaccf9f85a5d7fae598cfc8c00fddfcbf6c Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 29 Jan 2026 13:00:42 +0100 Subject: [PATCH 038/108] Questions: Improvements in Migration and Display --- .../src/AnswerForm/Migration/Migration.php | 6 +- .../Cloze/Migration/MigrationCloze.php | 35 +++++--- .../Cloze/Migration/MigrationLongMenu.php | 25 ++++-- .../Cloze/Migration/MigrationNumeric.php | 19 +++-- .../Cloze/Migration/MigrationTextSubset.php | 15 +++- .../Cloze/Properties/Gaps/Factory.php | 3 +- .../Questions/src/Persistence/CoreTables.php | 3 +- .../Questions/src/Persistence/JoinType.php | 2 +- .../ILIAS/Questions/src/Persistence/Query.php | 13 +-- .../Questions/src/Persistence/Repository.php | 6 +- .../Presentation/Layout/QuestionsTable.php | 5 +- .../Questions/src/Presentation/Views/Edit.php | 62 +++++++++----- .../src/Setup/OverarchingQuestionTables.php | 11 ++- .../src/Setup/QuestionsMigration.php | 84 +++++++++++-------- 14 files changed, 187 insertions(+), 102 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php index 590824fb049c..326bb0349021 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerForm\Migration; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Setup\Environment; interface Migration { @@ -34,7 +35,8 @@ public function getDefinitionClass(): string; public function getTableNameSpace(): TableNameSpace; - public function buildInsertStatement( + public function completeMigrationInsert( + Environment $environment, MigrationInsert $migration_insert - ): MigrationInsert; + ): ?MigrationInsert; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 99133c7f92c3..fed99b2ddaaf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -29,6 +29,8 @@ use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Setup\Environment; class MigrationCloze implements Migration { @@ -59,11 +61,13 @@ public function getTableNameSpace(): TableNameSpace } #[\Override] - public function buildInsertStatement( + public function completeMigrationInsert( + Environment $environment, MigrationInsert $migration_insert - ): MigrationInsert { + ): ?MigrationInsert { $answer_input_mapping = []; $answer_options_mapping = []; + foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() @@ -110,6 +114,10 @@ public function buildInsertStatement( ); } + if (!isset($db_row)) { + return null; + } + if ($db_row->combinations_enabled) { $migration_insert = $this->addCombinationInserStatements( $migration_insert, @@ -161,7 +169,7 @@ private function fetchCombinationsDBValues( int $old_question_id ): \Generator { $query = $db->query( - 'SELECT combination_id, gap_fi, answer, points FROM qpl_a_cloze_combi_res' . PHP_EOL + 'SELECT combination_id, gap_fi, answer, points, row_id FROM qpl_a_cloze_combi_res' . PHP_EOL . "WHERE question_fi = {$db->quote($old_question_id)}" . PHP_EOL . 'ORDER BY combination_id, row_id' ); @@ -181,12 +189,21 @@ private function addCombinationInserStatements( $migration_insert->getDb(), $migration_insert->getOldQuestionId() ) as $db_row) { + if ($db_row->answer !== 'out_of_bound' + && !isset($answer_options_mapping[$db_row->gap_fi][$db_row->answer])) { + continue; + } + + $answer_option = $db_row->answer === 'out_of_bound' + ? reset($answer_options_mapping[$db_row->gap_fi]) + : $answer_options_mapping[$db_row->gap_fi][$db_row->answer]; + if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); $migration_insert = $migration_insert->withAdditionalInsert( new Insert( $this->persistence->getColumns( - $this->persistence->getTableNameSpace(), + $migration_insert->getTableNameBuilder(), TableTypes::Additional, $this->persistence->getCombinationsTableIdentifier() ), @@ -205,14 +222,10 @@ private function addCombinationInserStatements( ); } - $answer_option = $db_row->answer === 'out_of_bound' - ? reset($answer_options_mapping[$db_row->gap_fi]) - : $answer_options_mapping[$db_row->gap_fi][$db_row->answer]; - $migration_insert = $migration_insert->withAdditionalInsert( new Insert( $this->persistence->getColumns( - $this->persistence->getTableNameSpace(), + $migration_insert->getTableNameBuilder(), TableTypes::Additional, $this->persistence->getCombinationToAnswerOptionsTableIdentifier() ), @@ -223,11 +236,11 @@ private function addCombinationInserStatements( ), new Value( \ilDBConstants::T_TEXT, - $answer_input_mapping[$db_row->gap_fi] + $answer_input_mapping[$db_row->gap_fi]->toString() ), new Value( \ilDBConstants::T_TEXT, - $answer_option['answer_option_id'] + $answer_option['answer_option_id']->toString() ), new Value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 59b622f36ed3..842d9f8d6b4d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -25,6 +25,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Setup\Environment; class MigrationLongMenu implements Migration { @@ -54,9 +55,10 @@ public function getTableNameSpace(): TableNameSpace } #[\Override] - public function buildInsertStatement( + public function completeMigrationInsert( + Environment $environment, MigrationInsert $migration_insert - ): MigrationInsert { + ): ?MigrationInsert { $answer_input_mapping = []; foreach ($this->fetchDBValues( @@ -78,7 +80,7 @@ public function buildInsertStatement( null, null, null, - $db_row->min_autocomplete, + $db_row->min_auto_complete, $db_row->shuffle_answers === '1' ? 1 : 0 ) ); @@ -90,17 +92,24 @@ public function buildInsertStatement( 'points' => 0.0 ], $this->loadAnswersFromFile( + $environment->getResource(Environment::RESOURCE_ILIAS_INI), + $environment->getResource(Environment::RESOURCE_CLIENT_ID), $migration_insert->getOldQuestionId(), $db_row->gap_number ) ); } - if ($answers[$db_row->position]['text'] === trim($db_row->answer_text)) { + if (isset($answers[$db_row->position]) + && $answers[$db_row->position]['text'] === trim($db_row->answer_text)) { $answers[$db_row->position]['points'] = $db_row->points; } } + if (!isset($db_row)) { + return null; + } + foreach ($answers as $position => $answer) { $migration_insert = $migration_insert->withAdditionalInsert( $this->buildAnswerOptionInsertStatement( @@ -129,7 +138,7 @@ public function buildInsertStatement( )->withAdditionalTextLegacy( $this->replaceGapsAndSantizeLegacyClozeText( '\[Longmenu \d+\]', - $db_row->cloze_text, + $db_row->long_menu_text, $answer_input_mapping ) ); @@ -152,16 +161,18 @@ private function fetchDBValues( } private function loadAnswersFromFile( + \ilIniFile $ini, + string $client_id, int $old_question_id, int $gap_id ): array { - $file = ilFileUtils::getDataDir() . "/assessment/longMenuQuestion/{$old_question_id}/{$gap_id}.txt"; + $file = "{$ini->readVariable('clients', 'datadir')}/{$client_id}/assessment/longMenuQuestion/{$old_question_id}/{$gap_id}.txt"; if (!file_exists($file)) { return []; } - return exlode( + return explode( "\n", file_get_contents($file) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index ad9825006ed1..babc3bfe10eb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -25,7 +25,9 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Setup\Environment; class MigrationNumeric implements Migration { @@ -57,13 +59,18 @@ public function getTableNameSpace(): TableNameSpace } #[\Override] - public function buildInsertStatement( + public function completeMigrationInsert( + Environment $environment, MigrationInsert $migration_insert - ): MigrationInsert { + ): ?MigrationInsert { $db_row = $this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() - )->current(); + )?->current(); + + if ($db_row === null) { + return null; + } $answer_form_id = $migration_insert->getAnswerFormId(); $gap_id = $migration_insert->getUuid(); @@ -101,8 +108,8 @@ public function buildInsertStatement( ScoringIdentical::ScoreAll, 0 ) - )->withAdditionalTextLegacy( - '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gap_id->toString()) . '}}' + )->withAdditionalText( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . $gap_id->toString() . '}}' ); } @@ -112,7 +119,7 @@ private function fetchDBValues( ): \Generator { $query = $db->query( 'SELECT points, lowerlimit, upperlimit FROM qpl_num_range' . PHP_EOL - . "WHERE q.question_fi = {$db->quote($old_question_id)}" + . "WHERE question_fi = {$db->quote($old_question_id)}" ); while (($row = $db->fetchObject($query)) !== null) { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 2b5af74ffdeb..26b67999b624 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -20,13 +20,14 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; +use ILIAS\Data\UUID\Uuid; use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\Persistence\TableNameSpace; -use ILIAS\Data\UUID\Uuid; +use ILIAS\Setup\Environment; class MigrationTextSubset implements Migration { @@ -56,10 +57,12 @@ public function getTableNameSpace(): TableNameSpace } #[\Override] - public function buildInsertStatement( + public function completeMigrationInsert( + Environment $environment, MigrationInsert $migration_insert - ): MigrationInsert { + ): ?MigrationInsert { $gaps = []; + foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() @@ -89,6 +92,10 @@ public function buildInsertStatement( } } + if (!isset($db_row)) { + return null; + } + return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( @@ -96,7 +103,7 @@ public function buildInsertStatement( $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::OnlyScoreDistinct, - false + 0 ) )->withAdditionalText( implode( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 2dcfeae21079..b2b3b0cb9fab 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -127,8 +127,9 @@ private function buildGapFromDBValues( array $values, array $answer_options ): Gap { + $answer_input_uuid = $this->uuid_factory->fromString($values['id']); return new Gap( - $this->uuid_factory->fromString($values['id']), + $answer_input_uuid, $this->uuid_factory->fromString($values['answer_form_id']), $values['position'], $answer_options[$values['id']], diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php index d06a76b8a4b5..ca083a2cc89c 100644 --- a/components/ILIAS/Questions/src/Persistence/CoreTables.php +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -60,7 +60,8 @@ enum CoreTables: string private const string MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN = 'old_question_id'; private const array MIGRATIONS_TABLE_COLUMNS = [ 'old_question_id', - 'new_question_id' + 'new_question_id', + 'success' ]; case Questions = 'qsts_questions'; diff --git a/components/ILIAS/Questions/src/Persistence/JoinType.php b/components/ILIAS/Questions/src/Persistence/JoinType.php index e020447a0056..3a7d11732407 100644 --- a/components/ILIAS/Questions/src/Persistence/JoinType.php +++ b/components/ILIAS/Questions/src/Persistence/JoinType.php @@ -23,5 +23,5 @@ enum JoinType: string { case Inner = 'INNER'; - case Left = 'left'; + case Left = 'LEFT'; } diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 54dc4463ee86..54f04994b400 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Persistence; +use ILIAS\Data\Range; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; @@ -31,7 +32,7 @@ class Query private array $where = []; private array $joins = []; private array $order = []; - private ?int $limit = null; + private ?Range $range = null; private array $binding_types = []; private array $binding_values = []; @@ -61,7 +62,7 @@ public function __construct( ); $this->joins[] = new Join( - $questions_id_column, + $questions_linking_table_definition->getIdColumn(), $questions_table_definition->getIdColumn(), JoinType::Inner ); @@ -137,11 +138,11 @@ public function withAdditionalOrder( return $clone; } - public function withLimit( - int $limit + public function withRange( + Range $range ): self { $clone = clone $this; - $clone->limit = $limit; + $clone->range = $range; return $clone; } @@ -208,7 +209,7 @@ static function (array $c, Order $v): array { [] ) ) . PHP_EOL - . ($this->limit !== null ? "LIMIT = {$this->limit}" : ''), + . ($this->range !== null ? "LIMIT {$this->range->getStart()}, {$this->range->getLength()}" : ''), $this->binding_types, $this->binding_values ); diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index aa9eda087481..487a0c866b8c 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -25,6 +25,8 @@ use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\Order as DataOrder; +use ILIAS\Data\Range as DataRange; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; @@ -52,11 +54,11 @@ public function getNew( */ public function getQuestionDataOnlyForAllQuestions(): \Generator { - foreach ((new Query( + foreach ($query = new Query( $this->db, $this->answer_form_factory, $this->refinery - ))->loadNextRecord() as $query_with_record) { + )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, [] diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index ce58b1f44cc9..9e46ea64fa40 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -95,7 +95,8 @@ private function buildContent(): array $this->getColums(), )->withActions( $this->getActions() - )->withRequest($this->request) + )->withRange(new Range(0, 20)) + ->withRequest($this->request) ]; } @@ -143,7 +144,7 @@ private function getActions(): array return [ 'delete' => $this->ui_factory->table()->action()->standard( $this->lng->txt('delete'), - $this->environment->withActionParameter(Edit::CMD_DELETE_QUESTION) + $this->environment->withActionParameter(Edit::CMD_DELETE_QUESTIONS) ->getUrlBuilder(), $this->environment->getQuestionIdsToken() )->withAsync(true) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 76e95d60b572..47a65e275812 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -25,6 +25,7 @@ use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Questions\Presentation\Definitions\Editability; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; @@ -42,7 +43,6 @@ use ILIAS\HTTP\Services as HTTP; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; -use ILIAS\UI\Component\Item\Standard as StandardItem; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; use ILIAS\Style\Content\Service as ContentStyle; @@ -52,7 +52,7 @@ class Edit { private const string CMD_CREATE_QUESTION = 'create'; public const string CMD_EDIT_QUESTION = 'edit'; - public const string CMD_DELETE_QUESTION = 'delete'; + public const string CMD_DELETE_QUESTIONS = 'delete'; private const string CMD_CREATE_ANSWER_FORM = 'create_af'; public const string CMD_OTHER_ANSWER_FORM = 'other_af'; private const string CMD_EDIT_FEEDBACK = 'edit_f'; @@ -126,7 +126,7 @@ public function show( return match($environment->getAction()) { self::CMD_CREATE_QUESTION => $this->createQuestion($environment), self::CMD_EDIT_QUESTION => $this->editQuestion($environment), - self::CMD_DELETE_QUESTION => $this->deleteQuestion($environment), + self::CMD_DELETE_QUESTIONS => $this->deleteQuestions($environment), default => $this->showTable($toolbar, $environment) }; } @@ -322,7 +322,7 @@ private function editQuestion( ); } - private function deleteQuestion( + private function deleteQuestions( EnvironmentImplementation $environment ): Async { $question_ids = $environment->getQuestionIds(); @@ -335,12 +335,8 @@ private function deleteQuestion( ); } - if ($environment->getStep() === self::CMD_DELETE_QUESTION) { - $this->questions_repository->delete( - iterator_to_array( - $this->questions_repository->getForQuestionIds($question_ids) - ) - ); + if ($environment->getStep() === self::CMD_DELETE_QUESTIONS) { + $this->deleteSelectedQuestions($question_ids); $this->ctrl->redirectToURL( $environment->getUrlBuilder()->buildURI()->__toString() ); @@ -351,9 +347,9 @@ private function deleteQuestion( $this->lng->txt('confirm'), $this->lng->txt('qpl_confirm_delete_questions'), $environment->withActionParameter( - self::CMD_DELETE_QUESTION + self::CMD_DELETE_QUESTIONS )->getUrlBuilderWithStepParameter( - self::CMD_DELETE_QUESTION + self::CMD_DELETE_QUESTIONS )->buildURI()->__toString() )->withAffectedItems( $this->buildAffectedItems($question_ids) @@ -484,18 +480,25 @@ private function buildItemGroupForQuestionListSlate( ): ItemGroup { return $this->ui_factory->item()->group( '', - array_map( - fn(QuestionImplementation $v): StandardItem => $this->ui_factory->item()->standard( - $v->toEditLink( - $this->ui_factory->link(), - $environment->withActionParameter(self::CMD_EDIT_QUESTION) - ) - ), - iterator_to_array($this->questions_repository->getQuestionDataOnlyForAllQuestions()) - ) + $this->builEditLinksForQuestionListSlate($environment) ); } + private function builEditLinksForQuestionListSlate( + Environment $environment + ): array { + $links = []; + foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { + $links[] = $this->ui_factory->item()->standard( + $question->toEditLink( + $this->ui_factory->link(), + $environment->withActionParameter(self::CMD_EDIT_QUESTION) + ) + ); + } + return $links; + } + private function buildEditStartView( EnvironmentImplementation $environment, QuestionImplementation $question @@ -570,7 +573,22 @@ private function checkCapabilities( } } - public function buildEnvironment( + private function deleteSelectedQuestions( + array $question_ids + ): void { + $questions_to_delete = []; + foreach ($this->questions_repository->getForQuestionIds($question_ids) as $question) { + if (count($questions_to_delete) < 100) { + $questions_to_delete[] = $question; + continue; + } + + $this->questions_repository->delete($questions_to_delete); + $questions_to_delete = []; + } + } + + private function buildEnvironment( URI $base_uri, int $obj_id ): EnvironmentImplementation { diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 8f045de511d0..57775096067b 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -131,7 +131,7 @@ public function step_2(): void ], 'additional_text_legacy' => [ 'type' => \ilDBConstants::T_CLOB, - 'notnull' => false + 'notnull' => true ] ]); } @@ -213,14 +213,19 @@ public function step_5(): void $table_name = CoreTables::MigrationsTable->value; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ - 'old_question_id' => [ + 'new_question_id' => [ 'type' => \ilDBConstants::T_TEXT, 'length' => 64, 'notnull' => true ], - 'new_question_id' => [ + 'old_question_id' => [ 'type' => \ilDBConstants::T_INTEGER, 'length' => 4, + 'notnull' => false + ], + 'success' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, 'notnull' => true ] ]); diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index a07550d2838b..1ec6b1813e56 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -37,6 +37,7 @@ class QuestionsMigration implements Migration { private const string OLD_QUESTIONS_TABLE = 'qpl_questions'; + private const string OLD_QUESTION_TYPE_TABLE = 'qpl_qst_type'; private const string TEST_QUESTIONS_SEQUENCE_TABLE = 'tst_test_question'; private \ilDBInterface $db; @@ -44,7 +45,6 @@ class QuestionsMigration implements Migration private UuidFactory $uuid_factory; private readonly array $answer_form_migrations; - private bool $ilias_is_initialized = false; private ?array $question_to_learning_module_mapping = null; private ?array $allready_migrated_questions = null; private ?array $allready_migrated_questions_in_qpls = null; @@ -92,19 +92,12 @@ public function prepare(Environment $environment): void public function step( Environment $environment ): void { - /** - * sk, 2026-01-14: Sadly this is necessary to clone the question pages - * without duplicating a humongous amount of code. It is mighty - * depressing, but the structure of `COPage` is yay stupid. - */ - if (!$this->ilias_is_initialized) { - \ilContext::init(\ilContext::CONTEXT_CRON); - entry_point('ILIAS Legacy Initialisation Adapter'); - $this->ilias_is_initialized = true; - } - $db_values = $this->fetchValidRecord(); + if ($db_values === null) { + return; + } + if ($db_values->obj_fi === 0) { $db_values->obj_fi = $this->getObjIdFromLearningModulMapping($db_values->question_id); } @@ -122,7 +115,8 @@ public function step( $new_question_id = $this->uuid_factory->uuid4(); - $answer_form_migration->buildInsertStatement( + $migration_insert = $answer_form_migration->completeMigrationInsert( + $environment, $this->buildMigrationInsert( $answer_form_migration, [ @@ -148,18 +142,31 @@ public function step( $new_question_id, $db_values ) - )->run(); + ); + + if ($migration_insert === null) { + $this->db->manipulate( + $this->buildInsertMigrationStatement( + $db_values->question_id, + null + )->toManipulateString($this->db) + ); + $this->io->inform( + "{$db_values->question_id} could not be migrated due to missing question data." + ); + return; + } - $this->io->inform($new_question_id->toString()); + $migration_insert->run(); + $this->io->inform("{$new_question_id->toString()} successfully migrated."); } #[\Override] public function getRemainingAmountOfSteps(): int { - return $this->db->fetchObject( - $this->db->query( - 'SELECT COUNT(question_id) cnt FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL - . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL + $query = $this->db->query( + 'SELECT COUNT(question_id) cnt FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL . 'WHERE t.type_tag IN (' . implode( @@ -171,14 +178,17 @@ public function getRemainingAmountOfSteps(): int ) . ')' . PHP_EOL . 'AND q.complete = 1' . PHP_EOL . 'AND m.old_question_id IS NULL' - ) + ); + return $this->db->fetchObject( + $query )->cnt; } - private function fetchValidRecord(): \stdClass + private function fetchValidRecord(): ?\stdClass { - $query_string = 'SELECT q.*, t.type_tag, s.sequence FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL - . 'JOIN qpl_qst_type t ON q.question_type_fi = t.question_type_id' . PHP_EOL + $query = $this->db->query( + 'SELECT q.*, t.type_tag, s.sequence FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' s ON q.question_id = s.question_fi' . PHP_EOL . 'WHERE t.type_tag IN (' @@ -190,13 +200,14 @@ private function fetchValidRecord(): \stdClass ) ) . ')' . PHP_EOL . 'AND q.complete = 1' . PHP_EOL - . 'AND m.old_question_id IS NULL' . PHP_EOL - . 'LIMIT 1'; + . 'AND m.old_question_id IS NULL' + ); do { - $db_values = $this->db->fetchObject( - $this->db->query($query_string) - ); + $db_values = $this->db->fetchObject($query); + if ($db_values === null) { + return null; + } } while (!$this->areDbValuesValid($db_values)); $db_values->original_id = $this->cleanupAndMigrateOriginalId($db_values->original_id); @@ -225,7 +236,7 @@ private function cleanupAndMigrateOriginalId( ?int $original_id ): ?Uuid { if ($original_id === null - || in_array($this->allready_migrated_questions_in_qpls, $original_id)) { + || in_array($original_id, $this->allready_migrated_questions_in_qpls)) { return null; } return $this->uuid_factory->fromString( @@ -235,10 +246,9 @@ private function cleanupAndMigrateOriginalId( private function loadAlreadyMigratedQuestions(): void { - $query = $this->db->query( 'SELECT m.*, o.type FROM ' . CoreTables::MigrationsTable->value . ' m' . PHP_EOL - . 'JOIN ' . CoreTables::Linking->value . 'l' . PHP_EOL + . 'JOIN ' . CoreTables::Linking->value . ' l' . PHP_EOL . 'ON m.new_question_id = l.question_id' . PHP_EOL . 'JOIN object_data o ON l.obj_id = o.obj_id' . PHP_EOL ); @@ -260,7 +270,7 @@ private function getObjIdFromLearningModulMapping( $this->loadQuestionsToLearningModuleMapping(); } - $this->question_to_learning_module_mapping[$question_id] ?? null; + return $this->question_to_learning_module_mapping[$question_id] ?? null; } private function loadQuestionsToLearningModuleMapping(): void @@ -322,13 +332,19 @@ private function buildInsertQuestionStatement( private function buildInsertMigrationStatement( int $old_question_id, - Uuid $new_question_id + ?Uuid $new_question_id ): Insert { return new Insert( CoreTables::MigrationsTable->getColumns(), [ new Value(\ilDBConstants::T_INTEGER, $old_question_id), - new Value(\ilDBConstants::T_TEXT, $new_question_id->toString()) + new Value(\ilDBConstants::T_TEXT, $new_question_id?->toString()), + new Value( + \ilDBConstants::T_INTEGER, + $new_question_id === null + ? '0' + : '1' + ) ] ); } From bd34ec574178e99c8bd71c07b6814c804305c606 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 30 Jan 2026 10:14:49 +0100 Subject: [PATCH 039/108] Questions: Small Improvements --- .../Questions/src/AnswerForm/Factory.php | 4 ++ components/ILIAS/Questions/src/Collector.php | 7 ++- .../Questions/src/Persistence/Repository.php | 43 ++++++++++++++++--- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index 5836840dbca5..b951d2bc6fa1 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -49,6 +49,10 @@ function (array $c, Definition $v) { ); } + /** + * + * @return array + */ public function getAvailableDefinitions(): array { return array_values($this->available_answer_form_types); diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php index 68df3dc761ef..d3ecf6764511 100644 --- a/components/ILIAS/Questions/src/Collector.php +++ b/components/ILIAS/Questions/src/Collector.php @@ -22,6 +22,7 @@ use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\Question; +use ILIAS\Data\UUID\Uuid; class Collector { @@ -42,14 +43,16 @@ public function withRequiredCapabilities( } public function getQuestionsForId( - int $id + Uuid $id ): ?Question { return $this->repository->getForQuestionId($id); } /** + * Use with Care: This is going to be freakishly expensive, if you ask + * for a lot of questions as the query will contain a huge amount of joins! * - * @param list $ids + * @param list<\ILIAS\Data\Uuid> $ids * @return \Generator */ public function getQuestionsForIds( diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 487a0c866b8c..8a8c8a5689c8 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -113,13 +113,14 @@ public function getForQuestionId( ), Operator::Equal ) - ) + ), + [$question_id] )->current(); } /** * - * @param array<\ILIAS\Data\Uuid> $question_ids + * @param list<\ILIAS\Data\Uuid> $question_ids * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ public function getForQuestionIds( @@ -142,7 +143,8 @@ public function getForQuestionIds( ), Operator::In ) - ) + ), + $question_ids ); } @@ -201,13 +203,15 @@ public function delete( } /** + * @param array<\ILIAS\Data\Uuid> $question_ids * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ private function getForBaseQuery( - Query $query + Query $query, + array $question_ids ): \Generator { $query_with_answer_forms = array_reduce( - $this->answer_form_factory->getAvailableDefinitions(), + $this->getAnswerFormTypesForQuestionIds($question_ids), fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( $c, CoreTables::AnswerForms->getIdColumn() @@ -223,6 +227,9 @@ private function getForBaseQuery( } } + /** + * $param array<\ILIAS\Questions\AnswerForms\Properties> $answer_forms + */ private function retrieveQuestionFromQuery( Query $query, array $answer_forms @@ -261,6 +268,9 @@ private function retrieveQuestionFromQuery( return $this->migrateQuestionPage($question); } + /** + * @return array<\ILIAS\Questions\AnswerForms\Properties> + */ private function retrieveAnswerFormsFromQuery( Query $query ): array { @@ -292,6 +302,29 @@ function (array $vs) use ($query): array { ); } + /** + * @param array<\ILIAS\Data\Uuid> $question_ids + * @return array<\ILIAS\Questions\AnswerForm\Definition> + */ + private function getAnswerFormTypesForQuestionIds( + array $question_ids + ): array { + $query = $this->db->query( + 'SELECT DISTINCT type FROM ' . CoreTables::AnswerForms->value . PHP_EOL + . "WHERE {$this->db->in( + 'question_id', + $question_ids, + false, + \ilDBConstants::T_TEXT + )}" + ); + $answer_form_types = []; + while (($type_class = $this->db->fetchObject($query)?->type) !== null) { + $answer_form_types[] = $this->answer_form_factory->getDefinitionForClass($type_class); + } + return $answer_form_types; + } + /** * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions */ From 9804a4b9e1d51b47e720893110b84083eb79104e Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 30 Jan 2026 11:21:50 +0100 Subject: [PATCH 040/108] Questions: TextLegacy Cannot Be Null --- .../Questions/src/AnswerForm/Migration/MigrationInsert.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index 9c02ad13fc98..799e49ffcfc6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -34,7 +34,7 @@ class MigrationInsert private ?int $image_size = null; private ?bool $shuffle_answer_options = null; private string $additional_text = ''; - private ?string $additional_text_legacy = null; + private ?string $additional_text_legacy = ''; public function __construct( private readonly \ilDBInterface $db, From a2d9128a296f192c6b43e68a957072b6d9cae20f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 30 Jan 2026 11:42:00 +0100 Subject: [PATCH 041/108] Questions: Fix Deleting Questions --- components/ILIAS/Questions/src/Presentation/Views/Edit.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 47a65e275812..d35f0adf2fbb 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -586,6 +586,8 @@ private function deleteSelectedQuestions( $this->questions_repository->delete($questions_to_delete); $questions_to_delete = []; } + + $this->questions_repository->delete($questions_to_delete); } private function buildEnvironment( From e7c92c3b7b0d07c3782b2d11adbcd4af0a216f97 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 30 Jan 2026 15:02:47 +0100 Subject: [PATCH 042/108] Questions: No Duplicating Questions on Page Update --- .../Legacy/PageEditor/QstsQuestionPage.php | 20 ------------- .../Questions/src/Persistence/Repository.php | 6 +--- .../classes/class.ilAssQuestionPage.php | 30 +++++++++++++++++++ 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 09d5a51e28e5..9e23acb982f2 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -40,24 +40,4 @@ public function setQuestion( ): void { $this->question = $question; } - - public function migrateQuestionElementToAnswerForm(): void - { - global $DIC; - $dom_util = $DIC->copage()->internal()->domain()->domUtil(); - - $answer_forms = $this->question->getAnswerForms(); - - $answer_form_node = new ilPCAnswerForm($this); - $answer_form_node->createPageContentNode(); - $answer_form_node->writePCId($this->generatePCId()); - $answer_form_node->create( - array_shift($answer_forms)->getAnswerFormId() - ); - - $dom_util->path($this->getDomDoc(), '//Question') - ->item(0)->parentNode->replaceWith($answer_form_node->getDomNode()); - - $this->update(); - } } diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 8a8c8a5689c8..7bea0e758777 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -398,13 +398,9 @@ private function migrateQuestionPage( $new_page_id = $this->getNextAvailableQuestionPageId(); $old_qsts_page = new \ilAssQuestionPage($old_page_id); + $old_qsts_page->setQuestion($question); $old_qsts_page->copyToAnswerForm($new_page_id, $question); - $new_qsts_page = new \QstsQuestionPage($new_page_id); - $new_qsts_page->setQuestion($question); - $new_qsts_page->buildDom(); - $new_qsts_page->migrateQuestionElementToAnswerForm(); - $new_question = $question->withPageId($new_page_id); $this->update([$new_question]); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index 78787cd9dbc1..72c456ba3bab 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -22,6 +22,8 @@ class ilAssQuestionPage extends ilPageObject { + private readonly QuestionImplementation $question; + /** * Get parent type * @return string parent type @@ -31,10 +33,19 @@ public function getParentType(): string return "qpl"; } + public function setQuestion( + QuestionImplementation $question + ): void { + $this->question = $question; + } + public function copyToAnswerForm( int $new_id, QuestionImplementation $question ): void { + $this->buildDom(); + $this->migrateQuestionElementToAnswerForm(); + $new_page_object = new QstsQuestionPage(); $new_page_object->setParentId($this->getParentId()); $new_page_object->setId($new_id); @@ -45,4 +56,23 @@ public function copyToAnswerForm( $new_page_object->setQuestion($question); $new_page_object->create(false); } + + private function migrateQuestionElementToAnswerForm(): void + { + global $DIC; + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + $answer_forms = $this->question->getAnswerForms(); + + $answer_form_node = new ilPCAnswerForm($this); + $answer_form_node->createPageContentNode(); + $answer_form_node->writePCId($this->generatePCId()); + $answer_form_node->create( + array_shift($answer_forms)->getAnswerFormId() + ); + + $dom_util->path($this->getDomDoc(), '//Question') + ->item(0)->parentNode->replaceWith($answer_form_node->getDomNode()); + $this->xml = $this->getXMLFromDom(); + } } From e1efa8f7a8f734384268c49bbd2f5520388aac5b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 2 Feb 2026 10:06:03 +0100 Subject: [PATCH 043/108] Questions: Fix Cloze Migrations --- .../Migration/BasicMigrationFunctions.php | 159 +++++++++++--- .../Cloze/Migration/MigrationCloze.php | 201 ++++++++++-------- .../Cloze/Migration/MigrationLongMenu.php | 55 ++--- .../Cloze/Migration/MigrationNumeric.php | 2 + .../Cloze/Migration/MigrationTextSubset.php | 48 +++-- 5 files changed, 300 insertions(+), 165 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index c5b9d830b705..58b6ce7d69fc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -35,6 +35,7 @@ trait BasicMigrationFunctions private function buildGapInsertStatement( Persistence $persistence, TableNameBuilder $table_name_builder, + ?Insert $gaps_insert, Uuid $answer_input_id, Uuid $answer_form_id, int $position, @@ -45,28 +46,69 @@ private function buildGapInsertStatement( ?int $min_autocomplete, ?int $shuffle ): Insert { - return new Insert( - $persistence->getColumns( - $table_name_builder, - TableTypes::AnswerInputs - ), - [ - new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $position), - new Value(\ilDBConstants::T_TEXT, $gap_type), - new Value(\ilDBConstants::T_INTEGER, $max_chars), - new Value(\ilDBConstants::T_FLOAT, $step_size), - new Value(\ilDBConstants::T_INTEGER, $matching_options?->value), - new Value(\ilDBConstants::T_INTEGER, $min_autocomplete), - new Value(\ilDBConstants::T_INTEGER, $shuffle) - ] + if ($gaps_insert === null) { + return new Insert( + $persistence->getColumns( + $table_name_builder, + TableTypes::AnswerInputs + ), + $this->buildGapValuesForInsert( + $answer_input_id, + $answer_form_id, + $position, + $gap_type, + $max_chars, + $step_size, + $matching_options, + $min_autocomplete, + $shuffle + ) + ); + } + + return $gaps_insert->withAdditionalValues( + $this->buildGapValuesForInsert( + $answer_input_id, + $answer_form_id, + $position, + $gap_type, + $max_chars, + $step_size, + $matching_options, + $min_autocomplete, + $shuffle + ) ); } + private function buildGapValuesForInsert( + Uuid $answer_input_id, + Uuid $answer_form_id, + int $position, + string $gap_type, + ?int $max_chars, + ?float $step_size, + ?TextMatchingOptions $matching_options, + ?int $min_autocomplete, + ?int $shuffle + ): array { + return [ + new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $position), + new Value(\ilDBConstants::T_TEXT, $gap_type), + new Value(\ilDBConstants::T_INTEGER, $max_chars), + new Value(\ilDBConstants::T_FLOAT, $step_size), + new Value(\ilDBConstants::T_INTEGER, $matching_options?->value), + new Value(\ilDBConstants::T_INTEGER, $min_autocomplete), + new Value(\ilDBConstants::T_INTEGER, $shuffle) + ]; + } + private function buildAnswerOptionInsertStatement( Persistence $persistence, TableNameBuilder $table_name_builder, + ?Insert $options_insert, Uuid $answer_option_id, Uuid $answer_input_id, int $position, @@ -75,29 +117,62 @@ private function buildAnswerOptionInsertStatement( ?float $lower_limit, ?float $upper_limit ): Insert { - - return new Insert( - $persistence->getColumns( - $table_name_builder, - TableTypes::AnswerOptions - ), - [ - new Value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), - new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $position), - new Value(\ilDBConstants::T_TEXT, $text_value), - new Value(\ilDBConstants::T_FLOAT, $points), - new Value(\ilDBConstants::T_FLOAT, $lower_limit), - new Value( - \ilDBConstants::T_FLOAT, - $lower_limit !== $upper_limit - ? $upper_limit - : null + if ($options_insert === null) { + return new Insert( + $persistence->getColumns( + $table_name_builder, + TableTypes::AnswerOptions + ), + $this->buildOptionValuesForInsert( + $answer_option_id, + $answer_input_id, + $position, + $text_value, + $points, + $lower_limit, + $upper_limit ) - ] + ); + } + + return $options_insert->withAdditionalValues( + $this->buildOptionValuesForInsert( + $answer_option_id, + $answer_input_id, + $position, + $text_value, + $points, + $lower_limit, + $upper_limit + ) ); } + private function buildOptionValuesForInsert( + Uuid $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + float $points, + ?float $lower_limit, + ?float $upper_limit + ): array { + return [ + new Value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), + new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + new Value(\ilDBConstants::T_INTEGER, $position), + new Value(\ilDBConstants::T_TEXT, $text_value), + new Value(\ilDBConstants::T_FLOAT, $points), + new Value(\ilDBConstants::T_FLOAT, $lower_limit), + new Value( + \ilDBConstants::T_FLOAT, + $lower_limit !== $upper_limit + ? $upper_limit + : null + ) + ]; + } + private function buildAnswerFormInsertStatement( Persistence $persistence, TableNameBuilder $table_name_builder, @@ -128,6 +203,20 @@ private function buildScoringIdenticalFromOld( return ScoringIdentical::OnlyScoreDistinct; } + private function buildNewTextRatingFromOld( + string $old_text_rating + ): TextMatchingOptions { + return match($old_text_rating) { + \assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE => TextMatchingOptions::CaseInsensitive, + \assClozeGap::TEXTGAP_RATING_CASESENSITIVE => TextMatchingOptions::CaseSensitive, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1 => TextMatchingOptions::Levenstein1, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2 => TextMatchingOptions::Levenstein2, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3 => TextMatchingOptions::Levenstein3, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4 => TextMatchingOptions::Levenstein4, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => TextMatchingOptions::Levenstein5 + }; + } + private function replaceGapsAndSantizeLegacyClozeText( string $gap_replace_regex, string $text, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index fed99b2ddaaf..db0cd67c13a4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -25,11 +25,12 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; -use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Insert; +use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Value; +use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\Environment; class MigrationCloze implements Migration @@ -67,6 +68,8 @@ public function completeMigrationInsert( ): ?MigrationInsert { $answer_input_mapping = []; $answer_options_mapping = []; + $gaps_insert = null; + $answer_options_insert = null; foreach ($this->fetchDBValues( $migration_insert->getDb(), @@ -76,20 +79,19 @@ public function completeMigrationInsert( if (!isset($answer_input_mapping[$db_row->gap_id])) { $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); $answer_options_mapping[$db_row->gap_id] = []; - $migration_insert = $migration_insert->withAdditionalInsert( - $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $answer_input_mapping[$db_row->gap_id], - $answer_form_id, - $db_row->gap_id, - $this->buildNewGapTypeIdentifierFromOld((int) $db_row->cloze_type), - null, - null, - $this->buildNewTextRatingFromOld($db_row->textgap_rating), - null, - $db_row->shuffle === '1' ? 1 : 0 - ) + $gaps_insert = $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $gaps_insert, + $answer_input_mapping[$db_row->gap_id], + $answer_form_id, + $db_row->gap_id, + $this->buildNewGapTypeIdentifierFromOld((int) $db_row->cloze_type), + null, + null, + $this->buildNewTextRatingFromOld($db_row->textgap_rating), + null, + $db_row->shuffle === '1' ? 1 : 0 ); } @@ -99,18 +101,17 @@ public function completeMigrationInsert( 'answer_option_id' => $answer_option_id ]; - $migration_insert = $migration_insert->withAdditionalInsert( - $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $answer_option_id, - $answer_input_mapping[$db_row->gap_id], - $db_row->aorder, - $db_row->answertext, - $db_row->points, - $this->limitToFloat($this->math, $db_row->lowerlimit), - $this->limitToFloat($this->math, $db_row->upperlimit) - ) + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $answer_option_id, + $answer_input_mapping[$db_row->gap_id], + $db_row->aorder, + $db_row->answertext, + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) ); } @@ -119,7 +120,7 @@ public function completeMigrationInsert( } if ($db_row->combinations_enabled) { - $migration_insert = $this->addCombinationInserStatements( + $migration_insert = $this->addCombinationInsertStatements( $migration_insert, $answer_input_mapping, $answer_options_mapping @@ -135,6 +136,10 @@ public function completeMigrationInsert( $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), $db_row->combinations_enabled ) + )->withAdditionalInsert( + $gaps_insert + )->withAdditionalInsert( + $answer_options_insert )->withAdditionalTextLegacy( $this->replaceGapsAndSantizeLegacyClozeText( '\[gap\].+?\[\/gap\]', @@ -179,12 +184,14 @@ private function fetchCombinationsDBValues( } } - private function addCombinationInserStatements( + private function addCombinationInsertStatements( MigrationInsert $migration_insert, array $answer_input_mapping, array $answer_options_mapping ): MigrationInsert { $combination_mapping = []; + $combinations_insert = null; + $combinations_to_answer_options_insert = null; foreach ($this->fetchCombinationsDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() @@ -200,58 +207,88 @@ private function addCombinationInserStatements( if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); - $migration_insert = $migration_insert->withAdditionalInsert( - new Insert( - $this->persistence->getColumns( - $migration_insert->getTableNameBuilder(), - TableTypes::Additional, - $this->persistence->getCombinationsTableIdentifier() - ), - [ - new Value( - \ilDBConstants::T_TEXT, - $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString() - ), - new Value( - \ilDBConstants::T_TEXT, - $migration_insert->getAnswerFormId()->toString() - ), - new Value(\ilDBConstants::T_FLOAT, $db_row->points), - ] - ) + $combinations_insert = $this->buildCombinationsInsert( + $migration_insert->getTableNameBuilder(), + $combinations_insert, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), + $migration_insert->getAnswerFormId()->toString(), + $db_row->points ); } - $migration_insert = $migration_insert->withAdditionalInsert( - new Insert( - $this->persistence->getColumns( - $migration_insert->getTableNameBuilder(), - TableTypes::Additional, - $this->persistence->getCombinationToAnswerOptionsTableIdentifier() - ), - [ - new Value( - \ilDBConstants::T_TEXT, - $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString() - ), - new Value( - \ilDBConstants::T_TEXT, - $answer_input_mapping[$db_row->gap_fi]->toString() - ), - new Value( - \ilDBConstants::T_TEXT, - $answer_option['answer_option_id']->toString() - ), - new Value( - \ilDBConstants::T_TEXT, - $this->buildRangeValue($answer_option['is_numeric'], $db_row->answer) - ), - ] - ) + $combinations_to_answer_options_insert = $this->buildCombinationsToAnswerOptionsInsert( + $migration_insert->getTableNameBuilder(), + $combinations_to_answer_options_insert, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), + $answer_input_mapping[$db_row->gap_fi]->toString(), + $answer_option['answer_option_id']->toString(), + $this->buildRangeValue($answer_option['is_numeric'], $db_row->answer) ); } - return $migration_insert; + return $migration_insert->withAdditionalInsert($combinations_insert) + ->withAdditionalInsert($combinations_to_answer_options_insert); + } + + private function buildCombinationsInsert( + TableNameBuilder $table_name_builder, + ?Insert $combinations_insert, + Uuid $combination_id, + Uuid $answer_form_id, + float $points + ): Insert { + if ($combinations_insert === null) { + return new Insert( + $this->persistence->getColumns( + $table_name_builder, + TableTypes::Additional, + $this->persistence->getCombinationsTableIdentifier() + ), + [ + new Value(\ilDBConstants::T_TEXT, $combination_id), + new Value(\ilDBConstants::T_TEXT, $answer_form_id), + new Value(\ilDBConstants::T_FLOAT, $points), + ] + ); + } + + return $combinations_insert->withAdditionalValues([ + new Value(\ilDBConstants::T_TEXT, $combination_id), + new Value(\ilDBConstants::T_TEXT, $answer_form_id), + new Value(\ilDBConstants::T_FLOAT, $points), + ]); + } + + private function buildCombinationsToAnswerOptionsInsert( + TableNameBuilder $table_name_builder, + ?Insert $combinations_to_answer_options_insert, + Uuid $combination_id, + Uuid $gap_id, + Uuid $answer_option_id, + InRange $in_range + ): Insert { + if ($combinations_to_answer_options_insert === null) { + return new Insert( + $this->persistence->getColumns( + $table_name_builder, + TableTypes::Additional, + $this->persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + new Value(\ilDBConstants::T_TEXT, $combination_id), + new Value(\ilDBConstants::T_TEXT, $gap_id), + new Value(\ilDBConstants::T_TEXT, $answer_option_id), + new Value(\ilDBConstants::T_TEXT, $in_range) + ] + ); + } + + return $combinations_to_answer_options_insert->withAdditionalValues([ + new Value(\ilDBConstants::T_TEXT, $combination_id), + new Value(\ilDBConstants::T_TEXT, $gap_id), + new Value(\ilDBConstants::T_TEXT, $answer_option_id), + new Value(\ilDBConstants::T_TEXT, $in_range) + ]); } private function buildNewGapTypeIdentifierFromOld( @@ -264,20 +301,6 @@ private function buildNewGapTypeIdentifierFromOld( }; } - private function buildNewTextRatingFromOld( - string $old_text_rating - ): TextMatchingOptions { - return match($old_text_rating) { - \assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE => TextMatchingOptions::CaseInsensitive, - \assClozeGap::TEXTGAP_RATING_CASESENSITIVE => TextMatchingOptions::CaseSensitive, - \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1 => TextMatchingOptions::Levenstein1, - \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2 => TextMatchingOptions::Levenstein2, - \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3 => TextMatchingOptions::Levenstein3, - \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4 => TextMatchingOptions::Levenstein4, - \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => TextMatchingOptions::Levenstein5 - }; - } - private function buildRangeValue( bool $is_numeric, string $value diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 842d9f8d6b4d..f7ce89c023dc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -60,6 +60,8 @@ public function completeMigrationInsert( MigrationInsert $migration_insert ): ?MigrationInsert { $answer_input_mapping = []; + $gaps_insert = null; + $answer_options_insert = null; foreach ($this->fetchDBValues( $migration_insert->getDb(), @@ -69,20 +71,19 @@ public function completeMigrationInsert( if (!isset($answer_input_mapping[$db_row->gap_number])) { $answer_input_mapping[$db_row->gap_number] = $migration_insert->getUuid(); - $migration_insert = $migration_insert->withAdditionalInsert( - $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $answer_input_mapping[$db_row->gap_number], - $answer_form_id, - $db_row->gap_number, - $this->buildNewGapTypeIdentifierFromOld($db_row->type), - null, - null, - null, - $db_row->min_auto_complete, - $db_row->shuffle_answers === '1' ? 1 : 0 - ) + $gaps_insert = $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $gaps_insert, + $answer_input_mapping[$db_row->gap_number], + $answer_form_id, + $db_row->gap_number, + $this->buildNewGapTypeIdentifierFromOld($db_row->type), + null, + null, + null, + $db_row->min_auto_complete, + $db_row->shuffle_answers === '1' ? 1 : 0 ); $answers = array_map( @@ -111,18 +112,17 @@ public function completeMigrationInsert( } foreach ($answers as $position => $answer) { - $migration_insert = $migration_insert->withAdditionalInsert( - $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getTableNameBuilder(), - $answer['answer_input_id'], - $answer_input_mapping[$db_row->gap_number], - $position, - $answer['text'], - $answer['points'], - null, - null - ) + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $answer['answer_input_id'], + $answer_input_mapping[$db_row->gap_number], + $position, + $answer['text'], + $answer['points'], + null, + null ); } @@ -141,7 +141,8 @@ public function completeMigrationInsert( $db_row->long_menu_text, $answer_input_mapping ) - ); + )->withAdditionalInsert($gaps_insert) + ->withAdditionalInsert($answer_options_insert); } private function fetchDBValues( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index babc3bfe10eb..f11a705ef52a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -78,6 +78,7 @@ public function completeMigrationInsert( $this->buildGapInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), + null, $gap_id, $answer_form_id, 0, @@ -92,6 +93,7 @@ public function completeMigrationInsert( $this->buildAnswerOptionInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), + null, $migration_insert->getUuid(), $gap_id, 0, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 26b67999b624..2217e30ec2cb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -61,33 +61,51 @@ public function completeMigrationInsert( Environment $environment, MigrationInsert $migration_insert ): ?MigrationInsert { + $answer_form_id = $migration_insert->getAnswerFormId(); + $answer_options_insert = null; $gaps = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() ) as $db_row) { - $answer_form_id = $migration_insert->getAnswerFormId(); - if ($gaps === []) { + $gaps_insert = null; for ($i = 0; $i < $db_row->correctanswers; $i++) { - $gaps[] = $migration_insert->getUuid(); - } - } + $gap_id = $migration_insert->getUuid(); + $gaps[] = $gap_id; - foreach ($gaps as $gap_id) { - $migration_insert = $migration_insert->withAdditionalInsert( - $this->buildAnswerOptionInsertStatement( + $gaps_insert = $this->buildGapInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), - $migration_insert->getUuid(), + $gaps_insert, $gap_id, - $db_row->aorder, - $db_row->answertext, - $db_row->points, + $answer_form_id, + $i, + 'text', null, - null - ) + null, + $this->buildNewTextRatingFromOld($db_row->textgap_rating), + null, + 0 + ); + } + + $migration_insert = $migration_insert->withAdditionalInsert($gaps_insert); + } + + foreach ($gaps as $gap_id) { + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $migration_insert->getUuid(), + $gap_id, + $db_row->aorder, + $db_row->answertext, + $db_row->points, + null, + null ); } } @@ -105,6 +123,8 @@ public function completeMigrationInsert( ScoringIdentical::OnlyScoreDistinct, 0 ) + )->withAdditionalInsert( + $answer_options_insert )->withAdditionalText( implode( "\n\n", From ec6d065b0a680ef36f93457813a97351f24e6d28 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 2 Feb 2026 18:23:26 +0100 Subject: [PATCH 044/108] Questions: Add Page Component for Legacy Text --- .../PageEditor/QstsQuestionPageConfig.php | 1 + .../class.ilPCLegacyAnswerFormText.php | 59 +++++++++++++++++++ .../class.ilPCLegacyAnswerFormTextGUI.php | 33 +++++++++++ components/ILIAS/Questions/service.xml | 1 + .../classes/class.ilAssQuestionPage.php | 59 +++++++++++++++++-- lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 7 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php create mode 100644 components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php index 0f811288baa8..5a671e51ae45 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php @@ -24,6 +24,7 @@ public function init(): void { $this->setEnablePCType('Tabs', true); $this->setEnablePCType('AnswerForm', true); + $this->setEnablePCType('LegacyAnswerFormText', true); $this->setEnableInternalLinks(false); $this->setEnablePageToc(true); } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php new file mode 100644 index 000000000000..19c9c39495a3 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php @@ -0,0 +1,59 @@ +setType('laft'); + } + + #[\Override] + public function modifyPageContentPostXsl( + string $output, + string $mode, + bool $abstract_only = false + ): string { + if ($this->pg_obj::class !== QstsQuestionPage::class) { + return $output; + } + + return mb_ereg_replace_callback( + self::TEXT_PLACEHOLDER, + static fn(array $matches): string => base64_decode($matches[1]), + $output + ); + } + + public function create( + string $legacy_answer_form_text + ): void { + $this->createInitialChildNode( + $this->hier_id, + '', + self::ELEMENT_TAG, + [self::TEXT_ATTRIBUTE => $legacy_answer_form_text] + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php new file mode 100644 index 000000000000..ba24c634cdc7 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php @@ -0,0 +1,33 @@ +tpl->setOnScreenMessage(MessageBox::FAILURE, $this->lng->txt('legacy_text_cannot_be_edited'), true); + $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); + } +} diff --git a/components/ILIAS/Questions/service.xml b/components/ILIAS/Questions/service.xml index 4f310194a537..2f7a463b3da2 100644 --- a/components/ILIAS/Questions/service.xml +++ b/components/ILIAS/Questions/service.xml @@ -8,6 +8,7 @@ + diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index 72c456ba3bab..4a86f5482d2e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -19,6 +19,7 @@ declare(strict_types=1); use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Data\UUID\Uuid; class ilAssQuestionPage extends ilPageObject { @@ -62,17 +63,63 @@ private function migrateQuestionElementToAnswerForm(): void global $DIC; $dom_util = $DIC->copage()->internal()->domain()->domUtil(); - $answer_forms = $this->question->getAnswerForms(); + /** @var \ILIAS\Questions\AnswerForm\Properties $answer_form_properties */ + $answer_form_properties = $this->question->getAnswerForms(); + $dom_util->path($this->getDomDoc(), '//Question') + ->item(0)->parentNode->replaceWith( + $this->buildLegacyAnswerFormTextNode(), + $this->buildAnswerFormNode( + reset($answer_form_properties)->getAnswerFormId() + ) + ); + $this->xml = $this->getXMLFromDom(); + } + + private function buildLegacyAnswerFormTextNode(): DOMNode + { + $legacy_answer_form_text_node = new ilPCLegacyAnswerFormText($this); + $legacy_answer_form_text_node->createPageContentNode(); + $legacy_answer_form_text_node->writePCId($this->generatePCId()); + $legacy_answer_form_text_node->create( + $this->retrieveLegacyPageElementContent() + ); + + return $legacy_answer_form_text_node->getDomNode(); + } + + private function buildAnswerFormNode( + Uuid $answer_form_id + ): DOMNode { $answer_form_node = new ilPCAnswerForm($this); $answer_form_node->createPageContentNode(); $answer_form_node->writePCId($this->generatePCId()); - $answer_form_node->create( - array_shift($answer_forms)->getAnswerFormId() + $answer_form_node->create($answer_form_id); + + return $answer_form_node->getDomNode(); + } + + private function retrieveLegacyPageElementContent(): string + { + $question_info = $this->db->fetchObject( + $this->db->query( + "SELECT add_cont_edit_mode, question_text FROM qpl_questions WHERE question_id = {$this->id}" + ) ); - $dom_util->path($this->getDomDoc(), '//Question') - ->item(0)->parentNode->replaceWith($answer_form_node->getDomNode()); - $this->xml = $this->getXMLFromDom(); + $purified_content = ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution') + ->purify($question_info->question_text); + + if ($question_info->add_cont_edit_mode === assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE + || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') { + $purified_content = nl2br($purified_content); + } + return base64_encode( + ilLegacyFormElementsUtil::prepareTextareaOutput( + $purified_content, + true, + true + ) + ); } } diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index fac960933e89..300fceedea6e 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -14131,6 +14131,7 @@ qsts#:#cloze_enable_combinations#:#Enable Gap Combination qsts#:#cont_ed_insert_answf#:#Antwortformular einfügen qsts#:#create_answer_form#:#Antwortformular erstellen qsts#:#edit_basic_form_properties#:#Grundeinstellungen der Frage bearbeiten +qsts#:#legacy_text_cannot_be_edited#:#Der Text stammt aus einer überführten Frage und kann nicht geändert werden. qsts#:#qst_lifecycle#:#Lebenszyklus qsts#:#qst_lifecycle_draft#:#Entwurf qsts#:#qst_lifecycle_filter_all#:#Alle Lebenszyklen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 306f683e8b6e..7a1e3493a148 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -14102,6 +14102,7 @@ qsts#:#cloze_enable_combinations#:#Lückenkombinationen aktivieren qsts#:#cont_ed_insert_answf#:#Insert Answer Form qsts#:#create_answer_form#:#Create Answer Form qsts#:#edit_basic_form_properties#:#Edit Basic Form Properties +qsts#:#legacy_text_cannot_be_edited#:#This text was migrated from the previous question implementation and cannot be edited. qsts#:#qst_lifecycle#:#Lifecycle qsts#:#qst_lifecycle_draft#:#Draft qsts#:#qst_lifecycle_filter_all#:#All Lifecycles From 462cacb6a02cc18af5086d42e5cfe909f94f5a15 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 2 Feb 2026 18:28:07 +0100 Subject: [PATCH 045/108] COPage/Export: Add LegacyTextElement --- components/ILIAS/COPage/xsl/page.xsl | 3 +++ components/ILIAS/Export/xml/ilias_pg_12.dtd | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/COPage/xsl/page.xsl b/components/ILIAS/COPage/xsl/page.xsl index bcdd7a8af4a1..c69afb7f61bf 100755 --- a/components/ILIAS/COPage/xsl/page.xsl +++ b/components/ILIAS/COPage/xsl/page.xsl @@ -3721,6 +3721,9 @@
+ + [[[LEGACY_ANSWER_FORM_TEXT_]]] + [[[ANSWER_FORM_]]] diff --git a/components/ILIAS/Export/xml/ilias_pg_12.dtd b/components/ILIAS/Export/xml/ilias_pg_12.dtd index 0071822931b3..64e6145fe231 100755 --- a/components/ILIAS/Export/xml/ilias_pg_12.dtd +++ b/components/ILIAS/Export/xml/ilias_pg_12.dtd @@ -23,7 +23,7 @@ - + QRef CDATA #REQUIRED > + + + + Date: Mon, 2 Feb 2026 18:36:57 +0100 Subject: [PATCH 046/108] Questions: Little Improvement in SubText Migration --- .../Cloze/Migration/MigrationTextSubset.php | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 2217e30ec2cb..ab57e0f4d3c3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -126,13 +126,7 @@ public function completeMigrationInsert( )->withAdditionalInsert( $answer_options_insert )->withAdditionalText( - implode( - "\n\n", - array_map( - fn(Uuid $v) => "{{GAP_{$v->toString()}}}", - $gaps, - ) - ) + $this->buildAdditionalTextFromGapsArray($gaps) ); } @@ -150,4 +144,19 @@ private function fetchDBValues( yield $row; } } + + private function buildAdditionalTextFromGapsArray( + array $gaps + ): string { + $text_array = []; + foreach ($gaps as $index => $gap) { + $position = $index + 1; + $text_array[] = "{$position}. {{GAP_{$gap->toString()}}}"; + } + + return implode( + "\n\n", + $text_array + ); + } } From 8cd32b007d6976cfc2e329647e02f0cbe676c2b1 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 2 Feb 2026 18:50:02 +0100 Subject: [PATCH 047/108] Questions: Rename Function for More Clarity --- components/ILIAS/Questions/src/Presentation/Views/Edit.php | 4 ++-- .../ILIAS/Questions/src/Question/QuestionImplementation.php | 4 ++-- .../ILIAS/Questions/src/Question/Views/Participant.php | 6 +++--- .../TestQuestionPool/classes/class.ilAssQuestionPage.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index d35f0adf2fbb..792466fc2e14 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -243,7 +243,7 @@ public function editAnswerForm( } $this->questions_repository->update( - [$question->withAnswerForm($next)] + [$question->withAnswerFormProperties($next)] ); $this->ctrl->redirectToURL( @@ -423,7 +423,7 @@ private function forwardCreateAnswerFormCmd( } $this->questions_repository->create( - [$question->withAnswerForm($create)] + [$question->withAnswerFormProperties($create)] ); $content_object->create($create->getAnswerFormId()); diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index dc53fde12789..94c0861d2b3c 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -203,7 +203,7 @@ public function getCreated(): ?\DateTimeImmutable return $this->created; } - public function getAnswerForms(): array + public function getAnswerFormProperties(): array { return $this->answer_forms; } @@ -214,7 +214,7 @@ public function getAnswerFormPropertiesByIdString( return $this->answer_forms[$form_id] ?? null; } - public function withAnswerForm( + public function withAnswerFormProperties( AnswerFormProperties $answer_form ): self { $clone = clone $this; diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index 775aff19fd8e..f36881863239 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -37,7 +37,7 @@ public function __construct( public function withIsAsync( bool $async ): self { - foreach ($this->question->getAnswerForms() as $form) { + foreach ($this->question->getAnswerFormProperties() as $form) { if (!$form->getType()->isAsyncPresentationAvailable()) { throw \Exception('This QuestionType has no async presentation.'); } @@ -58,7 +58,7 @@ public function withIsInteractive( public function withShowMarks( bool $show_marks ): self { - foreach ($this->question->getAnswerForms() as $form) { + foreach ($this->question->getAnswerFormProperties() as $form) { if (!$form->getType()->isMarkable()) { throw \Exception('This QuestionType cannot be marked.'); } @@ -72,7 +72,7 @@ public function withShowMarks( public function withShowCorrectSolution( bool $show_correct_solution ): self { - foreach ($this->question->getAnswerForms() as $form) { + foreach ($this->question->getAnswerFormProperties() as $form) { if (!$form->getType()->isMarkable()) { throw \Exception('This QuestionType cannot be marked.'); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index 4a86f5482d2e..8f3a80954dea 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -64,7 +64,7 @@ private function migrateQuestionElementToAnswerForm(): void $dom_util = $DIC->copage()->internal()->domain()->domUtil(); /** @var \ILIAS\Questions\AnswerForm\Properties $answer_form_properties */ - $answer_form_properties = $this->question->getAnswerForms(); + $answer_form_properties = $this->question->getAnswerFormProperties(); $dom_util->path($this->getDomDoc(), '//Question') ->item(0)->parentNode->replaceWith( From bccac8111bf81b7788f0248ace5de7efa8a5d76f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 3 Feb 2026 10:56:23 +0100 Subject: [PATCH 048/108] UI: Add Icon For Questions --- .../src/UserSettings/EditingMode.php | 136 ++++++++++++++++++ .../src/UserSettings/EditingModes.php | 136 ++++++++++++++++++ .../Questions/src/UserSettings/Settings.php | 33 +++++ .../resources/images/standard/icon_qsts.svg | 41 ++++++ .../UI/src/Component/Symbol/Icon/Standard.php | 2 +- .../Component/Symbol/Icon/Standard.php | 2 +- 6 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 components/ILIAS/Questions/src/UserSettings/EditingMode.php create mode 100644 components/ILIAS/Questions/src/UserSettings/EditingModes.php create mode 100644 components/ILIAS/Questions/src/UserSettings/Settings.php create mode 100755 components/ILIAS/UI/resources/images/standard/icon_qsts.svg diff --git a/components/ILIAS/Questions/src/UserSettings/EditingMode.php b/components/ILIAS/Questions/src/UserSettings/EditingMode.php new file mode 100644 index 000000000000..7dc14af45b20 --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/EditingMode.php @@ -0,0 +1,136 @@ +txt('cal_user_date_format'); + } + + public function getSettingsPage(): AvailablePages + { + return AvailablePages::MainSettings; + } + + public function getSection(): AvailableSections + { + return AvailableSections::DateTime; + } + + public function getInput( + FieldFactory $field_factory, + Language $lng, + Refinery $refinery, + \ilSetting $settings, + ?\ilObjUser $user = null + ): Input { + $lng->loadLanguageModule('dateplaner'); + return $field_factory->select( + $lng->txt('cal_user_date_format'), + $this->buildOptions(), + $lng->txt('cal_date_format_info') + )->withRequired(true) + ->withValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + } + + public function getLegacyInput( + Language $lng, + \ilSetting $settings, + ?\ilObjUser $user = null + ): \ilFormPropertyGUI { + $lng->loadLanguageModule('dateplaner'); + $input = new \ilSelectInputGUI($lng->txt('cal_user_date_format')); + $input->setOptions($this->buildOptions()); + $input->setInfo($lng->txt('cal_date_format_info')); + $input->setValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + return $input; + } + + public function getDefaultValueForDisplay( + Language $lng, + \ilSetting $settings + ): string { + return $this->buildOptions()[\ilCalendarSettings::_getInstance()->getDefaultDateFormat()]; + } + + private function buildOptions(): array + { + $year = date('Y'); + return [ + \ilCalendarSettings::DATE_FORMAT_DMY => '31.10.' . $year, + \ilCalendarSettings::DATE_FORMAT_YMD => $year . '-10-31', + \ilCalendarSettings::DATE_FORMAT_MDY => '10/31/' . $year + ]; + } + + public function hasUserPersonalizedSetting( + \ilSetting $settings, + \ilObjUser $user + ): bool { + return $this->retrieveValueFromUser($user) + !== \ilCalendarSettings::_getInstance()->getDefaultDateFormat(); + } + + public function persistUserInput( + \ilObjUser $user, + mixed $input + ): \ilObjUser { + $user->setPref( + 'date_format', + $input !== null ? $input : (string) \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + return $user; + } + + public function retrieveValueFromUser(\ilObjUser $user): int + { + return (int) ($user->getPref('date_format') + ?? \ilCalendarSettings::_getInstance()->getDefaultDateFormat()); + } +} diff --git a/components/ILIAS/Questions/src/UserSettings/EditingModes.php b/components/ILIAS/Questions/src/UserSettings/EditingModes.php new file mode 100644 index 000000000000..7dc14af45b20 --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/EditingModes.php @@ -0,0 +1,136 @@ +txt('cal_user_date_format'); + } + + public function getSettingsPage(): AvailablePages + { + return AvailablePages::MainSettings; + } + + public function getSection(): AvailableSections + { + return AvailableSections::DateTime; + } + + public function getInput( + FieldFactory $field_factory, + Language $lng, + Refinery $refinery, + \ilSetting $settings, + ?\ilObjUser $user = null + ): Input { + $lng->loadLanguageModule('dateplaner'); + return $field_factory->select( + $lng->txt('cal_user_date_format'), + $this->buildOptions(), + $lng->txt('cal_date_format_info') + )->withRequired(true) + ->withValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + } + + public function getLegacyInput( + Language $lng, + \ilSetting $settings, + ?\ilObjUser $user = null + ): \ilFormPropertyGUI { + $lng->loadLanguageModule('dateplaner'); + $input = new \ilSelectInputGUI($lng->txt('cal_user_date_format')); + $input->setOptions($this->buildOptions()); + $input->setInfo($lng->txt('cal_date_format_info')); + $input->setValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + return $input; + } + + public function getDefaultValueForDisplay( + Language $lng, + \ilSetting $settings + ): string { + return $this->buildOptions()[\ilCalendarSettings::_getInstance()->getDefaultDateFormat()]; + } + + private function buildOptions(): array + { + $year = date('Y'); + return [ + \ilCalendarSettings::DATE_FORMAT_DMY => '31.10.' . $year, + \ilCalendarSettings::DATE_FORMAT_YMD => $year . '-10-31', + \ilCalendarSettings::DATE_FORMAT_MDY => '10/31/' . $year + ]; + } + + public function hasUserPersonalizedSetting( + \ilSetting $settings, + \ilObjUser $user + ): bool { + return $this->retrieveValueFromUser($user) + !== \ilCalendarSettings::_getInstance()->getDefaultDateFormat(); + } + + public function persistUserInput( + \ilObjUser $user, + mixed $input + ): \ilObjUser { + $user->setPref( + 'date_format', + $input !== null ? $input : (string) \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + ); + return $user; + } + + public function retrieveValueFromUser(\ilObjUser $user): int + { + return (int) ($user->getPref('date_format') + ?? \ilCalendarSettings::_getInstance()->getDefaultDateFormat()); + } +} diff --git a/components/ILIAS/Questions/src/UserSettings/Settings.php b/components/ILIAS/Questions/src/UserSettings/Settings.php new file mode 100644 index 000000000000..712899fd1073 --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/Settings.php @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php b/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php index f27efd55ac11..c405aacc3452 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php +++ b/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php @@ -184,7 +184,7 @@ interface Standard extends Icon public const GCON = 'gcon'; //Group Conversaion public const FILS = 'fils'; //File System Service public const TALA = 'tala'; //Employee Talk Template Admin - public const QST = 'ques'; //Question + public const QSTS = 'qsts'; //Question Component public const GSFO = 'gsfo'; //Footer Administration public const STUS = 'stus'; //Shortlink public const ADMA = 'adma'; //Administration - General Settings diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Icon/Standard.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Icon/Standard.php index ae2788bb5693..134cb392eba1 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Icon/Standard.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Icon/Standard.php @@ -152,7 +152,7 @@ class Standard extends Icon implements C\Symbol\Icon\Standard self::CON, self::FILS, self::TALA, - self::QST, + self::QSTS, self::STUS, self::GSFO, self::ADMA, From c68562e6d10a7ec3448ac3e0369902dd3bc27771 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 3 Feb 2026 10:57:53 +0100 Subject: [PATCH 049/108] UI: Add Icon For Questions --- .../ILIAS/Questions/Legacy/LocalDIC.php | 4 +- components/ILIAS/Questions/Questions.php | 2 + .../Questions/src/Presentation/Views/Edit.php | 8 ++ .../src/Question/QuestionImplementation.php | 5 + .../Questions/src/Question/Views/Edit.php | 107 +++++++++++----- .../src/UserSettings/EditingMode.php | 87 +++++++------ .../src/UserSettings/EditingModes.php | 116 ++---------------- .../Questions/src/UserSettings/Settings.php | 5 +- 8 files changed, 162 insertions(+), 172 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 31d693bc6c15..67ca265a7be7 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -76,7 +76,9 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Edit::class] = static fn($c): Edit => new Edit( $DIC['lng'], - $DIC['ilUser'], + $DIC['ilSetting'], + $DIC['user']->getSettings(), + $DIC['user']->getLoggedInUser(), $DIC['refinery'], $DIC['ui.factory'], $DIC['ui.renderer'], diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 0354c1e3430b..5f1ce88139e0 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -64,6 +64,8 @@ public function init( ); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); + $contribute[User\Settings\UserSettings::class] = fn() => + new Questions\UserSettings\Settings(); $internal[Persistence::class] = static fn() => new Persistence( new TableNameSpaceCore('cloze') diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 792466fc2e14..bb4423b1301b 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -45,6 +45,7 @@ use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; +use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Style\Content\Service as ContentStyle; use ILIAS\GlobalScreen\Services as GlobalScreen; @@ -64,6 +65,8 @@ class Edit public function __construct( private readonly Language $lng, + private readonly \ilSetting $settings, + private readonly UserSettings $user_settings, private readonly \ilObjUser $current_user, private readonly Refinery $refinery, private readonly UIFactory $ui_factory, @@ -260,6 +263,8 @@ private function createQuestion( $environment->getObjId() )->getEditView( $this->lng, + $this->settings, + $this->user_settings, $this->current_user, $this->ui_factory, $this->refinery, @@ -298,6 +303,7 @@ private function editQuestion( $edit = $question->getEditView( $this->lng, + $this->user_settings, $this->current_user, $this->ui_factory, $this->refinery, @@ -505,6 +511,8 @@ private function buildEditStartView( ): EditForm { return $question->getEditView( $this->lng, + $this->settings, + $this->user_settings, $this->current_user, $this->ui_factory, $this->refinery, diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 94c0861d2b3c..a607cd490046 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -39,6 +39,7 @@ use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Component\Table\DataRow; +use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Refinery\Factory as Refinery; use Psr\Http\Message\RequestInterface; @@ -247,6 +248,8 @@ public function isClone(): bool public function getEditView( Language $lng, + \ilSetting $settings, + UserSettings $user_settings, \ilObjUser $current_user, UIFactory $ui_factory, Refinery $refinery, @@ -255,6 +258,8 @@ public function getEditView( ): Views\Edit { return new Views\Edit( $lng, + $settings, + $user_settings, $current_user, $ui_factory, $refinery, diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 5a21f551dbc4..8cccb9cc4ae3 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -25,10 +25,12 @@ use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\UserSettings\EditingMode; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Component\Input\Field\Section; +use ILIAS\UI\Component\Panel\Standard as StandardPanel; +use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use Psr\Http\Message\RequestInterface; @@ -39,6 +41,8 @@ class Edit public function __construct( private readonly Language $lng, + private readonly \ilSetting $settings, + private readonly UserSettings $user_settings, private readonly \ilObjUser $current_user, private readonly UIFactory $ui_factory, private readonly Refinery $refinery, @@ -53,8 +57,11 @@ public function create( EnvironmentImplementation $environment ): EditForm|Question { return match ($environment->getStep()) { - self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), - default => $this->buildBasicPropertiesForm($environment) + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm( + $environment, + true + ), + default => $this->buildBasicPropertiesForm($environment, true) }; } @@ -63,28 +70,37 @@ public function edit( Participant $participant_view ): EditForm|Question { return match ($environment->getStep()) { - self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm($environment), - default => $this->buildBasicPropertiesForm($environment)->withContentAfterForm( + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm( + $environment, + false + ), + default => $this->buildBasicPropertiesForm( + $environment, + false + )->withContentAfterForm( $this->buildPreviewPanel($environment, $participant_view) ) }; } private function buildBasicPropertiesForm( - EnvironmentImplementation $environment + EnvironmentImplementation $environment, + bool $is_context_create ): EditForm { return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), - $this->buildBasicPropertiesInputs(), + $this->buildBasicPropertiesSection($is_context_create), true ); } private function processBasicPropertiesForm( - EnvironmentImplementation $environment + EnvironmentImplementation $environment, + bool $is_context_create ): EditForm|Question { $form = $this->buildBasicPropertiesForm( - $environment + $environment, + $is_context_create )->withRequest($this->request); $data = $form->getData(); @@ -93,38 +109,69 @@ private function processBasicPropertiesForm( : $data; } - private function buildBasicPropertiesInputs(): Section - { + private function buildBasicPropertiesSection( + bool $needs_create_mode_input + ): Section { $ff = $this->ui_factory->input()->field(); + + $inputs = $this->buildBasicPropertiesInputs(); + $values = $this->buildBasicPropertiesBasicValuesArray(); + + if ($needs_create_mode_input) { + $user_settings = $this->user_settings + ->getSettingByDefinitionClass(EditingMode::class); + $inputs['create_mode'] = $user_settings->getInput( + $ff, + $this->lng, + $this->refinery, + $this->settings + ); + $values['create_mode'] = $user_settings->retrieveValueFromUser( + $this->current_user + ); + } + $section = $ff->section( - [ - 'title' => $ff->text($this->lng->txt('title')) - ->withRequired(true), - 'author' => $ff->text($this->lng->txt('author')), - 'lifecycle' => $ff->select( - $this->lng->txt('qst_lifecycle'), - array_reduce( - Lifecycle::cases(), - function (array $c, Lifecycle $v): array { - $c[$v->value] = $this->lng->txt("qst_lifecycle_{$v->value}"); - return $c; - }, - [] - ) - )->withRequired(true), - 'remarks' => $ff->textarea($this->lng->txt('qst_remarks')) - ], + $inputs, $this->lng->txt('edit_basic_form_properties') )->withAdditionalTransformation($this->buildAddBasicPropertiesToQuestionTrafo()); - return $section->withValue([ + return $section->withValue($values); + } + + private function buildBasicPropertiesInputs(): array + { + $ff = $this->ui_factory->input()->field(); + + return [ + 'title' => $ff->text($this->lng->txt('title')) + ->withRequired(true), + 'author' => $ff->text($this->lng->txt('author')), + 'lifecycle' => $ff->select( + $this->lng->txt('qst_lifecycle'), + array_reduce( + Lifecycle::cases(), + function (array $c, Lifecycle $v): array { + $c[$v->value] = $this->lng->txt("qst_lifecycle_{$v->value}"); + return $c; + }, + [] + ) + )->withRequired(true), + 'remarks' => $ff->textarea($this->lng->txt('qst_remarks')) + ]; + } + + private function buildBasicPropertiesBasicValuesArray(): array + { + return [ 'title' => $this->question->getTitle(), 'author' => $this->question->getAuthor() !== '' ? $this->question->getAuthor() : $this->current_user->getFullname(), 'lifecycle' => $this->question->getLifecycle()->value, 'remarks' => $this->question->getRemarks() - ]); + ]; } private function buildAddBasicPropertiesToQuestionTrafo(): Transformation diff --git a/components/ILIAS/Questions/src/UserSettings/EditingMode.php b/components/ILIAS/Questions/src/UserSettings/EditingMode.php index 7dc14af45b20..8da6be0068d9 100644 --- a/components/ILIAS/Questions/src/UserSettings/EditingMode.php +++ b/components/ILIAS/Questions/src/UserSettings/EditingMode.php @@ -18,43 +18,50 @@ declare(strict_types=1); -namespace ILIAS\Calendar\UserSettings; +namespace ILIAS\Questions\UserSettings; use ILIAS\User\Settings\SettingDefinition; use ILIAS\User\Settings\AvailablePages; use ILIAS\User\Settings\AvailableSections; use ILIAS\Language\Language; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; +use ILIAS\UI\Component\Input\Field\Radio; use ILIAS\UI\Component\Input\Input; use ILIAS\Refinery\Factory as Refinery; -class DateFormat implements SettingDefinition +class EditingMode implements SettingDefinition { + #[\Override] public function getIdentifier(): string { - return 'date_format'; + return 'question_editing_mode'; } + #[\Override] public function isAvailable(): bool { return true; } + #[\Override] public function getLabel(Language $lng): string { - return $lng->txt('cal_user_date_format'); + return $lng->txt('question_editing_mode'); } + #[\Override] public function getSettingsPage(): AvailablePages { return AvailablePages::MainSettings; } + #[\Override] public function getSection(): AvailableSections { - return AvailableSections::DateTime; + return AvailableSections::Additional; } + #[\Override] public function getInput( FieldFactory $field_factory, Language $lng, @@ -62,75 +69,83 @@ public function getInput( \ilSetting $settings, ?\ilObjUser $user = null ): Input { - $lng->loadLanguageModule('dateplaner'); - return $field_factory->select( - $lng->txt('cal_user_date_format'), - $this->buildOptions(), - $lng->txt('cal_date_format_info') - )->withRequired(true) - ->withValue( + $lng->loadLanguageModule('questions'); + return array_reduce( + EditingModes::cases(), + fn(Radio $c, EditingModes $v): Radio => $c->withOption( + $v->value, + $v->getLabelForInput($lng), + $v->getBylineForInput($lng) + ), + $field_factory->radio( + $lng->txt('create_mode') + ) + )->withValue( $user !== null ? $this->retrieveValueFromUser($user) - : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + : EditingModes::getDefaultMode()->value ); } + #[\Override] public function getLegacyInput( Language $lng, \ilSetting $settings, ?\ilObjUser $user = null ): \ilFormPropertyGUI { - $lng->loadLanguageModule('dateplaner'); - $input = new \ilSelectInputGUI($lng->txt('cal_user_date_format')); - $input->setOptions($this->buildOptions()); - $input->setInfo($lng->txt('cal_date_format_info')); + $lng->loadLanguageModule('questions'); + $input = new \ilRadioGroupInputGUI($lng->txt('create_mode')); + $input->setOptions( + array_map( + fn(EditingModes $v): \ilRadioOption => new \ilRadioOption( + $v->getLabelForInput($lng), + $v->value, + $v->getBylineForInput($lng) + ), + EditingModes::cases() + ) + ); $input->setValue( $user !== null ? $this->retrieveValueFromUser($user) - : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + : EditingModes::getDefaultMode()->value ); return $input; } + #[\Override] public function getDefaultValueForDisplay( Language $lng, \ilSetting $settings ): string { - return $this->buildOptions()[\ilCalendarSettings::_getInstance()->getDefaultDateFormat()]; - } - - private function buildOptions(): array - { - $year = date('Y'); - return [ - \ilCalendarSettings::DATE_FORMAT_DMY => '31.10.' . $year, - \ilCalendarSettings::DATE_FORMAT_YMD => $year . '-10-31', - \ilCalendarSettings::DATE_FORMAT_MDY => '10/31/' . $year - ]; + return EditingModes::getDefaultMode()->getLabelForInput($lng); } + #[\Override] public function hasUserPersonalizedSetting( \ilSetting $settings, \ilObjUser $user ): bool { - return $this->retrieveValueFromUser($user) - !== \ilCalendarSettings::_getInstance()->getDefaultDateFormat(); + return EditingModes::tryFrom($this->retrieveValueFromUser($user)) + !== EditingModes::getDefaultMode(); } + #[\Override] public function persistUserInput( \ilObjUser $user, mixed $input ): \ilObjUser { $user->setPref( - 'date_format', - $input !== null ? $input : (string) \ilCalendarSettings::_getInstance()->getDefaultDateFormat() + 'question_editing_mode', + $input !== null ? $input : EditingModes::getDefaultMode()->value ); return $user; } - public function retrieveValueFromUser(\ilObjUser $user): int + #[\Override] + public function retrieveValueFromUser(\ilObjUser $user): string { - return (int) ($user->getPref('date_format') - ?? \ilCalendarSettings::_getInstance()->getDefaultDateFormat()); + return $user->getPref('question_editing_mode') + ?? EditingModes::getDefaultMode()->value; } } diff --git a/components/ILIAS/Questions/src/UserSettings/EditingModes.php b/components/ILIAS/Questions/src/UserSettings/EditingModes.php index 7dc14af45b20..286520c460a5 100644 --- a/components/ILIAS/Questions/src/UserSettings/EditingModes.php +++ b/components/ILIAS/Questions/src/UserSettings/EditingModes.php @@ -18,119 +18,29 @@ declare(strict_types=1); -namespace ILIAS\Calendar\UserSettings; +namespace ILIAS\Questions\UserSettings; -use ILIAS\User\Settings\SettingDefinition; -use ILIAS\User\Settings\AvailablePages; -use ILIAS\User\Settings\AvailableSections; use ILIAS\Language\Language; -use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Input; -use ILIAS\Refinery\Factory as Refinery; -class DateFormat implements SettingDefinition +enum EditingModes: string { - public function getIdentifier(): string - { - return 'date_format'; - } - - public function isAvailable(): bool - { - return true; - } - - public function getLabel(Language $lng): string - { - return $lng->txt('cal_user_date_format'); - } - - public function getSettingsPage(): AvailablePages - { - return AvailablePages::MainSettings; - } - - public function getSection(): AvailableSections - { - return AvailableSections::DateTime; - } - - public function getInput( - FieldFactory $field_factory, - Language $lng, - Refinery $refinery, - \ilSetting $settings, - ?\ilObjUser $user = null - ): Input { - $lng->loadLanguageModule('dateplaner'); - return $field_factory->select( - $lng->txt('cal_user_date_format'), - $this->buildOptions(), - $lng->txt('cal_date_format_info') - )->withRequired(true) - ->withValue( - $user !== null - ? $this->retrieveValueFromUser($user) - : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() - ); - } - - public function getLegacyInput( - Language $lng, - \ilSetting $settings, - ?\ilObjUser $user = null - ): \ilFormPropertyGUI { - $lng->loadLanguageModule('dateplaner'); - $input = new \ilSelectInputGUI($lng->txt('cal_user_date_format')); - $input->setOptions($this->buildOptions()); - $input->setInfo($lng->txt('cal_date_format_info')); - $input->setValue( - $user !== null - ? $this->retrieveValueFromUser($user) - : \ilCalendarSettings::_getInstance()->getDefaultDateFormat() - ); - return $input; - } + case Simple = 'simple'; + case Full = 'full'; - public function getDefaultValueForDisplay( - Language $lng, - \ilSetting $settings + public function getLabelForInput( + Language $lng ): string { - return $this->buildOptions()[\ilCalendarSettings::_getInstance()->getDefaultDateFormat()]; + return $lng->txt("editing_mode_{$this->value}"); } - private function buildOptions(): array - { - $year = date('Y'); - return [ - \ilCalendarSettings::DATE_FORMAT_DMY => '31.10.' . $year, - \ilCalendarSettings::DATE_FORMAT_YMD => $year . '-10-31', - \ilCalendarSettings::DATE_FORMAT_MDY => '10/31/' . $year - ]; - } - - public function hasUserPersonalizedSetting( - \ilSetting $settings, - \ilObjUser $user - ): bool { - return $this->retrieveValueFromUser($user) - !== \ilCalendarSettings::_getInstance()->getDefaultDateFormat(); - } - - public function persistUserInput( - \ilObjUser $user, - mixed $input - ): \ilObjUser { - $user->setPref( - 'date_format', - $input !== null ? $input : (string) \ilCalendarSettings::_getInstance()->getDefaultDateFormat() - ); - return $user; + public function getBylineForInput( + Language $lng + ): string { + return $lng->txt("byline_editing_mode_{$this->value}"); } - public function retrieveValueFromUser(\ilObjUser $user): int + public static function getDefaultMode(): self { - return (int) ($user->getPref('date_format') - ?? \ilCalendarSettings::_getInstance()->getDefaultDateFormat()); + return self::Simple; } } diff --git a/components/ILIAS/Questions/src/UserSettings/Settings.php b/components/ILIAS/Questions/src/UserSettings/Settings.php index 712899fd1073..898dfd5e41aa 100644 --- a/components/ILIAS/Questions/src/UserSettings/Settings.php +++ b/components/ILIAS/Questions/src/UserSettings/Settings.php @@ -18,16 +18,17 @@ declare(strict_types=1); -namespace ILIAS\Authentication\UserSettings; +namespace ILIAS\Questions\UserSettings; use ILIAS\User\Settings\UserSettings; class Settings implements UserSettings { + #[\Override] public function getSettingConfigurations(): array { return [ - Password::class + EditingMode::class ]; } } From e42f7a40ba29bf1e00ea0bf74ddeb96d83badea7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 5 Feb 2026 11:06:11 +0100 Subject: [PATCH 050/108] Questions: Add Simple Create Mode --- .../class.ilObjQuestionsGUI.php | 51 ++++- .../ILIAS/Questions/Legacy/LocalDIC.php | 7 +- .../ConfigurationRepository.php | 85 +++++++ .../Administration/class.ConfigurationGUI.php | 116 ++++++++++ .../Definitions/EnvironmentImplementation.php | 131 ++++++++--- .../Presentation/Layout/QuestionsTable.php | 6 +- .../Questions/src/Presentation/Views/Edit.php | 208 ++++++++++++------ .../src/Question/QuestionImplementation.php | 23 +- .../Questions/src/Question/Views/Edit.php | 135 +++++++----- .../{EditingMode.php => CreateMode.php} | 77 +++++-- .../{EditingModes.php => CreateModes.php} | 6 +- .../Questions/src/UserSettings/Settings.php | 2 +- 12 files changed, 644 insertions(+), 203 deletions(-) create mode 100755 components/ILIAS/Questions/src/Administration/ConfigurationRepository.php create mode 100755 components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php rename components/ILIAS/Questions/src/UserSettings/{EditingMode.php => CreateMode.php} (60%) rename components/ILIAS/Questions/src/UserSettings/{EditingModes.php => CreateModes.php} (86%) diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index ff2f24adef02..ffe88d1e6855 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Questions\Administration\ConfigurationGUI; +use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; @@ -27,11 +29,19 @@ /** * @ilCtrl_isCalledBy ilObjQuestionsGUI: ilAdministrationGUI * @ilCtrl_Calls ilObjQuestionsGUI: ilPermissionGUI + * @ilCtrl_Calls ilObjQuestionsGUI: ILIAS\Questions\Administration\ConfigurationGUI * @ilCtrl_Calls ilObjQuestionsGUI: QstsQuestionPageGUI */ class ilObjQuestionsGUI extends ilObjectGUI { - private Edit $edit_view; + private const string TAB_IDENTIFIER_QUESTIONS = 'questions'; + private const string TAB_IDENTIFIER_SETTINGS = 'settings'; + private const string TAB_IDENTIFIER_UNITS = 'units'; + private const string TAB_IDENTIFIER_PERMISSIONS = 'perm_settings'; + + private readonly UnitsRepository $units_repository; + private readonly Edit $edit_view; + private readonly ConfigurationRepository $configurations_repository; private DataFactory $data_factory; @@ -44,7 +54,10 @@ public function __construct( global $DIC; $this->data_factory = new DataFactory(); - $this->edit_view = LocalDIC::dic()[Edit::class]; + $local_dic = LocalDIC::dic(); + $this->units_repository = $local_dic[UnitsRepository::class]; + $this->edit_view = $local_dic[Edit::class]; + $this->configurations_repository = $local_dic[ConfigurationRepository::class]; $this->type = 'qsts'; @@ -70,7 +83,7 @@ public function executeCommand(): void break; case strtolower(ilPermissionGUI::class): - $this->tabs_gui->activateTab('perm_settings'); + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_PERMISSIONS); $this->ctrl->forwardCommand(new \ilPermissionGUI($this)); break; @@ -83,6 +96,22 @@ public function executeCommand(): void ); break; + case strtolower(ConfigurationGUI::class): + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_SETTINGS); + $this->ctrl->forwardCommand( + new ConfigurationGUI( + $this->ctrl, + $this->http, + $this->lng, + $this->refinery, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->configuration_repository + ) + ); + break; + default: if ($cmd === null || $cmd === '' || $cmd === 'view') { $cmd = 'viewQuestions'; @@ -116,17 +145,23 @@ protected function getTabs(): void { if ($this->rbac_system->checkAccess('read', $this->object->getRefId())) { $this->tabs_gui->addTab( - 'questions', - $this->lng->txt('questions'), + self::TAB_IDENTIFIER_QUESTIONS, + $this->lng->txt(self::TAB_IDENTIFIER_QUESTIONS), $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') ); + + $this->tabs_gui->addTab( + self::TAB_IDENTIFIER_SETTINGS, + $this->lng->txt(self::TAB_IDENTIFIER_SETTINGS), + $this->ctrl->getLinkTargetByClass(ConfigurationGUI::class, ''), + ); } if ($this->rbac_system->checkAccess('edit_permission', $this->object->getRefId())) { $this->tabs_gui->addTab( - 'perm_settings', - $this->lng->txt('perm_settings'), - $this->ctrl->getLinkTargetByClass([self::class, ilPermissionGUI::class], 'perm') + self::TAB_IDENTIFIER_PERMISSIONS, + $this->lng->txt(self::TAB_IDENTIFIER_PERMISSIONS), + $this->ctrl->getLinkTargetByClass([self::class, ilPermissionGUI::class], 'perm'), ); } } diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 67ca265a7be7..d68be010cefd 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -20,15 +20,19 @@ namespace ILIAS\Questions\Legacy; +use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Persistence\Repository as QuestionsRepository; use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; +use ILIAS\Questions\Units\Repository as UnitsRepository; +use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\DI\Container as ILIASContainer; +use ILIAS\User\Settings\Settings as UserSettings; use Mustache\Engine as MustacheEngine; use Pimple\Container as PimpleContainer; @@ -76,8 +80,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Edit::class] = static fn($c): Edit => new Edit( $DIC['lng'], - $DIC['ilSetting'], - $DIC['user']->getSettings(), + $c[ConfigurationRepository::class], $DIC['user']->getLoggedInUser(), $DIC['refinery'], $DIC['ui.factory'], diff --git a/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php new file mode 100755 index 000000000000..34ddc3dd9a1a --- /dev/null +++ b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php @@ -0,0 +1,85 @@ +user_setting_create_mode->isChangeableByUser(); + } + + public function getGlobalCreateMode(): CreateModes + { + return CreateModes::tryFrom( + $this->questions_settings->get( + self::SETTINGS_KEY_CREATE_MODE, + '' + ) + ) ?? CreateModes::getDefaultMode(); + } + + public function isCreateModeSimple( + EnvironmentImplementation $environment + ): bool { + return $this->isCreateModeChangeableByUser() && $environment->isCreateModeSimple() + || $this->getGlobalCreateMode() === CreateModes::Simple; + } + + public function persistCreateMode( + CreateModes $create_mode + ): void { + $this->questions_settings->set( + self::SETTINGS_KEY_CREATE_MODE, + $create_mode->value + ); + } + + public function getInputForCreateMode( + FieldFactory $field_factory, + Language $lng, + Refinery $refinery + ): Input { + return $this->user_setting_create_mode->getInput( + $field_factory, + $lng, + $refinery, + $this->common_settings + ); + } +} diff --git a/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php new file mode 100755 index 000000000000..925924390013 --- /dev/null +++ b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php @@ -0,0 +1,116 @@ +ctrl->getCmd(self::CMD_DEFAULT) . 'Cmd'; + $this->$cmd(); + } + + private function viewCmd( + ?StandardForm $form = null + ): void { + $this->tpl->setContent( + $this->ui_renderer->render( + $form ?? $this->buildSettingsForm() + ) + ); + } + + private function saveCmd(): void + { + $form = $this->buildSettingsForm()->withRequest($this->http->request()); + $data = $form->getData(); + if ($data === null) { + $this->viewCmd($form); + } + + $this->repository->persistCreateMode( + $data['default_user_settings']['create_mode'] + ); + + $this->ctrl->redirectByClass( + [ + \ilAdministrationGUI::class, + \ilObjQuestionsGUI::class, + self::class + ] + ); + } + + private function buildSettingsForm(): StandardForm + { + return $this->ui_factory->input()->container()->form()->standard( + $this->ctrl->getLinkTargetByClass( + [ + \ilAdministrationGUI::class, + \ilObjQuestionsGUI::class, + self::class + ], + self::CMD_SAVE + ), + [ + 'default_user_settings' => $this->ui_factory->input()->field()->section( + [ + 'create_mode' => $this->repository->getInputForCreateMode( + $this->ui_factory->input()->field(), + $this->lng, + $this->refinery + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + static fn(string $v): CreateModes => CreateModes::tryFrom($v) + ?? CreateModes::getDefaultMode() + ) + ) + ], + $this->lng->txt('default_user_settings') + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index f4baf4d425e7..acb7da1230ca 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -40,10 +40,12 @@ class EnvironmentImplementation implements Environment private const string TOKEN_STRING_ACTION = 'a'; private const string TOKEN_STRING_STEP = 's'; private const string TOKEN_STRING_QUESTION_ID = 'q'; - private const string TOKEN_STRING_QUESTION_IDS = 'qs'; - private const string TOKEN_TYPE_HASH = 't'; - private const string TOKEN_TABLE_ROW_ID = 'r'; - private const string TOKEN_CARRY_ID = 'c'; + private const string TOKEN_STRING_TYPE_HASH = 't'; + private const string TOKEN_STRING_TABLE_ROW_ID = 'r'; + private const string TOKEN_STRING_CARRY_ID = 'c'; + private const string TOKEN_STRING_CREATE_MODE = 'cm'; + + private const string PARAMETER_STRING_HIER_ID = 'hier_id'; private const string INTERRUPTIVE_ITEMS_KEY = 'interruptive_items'; @@ -57,10 +59,7 @@ class EnvironmentImplementation implements Environment private readonly URLBuilderToken $action_token; private readonly URLBuilderToken $step_token; private readonly URLBuilderToken $question_id_token; - private readonly URLBuilderToken $question_ids_token; - private readonly URLBuilderToken $type_hash_token; private readonly URLBuilderToken $table_row_token; - private readonly URLBuilderToken $carry_token; private ?array $table_row_ids = null; @@ -215,11 +214,6 @@ public function getObjId(): int return $this->obj_id; } - public function getQuestionIdsToken(): URLBuilderToken - { - return $this->question_ids_token; - } - public function getAction(): string { return $this->retrieveStringValueForToken($this->action_token); @@ -246,18 +240,50 @@ public function withQuestionIdParameter( public function withAnswerFormTypeHashParameter( string $type_hash ): self { + [ + $url_builder, + $type_hash_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->type_hash_token, $type_hash); + $clone->url_builder = $url_builder + ->withParameter($type_hash_token, $type_hash); return $clone; } public function withCarryParameter( string $carry ): self { + [ + $url_builder, + $carry_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CARRY_ID + ); + $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->carry_token, $carry); + $clone->url_builder = $url_builder + ->withParameter($carry_token, $carry); + return $clone; + } + + public function withCreateModeParameter(): self + { + [ + $url_builder, + $create_mode_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + + $clone = clone $this; + $clone->url_builder = $url_builder + ->withParameter($create_mode_token, '1'); return $clone; } @@ -275,14 +301,14 @@ public function getQuestionId(): ?Uuid } /** - * This function will either return the QuestionIds from the corresponding - * $_GET parameter OR from an InterruptiveItems $_POST value. + * This function will either return the QuestionIds from the $_GET parameter + * for row ids OR from an InterruptiveItems $_POST value. * @return array<\ILIAS\Data\UUID\Uuid>|string|null */ public function getQuestionIds(): array|string|null { return $this->http->wrapper()->query()->retrieve( - $this->question_ids_token->getName(), + $this->table_row_token->getName(), $this->refinery->byTrying([ $this->refinery->custom()->transformation( fn($v): string => $v === ['ALL_OBJECTS'] @@ -309,18 +335,38 @@ public function getQuestionIds(): array|string|null public function getTypeClassHash(): string { - return $this->retrieveStringValueForToken($this->type_hash_token); + [,$type_hash_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + return $this->retrieveStringValueForToken($type_hash_token); } public function getCarry( Transformation $to_form_transformation ): Input|array|string|null { + [, $carry_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CARRY_ID + ); return $this->http->wrapper()->query()->retrieve( - $this->carry_token->getName(), + $carry_token->getName(), $to_form_transformation ); } + public function isCreateModeSimple(): bool + { + [, $create_mode_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + + return $this->http->wrapper()->query()->has( + $create_mode_token->getName() + ); + } + public function setEditAnswerFormTabs( string $cmd_feedback, string $cmd_content_for_repetition @@ -359,12 +405,41 @@ public function setEditAnswerFormTabs( $this->tabs_gui->activateSubTab(self::TAB_ID_ANSWER_FORM); } - public function setParametersForQuestionCmds(): void + public function preserveParametersForPageEditorCmds(): void { + $this->setQuestionIdParamterForPageEditorCmds($this->getQuestionId()); + } + + public function setParamtersForSimpleCreateCmd( + Uuid $question_id + ): void { + $this->setQuestionIdParamterForPageEditorCmds($question_id); + + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + self::PARAMETER_STRING_HIER_ID, + '1' + ); + + [, $create_mode_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $create_mode_token->getName(), + '1' + ); + } + + private function setQuestionIdParamterForPageEditorCmds( + Uuid $question_id + ): void { $this->ctrl->setParameterByClass( \QstsQuestionPageGUI::class, $this->question_id_token->getName(), - $this->getQuestionId()->toString() + $question_id->toString() ); } @@ -376,20 +451,14 @@ private function acquireURLBuilderAndParameters( $this->action_token, $this->step_token, $this->question_id_token, - $this->question_ids_token, - $this->type_hash_token, - $this->table_row_token, - $this->carry_token + $this->table_row_token ] = (new URLBuilder($base_uri)) ->acquireParameters( self::QUERY_PARAMETER_NAME_SPACE, self::TOKEN_STRING_ACTION, self::TOKEN_STRING_STEP, self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_QUESTION_IDS, - self::TOKEN_TYPE_HASH, - self::TOKEN_TABLE_ROW_ID, - self::TOKEN_CARRY_ID + self::TOKEN_STRING_TABLE_ROW_ID ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 9e46ea64fa40..b2f5bde3687c 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -65,7 +65,7 @@ public function getRows( mixed $additional_parameters ): \Generator { $environment_with_action = $this->environment->withActionParameter( - Edit::CMD_EDIT_QUESTION + Edit::ACTION_EDIT_QUESTION ); foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { yield $question->toTableRow( @@ -144,9 +144,9 @@ private function getActions(): array return [ 'delete' => $this->ui_factory->table()->action()->standard( $this->lng->txt('delete'), - $this->environment->withActionParameter(Edit::CMD_DELETE_QUESTIONS) + $this->environment->withActionParameter(Edit::ACTION_DELETE_QUESTIONS) ->getUrlBuilder(), - $this->environment->getQuestionIdsToken() + $this->environment->getTableRowIdToken() )->withAsync(true) ]; } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index bb4423b1301b..a443fab75035 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Presentation\Views; +use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; @@ -35,8 +36,11 @@ use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\UserSettings\CreateMode; +use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\UICore\GlobalTemplate; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -51,13 +55,13 @@ class Edit { - private const string CMD_CREATE_QUESTION = 'create'; - public const string CMD_EDIT_QUESTION = 'edit'; - public const string CMD_DELETE_QUESTIONS = 'delete'; - private const string CMD_CREATE_ANSWER_FORM = 'create_af'; - public const string CMD_OTHER_ANSWER_FORM = 'other_af'; - private const string CMD_EDIT_FEEDBACK = 'edit_f'; - private const string CMD_EDIT_CONTENT_FOR_REPETITION = 'edit_cfr'; + private const string ACTION_CREATE_QUESTION = 'create'; + public const string ACTION_EDIT_QUESTION = 'edit'; + public const string ACTION_DELETE_QUESTIONS = 'delete'; + private const string ACTION_CREATE_ANSWER_FORM = 'create_af'; + public const string ACTION_OTHER_ANSWER_FORM = 'other_af'; + private const string ACTION_EDIT_FEEDBACK = 'edit_f'; + private const string ACTION_EDIT_CONTENT_FOR_REPETITION = 'edit_cfr'; private array $required_capabilities = []; private Editability $editability = Editability::Full; @@ -65,8 +69,7 @@ class Edit public function __construct( private readonly Language $lng, - private readonly \ilSetting $settings, - private readonly UserSettings $user_settings, + private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, private readonly Refinery $refinery, private readonly UIFactory $ui_factory, @@ -127,9 +130,9 @@ public function show( ); return match($environment->getAction()) { - self::CMD_CREATE_QUESTION => $this->createQuestion($environment), - self::CMD_EDIT_QUESTION => $this->editQuestion($environment), - self::CMD_DELETE_QUESTIONS => $this->deleteQuestions($environment), + self::ACTION_CREATE_QUESTION => $this->createQuestion($environment), + self::ACTION_EDIT_QUESTION => $this->editQuestion($environment), + self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), default => $this->showTable($toolbar, $environment) }; } @@ -145,7 +148,7 @@ public function forwardPageCmds( $obj_id ); $this->initializeEditMode($environment); - $environment->setParametersForQuestionCmds(); + $environment->preserveParametersForPageEditorCmds(); $this->content_style->gui()->addCss( $tpl, @@ -161,7 +164,7 @@ public function forwardPageCmds( $obj_id )->withReturnURI( $environment - ->withActionParameter(self::CMD_EDIT_QUESTION) + ->withActionParameter(self::ACTION_EDIT_QUESTION) ->withQuestionIdParameter($environment->getQuestionId()) ->getUrlBuilder() ->buildURI() @@ -179,7 +182,7 @@ public function createAnswerForm( $environment = $this->buildEnvironment( $base_uri, $obj_id - )->withActionParameter(self::CMD_CREATE_ANSWER_FORM) + )->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); $answer_form_type_class_hash = $environment->getTypeClassHash(); @@ -205,7 +208,7 @@ public function createAnswerForm( } return match($environment->getAction()) { - self::CMD_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + self::ACTION_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( $environment, $question, $content_object @@ -228,14 +231,14 @@ public function editAnswerForm( ->withQuestionIdParameter($question->getId()); $environment->setEditAnswerFormTabs( - self::CMD_EDIT_FEEDBACK, - self::CMD_EDIT_CONTENT_FOR_REPETITION + self::ACTION_EDIT_FEEDBACK, + self::ACTION_EDIT_CONTENT_FOR_REPETITION ); $edit_view = $type->getEditView(); - if ($environment->getAction() === self::CMD_OTHER_ANSWER_FORM) { - $environment = $environment->withActionParameter(self::CMD_OTHER_ANSWER_FORM); + if ($environment->getAction() === self::ACTION_OTHER_ANSWER_FORM) { + $environment = $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM); $next = $edit_view->other($environment); } else { $next = $edit_view->edit($environment); @@ -263,15 +266,14 @@ private function createQuestion( $environment->getObjId() )->getEditView( $this->lng, - $this->settings, - $this->user_settings, + $this->configuration_repository, $this->current_user, $this->ui_factory, $this->refinery, $this->http->request(), $this->ctrl )->create( - $environment->withActionParameter(self::CMD_CREATE_QUESTION) + $environment->withActionParameter(self::ACTION_CREATE_QUESTION) ); if ($create instanceof EditForm) { @@ -280,13 +282,11 @@ private function createQuestion( $this->questions_repository->create([$create]); return $this->ctrl->redirectToURL( - $environment - ->withDefaultStep() - ->withActionParameter(self::CMD_EDIT_QUESTION) - ->withQuestionIdParameter($create->getId()) - ->getUrlBuilder() - ->buildURI() - ->__toString() + $this->buildAfterQuestionCreationRedirectUri( + $environment, + $create->getCreateMode(), + $create->getId() + ) ); } @@ -303,7 +303,7 @@ private function editQuestion( $edit = $question->getEditView( $this->lng, - $this->user_settings, + $this->configuration_repository, $this->current_user, $this->ui_factory, $this->refinery, @@ -311,7 +311,7 @@ private function editQuestion( $this->ctrl )->edit( $environment_with_question_parameter - ->withActionParameter(self::CMD_EDIT_QUESTION), + ->withActionParameter(self::ACTION_EDIT_QUESTION), $question->getParticipantView() ); @@ -323,7 +323,7 @@ private function editQuestion( return $this->buildEditStartView( $environment_with_question_parameter ->withDefaultStep() - ->withActionParameter(self::CMD_EDIT_QUESTION), + ->withActionParameter(self::ACTION_EDIT_QUESTION), $edit ); } @@ -341,7 +341,7 @@ private function deleteQuestions( ); } - if ($environment->getStep() === self::CMD_DELETE_QUESTIONS) { + if ($environment->getStep() === self::ACTION_DELETE_QUESTIONS) { $this->deleteSelectedQuestions($question_ids); $this->ctrl->redirectToURL( $environment->getUrlBuilder()->buildURI()->__toString() @@ -353,9 +353,9 @@ private function deleteQuestions( $this->lng->txt('confirm'), $this->lng->txt('qpl_confirm_delete_questions'), $environment->withActionParameter( - self::CMD_DELETE_QUESTIONS + self::ACTION_DELETE_QUESTIONS )->getUrlBuilderWithStepParameter( - self::CMD_DELETE_QUESTIONS + self::ACTION_DELETE_QUESTIONS )->buildURI()->__toString() )->withAffectedItems( $this->buildAffectedItems($question_ids) @@ -370,7 +370,7 @@ private function showTable( $toolbar->addComponent( $this->ui_factory->button()->standard( $this->lng->txt('create'), - $environment->withActionParameter(self::CMD_CREATE_QUESTION) + $environment->withActionParameter(self::ACTION_CREATE_QUESTION) ->getUrlBuilder() ->buildURI() ->__toString() @@ -396,24 +396,34 @@ private function processCreateAnswerForm( $form = $this->buildCreateAnswerForm($environment)->withRequest($this->http->request()); $data = $form->getData(); - return $data === null - ? $form - : $this->forwardCreateAnswerFormCmd( - $environment->withAnswerFormProperties( - $data->buildProperties( - $this->answer_form_factory->getDefaultTypeGenericProperties( - $question->getId(), - $data - ), - null - ) - )->withAnswerFormTypeHashParameter( - $this->answer_form_factory->getHashedClass($data::class) - ), + if ($data === null) { + return $form; + } + + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $this->addQuestionTextToPage( $question, - $content_object, - $data->getEditView() + $data['question_text'] ); + } + + $type_definition = $data['type']; + return $this->forwardCreateAnswerFormCmd( + $environment->withAnswerFormProperties( + $type_definition->buildProperties( + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId(), + $type_definition + ), + null + ) + )->withAnswerFormTypeHashParameter( + $this->answer_form_factory->getHashedClass($type_definition::class) + ), + $question, + $content_object, + $type_definition->getEditView() + ); } private function forwardCreateAnswerFormCmd( @@ -456,7 +466,7 @@ private function initializeEditMode( $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::URL_CREATE_QUESTION, $environment - ->withActionParameter(self::CMD_CREATE_QUESTION) + ->withActionParameter(self::ACTION_CREATE_QUESTION) ->getUrlBuilder() ->buildURI() ); @@ -498,7 +508,7 @@ private function builEditLinksForQuestionListSlate( $links[] = $this->ui_factory->item()->standard( $question->toEditLink( $this->ui_factory->link(), - $environment->withActionParameter(self::CMD_EDIT_QUESTION) + $environment->withActionParameter(self::ACTION_EDIT_QUESTION) ) ); } @@ -511,8 +521,7 @@ private function buildEditStartView( ): EditForm { return $question->getEditView( $this->lng, - $this->settings, - $this->user_settings, + $this->configuration_repository, $this->current_user, $this->ui_factory, $this->refinery, @@ -525,23 +534,34 @@ private function buildEditStartView( } private function buildCreateAnswerForm( - EnvironmentImplementation $environemt + EnvironmentImplementation $environment ): EditForm { $if = $this->ui_factory->input(); - return $environemt->getPresentationFactory()->getEditForm( - $environemt->getUrlBuilder(), + + $inputs = []; + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $inputs['question_text'] = $if->field()->textarea( + $this->lng->txt('question_text') + ); + $environment = $environment->withCreateModeParameter(); + } + + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilder(), $if->field()->section( - [ - $if->field()->select( - $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) - )->withRequired(true) - ], + $inputs + [ + 'type' => $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): ?Definition => $this->answer_form_factory + ->buildTypeDefinitionFromSelectValue($v) + ) + ) + ], $this->lng->txt('create_answer_form') - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $vs): ?Definition => $this->answer_form_factory->buildTypeDefinitionFromSelectValue($vs[0]) - ) ), false ); @@ -598,6 +618,27 @@ private function deleteSelectedQuestions( $this->questions_repository->delete($questions_to_delete); } + private function addQuestionTextToPage( + QuestionImplementation $question, + string $text + ): void { + $page = new \QstsQuestionPage( + $question->getPageId() + ); + $page->setQuestion($question); + $page->buildDom(); + + $page_element = new \ilPCParagraph( + $page + ); + + $page_element->create($page, 'pg'); + $page_element->setLanguage($this->current_user->getLanguage()); + $page_element->setText($text); + + $page->update(); + } + private function buildEnvironment( URI $base_uri, int $obj_id @@ -615,4 +656,31 @@ private function buildEnvironment( $obj_id ); } + + private function buildAfterQuestionCreationRedirectUri( + EnvironmentImplementation $environment, + CreateModes $create_mode, + Uuid $question_uuid + ): string { + if ($create_mode !== CreateModes::Simple) { + return $environment + ->withDefaultStep() + ->withActionParameter(self::ACTION_EDIT_QUESTION) + ->withQuestionIdParameter($question_uuid) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + $environment->setParamtersForSimpleCreateCmd($question_uuid); + + return $this->ctrl->getLinkTargetByClass( + [ + \QstsQuestionPageGUI::class, + \ilPageEditorGUI::class, + \ilPCAnswerFormGUI::class + ], + 'insert' + ); + } } diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index a607cd490046..0532d6e491ab 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Question; +use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Persistence\CoreTables; use ILIAS\Questions\Persistence\Column; @@ -32,6 +33,7 @@ use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; @@ -39,12 +41,12 @@ use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Component\Table\DataRow; -use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Refinery\Factory as Refinery; use Psr\Http\Message\RequestInterface; class QuestionImplementation implements Question { + private ?CreateModes $create_mode = null; private bool $linking_information_updated = false; private bool $self_updated = false; private bool $page_id_updated = false; @@ -246,10 +248,22 @@ public function isClone(): bool return $this->original_id !== null; } + public function getCreateMode(): ?CreateModes + { + return $this->create_mode; + } + + public function withCreateMode( + CreateModes $create_mode + ): self { + $clone = clone $this; + $clone->create_mode = $create_mode; + return $clone; + } + public function getEditView( Language $lng, - \ilSetting $settings, - UserSettings $user_settings, + ConfigurationRepository $configuration_repository, \ilObjUser $current_user, UIFactory $ui_factory, Refinery $refinery, @@ -258,8 +272,7 @@ public function getEditView( ): Views\Edit { return new Views\Edit( $lng, - $settings, - $user_settings, + $configuration_repository, $current_user, $ui_factory, $refinery, diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 8cccb9cc4ae3..72ddeb484e2e 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -20,17 +20,17 @@ namespace ILIAS\Questions\Question\Views; +use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Questions\UserSettings\EditingMode; +use ILIAS\Questions\UserSettings\CreateMode; +use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use Psr\Http\Message\RequestInterface; @@ -41,8 +41,7 @@ class Edit public function __construct( private readonly Language $lng, - private readonly \ilSetting $settings, - private readonly UserSettings $user_settings, + private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, private readonly UIFactory $ui_factory, private readonly Refinery $refinery, @@ -57,11 +56,10 @@ public function create( EnvironmentImplementation $environment ): EditForm|Question { return match ($environment->getStep()) { - self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm( - $environment, - true + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesCreateForm( + $environment ), - default => $this->buildBasicPropertiesForm($environment, true) + default => $this->buildBasicPropertiesCreateForm($environment) }; } @@ -70,73 +68,98 @@ public function edit( Participant $participant_view ): EditForm|Question { return match ($environment->getStep()) { - self::CMD_SAVE_QUESTION => $this->processBasicPropertiesForm( - $environment, - false + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesEditingForm( + $environment ), - default => $this->buildBasicPropertiesForm( - $environment, - false - )->withContentAfterForm( - $this->buildPreviewPanel($environment, $participant_view) - ) + default => $this->buildBasicPropertiesEditingForm($environment) + ->withContentAfterForm( + $this->buildPreviewPanel($environment, $participant_view) + ) }; } - private function buildBasicPropertiesForm( - EnvironmentImplementation $environment, - bool $is_context_create + private function buildBasicPropertiesCreateForm( + EnvironmentImplementation $environment ): EditForm { + $ff = $this->ui_factory->input()->field(); + + $inputs = [ + 'question' => $ff->group( + $this->buildBasicPropertiesInputs() + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() + ) + ]; + + if ($this->configuration_repository->isCreateModeChangeableByUser()) { + $inputs['create_mode'] = $this->configuration_repository->getInputForCreateMode( + $ff, + $this->lng, + $this->refinery + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): CreateModes => CreateModes::tryFrom($v) + ?? $this->configuration_repository->getGlobalCreateMode() + ) + ); + } + return $environment->getPresentationFactory()->getEditForm( $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), - $this->buildBasicPropertiesSection($is_context_create), + $ff->section( + $inputs, + $this->lng->txt('edit_basic_form_properties') + ), true ); } - private function processBasicPropertiesForm( - EnvironmentImplementation $environment, - bool $is_context_create + private function processBasicPropertiesCreateForm( + EnvironmentImplementation $environment ): EditForm|Question { - $form = $this->buildBasicPropertiesForm( - $environment, - $is_context_create + $form = $this->buildBasicPropertiesCreateForm( + $environment )->withRequest($this->request); $data = $form->getData(); + + $mode = $data['create_mode']; + return $data === null ? $form - : $data; + : $data['question']->withCreateMode($mode); } - private function buildBasicPropertiesSection( - bool $needs_create_mode_input - ): Section { - $ff = $this->ui_factory->input()->field(); - - $inputs = $this->buildBasicPropertiesInputs(); - $values = $this->buildBasicPropertiesBasicValuesArray(); - - if ($needs_create_mode_input) { - $user_settings = $this->user_settings - ->getSettingByDefinitionClass(EditingMode::class); - $inputs['create_mode'] = $user_settings->getInput( - $ff, - $this->lng, - $this->refinery, - $this->settings - ); - $values['create_mode'] = $user_settings->retrieveValueFromUser( - $this->current_user - ); - } + private function buildBasicPropertiesEditingForm( + EnvironmentImplementation $environment + ): EditForm { + return $environment->getPresentationFactory()->getEditForm( + $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), + $this->ui_factory->input()->field()->section( + $this->buildBasicPropertiesInputs(), + $this->lng->txt('edit_basic_form_properties') + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() + ), + true + ); + } - $section = $ff->section( - $inputs, - $this->lng->txt('edit_basic_form_properties') - )->withAdditionalTransformation($this->buildAddBasicPropertiesToQuestionTrafo()); + private function processBasicPropertiesEditingForm( + EnvironmentImplementation $environment + ): EditForm|Question { + $form = $this->buildBasicPropertiesEditingForm( + $environment + )->withRequest($this->request); - return $section->withValue($values); + $data = $form->getData(); + return $data === null + ? $form + : $data; } private function buildBasicPropertiesInputs(): array @@ -197,7 +220,7 @@ private function buildPreviewPanel( EnvironmentImplementation $environment, Participant $participant_view ): StandardPanel { - $environment->setParametersForQuestionCmds(); + $environment->preserveParametersForPageEditorCmds(); return $this->ui_factory->panel()->standard( $this->lng->txt('preview'), $this->ui_factory->legacy()->content( diff --git a/components/ILIAS/Questions/src/UserSettings/EditingMode.php b/components/ILIAS/Questions/src/UserSettings/CreateMode.php similarity index 60% rename from components/ILIAS/Questions/src/UserSettings/EditingMode.php rename to components/ILIAS/Questions/src/UserSettings/CreateMode.php index 8da6be0068d9..437d76125b97 100644 --- a/components/ILIAS/Questions/src/UserSettings/EditingMode.php +++ b/components/ILIAS/Questions/src/UserSettings/CreateMode.php @@ -29,12 +29,17 @@ use ILIAS\UI\Component\Input\Input; use ILIAS\Refinery\Factory as Refinery; -class EditingMode implements SettingDefinition +class CreateMode implements SettingDefinition { + private \ilSetting $settings; + public function __construct() + { + $this->settings = new \ilSetting('questions'); + } #[\Override] public function getIdentifier(): string { - return 'question_editing_mode'; + return 'question_create_mode'; } #[\Override] @@ -46,7 +51,7 @@ public function isAvailable(): bool #[\Override] public function getLabel(Language $lng): string { - return $lng->txt('question_editing_mode'); + return $lng->txt('question_create_mode'); } #[\Override] @@ -71,8 +76,8 @@ public function getInput( ): Input { $lng->loadLanguageModule('questions'); return array_reduce( - EditingModes::cases(), - fn(Radio $c, EditingModes $v): Radio => $c->withOption( + CreateModes::cases(), + fn(Radio $c, CreateModes $v): Radio => $c->withOption( $v->value, $v->getLabelForInput($lng), $v->getBylineForInput($lng) @@ -83,7 +88,10 @@ public function getInput( )->withValue( $user !== null ? $this->retrieveValueFromUser($user) - : EditingModes::getDefaultMode()->value + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) ); } @@ -94,21 +102,31 @@ public function getLegacyInput( ?\ilObjUser $user = null ): \ilFormPropertyGUI { $lng->loadLanguageModule('questions'); - $input = new \ilRadioGroupInputGUI($lng->txt('create_mode')); - $input->setOptions( - array_map( - fn(EditingModes $v): \ilRadioOption => new \ilRadioOption( - $v->getLabelForInput($lng), - $v->value, - $v->getBylineForInput($lng) - ), - EditingModes::cases() - ) + $input = array_reduce( + CreateModes::cases(), + function ( + \ilRadioGroupInputGUI $c, + CreateModes $v + ) use ($lng): \ilRadioGroupInputGUI { + $c->addOption( + new \ilRadioOption( + $v->getLabelForInput($lng), + $v->value, + $v->getBylineForInput($lng) + ) + ); + return $c; + }, + new \ilRadioGroupInputGUI($lng->txt('create_mode')) ); + $input->setValue( $user !== null ? $this->retrieveValueFromUser($user) - : EditingModes::getDefaultMode()->value + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) ); return $input; } @@ -118,7 +136,7 @@ public function getDefaultValueForDisplay( Language $lng, \ilSetting $settings ): string { - return EditingModes::getDefaultMode()->getLabelForInput($lng); + return CreateModes::getDefaultMode()->getLabelForInput($lng); } #[\Override] @@ -126,8 +144,11 @@ public function hasUserPersonalizedSetting( \ilSetting $settings, \ilObjUser $user ): bool { - return EditingModes::tryFrom($this->retrieveValueFromUser($user)) - !== EditingModes::getDefaultMode(); + return $this->retrieveValueFromUser($user) + !== $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ); } #[\Override] @@ -136,8 +157,13 @@ public function persistUserInput( mixed $input ): \ilObjUser { $user->setPref( - 'question_editing_mode', - $input !== null ? $input : EditingModes::getDefaultMode()->value + 'question_create_mode', + $input !== null + ? $input + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) ); return $user; } @@ -145,7 +171,10 @@ public function persistUserInput( #[\Override] public function retrieveValueFromUser(\ilObjUser $user): string { - return $user->getPref('question_editing_mode') - ?? EditingModes::getDefaultMode()->value; + return $user->getPref('question_create_mode') + ?? $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ); } } diff --git a/components/ILIAS/Questions/src/UserSettings/EditingModes.php b/components/ILIAS/Questions/src/UserSettings/CreateModes.php similarity index 86% rename from components/ILIAS/Questions/src/UserSettings/EditingModes.php rename to components/ILIAS/Questions/src/UserSettings/CreateModes.php index 286520c460a5..ee6e8c97169f 100644 --- a/components/ILIAS/Questions/src/UserSettings/EditingModes.php +++ b/components/ILIAS/Questions/src/UserSettings/CreateModes.php @@ -22,7 +22,7 @@ use ILIAS\Language\Language; -enum EditingModes: string +enum CreateModes: string { case Simple = 'simple'; case Full = 'full'; @@ -30,13 +30,13 @@ enum EditingModes: string public function getLabelForInput( Language $lng ): string { - return $lng->txt("editing_mode_{$this->value}"); + return $lng->txt("create_mode_{$this->value}"); } public function getBylineForInput( Language $lng ): string { - return $lng->txt("byline_editing_mode_{$this->value}"); + return $lng->txt("byline_create_mode_{$this->value}"); } public static function getDefaultMode(): self diff --git a/components/ILIAS/Questions/src/UserSettings/Settings.php b/components/ILIAS/Questions/src/UserSettings/Settings.php index 898dfd5e41aa..959f42df91b3 100644 --- a/components/ILIAS/Questions/src/UserSettings/Settings.php +++ b/components/ILIAS/Questions/src/UserSettings/Settings.php @@ -28,7 +28,7 @@ class Settings implements UserSettings public function getSettingConfigurations(): array { return [ - EditingMode::class + CreateMode::class ]; } } From c84c7353e3b4a7a1c01448c80fd73a5ebe536cdc Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 5 Feb 2026 18:17:36 +0100 Subject: [PATCH 051/108] Questions: Add Add Legacy Button --- .../Cloze/Properties/Properties.php | 21 +++++-- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 57 ++++++++++++++++--- .../src/Presentation/Layout/EditForm.php | 33 +++++++++-- .../src/Presentation/Layout/Factory.php | 10 +++- .../Questions/src/Presentation/Views/Edit.php | 2 +- .../Questions/src/Question/Views/Edit.php | 8 ++- 6 files changed, 104 insertions(+), 27 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 6a4fb96cd06f..235a53e25314 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -210,15 +210,24 @@ public function buildBasicEditingInputs( FieldFactory $ff, Refinery $refinery, Factory $propteries_factory, - ClozeTextFactory $cloze_text_factory + ClozeTextFactory $cloze_text_factory, + bool $add_legacy_cloze_text_to_input ): Section { + $cloze_text_input = $this->getClozeText()->getInput( + $lng, + $ff, + $cloze_text_factory + ); + + if ($add_legacy_cloze_text_to_input) { + $cloze_text_input = $cloze_text_input->withValue( + strip_tags($this->legacy_cloze_text) + ); + } + return $ff->section( [ - self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getInput( - $lng, - $ff, - $cloze_text_factory - ), + self::FORM_KEY_CLOZE_TEXT => $cloze_text_input, self::FORM_KEY_IDENTICAL_SCORING => ScoringIdentical::buildInput( $lng, $ff, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 7f4acf5ed02e..c2c018a0f1ff 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -42,6 +42,7 @@ class Edit implements EditViewInterface { private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; + private const string STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES = 'altbp'; private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; private const string STEP_SET_GAP_TYPES = 'sgt'; public const string STEP_JUMP_TO_SET_GAP_TYPES = 'jsgt'; @@ -103,7 +104,10 @@ public function edit( $environment->setEditAnswerFormBackTarget(); return match ($step) { - self::STEP_EDIT_BASIC_PROPERTIES => $this->buildBasicEditingForm($environment), + self::STEP_EDIT_BASIC_PROPERTIES => + $this->buildBasicEditingForm($environment, false), + self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => + $this->addLegacyTextToBasicProperties($environment), default => $this->callIntermediateStep($environment, $step) }; } @@ -178,19 +182,48 @@ private function callIntermediateStep( } private function buildBasicEditingForm( - Environment $environment + Environment $environment, + bool $add_legacy_cloze_text_to_input ): EditForm { - return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES), - $environment->getAnswerFormProperties()->buildBasicEditingInputs( + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ + $answer_form_properties = $environment->getAnswerFormProperties(); + + $editing_form = $environment->getPresentationFactory()->getEditForm( + $environment + ->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES) + ->buildURI(), + $answer_form_properties->buildBasicEditingInputs( $this->lng, $this->ui_factory->input()->field(), $this->refinery, $this->properties_factory, - $this->cloze_text_factory + $this->cloze_text_factory, + $add_legacy_cloze_text_to_input ), false ); + + if (!$add_legacy_cloze_text_to_input + && $answer_form_properties->getLegacyClozeText() !== '' + && $answer_form_properties->getClozeText()->getRawRepresentationForPersistence() === '') { + return $editing_form->withInsertLegacyTextsButton( + $environment->getUrlBuilderWithStepParameter( + self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES + )->buildURI() + ); + } + + return $editing_form; + } + + private function addLegacyTextToBasicProperties( + Environment $environment + ): EditForm { + return $this->buildBasicEditingForm( + $environment, + true + ); } private function processBasicEditingForm( @@ -235,7 +268,9 @@ private function buildGapTypesForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS), + $environment + ->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS) + ->buildURI(), $properties->getGaps()->buildGapsTypeInputs( $this->lng, $ff, @@ -278,7 +313,9 @@ private function buildAnswerOptionsForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS), + $environment + ->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS) + ->buildURI(), $properties->getGaps()->buildAnswerOptionsInputs( $this->lng, $ff, @@ -319,7 +356,9 @@ private function buildAssignPointsForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::STEP_SAVE), + $environment + ->getUrlBuilderWithStepParameter(self::STEP_SAVE) + ->buildURI(), $properties->getGaps()->buildPointInputs( $this->lng, $ff, diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 3b6dfbd7c8b1..acf632d20f97 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -20,14 +20,15 @@ namespace ILIAS\Questions\Presentation\Layout; +use ILIAS\Data\URI; use ILIAS\Language\Language; -use ILIAS\UI\Component\Input\Container\Form\Factory as FormFactory; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\UI\URLBuilder; use ILIAS\UI\Renderer as UIRenderer; use Psr\Http\Message\ServerRequestInterface; @@ -41,11 +42,12 @@ class EditForm implements Renderable private ?StandardPanel $content_before_form = null; private ?StandardPanel $content_after_form = null; private ?InterruptiveModal $confirmation = null; + private ?MessageBox $insert_legacy_text_button = null; public function __construct( - private readonly FormFactory $form_factory, + private readonly UIFactory $ui_factory, private readonly Language $lng, - private readonly URLBuilder $url_builder, + private readonly URI $form_target_uri, private readonly Section $main_section_inputs, private readonly bool $is_final_step, private readonly ?Group $carry_inputs @@ -77,6 +79,21 @@ public function withConfirmation( return $clone; } + public function withInsertLegacyTextsButton( + URI $target_uri + ): self { + $clone = clone $this; + $clone->insert_legacy_text_button = $this->ui_factory->messageBox()->info( + $this->lng->txt('insert_legacy_texts_info') + )->withButtons([ + $this->ui_factory->button()->standard( + $this->lng->txt('insert_legacy_texts'), + $target_uri->__toString() + ) + ]); + return $clone; + } + public function render( UIRenderer $ui_renderer ): string { @@ -119,6 +136,10 @@ function ($id) { ); } + if ($this->insert_legacy_text_button !== null) { + $content[] = $this->insert_legacy_text_button; + } + $content[] = $this->form; if ($this->content_after_form !== null) { @@ -130,8 +151,8 @@ function ($id) { private function buildForm(): StandardForm { - $form = $this->form_factory->standard( - $this->url_builder->buildURI()->__toString(), + $form = $this->ui_factory->input()->container()->form()->standard( + $this->form_target_uri->__toString(), $this->buildFormInputs() ); diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index 77a0b02922a8..8dc459e4caef 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -61,16 +61,20 @@ public function getEditOverview( ); } + /** + * @param URLBuilder $url_builder The url_builder MUST have the step set, + * to which the form shall be sent. + */ public function getEditForm( - URLBuilder $url_builder, + URI $form_target_uri, Section $main_section_inputs, bool $is_final_step, ?Group $carry_inputs = null ): EditForm { return new EditForm( - $this->ui_factory->input()->container()->form(), + $this->ui_factory, $this->lng, - $url_builder, + $form_target_uri, $main_section_inputs, $is_final_step, $carry_inputs diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index a443fab75035..4aed1c8c0c6d 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -547,7 +547,7 @@ private function buildCreateAnswerForm( } return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilder(), + $environment->getUrlBuilder()->buildURI(), $if->field()->section( $inputs + [ 'type' => $if->field()->select( diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 72ddeb484e2e..066579e2fd70 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -107,7 +107,9 @@ private function buildBasicPropertiesCreateForm( } return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), + $environment + ->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION) + ->buildURI(), $ff->section( $inputs, $this->lng->txt('edit_basic_form_properties') @@ -136,7 +138,9 @@ private function buildBasicPropertiesEditingForm( EnvironmentImplementation $environment ): EditForm { return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION), + $environment + ->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION) + ->buildURI(), $this->ui_factory->input()->field()->section( $this->buildBasicPropertiesInputs(), $this->lng->txt('edit_basic_form_properties') From dbfb718bd3562a31ea94fc6b3607cd17f119b672 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 5 Feb 2026 20:32:42 +0100 Subject: [PATCH 052/108] Questions: Proberly Escape Legacy Texts --- .../AnswerForm/Migration/MigrationInsert.php | 8 +- .../Migration/MigrationPurifier.php | 117 ++++++++++++++++++ .../Migration/SanitizeLegacyText.php | 60 +++++++++ .../Migration/BasicMigrationFunctions.php | 18 ++- .../Cloze/Migration/MigrationCloze.php | 4 +- .../Cloze/Migration/MigrationLongMenu.php | 4 +- .../Cloze/Properties/Properties.php | 2 +- .../src/Setup/QuestionsMigration.php | 3 +- 8 files changed, 209 insertions(+), 7 deletions(-) create mode 100755 components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index 799e49ffcfc6..222adfdf1cc0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -45,7 +45,8 @@ public function __construct( private readonly int $old_question_id, private readonly Uuid $new_question_id, private readonly Uuid $answer_form_id, - private readonly string $definition_class + private readonly string $definition_class, + private readonly bool $ilias_page_editor_used_for_additional_texts ) { } @@ -79,6 +80,11 @@ public function getAnswerFormId(): Uuid return $this->answer_form_id; } + public function wasIliasPageEditorUsedForAdditionalTexts(): bool + { + return $this->ilias_page_editor_used_for_additional_texts; + } + public function withAvailablePoints( float $available_points ): self { diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php new file mode 100755 index 000000000000..2db2d50a90f7 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php @@ -0,0 +1,117 @@ +set('HTML.DefinitionID', 'migration'); + $config->set('HTML.DefinitionRev', 2); + $config->set('Cache.SerializerPath', sys_get_temp_dir()); + $config->set('HTML.Doctype', 'XHTML 1.0 Strict'); + $config->set('HTML.AllowedElements', $this->getAllowedElements()); + $config->set('HTML.ForbiddenAttributes', 'div@style'); + $config->autoFinalize = false; + $config->set( + 'URI.AllowedSchemes', + array_merge( + $config->get('URI.AllowedSchemes'), + ['data' => true] + ) + ); + $config->autoFinalize = true; + if (($def = $config->maybeGetRawHTMLDefinition()) !== null) { + $def->addAttribute('img', 'data-id', 'Number'); + $def->addAttribute('a', 'target', 'Enum#_blank,_self,_target,_top'); + } + + return $config; + } + + private function getAllowedElements(): array + { + return $this->removeUnsupportedElements( + $this->makeElementListTinyMceCompliant( + $this->getElementsUsedForAdvancedEditing() + ) + ); + } + + private function getElementsUsedForAdvancedEditing(): array + { + $tags = $this->db->fetchObject( + $this->db->query( + 'SELECT value FROM settings' . PHP_EOL + . 'WHERE module = "advanced_editing"' . PHP_EOL + . 'AND keyword = "advanced_editing_used_html_tags_assessment"' + ) + )?->value ?? ''; + + if ($tags === '') { + return self::DEFAULT_TAGS; + } + + return unserialize($tags, ['allowed_classes' => false]); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php b/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php new file mode 100644 index 000000000000..ea64832261d8 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php @@ -0,0 +1,60 @@ +purifiery === null) { + $this->purifiery = new MigrationPurifier($db); + $this->rte_used = $this->fetchIsRteUsedFromDb($db); + } + $cleaned_text = $this->purifiery->purify($text); + + if ($ilias_page_editor_text || !$this->rte_used) { + $cleaned_text = nl2br($cleaned_text); + } + + return \ilLegacyFormElementsUtil::prepareTextareaOutput( + $cleaned_text, + true + ); + } + + private function fetchIsRteUsedFromDb( + \ilDBInterface $db + ): bool { + return $db->fetchObject( + $db->query( + 'SELECT value FROM settings' . PHP_EOL + . 'WHERE module = "advanced_editing"' . PHP_EOL + . 'AND keyword = "advanced_editing_javascript_editor"' + ) + )?->value === 'tinymce'; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index 58b6ce7d69fc..5a5bbc975948 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; +use ILIAS\Questions\AnswerForm\Migration\SanitizeLegacyText; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; @@ -32,6 +33,8 @@ trait BasicMigrationFunctions { + use SanitizeLegacyText; + private function buildGapInsertStatement( Persistence $persistence, TableNameBuilder $table_name_builder, @@ -218,9 +221,11 @@ private function buildNewTextRatingFromOld( } private function replaceGapsAndSantizeLegacyClozeText( + \ilDBInterface $db, string $gap_replace_regex, string $text, - array $gaps_mapping + array $gaps_mapping, + bool $ilias_page_editor_text ): string { ksort($gaps_mapping); @@ -229,7 +234,11 @@ private function replaceGapsAndSantizeLegacyClozeText( function (array $matches) use (&$gaps_mapping): string { return '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gaps_mapping) . '}}'; }, - $text + $this->sanitizeLegacyText( + $db, + $text, + $ilias_page_editor_text + ) ); } @@ -239,4 +248,9 @@ private function limitToFloat( ): float { return (float) $math->e($limit); } + + private function getHtmlQuestionContentPurifier(): \ilHtmlPurifierInterface + { + return \ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution'); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index db0cd67c13a4..48442e2f7f82 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -142,9 +142,11 @@ public function completeMigrationInsert( $answer_options_insert )->withAdditionalTextLegacy( $this->replaceGapsAndSantizeLegacyClozeText( + $migration_insert->getDb(), '\[gap\].+?\[\/gap\]', $db_row->cloze_text, - $answer_input_mapping + $answer_input_mapping, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index f7ce89c023dc..f65f0a6e2a50 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -137,9 +137,11 @@ public function completeMigrationInsert( ) )->withAdditionalTextLegacy( $this->replaceGapsAndSantizeLegacyClozeText( + $migration_insert->getDb(), '\[Longmenu \d+\]', $db_row->long_menu_text, - $answer_input_mapping + $answer_input_mapping, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() ) )->withAdditionalInsert($gaps_insert) ->withAdditionalInsert($answer_options_insert); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 235a53e25314..4617fe9c272c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -131,7 +131,7 @@ public function getLegacyClozeText(): string public function getClozeTextForPresentation(): string { return $this->cloze_text->getRawRepresentationForPersistence() === '' - ? $this->legacy_cloze_text + ? \ilRTE::_replaceMediaObjectImageSrc($this->legacy_cloze_text, 0) : $this->cloze_text->getRenderedMarkdownForParticipantPresentation(); } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 1ec6b1813e56..a02fcb248534 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -366,7 +366,8 @@ private function buildMigrationInsert( $db_values->question_id, $new_question_id, $this->uuid_factory->uuid4(), - $answer_form_migration->getDefinitionClass() + $answer_form_migration->getDefinitionClass(), + $db_values->add_cont_edit_mode === \assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE ); } } From 358ec70bf6b1578a70eb35397d60f6f772acb723 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 11:11:18 +0100 Subject: [PATCH 053/108] Questions: Add forgotten Parameter --- .../ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index c2c018a0f1ff..011958951da6 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -71,7 +71,7 @@ public function create( $step = $environment->getStep(); return match($step) { - '' => $this->buildBasicEditingForm($environment), + '' => $this->buildBasicEditingForm($environment, false), default => $this->callIntermediateStep($environment, $step) }; } From 5ad9964a527c2269dca9d66ffe65ce1fff6f23d6 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 11:40:29 +0100 Subject: [PATCH 054/108] Questions: Fix Migration of LongMenu-Forms --- .../Cloze/Migration/MigrationLongMenu.php | 59 ++++++++++++------- .../ILIAS/Questions/src/Persistence/Query.php | 5 +- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index f65f0a6e2a50..f88a3d445179 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -59,23 +59,25 @@ public function completeMigrationInsert( Environment $environment, MigrationInsert $migration_insert ): ?MigrationInsert { + $answer_form_id = $migration_insert->getAnswerFormId(); + $answer_input_mapping = []; $gaps_insert = null; - $answer_options_insert = null; + $answer_options_to_insert = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), $migration_insert->getOldQuestionId() ) as $db_row) { - $answer_form_id = $migration_insert->getAnswerFormId(); if (!isset($answer_input_mapping[$db_row->gap_number])) { - $answer_input_mapping[$db_row->gap_number] = $migration_insert->getUuid(); + $answer_input_id = $migration_insert->getUuid(); + $answer_input_mapping[$db_row->gap_number] = $answer_input_id; $gaps_insert = $this->buildGapInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), $gaps_insert, - $answer_input_mapping[$db_row->gap_number], + $answer_input_id, $answer_form_id, $db_row->gap_number, $this->buildNewGapTypeIdentifierFromOld($db_row->type), @@ -86,39 +88,56 @@ public function completeMigrationInsert( $db_row->shuffle_answers === '1' ? 1 : 0 ); - $answers = array_map( - fn(string $v) => [ - 'answer_input_id' => $migration_insert->getUuid(), - 'text' => trim($v), + $answers_from_file = $this->loadAnswersFromFile( + $environment->getResource(Environment::RESOURCE_ILIAS_INI), + $environment->getResource(Environment::RESOURCE_CLIENT_ID), + $migration_insert->getOldQuestionId(), + $db_row->gap_number + ); + + $answer_options_to_insert[$answer_input_id->toString()] = array_map( + fn(int $v) => [ + 'answer_option_id' => $migration_insert->getUuid(), + 'answer_input_id' => $answer_input_id, + 'position' => $v, + 'text' => trim($answers_from_file[$v]), 'points' => 0.0 ], - $this->loadAnswersFromFile( - $environment->getResource(Environment::RESOURCE_ILIAS_INI), - $environment->getResource(Environment::RESOURCE_CLIENT_ID), - $migration_insert->getOldQuestionId(), - $db_row->gap_number - ) + array_keys($answers_from_file) ); } - if (isset($answers[$db_row->position]) - && $answers[$db_row->position]['text'] === trim($db_row->answer_text)) { - $answers[$db_row->position]['points'] = $db_row->points; + $answer_input_id_string = $answer_input_mapping[$db_row->gap_number]->toString(); + $position = array_find_key( + $answer_options_to_insert[$answer_input_id_string], + fn(array $v, int $k): bool => trim($v['text']) === trim($db_row->answer_text) + ); + + if ($position !== null) { + $answer_options_to_insert[$answer_input_id_string][$position]['points'] = $db_row->points; + ; } + + } if (!isset($db_row)) { return null; } - foreach ($answers as $position => $answer) { + $answer_options_insert = null; + foreach (array_reduce( + $answer_options_to_insert, + fn(array $c, array $v): array => [...$c, ...$v], + [] + ) as $answer) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->persistence, $migration_insert->getTableNameBuilder(), $answer_options_insert, + $answer['answer_option_id'], $answer['answer_input_id'], - $answer_input_mapping[$db_row->gap_number], - $position, + $answer['position'], $answer['text'], $answer['points'], null, diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 54f04994b400..9588187b8a40 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -175,7 +175,10 @@ public function retrieveCurrentRecord( $table_name = $table->getName(); $filtered_record = []; foreach ($this->current_record as $data_set) { - $filtered_record[] = $this->filterDataSetByTable($table_name, $data_set); + $filtered_dataset = $this->filterDataSetByTable($table_name, $data_set); + if (array_filter($filtered_dataset) !== []) { + $filtered_record[] = $filtered_dataset; + } } return $transformation->transform($filtered_record); From 9f22d3eb25cdf12feaf4dc9150d033f8d1248356 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 12:13:49 +0100 Subject: [PATCH 055/108] Questions: Show Legacy Text in Panel --- .../Cloze/Layout/CombinationsOverview.php | 6 ++-- .../Cloze/Properties/ClozeText/Text.php | 35 +++++++++++-------- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 12 ++++--- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index a0f344715a59..14c4240512c6 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -196,7 +196,8 @@ private function buildSetCombinationGapsModal(): RoundTripModal $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $gaps + $gaps, + $properties->getLegacyClozeText() ), [ 'combination' => $gaps->buildGapsMultiSelect( @@ -256,7 +257,8 @@ private function buildSetCombinationValuesModal( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $gaps + $gaps, + $properties->getLegacyClozeText() ), [ 'values_awarding_points' => $inputs diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index d6e9cfb092b9..3d85aff02311 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -88,29 +88,20 @@ public function getRenderedMarkdownForParticipantPresentation(): string public function buildPanelForEditing( UIFactory $ui_factory, Language $lng, - Gaps $gaps + Gaps $gaps, + string $legacy_cloze_text ): StandardPanel { return $ui_factory->panel()->standard( $lng->txt('cloze_text'), - $ui_factory->legacy()->content( + $ui_factory->legacy()->latexContent( $this->getRenderedMarkdownForEditingPresentation( - $gaps + $gaps, + $legacy_cloze_text ) ) ); } - public function getRenderedMarkdownForEditingPresentation( - Gaps $gaps - ): string { - return $this->mustache_engine->render( - $this->refinery->string()->markdown()->toHTML()->transform( - $this->cloze_text->getRawRepresentation() - ), - $gaps->getPlaceholderArrayForEditFormPanel() - ); - } - public function updateGapsFromMarkdown( Uuid $answer_form_id, Gaps $pre_existing_gaps @@ -171,6 +162,22 @@ function (array $matches) use (&$new_gaps): string { return $clone; } + private function getRenderedMarkdownForEditingPresentation( + Gaps $gaps, + string $legacy_cloze_text + ): string { + $text = $this->cloze_text->getRawRepresentation() === '' + ? $legacy_cloze_text + : $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ); + + return $this->mustache_engine->render( + $text, + $gaps->getPlaceholderArrayForEditFormPanel() + ); + } + private function hasAtLeastOneGap(): bool { if ($this->cloze_text->getRawRepresentation() === '') { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 011958951da6..2c9d69022feb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -285,7 +285,8 @@ private function buildGapTypesForm( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $properties->getGaps() + $properties->getGaps(), + $properties->getLegacyClozeText() ) ); } @@ -328,7 +329,8 @@ private function buildAnswerOptionsForm( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $properties->getGaps() + $properties->getGaps(), + $properties->getLegacyClozeText() ) ); } @@ -371,7 +373,8 @@ private function buildAssignPointsForm( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $properties->getGaps() + $properties->getGaps(), + $properties->getLegacyClozeText() ) ); } @@ -390,7 +393,8 @@ private function processAssignPointsForm( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, $this->lng, - $properties->getGaps() + $properties->getGaps(), + $properties->getLegacyClozeText() ) ) : $properties->withGaps($data); } From c603201f1d68bbeabcbfc1de79ee44a1bfb1cecf Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 13:33:36 +0100 Subject: [PATCH 056/108] Questions: Move Adding Text to Page --- .../Legacy/PageEditor/QstsQuestionPage.php | 18 ++++++++++++ .../Questions/src/Presentation/Views/Edit.php | 28 ++----------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 9e23acb982f2..cc8e263462ca 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -40,4 +40,22 @@ public function setQuestion( ): void { $this->question = $question; } + + public function addQuestionText( + string $text + ): void { + $this->buildDom(); + + $lng = $this->user->getLanguage(); + if ($lng === '') { + $lng = 'de'; + } + + $page_element = new \ilPCParagraph($this); + $page_element->create($this, 'pg'); + $page_element->setLanguage($lng); + $page_element->setText($text); + + $this->update(); + } } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 4aed1c8c0c6d..f96c9d373c73 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -401,10 +401,9 @@ private function processCreateAnswerForm( } if ($this->configuration_repository->isCreateModeSimple($environment)) { - $this->addQuestionTextToPage( - $question, - $data['question_text'] - ); + $question_page = new \QstsQuestionPage($question->getPageId()); + $question_page->setQuestion($question); + $question_page->addQuestionText($data['question_text']); } $type_definition = $data['type']; @@ -618,27 +617,6 @@ private function deleteSelectedQuestions( $this->questions_repository->delete($questions_to_delete); } - private function addQuestionTextToPage( - QuestionImplementation $question, - string $text - ): void { - $page = new \QstsQuestionPage( - $question->getPageId() - ); - $page->setQuestion($question); - $page->buildDom(); - - $page_element = new \ilPCParagraph( - $page - ); - - $page_element->create($page, 'pg'); - $page_element->setLanguage($this->current_user->getLanguage()); - $page_element->setText($text); - - $page->update(); - } - private function buildEnvironment( URI $base_uri, int $obj_id From fdab3421d12c41abe84b051b097a44fd016d4528 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 13:48:05 +0100 Subject: [PATCH 057/108] Questions: Function Needs to be Public --- .../class.ilPCLegacyAnswerFormText.php | 4 ++- .../Cloze/Properties/ClozeText/Text.php | 32 +++++++++---------- .../Cloze/Properties/Properties.php | 5 ++- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php index 19c9c39495a3..a5fdabc3cbfb 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php @@ -41,7 +41,9 @@ public function modifyPageContentPostXsl( return mb_ereg_replace_callback( self::TEXT_PLACEHOLDER, - static fn(array $matches): string => base64_decode($matches[1]), + static fn(array $matches): string => \ilRTE::_replaceMediaObjectImageSrc( + base64_decode($matches[1]) + ), $output ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 3d85aff02311..17b414a3abfe 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -102,6 +102,22 @@ public function buildPanelForEditing( ); } + public function getRenderedMarkdownForEditingPresentation( + Gaps $gaps, + string $legacy_cloze_text + ): string { + $text = $this->cloze_text->getRawRepresentation() === '' + ? $legacy_cloze_text + : $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ); + + return $this->mustache_engine->render( + $text, + $gaps->getPlaceholderArrayForEditFormPanel() + ); + } + public function updateGapsFromMarkdown( Uuid $answer_form_id, Gaps $pre_existing_gaps @@ -162,22 +178,6 @@ function (array $matches) use (&$new_gaps): string { return $clone; } - private function getRenderedMarkdownForEditingPresentation( - Gaps $gaps, - string $legacy_cloze_text - ): string { - $text = $this->cloze_text->getRawRepresentation() === '' - ? $legacy_cloze_text - : $this->refinery->string()->markdown()->toHTML()->transform( - $this->cloze_text->getRawRepresentation() - ); - - return $this->mustache_engine->render( - $text, - $gaps->getPlaceholderArrayForEditFormPanel() - ); - } - private function hasAtLeastOneGap(): bool { if ($this->cloze_text->getRawRepresentation() === '') { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 4617fe9c272c..42f0d41c5a2a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -181,7 +181,10 @@ public function getBasicPropertiesForListing( ): array { return [ $lng->txt('cloze_text') => $this->cloze_text - ->getRenderedMarkdownForEditingPresentation($this->gaps), + ->getRenderedMarkdownForEditingPresentation( + $this->gaps, + $this->getLegacyClozeText() + ), $lng->txt('score_identical') => $this->scoring_identical ->getTranslatedOptionName($lng), $lng->txt('gap_combinations') => $this->combinations->areCombinationsEnabled() From 6199b1fcc61b3b2a08f16847c4e2f6df573a39e5 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 14:19:33 +0100 Subject: [PATCH 058/108] Questions: Update Method Call --- .../ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 2c9d69022feb..5bbd49213efc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -230,7 +230,8 @@ private function processBasicEditingForm( Environment $environment ): EditForm|Properties { $form = $this->buildBasicEditingForm( - $environment + $environment, + false )->withRequest($this->http->request()); $data = $form->getData(); From 469ce1a8f0475c3e3dd1a8d28fc7a3f889b87eb7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 6 Feb 2026 16:31:09 +0100 Subject: [PATCH 059/108] Questions: Introduce Persistence Factory --- .../ILIAS/Questions/Legacy/LocalDIC.php | 13 +- components/ILIAS/Questions/Questions.php | 3 + .../AnswerForm/Migration/MigrationInsert.php | 52 ++- .../Questions/src/AnswerForm/Persistence.php | 4 + .../src/AnswerForm/TypeGenericProperties.php | 124 +++++--- .../Migration/BasicMigrationFunctions.php | 58 ++-- .../Cloze/Migration/MigrationCloze.php | 43 ++- .../Cloze/Migration/MigrationLongMenu.php | 3 + .../Cloze/Migration/MigrationNumeric.php | 3 + .../Cloze/Migration/MigrationTextSubset.php | 3 + .../src/AnswerFormTypes/Cloze/Persistence.php | 114 +++++-- .../Properties/Combinations/Combination.php | 56 ++-- .../Cloze/Properties/Combinations/Factory.php | 2 + .../Properties/Combinations/MatchingValue.php | 26 +- .../Cloze/Properties/Factory.php | 1 + .../Gaps/AnswerOptions/AnswerOption.php | 50 ++- .../Gaps/AnswerOptions/AnswerOptions.php | 24 +- .../Properties/Gaps/AnswerOptions/Factory.php | 5 +- .../Cloze/Properties/Gaps/Factory.php | 5 +- .../Cloze/Properties/Gaps/Gap.php | 38 ++- .../Cloze/Properties/Gaps/Gaps.php | 38 ++- .../Cloze/Properties/Properties.php | 52 ++- .../Questions/src/Persistence/CoreTables.php | 50 +-- .../Questions/src/Persistence/Factory.php | 125 ++++++++ .../ILIAS/Questions/src/Persistence/Join.php | 2 +- .../Questions/src/Persistence/Manipulate.php | 6 + .../ILIAS/Questions/src/Persistence/Order.php | 2 +- .../ILIAS/Questions/src/Persistence/Query.php | 56 +++- .../Questions/src/Persistence/Repository.php | 48 ++- .../ILIAS/Questions/src/Persistence/Table.php | 4 +- .../Questions/src/Persistence/TableTypes.php | 5 +- .../ILIAS/Questions/src/Persistence/Where.php | 6 +- .../Questions/src/Presentation/Views/Edit.php | 1 - .../src/Question/QuestionImplementation.php | 295 ++++++++++++------ .../ILIAS/Questions/src/Setup/Agent.php | 3 + .../src/Setup/QuestionsMigration.php | 94 ++++-- 36 files changed, 1021 insertions(+), 393 deletions(-) create mode 100644 components/ILIAS/Questions/src/Persistence/Factory.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index d68be010cefd..8a2fc5f108cf 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -25,6 +25,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Questions\Units\Repository as UnitsRepository; @@ -32,7 +33,6 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\DI\Container as ILIASContainer; -use ILIAS\User\Settings\Settings as UserSettings; use Mustache\Engine as MustacheEngine; use Pimple\Container as PimpleContainer; @@ -57,6 +57,14 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[MustacheEngine::class] = static fn($c): MustacheEngine => new MustacheEngine(['escape' => static fn($v) => $v]); + $dic[ConfigurationRepository::class] = static fn($c): ConfigurationRepository + => new ConfigurationRepository( + $DIC['ilSetting'], + $DIC['user']->getSettings()->getSettingByDefinitionClass( + CreateMode::class + ), + new \ilSetting('questions') + ); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( $c[UuidFactory::class], @@ -68,7 +76,8 @@ protected static function buildDIC(ILIASContainer $DIC): self new QuestionsRepository( $DIC['ilDB'], $DIC['refinery'], - new UuidFactory(), + $c[UuidFactory::class], + new PersistenceFactory(), $c[AnswerFormFactory::class] ); $dic[LayoutFactory::class] = static fn($c): LayoutFactory => diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 5f1ce88139e0..00a64c0ca053 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -27,6 +27,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationNumeric; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationTextSubset; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Questions\Setup\Agent; use ILIAS\Setup\Agent as AgentInterface; @@ -46,6 +47,7 @@ public function init( $define[] = AnswerFormDefinition::class; $contribute[AgentInterface::class] = static fn() => new Agent( + $internal[PersistenceFactory::class], $seek[AnswerFormMigration::class] ); $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze( @@ -70,6 +72,7 @@ public function init( $internal[Persistence::class] = static fn() => new Persistence( new TableNameSpaceCore('cloze') ); + $internal[PersistenceFactory::class] = static fn() => new PersistenceFactory(); $internal[\EvalMath::class] = static fn() => new \EvalMath(); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index 222adfdf1cc0..bf2ee1afeb80 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -21,9 +21,9 @@ namespace ILIAS\Questions\AnswerForm\Migration; use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\Value; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\CLI\IOWrapper; @@ -40,6 +40,7 @@ public function __construct( private readonly \ilDBInterface $db, private readonly IOWrapper $io, private readonly UuidFactory $uuid_factory, + private readonly PersistenceFactory $persistence_factory, private readonly TableNameBuilder $table_name_builder, private array $inserts, private readonly int $old_question_id, @@ -65,6 +66,11 @@ public function getUuid(): Uuid return $this->uuid_factory->uuid4(); } + public function getPersistenceFactory(): PersistenceFactory + { + return $this->persistence_factory; + } + public function getTableNameBuilder(): TableNameBuilder { return $this->table_name_builder; @@ -160,23 +166,45 @@ function (\ilDBInterface $db) use ($manipulates): void { private function buildCoreAnswerFormInsertStatement(): Insert { - return new Insert( - CoreTables::AnswerForms->getColumns(), + return $this->persistence_factory->insert( + CoreTables::AnswerForms->getColumns( + $this->persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->definition_class), - new Value(\ilDBConstants::T_TEXT, $this->new_question_id->toString()), - new Value(\ilDBConstants::T_FLOAT, $this->available_points), - new Value(\ilDBConstants::T_INTEGER, $this->image_size), - new Value( + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->definition_class + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->new_question_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $this->persistence_factory->value( \ilDBConstants::T_INTEGER, $this->shuffle_answer_options === null ? null : ($this->shuffle_answer_options ? 1 : 0) ), - new Value(\ilDBConstants::T_TEXT, $this->additional_text), - new Value(\ilDBConstants::T_TEXT, $this->additional_text_legacy) - + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text_legacy + ) ] ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php index 6b0484912f80..f4aba32eca06 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerForm; use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; @@ -31,6 +32,7 @@ interface Persistence public function getTableNameSpace(): TableNameSpace; public function getColumns( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '', @@ -38,12 +40,14 @@ public function getColumns( ): array; public function getIdColumn( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '' ): Column; public function getForeignKeyColumn( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '' diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 9f924b9b78d3..6e90e88dd8a6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -22,12 +22,12 @@ use ILIAS\Questions\Persistence\CoreTables; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\Storable; use ILIAS\Questions\Persistence\Update; -use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Persistence\Where; use ILIAS\Data\UUID\Uuid; @@ -85,6 +85,7 @@ public function getAdditionalTextLegacy(): string return $this->additional_text_legacy; } + #[\Override] public function toStorage( Manipulate $manipulate ): Manipulate { @@ -95,23 +96,28 @@ public function toStorage( } return $manipulate->withAdditionalStatement( $manipulate->getManipulationType() === ManipulationType::Create - ? $this->buildInsertStatement() - : $this->buildUpdateStatement() + ? $this->buildInsertStatement($manipulate->getPersistenceFactory()) + : $this->buildUpdateStatement($manipulate->getPersistenceFactory()) ); } + #[\Override] public function toDelete( Manipulate $manipulate ): Manipulate { $answer_form_table_definition = CoreTables::AnswerForms; return $manipulate->withAdditionalStatement( - new Delete( - $answer_form_table_definition->getTable(), + $manipulate->getPersistenceFactory()->delete( + $answer_form_table_definition->getTable( + $manipulate->getPersistenceFactory() + ), [ - new Where( - $answer_form_table_definition->getIdColumn(), - new Value( + $manipulate->getPersistenceFactory()->where( + $answer_form_table_definition->getIdColumn( + $manipulate->getPersistenceFactory() + ), + $manipulate->getPersistenceFactory()->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ) @@ -121,45 +127,89 @@ public function toDelete( ); } - private function buildInsertStatement(): Insert - { - return new Insert( - CoreTables::AnswerForms->getColumns(), + private function buildInsertStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::AnswerForms->getColumns( + $persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->definition::class), - new Value(\ilDBConstants::T_TEXT, $this->question_id->toString()), - new Value(\ilDBConstants::T_FLOAT, $this->available_points), - new Value(\ilDBConstants::T_INTEGER, $this->image_size), - new Value(\ilDBConstants::T_INTEGER, $this->getShuffleAnswerOptionsForStorage()), - new Value(\ilDBConstants::T_TEXT, $this->additional_text), - new Value(\ilDBConstants::T_TEXT, $this->additional_text_legacy) - + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->definition::class + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->question_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->getShuffleAnswerOptionsForStorage() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text_legacy + ) ] ); } - private function buildUpdateStatement(): Update - { + private function buildUpdateStatement( + PersistenceFactory $persistence_factory + ): Update { $answer_form_table_definition = CoreTables::AnswerForms; - return new Update( - $answer_form_table_definition->getColumns([ - 'id', - 'type', - 'question_id', - 'additional_text_legacy' - ]), + return $persistence_factory->update( + $answer_form_table_definition->getColumns( + $persistence_factory, + [ + 'id', + 'type', + 'question_id', + 'additional_text_legacy' + ] + ), [ - new Value(\ilDBConstants::T_FLOAT, $this->available_points), - new Value(\ilDBConstants::T_INTEGER, $this->image_size), - new Value(\ilDBConstants::T_INTEGER, $this->getShuffleAnswerOptionsForStorage()), - new Value(\ilDBConstants::T_TEXT, $this->additional_text) + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->getShuffleAnswerOptionsForStorage() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ) ], [ - new Where( - $answer_form_table_definition->getIdColumn(), - new Value( + $persistence_factory->where( + $answer_form_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index 5a5bbc975948..58242849b853 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -24,10 +24,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; @@ -37,6 +37,7 @@ trait BasicMigrationFunctions private function buildGapInsertStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $gaps_insert, Uuid $answer_input_id, @@ -50,12 +51,14 @@ private function buildGapInsertStatement( ?int $shuffle ): Insert { if ($gaps_insert === null) { - return new Insert( + return $persistence_factory->insert( $persistence->getColumns( + $persistence_factory, $table_name_builder, TableTypes::AnswerInputs ), $this->buildGapValuesForInsert( + $persistence_factory, $answer_input_id, $answer_form_id, $position, @@ -71,6 +74,7 @@ private function buildGapInsertStatement( return $gaps_insert->withAdditionalValues( $this->buildGapValuesForInsert( + $persistence_factory, $answer_input_id, $answer_form_id, $position, @@ -85,6 +89,7 @@ private function buildGapInsertStatement( } private function buildGapValuesForInsert( + PersistenceFactory $persistence_factory, Uuid $answer_input_id, Uuid $answer_form_id, int $position, @@ -96,20 +101,21 @@ private function buildGapValuesForInsert( ?int $shuffle ): array { return [ - new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $position), - new Value(\ilDBConstants::T_TEXT, $gap_type), - new Value(\ilDBConstants::T_INTEGER, $max_chars), - new Value(\ilDBConstants::T_FLOAT, $step_size), - new Value(\ilDBConstants::T_INTEGER, $matching_options?->value), - new Value(\ilDBConstants::T_INTEGER, $min_autocomplete), - new Value(\ilDBConstants::T_INTEGER, $shuffle) + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_type), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $max_chars), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $step_size), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $matching_options?->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $min_autocomplete), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $shuffle) ]; } private function buildAnswerOptionInsertStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $options_insert, Uuid $answer_option_id, @@ -121,12 +127,14 @@ private function buildAnswerOptionInsertStatement( ?float $upper_limit ): Insert { if ($options_insert === null) { - return new Insert( + return $persistence_factory->insert( $persistence->getColumns( + $persistence_factory, $table_name_builder, TableTypes::AnswerOptions ), $this->buildOptionValuesForInsert( + $persistence_factory, $answer_option_id, $answer_input_id, $position, @@ -140,6 +148,7 @@ private function buildAnswerOptionInsertStatement( return $options_insert->withAdditionalValues( $this->buildOptionValuesForInsert( + $persistence_factory, $answer_option_id, $answer_input_id, $position, @@ -152,6 +161,7 @@ private function buildAnswerOptionInsertStatement( } private function buildOptionValuesForInsert( + PersistenceFactory $persistence_factory, Uuid $answer_option_id, Uuid $answer_input_id, int $position, @@ -161,13 +171,13 @@ private function buildOptionValuesForInsert( ?float $upper_limit ): array { return [ - new Value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), - new Value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $position), - new Value(\ilDBConstants::T_TEXT, $text_value), - new Value(\ilDBConstants::T_FLOAT, $points), - new Value(\ilDBConstants::T_FLOAT, $lower_limit), - new Value( + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $text_value), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $lower_limit), + $persistence_factory->value( \ilDBConstants::T_FLOAT, $lower_limit !== $upper_limit ? $upper_limit @@ -178,20 +188,22 @@ private function buildOptionValuesForInsert( private function buildAnswerFormInsertStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, Uuid $answer_form_id, ScoringIdentical $scoring_identical, int $combinations_enabled ): Insert { - return new Insert( + return $persistence_factory->insert( $persistence->getColumns( + $persistence_factory, $table_name_builder, TableTypes::TypeSpecificAnswerForms ), [ - new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - new Value(\ilDBConstants::T_TEXT, $scoring_identical->value), - new Value(\ilDBConstants::T_INTEGER, $combinations_enabled) + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $scoring_identical->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $combinations_enabled) ] ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 48442e2f7f82..36759ff980e7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -25,11 +25,11 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\Environment; @@ -81,6 +81,7 @@ public function completeMigrationInsert( $answer_options_mapping[$db_row->gap_id] = []; $gaps_insert = $this->buildGapInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_mapping[$db_row->gap_id], @@ -103,6 +104,7 @@ public function completeMigrationInsert( $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer_option_id, @@ -131,6 +133,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), @@ -210,6 +213,7 @@ private function addCombinationInsertStatements( if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); $combinations_insert = $this->buildCombinationsInsert( + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $combinations_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), @@ -219,6 +223,7 @@ private function addCombinationInsertStatements( } $combinations_to_answer_options_insert = $this->buildCombinationsToAnswerOptionsInsert( + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $combinations_to_answer_options_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), @@ -233,6 +238,7 @@ private function addCombinationInsertStatements( } private function buildCombinationsInsert( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $combinations_insert, Uuid $combination_id, @@ -240,28 +246,30 @@ private function buildCombinationsInsert( float $points ): Insert { if ($combinations_insert === null) { - return new Insert( + return $persistence_factory->insert( $this->persistence->getColumns( + $persistence_factory, $table_name_builder, TableTypes::Additional, $this->persistence->getCombinationsTableIdentifier() ), [ - new Value(\ilDBConstants::T_TEXT, $combination_id), - new Value(\ilDBConstants::T_TEXT, $answer_form_id), - new Value(\ilDBConstants::T_FLOAT, $points), + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), ] ); } return $combinations_insert->withAdditionalValues([ - new Value(\ilDBConstants::T_TEXT, $combination_id), - new Value(\ilDBConstants::T_TEXT, $answer_form_id), - new Value(\ilDBConstants::T_FLOAT, $points), + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), ]); } private function buildCombinationsToAnswerOptionsInsert( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $combinations_to_answer_options_insert, Uuid $combination_id, @@ -270,26 +278,27 @@ private function buildCombinationsToAnswerOptionsInsert( InRange $in_range ): Insert { if ($combinations_to_answer_options_insert === null) { - return new Insert( + return $persistence_factory->insert( $this->persistence->getColumns( + $persistence_factory, $table_name_builder, TableTypes::Additional, $this->persistence->getCombinationToAnswerOptionsTableIdentifier() ), [ - new Value(\ilDBConstants::T_TEXT, $combination_id), - new Value(\ilDBConstants::T_TEXT, $gap_id), - new Value(\ilDBConstants::T_TEXT, $answer_option_id), - new Value(\ilDBConstants::T_TEXT, $in_range) + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) ] ); } return $combinations_to_answer_options_insert->withAdditionalValues([ - new Value(\ilDBConstants::T_TEXT, $combination_id), - new Value(\ilDBConstants::T_TEXT, $gap_id), - new Value(\ilDBConstants::T_TEXT, $answer_option_id), - new Value(\ilDBConstants::T_TEXT, $in_range) + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) ]); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index f88a3d445179..93a1e55cc85d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -75,6 +75,7 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_id, @@ -133,6 +134,7 @@ public function completeMigrationInsert( ) as $answer) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer['answer_option_id'], @@ -149,6 +151,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld($db_row->identical_scoring), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index f11a705ef52a..4fc25a6fbf31 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -77,6 +77,7 @@ public function completeMigrationInsert( return $migration_insert->withAdditionalInsert( $this->buildGapInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), null, $gap_id, @@ -92,6 +93,7 @@ public function completeMigrationInsert( )->withAdditionalInsert( $this->buildAnswerOptionInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), null, $migration_insert->getUuid(), @@ -105,6 +107,7 @@ public function completeMigrationInsert( )->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::ScoreAll, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index ab57e0f4d3c3..c2ba57c42977 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -77,6 +77,7 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $gap_id, @@ -97,6 +98,7 @@ public function completeMigrationInsert( foreach ($gaps as $gap_id) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $migration_insert->getUuid(), @@ -118,6 +120,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->persistence, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::OnlyScoreDistinct, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php index 3fa8bb631adf..3c3e6072cd9e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -23,11 +23,9 @@ use ILIAS\Questions\AnswerForm\Persistence as PersistenceInterface; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\Join; use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\Persistence\Order; -use ILIAS\Questions\Persistence\Select; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Questions\Persistence\TableNameSpaceCore; @@ -99,12 +97,17 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function getColumns( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '', array $columns_to_skip = [] ): array { - $table = $table_type->getTable($table_name_builder, $table_identifier); + $table = $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ); $column_identifiers = match($table_type) { TableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, TableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, @@ -115,7 +118,7 @@ public function getColumns( } }; return array_map( - fn(string $v): Column => new Column($table, $v), + fn(string $v): Column => $persistence_factory->column($table, $v), array_values( array_filter( $column_identifiers, @@ -127,20 +130,25 @@ public function getColumns( #[\Override] public function getIdColumn( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '' ): Column { if ($table_type === TableTypes::TypeSpecificAnswerForms) { - return new Column( - $table_type->getTable($table_name_builder), + return $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN ); } if ($table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { - return new Column( + return $persistence_factory->column( $table_type->getTable( + $persistence_factory, $table_name_builder, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ), @@ -148,33 +156,51 @@ public function getIdColumn( ); } - return new Column( - $table_type->getTable($table_name_builder, $table_identifier), + return $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ), self::ID_COLUMN ); } #[\Override] public function getForeignKeyColumn( + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, string $table_identifier = '' ): Column { return match($table_type) { - TableTypes::TypeSpecificAnswerForms => new Column( - $table_type->getTable($table_name_builder), + TableTypes::TypeSpecificAnswerForms => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), self::ANSWER_FORM_TABLE_ID_COLUMN ), - TableTypes::AnswerInputs => new Column( - $table_type->getTable($table_name_builder), + TableTypes::AnswerInputs => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN ), - TableTypes::AnswerOptions => new Column( - $table_type->getTable($table_name_builder), + TableTypes::AnswerOptions => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ), - TableTypes::Additional => new Column( - $table_type->getTable($table_name_builder, $table_identifier), + TableTypes::Additional => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ), $table_identifier === self::COMBINATION_TABLE_IDENTIFIER ? self::COMBINATION_TABLE_FOREIGN_KEY_COLUMN : self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN @@ -187,6 +213,7 @@ public function completeQuery( Query $query, Column $answer_form_id_column ): Query { + $persistence_factory = $query->getPersistenceFactory(); $table_name_builder = $query->getTableNameBuilder(Definition::class); $answer_form_specific_table_definition = TableTypes::TypeSpecificAnswerForms; @@ -195,77 +222,93 @@ public function completeQuery( $additional_table_definition = TableTypes::Additional; $combinations_id_column = $this->getIdColumn( + $persistence_factory, $table_name_builder, $additional_table_definition, self::COMBINATION_TABLE_IDENTIFIER ); return $query->withAdditionalJoin( - new Join( + $persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $answer_form_specific_table_definition ), JoinType::Left ) )->withAdditionalSelect( - new Select( - $this->getColumns($table_name_builder, $answer_form_specific_table_definition) + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $answer_form_specific_table_definition + ) ) )->withAdditionalJoin( - new Join( + $persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $answer_input_table_definition ), JoinType::Left ) )->withAdditionalSelect( - new Select( + $persistence_factory->select( $this->getColumns( + $persistence_factory, $table_name_builder, $answer_input_table_definition ) ) )->withAdditionalJoin( - new Join( + $persistence_factory->join( $this->getIdColumn( + $persistence_factory, $table_name_builder, $answer_input_table_definition ), $this->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $answer_options_table_definition ), JoinType::Left ) )->withAdditionalOrder( - new Order( + $persistence_factory->order( $this->getIdColumn( + $persistence_factory, $table_name_builder, $answer_input_table_definition ) ) )->withAdditionalSelect( - new Select( + $persistence_factory->select( $this->getColumns( + $persistence_factory, $table_name_builder, $answer_options_table_definition ) ) )->withAdditionalOrder( - new Order( - new Column( - $answer_options_table_definition->getTable($table_name_builder), + $persistence_factory->order( + $persistence_factory->column( + $answer_options_table_definition->getTable( + $persistence_factory, + $table_name_builder + ), 'position' ) ) )->withAdditionalJoin( - new Join( + $persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $additional_table_definition, self::COMBINATION_TABLE_IDENTIFIER @@ -273,17 +316,19 @@ public function completeQuery( JoinType::Left ) )->withAdditionalSelect( - new Select( + $persistence_factory->select( $this->getColumns( + $persistence_factory, $table_name_builder, $additional_table_definition, self::COMBINATION_TABLE_IDENTIFIER ) ) )->withAdditionalJoin( - new Join( + $persistence_factory->join( $combinations_id_column, $this->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $additional_table_definition, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER @@ -291,15 +336,16 @@ public function completeQuery( JoinType::Left ) )->withAdditionalSelect( - new Select( + $persistence_factory->select( $this->getColumns( + $persistence_factory, $table_name_builder, $additional_table_definition, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ) ) )->withAdditionalOrder( - new Order( + $persistence_factory->order( $combinations_id_column ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 8ba82d26f080..ed6a55d775bd 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -21,16 +21,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Replace; -use ILIAS\Questions\Persistence\Table; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Persistence\Where; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -111,14 +108,16 @@ public function toStorage( fn(Manipulate $c, MatchingValue $v): Manipulate => $c->withAdditionalStatement( $v->toStorage( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ), $manipulate->withAdditionalStatement( $this->buildReplace( - $answer_form_id, $persistence, - $table_name_builder + $manipulate->getPersistenceFactory(), + $table_name_builder, + $answer_form_id ) ) ); @@ -130,54 +129,65 @@ public function toDelete( Manipulate $manipulate ): Manipulate { return $manipulate->withAdditionalStatement( - $this->buildDelete($persistence, $table_name_builder) + $this->buildDelete( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) )->withAdditionalStatement( $this->buildDeleteForLinkedValues( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ); } private function buildReplace( - Uuid $answer_form_id, Persistence $persistence, - TableNameBuilder $table_name_builder + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + Uuid $answer_form_id ): Replace { $table_definition = TableTypes::Additional; - return new Replace( + return $persistence_factory->replace( $persistence->getColumns( + $persistence_factory, $table_name_builder, $table_definition, $persistence->getCombinationsTableIdentifier() ), [ - new Value(\ilDBConstants::T_TEXT, $this->id->toString()), - new Value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - new Value(\ilDBConstants::T_FLOAT, $this->available_points) + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->available_points) ] ); } private function buildDelete( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $table_definition = TableTypes::Additional; - return new Delete( - new Table( + return $persistence_factory->delete( + $persistence_factory->table( $table_definition, $table_name_builder, $persistence->getCombinationsTableIdentifier() ), [ - new Where( + $persistence_factory->where( $persistence->getIdColumn( $table_name_builder, $table_definition, $persistence->getCombinationsTableIdentifier() ), - new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) ) ] ); @@ -185,23 +195,27 @@ private function buildDelete( private function buildDeleteForLinkedValues( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $table_definition = TableTypes::Additional; - return new Delete( - new Table( + return $persistence_factory->delete( + $persistence_factory->table( $table_definition, $table_name_builder, $persistence->getCombinationToAnswerOptionsTableIdentifier() ), [ - new Where( + $persistence_factory->where( $persistence->getIdColumn( $table_name_builder, $table_definition, $persistence->getCombinationToAnswerOptionsTableIdentifier() ), - new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) ) ] ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index 609c3141ac2a..9add58b34293 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -189,6 +189,7 @@ private function retrieveCombinationsFromQuery( ): array { return $query->retrieveCurrentRecord( TableTypes::Additional->getTable( + $query->getPersistenceFactory(), $query->getTableNameBuilder( $type_generic_properties->getDefinition()::class ), @@ -238,6 +239,7 @@ private function retrieveMatchingValuesFromQuery( ): array { return $query->retrieveCurrentRecord( TableTypes::Additional->getTable( + $query->getPersistenceFactory(), $query->getTableNameBuilder( $type_generic_properties->getDefinition()::class ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index 6f7a97fbc520..50eb350fbece 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -23,10 +23,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; @@ -74,6 +74,7 @@ public function buildPresentationString( public function toStorage( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { if ($this->answer_option === null) { @@ -83,17 +84,30 @@ public function toStorage( } $table_definition = TableTypes::Additional; - return new Replace( + return $persistence_factory->replace( $persistence->getColumns( + $persistence_factory, $table_name_builder, $table_definition, $persistence->getCombinationToAnswerOptionsTableIdentifier() ), [ - new Value(\ilDBConstants::T_TEXT, $this->combination_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->gap->getAnswerInputId()->toString()), - new Value(\ilDBConstants::T_TEXT, $this->answer_option->getAnswerOptionId()->toString()), - new Value(\ilDBConstants::T_TEXT, $this->in_range?->value) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->combination_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->gap->getAnswerInputId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_option->getAnswerOptionId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->in_range?->value + ) ] ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 44dab3e82a0e..6e1f2610216d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -67,6 +67,7 @@ public function fromData( 'combinations_enabled' => $combinations_enabled ] = $query->retrieveCurrentRecord( TableTypes::TypeSpecificAnswerForms->getTable( + $query->getPersistenceFactory(), $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) ), $query->getRefinery()->custom()->transformation( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index ff0000cdc243..a59e4b4be113 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; -use ILIAS\Questions\Persistence\Value; use ILIAS\Data\UUID\Uuid; class AnswerOption @@ -138,32 +138,54 @@ public function buildArrayForHiddenInput(): array } public function buildReplace( + PersistenceFactory $persistence_factory, ?Replace $replace, array $columns ): Replace { if ($replace === null) { - return new Replace( + return $persistence_factory->replace( $columns, - $this->buildValuesForGapReplace() + $this->buildValuesForGapReplace($persistence_factory) ); } return $replace->withAdditionalValues( - $this->buildValuesForGapReplace() + $this->buildValuesForGapReplace($persistence_factory) ); } - private function buildValuesForGapReplace(): array - { + private function buildValuesForGapReplace( + PersistenceFactory $persistence_factory + ): array { return [ - new Value(\ilDBConstants::T_TEXT, $this->answer_option_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $this->position), - new Value(\ilDBConstants::T_TEXT, $this->text_value), - new Value(\ilDBConstants::T_FLOAT, $this->available_points), - new Value(\ilDBConstants::T_FLOAT, $this->lower_limit), - new Value(\ilDBConstants::T_FLOAT, $this->upper_limit) - + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_option_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_input_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->text_value + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->lower_limit + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->upper_limit + ) ]; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 00bf3b3b704a..06efbda84b08 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -21,11 +21,10 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; @@ -241,14 +240,19 @@ function (array $c, AnswerOption $v) use ($ff, $build_label): array { public function buildReplace( ?Replace $replace, Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { return array_reduce( $this->answer_options, fn(?Replace $c, AnswerOption $v): Replace => $v->buildReplace( + $persistence_factory, $c, - $persistence->getColumns($table_name_builder, TableTypes::AnswerOptions), - $table_name_builder + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::AnswerOptions + ) ), $replace ); @@ -256,19 +260,23 @@ public function buildReplace( public function buildDelete( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $answer_options_table_definition = TableTypes::AnswerOptions; - return new Delete( - $answer_options_table_definition->getTable($table_name_builder), + return $persistence_factory->delete( + $answer_options_table_definition->getTable( + $persistence_factory, + $table_name_builder + ), [ - new Where( + $persistence_factory->where( $persistence->getForeignKeyColumn( $table_name_builder, $answer_options_table_definition ), - new Value( + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_input_id->toString() ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php index fde7139f60e7..116107608504 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -80,7 +80,10 @@ public function fromDatabase( Query $query ): array { return $query->retrieveCurrentRecord( - TableTypes::AnswerOptions->getTable($query->getTableNameBuilder(Definition::class)), + TableTypes::AnswerOptions->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder(Definition::class) + ), $query->getRefinery()->custom()->transformation( function (array $vs): array { $previous_answer_input_id = null; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index b2b3b0cb9fab..077fc8ce9089 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -100,7 +100,10 @@ public function fromDatabase( $answer_options = $this->answer_options_factory->fromDatabase($query); return $query->retrieveCurrentRecord( - TableTypes::AnswerInputs->getTable($query->getTableNameBuilder(Definition::class)), + TableTypes::AnswerInputs->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder(Definition::class) + ), $query->getRefinery()->custom()->transformation( function (array $vs) use ($answer_form_id, $answer_options): Gaps { $previous_answer_input_id = null; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index b8ab11b85caa..82e9ccc18918 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -23,10 +23,10 @@ use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\Definitions\TextMatchingOptions; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; @@ -243,6 +243,7 @@ function (CarryWrapper $v) use ($refinery, $gaps_factory): self { public function buildReplace( ?Replace $replace, Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { if ($this->type === null) { @@ -254,14 +255,18 @@ public function buildReplace( $table_definition = TableTypes::AnswerInputs; if ($replace === null) { - return new Replace( - $persistence->getColumns($table_name_builder, $table_definition), - $this->buildValuesForGapReplace() + return $persistence_factory->replace( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $this->buildValuesForGapReplace($persistence_factory) ); } return $replace->withAdditionalValues( - $this->buildValuesForGapReplace() + $this->buildValuesForGapReplace($persistence_factory) ); } @@ -359,18 +364,19 @@ public function toTableRow( ); } - private function buildValuesForGapReplace(): array - { + private function buildValuesForGapReplace( + PersistenceFactory $persistence_factory + ): array { return [ - new Value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $this->position), - new Value(\ilDBConstants::T_TEXT, $this->type->getIdentifier()), - new Value(\ilDBConstants::T_INTEGER, $this->max_chars), - new Value(\ilDBConstants::T_FLOAT, $this->step_size), - new Value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), - new Value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), - new Value( + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->type->getIdentifier()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->max_chars), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->step_size), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), + $persistence_factory->value( \ilDBConstants::T_INTEGER, $this->shuffle_answer_options === null ? null diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 20c9305e66a0..6480a378bd31 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -21,13 +21,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Junctor; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Operator; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; @@ -357,11 +356,13 @@ public function toStorage( 'gaps' => $v->buildReplace( $c['gaps'], $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ), 'answer_options' => $v->getAnswerOptions()->buildReplace( $c['answer_options'], $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ], @@ -374,6 +375,7 @@ public function toStorage( return $manipulate->withAdditionalStatement( $this->buildDeleteForRemovedGaps( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) )->withAdditionalStatement($replace_for_gaps) @@ -390,12 +392,14 @@ public function toDelete( fn(Manipulate $c, Gap $v): Manipulate => $c->withAdditionalStatement( $v->getAnswerOptions()->buildDelete( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ), $manipulate->withAdditionalStatement( $this->buildDeleteForDeletionOfAnswerForm( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ) @@ -404,28 +408,34 @@ public function toDelete( private function buildDeleteForRemovedGaps( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $table_definition = TableTypes::AnswerInputs; - return new Delete( - $table_definition->getTable($table_name_builder), + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), [ - new Where( + $persistence_factory->where( $persistence->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $table_definition ), - new Value( + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ) ), - new Where( + $persistence_factory->where( $persistence->getIdColumn( + $persistence_factory, $table_name_builder, $table_definition ), - new Value( + $persistence_factory->value( \ilDBConstants::T_TEXT, array_map( fn(Gap $v): string => $v->getAnswerInputId()->toString(), @@ -442,19 +452,23 @@ private function buildDeleteForRemovedGaps( private function buildDeleteForDeletionOfAnswerForm( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $table_definition = TableTypes::AnswerInputs; - return new Delete( - $table_definition->getTable($table_name_builder), + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), [ - new Where( + $persistence_factory->where( $persistence->getForeignKeyColumn( $table_name_builder, $table_definition ), - new Value( + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 42f0d41c5a2a..4112004e6e5a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -32,14 +32,13 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; @@ -346,9 +345,11 @@ public function toStorage( $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create ? $this->buildInsertAnswerFormStatement( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) : $this->buildUpdateAnswerFormStatement( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ); @@ -381,6 +382,7 @@ public function toDelete( $manipulate->withAdditionalStatement( $this->buildDeleteAnswerFormStatement( $persistence, + $manipulate->getPersistenceFactory(), $table_name_builder ) ), @@ -391,18 +393,26 @@ public function toDelete( private function buildInsertAnswerFormStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Insert { $table_definition = TableTypes::TypeSpecificAnswerForms; - return new Insert( + return $persistence_factory->insert( $persistence->getColumns( + $persistence_factory, $table_name_builder, $table_definition ), [ - new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), - new Value( + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->scoring_identical->value + ), + $persistence_factory->value( \ilDBConstants::T_INTEGER, $this->combinations->areCombinationsEnabled() ? 1 : 0 ) @@ -412,30 +422,38 @@ private function buildInsertAnswerFormStatement( private function buildUpdateAnswerFormStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Update { $table_definition = TableTypes::TypeSpecificAnswerForms; - return new Update( + return $persistence_factory->update( $persistence->getColumns( + $persistence_factory, $table_name_builder, $table_definition, '', ['answer_form_id'] ), [ - new Value(\ilDBConstants::T_TEXT, $this->scoring_identical->value), - new Value( + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->scoring_identical->value + ), + $persistence_factory->value( \ilDBConstants::T_INTEGER, $this->combinations->areCombinationsEnabled() ? 1 : 0 ) ], [ - new Where( + $persistence_factory->where( $persistence->getIdColumn( $table_name_builder, $table_definition ), - new Value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) ) ] ); @@ -460,19 +478,23 @@ private function addReplaceCombinationsStatements( private function buildDeleteAnswerFormStatement( Persistence $persistence, + PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { $table_definition = TableTypes::TypeSpecificAnswerForms; - return new Delete( - $table_definition->getTable($table_name_builder), + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), [ - new Where( + $persistence_factory->where( $persistence->getForeignKeyColumn( $table_name_builder, $table_definition ), - new Value( + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ) diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php index ca083a2cc89c..d181857711ce 100644 --- a/components/ILIAS/Questions/src/Persistence/CoreTables.php +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -70,15 +70,17 @@ enum CoreTables: string case Linking = 'qsts_linking'; case MigrationsTable = 'qsts_migrations'; - public function getTable(): Table - { - return new Table($this); + public function getTable( + Factory $persistence_factory + ): Table { + return $persistence_factory->table($this); } public function getColumns( + Factory $persistence_factory, array $columns_to_skip = [] ): array { - $table = $this->getTable(); + $table = $this->getTable($persistence_factory); $column_identifiers = match($this) { self::Questions => self::QUESTION_TABLE_COLUMNS, self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, @@ -86,7 +88,7 @@ public function getColumns( self::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS }; return array_map( - fn(string $v): Column => new Column($table, $v), + fn(string $v): Column => $persistence_factory->column($table, $v), array_values( array_filter( $column_identifiers, @@ -96,41 +98,43 @@ public function getColumns( ); } - public function getIdColumn(): Column - { + public function getIdColumn( + Factory $persistence_factory + ): Column { return match($this) { - self::Questions => new Column( - $this->getTable(), + self::Questions => $persistence_factory->column( + $this->getTable($persistence_factory), self::QUESTION_TABLE_ID_COLUMN ), - self::AnswerForms => new Column( - $this->getTable(), + self::AnswerForms => $persistence_factory->column( + $this->getTable($persistence_factory), self::ANSWER_FORM_TABLE_ID_COLUMN ), - self::Linking => new Column( - $this->getTable(), + self::Linking => $persistence_factory->column( + $this->getTable($persistence_factory), self::LINKING_TABLE_ID_COLUMN ), - self::MigrationsTable => new Column( - $this->getTable(), + self::MigrationsTable => $persistence_factory->column( + $this->getTable($persistence_factory), self::MIGRATIONS_TABLE_ID_COLUMN ) }; } - public function getForeignKeyColumn(): ?Column - { + public function getForeignKeyColumn( + Factory $persistence_factory + ): ?Column { return match($this) { - self::AnswerForms => new Column( - $this->getTable(), + self::AnswerForms => $persistence_factory->column( + $this->getTable($persistence_factory), self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN ), - self::Linking => new Column( - $this->getTable(), + self::Linking => $persistence_factory->column( + $this->getTable($persistence_factory), self::LINKING_TABLE_FOREIGN_KEY_COLUMN ), - self::MigrationsTable => new Column( - $this->getTable(), + self::MigrationsTable => $persistence_factory->column( + $this->getTable($persistence_factory), self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN ), default => null diff --git a/components/ILIAS/Questions/src/Persistence/Factory.php b/components/ILIAS/Questions/src/Persistence/Factory.php new file mode 100644 index 000000000000..6ba8487d645f --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Factory.php @@ -0,0 +1,125 @@ + $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function insert( + array $columns, + array $values + ): Insert { + return new Insert($columns, $values); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function replace( + array $columns, + array $values + ): Replace { + return new Replace($columns, $values); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ + public function update( + array $columns, + array $values, + array $where + ): Update { + return new Update($columns, $values, $where); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ + public function delete( + Table $table, + array $where + ): Delete { + return new Delete($table, $where); + } + + public function table( + CoreTables|TableTypes $table_definition, + ?TableNameBuilder $table_name_builder = null, + string $table_identifier = '' + ): Table { + return new Table($table_definition, $table_name_builder, $table_identifier); + } + + public function column( + Table $table, + string $identifier + ): Column { + return new Column($table, $identifier); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + */ + public function select( + array $columns + ): Select { + return new Select($columns); + } + + public function join( + Column $left, + Column $right, + JoinType $type = JoinType::Inner + ): Join { + return new Join($left, $right, $type); + } + + public function where( + Column $left, + Value $right, + Operator $comparison = Operator::Equal, + Junctor $junctor = Junctor::Conjunction, + bool $negate = false + ): Where { + return new Where($left, $right, $comparison, $junctor, $negate); + } + + public function order( + Column $column, + OrderDirection $direction = OrderDirection::Asc + ): Order { + return new Order($column, $direction); + } + + public function value( + string $type, + null|string|int|float|array $value + ): Value { + return new Value($type, $value); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php index 4e1a0446b741..6ef4e4d941fd 100644 --- a/components/ILIAS/Questions/src/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -25,7 +25,7 @@ class Join public function __construct( private readonly Column $left, private readonly Column $right, - private readonly JoinType $type = JoinType::Inner + private readonly JoinType $type ) { } diff --git a/components/ILIAS/Questions/src/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php index baaf5340e61b..81757ebee3c8 100644 --- a/components/ILIAS/Questions/src/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -29,11 +29,17 @@ class Manipulate public function __construct( private readonly \ilDBInterface $db, + private readonly Factory $persistence_factory, private readonly AnswerFormFactory $answer_form_factory, private readonly ManipulationType $type ) { } + public function getPersistenceFactory(): Factory + { + return $this->persistence_factory; + } + public function getManipulationType(): ManipulationType { return $this->type; diff --git a/components/ILIAS/Questions/src/Persistence/Order.php b/components/ILIAS/Questions/src/Persistence/Order.php index f805003533ed..b7d73975448e 100644 --- a/components/ILIAS/Questions/src/Persistence/Order.php +++ b/components/ILIAS/Questions/src/Persistence/Order.php @@ -24,7 +24,7 @@ class Order { public function __construct( private readonly Column $column, - private readonly OrderDirection $direction = OrderDirection::Asc + private readonly OrderDirection $direction ) { } diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 9588187b8a40..214dc058735e 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -41,47 +41,69 @@ class Query public function __construct( private readonly \ilDBInterface $db, + private readonly Factory $persistence_factory, private readonly AnswerFormFactory $answer_form_factory, private readonly Refinery $refinery ) { $questions_linking_table_definition = CoreTables::Linking; $questions_table_definition = CoreTables::Questions; $answer_form_table_definition = CoreTables::AnswerForms; - $questions_id_column = $questions_table_definition->getIdColumn(); + $questions_id_column = $questions_table_definition->getIdColumn( + $this->persistence_factory + ); - $this->select[] = new Select( - $questions_linking_table_definition->getColumns() + $this->select[] = $this->persistence_factory->select( + $questions_linking_table_definition->getColumns( + $this->persistence_factory + ) ); - $this->select[] = new Select( - $questions_table_definition->getColumns() + $this->select[] = $this->persistence_factory->select( + $questions_table_definition->getColumns( + $this->persistence_factory + ) ); - $this->select[] = new Select( - $answer_form_table_definition->getColumns() + $this->select[] = $this->persistence_factory->select( + $answer_form_table_definition->getColumns( + $this->persistence_factory + ) ); - $this->joins[] = new Join( - $questions_linking_table_definition->getIdColumn(), - $questions_table_definition->getIdColumn(), + $this->joins[] = $this->persistence_factory->join( + $questions_linking_table_definition->getIdColumn( + $this->persistence_factory + ), + $questions_table_definition->getIdColumn( + $this->persistence_factory + ), JoinType::Inner ); - $this->joins[] = new Join( + $this->joins[] = $this->persistence_factory->join( $questions_id_column, - $answer_form_table_definition->getForeignKeyColumn(), + $answer_form_table_definition->getForeignKeyColumn( + $this->persistence_factory + ), JoinType::Left ); - $this->order[] = new Order( + $this->order[] = $this->persistence_factory->order( $questions_id_column ); - $this->order[] = new Order( - $answer_form_table_definition->getIdColumn() + $this->order[] = $this->persistence_factory->order( + $answer_form_table_definition->getIdColumn( + $this->persistence_factory + ) ); } + public function getPersistenceFactory(): Factory + { + return $this->persistence_factory; + } + public function getPersistenceForDefinitionClass( string $definition_class ): Persistence { @@ -148,7 +170,9 @@ public function withRange( public function loadNextRecord(): \Generator { - $alias = CoreTables::Questions->getIdColumn()->getColumnAlias(); + $alias = CoreTables::Questions->getIdColumn( + $this->persistence_factory + )->getColumnAlias(); $result = $this->toSql(); diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index 7bea0e758777..f395ab96b436 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -36,6 +36,7 @@ public function __construct( private readonly \ilDBInterface $db, private readonly Refinery $refinery, private readonly UuidFactory $uuid_factory, + private readonly Factory $persistence_factory, private readonly AnswerFormFactory $answer_form_factory ) { } @@ -56,6 +57,7 @@ public function getQuestionDataOnlyForAllQuestions(): \Generator { foreach ($query = new Query( $this->db, + $this->persistence_factory, $this->answer_form_factory, $this->refinery )->loadNextRecord() as $query_with_record) { @@ -74,12 +76,15 @@ public function getQuestionDataOnlyForQuestionIds( ): \Generator { foreach ((new Query( $this->db, + $this->persistence_factory, $this->answer_form_factory, $this->refinery )->withAdditionalWhere( - new Where( - CoreTables::Questions->getIdColumn(), - new Value( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( \ilDBConstants::T_TEXT, array_map( fn(Uuid $v): string => $v->toString(), @@ -102,12 +107,15 @@ public function getForQuestionId( return $this->getForBaseQuery( (new Query( $this->db, + $this->persistence_factory, $this->answer_form_factory, $this->refinery ))->withAdditionalWhere( - new Where( - CoreTables::Questions->getIdColumn(), - new Value( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( \ilDBConstants::T_TEXT, $question_id->toString() ), @@ -129,12 +137,15 @@ public function getForQuestionIds( yield from $this->getForBaseQuery( (new Query( $this->db, + $this->persistence_factory, $this->answer_form_factory, $this->refinery ))->withAdditionalWhere( - new Where( - CoreTables::Questions->getIdColumn(), - new Value( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( \ilDBConstants::T_TEXT, array_map( fn(Uuid $v): string => $v->toString(), @@ -162,6 +173,7 @@ public function create( ), new Manipulate( $this->db, + $this->persistence_factory, $this->answer_form_factory, ManipulationType::Create ) @@ -178,6 +190,7 @@ public function update( $questions, new Manipulate( $this->db, + $this->persistence_factory, $this->answer_form_factory, ManipulationType::Update ) @@ -192,6 +205,7 @@ public function delete( fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), new Manipulate( $this->db, + $this->persistence_factory, $this->answer_form_factory, ManipulationType::Delete ) @@ -214,7 +228,9 @@ private function getForBaseQuery( $this->getAnswerFormTypesForQuestionIds($question_ids), fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( $c, - CoreTables::AnswerForms->getIdColumn() + CoreTables::AnswerForms->getIdColumn( + $this->persistence_factory + ) ), $query ); @@ -235,12 +251,16 @@ private function retrieveQuestionFromQuery( array $answer_forms ): QuestionImplementation { $linking_info = $query->retrieveCurrentRecord( - CoreTables::Linking->getTable(), + CoreTables::Linking->getTable( + $query->getPersistenceFactory() + ), $this->refinery->identity() ); $question = $query->retrieveCurrentRecord( - CoreTables::Questions->getTable(), + CoreTables::Questions->getTable( + $query->getPersistenceFactory() + ), $this->refinery->custom()->transformation( fn(array $vs): QuestionImplementation => new QuestionImplementation( $this->uuid_factory->fromString($vs[0]['id']), @@ -275,7 +295,9 @@ private function retrieveAnswerFormsFromQuery( Query $query ): array { return $query->retrieveCurrentRecord( - CoreTables::AnswerForms->getTable(), + CoreTables::AnswerForms->getTable( + $query->getPersistenceFactory() + ), $this->refinery->custom()->transformation( function (array $vs) use ($query): array { if (count($vs) === 1 && $vs[0]['type'] === null) { diff --git a/components/ILIAS/Questions/src/Persistence/Table.php b/components/ILIAS/Questions/src/Persistence/Table.php index c9ff6f5cba6e..443c8b2885bd 100644 --- a/components/ILIAS/Questions/src/Persistence/Table.php +++ b/components/ILIAS/Questions/src/Persistence/Table.php @@ -24,8 +24,8 @@ class Table { public function __construct( private readonly CoreTables|TableTypes $table_definition, - private readonly ?TableNameBuilder $table_name_builder = null, - private readonly string $table_identifier = '' + private readonly ?TableNameBuilder $table_name_builder, + private readonly string $table_identifier ) { } diff --git a/components/ILIAS/Questions/src/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Persistence/TableTypes.php index a12804600aa5..7caed2f133de 100644 --- a/components/ILIAS/Questions/src/Persistence/TableTypes.php +++ b/components/ILIAS/Questions/src/Persistence/TableTypes.php @@ -29,16 +29,17 @@ enum TableTypes case Additional; public function getTable( + Factory $persistence_factory, TableNameBuilder $table_name_builder, string $table_identifier = '' ): Table { return match($this) { - self::Additional => new Table( + self::Additional => $persistence_factory->table( $this, $table_name_builder, $table_identifier ), - default => new Table( + default => $persistence_factory->table( $this, $table_name_builder ) diff --git a/components/ILIAS/Questions/src/Persistence/Where.php b/components/ILIAS/Questions/src/Persistence/Where.php index b57d202bdde3..c0714bb73224 100644 --- a/components/ILIAS/Questions/src/Persistence/Where.php +++ b/components/ILIAS/Questions/src/Persistence/Where.php @@ -25,9 +25,9 @@ class Where public function __construct( private readonly Column $left, private readonly Value $right, - private readonly Operator $comparison = Operator::Equal, - private readonly Junctor $junctor = Junctor::Conjunction, - private readonly bool $negate = false + private readonly Operator $comparison, + private readonly Junctor $junctor, + private readonly bool $negate ) { } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index f96c9d373c73..ae5dcfc2c1f1 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -49,7 +49,6 @@ use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; -use ILIAS\User\Settings\Settings as UserSettings; use ILIAS\Style\Content\Service as ContentStyle; use ILIAS\GlobalScreen\Services as GlobalScreen; diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 0532d6e491ab..72223f67f1e4 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -23,14 +23,12 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Persistence\CoreTables; -use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; -use ILIAS\Questions\Persistence\Value; -use ILIAS\Questions\Persistence\Where; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; @@ -336,11 +334,17 @@ public function toDelete( ): Manipulate { return $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate->withAdditionalStatement( - $this->buildDeleteQuestionStatement() + $this->buildDeleteQuestionStatement( + $manipulate->getPersistenceFactory() + ) )->withAdditionalStatement( - $this->buildDeleteLinkingStatement() + $this->buildDeleteLinkingStatement( + $manipulate->getPersistenceFactory() + ) )->withAdditionalStatement( - $this->buildDeleteMigrationStatement() + $this->buildDeleteMigrationStatement( + $manipulate->getPersistenceFactory() + ) ), $this->answer_forms ); @@ -352,9 +356,13 @@ private function addInsertStatementsToManipulation( if ($this->created === null) { $manipulate = $manipulate ->withAdditionalStatement( - $this->buildInsertLinkingStatement() + $this->buildInsertLinkingStatement( + $manipulate->getPersistenceFactory() + ) )->withAdditionalStatement( - $this->buildInsertQuestionStatement() + $this->buildInsertQuestionStatement( + $manipulate->getPersistenceFactory() + ) ); } @@ -387,13 +395,17 @@ private function addUpdateStatementsToManipulation( if ($this->self_updated) { $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdateQuestionStatement() + $this->buildUpdateQuestionStatement( + $manipulate->getPersistenceFactory() + ) ); } if ($this->page_id) { $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdatePageIdStatement() + $this->buildUpdatePageIdStatement( + $manipulate->getPersistenceFactory() + ) ); } @@ -438,91 +450,176 @@ private function addDeleteAnswerFormsStatementsToManipulate( - private function buildInsertLinkingStatement(): Insert - { - return new Insert( - CoreTables::Linking->getColumns(), + private function buildInsertLinkingStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::Linking->getColumns( + $persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $this->id->toString()), - new Value(\ilDBConstants::T_INTEGER, $this->parent_obj_id), - new Value(\ilDBConstants::T_INTEGER, $this->position) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) ] ); } - private function buildInsertQuestionStatement(): Insert - { - return new Insert( - CoreTables::Questions->getColumns(), + private function buildInsertQuestionStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::Questions->getColumns( + $persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $this->id->toString()), - new Value(\ilDBConstants::T_INTEGER, $this->page_id), - new Value(\ilDBConstants::T_TEXT, $this->title), - new Value(\ilDBConstants::T_TEXT, $this->author), - new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), - new Value(\ilDBConstants::T_TEXT, $this->remarks), - new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), - new Value(\ilDBConstants::T_INTEGER, time()), - new Value(\ilDBConstants::T_INTEGER, time()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) ] ); } - private function buildUpdateLinkingStatement(): Update - { + private function buildUpdateLinkingStatement( + PersistenceFactory $persistence_factory + ): Update { $linking_table_definition = CoreTables::Linking; - return new Update( + return $persistence_factory->update( $linking_table_definition->getColumns( + $persistence_factory, [CoreTables::LINKING_TABLE_ID_COLUMN] ), [ - new Value(\ilDBConstants::T_INTEGER, $this->parent_obj_id), - new Value(\ilDBConstants::T_INTEGER, $this->position) + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) ], [ - new Where( - $linking_table_definition->getIdColumn(), - new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + $persistence_factory->where( + $linking_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) ) ] ); } - private function buildUpdateQuestionStatement(): Update - { + private function buildUpdateQuestionStatement( + PersistenceFactory $persistence_factory + ): Update { $questions_table_definition = CoreTables::Questions; - return new Update( - $questions_table_definition->getColumns([ - CoreTables::ANSWER_FORM_TABLE_ID_COLUMN, - 'page_id', - 'created' - ]), + return $persistence_factory->update( + $questions_table_definition->getColumns( + $persistence_factory, + [ + CoreTables::ANSWER_FORM_TABLE_ID_COLUMN, + 'page_id', + 'created' + ] + ), [ - new Value(\ilDBConstants::T_TEXT, $this->title), - new Value(\ilDBConstants::T_TEXT, $this->author), - new Value(\ilDBConstants::T_TEXT, $this->lifecycle->value), - new Value(\ilDBConstants::T_TEXT, $this->remarks), - new Value(\ilDBConstants::T_TEXT, $this->original_id?->toString()), - new Value(\ilDBConstants::T_INTEGER, time()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) ], [ - new Where( - $questions_table_definition->getIdColumn(), - new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + $persistence_factory->where( + $questions_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) ) ] ); } - private function buildDeleteQuestionStatement(): Delete - { + private function buildDeleteQuestionStatement( + PersistenceFactory $persistence_factory + ): Delete { $table_definition = CoreTables::Questions; - return new Delete( - $table_definition->getTable(), + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), [ - new Where( - $table_definition->getIdColumn(), - new Value( + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->id->toString() ) @@ -531,15 +628,18 @@ private function buildDeleteQuestionStatement(): Delete ); } - private function buildDeleteLinkingStatement(): Delete - { + private function buildDeleteLinkingStatement( + PersistenceFactory $persistence_factory + ): Delete { $table_definition = CoreTables::Linking; - return new Delete( - $table_definition->getTable(), + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), [ - new Where( - $table_definition->getIdColumn(), - new Value( + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->id->toString() ) @@ -549,18 +649,21 @@ private function buildDeleteLinkingStatement(): Delete } /** - * skergomard, 2026-01-86: This we only need while the migrations exist, after + * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after * this MUST go! */ - private function buildDeleteMigrationStatement(): Delete - { + private function buildDeleteMigrationStatement( + PersistenceFactory $persistence_factory + ): Delete { $table_definition = CoreTables::MigrationsTable; - return new Delete( - $table_definition->getTable(), + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), [ - new Where( - $table_definition->getIdColumn(), - new Value( + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->id->toString() ) @@ -570,31 +673,43 @@ private function buildDeleteMigrationStatement(): Delete } /** - * skergomard, 2026-01-26: This we only need while the migrations exist, after + * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after * this a question MUST never change the page assigned to it after its creation! */ - private function buildUpdatePageIdStatement(): Update - { + private function buildUpdatePageIdStatement( + PersistenceFactory $persistence_factory + ): Update { $questions_table_definition = CoreTables::Questions; - return new Update( + return $persistence_factory->update( [ - new Column( - $questions_table_definition->getTable(), + $persistence_factory->column( + $questions_table_definition->getTable($persistence_factory), 'page_id' ), - new Column( - $questions_table_definition->getTable(), + $persistence_factory->column( + $questions_table_definition->getTable($persistence_factory), 'last_update' ) ], [ - new Value(\ilDBConstants::T_TEXT, $this->page_id), - new Value(\ilDBConstants::T_INTEGER, time()) + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) ], [ - new Where( - $questions_table_definition->getIdColumn(), - new Value(\ilDBConstants::T_TEXT, $this->id->toString()) + $persistence_factory->where( + $questions_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) ) ] ); diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index a8708db82f2e..19f96fb48107 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Setup; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpaceCore; use ILIAS\Refinery\Transformation; @@ -36,6 +37,7 @@ class Agent implements SetupAgent use HasNoNamedObjective; public function __construct( + private readonly PersistenceFactory $persistence_factory, private readonly array $answer_form_migrations ) { } @@ -101,6 +103,7 @@ public function getMigrations(): array { return [ new QuestionsMigration( + $this->persistence_factory, $this->answer_form_migrations ) ]; diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index a02fcb248534..6f9e9a9265ee 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -24,9 +24,9 @@ use ILIAS\Questions\AnswerForm\Migration\MigrationInsert as AnswerFormMigrationInsert; use ILIAS\Questions\Persistence\CoreTables; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\Value; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup; @@ -50,6 +50,7 @@ class QuestionsMigration implements Migration private ?array $allready_migrated_questions_in_qpls = null; public function __construct( + private readonly PersistenceFactory $persistence_factory, array $answer_form_migrations ) { $this->answer_form_migrations = array_reduce( @@ -295,12 +296,23 @@ private function buildInsertLinkingStatement( int $obj_id, ?int $position ): Insert { - return new Insert( - CoreTables::Linking->getColumns(), + return $this->persistence_factory->insert( + CoreTables::Linking->getColumns( + $this->persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $new_question_id->toString()), - new Value(\ilDBConstants::T_INTEGER, $obj_id), - new Value(\ilDBConstants::T_INTEGER, $position) + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $new_question_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $obj_id + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $position + ) ] ); } @@ -314,18 +326,47 @@ private function buildInsertQuestionStatement( ?Uuid $original_id, int $create_date ): Insert { - return new Insert( - CoreTables::Questions->getColumns(), + return $this->persistence_factory->insert( + CoreTables::Questions->getColumns( + $this->persistence_factory + ), [ - new Value(\ilDBConstants::T_TEXT, $id->toString()), - new Value(\ilDBConstants::T_INTEGER, 0), - new Value(\ilDBConstants::T_TEXT, $title), - new Value(\ilDBConstants::T_TEXT, $author), - new Value(\ilDBConstants::T_TEXT, $lifecycle->value), - new Value(\ilDBConstants::T_TEXT, $remarks), - new Value(\ilDBConstants::T_TEXT, $original_id?->toString()), - new Value(\ilDBConstants::T_INTEGER, time()), - new Value(\ilDBConstants::T_INTEGER, $create_date) + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + 0 + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $title + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $author + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $lifecycle->value + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $remarks + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $original_id?->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $create_date + ) ] ); } @@ -334,12 +375,20 @@ private function buildInsertMigrationStatement( int $old_question_id, ?Uuid $new_question_id ): Insert { - return new Insert( - CoreTables::MigrationsTable->getColumns(), + return $this->persistence_factory->insert( + CoreTables::MigrationsTable->getColumns( + $this->persistence_factory + ), [ - new Value(\ilDBConstants::T_INTEGER, $old_question_id), - new Value(\ilDBConstants::T_TEXT, $new_question_id?->toString()), - new Value( + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $old_question_id + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $new_question_id?->toString() + ), + $this->persistence_factory->value( \ilDBConstants::T_INTEGER, $new_question_id === null ? '0' @@ -359,6 +408,7 @@ private function buildMigrationInsert( $this->db, $this->io, $this->uuid_factory, + $this->persistence_factory, new TableNameBuilder( $answer_form_migration->getTableNameSpace() ), From a7a626046bf1bc18741cd48b4682f69e1b74854b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 10 Feb 2026 12:10:11 +0100 Subject: [PATCH 060/108] Questions: Move to New Mechanism For Carry --- .../Cloze/Layout/CombinationsOverview.php | 50 ++++++++------ .../Cloze/Layout/OverviewTable.php | 11 +++- .../Cloze/Properties/Combinations/Factory.php | 52 +++++---------- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 32 ++++----- .../Presentation/Definitions/Environment.php | 8 +-- .../Definitions/EnvironmentImplementation.php | 20 +++--- .../src/Presentation/Layout/EditForm.php | 65 +++++++++---------- .../src/Presentation/Layout/Factory.php | 13 ++-- .../src/Presentation/Layout/InputsBuilder.php | 39 ++++++----- .../Questions/src/Presentation/Views/Edit.php | 6 +- .../Questions/src/Question/Views/Edit.php | 26 ++++---- 11 files changed, 157 insertions(+), 165 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index 14c4240512c6..e509f2007f27 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -147,12 +147,16 @@ private function getActions(): array return [ $af->single( $this->lng->txt('edit'), - $this->environment->getUrlBuilderWithStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES), + $this->environment + ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) + ->getUrlBuilder(), $this->environment->getTableRowIdToken() )->withAsync(true), $af->single( $this->lng->txt('delete'), - $this->environment->getUrlBuilderWithStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION), + $this->environment + ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) + ->getUrlBuilder(), $this->environment->getTableRowIdToken() )->withAsync(true) ]; @@ -217,7 +221,8 @@ private function buildSetCombinationGapsModal(): RoundTripModal ) ], $this->environment - ->getUrlBuilderWithStepParameter(self::STEP_SET_COMBINATION_VALUES) + ->withStepParameter(self::STEP_SET_COMBINATION_VALUES) + ->getUrlBuilder() ->buildURI() ->__toString() )->withSubmitLabel($this->lng->txt('next')); @@ -265,7 +270,8 @@ private function buildSetCombinationValuesModal( ], $inputs_builder->addCarryToEnvironment( $this->environment - )->getUrlBuilderWithStepParameter(self::STEP_SAVE) + )->withStepParameter(self::STEP_SAVE) + ->getUrlBuilder() ->buildURI() ->__toString() ); @@ -291,9 +297,10 @@ private function confirmDeleteCombination( return $this->ui_factory->modal()->interruptive( $this->lng->txt('confirm'), $this->lng->txt('delete_combination'), - $this->environment->getUrlBuilderWithStepParameter( + $this->environment->withStepParameter( self::STEP_DELETE_COMBINATION - )->withParameter( + )->getUrlBuilder() + ->withParameter( $this->environment->getTableRowIdToken(), [$affected_item->getId()->toString()] )->buildURI()->__toString() @@ -320,20 +327,25 @@ private function deleteCombination(): Properties private function buildInputsBuilder( ?Combination $combination, ): InputsBuilder { - $properties = $this->environment->getAnswerFormProperties(); $inputs_builder = $this->environment->getPresentationFactory()->getInputsBuilder( - $this->combinations_factory->getToCombinationTransformation( - $this->ui_factory->input()->field(), - $this->refinery, - $this->lng, - $properties - ), - $combination?->buildPointsInputs( - $this->ui_factory->input()->field(), - $this->refinery, - $this->lng, - $this->combinations_factory, - $properties + $this->refinery->custom()->transformation( + function (?string $v) use ($combination): ?Group { + $properties = $this->environment->getAnswerFormProperties(); + if ($combination === null) { + $combination = $this->combinations_factory + ->buildCombinationFromCarryValue( + $v, + $properties + ); + } + + return $combination?->buildPointsInputs( + $this->ui_factory->input()->field(), + $this->refinery, + $this->lng, + $properties + ); + } ) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index 039947f97b54..b64fa3ec6779 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -91,17 +91,22 @@ private function getActions(): array return [ 'edit_gaps' => $this->table_factory->action()->standard( $this->lng->txt('edit_gaps'), - $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_GAP_TYPES), + $this->environment + ->withStepParameter(Edit::STEP_JUMP_TO_SET_GAP_TYPES) + ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_answer_options' => $this->table_factory->action()->standard( $this->lng->txt('edit_answer_options'), - $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS), + $this->environment + ->withStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS), $this->environment->getTableRowIdToken() ), 'edit_points' => $this->table_factory->action()->standard( $this->lng->txt('edit_available_points'), - $this->environment->getUrlBuilderWithStepParameter(Edit::STEP_JUMP_TO_SET_POINTS), + $this->environment + ->withStepParameter(Edit::STEP_JUMP_TO_SET_POINTS) + ->getUrlBuilder(), $this->environment->getTableRowIdToken() ) ]; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index 9add58b34293..5eba4b2ba5e5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -143,43 +143,27 @@ function (array $c, string $v) use ( ); } - public function getToCombinationTransformation( - FieldFactory $field_factory, - Refinery $refinery, - Language $lng, + public function buildCombinationFromCarryValue( + string $carry, Properties $properties - ): CustomTransformation { - return $refinery->custom()->transformation( - function (string $v) use ( - $field_factory, - $refinery, - $lng, - $properties - ): Group { - $values_array = json_decode($v, true); - $combination_id = $this->uuid_factory->fromString( - array_key_first($values_array) - ); - return new Combination( + ): Combination { + $values_array = json_decode($carry, true); + $combination_id = $this->uuid_factory->fromString( + array_key_first($values_array) + ); + + return new Combination( + $combination_id, + null, + array_map( + fn(string $v): MatchingValue => new MatchingValue( $combination_id, - null, - array_map( - fn(string $v): MatchingValue => new MatchingValue( - $combination_id, - $properties->getGaps()->getGapById( - $this->uuid_factory->fromString($v) - ) - ), - $values_array[$combination_id->toString()] + $properties->getGaps()->getGapById( + $this->uuid_factory->fromString($v) ) - )->buildPointsInputs( - $field_factory, - $refinery, - $lng, - $this, - $properties - ); - } + ), + $values_array[$combination_id->toString()] + ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 5bbd49213efc..b4a7617b0e99 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -96,7 +96,8 @@ public function edit( if ($step === '') { return $environment->getPresentationFactory()->getEditOverview( $environment, - $environment->getUrlBuilderWithStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + $environment->withStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + ->getUrlBuilder() ->buildURI() ); } @@ -190,9 +191,7 @@ private function buildBasicEditingForm( $answer_form_properties = $environment->getAnswerFormProperties(); $editing_form = $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::STEP_SET_GAP_TYPES) - ->buildURI(), + $environment->withStepParameter(self::STEP_SET_GAP_TYPES), $answer_form_properties->buildBasicEditingInputs( $this->lng, $this->ui_factory->input()->field(), @@ -208,9 +207,10 @@ private function buildBasicEditingForm( && $answer_form_properties->getLegacyClozeText() !== '' && $answer_form_properties->getClozeText()->getRawRepresentationForPersistence() === '') { return $editing_form->withInsertLegacyTextsButton( - $environment->getUrlBuilderWithStepParameter( + $environment->withStepParameter( self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES - )->buildURI() + )->getUrlBuilder() + ->buildURI() ); } @@ -269,9 +269,7 @@ private function buildGapTypesForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::STEP_SET_ANSWER_OPTIONS) - ->buildURI(), + $environment->withStepParameter(self::STEP_SET_ANSWER_OPTIONS), $properties->getGaps()->buildGapsTypeInputs( $this->lng, $ff, @@ -279,9 +277,7 @@ private function buildGapTypesForm( $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), $environment->getTableRowIds() ), - false, - $properties->withClozeText($properties->getClozeText()) - ->buildCarryInputs($ff) + false )->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( $this->ui_factory, @@ -315,9 +311,7 @@ private function buildAnswerOptionsForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::STEP_SET_POINTS) - ->buildURI(), + $environment->withStepParameter(self::STEP_SET_POINTS), $properties->getGaps()->buildAnswerOptionsInputs( $this->lng, $ff, @@ -359,9 +353,7 @@ private function buildAssignPointsForm( $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::STEP_SAVE) - ->buildURI(), + $environment->withStepParameter(self::STEP_SAVE), $properties->getGaps()->buildPointInputs( $this->lng, $ff, @@ -410,9 +402,9 @@ private function buildRemovedGapsConfirmation( return $this->ui_factory->modal()->interruptive( $this->lng->txt('confirm'), $this->lng->txt('confirm_remove_gaps'), - $environment->getUrlBuilderWithStepParameter( + $environment->withStepParameter( self::STEP_CONFIRMED_GAP_REMOVAL - )->buildURI()->__toString() + )->getUrlBuilder()->buildURI()->__toString() )->withAffectedItems( array_map( fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 0ef27b1057ce..900612a6949a 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -42,10 +42,6 @@ public function getPresentationFactory(): Factory; public function getUrlBuilder(): URLBuilder; - public function getUrlBuilderWithStepParameter( - string $step - ): URLBuilder; - public function getTableRowIdToken(): URLBuilderToken; public function getTableRowIds(): array; @@ -62,5 +58,9 @@ public function withAnswerFormProperties( Properties $properties ): self; + public function withStepParameter( + string $step + ): self; + public function withPreservedTableRowIdsParameter(): self; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index acb7da1230ca..d33ec5be9737 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -96,11 +96,10 @@ public function addEditAnswerFormSubTab( $this->tabs_gui->addSubTab( $step, $this->lng->txt($language_variable), - $this->getUrlBuilderWithStepParameter($step) - ->withParameter( - $this->action_token, - Edit::CMD_OTHER_ANSWER_FORM - )->buildURI() + $this->withStepParameter($step) + ->withActionParameter(Edit::ACTION_OTHER_ANSWER_FORM) + ->getUrlBuilder() + ->buildURI() ->__toString() ); } @@ -125,10 +124,13 @@ public function getUrlBuilder(): URLBuilder } #[\Override] - public function getUrlBuilderWithStepParameter( + public function withStepParameter( string $step - ): URLBuilder { - return $this->getUrlBuilder()->withParameter($this->step_token, $step); + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->step_token, $step); + return $clone; } #[\Override] @@ -344,7 +346,7 @@ public function getTypeClassHash(): string public function getCarry( Transformation $to_form_transformation - ): Input|array|string|null { + ): Input|array|null { [, $carry_token] = $this->url_builder->acquireParameter( self::QUERY_PARAMETER_NAME_SPACE, self::TOKEN_STRING_CARRY_ID diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index acf632d20f97..0b718c1d13cf 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -20,13 +20,12 @@ namespace ILIAS\Questions\Presentation\Layout; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\URI; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; -use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; @@ -35,7 +34,6 @@ class EditForm implements Renderable { private const string MAIN_SECTION_NAME = 'form'; - public const string CARRY_SECTION_NAME = 'carry'; private StandardForm $form; @@ -47,12 +45,22 @@ class EditForm implements Renderable public function __construct( private readonly UIFactory $ui_factory, private readonly Language $lng, - private readonly URI $form_target_uri, - private readonly Section $main_section_inputs, - private readonly bool $is_final_step, - private readonly ?Group $carry_inputs + Environment $environment, + InputsBuilder $inputs_builder, + bool $is_final_step ) { - $this->form = $this->buildForm(); + $this->form = $this->buildForm( + $environment, + $inputs_builder, + $is_final_step + ); + } + + #[\Override] + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); } public function withContentBeforeForm( @@ -94,12 +102,6 @@ public function withInsertLegacyTextsButton( return $clone; } - public function render( - UIRenderer $ui_renderer - ): string { - return $ui_renderer->render($this->buildContent()); - } - public function withRequest( ServerRequestInterface $request ): self { @@ -149,31 +151,28 @@ function ($id) { return $content; } - private function buildForm(): StandardForm - { + private function buildForm( + Environment $environment, + InputsBuilder $inputs_builder, + bool $is_final_step + ): StandardForm { $form = $this->ui_factory->input()->container()->form()->standard( - $this->form_target_uri->__toString(), - $this->buildFormInputs() + $inputs_builder + ->addCarryToEnvironment($environment) + ->getUrlBuilder() + ->buildURI() + ->__toString(), + [ + self::MAIN_SECTION_NAME => $inputs_builder->getInputs( + $environment + ) + ] ); - if ($this->is_final_step) { + if ($is_final_step) { return $form->withSubmitLabel($this->lng->txt('save')); } return $form->withSubmitLabel($this->lng->txt('next')); } - - private function buildFormInputs(): array - { - $form_inputs = [ - self::MAIN_SECTION_NAME => $this->main_section_inputs - ]; - - if ($this->carry_inputs !== null) { - $form_inputs[self::CARRY_SECTION_NAME] = $this->carry_inputs - ->withDedicatedName(self::CARRY_SECTION_NAME); - } - - return $form_inputs; - } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index 8dc459e4caef..cbabfcb51db3 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -30,7 +30,6 @@ use ILIAS\Refinery\Custom\Transformation as CustomTransformation; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\URLBuilder; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Input\Input; @@ -62,11 +61,11 @@ public function getEditOverview( } /** - * @param URLBuilder $url_builder The url_builder MUST have the step set, + * @param Environment $environment The environment MUST have the step set, * to which the form shall be sent. */ public function getEditForm( - URI $form_target_uri, + Environment $environment, Section $main_section_inputs, bool $is_final_step, ?Group $carry_inputs = null @@ -74,7 +73,7 @@ public function getEditForm( return new EditForm( $this->ui_factory, $this->lng, - $form_target_uri, + $environment, $main_section_inputs, $is_final_step, $carry_inputs @@ -98,13 +97,11 @@ public function getAsync( * carry is present and you want to use them directly. */ public function getInputsBuilder( - CustomTransformation $to_inputs, - Input|array|null $inputs = null + CustomTransformation $to_inputs ): InputsBuilder { return new InputsBuilder( $this->refinery, - $to_inputs, - $inputs + $to_inputs ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php index 6089cf295a80..3d11c950b940 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php @@ -36,17 +36,21 @@ class InputsBuilder { private ?string $carry = null; + /** + * @param CustomTransformation $to_inputs This Transformation receives the + * value set by `self::withCarry()` if it is available, otherwise the value + * will be null. + */ public function __construct( private readonly Refinery $refinery, - private readonly CustomTransformation $to_inputs, - private Input|array|null $inputs + private readonly CustomTransformation $to_inputs ) { } /** * @param string $carry The `string` will be `base64-encoded` before - * adding it to the `Query`. The string whill be passed to the `$to_inputs` - * to allow the recreation of the inputs. + * adding it to the `Query`. The string will be passed to the `$to_inputs` + * transformation to allow the recreation of the inputs. */ public function withCarry( string $carry @@ -66,23 +70,22 @@ public function addCarryToEnvironment( ); } + /** + * @return \ILIAS\UI\Component\Input\Input|array|null The return value will + * be null, if no inputs could be built. This is a sign that something is + * wrong with carry. This might mean that there is a mistake in the carry + * that was set, but it can also be the consequence of somebody messing + * with the query. + */ public function getInputs( EnvironmentImplementation $environment - ): Input|array { - if ($this->inputs === null) { - $this->buildInputs($environment); - } - - return $this->inputs; - } - - private function buildInputs( - EnvironmentImplementation $environment - ): void { - $this->inputs = $environment->getCarry( + ): Input|array|null { + return $environment->getCarry( $this->refinery->custom()->transformation( - fn(string $v): Input|array => $this->to_inputs->transform( - base64_decode($v) + fn(?string $v): Input|array|null => $this->to_inputs->transform( + $v === null + ? null + : base64_decode($v) ) ) ); diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index ae5dcfc2c1f1..2aa339784cae 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -353,9 +353,9 @@ private function deleteQuestions( $this->lng->txt('qpl_confirm_delete_questions'), $environment->withActionParameter( self::ACTION_DELETE_QUESTIONS - )->getUrlBuilderWithStepParameter( + )->withStepParameter( self::ACTION_DELETE_QUESTIONS - )->buildURI()->__toString() + )->getUrlBuilder()->buildURI()->__toString() )->withAffectedItems( $this->buildAffectedItems($question_ids) ) @@ -545,7 +545,7 @@ private function buildCreateAnswerForm( } return $environment->getPresentationFactory()->getEditForm( - $environment->getUrlBuilder()->buildURI(), + $environment, $if->field()->section( $inputs + [ 'type' => $if->field()->select( diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 066579e2fd70..f51e86f1b11e 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -107,14 +107,12 @@ private function buildBasicPropertiesCreateForm( } return $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION) - ->buildURI(), + $environment->withStepParameter(self::CMD_SAVE_QUESTION), $ff->section( $inputs, $this->lng->txt('edit_basic_form_properties') ), - true + false ); } @@ -138,16 +136,16 @@ private function buildBasicPropertiesEditingForm( EnvironmentImplementation $environment ): EditForm { return $environment->getPresentationFactory()->getEditForm( - $environment - ->getUrlBuilderWithStepParameter(self::CMD_SAVE_QUESTION) - ->buildURI(), - $this->ui_factory->input()->field()->section( - $this->buildBasicPropertiesInputs(), - $this->lng->txt('edit_basic_form_properties') - )->withAdditionalTransformation( - $this->buildAddBasicPropertiesToQuestionTrafo() - )->withValue( - $this->buildBasicPropertiesBasicValuesArray() + $environment->withStepParameter(self::CMD_SAVE_QUESTION), + $environment->getPresentationFactory()->getInputsBuilder( + $this->ui_factory->input()->field()->section( + $this->buildBasicPropertiesInputs(), + $this->lng->txt('edit_basic_form_properties') + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() + ) ), true ); From 3d5619007c83009fd3d1d4ed3b3f949803a298ef Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 11 Feb 2026 10:14:57 +0100 Subject: [PATCH 061/108] Questions: Fix Typo --- .../class.ilObjQuestionsGUI.php | 2 +- .../Presentation/Definitions/CarryWrapper.php | 61 ------------------- .../src/Presentation/Definitions/Leaf.php | 34 ----------- 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php delete mode 100755 components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index ffe88d1e6855..b5df38d3c6fb 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -41,7 +41,7 @@ class ilObjQuestionsGUI extends ilObjectGUI private readonly UnitsRepository $units_repository; private readonly Edit $edit_view; - private readonly ConfigurationRepository $configurations_repository; + private readonly ConfigurationRepository $configuration_repository; private DataFactory $data_factory; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php b/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php deleted file mode 100755 index 3733be691737..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/CarryWrapper.php +++ /dev/null @@ -1,61 +0,0 @@ -transform( - $this->retrieveValueFromArray($key) - ); - } - - private function retrieveValueFromArray( - string $key - ): mixed { - $value = $this->raw_values[$key] ?? null; - - if ($value === null) { - return null; - } - - if ($value instanceof Leaf) { - return $value->get(); - } - - return new self($value); - } -} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php b/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php deleted file mode 100755 index 55e80b2bc9a3..000000000000 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Leaf.php +++ /dev/null @@ -1,34 +0,0 @@ -value; - } -} From 152770c4472a5bd7cad8ee0bbe1c7f4415b377e0 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 11 Feb 2026 10:45:15 +0100 Subject: [PATCH 062/108] Questions: Implement New Carry Mechanism --- .../Cloze/Properties/ClozeText/Text.php | 2 +- .../Gaps/AnswerOptions/AnswerOptions.php | 28 +++++++ .../Cloze/Properties/Gaps/Gap.php | 69 +++++++++-------- .../Cloze/Properties/Gaps/Gaps.php | 37 ++++++++- .../Cloze/Properties/Properties.php | 76 ++++++++++++------- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 72 +++++++++--------- .../src/Presentation/Layout/Factory.php | 20 ----- 7 files changed, 188 insertions(+), 116 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 17b414a3abfe..2d842846e9f0 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -73,7 +73,7 @@ public function getCarryInputs( return $ff->hidden()->withValue($this->getTextForOutputInHiddenInput()); } - public function getRawRepresentationForPersistence(): string + public function getRawRepresentation(): string { return $this->cloze_text->getRawRepresentation(); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 06efbda84b08..90c1b6bafad8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -206,6 +206,34 @@ function (array $c, AnswerOption $v): array { ); } + public function buildQueryCarry(): array + { + return array_map( + fn(AnswerOption $v) => $v->getAnswerOptionId()->toString(), + $this->answer_options + ); + } + + public function withValuesFromQueryCarry( + array $carry + ): self { + $clone = clone $this; + $clone->answer_options = array_reduce( + $carry, + function (array $c, string $v): array { + $c[$v] = $this->factory->buildAnswerOption( + $v, + $this->answer_input_id, + 0, + '' + ); + return $c; + }, + [] + ); + return $clone; + } + public function buildHiddenInputValue(): string { return json_encode([ diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 82e9ccc18918..ea961413dc83 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -42,13 +42,13 @@ class Gap { public const string GAP_PLACEHOLDER_NAME = 'GAP'; - private const string FORM_KEY_TYPE = 'type'; - private const string FORM_KEY_MAX_CHARS = 'max_chars'; - private const string FORM_KEY_STEP_SIZE = 'step_size'; - private const string FORM_KEY_TEXT_MATCHING_METHOD = 'matching_method'; - private const string FORM_KEY_MIN_AUTOCOMPLETE = 'min_autocomplete'; - private const string FORM_KEY_SHUFFLE_ANSWER_OPTIONS = 'shuffle'; - private const string FORM_KEY_ANSWER_OPTIONS = 'answer_options'; + private const string KEY_TYPE = 'type'; + private const string KEY_MAX_CHARS = 'max_chars'; + private const string KEY_STEP_SIZE = 'step_size'; + private const string KEY_TEXT_MATCHING_METHOD = 'matching_method'; + private const string KEY_MIN_AUTOCOMPLETE = 'min_autocomplete'; + private const string KEY_SHUFFLE_ANSWER_OPTIONS = 'shuffle'; + private const string KEY_ANSWER_OPTIONS = 'answer_options'; /** * @param array $answer_options @@ -181,42 +181,51 @@ public function withAnswerOptions( return $clone; } + public function getQueryCarry(): array + { + return [ + self::KEY_TYPE => $this->type?->getIdentifier() ?? '', + self::KEY_ANSWER_OPTIONS => $this->answer_options->buildQueryCarry() + ]; + } + + public function withValuesFromQueryCarry( + Factory $gap_factory, + array $carry + ): self { + $clone = clone $this; + $clone->type = $gap_factory->getGapTypeByIdentifier($carry[self::KEY_TYPE]); + $clone->answer_options = $clone->answer_options->withValuesFromQueryCarry( + $carry[self::KEY_ANSWER_OPTIONS] + ); + } + public function getCarryInputs( FieldFactory $ff ): Group { $inputs = []; - if ($this->type !== null) { - $inputs[self::FORM_KEY_TYPE] = $ff->hidden()->withValue($this->type?->getIdentifier() ?? '') - ->withDedicatedName(self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId()); - } if ($this->max_chars !== null) { - $inputs[self::FORM_KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()) - ->withDedicatedName(self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()); } if ($this->step_size !== null) { - $inputs[self::FORM_KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()) - ->withDedicatedName(self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()); } if ($this->text_matching_method !== null) { - $inputs[self::FORM_KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value) - ->withDedicatedName(self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value); } if ($this->min_autocomplete !== null) { - $inputs[self::FORM_KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()) - ->withDedicatedName(self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()); } if ($this->shuffle_answer_options !== null) { - $inputs[self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0') - ->withDedicatedName(self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0'); } - $inputs[self::FORM_KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()) - ->withDedicatedName(self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId()); + $inputs[self::KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()); return $ff->group($inputs); } @@ -392,7 +401,7 @@ private function retrieveTypeFromCarry( array $available_gap_types ): ?Type { return $carry->retrieve( - self::FORM_KEY_TYPE . $this->getShortenedAnswerInputId(), + self::KEY_TYPE . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( fn(?string $v): ?Type => $available_gap_types[$v] ?? $this->getType() ) @@ -404,7 +413,7 @@ private function retrieveMaxCharsFromCarry( CarryWrapper $carry ): ?int { return $carry->retrieve( - self::FORM_KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), + self::KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->int(), $refinery->always($this->getMaxChars()) @@ -417,7 +426,7 @@ private function retrieveStepSizeFromCarry( CarryWrapper $carry ): ?float { return $carry->retrieve( - self::FORM_KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), + self::KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->float(), $refinery->always($this->getStepSize()) @@ -430,7 +439,7 @@ private function retrieveTextMatchingMethodFromCarry( CarryWrapper $carry ): ?TextMatchingOptions { return $carry->retrieve( - self::FORM_KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), + self::KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( fn(?string $v): ?TextMatchingOptions => $v !== null ? TextMatchingOptions::tryFrom($v) @@ -444,7 +453,7 @@ private function retrieveMinAutocompleteFromCarry( CarryWrapper $carry ): ?int { return $carry->retrieve( - self::FORM_KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), + self::KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->int(), $refinery->always($this->getMinAutocomplete()) @@ -457,7 +466,7 @@ private function retrieveShuffleAnswerOptionsFromCarry( CarryWrapper $carry ): ?bool { return $carry->retrieve( - self::FORM_KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + self::KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), $refinery->byTrying([ $refinery->kindlyTo()->bool(), $refinery->always($this->getShuffleAnswerOptions()) @@ -470,7 +479,7 @@ private function retrieveAnswerOptionsFromCarry( CarryWrapper $carry ): AnswerOptions { return $carry->retrieve( - self::FORM_KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), + self::KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), $refinery->custom()->transformation( fn(?string $v): AnswerOptions => $this->answer_options ->withValuesFromHiddenInputValue($v) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 6480a378bd31..217453596bcf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -212,6 +212,7 @@ public function buildAnswerOptionsInputs( Language $lng, FieldFactory $ff, Refinery $refinery, + ?string $carry, array $selected_gaps ): Section { return $ff->section( @@ -289,6 +290,18 @@ function (array $c, Gap $v): array { ); } + public function getQueryCarry(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->getQueryCarry(); + return $c; + }, + [] + ); + } + public function getCarryInputs( FieldFactory $ff ): Group { @@ -296,8 +309,7 @@ public function getCarryInputs( array_reduce( $this->gaps, function (array $c, Gap $v) use ($ff): array { - $c[$v->getAnswerInputId()->toString()] = $v->getCarryInputs($ff) - ->withDedicatedName($v->getAnswerInputId()->toString()); + $c[$v->getAnswerInputId()->toString()] = $v->getCarryInputs($ff); return $c; }, [] @@ -305,6 +317,27 @@ function (array $c, Gap $v) use ($ff): array { ); } + public function withValuesFromQueryCarry( + Factory $gaps_factory, + array $carry + ): self { + $clone = clone $this; + foreach ($carry as $answer_input_id => $gap_definition) { + if (!isset($clone->gaps[$answer_input_id])) { + $clone->gaps[$answer_input_id] = $gaps_factory->getNewGap( + $this->answer_form_id, + 0, + $answer_input_id + ); + } + + $clone->gaps[$answer_input_id] = $clone->gaps[$answer_input_id] + ->withValuesFromQueryCarry($this->factory, $gap_definition); + } + + return $clone; + } + public function getFromCarryTransformation( Refinery $refinery, Factory $gaps_factory diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 4112004e6e5a..0ece42581068 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -40,7 +40,6 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -53,11 +52,11 @@ class Properties implements PropertiesInterface { - private const string FORM_KEY_ID = 'form_id'; - private const string FORM_KEY_CLOZE_TEXT = 'cloze_text'; - private const string FORM_KEY_IDENTICAL_SCORING = 'identical_scoring'; - private const string FORM_KEY_ENABLE_COMBINATIONS = 'enable_combinations'; - private const string FORM_KEY_GAPS_TO_EDIT = 'gaps'; + private const string KEY_ID = 'form_id'; + private const string KEY_CLOZE_TEXT = 'cloze_text'; + private const string KEY_IDENTICAL_SCORING = 'identical_scoring'; + private const string KEY_ENABLE_COMBINATIONS = 'enable_combinations'; + private const string KEY_GAPS_TO_EDIT = 'gaps'; private bool $updated_combinations = false; @@ -104,7 +103,7 @@ public function getTypeGenericProperties(): TypeGenericProperties null, null, null, - $this->cloze_text->getRawRepresentationForPersistence(), + $this->cloze_text->getRawRepresentation(), $this->legacy_cloze_text ); } @@ -129,7 +128,7 @@ public function getLegacyClozeText(): string public function getClozeTextForPresentation(): string { - return $this->cloze_text->getRawRepresentationForPersistence() === '' + return $this->cloze_text->getRawRepresentation() === '' ? \ilRTE::_replaceMediaObjectImageSrc($this->legacy_cloze_text, 0) : $this->cloze_text->getRenderedMarkdownForParticipantPresentation(); } @@ -229,14 +228,14 @@ public function buildBasicEditingInputs( return $ff->section( [ - self::FORM_KEY_CLOZE_TEXT => $cloze_text_input, - self::FORM_KEY_IDENTICAL_SCORING => ScoringIdentical::buildInput( + self::KEY_CLOZE_TEXT => $cloze_text_input, + self::KEY_IDENTICAL_SCORING => ScoringIdentical::buildInput( $lng, $ff, $refinery, $this->scoring_identical )->withValue($this->getScoringOfIdenticalResponses()->value), - self::FORM_KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) + self::KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) ->withValue($this->combinations->areCombinationsEnabled()) ], $lng->txt('create_answer_form') @@ -244,36 +243,55 @@ public function buildBasicEditingInputs( $refinery->custom()->transformation( fn(array $vs): self => $propteries_factory->fromForm( $this, - $vs[self::FORM_KEY_CLOZE_TEXT], - $vs[self::FORM_KEY_IDENTICAL_SCORING], - $vs[self::FORM_KEY_ENABLE_COMBINATIONS] + $vs[self::KEY_CLOZE_TEXT], + $vs[self::KEY_IDENTICAL_SCORING], + $vs[self::KEY_ENABLE_COMBINATIONS] ) ) ); } + public function buildQueryCarry(): string + { + return json_encode($this->gaps->getQueryCarry()); + } + + public function withValuesFromQueryCarry( + ClozeTextFactory $cloze_text_factory, + ?string $carry + ): self { + if ($carry === null + || !is_array( + ($carry_array = json_decode($carry)) + )) { + return $this; + } + + $clone = clone $this; + $clone->gaps = $this->getGaps()->withValuesFromQueryCarry( + $cloze_text_factory, + $carry_array + ); + return $clone; + } + public function buildCarryInputs( FieldFactory $ff ): Group { return $ff->group( [ - self::FORM_KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()) - ->withDedicatedName(self::FORM_KEY_ID), - self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getCarryInputs($ff) - ->withDedicatedName(self::FORM_KEY_CLOZE_TEXT), - self::FORM_KEY_GAPS_TO_EDIT => $this->gaps->getCarryInputs($ff) - ->withDedicatedName(self::FORM_KEY_GAPS_TO_EDIT), - self::FORM_KEY_IDENTICAL_SCORING => $ff->hidden() - ->withDedicatedName(self::FORM_KEY_IDENTICAL_SCORING) + self::KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()), + self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getCarryInputs($ff), + self::KEY_GAPS_TO_EDIT => $this->gaps->getCarryInputs($ff), + self::KEY_IDENTICAL_SCORING => $ff->hidden() ->withValue($this->getScoringOfIdenticalResponses()->value), - self::FORM_KEY_ENABLE_COMBINATIONS => $ff->hidden() - ->withDedicatedName(self::FORM_KEY_ENABLE_COMBINATIONS) + self::KEY_ENABLE_COMBINATIONS => $ff->hidden() ->withValue($this->combinations->areCombinationsEnabled() ? 1 : 0) ] ); } - public function withValuesFromCarry( + public function withValuesFromInputs( Refinery $refinery, ClozeTextFactory $cloze_text_factory, GapsFactory $gaps_factory, @@ -281,7 +299,7 @@ public function withValuesFromCarry( ): Properties { $clone = clone $this; $clone->cloze_text = $carry->retrieve( - self::FORM_KEY_CLOZE_TEXT, + self::KEY_CLOZE_TEXT, $refinery->byTrying([ $refinery->custom()->transformation( fn(?string $v): Text => $v === null @@ -293,7 +311,7 @@ public function withValuesFromCarry( ); $clone->scoring_identical = $carry->retrieve( - self::FORM_KEY_IDENTICAL_SCORING, + self::KEY_IDENTICAL_SCORING, $refinery->byTrying([ $refinery->custom()->transformation( fn(?string $v): ScoringIdentical => $v === null @@ -305,7 +323,7 @@ public function withValuesFromCarry( ); $clone->combinations = $carry->retrieve( - self::FORM_KEY_ENABLE_COMBINATIONS, + self::KEY_ENABLE_COMBINATIONS, $refinery->byTrying([ $refinery->custom()->transformation( fn($v): Combinations => $clone->combinations->withCombinationsEnabled( @@ -317,7 +335,7 @@ public function withValuesFromCarry( ); $clone->gaps = $carry->retrieve( - self::FORM_KEY_GAPS_TO_EDIT, + self::KEY_GAPS_TO_EDIT, $clone->cloze_text->updateGapsFromMarkdown( $this->getAnswerFormId(), $this->getGaps() diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index b4a7617b0e99..7fe3308d2ce2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -35,6 +35,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; use ILIAS\UI\URLBuilder; @@ -141,21 +142,6 @@ private function callIntermediateStep( ): EditForm|Properties { $initialized_environment = $environment->withPreservedTableRowIdsParameter(); - if ($step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS - && $step !== self::STEP_JUMP_TO_SET_ANSWER_OPTIONS - && $step !== self::STEP_JUMP_TO_SET_POINTS) { - $initialized_environment = $initialized_environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withValuesFromCarry( - $this->refinery, - $this->cloze_text_factory, - $this->gap_factory, - $environment->getPresentationFactory()->getCarrySectionData( - $this->http->wrapper()->post() - ) - ) - ); - } - return match ($step) { self::STEP_SET_GAP_TYPES, self::STEP_CONFIRMED_GAP_REMOVAL => $this->processBasicEditingForm( @@ -192,20 +178,24 @@ private function buildBasicEditingForm( $editing_form = $environment->getPresentationFactory()->getEditForm( $environment->withStepParameter(self::STEP_SET_GAP_TYPES), - $answer_form_properties->buildBasicEditingInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->refinery, - $this->properties_factory, - $this->cloze_text_factory, - $add_legacy_cloze_text_to_input + $environment->getPresentationFactory()->getInputsBuilder( + $this->refinery->custom()->transformation( + fn(null $carry) => $answer_form_properties->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory, + $add_legacy_cloze_text_to_input + ) + ) ), false ); if (!$add_legacy_cloze_text_to_input && $answer_form_properties->getLegacyClozeText() !== '' - && $answer_form_properties->getClozeText()->getRawRepresentationForPersistence() === '') { + && $answer_form_properties->getClozeText()->getRawRepresentation() === '') { return $editing_form->withInsertLegacyTextsButton( $environment->withStepParameter( self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES @@ -266,16 +256,26 @@ private function processBasicEditingForm( private function buildGapTypesForm( Environment $environment ): EditForm { + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ $properties = $environment->getAnswerFormProperties(); $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( $environment->withStepParameter(self::STEP_SET_ANSWER_OPTIONS), - $properties->getGaps()->buildGapsTypeInputs( - $this->lng, - $ff, - $this->refinery, - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), - $environment->getTableRowIds() + $environment->getPresentationFactory()->getInputsBuilder( + $this->refinery->custom()->transformation( + fn(?string $carry) => $properties + ->withValuesFromQueryCarry($carry) + ->getGaps() + ->buildGapsTypeInputs( + $this->lng, + $ff, + $this->refinery, + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $environment->getTableRowIds() + ) + ) + )->withCarry( + $properties->buildQueryCarry() ), false )->withContentBeforeForm( @@ -312,11 +312,15 @@ private function buildAnswerOptionsForm( $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( $environment->withStepParameter(self::STEP_SET_POINTS), - $properties->getGaps()->buildAnswerOptionsInputs( - $this->lng, - $ff, - $this->refinery, - $environment->getTableRowIds() + $this->refinery->custom()->transformation( + fn(?string $carry): Section => $properties->getGaps() + ->buildAnswerOptionsInputs( + $this->lng, + $ff, + $this->refinery, + $carry, + $environment->getTableRowIds() + ) ), false, $properties->buildCarryInputs($ff) diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index cbabfcb51db3..b6848cfbcfe2 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -104,24 +104,4 @@ public function getInputsBuilder( $to_inputs ); } - - public function getCarrySectionData( - ArrayBasedRequestWrapper $post_wrapper - ): CarryWrapper { - return new CarryWrapper( - array_reduce( - $post_wrapper->keys(), - function (array $c, string $v) use ($post_wrapper): array { - $value = new Leaf( - $post_wrapper->retrieve($v, $this->refinery->identity()) - ); - foreach (array_reverse(explode('/', $v)) as $path_element) { - $value = [$path_element => $value]; - } - return array_merge_recursive($c, $value); - }, - [] - )['form'][EditForm::CARRY_SECTION_NAME] ?? [] - ); - } } From e2588a9d39b617049bf33598221abcd138d00c64 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 11 Feb 2026 17:58:30 +0100 Subject: [PATCH 063/108] Questions: Fix Input on LegacyClozeText --- .../Cloze/Layout/OverviewTable.php | 3 ++- .../Cloze/Properties/ClozeText/Text.php | 8 +++--- .../Cloze/Properties/Factory.php | 25 +++++++++++++------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index b64fa3ec6779..7f28cefc1fad 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -99,7 +99,8 @@ private function getActions(): array 'edit_answer_options' => $this->table_factory->action()->standard( $this->lng->txt('edit_answer_options'), $this->environment - ->withStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS), + ->withStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS) + ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_points' => $this->table_factory->action()->standard( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 2d842846e9f0..dddc27c0d086 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -47,7 +47,8 @@ public function __construct( public function getInput( Language $lng, FieldFactory $ff, - Factory $cloze_text_factory + Factory $cloze_text_factory, + bool $is_required ): MarkdownInput { return $ff->markdown( new \ilUIMarkdownPreviewGUI(), @@ -60,10 +61,11 @@ public function getInput( ) )->withAdditionalTransformation( $this->refinery->custom()->constraint( - fn(self $v): bool => $v->hasAtLeastOneGap(), + fn(self $v): bool => !$is_required && $v->getRawRepresentation() === '' + || $v->hasAtLeastOneGap(), $lng->txt('no_gaps') ) - )->withRequired(true) + )->withRequired($is_required) ->withValue($this->cloze_text->getRawRepresentation()); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 6e1f2610216d..b8f890786c84 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -114,21 +114,30 @@ public function fromForm( ScoringIdentical $scoring_of_identical_responses, bool $combinations_enabled ): Properties { + $updated_properties = $properties + ->withScoringOfIdenticalResponses($scoring_of_identical_responses) + ->withCombinations( + $properties->getCombinations()->withCombinationsEnabled( + $combinations_enabled + ) + ); + + if ($updated_properties->getLegacyClozeText() !== '' + && $cloze_text->getRawRepresentation() === '') { + return $updated_properties; + } + $updated_gaps = $cloze_text->updateGapsFromMarkdown( $properties->getAnswerFormId(), $properties->getGaps() ); - return $properties + return $updated_properties ->withClozeText( - $cloze_text->withIdsOfNewGapsInClozeText($updated_gaps->getUndefinedGaps()) - )->withGaps($updated_gaps) - ->withScoringOfIdenticalResponses($scoring_of_identical_responses) - ->withCombinations( - $properties->getCombinations()->withCombinationsEnabled( - $combinations_enabled + $cloze_text->withIdsOfNewGapsInClozeText( + $updated_gaps->getUndefinedGaps() ) - ); + )->withGaps($updated_gaps); } private function retrieveFirstMatchingRowFromDBRecords( From 0d1f58aa5efb4047070c41d4cfba9aa9913f0371 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 11 Feb 2026 17:59:16 +0100 Subject: [PATCH 064/108] Questions: Fixes in Combinations --- .../AnswerFormTypes/Cloze/Layout/CombinationsOverview.php | 4 +++- .../Cloze/Properties/Combinations/Combination.php | 5 +++-- .../Cloze/Properties/Combinations/Combinations.php | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index e509f2007f27..f86e827a1f74 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -33,6 +33,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Table\Data as DataTable; @@ -329,7 +330,7 @@ private function buildInputsBuilder( ): InputsBuilder { $inputs_builder = $this->environment->getPresentationFactory()->getInputsBuilder( $this->refinery->custom()->transformation( - function (?string $v) use ($combination): ?Group { + function (?string $v) use ($combination): ?Section { $properties = $this->environment->getAnswerFormProperties(); if ($combination === null) { $combination = $this->combinations_factory @@ -343,6 +344,7 @@ function (?string $v) use ($combination): ?Group { $this->ui_factory->input()->field(), $this->refinery, $this->lng, + $this->combinations_factory, $properties ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index ed6a55d775bd..f66bdfc00fba 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -33,6 +33,7 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Group; +use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -252,7 +253,7 @@ public function buildPointsInputs( Language $lng, Factory $combinations_factory, Properties $properties - ): Group { + ): Section { return $field_factory->section( [ 'values' => $this->buildValuesInputs( @@ -311,7 +312,7 @@ private function buildValuesInputs( function (array $c, MatchingValue $v) use ($field_factory, $properties): array { $gap_id = $v->getGap()->getAnswerInputId(); $gap = $properties->getGaps()->getGapById($gap_id); - $c[$gap_id] = $field_factory->select( + $c[$gap_id->toString()] = $field_factory->select( $gap->buildShortenedGapName(), $gap->getType()->getCombinationsSelectValues($gap) )->withRequired(true) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index 90c6540c8a3e..e3efa80e6f82 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -95,8 +95,8 @@ public function hasMatchingCombinationForAnswerOptionIds( if ($combination->containsAnswerOptionsExactly($vs)) { return true; } - return false; } + return false; } public function getEditView( From 7e58d2802f7d66893a8de94ff0a2a9cd61c1b856 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 12 Feb 2026 15:58:06 +0100 Subject: [PATCH 065/108] Questions: Fix Typo in Variable Name --- .../Questions/Legacy/Administration/class.ilObjQuestionsGUI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index b5df38d3c6fb..96fc466fafea 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -57,7 +57,7 @@ public function __construct( $local_dic = LocalDIC::dic(); $this->units_repository = $local_dic[UnitsRepository::class]; $this->edit_view = $local_dic[Edit::class]; - $this->configurations_repository = $local_dic[ConfigurationRepository::class]; + $this->configuration_repository = $local_dic[ConfigurationRepository::class]; $this->type = 'qsts'; From 84daed4ab90f5e14607aa4dbde23a3c447a6e45b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 14:33:09 +0100 Subject: [PATCH 066/108] Questions: Move To InputsBuilderSession --- .../ILIAS/Questions/Legacy/LocalDIC.php | 13 +- .../Questions/src/AnswerForm/Factory.php | 5 +- .../Cloze/Layout/CombinationsOverview.php | 37 +- .../Cloze/Layout/OverviewTable.php | 18 +- .../Cloze/Properties/ClozeText/Factory.php | 12 - .../Cloze/Properties/ClozeText/Text.php | 15 - .../Properties/Combinations/Combination.php | 2 + .../Cloze/Properties/Factory.php | 21 +- .../Gaps/AnswerOptions/AnswerOption.php | 4 +- .../Gaps/AnswerOptions/AnswerOptions.php | 117 ++-- .../Cloze/Properties/Gaps/Factory.php | 4 + .../Cloze/Properties/Gaps/Gap.php | 189 ++---- .../Cloze/Properties/Gaps/Gaps.php | 186 +++--- .../Cloze/Properties/Properties.php | 123 +--- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 311 ++++------ .../AnswerFormTypes/Cloze/Views/EditGaps.php | 550 ++++++++++++++++++ .../Presentation/Definitions/Environment.php | 11 + .../Definitions/EnvironmentImplementation.php | 136 +++-- .../src/Presentation/Layout/EditForm.php | 50 +- .../src/Presentation/Layout/EditOverview.php | 8 +- .../src/Presentation/Layout/Factory.php | 47 +- .../src/Presentation/Layout/InputsBuilder.php | 71 +-- .../Layout/InputsBuilderSession.php | 89 +++ .../Questions/src/Presentation/Views/Edit.php | 19 +- .../Questions/src/Question/Views/Edit.php | 26 +- 25 files changed, 1237 insertions(+), 827 deletions(-) create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 8a2fc5f108cf..02e23996a501 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -83,7 +83,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[LayoutFactory::class] = static fn($c): LayoutFactory => new LayoutFactory( $DIC['ui.factory'], - $DIC['refinery'], $DIC['http'], $DIC['lng'] ); @@ -120,6 +119,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Properties\Gaps\Factory::class] = static fn($c): Cloze\Properties\Gaps\Factory => new Cloze\Properties\Gaps\Factory( + $DIC['refinery'], $c[UuidFactory::class], $c[Cloze\Properties\Gaps\AnswerOptions\Factory::class], [ @@ -156,6 +156,15 @@ protected static function buildDIC(ILIASContainer $DIC): self => new Cloze\Persistence( new TableNameSpaceCore('cloze') ); + $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps + => new Cloze\Views\EditGaps( + $DIC['lng'], + $DIC['ui.factory'], + $DIC['refinery'], + $DIC['http'], + $c[Cloze\Properties\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class] + ); $dic[Cloze\Views\Edit::class] = static fn($c): Cloze\Views\Edit => new Cloze\Views\Edit( $DIC['lng'], @@ -165,7 +174,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['http'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\ClozeText\Factory::class], - $c[Cloze\Properties\Gaps\Factory::class] + $c[Cloze\Views\EditGaps::class] ); $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant => new Cloze\Views\Participant( diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index b951d2bc6fa1..b31721d78266 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -101,10 +101,11 @@ public function buildTypeDefinitionFromSelectValue( public function getDefaultTypeGenericProperties( Uuid $question_id, - Definition $type + Definition $type, + ?Uuid $answer_form_id = null ): TypeGenericProperties { return new TypeGenericProperties( - $this->uuid_factory->uuid4(), + $answer_form_id ?? $this->uuid_factory->uuid4(), $question_id, $type ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index f86e827a1f74..eb9d6b9b3f82 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -26,6 +26,7 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\InputsBuilder; +use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Data\Range; use ILIAS\Data\Order; @@ -177,7 +178,9 @@ private function buildAction(): Async return $this->environment->getPresentationFactory()->getAsync( match ($this->environment->getStep()) { self::STEP_JUMP_TO_SET_COMBINATION_VALUES => - $this->buildSetCombinationValuesModal($affected_item), + $this->buildSetCombinationValuesModal( + $this->buildInputsBuilder($affected_item) + ), self::STEP_CONFIRM_DELETE_COMBINATION => $this->confirmDeleteCombination($affected_item) } @@ -242,22 +245,19 @@ private function processSetCombinationGapsModal(): self return $clone; } - $set_values_modal = $clone->buildSetCombinationValuesModal($data['combination']); + $set_values_modal = $clone->buildSetCombinationValuesModal( + $this->buildInputsBuilder($data['combination']) + ); $clone->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); return $clone; } private function buildSetCombinationValuesModal( - ?Combination $combination = null + InputsBuilder $inputs_builder ): RoundTripModal { $properties = $this->environment->getAnswerFormProperties(); $gaps = $properties->getGaps(); - $inputs_builder = $this->buildInputsBuilder($combination); - $inputs = $inputs_builder->getInputs( - $this->environment - ); - return $this->ui_factory->modal()->roundtrip( $this->lng->txt('edit'), $properties->getClozeText()->buildPanelForEditing( @@ -267,11 +267,9 @@ private function buildSetCombinationValuesModal( $properties->getLegacyClozeText() ), [ - 'values_awarding_points' => $inputs + 'values_awarding_points' => $inputs_builder->getInputs() ], - $inputs_builder->addCarryToEnvironment( - $this->environment - )->withStepParameter(self::STEP_SAVE) + $this->environment->withStepParameter(self::STEP_SAVE) ->getUrlBuilder() ->buildURI() ->__toString() @@ -280,12 +278,14 @@ private function buildSetCombinationValuesModal( private function processSetCombinationValues(): self|Properties { - $set_values_modal = $this->buildSetCombinationValuesModal() + $inputs_builder = $this->buildInputsBuilder(null); + $set_values_modal = $this->buildSetCombinationValuesModal($inputs_builder) ->withRequest($this->http->request()); $data = $set_values_modal->getData(); if ($data === null) { $this->modal = $this->initializeModal($set_values_modal) ->withOnLoad($set_values_modal->getShowSignal()); + $inputs_builder->persistCarry(); return $this; } @@ -327,8 +327,9 @@ private function deleteCombination(): Properties private function buildInputsBuilder( ?Combination $combination, - ): InputsBuilder { - $inputs_builder = $this->environment->getPresentationFactory()->getInputsBuilder( + ): InputsBuilderSession { + $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( + $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), $this->refinery->custom()->transformation( function (?string $v) use ($combination): ?Section { $properties = $this->environment->getAnswerFormProperties(); @@ -352,9 +353,11 @@ function (?string $v) use ($combination): ?Section { ); if ($combination === null) { - return $inputs_builder; + return $builder; } - return $inputs_builder->withCarry($combination->buildCarryString()); + $builder_with_string = $builder->withCarry($combination->buildCarryString()); + $builder_with_string->persistCarry(); + return $builder_with_string; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index 7f28cefc1fad..e9b259ab5dbe 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Layout; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; +use ILIAS\Questions\AnswerFormTypes\Cloze\Views\EditGaps; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\Range; use ILIAS\Data\Order; @@ -79,10 +79,14 @@ private function getColums(): array $f = $this->table_factory->column(); return [ - 'gap' => $f->text($this->lng->txt('title')), - 'type' => $f->text($this->lng->txt('question_type')), - 'answers_options_awarding_points' => $f->text($this->lng->txt('answer_options_awarding_points')), + 'gap' => $f->text($this->lng->txt('title'))->withIsSortable(false), + 'type' => $f->text($this->lng->txt('cloze_type'))->withIsSortable(false), + 'answers_options_awarding_points' => $f + ->text($this->lng->txt('answer_options_awarding_points')) + ->withIsSortable(false), 'available_points' => $f->number($this->lng->txt('available_points')) + ->withDecimals(4) + ->withIsSortable(false) ]; } @@ -92,21 +96,21 @@ private function getActions(): array 'edit_gaps' => $this->table_factory->action()->standard( $this->lng->txt('edit_gaps'), $this->environment - ->withStepParameter(Edit::STEP_JUMP_TO_SET_GAP_TYPES) + ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_GAP_TYPES) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_answer_options' => $this->table_factory->action()->standard( $this->lng->txt('edit_answer_options'), $this->environment - ->withStepParameter(Edit::STEP_JUMP_TO_SET_ANSWER_OPTIONS) + ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_ANSWER_OPTIONS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_points' => $this->table_factory->action()->standard( $this->lng->txt('edit_available_points'), $this->environment - ->withStepParameter(Edit::STEP_JUMP_TO_SET_POINTS) + ->withStepParameter(EditGaps::STEP_JUMP_TO_ASSIGN_POINTS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php index 55e2ac30a4c6..b24c6ab3de23 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php @@ -43,16 +43,4 @@ public function buildFromTextString( $this->text_factory->markdown($text) ); } - - public function buildFromHiddenInputString( - string $text - ): Text { - return $this->buildFromTextString($this->unmaskTextFromOutputInHiddenInput($text)); - } - - private function unmaskTextFromOutputInHiddenInput( - string $text - ): string { - return str_replace(['\{\{', '\}\}'], ['{{', '}}'], $text); - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index dddc27c0d086..b1eba9f89ad4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -69,12 +69,6 @@ public function getInput( ->withValue($this->cloze_text->getRawRepresentation()); } - public function getCarryInputs( - FieldFactory $ff - ): HiddenInput { - return $ff->hidden()->withValue($this->getTextForOutputInHiddenInput()); - } - public function getRawRepresentation(): string { return $this->cloze_text->getRawRepresentation(); @@ -196,13 +190,4 @@ private function hasAtLeastOneGap(): bool return false; } - - private function getTextForOutputInHiddenInput(): string - { - return str_replace( - ['{{', '}}'], - ['\{\{', '\}\}'], - $this->cloze_text->getRawRepresentation() - ); - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index f66bdfc00fba..8f0f87d4076d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -181,6 +181,7 @@ private function buildDelete( [ $persistence_factory->where( $persistence->getIdColumn( + $persistence_factory, $table_name_builder, $table_definition, $persistence->getCombinationsTableIdentifier() @@ -209,6 +210,7 @@ private function buildDeleteForLinkedValues( [ $persistence_factory->where( $persistence->getIdColumn( + $persistence_factory, $table_name_builder, $table_definition, $persistence->getCombinationToAnswerOptionsTableIdentifier() diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index b8f890786c84..c5112d5622ce 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -108,7 +108,7 @@ function (array $vs) use ($type_generic_properties): array { ); } - public function fromForm( + public function fromBasicEditingForm( Properties $properties, ClozeText $cloze_text, ScoringIdentical $scoring_of_identical_responses, @@ -135,11 +135,28 @@ public function fromForm( return $updated_properties ->withClozeText( $cloze_text->withIdsOfNewGapsInClozeText( - $updated_gaps->getUndefinedGaps() + $updated_gaps->getIncompleteGaps() ) )->withGaps($updated_gaps); } + public function fromCarry( + Properties $properties, + ?string $carry + ): Properties { + if ($carry === null + || !is_array( + ($carry_array = json_decode($carry, true)) + )) { + return $properties; + } + + return $properties->withValuesFromCarry( + $this->cloze_text_factory, + $carry_array + ); + } + private function retrieveFirstMatchingRowFromDBRecords( string $answer_form_id, array $vs diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index a59e4b4be113..96ce83eb3252 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -114,11 +114,11 @@ public function withAvailablePoints( return $clone; } - public function buildArrayForHiddenInput(): array + public function toCarry(): array { $values = [ self::FORM_KEY_ID => $this->getAnswerOptionId()->toString(), - self::FORM_KEY_POSITION => $this->getPosition(), + self::FORM_KEY_POSITION => (string) $this->getPosition(), self::FORM_KEY_TEXT_VALUE => $this->getTextValue() ]; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 90c1b6bafad8..c673ec6291e2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -33,8 +33,14 @@ class AnswerOptions { + private const string KEY_IS_INCOMPLETE = 'is_incomplete'; + private const string KEY_ANSWER_OPIONS = 'answer_options'; + private const string KEY_ANSWER_OPTIONS_AWARDING_POINTS = 'answer_options_awarding_points'; + private array $answer_options_awarding_points; + private bool $is_incomplete = false; + public function __construct( private readonly Factory $factory, private readonly Uuid $answer_input_id, @@ -45,10 +51,19 @@ public function __construct( public function isIncomplete(): bool { - return $this->answer_options === [] + return $this->is_incomplete + || $this->answer_options === [] || $this->answer_options_awarding_points === []; } + public function withIsIncomplete( + bool $is_incomplete + ): self { + $clone = clone $this; + $clone->is_incomplete = $is_incomplete; + return $clone; + } + public function getAnswerOptionById( Uuid $answer_option_id ): ?AnswerOption { @@ -114,42 +129,6 @@ public function withAnswerOptions( return $clone; } - public function withValuesFromHiddenInputValue( - ?string $value - ): self { - if ($value === null - || !is_array( - ($decoded_value = json_decode($value, true)) - ) - ) { - return $this; - } - - $clone = clone $this; - $clone->answer_options = array_map( - fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( - $vs[AnswerOption::FORM_KEY_ID], - $this->answer_input_id, - $vs[AnswerOption::FORM_KEY_POSITION], - $vs[AnswerOption::FORM_KEY_TEXT_VALUE], - $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, - $vs[AnswerOption::FORM_KEY_UPPER_LIMIT] ?? null, - $vs[AnswerOption::FORM_KEY_AVAILABLE_POINTS] ?? null - ), - $decoded_value['answer_options'] ?? [] - ); - - $answer_inputs_awarding_points = $decoded_value['answer_options_awarding_points'] ?? []; - $clone->answer_options_awarding_points = array_filter( - $clone->answer_options, - fn(AnswerOption $v): bool => in_array( - $v->getAnswerOptionId()->toString(), - $answer_inputs_awarding_points - ) - ); - return $clone; - } - public function withAnswerOptionsFromTags( array $tags ): self { @@ -206,46 +185,47 @@ function (array $c, AnswerOption $v): array { ); } - public function buildQueryCarry(): array + public function toCarry(): array { - return array_map( - fn(AnswerOption $v) => $v->getAnswerOptionId()->toString(), - $this->answer_options - ); + return [ + self::KEY_IS_INCOMPLETE => $this->is_incomplete ? 1 : 0, + self::KEY_ANSWER_OPIONS => array_map( + fn(AnswerOption $v): array => $v->toCarry(), + $this->answer_options + ), + self::KEY_ANSWER_OPTIONS_AWARDING_POINTS => array_map( + fn(AnswerOption $v): string => $v->getAnswerOptionId()->toString(), + $this->answer_options_awarding_points + ) + ]; } - public function withValuesFromQueryCarry( + public function withValuesFromCarry( array $carry ): self { $clone = clone $this; - $clone->answer_options = array_reduce( - $carry, - function (array $c, string $v): array { - $c[$v] = $this->factory->buildAnswerOption( - $v, - $this->answer_input_id, - 0, - '' - ); - return $c; - }, - [] + $clone->is_incomplete = $carry[self::KEY_IS_INCOMPLETE] === 1; + $clone->answer_options = array_map( + fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( + $vs[AnswerOption::FORM_KEY_ID], + $this->answer_input_id, + (int) $vs[AnswerOption::FORM_KEY_POSITION], + $vs[AnswerOption::FORM_KEY_TEXT_VALUE], + $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_UPPER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_AVAILABLE_POINTS] ?? null + ), + $carry[self::KEY_ANSWER_OPIONS] ?? [] ); - return $clone; - } - public function buildHiddenInputValue(): string - { - return json_encode([ - 'answer_options' => array_map( - fn(AnswerOption $v): array => $v->buildArrayForHiddenInput(), - $this->answer_options - ), - 'answer_options_awarding_points' => array_map( - fn(AnswerOption $v): string => $v->getAnswerOptionId()->toString(), - $this->answer_options_awarding_points + $clone->answer_options_awarding_points = array_filter( + $clone->answer_options, + fn(AnswerOption $v): bool => in_array( + $v->getAnswerOptionId()->toString(), + $carry[self::KEY_ANSWER_OPTIONS_AWARDING_POINTS] ?? [] ) - ]); + ); + return $clone; } public function getEditPointsInputs( @@ -301,6 +281,7 @@ public function buildDelete( [ $persistence_factory->where( $persistence->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $answer_options_table_definition ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 077fc8ce9089..947c50361936 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -28,12 +28,14 @@ use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Refinery\Factory as Refinery; class Factory { private array $available_gap_types; public function __construct( + private readonly Refinery $refinery, private readonly UuidFactory $uuid_factory, private readonly AnswerOptionsFactory $answer_options_factory, array $available_gap_types @@ -78,6 +80,7 @@ public function getEmptyGapsObject( Uuid $answer_form_id, ): Gaps { return new Gaps( + $this->refinery, $this, $answer_form_id, [] @@ -117,6 +120,7 @@ function (array $vs) use ($answer_form_id, $answer_options): Gaps { $gaps[] = $this->buildGapFromDBValues($v, $answer_options); } return new Gaps( + $this->refinery, $this, $answer_form_id, $gaps diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index ea961413dc83..a27fc3e3bd58 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -43,6 +43,7 @@ class Gap public const string GAP_PLACEHOLDER_NAME = 'GAP'; private const string KEY_TYPE = 'type'; + private const string KEY_POSITION = 'position'; private const string KEY_MAX_CHARS = 'max_chars'; private const string KEY_STEP_SIZE = 'step_size'; private const string KEY_TEXT_MATCHING_METHOD = 'matching_method'; @@ -181,72 +182,80 @@ public function withAnswerOptions( return $clone; } - public function getQueryCarry(): array + public function toCarry(): array { - return [ + $inputs = [ self::KEY_TYPE => $this->type?->getIdentifier() ?? '', - self::KEY_ANSWER_OPTIONS => $this->answer_options->buildQueryCarry() + self::KEY_POSITION => $this->position, + self::KEY_ANSWER_OPTIONS => $this->answer_options->toCarry() ]; - } - - public function withValuesFromQueryCarry( - Factory $gap_factory, - array $carry - ): self { - $clone = clone $this; - $clone->type = $gap_factory->getGapTypeByIdentifier($carry[self::KEY_TYPE]); - $clone->answer_options = $clone->answer_options->withValuesFromQueryCarry( - $carry[self::KEY_ANSWER_OPTIONS] - ); - } - - public function getCarryInputs( - FieldFactory $ff - ): Group { - $inputs = []; if ($this->max_chars !== null) { - $inputs[self::KEY_MAX_CHARS] = $ff->hidden()->withValue($this->getMaxChars()); + $inputs[self::KEY_MAX_CHARS] = (string) $this->getMaxChars(); } if ($this->step_size !== null) { - $inputs[self::KEY_STEP_SIZE] = $ff->hidden()->withValue($this->getStepSize()); + $inputs[self::KEY_STEP_SIZE] = (string) $this->getStepSize(); } if ($this->text_matching_method !== null) { - $inputs[self::KEY_TEXT_MATCHING_METHOD] = $ff->hidden()->withValue($this->getTextMatchingMethod()->value); + $inputs[self::KEY_TEXT_MATCHING_METHOD] = $this->getTextMatchingMethod()->value; } if ($this->min_autocomplete !== null) { - $inputs[self::KEY_MIN_AUTOCOMPLETE] = $ff->hidden()->withValue($this->getMinAutocomplete()); + $inputs[self::KEY_MIN_AUTOCOMPLETE] = (string) $this->getMinAutocomplete(); } if ($this->shuffle_answer_options !== null) { - $inputs[self::KEY_SHUFFLE_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->getShuffleAnswerOptions() ? '1' : '0'); + $inputs[self::KEY_SHUFFLE_ANSWER_OPTIONS] = $this->getShuffleAnswerOptions() ? '1' : '0'; } - $inputs[self::KEY_ANSWER_OPTIONS] = $ff->hidden()->withValue($this->answer_options->buildHiddenInputValue()); - - return $ff->group($inputs); + return $inputs; } - public function getFromCarryTransformation( + public function withValuesFromCarry( Refinery $refinery, - Factory $gaps_factory - ): Transformation { - return $refinery->custom()->transformation( - function (CarryWrapper $v) use ($refinery, $gaps_factory): self { - $clone = clone $this; - $clone->type = $this->retrieveTypeFromCarry($refinery, $v, $gaps_factory->getAvailableGapTypes()); - $clone->max_chars = $this->retrieveMaxCharsFromCarry($refinery, $v); - $clone->step_size = $this->retrieveStepSizeFromCarry($refinery, $v); - $clone->text_matching_method = $this->retrieveTextMatchingMethodFromCarry($refinery, $v); - $clone->min_autocomplete = $this->retrieveMinAutocompleteFromCarry($refinery, $v); - $clone->shuffle_answer_options = $this->retrieveShuffleAnswerOptionsFromCarry($refinery, $v); - $clone->answer_options = $this->retrieveAnswerOptionsFromCarry($refinery, $v); - return $clone; - } - ); + Factory $gaps_factory, + array $carry + ): self { + if ($carry === null) { + return $this; + } + + $clone = clone $this; + $clone->type = $carry[self::KEY_TYPE] === '' + ? $this->getType() + : $gaps_factory->getGapTypeByIdentifier($carry[self::KEY_TYPE]); + $clone->position = $carry[self::KEY_POSITION]; + + $clone->max_chars = $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMaxChars()) + ])->transform($carry[self::KEY_MAX_CHARS] ?? null); + + $clone->step_size = $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always($this->getStepSize()) + ])->transform($carry[self::KEY_STEP_SIZE] ?? null); + + $clone->text_matching_method = is_string($carry[self::KEY_TEXT_MATCHING_METHOD] ?? null) + ? TextMatchingOptions::tryFrom($carry[self::KEY_TEXT_MATCHING_METHOD]) + : $this->getTextMatchingMethod(); + + $clone->min_autocomplete = $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMinAutocomplete()) + ])->transform($carry[self::KEY_MIN_AUTOCOMPLETE] ?? null); + + $clone->shuffle_answer_options = $refinery->byTrying([ + $refinery->kindlyTo()->bool(), + $refinery->always($this->getShuffleAnswerOptions()) + ])->transform($carry[self::KEY_SHUFFLE_ANSWER_OPTIONS] ?? null); + + $clone->answer_options = $this->answer_options + ->withValuesFromCarry($carry[self::KEY_ANSWER_OPTIONS]); + + return $clone; } public function buildReplace( @@ -395,98 +404,6 @@ private function buildValuesForGapReplace( ]; } - private function retrieveTypeFromCarry( - Refinery $refinery, - CarryWrapper $carry, - array $available_gap_types - ): ?Type { - return $carry->retrieve( - self::KEY_TYPE . $this->getShortenedAnswerInputId(), - $refinery->custom()->transformation( - fn(?string $v): ?Type => $available_gap_types[$v] ?? $this->getType() - ) - ); - } - - private function retrieveMaxCharsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?int { - return $carry->retrieve( - self::KEY_MAX_CHARS . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->int(), - $refinery->always($this->getMaxChars()) - ]) - ); - } - - private function retrieveStepSizeFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?float { - return $carry->retrieve( - self::KEY_STEP_SIZE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->float(), - $refinery->always($this->getStepSize()) - ]) - ); - } - - private function retrieveTextMatchingMethodFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?TextMatchingOptions { - return $carry->retrieve( - self::KEY_TEXT_MATCHING_METHOD . $this->getShortenedAnswerInputId(), - $refinery->custom()->transformation( - fn(?string $v): ?TextMatchingOptions => $v !== null - ? TextMatchingOptions::tryFrom($v) - : $this->getTextMatchingMethod() - ) - ); - } - - private function retrieveMinAutocompleteFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?int { - return $carry->retrieve( - self::KEY_MIN_AUTOCOMPLETE . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->int(), - $refinery->always($this->getMinAutocomplete()) - ]) - ); - } - - private function retrieveShuffleAnswerOptionsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): ?bool { - return $carry->retrieve( - self::KEY_SHUFFLE_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), - $refinery->byTrying([ - $refinery->kindlyTo()->bool(), - $refinery->always($this->getShuffleAnswerOptions()) - ]) - ); - } - - private function retrieveAnswerOptionsFromCarry( - Refinery $refinery, - CarryWrapper $carry - ): AnswerOptions { - return $carry->retrieve( - self::KEY_ANSWER_OPTIONS . $this->getShortenedAnswerInputId(), - $refinery->custom()->transformation( - fn(?string $v): AnswerOptions => $this->answer_options - ->withValuesFromHiddenInputValue($v) - ) - ); - } - private function getShortenedAnswerInputId(): string { return mb_substr($this->answer_input_id->toString(), 0, 4); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 217453596bcf..837e154ba746 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Junctor; @@ -28,13 +29,10 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; -use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\MultiSelect; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -47,6 +45,7 @@ class Gaps private array $gaps; public function __construct( + private readonly Refinery $refinery, private readonly Factory $factory, private Uuid $answer_form_id, array $gaps @@ -127,14 +126,27 @@ public function withResetGaps(): self return $clone; } - public function getUndefinedGaps(): array + public function getIncompleteGaps(): array { return array_filter( $this->gaps, - fn(Gap $v): bool => $v->isUndefined() + fn(Gap $v): bool => $v->getAnswerOptions()->isIncomplete() ); } + public function withMarkedIncompleteGaps(): self + { + $clone = clone $this; + $clone->gaps = array_map( + fn(Gap $v): Gap => $v->getAnswerOptions()->isIncomplete() + ? $v->withAnswerOptions( + $v->getAnswerOptions()->withIsIncomplete(true) + ) : $v, + $clone->gaps + ); + return $clone; + } + public function getRemovedGaps( self $old_gaps ): array { @@ -174,35 +186,40 @@ function (array $c, Gap $v): array { public function buildGapsTypeInputs( Language $lng, FieldFactory $ff, - Refinery $refinery, array $available_gap_types, + Properties $properties, + bool $is_in_creation_context, array $selected_gaps ): Section { return $ff->section( array_reduce( - $selected_gaps !== [] - ? $this->filterGapsBySelected($selected_gaps) - : $this->getUndefinedGaps(), + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), function (array $c, Gap $v) use ($ff, $available_gap_types): array { $c[$v->getAnswerInputId()->toString()] = $ff->select( $v->buildShortenedGapName(), $available_gap_types - )->withRequired(true); + )->withRequired(true) + ->withValue($v->getType()?->getIdentifier()); return $c; }, [] ), $lng->txt('select_gap_types') )->withAdditionalTransformation( - $refinery->custom()->transformation( - fn(array $vs): self => array_reduce( - array_keys($vs), - fn(self $c, string $v): self => $c->withGap( - $c->gaps[$v]->withType( - $this->factory->getGapTypeByIdentifier($vs[$v]) - ) - ), - $this + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap( + $c->gaps[$v]->withType( + $this->factory->getGapTypeByIdentifier($vs[$v]) + ) + ), + $this + ) ) ) ); @@ -211,15 +228,16 @@ function (array $c, Gap $v) use ($ff, $available_gap_types): array { public function buildAnswerOptionsInputs( Language $lng, FieldFactory $ff, - Refinery $refinery, - ?string $carry, + Properties $properties, + bool $is_in_creation_context, array $selected_gaps ): Section { return $ff->section( array_reduce( - $selected_gaps !== [] - ? $this->filterGapsBySelected($selected_gaps) - : $this->getGapsWithIncompleteAnswerOptions(), + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), function (array $c, Gap $v) use ($lng, $ff): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditAnswerOptionsSection( $lng, @@ -231,11 +249,13 @@ function (array $c, Gap $v) use ($lng, $ff): array { ), $lng->txt('add_answer_options') )->withAdditionalTransformation( - $refinery->custom()->transformation( - fn(array $vs): self => array_reduce( - array_keys($vs), - fn(self $c, string $v): self => $c->withGap($vs[$v]), - $this + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) ) ) ); @@ -244,14 +264,16 @@ function (array $c, Gap $v) use ($lng, $ff): array { public function buildPointInputs( Language $lng, FieldFactory $ff, - Refinery $refinery, + Properties $properties, + bool $is_in_creation_context, array $selected_gaps ): Section { return $ff->section( array_reduce( - $selected_gaps !== [] - ? $this->filterGapsBySelected($selected_gaps) - : $this->getGapsWithIncompleteAnswerOptions(), + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), function (array $c, Gap $v) use ($lng, $ff): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditPointsSection( $lng, @@ -263,11 +285,13 @@ function (array $c, Gap $v) use ($lng, $ff): array { ), $lng->txt('add_points') )->withAdditionalTransformation( - $refinery->custom()->transformation( - fn(array $vs): self => array_reduce( - array_keys($vs), - fn(self $c, string $v): self => $c->withGap($vs[$v]), - $this + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) ) ) ); @@ -290,41 +314,25 @@ function (array $c, Gap $v): array { ); } - public function getQueryCarry(): array + public function toCarry(): array { return array_reduce( $this->gaps, function (array $c, Gap $v): array { - $c[$v->getAnswerInputId()->toString()] = $v->getQueryCarry(); + $c[$v->getAnswerInputId()->toString()] = $v->toCarry(); return $c; }, [] ); } - public function getCarryInputs( - FieldFactory $ff - ): Group { - return $ff->group( - array_reduce( - $this->gaps, - function (array $c, Gap $v) use ($ff): array { - $c[$v->getAnswerInputId()->toString()] = $v->getCarryInputs($ff); - return $c; - }, - [] - ) - ); - } - - public function withValuesFromQueryCarry( - Factory $gaps_factory, + public function withValuesFromCarry( array $carry ): self { $clone = clone $this; foreach ($carry as $answer_input_id => $gap_definition) { if (!isset($clone->gaps[$answer_input_id])) { - $clone->gaps[$answer_input_id] = $gaps_factory->getNewGap( + $clone->gaps[$answer_input_id] = $this->factory->getNewGap( $this->answer_form_id, 0, $answer_input_id @@ -332,42 +340,21 @@ public function withValuesFromQueryCarry( } $clone->gaps[$answer_input_id] = $clone->gaps[$answer_input_id] - ->withValuesFromQueryCarry($this->factory, $gap_definition); + ->withValuesFromCarry( + $this->refinery, + $this->factory, + $gap_definition + ); } return $clone; } - public function getFromCarryTransformation( - Refinery $refinery, - Factory $gaps_factory - ): Transformation { - return $refinery->custom()->transformation( - function (?CarryWrapper $v) use ($refinery, $gaps_factory): self { - if ($v === null) { - return $this; - } - $clone = clone $this; - $clone->gaps = array_map( - fn(Gap $gap): Gap => $v->retrieve( - $gap->getAnswerInputId()->toString(), - $gap->getFromCarryTransformation( - $refinery, - $gaps_factory - ) - ), - $this->gaps - ); - return $clone; - } - ); - } - public function toTableRows( DataRowBuilder $row_builder, Language $lng ): \Generator { - foreach ($this->gaps as $gap) { + foreach ($this->orderGapsByPosition($this->gaps) as $gap) { yield $gap->toTableRow( $row_builder, $lng @@ -498,6 +485,7 @@ private function buildDeleteForDeletionOfAnswerForm( [ $persistence_factory->where( $persistence->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $table_definition ), @@ -510,12 +498,15 @@ private function buildDeleteForDeletionOfAnswerForm( ); } - private function getGapsWithIncompleteAnswerOptions(): array - { - return array_filter( - $this->gaps, - fn(Gap $v): bool => $v->getAnswerOptions()->isIncomplete() + private function orderGapsByPosition( + array $gaps + ): array { + usort( + $gaps, + fn(Gap $a, Gap $b) => $a->getPosition() <=> $b->getPosition() ); + + return $gaps; } private function extractIdFromTagName( @@ -524,6 +515,21 @@ private function extractIdFromTagName( return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); } + private function retrieveGapsForInputs( + bool $is_in_creation_context, + array $selected_gaps + ): array { + if ($is_in_creation_context) { + return $this->gaps; + } + + if ($selected_gaps === []) { + return $this->getIncompleteGaps(); + } + + return $this->filterGapsBySelected($selected_gaps); + } + private function filterGapsBySelected( array $selected_gaps ): array { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 0ece42581068..f06049303ec6 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -45,18 +45,16 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Table\Factory as TableFactory; use ILIAS\UI\Component\Table\Data as DataTable; use Psr\Http\Message\ServerRequestInterface; class Properties implements PropertiesInterface { - private const string KEY_ID = 'form_id'; private const string KEY_CLOZE_TEXT = 'cloze_text'; - private const string KEY_IDENTICAL_SCORING = 'identical_scoring'; + private const string KEY_SCORING_IDENTICAL = 'identical_scoring'; private const string KEY_ENABLE_COMBINATIONS = 'enable_combinations'; - private const string KEY_GAPS_TO_EDIT = 'gaps'; + private const string KEY_GAPS = 'gaps'; private bool $updated_combinations = false; @@ -210,14 +208,16 @@ public function buildBasicEditingInputs( Language $lng, FieldFactory $ff, Refinery $refinery, - Factory $propteries_factory, + Factory $properties_factory, ClozeTextFactory $cloze_text_factory, bool $add_legacy_cloze_text_to_input ): Section { - $cloze_text_input = $this->getClozeText()->getInput( + $cloze_text_input = $this->cloze_text->getInput( $lng, $ff, - $cloze_text_factory + $cloze_text_factory, + $this->legacy_cloze_text === '' + && $this->cloze_text->getRawRepresentation() !== '' ); if ($add_legacy_cloze_text_to_input) { @@ -229,7 +229,7 @@ public function buildBasicEditingInputs( return $ff->section( [ self::KEY_CLOZE_TEXT => $cloze_text_input, - self::KEY_IDENTICAL_SCORING => ScoringIdentical::buildInput( + self::KEY_SCORING_IDENTICAL => ScoringIdentical::buildInput( $lng, $ff, $refinery, @@ -241,110 +241,45 @@ public function buildBasicEditingInputs( $lng->txt('create_answer_form') )->withAdditionalTransformation( $refinery->custom()->transformation( - fn(array $vs): self => $propteries_factory->fromForm( + fn(array $vs): self => $properties_factory->fromBasicEditingForm( $this, $vs[self::KEY_CLOZE_TEXT], - $vs[self::KEY_IDENTICAL_SCORING], + $vs[self::KEY_SCORING_IDENTICAL], $vs[self::KEY_ENABLE_COMBINATIONS] ) ) ); } - public function buildQueryCarry(): string + public function toCarry(): string { - return json_encode($this->gaps->getQueryCarry()); + return json_encode([ + self::KEY_CLOZE_TEXT => $this->cloze_text->getRawRepresentation(), + self::KEY_GAPS => $this->gaps->toCarry(), + self::KEY_SCORING_IDENTICAL => $this->scoring_identical->value, + self::KEY_ENABLE_COMBINATIONS => $this->combinations + ->areCombinationsEnabled() ? 1 : 0 + ]); } - public function withValuesFromQueryCarry( + public function withValuesFromCarry( ClozeTextFactory $cloze_text_factory, - ?string $carry + array $carry ): self { - if ($carry === null - || !is_array( - ($carry_array = json_decode($carry)) - )) { - return $this; - } - $clone = clone $this; - $clone->gaps = $this->getGaps()->withValuesFromQueryCarry( - $cloze_text_factory, - $carry_array - ); - return $clone; - } - - public function buildCarryInputs( - FieldFactory $ff - ): Group { - return $ff->group( - [ - self::KEY_ID => $ff->hidden()->withValue($this->answer_form_id->toString()), - self::FORM_KEY_CLOZE_TEXT => $this->getClozeText()->getCarryInputs($ff), - self::KEY_GAPS_TO_EDIT => $this->gaps->getCarryInputs($ff), - self::KEY_IDENTICAL_SCORING => $ff->hidden() - ->withValue($this->getScoringOfIdenticalResponses()->value), - self::KEY_ENABLE_COMBINATIONS => $ff->hidden() - ->withValue($this->combinations->areCombinationsEnabled() ? 1 : 0) - ] - ); - } - public function withValuesFromInputs( - Refinery $refinery, - ClozeTextFactory $cloze_text_factory, - GapsFactory $gaps_factory, - CarryWrapper $carry - ): Properties { - $clone = clone $this; - $clone->cloze_text = $carry->retrieve( - self::KEY_CLOZE_TEXT, - $refinery->byTrying([ - $refinery->custom()->transformation( - fn(?string $v): Text => $v === null - ? throw new \InvalidArgumentException() - : $cloze_text_factory->buildFromHiddenInputString($v) - ), - $refinery->always($clone->cloze_text) - ]) + $clone->cloze_text = $cloze_text_factory->buildFromTextString( + $carry[self::KEY_CLOZE_TEXT] ); - - $clone->scoring_identical = $carry->retrieve( - self::KEY_IDENTICAL_SCORING, - $refinery->byTrying([ - $refinery->custom()->transformation( - fn(?string $v): ScoringIdentical => $v === null - ? throw new \InvalidArgumentException() - : ScoringIdentical::tryFrom($v) ?? $clone->scoring_identical - ), - $refinery->always($clone->scoring_identical) - ]) + $clone->gaps = $this->getGaps()->withValuesFromCarry( + $carry[self::KEY_GAPS] ); - - $clone->combinations = $carry->retrieve( - self::KEY_ENABLE_COMBINATIONS, - $refinery->byTrying([ - $refinery->custom()->transformation( - fn($v): Combinations => $clone->combinations->withCombinationsEnabled( - $refinery->kindlyTo()->bool()->transform($v) - ) - ), - $refinery->always($clone->combinations) - ]) + $clone->scoring_identical = ScoringIdentical::tryFrom( + $carry[self::KEY_SCORING_IDENTICAL] ); - - $clone->gaps = $carry->retrieve( - self::KEY_GAPS_TO_EDIT, - $clone->cloze_text->updateGapsFromMarkdown( - $this->getAnswerFormId(), - $this->getGaps() - )->getFromCarryTransformation( - $refinery, - $gaps_factory - ) + $clone->combinations = $this->combinations->withCombinationsEnabled( + $carry[self::KEY_ENABLE_COMBINATIONS] === 1 ); - return $clone; } @@ -465,6 +400,7 @@ private function buildUpdateAnswerFormStatement( [ $persistence_factory->where( $persistence->getIdColumn( + $persistence_factory, $table_name_builder, $table_definition ), @@ -509,6 +445,7 @@ private function buildDeleteAnswerFormStatement( [ $persistence_factory->where( $persistence->getForeignKeyColumn( + $persistence_factory, $table_name_builder, $table_definition ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 7fe3308d2ce2..ffa5b42c4554 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -24,12 +24,12 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\HTTP\Services as HTTPServices; use ILIAS\Language\Language; @@ -43,15 +43,9 @@ class Edit implements EditViewInterface { private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; + private const string STEP_PROCESS_BASIC_PROPERTIES = 'pbp'; private const string STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES = 'altbp'; private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; - private const string STEP_SET_GAP_TYPES = 'sgt'; - public const string STEP_JUMP_TO_SET_GAP_TYPES = 'jsgt'; - private const string STEP_SET_ANSWER_OPTIONS = 'sao'; - public const string STEP_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; - private const string STEP_SET_POINTS = 'sp'; - public const string STEP_JUMP_TO_SET_POINTS = 'jsp'; - private const string STEP_SAVE = 's'; public function __construct( private readonly Language $lng, @@ -61,7 +55,7 @@ public function __construct( private readonly HTTPServices $http, private readonly PropertiesFactory $properties_factory, private readonly ClozeTextFactory $cloze_text_factory, - private readonly GapFactory $gap_factory + private readonly EditGaps $edit_gaps ) { } @@ -72,8 +66,14 @@ public function create( $step = $environment->getStep(); return match($step) { - '' => $this->buildBasicEditingForm($environment, false), - default => $this->callIntermediateStep($environment, $step) + '' => $this->startEditing($environment), + self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + $environment + ), + default => $this->forwardCmdToEditGaps( + $environment, + $step + ) }; } @@ -99,18 +99,23 @@ public function edit( $environment, $environment->withStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) ->getUrlBuilder() - ->buildURI() ); } $environment->setEditAnswerFormBackTarget(); return match ($step) { - self::STEP_EDIT_BASIC_PROPERTIES => - $this->buildBasicEditingForm($environment, false), + self::STEP_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => $this->addLegacyTextToBasicProperties($environment), - default => $this->callIntermediateStep($environment, $step) + self::STEP_CONFIRMED_GAP_REMOVAL, + self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + $environment->withPreservedTableRowIdsParameter() + ), + default => $this->forwardCmdToEditGaps( + $environment->withPreservedTableRowIdsParameter(), + $step + ) }; } @@ -136,71 +141,65 @@ public function getFinishEditingUrl( return $environment->getUrlBuilder(); } - private function callIntermediateStep( + private function startEditing( + Environment $environment + ): EditForm { + $input_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false + ); + $input_builder->resetCarry(); + + return $this->buildBasicEditingForm( + $environment, + $input_builder, + false + ); + } + + private function forwardCmdToEditGaps( Environment $environment, string $step ): EditForm|Properties { - $initialized_environment = $environment->withPreservedTableRowIdsParameter(); + $processed_form = $this->edit_gaps->call($environment, $step); + if (is_string($processed_form)) { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false, + $processed_form + ); - return match ($step) { - self::STEP_SET_GAP_TYPES, - self::STEP_CONFIRMED_GAP_REMOVAL => $this->processBasicEditingForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesForm( - $initialized_environment - ), - self::STEP_SET_ANSWER_OPTIONS => $this->processGapTypesForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsForm( - $initialized_environment - ), - self::STEP_SET_POINTS => $this->processAnswerOptionsForm( - $initialized_environment - ), - self::STEP_JUMP_TO_SET_POINTS => $this->buildAssignPointsForm( - $initialized_environment - ), - self::STEP_SAVE => $this->processAssignPointsForm( - $initialized_environment - ) - }; + $inputs_builder->persistCarry(); + + return $this->buildBasicEditingForm( + $environment, + $inputs_builder, + false + ); + } + + return $processed_form; } private function buildBasicEditingForm( Environment $environment, + InputsBuilderSession $inputs_builder, bool $add_legacy_cloze_text_to_input ): EditForm { - - /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ - $answer_form_properties = $environment->getAnswerFormProperties(); - - $editing_form = $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::STEP_SET_GAP_TYPES), - $environment->getPresentationFactory()->getInputsBuilder( - $this->refinery->custom()->transformation( - fn(null $carry) => $answer_form_properties->buildBasicEditingInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->refinery, - $this->properties_factory, - $this->cloze_text_factory, - $add_legacy_cloze_text_to_input - ) - ) - ), - false + $editing_form = $this->buildEditFormForBasicInputs( + $environment, + $inputs_builder ); + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ + $properties = $environment->getAnswerFormProperties(); if (!$add_legacy_cloze_text_to_input - && $answer_form_properties->getLegacyClozeText() !== '' - && $answer_form_properties->getClozeText()->getRawRepresentation() === '') { + && $properties->getLegacyClozeText() !== '' + && $properties->getClozeText()->getRawRepresentation() === '') { return $editing_form->withInsertLegacyTextsButton( $environment->withStepParameter( self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES )->getUrlBuilder() - ->buildURI() ); } @@ -210,8 +209,16 @@ private function buildBasicEditingForm( private function addLegacyTextToBasicProperties( Environment $environment ): EditForm { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + true + ); + + $inputs_builder->persistCarry(); + return $this->buildBasicEditingForm( $environment, + $inputs_builder, true ); } @@ -219,13 +226,21 @@ private function addLegacyTextToBasicProperties( private function processBasicEditingForm( Environment $environment ): EditForm|Properties { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false, + ); + $form = $this->buildBasicEditingForm( $environment, + $inputs_builder, false )->withRequest($this->http->request()); + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $data */ $data = $form->getData(); if ($data === null) { + $inputs_builder->persistCarry(); return $form; } @@ -248,152 +263,58 @@ private function processBasicEditingForm( return $data; } - return $this->buildGapTypesForm( - $environment->withAnswerFormProperties($data) - ); - } - - private function buildGapTypesForm( - Environment $environment - ): EditForm { - /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); - return $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::STEP_SET_ANSWER_OPTIONS), - $environment->getPresentationFactory()->getInputsBuilder( - $this->refinery->custom()->transformation( - fn(?string $carry) => $properties - ->withValuesFromQueryCarry($carry) - ->getGaps() - ->buildGapsTypeInputs( - $this->lng, - $ff, - $this->refinery, - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), - $environment->getTableRowIds() - ) + return $this->edit_gaps->call( + $environment->withAnswerFormProperties( + $data->withGaps( + $data->getGaps()->withMarkedIncompleteGaps() ) - )->withCarry( - $properties->buildQueryCarry() - ), - false - )->withContentBeforeForm( - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $properties->getGaps(), - $properties->getLegacyClozeText() ) ); } - private function processGapTypesForm( - Environment $environment - ): EditForm { - $form = $this->buildGapTypesForm( - $environment - )->withRequest($this->http->request()); - - $data = $form->getData(); - return $data === null - ? $form - : $this->buildAnswerOptionsForm( - $environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withGaps($data) - ) - ); - } - - private function buildAnswerOptionsForm( - Environment $environment + private function buildEditFormForBasicInputs( + Environment $environment, + InputsBuilderSession $inputs_builder ): EditForm { - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); return $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::STEP_SET_POINTS), - $this->refinery->custom()->transformation( - fn(?string $carry): Section => $properties->getGaps() - ->buildAnswerOptionsInputs( - $this->lng, - $ff, - $this->refinery, - $carry, - $environment->getTableRowIds() - ) - ), - false, - $properties->buildCarryInputs($ff) - )->withContentBeforeForm( - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $properties->getGaps(), - $properties->getLegacyClozeText() - ) + $inputs_builder, + $environment + ->withStepParameter(self::STEP_PROCESS_BASIC_PROPERTIES) + ->getUrlBuilder(), + null, + false ); } - private function processAnswerOptionsForm( - Environment $environment - ): EditForm { - $form = $this->buildAnswerOptionsForm( - $environment - )->withRequest($this->http->request()); - - $data = $form->getData(); - return $data === null - ? $form - : $this->buildAssignPointsForm( - $environment->withAnswerFormProperties( - $environment->getAnswerFormProperties()->withGaps($data) + private function buildInputsBuilderForBasicInputs( + Environment $environment, + bool $add_legacy_cloze_text_to_input, + ?string $carry = null + ): InputsBuilderSession { + $inputs_builder = $environment->getPresentationFactory() + ->getSessionBasedInputsBuilder( + $environment->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + fn(?string $carry): Section => $this->properties_factory + ->fromCarry( + $environment->getAnswerFormProperties(), + $carry + )->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory, + $add_legacy_cloze_text_to_input + ) ) ); - } - - private function buildAssignPointsForm( - Environment $environment - ): EditForm { - $properties = $environment->getAnswerFormProperties(); - $ff = $this->ui_factory->input()->field(); - return $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::STEP_SAVE), - $properties->getGaps()->buildPointInputs( - $this->lng, - $ff, - $this->refinery, - $environment->getTableRowIds() - ), - true, - $properties->buildCarryInputs($ff) - )->withContentBeforeForm( - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $properties->getGaps(), - $properties->getLegacyClozeText() - ) - ); - } - private function processAssignPointsForm( - Environment $environment - ): EditForm|Properties { - $form = $this->buildAssignPointsForm( - $environment - )->withRequest($this->http->request()); + if ($carry === null) { + return $inputs_builder; + } - $properties = $environment->getAnswerFormProperties(); - $data = $form->getData(); - return $data === null - ? $form->withContentBeforeForm( - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $properties->getGaps(), - $properties->getLegacyClozeText() - ) - ) : $properties->withGaps($data); + return $inputs_builder->withCarry($carry); } /** diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php new file mode 100644 index 000000000000..0f27a7ef3d55 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -0,0 +1,550 @@ +step = $step_array[0]; + $this->start_step = $this->determineStartStepFromStep( + $step_array[1] ?? null + ); + + return match ($this->step) { + self::STEP_SET_GAP_TYPES, + self::STEP_JUMP_TO_SET_GAP_TYPES + => $this->buildGapTypesFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + => $this->backToEditBasicProperties( + $environment + ), + self::STEP_BACK_TO_SET_GAP_TYPES + => $this->backToGapTypesForm( + $environment + ), + self::STEP_SET_ANSWER_OPTIONS + => $this->forwardToAnswerOptionsForm( + $environment + ), + self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + => $this->buildAnswerOptionsFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_BACK_TO_SET_ANSWER_OPTIONS + => $this->backToSetAnswerOptionsForm( + $environment + ), + self::STEP_ASSIGN_POINTS + => $this->forwardToAssignPointsForm( + $environment + ), + self::STEP_JUMP_TO_ASSIGN_POINTS + => $this->buildAssignPointsFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_SAVE + => $this->processAssignPointsForm( + $environment + ) + }; + } + + private function backToEditBasicProperties( + Environment $environment + ): EditForm|string { + $processed_form = $this->processGapTypesForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $processed_form->toCarry(); + } + + private function backToGapTypesForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAnswerOptionsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildGapTypesFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildGapTypesFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder = $this->buildInputsBuilderForTypesForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder->persistCarry(); + + return $this->buildGapTypesForm( + $environment, + $inputs_builder + ); + } + + private function buildGapTypesForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ + $properties = $environment->getAnswerFormProperties(); + + $inputs_builder->persistCarry(); + + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_SET_ANSWER_OPTIONS + ), + $this->step === self::STEP_JUMP_TO_SET_GAP_TYPES + || $this->step === $this->start_step + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + ), + false + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processGapTypesForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_types = $this->buildInputsBuilderForTypesForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $properties = $inputs_builder_for_types->retrieveCarry( + $this->buildRetrievePropertiesTransformation($environment) + ); + + $form = $this->buildGapTypesForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder_for_types + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_types->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForTypesForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps()->buildGapsTypeInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function forwardToAnswerOptionsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processGapTypesForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAnswerOptionsFormWithCarry( + $environment, + $processed_form + ); + } + + private function backToSetAnswerOptionsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAssignPointsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAnswerOptionsFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildAnswerOptionsFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder = $this->buildInputsBuilderForAnswerOptionsForm( + $environment->getPresentationFactory(), + $properties, + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder->persistCarry(); + + return $this->buildAnswerOptionsForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder + ); + } + + private function buildAnswerOptionsForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_ASSIGN_POINTS + ), + $this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + || $this->step === $this->start_step + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_SET_GAP_TYPES + ), + false + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processAnswerOptionsForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_options = $this->buildInputsBuilderForAnswerOptionsForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $form = $this->buildAnswerOptionsForm( + $environment, + $inputs_builder_for_options + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_options->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForAnswerOptionsForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps() + ->buildAnswerOptionsInputs( + $this->lng, + $this->ui_factory->input()->field(), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function forwardToAssignPointsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAnswerOptionsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAssignPointsFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildAssignPointsFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( + $environment->getPresentationFactory(), + $properties, + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder_for_points->persistCarry(); + + return $this->buildAssignPointsForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder_for_points + ); + } + + private function buildAssignPointsForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_SAVE + ), + $this->step === self::STEP_JUMP_TO_ASSIGN_POINTS + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_SET_ANSWER_OPTIONS + ), + true + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processAssignPointsForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $form = $this->buildAssignPointsForm( + $environment, + $inputs_builder_for_points + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_points->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForPointsForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps() + ->buildPointInputs( + $this->lng, + $this->ui_factory->input()->field(), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function buildPostTarget( + Environment $environment, + string $next_step + ): URLBuilder { + if ($this->start_step !== null) { + $next_step = "{$next_step}_{$this->start_step}"; + } + + return $environment->withStepParameter($next_step)->getUrlBuilder(); + } + + private function determineStartStepFromStep( + ?string $start_step_from_get + ): ?string { + if ($start_step_from_get !== null) { + return $start_step_from_get; + } + + if ($this->step === self::STEP_JUMP_TO_SET_GAP_TYPES) { + return self::STEP_BACK_TO_SET_GAP_TYPES; + } + + if ($this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS) { + return self::STEP_BACK_TO_SET_ANSWER_OPTIONS; + } + + return null; + } + + private function buildRetrievePropertiesTransformation( + Environment $environment + ): CustomTransformation { + return $this->refinery->custom()->transformation( + fn(?string $carry): Properties => $this->properties_factory + ->fromCarry( + $environment->getAnswerFormProperties(), + $carry + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 900612a6949a..4a3f3a8467c2 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; +use ILIAS\Data\UUID\Uuid; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -52,6 +53,16 @@ public function withDefaultStep(): self; public function getEditability(): Editability; + public function isInCreationContext(): bool; + + /** + * Returns the answer form id of the current context either from the answer + * form properties, if available, or from the corresponding get parameter. + * There should always be one of the two ids availabe, but be aware that + * is not guaranteed, e.g. if somebody messed with the URI. + */ + public function getAnswerFormId(): ?Uuid; + public function getAnswerFormProperties(): ?Properties; public function withAnswerFormProperties( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index d33ec5be9737..185578cdc160 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -30,7 +30,6 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; -use ILIAS\UI\Component\Input\Input; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -40,9 +39,9 @@ class EnvironmentImplementation implements Environment private const string TOKEN_STRING_ACTION = 'a'; private const string TOKEN_STRING_STEP = 's'; private const string TOKEN_STRING_QUESTION_ID = 'q'; - private const string TOKEN_STRING_TYPE_HASH = 't'; private const string TOKEN_STRING_TABLE_ROW_ID = 'r'; - private const string TOKEN_STRING_CARRY_ID = 'c'; + private const string TOKEN_STRING_TYPE_HASH = 't'; + private const string TOKEN_STRING_ANSWER_FORM_ID = 'af'; private const string TOKEN_STRING_CREATE_MODE = 'cm'; private const string PARAMETER_STRING_HIER_ID = 'hier_id'; @@ -51,15 +50,19 @@ class EnvironmentImplementation implements Environment private const string TAB_ID_ANSWER_FORM = 'answer_form'; - private ?Properties $properties = null; + private ?Properties $answer_form_properties = null; private bool $default_step = false; + private bool $is_in_creation_context = false; private URLBuilder $url_builder; private readonly URLBuilderToken $action_token; private readonly URLBuilderToken $step_token; private readonly URLBuilderToken $question_id_token; private readonly URLBuilderToken $table_row_token; + private ?URLBuilderToken $type_hash_token = null; + private ?URLBuilderToken $answer_form_id_token = null; + private ?URLBuilderToken $create_mode_token = null; private ?array $table_row_ids = null; @@ -155,10 +158,41 @@ public function getEditability(): Editability return $this->editability; } + #[\Override] + public function isInCreationContext(): bool + { + return $this->is_in_creation_context; + } + + #[\Override] + public function getAnswerFormId(): ?Uuid + { + if ($this->answer_form_properties !== null) { + return $this->answer_form_properties->getAnswerFormId(); + } + + $answer_form_id_token = $this->answer_form_id_token; + if ($answer_form_id_token === null) { + [,$answer_form_id_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ANSWER_FORM_ID + ); + } + return $this->http->wrapper()->query()->retrieve( + $answer_form_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + $this->buildRetrieveUuidClosure() + ), + $this->refinery->always(null) + ]) + ); + } + #[\Override] public function getAnswerFormProperties(): ?Properties { - return $this->properties; + return $this->answer_form_properties; } #[\Override] @@ -166,7 +200,7 @@ public function withAnswerFormProperties( Properties $properties ): self { $clone = clone $this; - $clone->properties = $properties; + $clone->answer_form_properties = $properties; return $clone; } @@ -211,6 +245,14 @@ public function withPreservedTableRowIdsParameter(): self return $clone; } + public function withIsInCreationContext( + bool $is_in_creation_context + ): self { + $clone = clone $this; + $clone->is_in_creation_context = $is_in_creation_context; + return $clone; + } + public function getObjId(): int { return $this->obj_id; @@ -242,50 +284,53 @@ public function withQuestionIdParameter( public function withAnswerFormTypeHashParameter( string $type_hash ): self { + $clone = clone $this; [ - $url_builder, - $type_hash_token + $clone->url_builder, + $clone->type_hash_token ] = $this->url_builder->acquireParameter( self::QUERY_PARAMETER_NAME_SPACE, self::TOKEN_STRING_TYPE_HASH ); - $clone = clone $this; - $clone->url_builder = $url_builder - ->withParameter($type_hash_token, $type_hash); + $clone->url_builder = $clone->url_builder + ->withParameter($clone->type_hash_token, $type_hash); return $clone; } - public function withCarryParameter( - string $carry + public function withAnswerFormIdParameter( + Uuid $answer_form_id ): self { + $clone = clone $this; [ - $url_builder, - $carry_token + $clone->url_builder, + $clone->answer_form_id_token ] = $this->url_builder->acquireParameter( self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_CARRY_ID + self::TOKEN_STRING_ANSWER_FORM_ID ); - $clone = clone $this; - $clone->url_builder = $url_builder - ->withParameter($carry_token, $carry); + $clone->url_builder = $clone->url_builder->withParameter( + $clone->answer_form_id_token, + $answer_form_id->toString() + ); return $clone; } public function withCreateModeParameter(): self { + $clone = clone $this; + [ - $url_builder, - $create_mode_token + $clone->url_builder, + $clone->create_mode_token ] = $this->url_builder->acquireParameter( self::QUERY_PARAMETER_NAME_SPACE, self::TOKEN_STRING_CREATE_MODE ); - $clone = clone $this; - $clone->url_builder = $url_builder - ->withParameter($create_mode_token, '1'); + $clone->url_builder = $clone->url_builder + ->withParameter($clone->create_mode_token, '1'); return $clone; } @@ -295,7 +340,7 @@ public function getQuestionId(): ?Uuid $this->question_id_token->getName(), $this->refinery->byTrying([ $this->refinery->custom()->transformation( - $this->buildRetrieveQuestionIdClosure() + $this->buildRetrieveUuidClosure() ), $this->refinery->always(null) ]) @@ -319,7 +364,7 @@ public function getQuestionIds(): array|string|null ), $this->refinery->kindlyTo()->listOf( $this->refinery->custom()->transformation( - $this->buildRetrieveQuestionIdClosure() + $this->buildRetrieveUuidClosure() ) ), $this->refinery->always(null) @@ -328,7 +373,7 @@ public function getQuestionIds(): array|string|null self::INTERRUPTIVE_ITEMS_KEY, $this->refinery->kindlyTo()->listOf( $this->refinery->custom()->transformation( - $this->buildRetrieveQuestionIdClosure() + $this->buildRetrieveUuidClosure() ) ), $this->refinery->always(null) @@ -337,32 +382,25 @@ public function getQuestionIds(): array|string|null public function getTypeClassHash(): string { - [,$type_hash_token] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_TYPE_HASH - ); + $type_hash_token = $this->type_hash_token; + if ($type_hash_token === null) { + [,$type_hash_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + } return $this->retrieveStringValueForToken($type_hash_token); } - public function getCarry( - Transformation $to_form_transformation - ): Input|array|null { - [, $carry_token] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_CARRY_ID - ); - return $this->http->wrapper()->query()->retrieve( - $carry_token->getName(), - $to_form_transformation - ); - } - public function isCreateModeSimple(): bool { - [, $create_mode_token] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_CREATE_MODE - ); + $create_mode_token = $this->create_mode_token; + if ($create_mode_token === null) { + [, $create_mode_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + } return $this->http->wrapper()->query()->has( $create_mode_token->getName() @@ -481,7 +519,7 @@ private function buildStringTrafo(): Transformation ]); } - private function buildRetrieveQuestionIdClosure(): \Closure + private function buildRetrieveUuidClosure(): \Closure { return fn($v): Uuid => is_string($v) ? $this->uuid_factory->fromString($v) diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 0b718c1d13cf..22ffaa90cd42 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -20,15 +20,15 @@ namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Data\URI; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; +use ILIAS\UI\Component\Input\Input; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\URLBuilder; use Psr\Http\Message\ServerRequestInterface; class EditForm implements Renderable @@ -45,13 +45,15 @@ class EditForm implements Renderable public function __construct( private readonly UIFactory $ui_factory, private readonly Language $lng, - Environment $environment, - InputsBuilder $inputs_builder, + Input|InputsBuilder $inputs, + URLBuilder $default_form_action, + ?URLBuilder $back_form_action, bool $is_final_step ) { $this->form = $this->buildForm( - $environment, - $inputs_builder, + $inputs, + $default_form_action, + $back_form_action, $is_final_step ); } @@ -88,7 +90,7 @@ public function withConfirmation( } public function withInsertLegacyTextsButton( - URI $target_uri + URLBuilder $target_builder ): self { $clone = clone $this; $clone->insert_legacy_text_button = $this->ui_factory->messageBox()->info( @@ -96,7 +98,7 @@ public function withInsertLegacyTextsButton( )->withButtons([ $this->ui_factory->button()->standard( $this->lng->txt('insert_legacy_texts'), - $target_uri->__toString() + $target_builder->buildURI()->__toString() ) ]); return $clone; @@ -152,27 +154,35 @@ function ($id) { } private function buildForm( - Environment $environment, - InputsBuilder $inputs_builder, + Input|InputsBuilder $inputs, + URLBuilder $default_form_action, + ?URLBuilder $back_form_action, bool $is_final_step ): StandardForm { + if ($inputs instanceof InputsBuilder) { + $inputs = $inputs->getInputs(); + } $form = $this->ui_factory->input()->container()->form()->standard( - $inputs_builder - ->addCarryToEnvironment($environment) - ->getUrlBuilder() - ->buildURI() - ->__toString(), + $default_form_action->buildURI()->__toString(), [ - self::MAIN_SECTION_NAME => $inputs_builder->getInputs( - $environment - ) + self::MAIN_SECTION_NAME => $inputs ] ); + if ($back_form_action !== null) { + $form = $form->withAdditionalFormAction( + $back_form_action->buildURI()->__toString(), + $this->lng->txt('previous') + ); + } + + $submit_action_label = 'next'; if ($is_final_step) { - return $form->withSubmitLabel($this->lng->txt('save')); + $submit_action_label = 'save'; } - return $form->withSubmitLabel($this->lng->txt('next')); + return $form->withSubmitLabel( + $this->lng->txt($submit_action_label) + ); } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index fe795f394183..95651e4c55c5 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -22,11 +22,11 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Data\URI; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\URLBuilder; use Psr\Http\Message\ServerRequestInterface; class EditOverview implements Renderable @@ -36,7 +36,7 @@ public function __construct( private readonly Language $lng, private readonly ServerRequestInterface $request, private readonly Environment $environment, - private readonly URI $uri_to_edit_basic_answer_form_properties + private readonly URLBuilder $target_to_edit_basic_answer_form_properties ) { } @@ -70,7 +70,9 @@ private function buildBasicAnswerFormPanel(): StandardPanel if ($this->environment->getEditability() === Editability::Full) { $content[] = $this->ui_factory->button()->standard( $this->lng->txt('edit_basic_answer_form_properties'), - $this->uri_to_edit_basic_answer_form_properties->__toString() + $this->target_to_edit_basic_answer_form_properties + ->buildURI() + ->__toString() ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index b6848cfbcfe2..acb7c77337f9 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -20,28 +20,21 @@ namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\Leaf; -use ILIAS\Data\URI; use ILIAS\HTTP\Services as HttpService; -use ILIAS\HTTP\Wrapper\ArrayBasedRequestWrapper; use ILIAS\Language\Language; -use ILIAS\Refinery\Custom\Transformation as CustomTransformation; -use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Input\Input; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; +use ILIAS\UI\URLBuilder; class Factory { public function __construct( private readonly UIFactory $ui_factory, - private readonly Refinery $refinery, private readonly HttpService $http, private readonly Language $lng ) { @@ -49,14 +42,14 @@ public function __construct( public function getEditOverview( Environment $environment, - URI $uri_to_edit_basic_answer_form_properties + URLBuilder $target_to_edit_basic_answer_form_properties ): EditOverview { return new EditOverview( $this->ui_factory, $this->lng, $this->http->request(), $environment, - $uri_to_edit_basic_answer_form_properties + $target_to_edit_basic_answer_form_properties ); } @@ -65,18 +58,18 @@ public function getEditOverview( * to which the form shall be sent. */ public function getEditForm( - Environment $environment, - Section $main_section_inputs, - bool $is_final_step, - ?Group $carry_inputs = null + Input|InputsBuilder $main_section_inputs, + URLBuilder $default_form_action, + ?URLBuilder $back_form_action, + bool $is_final_step ): EditForm { return new EditForm( $this->ui_factory, $this->lng, - $environment, $main_section_inputs, - $is_final_step, - $carry_inputs + $default_form_action, + $back_form_action, + $is_final_step ); } @@ -89,18 +82,12 @@ public function getAsync( ); } - /** - * @param CustomTransformation $to_inputs This MUST return an `array` of - * inputs that will then be used in the form. The transformation will receive - * the string produced by `$to_carry` as parameter. - * @param Input|array|null $inputs If you provide inputs it is assumed that no - * carry is present and you want to use them directly. - */ - public function getInputsBuilder( - CustomTransformation $to_inputs - ): InputsBuilder { - return new InputsBuilder( - $this->refinery, + public function getSessionBasedInputsBuilder( + string $storage_key, + Transformation $to_inputs + ): InputsBuilderSession { + return new InputsBuilderSession( + $storage_key, $to_inputs ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php index 3d11c950b940..10bcd3ec1389 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php @@ -20,74 +20,9 @@ namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Custom\Transformation as CustomTransformation; -use ILIAS\UI\Component\Input\Input; +use ILIAS\UI\Component\Input\Field\Section; -/** - * This is a InputBuilder for multipart forms. It allows you to carry the necessary - * information from one page of the form to the next in the query. Be aware that - * you are thoroughly limited in the amount of information that you can carry over, - * so make sure to just set the minimum you absolutely need. - */ -class InputsBuilder +interface InputsBuilder { - private ?string $carry = null; - - /** - * @param CustomTransformation $to_inputs This Transformation receives the - * value set by `self::withCarry()` if it is available, otherwise the value - * will be null. - */ - public function __construct( - private readonly Refinery $refinery, - private readonly CustomTransformation $to_inputs - ) { - } - - /** - * @param string $carry The `string` will be `base64-encoded` before - * adding it to the `Query`. The string will be passed to the `$to_inputs` - * transformation to allow the recreation of the inputs. - */ - public function withCarry( - string $carry - ): self { - $clone = clone $this; - $clone->carry = $carry; - return $clone; - } - - public function addCarryToEnvironment( - EnvironmentImplementation $environment - ): Environment { - return $environment->withCarryParameter( - $this->carry === null - ? $environment->getCarry($this->refinery->identity()) - : base64_encode($this->carry) - ); - } - - /** - * @return \ILIAS\UI\Component\Input\Input|array|null The return value will - * be null, if no inputs could be built. This is a sign that something is - * wrong with carry. This might mean that there is a mistake in the carry - * that was set, but it can also be the consequence of somebody messing - * with the query. - */ - public function getInputs( - EnvironmentImplementation $environment - ): Input|array|null { - return $environment->getCarry( - $this->refinery->custom()->transformation( - fn(?string $v): Input|array|null => $this->to_inputs->transform( - $v === null - ? null - : base64_decode($v) - ) - ) - ); - } + public function getInputs(): Section; } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php new file mode 100644 index 000000000000..ee466a5b4c3f --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php @@ -0,0 +1,89 @@ +carry = $carry; + return $clone; + } + + public function persistCarry(): void + { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + \ilSession::set($this->storage_key, $this->carry); + } + + public function resetCarry(): void + { + \ilSession::clear($this->storage_key); + } + + public function retrieveCarry( + Transformation $transformation + ): mixed { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + + return $transformation->transform($this->carry); + } + + public function getInputs(): Section + { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + return $this->to_inputs->transform($this->carry); + } + + private function loadCarryFromSessionAndClear(): void + { + $this->carry = \ilSession::get($this->storage_key); + \ilSession::clear($this->storage_key); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 2aa339784cae..c13d3edf735e 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -129,7 +129,9 @@ public function show( ); return match($environment->getAction()) { - self::ACTION_CREATE_QUESTION => $this->createQuestion($environment), + self::ACTION_CREATE_QUESTION => $this->createQuestion( + $environment->withIsInCreationContext(true) + ), self::ACTION_EDIT_QUESTION => $this->editQuestion($environment), self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), default => $this->showTable($toolbar, $environment) @@ -181,7 +183,8 @@ public function createAnswerForm( $environment = $this->buildEnvironment( $base_uri, $obj_id - )->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) + )->withIsInCreationContext(true) + ->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); $answer_form_type_class_hash = $environment->getTypeClassHash(); @@ -195,7 +198,8 @@ public function createAnswerForm( $type->buildProperties( $this->answer_form_factory->getDefaultTypeGenericProperties( $question->getId(), - $type + $type, + $environment->getAnswerFormId(), ), null ) @@ -430,7 +434,11 @@ private function forwardCreateAnswerFormCmd( \ilPCAnswerForm $content_object, AnswerFormEditView $answer_form_edit_view ): ?EditForm { - $create = $answer_form_edit_view->create($environment); + $create = $answer_form_edit_view->create( + $environment->withAnswerFormIdParameter( + $environment->getAnswerFormId() + ) + ); if ($create instanceof EditForm) { return $create; @@ -545,7 +553,6 @@ private function buildCreateAnswerForm( } return $environment->getPresentationFactory()->getEditForm( - $environment, $if->field()->section( $inputs + [ 'type' => $if->field()->select( @@ -561,6 +568,8 @@ private function buildCreateAnswerForm( ], $this->lng->txt('create_answer_form') ), + $environment->getUrlBuilder(), + null, false ); } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index f51e86f1b11e..9684c49010f2 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -107,11 +107,14 @@ private function buildBasicPropertiesCreateForm( } return $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::CMD_SAVE_QUESTION), $ff->section( $inputs, $this->lng->txt('edit_basic_form_properties') ), + $environment + ->withStepParameter(self::CMD_SAVE_QUESTION) + ->getUrlBuilder(), + null, false ); } @@ -136,17 +139,18 @@ private function buildBasicPropertiesEditingForm( EnvironmentImplementation $environment ): EditForm { return $environment->getPresentationFactory()->getEditForm( - $environment->withStepParameter(self::CMD_SAVE_QUESTION), - $environment->getPresentationFactory()->getInputsBuilder( - $this->ui_factory->input()->field()->section( - $this->buildBasicPropertiesInputs(), - $this->lng->txt('edit_basic_form_properties') - )->withAdditionalTransformation( - $this->buildAddBasicPropertiesToQuestionTrafo() - )->withValue( - $this->buildBasicPropertiesBasicValuesArray() - ) + $this->ui_factory->input()->field()->section( + $this->buildBasicPropertiesInputs(), + $this->lng->txt('edit_basic_form_properties') + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() ), + $environment + ->withStepParameter(self::CMD_SAVE_QUESTION) + ->getUrlBuilder(), + null, true ); } From 202f14347c1ca2fdca806e7d0f0cd2d13c76eef7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 14:46:36 +0100 Subject: [PATCH 067/108] Questions: Make Create Button Primary --- components/ILIAS/Questions/src/Presentation/Views/Edit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index c13d3edf735e..87722931dfdf 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -371,7 +371,7 @@ private function showTable( EnvironmentImplementation $environment ): QuestionsTable { $toolbar->addComponent( - $this->ui_factory->button()->standard( + $this->ui_factory->button()->primary( $this->lng->txt('create'), $environment->withActionParameter(self::ACTION_CREATE_QUESTION) ->getUrlBuilder() From cbc0de98253556b9a7d64bdd7a6dec3bd0448f3b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 14:49:11 +0100 Subject: [PATCH 068/108] Questions: Change LangVar for Gap-Placeholder --- .../src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index b1eba9f89ad4..54868bb3e3f7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -30,7 +30,6 @@ use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Markdown as MarkdownInput; -use ILIAS\UI\Component\Input\Field\Hidden as HiddenInput; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use Mustache\Engine; @@ -54,7 +53,7 @@ public function getInput( new \ilUIMarkdownPreviewGUI(), $lng->txt('cloze_text') )->withMustacheVariables([ - Gap::GAP_PLACEHOLDER_NAME => $lng->txt('gap') + Gap::GAP_PLACEHOLDER_NAME => $lng->txt('insert_a_gap') ])->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(string $v): self => $cloze_text_factory->buildFromTextString($v) From 4196135ff8bcc67d6fd0bcea576c73181d109751 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 15:10:54 +0100 Subject: [PATCH 069/108] Questions: Fix Requirement of ClozeText --- .../src/AnswerFormTypes/Cloze/Properties/Properties.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index f06049303ec6..6a84e536af7d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -217,7 +217,8 @@ public function buildBasicEditingInputs( $ff, $cloze_text_factory, $this->legacy_cloze_text === '' - && $this->cloze_text->getRawRepresentation() !== '' + || $this->legacy_cloze_text !== '' + && $this->cloze_text->getRawRepresentation() !== '' ); if ($add_legacy_cloze_text_to_input) { From c1043a70e189110c1861b10e2a40dd000fd35a7f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 15:11:17 +0100 Subject: [PATCH 070/108] Questions: Change Title LangVars --- .../src/AnswerFormTypes/Cloze/Properties/Properties.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 6a84e536af7d..f3cb647494bb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -239,7 +239,7 @@ public function buildBasicEditingInputs( self::KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) ->withValue($this->combinations->areCombinationsEnabled()) ], - $lng->txt('create_answer_form') + $lng->txt('set_basic_properties') )->withAdditionalTransformation( $refinery->custom()->transformation( fn(array $vs): self => $properties_factory->fromBasicEditingForm( From 116d93ecf83e1bcdae3e54102e3a0e3b91aabacb Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 18 Feb 2026 15:24:45 +0100 Subject: [PATCH 071/108] Questions: Remove Tabs from Question Editing --- .../ILIAS/Questions/src/Presentation/Views/Edit.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 87722931dfdf..a8c6aea49c34 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -264,6 +264,10 @@ private function createQuestion( EnvironmentImplementation $environment ): EditForm { $this->initializeEditMode($environment); + $this->tabs_gui->setBackTarget( + $this->lng->txt('cancel'), + $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); $create = $this->questions_repository->getNew( $environment->getObjId() @@ -298,6 +302,10 @@ private function editQuestion( EnvironmentImplementation $environment ): EditForm { $this->initializeEditMode($environment); + $this->tabs_gui->setBackTarget( + $this->lng->txt('back'), + $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); $question_id = $environment->getQuestionId(); $question = $this->questions_repository->getForQuestionId($question_id); @@ -457,6 +465,8 @@ private function forwardCreateAnswerFormCmd( private function initializeEditMode( EnvironmentImplementation $environment ): void { + $this->tabs_gui->clearTargets(); + $this->global_screen->tool()->context()->current()->addAdditionalData( LayoutProvider::MODE_ENABLED, true From b36258752c670cbe294231248792ee47e2fdc2c3 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 19 Feb 2026 11:00:38 +0100 Subject: [PATCH 072/108] Questions: Add Create and New and Fix Redirects --- .../PageEditor/class.ilPCAnswerFormGUI.php | 9 --- .../Administration/class.ConfigurationGUI.php | 1 - .../Definitions/EnvironmentImplementation.php | 47 ++++++++++++++- .../src/Presentation/Layout/EditForm.php | 26 ++++++++- .../src/Presentation/Layout/Factory.php | 4 -- .../Questions/src/Presentation/Views/Edit.php | 58 +++++++++++++++++-- 6 files changed, 122 insertions(+), 23 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index ebf306f5b42d..97db48fcfafe 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -58,7 +58,6 @@ public function executeCommand() public function insertCmd(): void { - $this->setInsertTabs(); $content_obj = new ilPCAnswerForm($this->pg_obj); $content_obj->setHierId($this->hier_id); $this->tpl->setContent( @@ -93,12 +92,4 @@ public function editCmd(): void )->render($this->ui_renderer) ); } - - private function setInsertTabs(): void - { - $this->tabs->setBackTarget( - $this->lng->txt('cancel'), - $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') - ); - } } diff --git a/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php index 925924390013..74ff8631a861 100755 --- a/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php +++ b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\Administration; -use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\HTTP\Services as HTTP; use ILIAS\Language\Language; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index 185578cdc160..31fcb85d3032 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -43,6 +43,7 @@ class EnvironmentImplementation implements Environment private const string TOKEN_STRING_TYPE_HASH = 't'; private const string TOKEN_STRING_ANSWER_FORM_ID = 'af'; private const string TOKEN_STRING_CREATE_MODE = 'cm'; + private const string TOKEN_STRING_CREATE_AND_NEW = 'can'; private const string PARAMETER_STRING_HIER_ID = 'hier_id'; @@ -87,7 +88,7 @@ public function setEditAnswerFormBackTarget(): void $this->tabs_gui->clearTargets(); $this->tabs_gui->setBackTarget( $this->lng->txt('cancel'), - $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + $this->buildEditAnswerFormBackUrl()->buildURI()->__toString() ); } @@ -473,6 +474,21 @@ public function setParamtersForSimpleCreateCmd( ); } + public function isCreateAndNewAction(): bool + { + return $this->http->wrapper()->query()->has( + $this->buildURLBuilderTokenForCreateAndNew()->getName() + ); + } + + public function buildURLBuilderTokenForCreateAndNew(): URLBuilderToken + { + return new URLBuilderToken( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_AND_NEW + ); + } + private function setQuestionIdParamterForPageEditorCmds( Uuid $question_id ): void { @@ -483,6 +499,35 @@ private function setQuestionIdParamterForPageEditorCmds( ); } + private function buildEditAnswerFormBackUrl(): URLBuilder + { + if (!$this->is_in_creation_context) { + return $this->withDefaultStep()->getUrlBuilder(); + } + + if (!$this->isCreateModeSimple()) { + return new URLBuilder( + new URI( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( + \QstsQuestionPageGUI::class, + 'edit' + ) + ) + ); + } + + return $this->url_builder->withParameter( + $this->action_token, + Edit::ACTION_DELETE_QUESTIONS + )->withParameter( + $this->step_token, + Edit::ACTION_DELETE_QUESTIONS + )->withParameter( + $this->table_row_token, + [$this->getQuestionId()->toString()] + ); + } + private function acquireURLBuilderAndParameters( URI $base_uri ): void { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 22ffaa90cd42..469b158eaaf0 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -29,6 +29,7 @@ use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\URLBuilder; +use ILIAS\UI\URLBuilderToken; use Psr\Http\Message\ServerRequestInterface; class EditForm implements Renderable @@ -46,9 +47,9 @@ public function __construct( private readonly UIFactory $ui_factory, private readonly Language $lng, Input|InputsBuilder $inputs, - URLBuilder $default_form_action, + private URLBuilder $default_form_action, ?URLBuilder $back_form_action, - bool $is_final_step + private bool $is_final_step ) { $this->form = $this->buildForm( $inputs, @@ -65,6 +66,11 @@ public function render( return $ui_renderer->render($this->buildContent()); } + public function isFinalStep(): bool + { + return $this->is_final_step; + } + public function withContentBeforeForm( StandardPanel $content ): self { @@ -118,6 +124,22 @@ public function getData(): mixed return $data[self::MAIN_SECTION_NAME] ?? null; } + public function withAdditionalAction( + URLBuilderToken $parameter_token, + string $parameter_value, + string $label + ): self { + $clone = clone $this; + $clone->form = $this->form->withAdditionalFormAction( + $this->default_form_action->buildURI()->withParameter( + $parameter_token->getName(), + $parameter_value + )->__toString(), + $label + ); + return $clone; + } + private function buildContent(): array { $content = []; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index acb7c77337f9..4857dcc5e158 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -53,10 +53,6 @@ public function getEditOverview( ); } - /** - * @param Environment $environment The environment MUST have the step set, - * to which the form shall be sent. - */ public function getEditForm( Input|InputsBuilder $main_section_inputs, URLBuilder $default_form_action, diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index a8c6aea49c34..4ea4d170f9d6 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -36,7 +36,6 @@ use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; -use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -148,6 +147,13 @@ public function forwardPageCmds( $base_uri, $obj_id ); + + if ($this->ctrl->getCmd() === 'insert' + && $environment->getAction() === self::ACTION_DELETE_QUESTIONS) { + $this->deleteQuestions($environment); + return; + } + $this->initializeEditMode($environment); $environment->preserveParametersForPageEditorCmds(); @@ -187,6 +193,13 @@ public function createAnswerForm( ->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); + + $environment->setEditAnswerFormBackTarget(); + + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $environment = $environment->withCreateModeParameter(); + } + $answer_form_type_class_hash = $environment->getTypeClassHash(); if ($answer_form_type_class_hash !== '') { @@ -288,7 +301,7 @@ private function createQuestion( } $this->questions_repository->create([$create]); - return $this->ctrl->redirectToURL( + $this->ctrl->redirectToURL( $this->buildAfterQuestionCreationRedirectUri( $environment, $create->getCreateMode(), @@ -449,7 +462,12 @@ private function forwardCreateAnswerFormCmd( ); if ($create instanceof EditForm) { - return $create; + return $create->isFinalStep() + ? $create->withAdditionalAction( + $environment->buildURLBuilderTokenForCreateAndNew(), + '1', + $this->lng->txt('create_and_new') + ) : $create; } $this->questions_repository->create( @@ -459,7 +477,9 @@ private function forwardCreateAnswerFormCmd( $content_object->create($create->getAnswerFormId()); $content_object->getPage()->update(); - $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); + $this->ctrl->redirectToURL( + $this->buildAfterAnswerFormCreationRedirectUri($environment) + ); } private function initializeEditMode( @@ -558,8 +578,7 @@ private function buildCreateAnswerForm( if ($this->configuration_repository->isCreateModeSimple($environment)) { $inputs['question_text'] = $if->field()->textarea( $this->lng->txt('question_text') - ); - $environment = $environment->withCreateModeParameter(); + )->withRequired(true); } return $environment->getPresentationFactory()->getEditForm( @@ -679,4 +698,31 @@ private function buildAfterQuestionCreationRedirectUri( 'insert' ); } + + private function buildAfterAnswerFormCreationRedirectUri( + EnvironmentImplementation $environment, + ): string { + if (!$this->configuration_repository->isCreateModeSimple($environment)) { + return $this->ctrl->getLinkTargetByClass( + \QstsQuestionPageGUI::class, + 'edit' + ); + } + + $additonal_data = $this->global_screen + ->tool() + ->context() + ->current() + ->getAdditionalData(); + + if ($environment->isCreateAndNewAction()) { + return $additonal_data + ->get(LayoutProvider::URL_CREATE_QUESTION) + ->__toString(); + } + + return $additonal_data + ->get(LayoutProvider::URL_CLOSE_MODE_INFO) + ->__toString(); + } } From e2ccb5578b7106a9d41d167c677390353a2edf65 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 19 Feb 2026 12:23:42 +0100 Subject: [PATCH 073/108] Questions: Fix Crash on Missing Question Title --- components/ILIAS/Questions/src/Question/Views/Edit.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 9684c49010f2..34ad08af7d35 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -127,12 +127,9 @@ private function processBasicPropertiesCreateForm( )->withRequest($this->request); $data = $form->getData(); - - $mode = $data['create_mode']; - return $data === null ? $form - : $data['question']->withCreateMode($mode); + : $data['question']->withCreateMode($data['create_mode']); } private function buildBasicPropertiesEditingForm( From 6f0534d80adb3ca73a9b822363d983a28672fc23 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 19 Feb 2026 18:24:22 +0100 Subject: [PATCH 074/108] Questions: Add Central Facilities to Environment --- .../ILIAS/Questions/Legacy/LocalDIC.php | 8 - .../Questions/src/AnswerForm/Properties.php | 3 - .../Questions/src/AnswerForm/Views/Edit.php | 5 - .../Cloze/Layout/CombinationsOverview.php | 88 ++++++----- .../Cloze/Layout/OverviewTable.php | 55 +++---- .../Properties/Combinations/Combinations.php | 13 +- .../Combinations/EditCombinations.php | 12 -- .../Cloze/Properties/Combinations/Factory.php | 5 - .../Cloze/Properties/Properties.php | 6 - .../src/AnswerFormTypes/Cloze/Views/Edit.php | 48 ++---- .../AnswerFormTypes/Cloze/Views/EditGaps.php | 138 +++++++----------- .../Presentation/Definitions/Environment.php | 12 ++ .../Definitions/EnvironmentImplementation.php | 25 ++++ .../src/Presentation/Layout/EditOverview.php | 24 ++- .../src/Presentation/Layout/Factory.php | 8 +- .../Presentation/Layout/QuestionsTable.php | 42 +++--- .../Questions/src/Presentation/Views/Edit.php | 95 ++++++++---- .../src/Question/QuestionImplementation.php | 14 +- .../Questions/src/Question/Views/Edit.php | 85 ++++++----- 19 files changed, 318 insertions(+), 368 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 02e23996a501..af3587cf5fe7 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -158,20 +158,12 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps => new Cloze\Views\EditGaps( - $DIC['lng'], - $DIC['ui.factory'], - $DIC['refinery'], - $DIC['http'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] ); $dic[Cloze\Views\Edit::class] = static fn($c): Cloze\Views\Edit => new Cloze\Views\Edit( - $DIC['lng'], - $DIC['ui.factory'], $DIC['ilToolbar'], - $DIC['refinery'], - $DIC['http'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Views\EditGaps::class] diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index 16c5b5c5a19d..9a8419973118 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -44,9 +44,6 @@ public function getBasicPropertiesForListing( ): array; public function getOverviewTable( - TableFactory $table_factory, - Language $lng, - ServerRequestInterface $request, Environment $environment ): DataTable|OrderingTable; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index aa4106783d8c..ce0c6bf98fd0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -26,7 +26,6 @@ use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Questions\Presentation\Layout\Renderable; -use ILIAS\UI\URLBuilder; interface Edit { @@ -41,8 +40,4 @@ public function edit( public function other( Environment $environment ): Async|Renderable|Properties; - - public function getFinishEditingUrl( - Environment $environment - ): URLBuilder; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php index eb9d6b9b3f82..0d533ad8560c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -30,10 +30,6 @@ use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Data\Range; use ILIAS\Data\Order; -use ILIAS\HTTP\Services as Http; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; @@ -53,11 +49,7 @@ class CombinationsOverview implements DataRetrieval, Renderable private ?RoundTripModal $modal = null; public function __construct( - private readonly UIFactory $ui_factory, private readonly \ilToolbarGUI $toolbar, - private readonly Refinery $refinery, - private readonly Language $lng, - private readonly Http $http, private readonly Environment $environment, private readonly CombinationsFactory $combinations_factory ) { @@ -88,7 +80,10 @@ public function getRows( mixed $additional_parameters ): \Generator { yield from $this->environment->getAnswerFormProperties() - ->getCombinations()->toTableRows($this->lng, $row_builder); + ->getCombinations()->toTableRows( + $this->environment->getLanguage(), + $row_builder + ); } #[\Override] @@ -113,20 +108,20 @@ public function doAction(): Async|self|Properties private function buildTable(): DataTable { - return $this->ui_factory->table()->data( + return $this->environment->getUIFactory()->table()->data( $this, - $this->lng->txt('combinations'), + $this->environment->getLanguage()->txt('combinations'), $this->getColumns() )->withActions($this->getActions()) - ->withRequest($this->http->request()); + ->withRequest($this->environment->getHttpServices()->request()); } private function initializeModal( RoundTripModal $modal ): RoundTripModal { $this->toolbar->addComponent( - $this->ui_factory->button()->standard( - $this->lng->txt('add_combination'), + $this->environment->getUIFactory()->button()->standard( + $this->environment->getLanguage()->txt('add_combination'), $modal->getShowSignal() ) ); @@ -135,27 +130,27 @@ private function initializeModal( private function getColumns(): array { - $cf = $this->ui_factory->table()->column(); + $cf = $this->environment->getUIFactory()->table()->column(); return [ - 'gaps' => $cf->text($this->lng->txt('gaps')), - 'values' => $cf->text($this->lng->txt('values')), - 'available_points' => $cf->number($this->lng->txt('points'))->withDecimals(2) + 'gaps' => $cf->text($this->environment->getLanguage()->txt('gaps')), + 'values' => $cf->text($this->environment->getLanguage()->txt('values')), + 'available_points' => $cf->number($this->environment->getLanguage()->txt('points'))->withDecimals(2) ]; } private function getActions(): array { - $af = $this->ui_factory->table()->action(); + $af = $this->environment->getUIFactory()->table()->action(); return [ $af->single( - $this->lng->txt('edit'), + $this->environment->getLanguage()->txt('edit'), $this->environment ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) ->getUrlBuilder(), $this->environment->getTableRowIdToken() )->withAsync(true), $af->single( - $this->lng->txt('delete'), + $this->environment->getLanguage()->txt('delete'), $this->environment ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) ->getUrlBuilder(), @@ -190,8 +185,9 @@ private function buildAction(): Async private function buildNoItemsSelectedAsync(): Async { return new Async( - $this->http, - $this->ui_factory->messageBox()->failure('no_combination_selected') + $this->environment->getHttpServices(), + $this->environment->getUIFactory()->messageBox() + ->failure('no_combination_selected') ); } @@ -199,26 +195,26 @@ private function buildSetCombinationGapsModal(): RoundTripModal { $properties = $this->environment->getAnswerFormProperties(); $gaps = $properties->getGaps(); - return $this->ui_factory->modal()->roundtrip( - $this->lng->txt('add_combination'), + return $this->environment->getUIFactory()->modal()->roundtrip( + $this->environment->getLanguage()->txt('add_combination'), $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, + $this->environment->getUIFactory(), + $this->environment->getLanguage(), $gaps, $properties->getLegacyClozeText() ), [ 'combination' => $gaps->buildGapsMultiSelect( - $this->lng->txt('select_gaps_for_combinations'), - $this->ui_factory->input()->field() + $this->environment->getLanguage()->txt('select_gaps_for_combinations'), + $this->environment->getUIFactory()->input()->field() )->withRequired(true) ->withAdditionalTransformation( - $this->refinery->custom()->constraint( + $this->environment->getRefinery()->custom()->constraint( fn(array $v): bool => count($v) > 1, - $this->lng->txt('combination_needs_more_than_one') + $this->environment->getLanguage()->txt('combination_needs_more_than_one') ) )->withAdditionalTransformation( - $this->refinery->custom()->transformation( + $this->environment->getRefinery()->custom()->transformation( fn(array $v): Combination => $this->combinations_factory ->buildNewCombination($gaps, $v) ) @@ -229,7 +225,7 @@ private function buildSetCombinationGapsModal(): RoundTripModal ->getUrlBuilder() ->buildURI() ->__toString() - )->withSubmitLabel($this->lng->txt('next')); + )->withSubmitLabel($this->environment->getLanguage()->txt('next')); } private function processSetCombinationGapsModal(): self @@ -237,7 +233,7 @@ private function processSetCombinationGapsModal(): self $clone = clone $this; $set_gaps_modal = $clone->buildSetCombinationGapsModal() - ->withRequest($clone->http->request()); + ->withRequest($clone->environment->getHttpServices()->request()); $data = $set_gaps_modal->getData(); if ($data === null) { @@ -258,11 +254,11 @@ private function buildSetCombinationValuesModal( $properties = $this->environment->getAnswerFormProperties(); $gaps = $properties->getGaps(); - return $this->ui_factory->modal()->roundtrip( - $this->lng->txt('edit'), + return $this->environment->getUIFactory()->modal()->roundtrip( + $this->environment->getLanguage()->txt('edit'), $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, + $this->environment->getUIFactory(), + $this->environment->getLanguage(), $gaps, $properties->getLegacyClozeText() ), @@ -280,7 +276,7 @@ private function processSetCombinationValues(): self|Properties { $inputs_builder = $this->buildInputsBuilder(null); $set_values_modal = $this->buildSetCombinationValuesModal($inputs_builder) - ->withRequest($this->http->request()); + ->withRequest($this->environment->getHttpServices()->request()); $data = $set_values_modal->getData(); if ($data === null) { $this->modal = $this->initializeModal($set_values_modal) @@ -295,9 +291,9 @@ private function processSetCombinationValues(): self|Properties private function confirmDeleteCombination( Combination $affected_item ): InterruptiveModal { - return $this->ui_factory->modal()->interruptive( - $this->lng->txt('confirm'), - $this->lng->txt('delete_combination'), + return $this->environment->getUIFactory()->modal()->interruptive( + $this->environment->getLanguage()->txt('confirm'), + $this->environment->getLanguage()->txt('delete_combination'), $this->environment->withStepParameter( self::STEP_DELETE_COMBINATION )->getUrlBuilder() @@ -330,7 +326,7 @@ private function buildInputsBuilder( ): InputsBuilderSession { $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( + $this->environment->getRefinery()->custom()->transformation( function (?string $v) use ($combination): ?Section { $properties = $this->environment->getAnswerFormProperties(); if ($combination === null) { @@ -342,9 +338,9 @@ function (?string $v) use ($combination): ?Section { } return $combination?->buildPointsInputs( - $this->ui_factory->input()->field(), - $this->refinery, - $this->lng, + $this->environment->getUIFactory()->input()->field(), + $this->environment->getRefinery(), + $this->environment->getLanguage(), $this->combinations_factory, $properties ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index e9b259ab5dbe..9f49ed4d87ba 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -24,31 +24,27 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\Range; use ILIAS\Data\Order; -use ILIAS\Language\Language; -use ILIAS\UI\Component\Table\Factory as TableFactory; use ILIAS\UI\Component\Table\Data as DataTable; use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRowBuilder; -use Psr\Http\Message\ServerRequestInterface; class OverviewTable implements DataRetrieval { public function __construct( - private readonly TableFactory $table_factory, - private readonly Language $lng, - private readonly ServerRequestInterface $request, private readonly Environment $environment ) { } public function getTable(): DataTable { - return $this->table_factory->data( + return $this->environment->getUIFactory()->table()->data( $this, - $this->lng->txt('gaps'), + $this->environment->getLanguage()->txt('gaps'), $this->getColums() )->withActions($this->getActions()) - ->withRequest($this->request); + ->withRequest( + $this->environment->getHttpServices()->request() + ); } #[\Override] @@ -62,7 +58,7 @@ public function getRows( mixed $additional_parameters ): \Generator { yield from $this->environment->getAnswerFormProperties()->getGaps() - ->toTableRows($row_builder, $this->lng); + ->toTableRows($row_builder, $this->environment->getLanguage()); } #[\Override] @@ -71,44 +67,51 @@ public function getTotalRowCount( mixed $filter_data, mixed $additional_parameters ): ?int { - return $this->environment->getAnswerFormProperties()->getGaps()->getNumberOfGaps(); + return $this->environment->getAnswerFormProperties() + ->getGaps()->getNumberOfGaps(); } private function getColums(): array { - $f = $this->table_factory->column(); + $f = $this->environment->getUIFactory()->table()->column(); return [ - 'gap' => $f->text($this->lng->txt('title'))->withIsSortable(false), - 'type' => $f->text($this->lng->txt('cloze_type'))->withIsSortable(false), - 'answers_options_awarding_points' => $f - ->text($this->lng->txt('answer_options_awarding_points')) - ->withIsSortable(false), - 'available_points' => $f->number($this->lng->txt('available_points')) - ->withDecimals(4) - ->withIsSortable(false) + 'gap' => $f->text( + $this->environment->getLanguage()->txt('title') + )->withIsSortable(false), + 'type' => $f->text( + $this->environment->getLanguage()->txt('cloze_type') + )->withIsSortable(false), + 'answers_options_awarding_points' => $f->text( + $this->environment->getLanguage()->txt('answer_options_awarding_points') + )->withIsSortable(false), + 'available_points' => $f->number( + $this->environment->getLanguage()->txt('available_points') + )->withDecimals(4) + ->withIsSortable(false) ]; } private function getActions(): array { + $taf = $this->environment->getUIFactory()->table()->action(); return [ - 'edit_gaps' => $this->table_factory->action()->standard( - $this->lng->txt('edit_gaps'), + 'edit_gaps' => $taf->standard( + $this->environment->getLanguage()->txt('edit_gaps'), $this->environment ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_GAP_TYPES) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), - 'edit_answer_options' => $this->table_factory->action()->standard( - $this->lng->txt('edit_answer_options'), + 'edit_answer_options' => $taf->standard( + $this->environment->getLanguage()->txt('edit_answer_options'), $this->environment ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_ANSWER_OPTIONS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), - 'edit_points' => $this->table_factory->action()->standard( - $this->lng->txt('edit_available_points'), + 'edit_points' => $taf->standard( + $this->environment->getLanguage()->txt('edit_available_points'), $this->environment ->withStepParameter(EditGaps::STEP_JUMP_TO_ASSIGN_POINTS) ->getUrlBuilder(), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index e3efa80e6f82..5b4210702829 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -25,9 +25,6 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; -use ILIAS\HTTP\Services as HttpServices; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Table\DataRowBuilder; class Combinations @@ -100,18 +97,10 @@ public function hasMatchingCombinationForAnswerOptionIds( } public function getEditView( - UIFactory $ui_factory, - \ilToolbarGUI $toolbar, - Refinery $refinery, - Language $lng, - HttpServices $http + \ilToolbarGUI $toolbar ): EditCombinations { return new EditCombinations( - $ui_factory, $toolbar, - $refinery, - $lng, - $http, $this->combinations_factory ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php index 031189a70094..fe4a08f21609 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -25,10 +25,6 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Renderable; -use ILIAS\HTTP\Services as HTTPServices; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Factory as UIFactory; class EditCombinations { @@ -37,11 +33,7 @@ class EditCombinations private const string LANG_VAR_EDIT_COMBINATIONS = 'edit_combinations'; public function __construct( - private readonly UIFactory $ui_factory, private readonly \ilToolbarGUI $toolbar, - private readonly Refinery $refinery, - private readonly Language $lng, - private readonly HTTPServices $http, private readonly Factory $combinations_factory ) { } @@ -82,11 +74,7 @@ private function buildCombinationsOverview( Environment $environment ): CombinationsOverview { return new CombinationsOverview( - $this->ui_factory, $this->toolbar, - $this->refinery, - $this->lng, - $this->http, $environment, $this->combinations_factory ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index 5eba4b2ba5e5..e5a6a8ba8fe9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -28,11 +28,6 @@ use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Custom\Transformation as CustomTransformation; -use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Field\Group; class Factory { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index f3cb647494bb..ef0a748e6a43 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -191,15 +191,9 @@ public function getBasicPropertiesForListing( #[\Override] public function getOverviewTable( - TableFactory $table_factory, - Language $lng, - ServerRequestInterface $request, Environment $environment ): DataTable { return new OverviewTable( - $table_factory, - $lng, - $request, $environment )->getTable(); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index ffa5b42c4554..85bec54bc8d5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -31,14 +31,9 @@ use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; use ILIAS\Questions\Presentation\Layout\Renderable; -use ILIAS\HTTP\Services as HTTPServices; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; -use ILIAS\UI\URLBuilder; class Edit implements EditViewInterface { @@ -48,11 +43,7 @@ class Edit implements EditViewInterface private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; public function __construct( - private readonly Language $lng, - private readonly UIFactory $ui_factory, private readonly \ilToolbarGUI $toolbar, - private readonly Refinery $refinery, - private readonly HTTPServices $http, private readonly PropertiesFactory $properties_factory, private readonly ClozeTextFactory $cloze_text_factory, private readonly EditGaps $edit_gaps @@ -86,11 +77,7 @@ public function edit( $combinations = $environment->getAnswerFormProperties()->getCombinations(); if ($combinations->areCombinationsEnabled()) { $combinations->getEditView( - $this->ui_factory, - $this->toolbar, - $this->refinery, - $this->lng, - $this->http + $this->toolbar )->addCombinationsSubTab($environment); } @@ -126,21 +113,10 @@ public function other( return $environment ->getAnswerFormProperties() ->getCombinations()->getEditView( - $this->ui_factory, - $this->toolbar, - $this->refinery, - $this->lng, - $this->http + $this->toolbar )->show($environment); } - #[\Override] - public function getFinishEditingUrl( - Environment $environment - ): URLBuilder { - return $environment->getUrlBuilder(); - } - private function startEditing( Environment $environment ): EditForm { @@ -235,7 +211,7 @@ private function processBasicEditingForm( $environment, $inputs_builder, false - )->withRequest($this->http->request()); + )->withRequest($environment->getHttpServices()->request()); /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $data */ $data = $form->getData(); @@ -294,15 +270,15 @@ private function buildInputsBuilderForBasicInputs( $inputs_builder = $environment->getPresentationFactory() ->getSessionBasedInputsBuilder( $environment->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( + $environment->getRefinery()->custom()->transformation( fn(?string $carry): Section => $this->properties_factory ->fromCarry( $environment->getAnswerFormProperties(), $carry )->buildBasicEditingInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->refinery, + $environment->getLanguage(), + $environment->getUIFactory()->input()->field(), + $environment->getRefinery(), $this->properties_factory, $this->cloze_text_factory, $add_legacy_cloze_text_to_input @@ -324,16 +300,16 @@ private function buildRemovedGapsConfirmation( Environment $environment, array $removed_gaps ): InterruptiveModal { - return $this->ui_factory->modal()->interruptive( - $this->lng->txt('confirm'), - $this->lng->txt('confirm_remove_gaps'), + return $environment->getUIFactory()->modal()->interruptive( + $environment->getLanguage()->txt('confirm'), + $environment->getLanguage()->txt('confirm_remove_gaps'), $environment->withStepParameter( self::STEP_CONFIRMED_GAP_REMOVAL )->getUrlBuilder()->buildURI()->__toString() )->withAffectedItems( array_map( - fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() - ->interruptiveItem()->standard( + fn(Gap $v): InterruptiveItem => $environment->getUIFactory() + ->modal()->interruptiveItem()->standard( $v->getAnswerInputId()->toString(), $v->buildShortenedGapName() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php index 0f27a7ef3d55..d83f18bc458b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -25,13 +25,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\Factory as PresentationFactory; use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; -use ILIAS\HTTP\Services as HTTPServices; -use ILIAS\Language\Language; use ILIAS\Refinery\Custom\Transformation as CustomTransformation; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\URLBuilder; @@ -52,10 +47,6 @@ class EditGaps private ?string $start_step; public function __construct( - private readonly Language $lng, - private readonly UIFactory $ui_factory, - private readonly Refinery $refinery, - private readonly HTTPServices $http, private readonly PropertiesFactory $properties_factory, private readonly GapFactory $gap_factory ) { @@ -145,10 +136,7 @@ private function buildGapTypesFormWithCarry( Properties $properties ): EditForm { $inputs_builder = $this->buildInputsBuilderForTypesForm( - $environment->getPresentationFactory(), - $environment->getAnswerFormProperties(), - $environment->isInCreationContext(), - $environment->getTableRowIds() + $environment )->withCarry( $properties->toCarry() ); @@ -186,8 +174,8 @@ private function buildGapTypesForm( false )->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, + $environment->getUIFactory(), + $environment->getLanguage(), $properties->getGaps(), $properties->getLegacyClozeText() ) @@ -198,10 +186,7 @@ private function processGapTypesForm( Environment $environment ): EditForm|Properties { $inputs_builder_for_types = $this->buildInputsBuilderForTypesForm( - $environment->getPresentationFactory(), - $environment->getAnswerFormProperties(), - $environment->isInCreationContext(), - $environment->getTableRowIds() + $environment ); $properties = $inputs_builder_for_types->retrieveCarry( @@ -211,7 +196,7 @@ private function processGapTypesForm( $form = $this->buildGapTypesForm( $environment->withAnswerFormProperties($properties), $inputs_builder_for_types - )->withRequest($this->http->request()); + )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); if ($data === null) { @@ -223,31 +208,26 @@ private function processGapTypesForm( } private function buildInputsBuilderForTypesForm( - PresentationFactory $presentation_factory, - Properties $properties, - bool $is_in_creation_context, - array $table_row_ids + Environment $environment ): InputsBuilderSession { - return $presentation_factory->getSessionBasedInputsBuilder( - $properties->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( - function (?string $carry) use ( - $properties, - $is_in_creation_context, - $table_row_ids - ): Section { + return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( + $environment->getAnswerFormProperties()->getAnswerFormId()->toString(), + $environment->getRefinery()->custom()->transformation( + function (?string $carry) use ($environment): Section { $properties_from_carry = $this->properties_factory ->fromCarry( - $properties, + $environment->getAnswerFormProperties(), $carry ); return $properties_from_carry->getGaps()->buildGapsTypeInputs( - $this->lng, - $this->ui_factory->input()->field(), - $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $environment->getLanguage(), + $environment->getUIFactory()->input()->field(), + $this->gap_factory->getAvailableGapTypesOptionsArray( + $environment->getLanguage() + ), $properties_from_carry, - $is_in_creation_context, - $table_row_ids + $environment->isInCreationContext(), + $environment->getTableRowIds() ); } ) @@ -287,10 +267,8 @@ private function buildAnswerOptionsFormWithCarry( Properties $properties ): EditForm { $inputs_builder = $this->buildInputsBuilderForAnswerOptionsForm( - $environment->getPresentationFactory(), + $environment, $properties, - $environment->isInCreationContext(), - $environment->getTableRowIds() )->withCarry( $properties->toCarry() ); @@ -324,8 +302,8 @@ private function buildAnswerOptionsForm( false )->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, + $environment->getUIFactory(), + $environment->getLanguage(), $properties->getGaps(), $properties->getLegacyClozeText() ) @@ -336,16 +314,14 @@ private function processAnswerOptionsForm( Environment $environment ): EditForm|Properties { $inputs_builder_for_options = $this->buildInputsBuilderForAnswerOptionsForm( - $environment->getPresentationFactory(), - $environment->getAnswerFormProperties(), - $environment->isInCreationContext(), - $environment->getTableRowIds() + $environment, + $environment->getAnswerFormProperties() ); $form = $this->buildAnswerOptionsForm( $environment, $inputs_builder_for_options - )->withRequest($this->http->request()); + )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); if ($data === null) { @@ -357,18 +333,15 @@ private function processAnswerOptionsForm( } private function buildInputsBuilderForAnswerOptionsForm( - PresentationFactory $presentation_factory, - Properties $properties, - bool $is_in_creation_context, - array $table_row_ids + Environment $environment, + Properties $properties ): InputsBuilderSession { - return $presentation_factory->getSessionBasedInputsBuilder( + return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( $properties->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( + $environment->getRefinery()->custom()->transformation( function (?string $carry) use ( - $properties, - $is_in_creation_context, - $table_row_ids + $environment, + $properties ): Section { $properties_from_carry = $this->properties_factory ->fromCarry( @@ -377,11 +350,11 @@ function (?string $carry) use ( ); return $properties_from_carry->getGaps() ->buildAnswerOptionsInputs( - $this->lng, - $this->ui_factory->input()->field(), + $environment->getLanguage(), + $environment->getUIFactory()->input()->field(), $properties_from_carry, - $is_in_creation_context, - $table_row_ids + $environment->isInCreationContext(), + $environment->getTableRowIds() ); } ) @@ -407,10 +380,8 @@ private function buildAssignPointsFormWithCarry( Properties $properties ): EditForm { $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( - $environment->getPresentationFactory(), - $properties, - $environment->isInCreationContext(), - $environment->getTableRowIds() + $environment, + $properties )->withCarry( $properties->toCarry() ); @@ -443,8 +414,8 @@ private function buildAssignPointsForm( true )->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, + $environment->getUIFactory(), + $environment->getLanguage(), $properties->getGaps(), $properties->getLegacyClozeText() ) @@ -455,16 +426,14 @@ private function processAssignPointsForm( Environment $environment ): EditForm|Properties { $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( - $environment->getPresentationFactory(), - $environment->getAnswerFormProperties(), - $environment->isInCreationContext(), - $environment->getTableRowIds() + $environment, + $environment->getAnswerFormProperties() ); $form = $this->buildAssignPointsForm( $environment, $inputs_builder_for_points - )->withRequest($this->http->request()); + )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); if ($data === null) { @@ -476,18 +445,15 @@ private function processAssignPointsForm( } private function buildInputsBuilderForPointsForm( - PresentationFactory $presentation_factory, - Properties $properties, - bool $is_in_creation_context, - array $table_row_ids + Environment $environment, + Properties $properties ): InputsBuilderSession { - return $presentation_factory->getSessionBasedInputsBuilder( + return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( $properties->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( + $environment->getRefinery()->custom()->transformation( function (?string $carry) use ( - $properties, - $is_in_creation_context, - $table_row_ids + $environment, + $properties ): Section { $properties_from_carry = $this->properties_factory ->fromCarry( @@ -496,11 +462,11 @@ function (?string $carry) use ( ); return $properties_from_carry->getGaps() ->buildPointInputs( - $this->lng, - $this->ui_factory->input()->field(), + $environment->getLanguage(), + $environment->getUIFactory()->input()->field(), $properties_from_carry, - $is_in_creation_context, - $table_row_ids + $environment->isInCreationContext(), + $environment->getTableRowIds() ); } ) @@ -539,7 +505,7 @@ private function determineStartStepFromStep( private function buildRetrievePropertiesTransformation( Environment $environment ): CustomTransformation { - return $this->refinery->custom()->transformation( + return $environment->getRefinery()->custom()->transformation( fn(?string $carry): Properties => $this->properties_factory ->fromCarry( $environment->getAnswerFormProperties(), diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 4a3f3a8467c2..10416642efa6 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -23,11 +23,23 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\HTTP\Services as HTTPServices; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; interface Environment { + public function getHttpServices(): HTTPServices; + + public function getLanguage(): Language; + + public function getRefinery(): Refinery; + + public function getUIFactory(): UIFactory; + public function setEditAnswerFormBackTarget(): void; public function addEditAnswerFormSubTab( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index 31fcb85d3032..8b5b5bdbb5e6 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -30,6 +30,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -82,6 +83,30 @@ public function __construct( $this->acquireURLBuilderAndParameters($base_uri); } + #[\Override] + public function getHttpServices(): HTTPServices + { + return $this->http; + } + + #[\Override] + public function getLanguage(): Language + { + return $this->lng; + } + + #[\Override] + public function getRefinery(): Refinery + { + return $this->refinery; + } + + #[\Override] + public function getUIFactory(): UIFactory + { + return $this->presentation_factory->getUIFactory(); + } + #[\Override] public function setEditAnswerFormBackTarget(): void { diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index 95651e4c55c5..93e1fbc3d10e 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -22,19 +22,13 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Language\Language; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\URLBuilder; -use Psr\Http\Message\ServerRequestInterface; class EditOverview implements Renderable { public function __construct( - private readonly UIFactory $ui_factory, - private readonly Language $lng, - private readonly ServerRequestInterface $request, private readonly Environment $environment, private readonly URLBuilder $target_to_edit_basic_answer_form_properties ) { @@ -51,9 +45,6 @@ private function buildContent(): array return [ $this->buildBasicAnswerFormPanel(), $this->environment->getAnswerFormProperties()->getOverviewTable( - $this->ui_factory->table(), - $this->lng, - $this->request, $this->environment ) ]; @@ -62,22 +53,25 @@ private function buildContent(): array private function buildBasicAnswerFormPanel(): StandardPanel { $content = [ - $this->ui_factory->listing()->descriptive( - $this->environment->getAnswerFormProperties()->getBasicPropertiesForListing($this->lng) + $this->environment->getUIFactory()->listing()->descriptive( + $this->environment->getAnswerFormProperties() + ->getBasicPropertiesForListing( + $this->environment->getLanguage() + ) ) ]; if ($this->environment->getEditability() === Editability::Full) { - $content[] = $this->ui_factory->button()->standard( - $this->lng->txt('edit_basic_answer_form_properties'), + $content[] = $this->environment->getUIFactory()->button()->standard( + $this->environment->getLanguage()->txt('edit_basic_answer_form_properties'), $this->target_to_edit_basic_answer_form_properties ->buildURI() ->__toString() ); } - return $this->ui_factory->panel()->standard( - $this->lng->txt('basic_answer_form_properites'), + return $this->environment->getUIFactory()->panel()->standard( + $this->environment->getLanguage()->txt('basic_answer_form_properites'), $content ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index 4857dcc5e158..77dcb48782fa 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -45,9 +45,6 @@ public function getEditOverview( URLBuilder $target_to_edit_basic_answer_form_properties ): EditOverview { return new EditOverview( - $this->ui_factory, - $this->lng, - $this->http->request(), $environment, $target_to_edit_basic_answer_form_properties ); @@ -87,4 +84,9 @@ public function getSessionBasedInputsBuilder( $to_inputs ); } + + public function getUIFactory(): UIFactory + { + return $this->ui_factory; + } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index b2f5bde3687c..2137fa56b906 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -26,26 +26,20 @@ use ILIAS\Questions\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; -use ILIAS\Language\Language; use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRowBuilder; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; -use Psr\Http\Message\ServerRequestInterface; class QuestionsTable implements Renderable, DataRetrieval { public function __construct( - private readonly UIFactory $ui_factory, private readonly \ilUIService $ui_service, - private readonly Language $lng, - private readonly ServerRequestInterface $request, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, private readonly EnvironmentImplementation $environment ) { - $lng->loadLanguageModule('qpl'); + $environment->getLanguage()->loadLanguageModule('qpl'); } public function render( @@ -70,7 +64,6 @@ public function getRows( foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { yield $question->toTableRow( $row_builder, - $this->ui_factory, $environment_with_action ); } @@ -89,14 +82,14 @@ private function buildContent(): array { return [ $this->buildFilter($this->environment->getUrlBuilder()->buildURI()->__toString()), - $this->ui_factory->table()->data( + $this->environment->getUIFactory()->table()->data( $this, - $this->lng->txt('questions'), + $this->environment->getLanguage()->txt('questions'), $this->getColums(), )->withActions( $this->getActions() )->withRange(new Range(0, 20)) - ->withRequest($this->request) + ->withRequest($this->environment->getHttpServices()->request()) ]; } @@ -104,15 +97,20 @@ private function buildFilter( string $action ): Filter { $question_type_options = [ - '' => $this->lng->txt('filter_all_question_types') + '' => $this->environment->getLanguage()->txt('filter_all_question_types') ]; - $field_factory = $this->ui_factory->input()->field(); + $field_factory = $this->environment->getUIFactory()->input()->field(); $filter_inputs = [ - 'title' => $field_factory->text($this->lng->txt('title')), + 'title' => $field_factory->text( + $this->environment->getLanguage()->txt('title') + ), 'contains_type' => $field_factory->select( - $this->lng->txt('contains_type'), - $question_type_options + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + $this->environment->getLanguage()->txt('contains_type'), + $question_type_options + $this->answer_form_factory + ->getAnswerFormTypesArrayForSelect( + $this->environment->getLanguage() + ) ), ]; @@ -131,19 +129,21 @@ private function buildFilter( private function getColums(): array { - $f = $this->ui_factory->table()->column(); + $f = $this->environment->getUIFactory()->table()->column(); return [ - 'title' => $f->link($this->lng->txt('title')), - 'type' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true), + 'title' => $f->link($this->environment->getLanguage()->txt('title')), + 'type' => $f->text( + $this->environment->getLanguage()->txt('question_type') + )->withIsOptional(true, true), ]; } private function getActions(): array { return [ - 'delete' => $this->ui_factory->table()->action()->standard( - $this->lng->txt('delete'), + 'delete' => $this->environment->getUIFactory()->table()->action()->standard( + $this->environment->getLanguage()->txt('delete'), $this->environment->withActionParameter(Edit::ACTION_DELETE_QUESTIONS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 4ea4d170f9d6..52ad5057b3b8 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -251,25 +251,19 @@ public function editAnswerForm( self::ACTION_EDIT_CONTENT_FOR_REPETITION ); - $edit_view = $type->getEditView(); - - if ($environment->getAction() === self::ACTION_OTHER_ANSWER_FORM) { - $environment = $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM); - $next = $edit_view->other($environment); - } else { - $next = $edit_view->edit($environment); - } + [$environment, $next] = $this->doEditAction( + $environment, + $type_definition + ); - if (!($next instanceof AnswerFormProperties)) { + if ($next instanceof Renderable) { return $next; } - $this->questions_repository->update( - [$question->withAnswerFormProperties($next)] - ); + $this->storeEditResult($next); $this->ctrl->redirectToURL( - $edit_view->getFinishEditingUrl($environment)->buildURI()->__toString() + $environment->getUrlBuilder()->buildURI()->__toString() ); } @@ -279,18 +273,16 @@ private function createQuestion( $this->initializeEditMode($environment); $this->tabs_gui->setBackTarget( $this->lng->txt('cancel'), - $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + $environment->withDefaultStep()->getUrlBuilder() + ->buildURI() + ->__toString() ); $create = $this->questions_repository->getNew( $environment->getObjId() )->getEditView( - $this->lng, $this->configuration_repository, $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), $this->ctrl )->create( $environment->withActionParameter(self::ACTION_CREATE_QUESTION) @@ -326,12 +318,8 @@ private function editQuestion( ->withQuestionIdParameter($question_id); $edit = $question->getEditView( - $this->lng, $this->configuration_repository, $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), $this->ctrl )->edit( $environment_with_question_parameter @@ -402,10 +390,7 @@ private function showTable( ); return new QuestionsTable( - $this->ui_factory, $this->ui_services, - $this->lng, - $this->http->request(), $this->answer_form_factory, $this->questions_repository, $environment @@ -417,7 +402,8 @@ private function processCreateAnswerForm( QuestionImplementation $question, \ilPCAnswerForm $content_object ): EditForm { - $form = $this->buildCreateAnswerForm($environment)->withRequest($this->http->request()); + $form = $this->buildCreateAnswerForm($environment) + ->withRequest($this->http->request()); $data = $form->getData(); if ($data === null) { @@ -482,6 +468,56 @@ private function forwardCreateAnswerFormCmd( ); } + /** + * @return array{ + * \ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation, + * \ILIAS\Questions\AnswerForm\Definition + * } + */ + private function doEditAction( + EnvironmentImplementation $environment, + Definition $type_definition + ): array { + $action = $environment->getAction(); + if ($action === self::ACTION_EDIT_FEEDBACK) { + $environment = $environment->withActionParameter( + self::ACTION_OTHER_ANSWER_FORM + ); + return [ + $environment, + $type_definition->getCapability(Feedback::class)->edit($environment) + ]; + } + + + if ($action === self::ACTION_OTHER_ANSWER_FORM) { + $environment = $environment->withActionParameter( + self::ACTION_OTHER_ANSWER_FORM + ); + return [ + $environment, + $type_definition->getEditView()->other($environment) + ]; + } + + return [ + $environment, + $type_definition->getEditView()->edit($environment) + ]; + } + + private function storeEditResult( + Capability|AnswerFormProperties $result + ): void { + if ($result instanceof Capability) { + $this->questions_repository->storeCapability($result); + } + + $this->questions_repository->update( + [$question->withAnswerFormProperties($result)] + ); + } + private function initializeEditMode( EnvironmentImplementation $environment ): void { @@ -556,12 +592,8 @@ private function buildEditStartView( QuestionImplementation $question ): EditForm { return $question->getEditView( - $this->lng, $this->configuration_repository, $this->current_user, - $this->ui_factory, - $this->refinery, - $this->http->request(), $this->ctrl )->edit( $environment, @@ -586,7 +618,8 @@ private function buildCreateAnswerForm( $inputs + [ 'type' => $if->field()->select( $this->lng->txt('select_answer_form_type'), - $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + $this->answer_form_factory + ->getAnswerFormTypesArrayForSelect($this->lng) )->withRequired(true) ->withAdditionalTransformation( $this->refinery->custom()->transformation( diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 72223f67f1e4..9e7a5c4f23a1 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -33,14 +33,11 @@ use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; -use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Component\Table\DataRow; -use ILIAS\Refinery\Factory as Refinery; -use Psr\Http\Message\RequestInterface; class QuestionImplementation implements Question { @@ -260,21 +257,13 @@ public function withCreateMode( } public function getEditView( - Language $lng, ConfigurationRepository $configuration_repository, \ilObjUser $current_user, - UIFactory $ui_factory, - Refinery $refinery, - RequestInterface $request, \ilCtrl $ctrl ): Views\Edit { return new Views\Edit( - $lng, $configuration_repository, $current_user, - $ui_factory, - $refinery, - $request, $ctrl, $this ); @@ -303,13 +292,12 @@ public function toEditLink( public function toTableRow( DataRowBuilder $row_builder, - UIFactory $ui_factory, EnvironmentImplementation $environment ): DataRow { return $row_builder->buildDataRow( $this->id->toString(), [ - 'title' => $ui_factory->link()->standard( + 'title' => $environment->getUIFactory()->link()->standard( $this->title, $environment->withQuestionIdParameter( $this->id diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 34ad08af7d35..cda73a72a87f 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -26,26 +26,20 @@ use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Language\Language; -use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; -use Psr\Http\Message\RequestInterface; class Edit { private const string CMD_SAVE_QUESTION = 'sq'; public function __construct( - private readonly Language $lng, private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, - private readonly UIFactory $ui_factory, - private readonly Refinery $refinery, - private readonly RequestInterface $request, private readonly \ilCtrl $ctrl, private readonly QuestionImplementation $question ) { @@ -81,13 +75,18 @@ public function edit( private function buildBasicPropertiesCreateForm( EnvironmentImplementation $environment ): EditForm { - $ff = $this->ui_factory->input()->field(); + $ff = $environment->getUIFactory()->input()->field(); $inputs = [ 'question' => $ff->group( - $this->buildBasicPropertiesInputs() + $this->buildBasicPropertiesInputs( + $environment->getUIFactory()->input()->field(), + $environment->getLanguage() + ) )->withAdditionalTransformation( - $this->buildAddBasicPropertiesToQuestionTrafo() + $this->buildAddBasicPropertiesToQuestionTrafo( + $environment->getRefinery() + ) )->withValue( $this->buildBasicPropertiesBasicValuesArray() ) @@ -96,10 +95,10 @@ private function buildBasicPropertiesCreateForm( if ($this->configuration_repository->isCreateModeChangeableByUser()) { $inputs['create_mode'] = $this->configuration_repository->getInputForCreateMode( $ff, - $this->lng, - $this->refinery + $environment->getLanguage(), + $environment->getRefinery() )->withAdditionalTransformation( - $this->refinery->custom()->transformation( + $environment->getRefinery()->custom()->transformation( fn(string $v): CreateModes => CreateModes::tryFrom($v) ?? $this->configuration_repository->getGlobalCreateMode() ) @@ -109,7 +108,7 @@ private function buildBasicPropertiesCreateForm( return $environment->getPresentationFactory()->getEditForm( $ff->section( $inputs, - $this->lng->txt('edit_basic_form_properties') + $environment->getLanguage()->txt('edit_basic_form_properties') ), $environment ->withStepParameter(self::CMD_SAVE_QUESTION) @@ -124,7 +123,7 @@ private function processBasicPropertiesCreateForm( ): EditForm|Question { $form = $this->buildBasicPropertiesCreateForm( $environment - )->withRequest($this->request); + )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); return $data === null @@ -136,11 +135,16 @@ private function buildBasicPropertiesEditingForm( EnvironmentImplementation $environment ): EditForm { return $environment->getPresentationFactory()->getEditForm( - $this->ui_factory->input()->field()->section( - $this->buildBasicPropertiesInputs(), - $this->lng->txt('edit_basic_form_properties') + $environment->getUIFactory()->input()->field()->section( + $this->buildBasicPropertiesInputs( + $environment->getUIFactory()->input()->field(), + $environment->getLanguage() + ), + $environment->getLanguage()->txt('edit_basic_form_properties') )->withAdditionalTransformation( - $this->buildAddBasicPropertiesToQuestionTrafo() + $this->buildAddBasicPropertiesToQuestionTrafo( + $environment->getRefinery() + ) )->withValue( $this->buildBasicPropertiesBasicValuesArray() ), @@ -157,7 +161,7 @@ private function processBasicPropertiesEditingForm( ): EditForm|Question { $form = $this->buildBasicPropertiesEditingForm( $environment - )->withRequest($this->request); + )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); return $data === null @@ -165,26 +169,26 @@ private function processBasicPropertiesEditingForm( : $data; } - private function buildBasicPropertiesInputs(): array - { - $ff = $this->ui_factory->input()->field(); - + private function buildBasicPropertiesInputs( + FieldFactory $field_factory, + Language $lng + ): array { return [ - 'title' => $ff->text($this->lng->txt('title')) + 'title' => $field_factory->text($lng->txt('title')) ->withRequired(true), - 'author' => $ff->text($this->lng->txt('author')), - 'lifecycle' => $ff->select( - $this->lng->txt('qst_lifecycle'), + 'author' => $field_factory->text($lng->txt('author')), + 'lifecycle' => $field_factory->select( + $lng->txt('qst_lifecycle'), array_reduce( Lifecycle::cases(), - function (array $c, Lifecycle $v): array { - $c[$v->value] = $this->lng->txt("qst_lifecycle_{$v->value}"); + function (array $c, Lifecycle $v) use ($lng): array { + $c[$v->value] = $lng->txt("qst_lifecycle_{$v->value}"); return $c; }, [] ) )->withRequired(true), - 'remarks' => $ff->textarea($this->lng->txt('qst_remarks')) + 'remarks' => $field_factory->textarea($lng->txt('qst_remarks')) ]; } @@ -200,9 +204,10 @@ private function buildBasicPropertiesBasicValuesArray(): array ]; } - private function buildAddBasicPropertiesToQuestionTrafo(): Transformation - { - return $this->refinery->custom()->transformation( + private function buildAddBasicPropertiesToQuestionTrafo( + Refinery $refinery + ): Transformation { + return $refinery->custom()->transformation( function (array $vs): QuestionImplementation { $question = $this->question ->withTitle($vs['title']) @@ -224,15 +229,15 @@ private function buildPreviewPanel( Participant $participant_view ): StandardPanel { $environment->preserveParametersForPageEditorCmds(); - return $this->ui_factory->panel()->standard( - $this->lng->txt('preview'), - $this->ui_factory->legacy()->content( + return $environment->getUIFactory()->panel()->standard( + $environment->getLanguage()->txt('preview'), + $environment->getUIFactory()->legacy()->content( $participant_view->get($environment->getObjId()) ) )->withActions( - $this->ui_factory->dropdown()->standard([ - $this->ui_factory->link()->standard( - $this->lng->txt('edit'), + $environment->getUIFactory()->dropdown()->standard([ + $environment->getUIFactory()->link()->standard( + $environment->getLanguage()->txt('edit'), $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') ) ]) From 78cde277ebc87f3cdae9e0b54a312a08e7f1b58e Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 19 Feb 2026 18:30:44 +0100 Subject: [PATCH 075/108] Questions: Rename type to type_definition --- .../ILIAS/Questions/src/AnswerForm/Factory.php | 10 +++++----- .../ILIAS/Questions/src/Presentation/Views/Edit.php | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index b31721d78266..98e9545862a4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -92,22 +92,22 @@ public function getHashedClass(string $class): string public function buildTypeDefinitionFromSelectValue( string $value ): Definition { - $type = $this->available_answer_form_types[$value] ?? null; - if ($type === null) { + $type_definition = $this->available_answer_form_types[$value] ?? null; + if ($type_definition === null) { throw new InvalidArgumentException('This type of answer form does not exist.'); } - return $type; + return $type_definition; } public function getDefaultTypeGenericProperties( Uuid $question_id, - Definition $type, + Definition $type_definition, ?Uuid $answer_form_id = null ): TypeGenericProperties { return new TypeGenericProperties( $answer_form_id ?? $this->uuid_factory->uuid4(), $question_id, - $type + $type_definition ); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 52ad5057b3b8..8c93c06d2439 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -203,15 +203,15 @@ public function createAnswerForm( $answer_form_type_class_hash = $environment->getTypeClassHash(); if ($answer_form_type_class_hash !== '') { - $type = $this->answer_form_factory + $type_definition = $this->answer_form_factory ->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash); return $this->forwardCreateAnswerFormCmd( $environment->withAnswerFormProperties( - $type->buildProperties( + $type_definition->buildProperties( $this->answer_form_factory->getDefaultTypeGenericProperties( $question->getId(), - $type, + $type_definition, $environment->getAnswerFormId(), ), null @@ -219,7 +219,7 @@ public function createAnswerForm( )->withAnswerFormTypeHashParameter($answer_form_type_class_hash), $question, $content_object, - $type->getEditView() + $type_definition->getEditView() ); } @@ -238,8 +238,8 @@ public function editAnswerForm( int $obj_id, QuestionImplementation $question, AnswerFormProperties $answer_form_properties, - Definition $type - ): Async|Renderable { + Definition $type_definition + ): Renderable { $environment = $this->buildEnvironment( $base_uri, $obj_id From 51f01f53e781ce5b1245283b92cdc0fa9e0e6e75 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 25 Feb 2026 18:58:21 +0100 Subject: [PATCH 076/108] Questions: Start Implementing Feedback --- .../ILIAS/Questions/Legacy/LocalDIC.php | 10 +- .../AnswerForm/Capabilities/Capability.php | 11 +- .../Capabilities/{ => Feedback}/Feedback.php | 3 +- .../Capabilities/Feedback/Repository.php | 119 ++++++ .../Capabilities/{ => Marking}/Marking.php | 3 +- .../src/AnswerForm/Capabilities/Skills.php | 30 -- .../Questions/src/AnswerForm/Persistence.php | 2 +- .../Questions/src/AnswerForm/Properties.php | 2 - .../Cloze/Capabilities/Feedback.php | 40 +- .../Cloze/Capabilities/Marking.php | 27 +- .../Cloze/Capabilities/Skills.php | 40 -- .../Cloze/Layout/FeedbackOverview.php | 358 ++++++++++++++++++ .../src/AnswerFormTypes/Cloze/Persistence.php | 2 +- .../Cloze/Properties/Properties.php | 3 - .../Questions/src/Persistence/Repository.php | 110 ++++-- .../Questions/src/Presentation/Views/Edit.php | 12 +- .../src/Setup/OverarchingQuestionTables.php | 94 +++++ 17 files changed, 747 insertions(+), 119 deletions(-) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{ => Feedback}/Feedback.php (87%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{ => Marking}/Marking.php (87%) delete mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/FeedbackOverview.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index af3587cf5fe7..a0ce80cad607 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -21,11 +21,12 @@ namespace ILIAS\Questions\Legacy; use ILIAS\Questions\Administration\ConfigurationRepository; -use ILIAS\Questions\Persistence\Repository as QuestionsRepository; +use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Persistence\TableNameSpaceCore; -use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; +use ILIAS\Questions\Persistence\Repository as QuestionsRepository; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Questions\Units\Repository as UnitsRepository; @@ -177,9 +178,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Properties\Factory::class], $c[Cloze\Persistence::class], [ - Cloze\Capabilities\Marking::class => new Cloze\Capabilities\Marking(), - Cloze\Capabilities\Feedback::class => new Cloze\Capabilities\Feedback(), - Cloze\Capabilities\Skills::class => new Cloze\Capabilities\Skills() + Capabilities\Marking::class => new Cloze\Capabilities\Marking(), + Capabilities\Feedback::class => new Cloze\Capabilities\Feedback() ], $c[Cloze\Views\Edit::class], $c[Cloze\Views\Participant::class] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index 18f055fc1589..6111750a1804 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -20,7 +20,16 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; -interface Capability +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Persistence\Storable; + +interface Capability extends Storable { public function isConfigured(): bool; + + public function edit( + Environment $environment + ): self|Async|Renderable; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php similarity index 87% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php index 8da8607347e7..7dcfab474ae3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php @@ -18,8 +18,9 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities; +namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\Question\Response; interface Feedback extends Capability diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php new file mode 100644 index 000000000000..12a6505e028a --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php @@ -0,0 +1,119 @@ + + */ + public function getFor( + Uuid $answer_form_id + ): \Generator { + foreach ($query = new Query( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + $this->refinery + )->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + [] + ); + } + } + + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ + public function create( + array $questions + ): void { + $this->store( + array_map( + fn(QuestionImplementation $v): QuestionImplementation => $v + ->withPageId($this->buildQuestionPage($v->getParentObjId())), + $questions + ), + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Create + ) + ); + } + + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ + public function update( + array $questions + ): void { + $this->store( + $questions, + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Update + ) + ); + } + + public function delete( + array $questions + ): void { + array_reduce( + $questions, + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Delete + ) + )->run(); + + foreach ($questions as $question) { + (new \QstsQuestionPage($question->getPageId()))->delete(); + } + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php similarity index 87% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php index 73f28e03c4d0..ba81dce2eed4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php @@ -18,8 +18,9 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities; +namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\Response\Response; interface Marking extends Capability diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php deleted file mode 100644 index 4273cd894f10..000000000000 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Skills.php +++ /dev/null @@ -1,30 +0,0 @@ -getStep()) { + self::STEP_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), + self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => + $this->addLegacyTextToBasicProperties($environment), + self::STEP_CONFIRMED_GAP_REMOVAL, + self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + $environment->withPreservedTableRowIdsParameter() + ), + default => $this->forwardCmdToEditGaps( + $environment->withPreservedTableRowIdsParameter(), + $step + ) + }; + } + + #[\Override] + public function toStorage( + Manipulate $manipulate + ): Manipulate { + return $manipulate; + } + + #[\Override] + public function toDelete( + Manipulate $manipulate + ): Manipulate { + return $manipulate; + } + #[\Override] public function getGeneralFeedback( Response $response diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php index 947979f5abfc..13e7bfd1d102 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php @@ -20,7 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Marking as MarkingInterface; +use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking as MarkingInterface; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Questions\Response\Response; class Marking implements MarkingInterface @@ -31,6 +36,26 @@ public function isConfigured(): bool return false; } + #[\Override] + public function edit( + Environment $environment + ): self|Async|EditForm|EditOverview { + return false; + } + + #[\Override] + public function toStorage( + Manipulate $manipulate + ): Manipulate { + return $manipulate; + } + + #[\Override] + public function toDelete( + Manipulate $manipulate + ): Manipulate { + return $manipulate; + } #[\Override] public function addAchievedPointsToResponse( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php deleted file mode 100644 index eb7380fb3d32..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Skills.php +++ /dev/null @@ -1,40 +0,0 @@ -initializeModal($this->buildSetCombinationGapsModal()), + $this->buildTable() + ]; + if ($this->modal !== null) { + $content[] = $this->modal; + } + return $ui_renderer->render($content); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + yield from $this->environment->getAnswerFormProperties() + ->getCombinations()->toTableRows($this->lng, $row_builder); + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return $this->environment->getAnswerFormProperties() + ->getCombinations()->getNumberOfCombinations(); + } + + public function doAction(): Async|self|Properties + { + return match ($this->environment->getStep()) { + self::STEP_ADD_FEEDBACK => $this->processSetCombinationGapsModal(), + self::STEP_DELETE_FEEDBACK => $this->deleteCombination(), + self::STEP_SAVE => $this->processSetCombinationValues(), + default => $this->buildAction() + }; + } + + private function buildTable(): DataTable + { + return $this->ui_factory->table()->data( + $this, + $this->lng->txt('combinations'), + $this->getColumns() + )->withActions($this->getActions()) + ->withRequest($this->http->request()); + } + + private function initializeModal( + RoundTripModal $modal + ): RoundTripModal { + $this->toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('add_combination'), + $modal->getShowSignal() + ) + ); + return $modal; + } + + private function getColumns(): array + { + $cf = $this->ui_factory->table()->column(); + return [ + 'gaps' => $cf->text($this->lng->txt('gaps')), + 'values' => $cf->text($this->lng->txt('values')), + 'available_points' => $cf->number($this->lng->txt('points'))->withDecimals(2) + ]; + } + + private function getActions(): array + { + $af = $this->ui_factory->table()->action(); + return [ + $af->single( + $this->lng->txt('edit'), + $this->environment + ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + )->withAsync(true), + $af->single( + $this->lng->txt('delete'), + $this->environment + ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + )->withAsync(true) + ]; + } + + private function buildAction(): Async + { + $affected_item = $this->environment->getAnswerFormProperties() + ->getCombinations()->getCombinationById( + $this->environment->getTableRowIds()[0] + ); + + if ($affected_item === null) { + return $this->buildNoItemsSelectedAsync(); + } + + return $this->environment->getPresentationFactory()->getAsync( + match ($this->environment->getStep()) { + self::STEP_JUMP_TO_SET_COMBINATION_VALUES => + $this->buildSetCombinationValuesModal( + $this->buildInputsBuilder($affected_item) + ), + self::STEP_CONFIRM_DELETE_COMBINATION => + $this->confirmDeleteCombination($affected_item) + } + ); + } + + private function buildNoItemsSelectedAsync(): Async + { + return new Async( + $this->http, + $this->ui_factory->messageBox()->failure('no_combination_selected') + ); + } + + private function buildSetCombinationGapsModal(): RoundTripModal + { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('add_combination'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps, + $properties->getLegacyClozeText() + ), + [ + 'combination' => $gaps->buildGapsMultiSelect( + $this->lng->txt('select_gaps_for_combinations'), + $this->ui_factory->input()->field() + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->constraint( + fn(array $v): bool => count($v) > 1, + $this->lng->txt('combination_needs_more_than_one') + ) + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $v): Combination => $this->combinations_factory + ->buildNewCombination($gaps, $v) + ) + ) + ], + $this->environment + ->withStepParameter(self::STEP_ADD_FEEDBACK) + ->getUrlBuilder() + ->buildURI() + ->__toString() + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processSetCombinationGapsModal(): self + { + $clone = clone $this; + + $set_gaps_modal = $clone->buildSetCombinationGapsModal() + ->withRequest($clone->http->request()); + $data = $set_gaps_modal->getData(); + + if ($data === null) { + $clone->modal = $set_gaps_modal->withOnLoad($set_gaps_modal->getShowSignal()); + return $clone; + } + + $set_values_modal = $clone->buildSetCombinationValuesModal( + $this->buildInputsBuilder($data['combination']) + ); + $clone->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); + return $clone; + } + + private function buildSetCombinationValuesModal( + InputsBuilder $inputs_builder + ): RoundTripModal { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('edit'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps, + $properties->getLegacyClozeText() + ), + [ + 'values_awarding_points' => $inputs_builder->getInputs() + ], + $this->environment->withStepParameter(self::STEP_SAVE) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + private function processSetCombinationValues(): self|Properties + { + $inputs_builder = $this->buildInputsBuilder(null); + $set_values_modal = $this->buildSetCombinationValuesModal($inputs_builder) + ->withRequest($this->http->request()); + $data = $set_values_modal->getData(); + if ($data === null) { + $this->modal = $this->initializeModal($set_values_modal) + ->withOnLoad($set_values_modal->getShowSignal()); + $inputs_builder->persistCarry(); + return $this; + } + + return $data['values_awarding_points']; + } + + private function confirmDeleteCombination( + Combination $affected_item + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('delete_combination'), + $this->environment->withStepParameter( + self::STEP_DELETE_FEEDBACK + )->getUrlBuilder() + ->withParameter( + $this->environment->getTableRowIdToken(), + [$affected_item->getId()->toString()] + )->buildURI()->__toString() + ); + } + + private function deleteCombination(): Properties + { + $combination_identifier = $this->environment->getTableRowIds(); + if ($combination_identifier === []) { + return $this->environment->getAnswerFormProperties(); + } + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ + $answer_form_properties = $this->environment->getAnswerFormProperties(); + + return $answer_form_properties->withCombinations( + $answer_form_properties->getCombinations()->withoutCombination( + $combination_identifier[0] + ) + ); + } + + private function buildInputsBuilder( + ?Combination $combination, + ): InputsBuilderSession { + $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( + $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $v) use ($combination): ?Section { + $properties = $this->environment->getAnswerFormProperties(); + if ($combination === null) { + $combination = $this->combinations_factory + ->buildCombinationFromCarryValue( + $v, + $properties + ); + } + + return $combination?->buildPointsInputs( + $this->ui_factory->input()->field(), + $this->refinery, + $this->lng, + $this->combinations_factory, + $properties + ); + } + ) + ); + + if ($combination === null) { + return $builder; + } + + $builder_with_string = $builder->withCarry($combination->buildCarryString()); + $builder_with_string->persistCarry(); + return $builder_with_string; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php index 3c3e6072cd9e..1eff9def005e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -209,7 +209,7 @@ public function getForeignKeyColumn( } #[\Override] - public function completeQuery( + public function completeQuestionsQuery( Query $query, Column $answer_form_id_column ): Query { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index ef0a748e6a43..5ab16b51a21c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -30,7 +30,6 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; @@ -45,9 +44,7 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Table\Factory as TableFactory; use ILIAS\UI\Component\Table\Data as DataTable; -use Psr\Http\Message\ServerRequestInterface; class Properties implements PropertiesInterface { diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php index f395ab96b436..51786b44cb3d 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -55,12 +55,9 @@ public function getNew( */ public function getQuestionDataOnlyForAllQuestions(): \Generator { - foreach ($query = new Query( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, - $this->refinery - )->loadNextRecord() as $query_with_record) { + foreach ($this->buildQuestionsQuery()->loadNextRecord( + $this->buildGroupByColumn() + ) as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, [] @@ -74,12 +71,7 @@ public function getQuestionDataOnlyForAllQuestions(): \Generator public function getQuestionDataOnlyForQuestionIds( array $question_ids ): \Generator { - foreach ((new Query( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, - $this->refinery - )->withAdditionalWhere( + foreach ($this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( CoreTables::Questions->getIdColumn( $this->persistence_factory @@ -93,7 +85,7 @@ public function getQuestionDataOnlyForQuestionIds( ), Operator::In ) - ))->loadNextRecord() as $query_with_record) { + )->loadNextRecord($this->buildGroupByColumn()) as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, [] @@ -105,12 +97,7 @@ public function getForQuestionId( Uuid $question_id ): ?QuestionImplementation { return $this->getForBaseQuery( - (new Query( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, - $this->refinery - ))->withAdditionalWhere( + $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( CoreTables::Questions->getIdColumn( $this->persistence_factory @@ -135,12 +122,7 @@ public function getForQuestionIds( array $question_ids ): \Generator { yield from $this->getForBaseQuery( - (new Query( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, - $this->refinery - ))->withAdditionalWhere( + $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( CoreTables::Questions->getIdColumn( $this->persistence_factory @@ -226,7 +208,7 @@ private function getForBaseQuery( ): \Generator { $query_with_answer_forms = array_reduce( $this->getAnswerFormTypesForQuestionIds($question_ids), - fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( + fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuestionsQuery( $c, CoreTables::AnswerForms->getIdColumn( $this->persistence_factory @@ -235,7 +217,9 @@ private function getForBaseQuery( $query ); - foreach ($query_with_answer_forms->loadNextRecord() as $query_with_record) { + foreach ($query_with_answer_forms->loadNextRecord( + $this->buildGroupByColumn() + ) as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, $this->retrieveAnswerFormsFromQuery($query_with_record) @@ -361,6 +345,78 @@ private function store( )->run(); } + private function buildQuestionsQuery(): Query + { + $query = new Query( + $this->db, + $this->refinery, + $this->persistence_factory, + $this->answer_form_factory + ); + + $questions_linking_table_definition = CoreTables::Linking; + $questions_table_definition = CoreTables::Questions; + $answer_form_table_definition = CoreTables::AnswerForms; + $questions_id_column = $questions_table_definition->getIdColumn( + $this->persistence_factory + ); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $questions_linking_table_definition->getColumns( + $this->persistence_factory + ) + ) + )->withAdditionalSelect( + $this->select[] = $this->persistence_factory->select( + $questions_table_definition->getColumns( + $this->persistence_factory + ) + ) + )->withAdditionalSelect( + $this->select[] = $this->persistence_factory->select( + $answer_form_table_definition->getColumns( + $this->persistence_factory + ) + ) + )->withAdditionalJoin( + $this->joins[] = $this->persistence_factory->join( + $questions_linking_table_definition->getIdColumn( + $this->persistence_factory + ), + $questions_table_definition->getIdColumn( + $this->persistence_factory + ), + JoinType::Inner + ) + )->withAdditionalJoin( + $this->joins[] = $this->persistence_factory->join( + $questions_id_column, + $answer_form_table_definition->getForeignKeyColumn( + $this->persistence_factory + ), + JoinType::Left + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $questions_id_column + ) + )->withAdditionalOrder( + $this->order[] = $this->persistence_factory->order( + $answer_form_table_definition->getIdColumn( + $this->persistence_factory + ) + ) + ); + } + + private function buildGroupByColumn(): Column + { + return CoreTables::Questions->getIdColumn( + $this->persistence_factory + ); + } + private function buildAvailableUuid(): Uuid { do { diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 8c93c06d2439..7a9446077eda 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,6 +20,12 @@ namespace ILIAS\Questions\Presentation\Views; +use ILIAS\Questions\AnswerForm\Capabilities\Capability; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback; +use ILIAS\Questions\AnswerForm\Definition; +use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; @@ -30,10 +36,6 @@ use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; -use ILIAS\Questions\AnswerForm\Definition; -use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; -use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\UserSettings\CreateModes; @@ -452,7 +454,7 @@ private function forwardCreateAnswerFormCmd( ? $create->withAdditionalAction( $environment->buildURLBuilderTokenForCreateAndNew(), '1', - $this->lng->txt('create_and_new') + $this->lng->txt('save_and_new') ) : $create; } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 57775096067b..138ba06ace37 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -241,4 +241,98 @@ public function step_5(): void ); } } + + public function step_6(): void + { + $table_name = CoreTables::FeedbackGeneric->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'feedback_best_response' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'feedback_best_response_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'feedback_other_response' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'feedback_other_response_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields( + $table_name, + ['answer_form_id'] + )) { + $this->db->addPrimaryKey( + $table_name, + ['answer_form_id'] + ); + } + } + + public function step_7(): void + { + $table_name = CoreTables::FeedbackSpecific->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'parent_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'condition' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 16, + 'notnull' => false + ], + 'feedback_best_response_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'feedback' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'feedback_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields( + $table_name, + [ + 'answer_form_id', + 'parent_id', + 'condition' + ] + )) { + $this->db->addPrimaryKey( + $table_name, + [ + 'answer_form_id', + 'parent_id', + 'condition' + ], + ); + } + } } From 6e65e996c359f2a3906918996db95d1c3ce69d9c Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 25 Feb 2026 19:01:26 +0100 Subject: [PATCH 077/108] Questions: Move to Generic Persistence --- .../ILIAS/Questions/Legacy/LocalDIC.php | 39 ++- .../PageEditor/class.ilPCAnswerForm.php | 2 +- components/ILIAS/Questions/Questions.php | 82 ++++-- .../Feedback/FeedbackTableTypes.php} | 18 +- .../Capabilities/Feedback/Repository.php | 11 +- .../Questions/src/AnswerForm/Definition.php | 4 +- .../src/AnswerForm/Migration/Migration.php | 2 + .../AnswerForm/Migration/MigrationInsert.php | 11 +- .../AnswerFormGenericTableDefinitions.php | 137 ++++++++++ .../AnswerFormGenericTableTypes.php | 28 +++ .../AnswerFormSpecificTableTypes.php | 32 +++ .../src/AnswerForm/TypeGenericProperties.php | 77 ++++-- .../Cloze/Capabilities/Feedback.php | 3 + .../Cloze/Capabilities/Marking.php | 3 + .../src/AnswerFormTypes/Cloze/Definition.php | 8 +- .../Migration/BasicMigrationFunctions.php | 25 +- .../Cloze/Migration/MigrationCloze.php | 41 +-- .../Cloze/Migration/MigrationLongMenu.php | 20 +- .../Cloze/Migration/MigrationNumeric.php | 20 +- .../Cloze/Migration/MigrationTextSubset.php | 21 +- .../Properties/Combinations/Combination.php | 62 +++-- .../Properties/Combinations/Combinations.php | 22 +- .../Cloze/Properties/Combinations/Factory.php | 37 ++- .../Properties/Combinations/MatchingValue.php | 15 +- .../Cloze/Properties/Factory.php | 20 +- .../Gaps/AnswerOptions/AnswerOptions.php | 21 +- .../Properties/Gaps/AnswerOptions/Factory.php | 15 +- .../Cloze/Properties/Gaps/Factory.php | 21 +- .../Cloze/Properties/Gaps/Gap.php | 16 +- .../Cloze/Properties/Gaps/Gaps.php | 65 +++-- .../Cloze/Properties/Properties.php | 80 +++--- .../{Persistence.php => TableDefinitions.php} | 208 +++++++-------- components/ILIAS/Questions/src/Collector.php | 2 +- .../Questions/src/Persistence/CoreTables.php | 143 ----------- .../Questions/src/Persistence/Factory.php | 8 +- .../Questions/src/Persistence/Manipulate.php | 29 +-- .../ILIAS/Questions/src/Persistence/Query.php | 138 ++++------ .../Questions/src/Persistence/Storable.php | 2 + .../ILIAS/Questions/src/Persistence/Table.php | 14 +- .../TableDefinitions.php} | 22 +- .../src/Persistence/TableNameBuilder.php | 43 ++-- ...bleNameSpace.php => TableSubNameSpace.php} | 23 +- .../Questions/src/Persistence/TableTypes.php | 25 +- .../Presentation/Layout/QuestionsTable.php | 2 +- .../Questions/src/Presentation/Views/Edit.php | 2 +- .../{ => Question}/Persistence/Repository.php | 195 ++++++++------- .../Question/Persistence/TableDefinitions.php | 202 +++++++++++++++ .../src/Question/Persistence/TableTypes.php | 31 +++ .../src/Question/QuestionImplementation.php | 236 +++++++++++++----- .../ILIAS/Questions/src/Setup/Agent.php | 37 ++- .../src/Setup/ClozeQuestionTables.php | 32 ++- .../src/Setup/OverarchingQuestionTables.php | 38 ++- .../src/Setup/QuestionsMigration.php | 47 +++- .../src/Setup/SetupTableNameBuilder.php | 35 +++ 54 files changed, 1503 insertions(+), 969 deletions(-) rename components/ILIAS/Questions/src/{Persistence/TableNameSpaceCore.php => AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php} (64%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableTypes.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormSpecificTableTypes.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/{Persistence.php => TableDefinitions.php} (56%) delete mode 100644 components/ILIAS/Questions/src/Persistence/CoreTables.php rename components/ILIAS/Questions/src/{AnswerForm/Persistence.php => Persistence/TableDefinitions.php} (67%) rename components/ILIAS/Questions/src/Persistence/{TableNameSpace.php => TableSubNameSpace.php} (56%) rename components/ILIAS/Questions/src/{ => Question}/Persistence/Repository.php (73%) create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/TableTypes.php create mode 100644 components/ILIAS/Questions/src/Setup/SetupTableNameBuilder.php diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index a0ce80cad607..2bb86b72d955 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -23,10 +23,12 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Capabilities; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze; -use ILIAS\Questions\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\Repository as QuestionsRepository; +use ILIAS\Questions\Question\Persistence\Repository as QuestionsRepository; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Questions\Units\Repository as UnitsRepository; @@ -66,6 +68,16 @@ protected static function buildDIC(ILIASContainer $DIC): self ), new \ilSetting('questions') ); + $dic[PersistenceFactory::class] = static fn($c): PersistenceFactory + => new PersistenceFactory(); + $dic[QuestionTableDefinitions::class] = static fn($c): QuestionTableDefinitions + => new QuestionTableDefinitions( + $c[PersistenceFactory::class] + ); + $dic[AnswerFormGenericTableDefinitions::class] = static fn($c): AnswerFormGenericTableDefinitions + => new AnswerFormGenericTableDefinitions( + $c[PersistenceFactory::class] + ); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( $c[UuidFactory::class], @@ -78,7 +90,9 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilDB'], $DIC['refinery'], $c[UuidFactory::class], - new PersistenceFactory(), + $c[PersistenceFactory::class], + $c[QuestionTableDefinitions::class], + $c[AnswerFormGenericTableDefinitions::class], $c[AnswerFormFactory::class] ); $dic[LayoutFactory::class] = static fn($c): LayoutFactory => @@ -149,13 +163,18 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory => new Cloze\Properties\Factory( + $c[PersistenceFactory::class], $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Properties\Gaps\Factory::class], $c[Cloze\Properties\Combinations\Factory::class] ); - $dic[Cloze\Persistence::class] = static fn($c): Cloze\Persistence - => new Cloze\Persistence( - new TableNameSpaceCore('cloze') + $dic[Cloze\TableDefinitions::class] = static fn($c): Cloze\TableDefinitions + => new Cloze\TableDefinitions( + $c[PersistenceFactory::class], + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) ); $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps => new Cloze\Views\EditGaps( @@ -176,7 +195,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( $c[Cloze\Properties\Factory::class], - $c[Cloze\Persistence::class], + $c[Cloze\TableDefinitions::class], [ Capabilities\Marking::class => new Cloze\Capabilities\Marking(), Capabilities\Feedback::class => new Cloze\Capabilities\Feedback() @@ -185,7 +204,11 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Views\Participant::class] ); $dic[Cloze\Properties\Combinations\Factory::class] = static fn($c): Cloze\Properties\Combinations\Factory - => new Cloze\Properties\Combinations\Factory($c[UuidFactory::class]); + => new Cloze\Properties\Combinations\Factory( + $c[UuidFactory::class], + $c[PersistenceFactory::class], + $c[Cloze\TableDefinitions::class] + ); return $dic; } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 9d19474ebeea..0f42da2ed931 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -20,7 +20,7 @@ use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Legacy\LocalDIC; -use ILIAS\Questions\Persistence\Repository; +use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 00a64c0ca053..e14a5cedb57c 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -26,9 +26,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationLongMenu; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationNumeric; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationTextSubset; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Setup\Agent; use ILIAS\Setup\Agent as AgentInterface; @@ -48,31 +49,64 @@ public function init( $contribute[AgentInterface::class] = static fn() => new Agent( $internal[PersistenceFactory::class], + new TableNameBuilder( + 'qsts', + null + ), $seek[AnswerFormMigration::class] ); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze( - $internal[Persistence::class], - $internal[\EvalMath::class] - ); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu( - $internal[Persistence::class] - ); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric( - $internal[Persistence::class], - $internal[\EvalMath::class] - ); - $contribute[AnswerFormMigration::class] = static fn() => new MigrationTextSubset( - $internal[Persistence::class] - ); - $contribute[Component\Resource\PublicAsset::class] = fn() => - new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); - $contribute[User\Settings\UserSettings::class] = fn() => - new Questions\UserSettings\Settings(); + $contribute[AnswerFormMigration::class] = static fn() + => new MigrationCloze( + new TableDefinitions( + $internal[PersistenceFactory::class], + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) + ), + $internal[\EvalMath::class] + ); + $contribute[AnswerFormMigration::class] = static fn() + => new MigrationLongMenu( + new TableDefinitions( + $internal[PersistenceFactory::class], + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) + ) + ); + $contribute[AnswerFormMigration::class] = static fn() + => new MigrationNumeric( + new TableDefinitions( + $internal[PersistenceFactory::class], + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) + ), + $internal[\EvalMath::class] + ); + $contribute[AnswerFormMigration::class] = static fn() + => new MigrationTextSubset( + new TableDefinitions( + $internal[PersistenceFactory::class], + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) + ) + ); + $contribute[Component\Resource\PublicAsset::class] = fn() + => new Component\Resource\ComponentJS( + $this, + 'js/dist/ParticipantViewLongMenu.js' + ); + $contribute[User\Settings\UserSettings::class] = fn() + => new Questions\UserSettings\Settings(); - $internal[Persistence::class] = static fn() => new Persistence( - new TableNameSpaceCore('cloze') - ); - $internal[PersistenceFactory::class] = static fn() => new PersistenceFactory(); + $internal[PersistenceFactory::class] = static fn() + => new PersistenceFactory(); $internal[\EvalMath::class] = static fn() => new \EvalMath(); } } diff --git a/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php similarity index 64% rename from components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php index aa7815dabc80..9c4e3b20387d 100644 --- a/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php @@ -18,18 +18,12 @@ declare(strict_types=1); -namespace ILIAS\Questions\Persistence; +namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -class TableNameSpaceCore extends TableNameSpace -{ - public function __construct( - private readonly string $answer_form_id - ) { - } +use ILIAS\Questions\Persistence\TableTypes; - #[\Override] - public function getTypeSpecificTableNamePart(): string - { - return $this->answer_form_id; - } +enum FeedbackTableTypes: string implements TableTypes +{ + case FeedbackGeneric = 'feedback_generic'; + case FeedbackSpecific = 'feedback_specific'; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php index 12a6505e028a..0d74daa5e5a9 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php @@ -21,9 +21,8 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; -use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\Order as DataOrder; use ILIAS\Data\Range as DataRange; @@ -49,9 +48,8 @@ public function getFor( ): \Generator { foreach ($query = new Query( $this->db, - $this->persistence_factory, - $this->answer_form_factory, - $this->refinery + $this->refinery, + QuestionRepository::COMPONENT_NAMESPACE )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, @@ -74,7 +72,6 @@ public function create( ), new Manipulate( $this->db, - $this->persistence_factory, $this->answer_form_factory, ManipulationType::Create ) @@ -91,7 +88,6 @@ public function update( $questions, new Manipulate( $this->db, - $this->persistence_factory, $this->answer_form_factory, ManipulationType::Update ) @@ -106,7 +102,6 @@ public function delete( fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), new Manipulate( $this->db, - $this->persistence_factory, $this->answer_form_factory, ManipulationType::Delete ) diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 865ab114013f..af008afeb8fe 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -23,8 +23,8 @@ use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\AnswerForm\Views\Participant; -use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableDefinitions; use ILIAS\Language\Language; interface Definition @@ -35,7 +35,7 @@ public function buildProperties( TypeGenericProperties $type_generic_properties, ?Query $query ): Properties; - public function getPersistence(): Persistence; + public function getTableDefinitions(): TableDefinitions; public function hasCapability( string $capability_class_name diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php index 326bb0349021..fb6c8d20d2ff 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerForm\Migration; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -37,6 +38,7 @@ public function getTableNameSpace(): TableNameSpace; public function completeMigrationInsert( Environment $environment, + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index bf2ee1afeb80..abbc76d2a9ad 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -20,7 +20,9 @@ namespace ILIAS\Questions\AnswerForm\Migration; -use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; +use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -41,6 +43,8 @@ public function __construct( private readonly IOWrapper $io, private readonly UuidFactory $uuid_factory, private readonly PersistenceFactory $persistence_factory, + private readonly QuestionTableDefinitions $question_table_definitions, + private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, private readonly TableNameBuilder $table_name_builder, private array $inserts, private readonly int $old_question_id, @@ -167,8 +171,9 @@ function (\ilDBInterface $db) use ($manipulates): void { private function buildCoreAnswerFormInsertStatement(): Insert { return $this->persistence_factory->insert( - CoreTables::AnswerForms->getColumns( - $this->persistence_factory + $this->answer_form_generic_table_definitions->getColumns( + $this->table_name_builder, + AnswerFormGenericTableTypes::AnswerForms ), [ $this->persistence_factory->value( diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php new file mode 100644 index 000000000000..323a2951ac21 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php @@ -0,0 +1,137 @@ + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_name_builder, + $table_type + ), + $v + ), + array_values( + array_filter( + self::ANSWER_FORM_TABLE_COLUMNS, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + #[\Override] + public function getIdColumn( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + string $sub_table_identifier = '' + ): Column { + return $this->persistence_factory->column( + $this->persistence_factory->table( + $table_name_builder, + $table_type + ), + self::ANSWER_FORM_TABLE_ID_COLUMN + ); + } + + #[\Override] + public function getForeignKeyColumn( + TableNameBuilder $table_name_builder, + TableTypes $table_type, + string $sub_table_identifier = '' + ): Column { + return $this->persistence_factory->column( + $this->persistence_factory->table( + $table_name_builder, + $table_type + ), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ); + } + + #[\Override] + public function completeQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_name_builder = $query->getTableNameBuilder(null); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + AnswerFormGenericTableTypes::AnswerForms + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $base_table_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + AnswerFormGenericTableTypes::AnswerForms + ), + JoinType::Left + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableTypes.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableTypes.php new file mode 100644 index 000000000000..0e713c3128af --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableTypes.php @@ -0,0 +1,28 @@ +additional_text_legacy; } - #[\Override] public function toStorage( + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_definitions, Manipulate $manipulate ): Manipulate { if ($this->definition === null) { @@ -94,30 +94,46 @@ public function toStorage( 'You cannot save a Answer Form without a Type!' ); } + + $table_name_builder = $manipulate->getTableNameBuilder(null); + return $manipulate->withAdditionalStatement( $manipulate->getManipulationType() === ManipulationType::Create - ? $this->buildInsertStatement($manipulate->getPersistenceFactory()) - : $this->buildUpdateStatement($manipulate->getPersistenceFactory()) + ? $this->buildInsertStatement( + $persistence_factory, + $answer_form_generic_definitions, + $table_name_builder + ) : $this->buildUpdateStatement( + $persistence_factory, + $answer_form_generic_definitions, + $table_name_builder + ) ); } - #[\Override] public function toDelete( + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, Manipulate $manipulate ): Manipulate { - $answer_form_table_definition = CoreTables::AnswerForms; + $table_name_builder = $manipulate->getTableNameBuilder( + $answer_form_generic_table_definitions->getTableSubNameSpace() + ); + $table_type = AnswerFormGenericTableTypes::AnswerForms; return $manipulate->withAdditionalStatement( - $manipulate->getPersistenceFactory()->delete( - $answer_form_table_definition->getTable( - $manipulate->getPersistenceFactory() + $persistence_factory->delete( + $persistence_factory->table( + $table_name_builder, + $table_type ), [ - $manipulate->getPersistenceFactory()->where( - $answer_form_table_definition->getIdColumn( - $manipulate->getPersistenceFactory() + $persistence_factory->where( + $answer_form_generic_table_definitions->getIdColumn( + $table_name_builder, + $table_type ), - $manipulate->getPersistenceFactory()->value( + $persistence_factory->value( \ilDBConstants::T_TEXT, $this->answer_form_id->toString() ) @@ -128,11 +144,14 @@ public function toDelete( } private function buildInsertStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, + TableNameBuilder $table_name_builder ): Insert { return $persistence_factory->insert( - CoreTables::AnswerForms->getColumns( - $persistence_factory + $answer_form_generic_table_definitions->getColumns( + $table_name_builder, + AnswerFormGenericTableTypes::AnswerForms ), [ $persistence_factory->value( @@ -172,12 +191,17 @@ private function buildInsertStatement( } private function buildUpdateStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, + TableNameBuilder $table_name_builder ): Update { - $answer_form_table_definition = CoreTables::AnswerForms; + $table_type = AnswerFormGenericTableTypes::AnswerForms; + return $persistence_factory->update( - $answer_form_table_definition->getColumns( - $persistence_factory, + $answer_form_generic_table_definitions->getColumns( + $table_name_builder, + $table_type, + '', [ 'id', 'type', @@ -206,8 +230,9 @@ private function buildUpdateStatement( ], [ $persistence_factory->where( - $answer_form_table_definition->getIdColumn( - $persistence_factory + $answer_form_generic_table_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php index 6780745aeadd..6ea871a62712 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback as FeedbackInterface; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -57,6 +58,7 @@ public function edit( #[\Override] public function toStorage( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { return $manipulate; @@ -64,6 +66,7 @@ public function toStorage( #[\Override] public function toDelete( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { return $manipulate; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php index 13e7bfd1d102..cf455f69a4bb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking as MarkingInterface; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -45,6 +46,7 @@ public function edit( #[\Override] public function toStorage( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { return $manipulate; @@ -52,6 +54,7 @@ public function toStorage( #[\Override] public function toDelete( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { return $manipulate; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index 18967e1e3c3a..76d68ae18300 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -22,13 +22,13 @@ use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableDefinitions; use ILIAS\Language\Language; class Definition implements DefinitionInterface @@ -38,7 +38,7 @@ class Definition implements DefinitionInterface */ public function __construct( private readonly PropertiesFactory $properties_factory, - private readonly Persistence $persistence, + private readonly TableDefinitions $table_definitions, private readonly array $available_capabilities, private readonly Edit $edit_view, private readonly Participant $participant_view @@ -64,9 +64,9 @@ public function buildProperties( } #[\Override] - public function getPersistence(): Persistence + public function getTableDefinitions(): TableDefinitions { - return $this->persistence; + return $this->table_definitions; } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index 58242849b853..543aba160492 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -20,14 +20,14 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Migration\SanitizeLegacyText; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; @@ -36,7 +36,7 @@ trait BasicMigrationFunctions use SanitizeLegacyText; private function buildGapInsertStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $gaps_insert, @@ -52,10 +52,9 @@ private function buildGapInsertStatement( ): Insert { if ($gaps_insert === null) { return $persistence_factory->insert( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - TableTypes::AnswerInputs + AnswerFormSpecificTableTypes::AnswerInputs ), $this->buildGapValuesForInsert( $persistence_factory, @@ -114,7 +113,7 @@ private function buildGapValuesForInsert( } private function buildAnswerOptionInsertStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, ?Insert $options_insert, @@ -128,10 +127,9 @@ private function buildAnswerOptionInsertStatement( ): Insert { if ($options_insert === null) { return $persistence_factory->insert( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - TableTypes::AnswerOptions + AnswerFormSpecificTableTypes::AnswerOptions ), $this->buildOptionValuesForInsert( $persistence_factory, @@ -187,7 +185,7 @@ private function buildOptionValuesForInsert( } private function buildAnswerFormInsertStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, Uuid $answer_form_id, @@ -195,10 +193,9 @@ private function buildAnswerFormInsertStatement( int $combinations_enabled ): Insert { return $persistence_factory->insert( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - TableTypes::TypeSpecificAnswerForms + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), [ $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 36759ff980e7..c43d4a569563 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -20,16 +20,16 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\Environment; @@ -38,7 +38,7 @@ class MigrationCloze implements Migration use BasicMigrationFunctions; public function __construct( - private readonly Persistence $persistence, + private readonly TableDefinitions $table_definitions, private readonly \EvalMath $math ) { } @@ -58,12 +58,13 @@ public function getDefinitionClass(): string #[\Override] public function getTableNameSpace(): TableNameSpace { - return $this->persistence->getTableNameSpace(); + return $this->table_definitions->getTableNameSpace(); } #[\Override] public function completeMigrationInsert( Environment $environment, + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_input_mapping = []; @@ -80,8 +81,8 @@ public function completeMigrationInsert( $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); $answer_options_mapping[$db_row->gap_id] = []; $gaps_insert = $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_mapping[$db_row->gap_id], @@ -103,8 +104,8 @@ public function completeMigrationInsert( ]; $answer_options_insert = $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer_option_id, @@ -123,6 +124,7 @@ public function completeMigrationInsert( if ($db_row->combinations_enabled) { $migration_insert = $this->addCombinationInsertStatements( + $persistence_factory, $migration_insert, $answer_input_mapping, $answer_options_mapping @@ -132,8 +134,8 @@ public function completeMigrationInsert( return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), @@ -190,6 +192,7 @@ private function fetchCombinationsDBValues( } private function addCombinationInsertStatements( + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert, array $answer_input_mapping, array $answer_options_mapping @@ -213,7 +216,7 @@ private function addCombinationInsertStatements( if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); $combinations_insert = $this->buildCombinationsInsert( - $migration_insert->getPersistenceFactory(), + $persistence_factory, $migration_insert->getTableNameBuilder(), $combinations_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), @@ -223,7 +226,7 @@ private function addCombinationInsertStatements( } $combinations_to_answer_options_insert = $this->buildCombinationsToAnswerOptionsInsert( - $migration_insert->getPersistenceFactory(), + $persistence_factory, $migration_insert->getTableNameBuilder(), $combinations_to_answer_options_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), @@ -247,11 +250,10 @@ private function buildCombinationsInsert( ): Insert { if ($combinations_insert === null) { return $persistence_factory->insert( - $this->persistence->getColumns( - $persistence_factory, + $this->table_definitions->getColumns( $table_name_builder, - TableTypes::Additional, - $this->persistence->getCombinationsTableIdentifier() + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationsTableIdentifier() ), [ $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), @@ -279,11 +281,10 @@ private function buildCombinationsToAnswerOptionsInsert( ): Insert { if ($combinations_to_answer_options_insert === null) { return $persistence_factory->insert( - $this->persistence->getColumns( - $persistence_factory, + $this->table_definitions->getColumns( $table_name_builder, - TableTypes::Additional, - $this->persistence->getCombinationToAnswerOptionsTableIdentifier() + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), [ $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 93a1e55cc85d..3acb296defec 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -23,7 +23,8 @@ use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -32,7 +33,7 @@ class MigrationLongMenu implements Migration use BasicMigrationFunctions; public function __construct( - private readonly Persistence $persistence + private readonly TableDefinitions $table_definitions ) { } @@ -51,12 +52,13 @@ public function getDefinitionClass(): string #[\Override] public function getTableNameSpace(): TableNameSpace { - return $this->persistence->getTableNameSpace(); + return $this->table_definitions->getTableNameSpace(); } #[\Override] public function completeMigrationInsert( Environment $environment, + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_form_id = $migration_insert->getAnswerFormId(); @@ -74,8 +76,8 @@ public function completeMigrationInsert( $answer_input_mapping[$db_row->gap_number] = $answer_input_id; $gaps_insert = $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_id, @@ -133,8 +135,8 @@ public function completeMigrationInsert( [] ) as $answer) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer['answer_option_id'], @@ -150,8 +152,8 @@ public function completeMigrationInsert( return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld($db_row->identical_scoring), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index 4fc25a6fbf31..97ba88451dd4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -23,9 +23,10 @@ use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -34,7 +35,7 @@ class MigrationNumeric implements Migration use BasicMigrationFunctions; public function __construct( - private readonly Persistence $persistence, + private readonly TableDefinitions $table_definitions, private readonly \EvalMath $math ) { $this->math->suppress_errors = true; @@ -55,12 +56,13 @@ public function getDefinitionClass(): string #[\Override] public function getTableNameSpace(): TableNameSpace { - return $this->persistence->getTableNameSpace(); + return $this->table_definitions->getTableNameSpace(); } #[\Override] public function completeMigrationInsert( Environment $environment, + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $db_row = $this->fetchDBValues( @@ -76,8 +78,8 @@ public function completeMigrationInsert( $gap_id = $migration_insert->getUuid(); return $migration_insert->withAdditionalInsert( $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), null, $gap_id, @@ -92,8 +94,8 @@ public function completeMigrationInsert( ) )->withAdditionalInsert( $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), null, $migration_insert->getUuid(), @@ -106,8 +108,8 @@ public function completeMigrationInsert( ) )->withAdditionalInsert( $this->buildAnswerFormInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::ScoreAll, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index c2ba57c42977..495cff193a39 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -20,12 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; -use ILIAS\Data\UUID\Uuid; use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -34,7 +34,7 @@ class MigrationTextSubset implements Migration use BasicMigrationFunctions; public function __construct( - private readonly Persistence $persistence + private readonly TableDefinitions $table_definitions ) { } @@ -53,12 +53,13 @@ public function getDefinitionClass(): string #[\Override] public function getTableNameSpace(): TableNameSpace { - return $this->persistence->getTableNameSpace(); + return $this->table_definitions->getTableNameSpace(); } #[\Override] public function completeMigrationInsert( Environment $environment, + PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_form_id = $migration_insert->getAnswerFormId(); @@ -76,8 +77,8 @@ public function completeMigrationInsert( $gaps[] = $gap_id; $gaps_insert = $this->buildGapInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $gaps_insert, $gap_id, @@ -97,8 +98,8 @@ public function completeMigrationInsert( foreach ($gaps as $gap_id) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_options_insert, $migration_insert->getUuid(), @@ -119,8 +120,8 @@ public function completeMigrationInsert( return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( - $this->persistence, - $migration_insert->getPersistenceFactory(), + $this->table_definitions, + $persistence_factory, $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::OnlyScoreDistinct, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 8f0f87d4076d..43f1dddf8cef 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -20,14 +20,14 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -94,7 +94,8 @@ public function containsAnswerOptionsExactly( public function toStorage( Uuid $answer_form_id, - Persistence $persistence, + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder, Manipulate $manipulate ): Manipulate { @@ -108,15 +109,15 @@ public function toStorage( $this->matching_values, fn(Manipulate $c, MatchingValue $v): Manipulate => $c->withAdditionalStatement( $v->toStorage( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ), $manipulate->withAdditionalStatement( $this->buildReplace( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder, $answer_form_id ) @@ -125,38 +126,37 @@ public function toStorage( } public function toDelete( - Persistence $persistence, + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder, Manipulate $manipulate ): Manipulate { return $manipulate->withAdditionalStatement( $this->buildDelete( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) )->withAdditionalStatement( $this->buildDeleteForLinkedValues( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ); } private function buildReplace( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, Uuid $answer_form_id ): Replace { - $table_definition = TableTypes::Additional; return $persistence_factory->replace( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - $table_definition, - $persistence->getCombinationsTableIdentifier() + AnswerFormSpecificTableTypes::Additional, + $table_definitions->getCombinationsTableIdentifier() ), [ $persistence_factory->value(\ilDBConstants::T_TEXT, $this->id->toString()), @@ -167,24 +167,23 @@ private function buildReplace( } private function buildDelete( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $table_definition = TableTypes::Additional; + $table_definition = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->delete( $persistence_factory->table( - $table_definition, $table_name_builder, - $persistence->getCombinationsTableIdentifier() + $table_definition, + $table_definitions->getCombinationsTableIdentifier() ), [ $persistence_factory->where( - $persistence->getIdColumn( - $persistence_factory, + $table_definitions->getIdColumn( $table_name_builder, $table_definition, - $persistence->getCombinationsTableIdentifier() + $table_definitions->getCombinationsTableIdentifier() ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -196,24 +195,23 @@ private function buildDelete( } private function buildDeleteForLinkedValues( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $table_definition = TableTypes::Additional; + $table_definition = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->delete( $persistence_factory->table( - $table_definition, $table_name_builder, - $persistence->getCombinationToAnswerOptionsTableIdentifier() + $table_definition, + $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), [ $persistence_factory->where( - $persistence->getIdColumn( - $persistence_factory, + $table_definitions->getIdColumn( $table_name_builder, $table_definition, - $persistence->getCombinationToAnswerOptionsTableIdentifier() + $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index 5b4210702829..e6d8e6f75d92 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -20,7 +20,8 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; @@ -35,6 +36,7 @@ class Combinations public function __construct( private readonly Factory $combinations_factory, + private readonly PersistenceFactory $persistence_factory, private readonly Uuid $answer_form_id, private bool $enabled, array $combinations @@ -107,21 +109,23 @@ public function getEditView( public function toStorage( Manipulate $manipulate, - Persistence $persistence, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder ): Manipulate { return array_reduce( $this->combinations, fn(Manipulate $c, Combination $v): Manipulate => $v->toStorage( $this->answer_form_id, - $persistence, + $this->persistence_factory, + $table_definitions, $table_name_builder, $c ), array_reduce( $this->deleted_combinations, fn(Manipulate $c, Combination $v): Manipulate => $v->toDelete( - $persistence, + $this->persistence_factory, + $table_definitions, $table_name_builder, $manipulate ), @@ -132,17 +136,17 @@ public function toStorage( public function toDelete( Manipulate $manipulate, - Persistence $persistence, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder ): Manipulate { return array_reduce( $this->combinations, fn(Manipulate $c, Combination $v): Manipulate => $c->withAdditionalStatement( $v->toDelete( - $this->answer_form_id, - $manipulate, - $persistence, - $table_name_builder + $this->persistence_factory, + $table_definitions, + $table_name_builder, + $manipulate ) ), $manipulate diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index e5a6a8ba8fe9..bde794e3f0bf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -20,19 +20,23 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\TypeGenericProperties; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; class Factory { public function __construct( - private readonly UuidFactory $uuid_factory + private readonly UuidFactory $uuid_factory, + private readonly PersistenceFactory $persistence_factory, + private readonly TableDefinitions $table_definitions ) { } @@ -44,6 +48,7 @@ public function getCombinations( ): Combinations { return new Combinations( $this, + $this->persistence_factory, $type_generic_properties->getAnswerFormId(), $combinations_enabled, !$combinations_enabled || $gaps === null || $query === null @@ -52,7 +57,10 @@ public function getCombinations( $type_generic_properties, $gaps, $this->retrieveCombinationsFromQuery( - $type_generic_properties, + $type_generic_properties + ->getDefinition() + ->getTableDefinitions() + ->getTableSubNameSpace(), $query ), $query @@ -163,16 +171,16 @@ public function buildCombinationFromCarryValue( } private function retrieveCombinationsFromQuery( - TypeGenericProperties $type_generic_properties, + TableSubNameSpace $table_sub_name_space, Query $query ): array { return $query->retrieveCurrentRecord( - TableTypes::Additional->getTable( - $query->getPersistenceFactory(), + $this->persistence_factory->table( $query->getTableNameBuilder( - $type_generic_properties->getDefinition()::class + $table_sub_name_space ), - Persistence::COMBINATION_TABLE_IDENTIFIER + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationsTableIdentifier() ), $query->getRefinery()->custom()->transformation( fn(array $vs): array => $this->buildCombinationsFromQuery( @@ -217,12 +225,15 @@ private function retrieveMatchingValuesFromQuery( Query $query ): array { return $query->retrieveCurrentRecord( - TableTypes::Additional->getTable( - $query->getPersistenceFactory(), + $this->persistence_factory->table( $query->getTableNameBuilder( - $type_generic_properties->getDefinition()::class + $type_generic_properties + ->getDefinition() + ->getTableDefinitions() + ->getTableSubNameSpace(), ), - Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), $query->getRefinery()->custom()->transformation( function (array $vs) use ( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index 50eb350fbece..6fe1ce934f69 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -20,13 +20,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; @@ -73,7 +73,7 @@ public function buildPresentationString( } public function toStorage( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { @@ -83,13 +83,12 @@ public function toStorage( ); } - $table_definition = TableTypes::Additional; + $table_type = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->replace( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - $table_definition, - $persistence->getCombinationToAnswerOptionsTableIdentifier() + $table_type, + $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), [ $persistence_factory->value( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index c5112d5622ce..4d0573df90bb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -20,18 +20,20 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Text as ClozeText; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Factory as CombinationsFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapsFactory; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableTypes; class Factory { public function __construct( + private readonly PersistenceFactory $persistence_factory, private readonly ClozeTextFactory $cloze_text_factory, private readonly GapsFactory $gaps_factory, private readonly CombinationsFactory $combinations_factory @@ -66,9 +68,14 @@ public function fromData( 'scoring_identical_responses' => $scoring_identical_responses, 'combinations_enabled' => $combinations_enabled ] = $query->retrieveCurrentRecord( - TableTypes::TypeSpecificAnswerForms->getTable( - $query->getPersistenceFactory(), - $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) + $this->persistence_factory->table( + $query->getTableNameBuilder( + $type_generic_properties + ->getDefinition() + ->getTableDefinitions() + ->getTableSubNameSpace() + ), + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), $query->getRefinery()->custom()->transformation( function (array $vs) use ($type_generic_properties): array { @@ -85,6 +92,11 @@ function (array $vs) use ($type_generic_properties): array { ); $gaps = $this->gaps_factory->fromDatabase( + $this->persistence_factory, + $type_generic_properties + ->getDefinition() + ->getTableDefinitions() + ->getTableSubNameSpace(), $type_generic_properties->getAnswerFormId(), $query ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index c673ec6291e2..14115b191fb9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -20,12 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; @@ -247,7 +247,7 @@ function (array $c, AnswerOption $v) use ($ff, $build_label): array { public function buildReplace( ?Replace $replace, - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { @@ -256,10 +256,9 @@ public function buildReplace( fn(?Replace $c, AnswerOption $v): Replace => $v->buildReplace( $persistence_factory, $c, - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - TableTypes::AnswerOptions + AnswerFormSpecificTableTypes::AnswerOptions ) ), $replace @@ -267,23 +266,23 @@ public function buildReplace( } public function buildDelete( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $answer_options_table_definition = TableTypes::AnswerOptions; + $table_type = AnswerFormSpecificTableTypes::AnswerOptions; return $persistence_factory->delete( - $answer_options_table_definition->getTable( + $table_type->getTable( $persistence_factory, $table_name_builder ), [ $persistence_factory->where( - $persistence->getForeignKeyColumn( + $table_definitions->getForeignKeyColumn( $persistence_factory, $table_name_builder, - $answer_options_table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php index 116107608504..51072025880c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -20,9 +20,10 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as Refinery; @@ -77,12 +78,16 @@ public function buildAnswerOption( } public function fromDatabase( + PersistenceFactory $persistence_factory, + TableSubNameSpace $table_name_specifier, Query $query ): array { return $query->retrieveCurrentRecord( - TableTypes::AnswerOptions->getTable( - $query->getPersistenceFactory(), - $query->getTableNameBuilder(Definition::class) + $persistence_factory->table( + $query->getTableNameBuilder( + $table_name_specifier + ), + AnswerFormSpecificTableTypes::AnswerOptions ), $query->getRefinery()->custom()->transformation( function (array $vs): array { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php index 947c50361936..d0147e99db3d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -20,10 +20,11 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Factory as AnswerOptionsFactory; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; @@ -97,15 +98,23 @@ public function getGapTypeByIdentifier( } public function fromDatabase( + PersistenceFactory $persistence_factory, + TableSubNameSpace $table_name_specifier, Uuid $answer_form_id, Query $query ): Gaps { - $answer_options = $this->answer_options_factory->fromDatabase($query); + $answer_options = $this->answer_options_factory->fromDatabase( + $persistence_factory, + $table_name_specifier, + $query + ); return $query->retrieveCurrentRecord( - TableTypes::AnswerInputs->getTable( - $query->getPersistenceFactory(), - $query->getTableNameBuilder(Definition::class) + $persistence_factory->table( + $query->getTableNameBuilder( + $table_name_specifier + ), + AnswerFormSpecificTableTypes::AnswerInputs ), $query->getRefinery()->custom()->transformation( function (array $vs) use ($answer_form_id, $answer_options): Gaps { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index a27fc3e3bd58..76786fca4d76 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -20,23 +20,20 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerForm\Persistence; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Presentation\Definitions\CarryWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Input\Field\Group; use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; -use ILIAS\Refinery\Transformation; class Gap { @@ -260,7 +257,7 @@ public function withValuesFromCarry( public function buildReplace( ?Replace $replace, - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Replace { @@ -270,14 +267,13 @@ public function buildReplace( ); } - $table_definition = TableTypes::AnswerInputs; + $table_type = AnswerFormSpecificTableTypes::AnswerInputs; if ($replace === null) { return $persistence_factory->replace( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - $table_definition + $table_type ), $this->buildValuesForGapReplace($persistence_factory) ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 837e154ba746..59e5192a90aa 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; @@ -27,8 +28,7 @@ use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Operator; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -364,7 +364,8 @@ public function toTableRows( public function toStorage( Manipulate $manipulate, - Persistence $persistence, + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder ): Manipulate { [ @@ -375,14 +376,14 @@ public function toStorage( fn(array $c, Gap $v): array => [ 'gaps' => $v->buildReplace( $c['gaps'], - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ), 'answer_options' => $v->getAnswerOptions()->buildReplace( $c['answer_options'], - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ], @@ -394,8 +395,8 @@ public function toStorage( return $manipulate->withAdditionalStatement( $this->buildDeleteForRemovedGaps( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) )->withAdditionalStatement($replace_for_gaps) @@ -404,22 +405,23 @@ public function toStorage( public function toDelete( Manipulate $manipulate, - Persistence $persistence, + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder ): Manipulate { return array_reduce( $this->gaps, fn(Manipulate $c, Gap $v): Manipulate => $c->withAdditionalStatement( $v->getAnswerOptions()->buildDelete( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ), $manipulate->withAdditionalStatement( $this->buildDeleteForDeletionOfAnswerForm( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ) @@ -427,22 +429,21 @@ public function toDelete( } private function buildDeleteForRemovedGaps( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $table_definition = TableTypes::AnswerInputs; + $table_type = AnswerFormSpecificTableTypes::AnswerInputs; return $persistence_factory->delete( - $table_definition->getTable( - $persistence_factory, - $table_name_builder + $persistence_factory->table( + $table_name_builder, + $table_type ), [ $persistence_factory->where( - $persistence->getForeignKeyColumn( - $persistence_factory, + $table_definitions->getForeignKeyColumn( $table_name_builder, - $table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -450,10 +451,9 @@ private function buildDeleteForRemovedGaps( ) ), $persistence_factory->where( - $persistence->getIdColumn( - $persistence_factory, + $table_definitions->getIdColumn( $table_name_builder, - $table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -471,23 +471,22 @@ private function buildDeleteForRemovedGaps( } private function buildDeleteForDeletionOfAnswerForm( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $table_definition = TableTypes::AnswerInputs; + $table_type = AnswerFormSpecificTableTypes::AnswerInputs; return $persistence_factory->delete( - $table_definition->getTable( - $persistence_factory, - $table_name_builder + $persistence_factory->table( + $table_name_builder, + $table_type ), [ $persistence_factory->where( - $persistence->getForeignKeyColumn( - $persistence_factory, + $table_definitions->getForeignKeyColumn( $table_name_builder, - $table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 5ab16b51a21c..b4c75ed1b919 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; -use ILIAS\Questions\AnswerForm\Persistence; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Properties as PropertiesInterface; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; @@ -30,6 +30,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; @@ -37,7 +38,6 @@ use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; @@ -277,76 +277,73 @@ public function withValuesFromCarry( #[\Override] public function toStorage( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { - $persistence = $manipulate->getPersistenceForDefinitionClass( - $this->definition::class - ); + $table_definitions = $this->definition->getTableDefinitions(); $table_name_builder = $manipulate->getTableNameBuilder( - $this->definition::class + $table_definitions->getTableSubNameSpace() ); $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create ? $this->buildInsertAnswerFormStatement( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) : $this->buildUpdateAnswerFormStatement( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ); return $this->gaps->toStorage( $this->addReplaceCombinationsStatements( $manipulate, - $persistence, + $table_definitions, $table_name_builder )->withAdditionalStatement( $answer_form_statement ), - $persistence, + $persistence_factory, + $table_definitions, $table_name_builder ); } #[\Override] public function toDelete( + PersistenceFactory $persistence_factory, Manipulate $manipulate ): Manipulate { - $persistence = $manipulate->getPersistenceForDefinitionClass( - $this->definition::class - ); - + $table_definitions = $this->definition->getTableDefinitions(); $table_name_builder = $manipulate->getTableNameBuilder( - $this->definition::class + $table_definitions->getTableSubNameSpace() ); return $this->gaps->toDelete( $manipulate->withAdditionalStatement( $this->buildDeleteAnswerFormStatement( - $persistence, - $manipulate->getPersistenceFactory(), + $table_definitions, + $persistence_factory, $table_name_builder ) ), - $persistence, + $persistence_factory, + $table_definitions, $table_name_builder ); } private function buildInsertAnswerFormStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Insert { - $table_definition = TableTypes::TypeSpecificAnswerForms; return $persistence_factory->insert( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - $table_definition + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), [ $persistence_factory->value( @@ -366,16 +363,15 @@ private function buildInsertAnswerFormStatement( } private function buildUpdateAnswerFormStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Update { - $table_definition = TableTypes::TypeSpecificAnswerForms; + $table_type = AnswerFormSpecificTableTypes::TypeSpecificAnswerForms; return $persistence_factory->update( - $persistence->getColumns( - $persistence_factory, + $table_definitions->getColumns( $table_name_builder, - $table_definition, + $table_type, '', ['answer_form_id'] ), @@ -391,10 +387,9 @@ private function buildUpdateAnswerFormStatement( ], [ $persistence_factory->where( - $persistence->getIdColumn( - $persistence_factory, + $table_definitions->getIdColumn( $table_name_builder, - $table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -407,7 +402,7 @@ private function buildUpdateAnswerFormStatement( private function addReplaceCombinationsStatements( Manipulate $manipulate, - Persistence $persistence, + TableDefinitions $table_definitions, TableNameBuilder $table_name_builder ): Manipulate { if (!$this->combinations->areCombinationsEnabled() @@ -417,29 +412,28 @@ private function addReplaceCombinationsStatements( return $this->combinations->toStorage( $manipulate, - $persistence, + $table_definitions, $table_name_builder ); } private function buildDeleteAnswerFormStatement( - Persistence $persistence, + TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder ): Delete { - $table_definition = TableTypes::TypeSpecificAnswerForms; + $table_type = AnswerFormSpecificTableTypes::TypeSpecificAnswerForms; return $persistence_factory->delete( - $table_definition->getTable( - $persistence_factory, - $table_name_builder + $persistence_factory->table( + $table_name_builder, + $table_type ), [ $persistence_factory->where( - $persistence->getForeignKeyColumn( - $persistence_factory, + $table_definitions->getForeignKeyColumn( $table_name_builder, - $table_definition + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php similarity index 56% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php index 1eff9def005e..813eddba6b8a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php @@ -20,17 +20,17 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; -use ILIAS\Questions\AnswerForm\Persistence as PersistenceInterface; -use ILIAS\Questions\Persistence\TableTypes; -use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; +use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableNameSpace; -use ILIAS\Questions\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableTypes; -class Persistence implements PersistenceInterface +class TableDefinitions implements TableDefinitionsInterface { private const string ID_COLUMN = 'id'; @@ -66,7 +66,7 @@ class Persistence implements PersistenceInterface 'upper_limit' ]; - public const string COMBINATION_TABLE_IDENTIFIER = 'combinations'; + private const string COMBINATION_TABLE_IDENTIFIER = 'combinations'; private const string COMBINATION_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array COMBINATION_TABLE_COLUMNS = [ 'id', @@ -74,7 +74,7 @@ class Persistence implements PersistenceInterface 'points' ]; - public const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER = 'combinations_to_answer_options'; + private const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER = 'combinations_to_answer_options'; private const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_ID_COLUMN = 'combination_id'; private const string COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN = 'combination_id'; private const array COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS = [ @@ -85,40 +85,43 @@ class Persistence implements PersistenceInterface ]; public function __construct( - private readonly TableNameSpaceCore $table_namespace + private readonly PersistenceFactory $persistence_factory, + private readonly TableSubNameSpace $table_name_specifiers ) { } #[\Override] - public function getTableNameSpace(): TableNameSpace + public function getTableSubNameSpace(): TableSubNameSpace { - return $this->table_namespace; + return $this->table_name_specifiers; } #[\Override] public function getColumns( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '', + string $sub_table_identifier = '', array $columns_to_skip = [] ): array { - $table = $table_type->getTable( - $persistence_factory, + $table = $this->persistence_factory->table( $table_name_builder, - $table_identifier + $table_type, + $sub_table_identifier ); $column_identifiers = match($table_type) { - TableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, - TableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, - TableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, - TableTypes::Additional => match($table_identifier) { + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, + AnswerFormSpecificTableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, + AnswerFormSpecificTableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, + AnswerFormSpecificTableTypes::Additional => match($sub_table_identifier) { self::COMBINATION_TABLE_IDENTIFIER => self::COMBINATION_TABLE_COLUMNS, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER => self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS } }; return array_map( - fn(string $v): Column => $persistence_factory->column($table, $v), + fn(string $v): Column => $this->persistence_factory->column( + $table, + $v + ), array_values( array_filter( $column_identifiers, @@ -130,78 +133,64 @@ public function getColumns( #[\Override] public function getIdColumn( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '' + string $sub_table_identifier = '' ): Column { - if ($table_type === TableTypes::TypeSpecificAnswerForms) { - return $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder - ), + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type, + $sub_table_identifier + ); + + if ($table_type === AnswerFormSpecificTableTypes::TypeSpecificAnswerForms) { + return $this->persistence_factory->column( + $table, self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN ); } - if ($table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { - return $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder, - self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER - ), + if ($sub_table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { + return $this->persistence_factory->column( + $table, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_ID_COLUMN ); } - return $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder, - $table_identifier - ), + return $this->persistence_factory->column( + $table, self::ID_COLUMN ); } #[\Override] public function getForeignKeyColumn( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '' + string $sub_table_identifier = '' ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type, + $sub_table_identifier + ); + return match($table_type) { - TableTypes::TypeSpecificAnswerForms => $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder - ), + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms => $this->persistence_factory->column( + $table, self::ANSWER_FORM_TABLE_ID_COLUMN ), - TableTypes::AnswerInputs => $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder - ), + AnswerFormSpecificTableTypes::AnswerInputs => $this->persistence_factory->column( + $table, self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN ), - TableTypes::AnswerOptions => $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder - ), + AnswerFormSpecificTableTypes::AnswerOptions => $this->persistence_factory->column( + $table, self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ), - TableTypes::Additional => $persistence_factory->column( - $table_type->getTable( - $persistence_factory, - $table_name_builder, - $table_identifier - ), - $table_identifier === self::COMBINATION_TABLE_IDENTIFIER + AnswerFormSpecificTableTypes::Additional => $this->persistence_factory->column( + $table, + $sub_table_identifier === self::COMBINATION_TABLE_IDENTIFIER ? self::COMBINATION_TABLE_FOREIGN_KEY_COLUMN : self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ) @@ -209,143 +198,126 @@ public function getForeignKeyColumn( } #[\Override] - public function completeQuestionsQuery( + public function completeQuery( Query $query, - Column $answer_form_id_column + ?Column $answer_form_id_column ): Query { - $persistence_factory = $query->getPersistenceFactory(); - $table_name_builder = $query->getTableNameBuilder(Definition::class); - - $answer_form_specific_table_definition = TableTypes::TypeSpecificAnswerForms; - $answer_input_table_definition = TableTypes::AnswerInputs; - $answer_options_table_definition = TableTypes::AnswerOptions; - $additional_table_definition = TableTypes::Additional; + $table_name_builder = $query->getTableNameBuilder( + $this->getTableSubNameSpace() + ); $combinations_id_column = $this->getIdColumn( - $persistence_factory, $table_name_builder, - $additional_table_definition, + AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ); return $query->withAdditionalJoin( - $persistence_factory->join( + $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $persistence_factory, $table_name_builder, - $answer_form_specific_table_definition + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), JoinType::Left ) )->withAdditionalSelect( - $persistence_factory->select( + $this->persistence_factory->select( $this->getColumns( - $persistence_factory, $table_name_builder, - $answer_form_specific_table_definition + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ) ) )->withAdditionalJoin( - $persistence_factory->join( + $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $persistence_factory, $table_name_builder, - $answer_input_table_definition + AnswerFormSpecificTableTypes::AnswerInputs ), JoinType::Left ) )->withAdditionalSelect( - $persistence_factory->select( + $this->persistence_factory->select( $this->getColumns( - $persistence_factory, $table_name_builder, - $answer_input_table_definition + AnswerFormSpecificTableTypes::AnswerInputs ) ) )->withAdditionalJoin( - $persistence_factory->join( + $this->persistence_factory->join( $this->getIdColumn( - $persistence_factory, $table_name_builder, - $answer_input_table_definition + AnswerFormSpecificTableTypes::AnswerInputs ), $this->getForeignKeyColumn( - $persistence_factory, $table_name_builder, - $answer_options_table_definition + AnswerFormSpecificTableTypes::AnswerOptions ), JoinType::Left ) )->withAdditionalOrder( - $persistence_factory->order( + $this->persistence_factory->order( $this->getIdColumn( - $persistence_factory, $table_name_builder, - $answer_input_table_definition + AnswerFormSpecificTableTypes::AnswerInputs ) ) )->withAdditionalSelect( - $persistence_factory->select( + $this->persistence_factory->select( $this->getColumns( - $persistence_factory, $table_name_builder, - $answer_options_table_definition + AnswerFormSpecificTableTypes::AnswerOptions ) ) )->withAdditionalOrder( - $persistence_factory->order( - $persistence_factory->column( - $answer_options_table_definition->getTable( - $persistence_factory, - $table_name_builder + $this->persistence_factory->order( + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_name_builder, + AnswerFormSpecificTableTypes::AnswerOptions ), 'position' ) ) )->withAdditionalJoin( - $persistence_factory->join( + $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $persistence_factory, $table_name_builder, - $additional_table_definition, + AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ), JoinType::Left ) )->withAdditionalSelect( - $persistence_factory->select( + $this->persistence_factory->select( $this->getColumns( - $persistence_factory, $table_name_builder, - $additional_table_definition, + AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ) ) )->withAdditionalJoin( - $persistence_factory->join( + $this->persistence_factory->join( $combinations_id_column, $this->getForeignKeyColumn( - $persistence_factory, $table_name_builder, - $additional_table_definition, + AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ), JoinType::Left ) )->withAdditionalSelect( - $persistence_factory->select( + $this->persistence_factory->select( $this->getColumns( - $persistence_factory, $table_name_builder, - $additional_table_definition, + AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ) ) )->withAdditionalOrder( - $persistence_factory->order( + $this->persistence_factory->order( $combinations_id_column ) ); diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php index d3ecf6764511..28fc8b6ad88b 100644 --- a/components/ILIAS/Questions/src/Collector.php +++ b/components/ILIAS/Questions/src/Collector.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions; -use ILIAS\Questions\Persistence\Repository; +use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Questions\Question\Question; use ILIAS\Data\UUID\Uuid; diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php deleted file mode 100644 index d181857711ce..000000000000 --- a/components/ILIAS/Questions/src/Persistence/CoreTables.php +++ /dev/null @@ -1,143 +0,0 @@ -table($this); - } - - public function getColumns( - Factory $persistence_factory, - array $columns_to_skip = [] - ): array { - $table = $this->getTable($persistence_factory); - $column_identifiers = match($this) { - self::Questions => self::QUESTION_TABLE_COLUMNS, - self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, - self::Linking => self::LINKING_TABLE_COLUMNS, - self::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS - }; - return array_map( - fn(string $v): Column => $persistence_factory->column($table, $v), - array_values( - array_filter( - $column_identifiers, - fn(string $v) => !in_array($v, $columns_to_skip) - ) - ) - ); - } - - public function getIdColumn( - Factory $persistence_factory - ): Column { - return match($this) { - self::Questions => $persistence_factory->column( - $this->getTable($persistence_factory), - self::QUESTION_TABLE_ID_COLUMN - ), - self::AnswerForms => $persistence_factory->column( - $this->getTable($persistence_factory), - self::ANSWER_FORM_TABLE_ID_COLUMN - ), - self::Linking => $persistence_factory->column( - $this->getTable($persistence_factory), - self::LINKING_TABLE_ID_COLUMN - ), - self::MigrationsTable => $persistence_factory->column( - $this->getTable($persistence_factory), - self::MIGRATIONS_TABLE_ID_COLUMN - ) - }; - } - - public function getForeignKeyColumn( - Factory $persistence_factory - ): ?Column { - return match($this) { - self::AnswerForms => $persistence_factory->column( - $this->getTable($persistence_factory), - self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN - ), - self::Linking => $persistence_factory->column( - $this->getTable($persistence_factory), - self::LINKING_TABLE_FOREIGN_KEY_COLUMN - ), - self::MigrationsTable => $persistence_factory->column( - $this->getTable($persistence_factory), - self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN - ), - default => null - }; - } -} diff --git a/components/ILIAS/Questions/src/Persistence/Factory.php b/components/ILIAS/Questions/src/Persistence/Factory.php index 6ba8487d645f..6a9b3f3e0b79 100644 --- a/components/ILIAS/Questions/src/Persistence/Factory.php +++ b/components/ILIAS/Questions/src/Persistence/Factory.php @@ -68,11 +68,11 @@ public function delete( } public function table( - CoreTables|TableTypes $table_definition, - ?TableNameBuilder $table_name_builder = null, - string $table_identifier = '' + TableNameBuilder $table_name_builder, + TableTypes $table, + string $sub_table_identifier = '' ): Table { - return new Table($table_definition, $table_name_builder, $table_identifier); + return new Table($table_name_builder, $table, $sub_table_identifier); } public function column( diff --git a/components/ILIAS/Questions/src/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php index 81757ebee3c8..dffad5d7fe79 100644 --- a/components/ILIAS/Questions/src/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -20,47 +20,28 @@ namespace ILIAS\Questions\Persistence; -use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\AnswerForm\Persistence; - class Manipulate { private array $statements = []; public function __construct( private readonly \ilDBInterface $db, - private readonly Factory $persistence_factory, - private readonly AnswerFormFactory $answer_form_factory, - private readonly ManipulationType $type + private readonly ManipulationType $type, + private readonly string $component_name_space ) { } - public function getPersistenceFactory(): Factory - { - return $this->persistence_factory; - } - public function getManipulationType(): ManipulationType { return $this->type; } - public function getPersistenceForDefinitionClass( - string $definition_class - ): Persistence { - return $this->answer_form_factory - ->getDefinitionForClass($definition_class) - ->getPersistence(); - } - public function getTableNameBuilder( - string $definition_class + ?TableSubNameSpace $table_sub_name_space ): TableNameBuilder { return new TableNameBuilder( - $this->answer_form_factory - ->getDefinitionForClass($definition_class) - ->getPersistence() - ->getTableNameSpace() + $this->component_name_space, + $table_sub_name_space ); } diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 214dc058735e..7cd20865a70a 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -20,8 +20,6 @@ namespace ILIAS\Questions\Persistence; -use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\AnswerForm\Persistence; use ILIAS\Data\Range; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; @@ -39,87 +37,24 @@ class Query private ?array $current_record = null; + /** + * @param Table $base_table The base table will be used as the table in the + * "From" statement + */ public function __construct( private readonly \ilDBInterface $db, - private readonly Factory $persistence_factory, - private readonly AnswerFormFactory $answer_form_factory, - private readonly Refinery $refinery + private readonly Refinery $refinery, + private string $component_name_space, + private Table $base_table ) { - $questions_linking_table_definition = CoreTables::Linking; - $questions_table_definition = CoreTables::Questions; - $answer_form_table_definition = CoreTables::AnswerForms; - $questions_id_column = $questions_table_definition->getIdColumn( - $this->persistence_factory - ); - - $this->select[] = $this->persistence_factory->select( - $questions_linking_table_definition->getColumns( - $this->persistence_factory - ) - ); - - $this->select[] = $this->persistence_factory->select( - $questions_table_definition->getColumns( - $this->persistence_factory - ) - ); - - $this->select[] = $this->persistence_factory->select( - $answer_form_table_definition->getColumns( - $this->persistence_factory - ) - ); - - $this->joins[] = $this->persistence_factory->join( - $questions_linking_table_definition->getIdColumn( - $this->persistence_factory - ), - $questions_table_definition->getIdColumn( - $this->persistence_factory - ), - JoinType::Inner - ); - - $this->joins[] = $this->persistence_factory->join( - $questions_id_column, - $answer_form_table_definition->getForeignKeyColumn( - $this->persistence_factory - ), - JoinType::Left - ); - - $this->order[] = $this->persistence_factory->order( - $questions_id_column - ); - - $this->order[] = $this->persistence_factory->order( - $answer_form_table_definition->getIdColumn( - $this->persistence_factory - ) - ); - } - - public function getPersistenceFactory(): Factory - { - return $this->persistence_factory; - } - - public function getPersistenceForDefinitionClass( - string $definition_class - ): Persistence { - return $this->answer_form_factory - ->getDefinitionForClass($definition_class) - ->getPersistence(); } public function getTableNameBuilder( - string $definition_class + ?TableSubNameSpace $table_sub_name_space ): TableNameBuilder { return new TableNameBuilder( - $this->answer_form_factory - ->getDefinitionForClass($definition_class) - ->getPersistence() - ->getTableNameSpace() + $this->component_name_space, + $table_sub_name_space ); } @@ -168,12 +103,9 @@ public function withRange( return $clone; } - public function loadNextRecord(): \Generator - { - $alias = CoreTables::Questions->getIdColumn( - $this->persistence_factory - )->getColumnAlias(); - + public function loadNextRecord( + ?Column $group_by + ): \Generator { $result = $this->toSql(); $this->current_record = [$this->db->fetchAssoc($result)]; @@ -181,15 +113,15 @@ public function loadNextRecord(): \Generator return null; } - while (($db_record = $this->db->fetchAssoc($result)) !== null) { - if ($db_record[$alias] === $this->current_record[0][$alias]) { - $this->current_record[] = $db_record; - continue; - } - yield $this; - $this->current_record = [$db_record]; + if ($group_by === null) { + yield from $this->loadNextRecordUngrouped($result); + return; } - yield $this; + + yield from $this->loadNextRecordGrouped( + $result, + $group_by->getColumnAlias() + ); } public function retrieveCurrentRecord( @@ -218,7 +150,7 @@ private function toSql(): \ilDBStatement static fn(array $c, Select $v): array => [...$c, ...$v->toColumnsArray()], [] ) - ) . ' FROM ' . CoreTables::Linking->value + ) . " FROM {$this->base_table->getName()}" . array_reduce( $this->joins, static fn(string $c, Join $v): string => $c . PHP_EOL . $v->toSql(), @@ -272,7 +204,31 @@ private function addValueToBinding( } } - public function filterDataSetByTable( + private function loadNextRecordGrouped( + \ilDBStatement $result, + string $group_by + ): \Generator { + while (($db_record = $this->db->fetchAssoc($result)) !== null) { + if ($db_record[$group_by] === $this->current_record[0][$group_by]) { + $this->current_record[] = $db_record; + continue; + } + yield $this; + $this->current_record = [$db_record]; + } + yield $this; + } + + private function loadNextRecordUngrouped( + \ilDBStatement $result + ): \Generator { + while (($db_record = $this->db->fetchAssoc($result)) !== null) { + $this->current_record = $db_record; + yield $this; + } + } + + private function filterDataSetByTable( string $table_name, array $data_set ): array { diff --git a/components/ILIAS/Questions/src/Persistence/Storable.php b/components/ILIAS/Questions/src/Persistence/Storable.php index 82290134d3e6..82e4478aed65 100644 --- a/components/ILIAS/Questions/src/Persistence/Storable.php +++ b/components/ILIAS/Questions/src/Persistence/Storable.php @@ -23,10 +23,12 @@ interface Storable { public function toStorage( + Factory $persistence_factory, Manipulate $manipulate ): Manipulate; public function toDelete( + Factory $persistence_factory, Manipulate $manipulate ): Manipulate; } diff --git a/components/ILIAS/Questions/src/Persistence/Table.php b/components/ILIAS/Questions/src/Persistence/Table.php index 443c8b2885bd..f1c26a97c039 100644 --- a/components/ILIAS/Questions/src/Persistence/Table.php +++ b/components/ILIAS/Questions/src/Persistence/Table.php @@ -23,21 +23,17 @@ class Table { public function __construct( - private readonly CoreTables|TableTypes $table_definition, - private readonly ?TableNameBuilder $table_name_builder, - private readonly string $table_identifier + private readonly TableNameBuilder $table_name_builder, + private readonly TableTypes $table, + private readonly string $sub_table_identifier ) { } public function getName(): string { - if ($this->table_definition instanceof CoreTables) { - return $this->table_definition->value; - } - return $this->table_name_builder->getTableNameFor( - $this->table_definition, - $this->table_identifier + $this->table, + $this->sub_table_identifier ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/Persistence/TableDefinitions.php similarity index 67% rename from components/ILIAS/Questions/src/AnswerForm/Persistence.php rename to components/ILIAS/Questions/src/Persistence/TableDefinitions.php index f46c4dd7fb73..975382281285 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence.php +++ b/components/ILIAS/Questions/src/Persistence/TableDefinitions.php @@ -18,43 +18,39 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm; +namespace ILIAS\Questions\Persistence; use ILIAS\Questions\Persistence\Column; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Questions\Persistence\TableTypes; -interface Persistence +interface TableDefinitions { - public function getTableNameSpace(): TableNameSpace; + public function getTableSubNameSpace(): ?TableSubNameSpace; public function getColumns( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '', + string $sub_table_identifier = '', array $columns_to_skip = [] ): array; public function getIdColumn( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '' + string $sub_table_identifier = '' ): Column; public function getForeignKeyColumn( - PersistenceFactory $persistence_factory, TableNameBuilder $table_name_builder, TableTypes $table_type, - string $table_identifier = '' + string $sub_table_identifier = '' ): Column; - public function completeQuestionsQuery( + public function completeQuery( Query $query, - Column $base_table_id_column, + ?Column $base_table_id_column ): Query; } diff --git a/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php index 7d007559198f..e5cd818baa10 100644 --- a/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php +++ b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php @@ -22,29 +22,42 @@ class TableNameBuilder { - private string $type_specific_part; - public function __construct( - TableNameSpace $table_name_space + private readonly string $component_name_space, + private readonly ?TableSubNameSpace $table_sub_name_space ) { - $this->type_specific_part = $table_name_space->getTypeSpecificTableNamePart(); + } public function getTableNameFor( - TableTypes $type, - string $identifier = '' + TableTypes $table, + string $specifier = '' ): string { - if ($type === TableTypes::Additional && $identifier === '') { + if ($table->value === '' && $specifier === '') { throw \InvalidArgumentException( - 'Identifier cannot be empty for type ' . TableTypes::Additional->name . '.' + 'Identifier cannot be empty if Type->value is empty.' ); } - return match ($type) { - TableTypes::TypeSpecificAnswerForms => "qsts_answer_forms_{$this->type_specific_part}", - TableTypes::AnswerInputs => "qsts_answer_inputs_{$this->type_specific_part}", - TableTypes::AnswerOptions => "qsts_answer_options_{$this->type_specific_part}", - TableTypes::Responses => "qsts_responses_{$this->type_specific_part}", - TableTypes::Additional => "qsts_{$this->type_specific_part}_{$identifier}" - }; + + $base_name = "{$this->component_name_space}"; + if ($table->value !== '') { + $base_name .= "_{$table->value}"; + } + + $additions = ''; + + if ($this->table_sub_name_space !== null) { + $additions = $this->table_sub_name_space->get(); + } + + if ($specifier !== '') { + $additions .= "_{$specifier}"; + } + + if ($additions === '') { + return $base_name; + } + + return "{$base_name}_{$additions}"; } } diff --git a/components/ILIAS/Questions/src/Persistence/TableNameSpace.php b/components/ILIAS/Questions/src/Persistence/TableSubNameSpace.php similarity index 56% rename from components/ILIAS/Questions/src/Persistence/TableNameSpace.php rename to components/ILIAS/Questions/src/Persistence/TableSubNameSpace.php index 2c8d735bfd20..fee67a8902f3 100644 --- a/components/ILIAS/Questions/src/Persistence/TableNameSpace.php +++ b/components/ILIAS/Questions/src/Persistence/TableSubNameSpace.php @@ -20,25 +20,32 @@ namespace ILIAS\Questions\Persistence; -class TableNameSpace +class TableSubNameSpace { /** - * @param string $vendor Maximum four characters used to create the tables for the question type - * @param string $answer_form_id Maximum eight characters used to create the tables for the question type + * @param string $vendor Maximum four characters used to create the tables + * @param string $sub_name_space Maximum eight characters used to create the tables */ public function __construct( private readonly string $vendor, - private readonly string $answer_form_id + private readonly string $sub_name_space ) { - if (mb_strlen($vendor) > 4 || mb_strlen($answer_form_id) > 8) { + if ($vendor === '' + || $sub_name_space === '' + || mb_strlen($vendor) > 6 + || mb_strlen($sub_name_space) > 8) { throw new \InvalidArgumentException( - '$vendor cannot be longer than 4, $answer_form_id can be longer then 8 characters.' + '$vendor cannot be empty or longer than 6, ' + . '$sub_name_space cannot be empty or longer than 8 characters.' ); } } - public function getTypeSpecificTableNamePart(): string + public function get(): string { - return "{$this->vendor}_{$this->answer_form_id}"; + if ($this->vendor === 'ILIAS') { + return $this->sub_name_space; + } + return "{$this->vendor}_{$this->sub_name_space}"; } } diff --git a/components/ILIAS/Questions/src/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Persistence/TableTypes.php index 7caed2f133de..09a938572931 100644 --- a/components/ILIAS/Questions/src/Persistence/TableTypes.php +++ b/components/ILIAS/Questions/src/Persistence/TableTypes.php @@ -20,29 +20,6 @@ namespace ILIAS\Questions\Persistence; -enum TableTypes +interface TableTypes { - case TypeSpecificAnswerForms; - case AnswerInputs; - case AnswerOptions; - case Responses; - case Additional; - - public function getTable( - Factory $persistence_factory, - TableNameBuilder $table_name_builder, - string $table_identifier = '' - ): Table { - return match($this) { - self::Additional => $persistence_factory->table( - $this, - $table_name_builder, - $table_identifier - ), - default => $persistence_factory->table( - $this, - $table_name_builder - ) - }; - } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 2137fa56b906..ef2a66328e86 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -23,7 +23,7 @@ use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Persistence\Repository; +use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; use ILIAS\UI\Component\Table\DataRetrieval; diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 7a9446077eda..51e8f474af84 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -36,7 +36,7 @@ use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; -use ILIAS\Questions\Persistence\Repository; +use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\URI; diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php similarity index 73% rename from components/ILIAS/Questions/src/Persistence/Repository.php rename to components/ILIAS/Questions/src/Question/Persistence/Repository.php index 51786b44cb3d..4615589fba7f 100644 --- a/components/ILIAS/Questions/src/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -18,12 +18,21 @@ declare(strict_types=1); -namespace ILIAS\Questions\Persistence; +namespace ILIAS\Questions\Question\Persistence; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; +use ILIAS\Questions\Persistence\Operator; +use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; +use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\Order as DataOrder; use ILIAS\Data\Range as DataRange; @@ -32,13 +41,23 @@ class Repository { + public const string COMPONENT_NAMESPACE = 'qsts'; + + private readonly TableNameBuilder $question_table_names_builder; + public function __construct( private readonly \ilDBInterface $db, private readonly Refinery $refinery, private readonly UuidFactory $uuid_factory, - private readonly Factory $persistence_factory, + private readonly PersistenceFactory $persistence_factory, + private readonly TableDefinitions $question_table_definitions, + private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, private readonly AnswerFormFactory $answer_form_factory ) { + $this->question_table_names_builder = new TableNameBuilder( + self::COMPONENT_NAMESPACE, + null + ); } public function getNew( @@ -73,8 +92,9 @@ public function getQuestionDataOnlyForQuestionIds( ): \Generator { foreach ($this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( - CoreTables::Questions->getIdColumn( - $this->persistence_factory + $this->question_table_definitions->getIdColumn( + $this->question_table_names_builder, + TableTypes::Questions ), $this->persistence_factory->value( \ilDBConstants::T_TEXT, @@ -99,8 +119,9 @@ public function getForQuestionId( return $this->getForBaseQuery( $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( - CoreTables::Questions->getIdColumn( - $this->persistence_factory + $this->question_table_definitions->getIdColumn( + $this->question_table_names_builder, + TableTypes::Questions ), $this->persistence_factory->value( \ilDBConstants::T_TEXT, @@ -124,8 +145,9 @@ public function getForQuestionIds( yield from $this->getForBaseQuery( $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( - CoreTables::Questions->getIdColumn( - $this->persistence_factory + $this->question_table_definitions->getIdColumn( + $this->question_table_names_builder, + TableTypes::Questions ), $this->persistence_factory->value( \ilDBConstants::T_TEXT, @@ -153,10 +175,7 @@ public function create( ->withPageId($this->buildQuestionPage($v->getParentObjId())), $questions ), - new Manipulate( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, + $this->buildManipulate( ManipulationType::Create ) ); @@ -170,10 +189,7 @@ public function update( ): void { $this->store( $questions, - new Manipulate( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, + $this->buildManipulate( ManipulationType::Update ) ); @@ -184,11 +200,14 @@ public function delete( ): void { array_reduce( $questions, - fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), - new Manipulate( - $this->db, - $this->persistence_factory, - $this->answer_form_factory, + fn(Manipulate $c, QuestionImplementation $v): Manipulate + => $v->toDelete( + $c, + $this->persistence_factory, + $this->question_table_definitions, + $this->answer_form_generic_table_definitions + ), + $this->buildManipulate( ManipulationType::Delete ) )->run(); @@ -208,10 +227,11 @@ private function getForBaseQuery( ): \Generator { $query_with_answer_forms = array_reduce( $this->getAnswerFormTypesForQuestionIds($question_ids), - fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuestionsQuery( + fn(Query $c, AnswerFormDefinition $v) => $v->getTableDefinitions()->completeQuery( $c, - CoreTables::AnswerForms->getIdColumn( - $this->persistence_factory + $this->answer_form_generic_table_definitions->getIdColumn( + $this->question_table_names_builder, + AnswerFormGenericTableTypes::AnswerForms ) ), $query @@ -235,15 +255,17 @@ private function retrieveQuestionFromQuery( array $answer_forms ): QuestionImplementation { $linking_info = $query->retrieveCurrentRecord( - CoreTables::Linking->getTable( - $query->getPersistenceFactory() + $this->persistence_factory->table( + $this->question_table_names_builder, + TableTypes::Linking ), $this->refinery->identity() ); $question = $query->retrieveCurrentRecord( - CoreTables::Questions->getTable( - $query->getPersistenceFactory() + $this->persistence_factory->table( + $this->question_table_names_builder, + TableTypes::Questions, ), $this->refinery->custom()->transformation( fn(array $vs): QuestionImplementation => new QuestionImplementation( @@ -279,8 +301,9 @@ private function retrieveAnswerFormsFromQuery( Query $query ): array { return $query->retrieveCurrentRecord( - CoreTables::AnswerForms->getTable( - $query->getPersistenceFactory() + $this->persistence_factory->table( + $this->question_table_names_builder, + AnswerFormGenericTableTypes::AnswerForms ), $this->refinery->custom()->transformation( function (array $vs) use ($query): array { @@ -315,8 +338,11 @@ function (array $vs) use ($query): array { private function getAnswerFormTypesForQuestionIds( array $question_ids ): array { + $table_name = $this->question_table_names_builder + ->getTableNameFor(AnswerFormGenericTableTypes::AnswerForms); + $query = $this->db->query( - 'SELECT DISTINCT type FROM ' . CoreTables::AnswerForms->value . PHP_EOL + "SELECT DISTINCT type FROM {$table_name}" . PHP_EOL . "WHERE {$this->db->in( 'question_id', $question_ids, @@ -340,80 +366,53 @@ private function store( ): void { array_reduce( $questions, - fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toStorage($c), + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toStorage( + $c, + $this->persistence_factory, + $this->question_table_definitions, + $this->answer_form_generic_table_definitions + ), $manipulate )->run(); } private function buildQuestionsQuery(): Query { - $query = new Query( - $this->db, - $this->refinery, - $this->persistence_factory, - $this->answer_form_factory - ); - - $questions_linking_table_definition = CoreTables::Linking; - $questions_table_definition = CoreTables::Questions; - $answer_form_table_definition = CoreTables::AnswerForms; - $questions_id_column = $questions_table_definition->getIdColumn( - $this->persistence_factory - ); - - return $query->withAdditionalSelect( - $this->persistence_factory->select( - $questions_linking_table_definition->getColumns( - $this->persistence_factory - ) - ) - )->withAdditionalSelect( - $this->select[] = $this->persistence_factory->select( - $questions_table_definition->getColumns( - $this->persistence_factory - ) - ) - )->withAdditionalSelect( - $this->select[] = $this->persistence_factory->select( - $answer_form_table_definition->getColumns( - $this->persistence_factory - ) - ) - )->withAdditionalJoin( - $this->joins[] = $this->persistence_factory->join( - $questions_linking_table_definition->getIdColumn( - $this->persistence_factory - ), - $questions_table_definition->getIdColumn( - $this->persistence_factory - ), - JoinType::Inner - ) - )->withAdditionalJoin( - $this->joins[] = $this->persistence_factory->join( - $questions_id_column, - $answer_form_table_definition->getForeignKeyColumn( - $this->persistence_factory + return $this->answer_form_generic_table_definitions->completeQuery( + $this->question_table_definitions->completeQuery( + new Query( + $this->db, + $this->refinery, + self::COMPONENT_NAMESPACE, + $this->persistence_factory->table( + $this->question_table_names_builder, + TableTypes::Linking + ) ), - JoinType::Left - ) - )->withAdditionalOrder( - $this->persistence_factory->order( - $questions_id_column - ) - )->withAdditionalOrder( - $this->order[] = $this->persistence_factory->order( - $answer_form_table_definition->getIdColumn( - $this->persistence_factory - ) + null + ), + $this->question_table_definitions->getIdColumn( + $this->question_table_names_builder, + TableTypes::Questions ) ); } + private function buildManipulate( + ManipulationType $manipulation_type + ): Manipulate { + return new Manipulate( + $this->db, + $manipulation_type, + self::COMPONENT_NAMESPACE + ); + } + private function buildGroupByColumn(): Column { - return CoreTables::Questions->getIdColumn( - $this->persistence_factory + return $this->question_table_definitions->getIdColumn( + $this->question_table_names_builder, + TableTypes::Questions ); } @@ -430,10 +429,13 @@ private function buildAvailableUuid(): Uuid private function checkAvailabilityOfId( Uuid $uuid ): bool { + $table_name = $this->question_table_names_builder + ->getTableNameFor(TableTypes::Questions); + return $this->db->fetchObject( $this->db->query( - 'SELECT COUNT(*) as cnt FROM ' . CoreTables::Questions->value - . " WHERE id='{$uuid->toString()}'" + "SELECT COUNT(*) as cnt FROM {$table_name}" . PHP_EOL + . "WHERE id='{$uuid->toString()}'" ) )->cnt === 0; } @@ -467,9 +469,12 @@ private function getNextAvailableQuestionPageId(): int private function migrateQuestionPage( QuestionImplementation $question ): QuestionImplementation { + $table_name = $this->question_table_names_builder + ->getTableNameFor(TableTypes::MigrationsTable); + $old_page_id = $this->db->fetchObject( $this->db->query( - 'SELECT old_question_id FROM ' . CoreTables::MigrationsTable->value . PHP_EOL + "SELECT old_question_id FROM {$table_name}" . PHP_EOL . "WHERE new_question_id = {$this->db->quote($question->getId(), \ilDBConstants::T_TEXT)}" ) )->old_question_id; diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php new file mode 100644 index 000000000000..7cd687d4bcfc --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php @@ -0,0 +1,202 @@ +persistence_factory->table( + $table_name_builder, + $table_type + ); + $column_identifiers = match($table_type) { + TableTypes::Questions => self::QUESTION_TABLE_COLUMNS, + TableTypes::Linking => self::LINKING_TABLE_COLUMNS, + TableTypes::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS + }; + return array_map( + fn(string $v): Column => $this->persistence_factory->column( + $table, + $v + ), + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + #[\Override] + public function getIdColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return match($table_type) { + TableTypes::Questions => $this->persistence_factory->column( + $table, + self::QUESTION_TABLE_ID_COLUMN + ), + TableTypes::Linking => $this->persistence_factory->column( + $table, + self::LINKING_TABLE_ID_COLUMN + ), + TableTypes::MigrationsTable => $this->persistence_factory->column( + $table, + self::MIGRATIONS_TABLE_ID_COLUMN + ) + }; + } + + #[\Override] + public function getForeignKeyColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return match($table_type) { + TableTypes::Linking => $this->persistence_factory->column( + $table, + self::LINKING_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::MigrationsTable => $this->persistence_factory->column( + $table, + self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN + ), + default => null + }; + } + + #[\Override] + public function completeQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_name_builder = $query->getTableNameBuilder(null); + $questions_id_column = $this->getIdColumn( + $table_name_builder, + TableTypes::Questions + ); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + TableTypes::Linking + ) + ) + )->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + TableTypes::Questions + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $this->getIdColumn( + $table_name_builder, + TableTypes::Linking + ), + $questions_id_column, + JoinType::Inner + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $questions_id_column + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $this->getIdColumn( + $table_name_builder, + TableTypes::Linking + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php new file mode 100644 index 000000000000..49229b969564 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Persistence/TableTypes.php @@ -0,0 +1,31 @@ +getManipulationType() === ManipulationType::Create - ? $this->addInsertStatementsToManipulation($manipulate) - : $this->addUpdateStatementsToManipulation($manipulate); + ? $this->addInsertStatementsToManipulation( + $manipulate, + $persistence_factory, + $question_tables_definitions + ) : $this->addUpdateStatementsToManipulation( + $manipulate, + $persistence_factory, + $question_tables_definitions, + $answer_form_generic_table_definitions + ); } public function toDelete( - Manipulate $manipulate + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions ): Manipulate { + $table_name_builder = $manipulate->getTableNameBuilder(null); + return $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate->withAdditionalStatement( $this->buildDeleteQuestionStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $table_name_builder ) )->withAdditionalStatement( $this->buildDeleteLinkingStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $table_name_builder ) )->withAdditionalStatement( $this->buildDeleteMigrationStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $table_name_builder ) ), + $persistence_factory, + $answer_form_generic_table_definitions, $this->answer_forms ); } private function addInsertStatementsToManipulation( - Manipulate $manipulate + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions ): Manipulate { if ($this->created === null) { $manipulate = $manipulate ->withAdditionalStatement( $this->buildInsertLinkingStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $manipulate->getTableNameBuilder(null) ) )->withAdditionalStatement( $this->buildInsertQuestionStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $manipulate->getTableNameBuilder(null) ) ); } @@ -372,19 +404,30 @@ private function addInsertStatementsToManipulation( } private function addUpdateStatementsToManipulation( - Manipulate $manipulate + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions ): Manipulate { + $table_name_builder = $manipulate->getTableNameBuilder(null); + if ($this->linking_information_updated) { $manipulate = $manipulate ->withAdditionalStatement( - $this->buildUpdateLinkingStatement() + $this->buildUpdateLinkingStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) ); } if ($this->self_updated) { $manipulate = $manipulate->withAdditionalStatement( $this->buildUpdateQuestionStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $table_name_builder ) ); } @@ -392,7 +435,9 @@ private function addUpdateStatementsToManipulation( if ($this->page_id) { $manipulate = $manipulate->withAdditionalStatement( $this->buildUpdatePageIdStatement( - $manipulate->getPersistenceFactory() + $persistence_factory, + $question_tables_definitions, + $table_name_builder ) ); } @@ -400,37 +445,65 @@ private function addUpdateStatementsToManipulation( if ($this->deleted_answer_forms !== []) { $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, $this->deleted_answer_forms ); } return $this->addAnswerFormStatementsToManipulate( $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, $this->updated_answer_forms ); } private function addAnswerFormStatementsToManipulate( Manipulate $manipulate, + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_forms ): Manipulate { return array_reduce( $answer_forms, - fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toStorage( - $v->getTypeGenericProperties()->toStorage($c) - ), + function ( + Manipulate $c, + AnswerFormProperties $v + ) use ( + $persistence_factory, + $answer_form_generic_table_definitions + ): Manipulate { + $manipulate_with_generic_properties = $v->getTypeGenericProperties() + ->toStorage( + $persistence_factory, + $answer_form_generic_table_definitions, + $c + ); + + return $v->toStorage( + $persistence_factory, + $manipulate_with_generic_properties + ); + }, $manipulate ); } private function addDeleteAnswerFormsStatementsToManipulate( Manipulate $manipulate, + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_forms_to_delete ): Manipulate { return array_reduce( $answer_forms_to_delete, fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( - $v->getTypeGenericProperties()->toDelete($c) + $v->getTypeGenericProperties()->toDelete( + $persistence_factory, + $answer_form_generic_table_definitions, + $c, + ) ), $manipulate ); @@ -439,11 +512,14 @@ private function addDeleteAnswerFormsStatementsToManipulate( private function buildInsertLinkingStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder ): Insert { return $persistence_factory->insert( - CoreTables::Linking->getColumns( - $persistence_factory + $question_tables_definitions->getColumns( + $table_name_builder, + QuestionTableTypes::Linking ), [ $persistence_factory->value( @@ -463,11 +539,14 @@ private function buildInsertLinkingStatement( } private function buildInsertQuestionStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder ): Insert { return $persistence_factory->insert( - CoreTables::Questions->getColumns( - $persistence_factory + $question_tables_definitions->getColumns( + $table_name_builder, + QuestionTableTypes::Questions ), [ $persistence_factory->value( @@ -511,13 +590,17 @@ private function buildInsertQuestionStatement( } private function buildUpdateLinkingStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder ): Update { - $linking_table_definition = CoreTables::Linking; + $table_type = QuestionTableTypes::Linking; return $persistence_factory->update( - $linking_table_definition->getColumns( - $persistence_factory, - [CoreTables::LINKING_TABLE_ID_COLUMN] + $question_tables_definitions->getColumns( + $table_name_builder, + $table_type, + '', + [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] ), [ $persistence_factory->value( @@ -531,7 +614,7 @@ private function buildUpdateLinkingStatement( ], [ $persistence_factory->where( - $linking_table_definition->getIdColumn( + $table_type->getIdColumn( $persistence_factory ), $persistence_factory->value( @@ -544,14 +627,18 @@ private function buildUpdateLinkingStatement( } private function buildUpdateQuestionStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder ): Update { - $questions_table_definition = CoreTables::Questions; + $table_type = QuestionTableTypes::Questions; return $persistence_factory->update( - $questions_table_definition->getColumns( - $persistence_factory, + $question_tables_definitions->getColumns( + $table_name_builder, + $table_type, + '', [ - CoreTables::ANSWER_FORM_TABLE_ID_COLUMN, + 'id', 'page_id', 'created' ] @@ -584,8 +671,9 @@ private function buildUpdateQuestionStatement( ], [ $persistence_factory->where( - $questions_table_definition->getIdColumn( - $persistence_factory + $question_tables_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -597,15 +685,22 @@ private function buildUpdateQuestionStatement( } private function buildDeleteQuestionStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder ): Delete { - $table_definition = CoreTables::Questions; + $table_type = QuestionTableTypes::Questions; return $persistence_factory->delete( - $table_definition->getTable($persistence_factory), + $persistence_factory->table( + $table_name_builder, + $table_type + ), [ $persistence_factory->where( - $table_definition->getIdColumn( - $persistence_factory + $question_tables_definitions->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -617,15 +712,21 @@ private function buildDeleteQuestionStatement( } private function buildDeleteLinkingStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder ): Delete { - $table_definition = CoreTables::Linking; + $table_type = QuestionTableTypes::Linking; return $persistence_factory->delete( - $table_definition->getTable($persistence_factory), + $persistence_factory->table( + $table_name_builder, + $table_type + ), [ $persistence_factory->where( - $table_definition->getIdColumn( - $persistence_factory + $table_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -641,15 +742,21 @@ private function buildDeleteLinkingStatement( * this MUST go! */ private function buildDeleteMigrationStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder ): Delete { - $table_definition = CoreTables::MigrationsTable; + $table_type = QuestionTableTypes::MigrationsTable; return $persistence_factory->delete( - $table_definition->getTable($persistence_factory), + $persistence_factory->table( + $table_name_builder, + $table_type + ), [ $persistence_factory->where( - $table_definition->getIdColumn( - $persistence_factory + $table_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, @@ -665,17 +772,25 @@ private function buildDeleteMigrationStatement( * this a question MUST never change the page assigned to it after its creation! */ private function buildUpdatePageIdStatement( - PersistenceFactory $persistence_factory + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder ): Update { - $questions_table_definition = CoreTables::Questions; + $table_type = QuestionTableTypes::Questions; return $persistence_factory->update( [ $persistence_factory->column( - $questions_table_definition->getTable($persistence_factory), + $persistence_factory->table( + $table_name_builder, + $table_type + ), 'page_id' ), $persistence_factory->column( - $questions_table_definition->getTable($persistence_factory), + $persistence_factory->table( + $table_name_builder, + $table_type + ), 'last_update' ) ], @@ -691,8 +806,9 @@ private function buildUpdatePageIdStatement( ], [ $persistence_factory->where( - $questions_table_definition->getIdColumn( - $persistence_factory + $table_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( \ilDBConstants::T_TEXT, diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index 19f96fb48107..ad55883a11f6 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -20,9 +20,12 @@ namespace ILIAS\Questions\Setup; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableNameSpaceCore; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Refinery\Transformation; use ILIAS\Setup\Agent as SetupAgent; use ILIAS\Setup\Agent\HasNoNamedObjective; @@ -38,6 +41,7 @@ class Agent implements SetupAgent public function __construct( private readonly PersistenceFactory $persistence_factory, + private readonly TableNameBuilder $question_table_name_builder, private readonly array $answer_form_migrations ) { } @@ -60,12 +64,24 @@ public function getUpdateObjective( 'Database is updated for ILIAS\Questions', false, new \ilDatabaseUpdateStepsExecutedObjective( - new OverarchingQuestionTables() + new OverarchingQuestionTables( + $this->question_table_name_builder + ) ), new \ilDatabaseUpdateStepsExecutedObjective( new ClozeQuestionTables( - new TableNameBuilder( - new TableNameSpaceCore('cloze') + new SetupTableNameBuilder( + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) + ), + new TableDefinitions( + $this->persistence_factory, + new TableSubNameSpace( + 'ILIAS', + 'cloze' + ) ) ) ), @@ -85,12 +101,14 @@ public function getStatusObjective( true, new \ilDatabaseUpdateStepsMetricsCollectedObjective( $storage, - new OverarchingQuestionTables() + new OverarchingQuestionTables( + $this->question_table_name_builder + ) ), new \ilDatabaseUpdateStepsMetricsCollectedObjective( $storage, new ClozeQuestionTables( - new TableNameBuilder( + new SetupTableNameBuilder( new TableNameSpaceCore('cloze') ) ) @@ -104,6 +122,13 @@ public function getMigrations(): array return [ new QuestionsMigration( $this->persistence_factory, + $this->question_table_name_builder, + new QuestionTableDefinitions( + $this->persistence_factory + ), + new AnswerFormGenericTableDefinitions( + $this->persistence_factory + ), $this->answer_form_migrations ) ]; diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index 2ca3f8714441..29707fcab9f0 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -20,16 +20,16 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\AnswerFormTypes\Cloze\Persistence; -use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; class ClozeQuestionTables implements \ilDatabaseUpdateSteps { protected \ilDBInterface $db; public function __construct( - private readonly TableNameBuilder $table_name_builder + private readonly SetupTableNameBuilder $table_name_builder, + private readonly TableDefinitions $table_definitions ) { } @@ -42,7 +42,9 @@ public function prepare( public function step_1(): void { - $table_name = $this->table_name_builder->getTableNameFor(TableTypes::TypeSpecificAnswerForms); + $table_name = $this->table_name_builder->getTableNameFor( + AnswerFormSpecificTableTypes::TypeSpecificAnswerForms + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ @@ -70,7 +72,9 @@ public function step_1(): void public function step_2(): void { - $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerInputs); + $table_name = $this->table_name_builder->getTableNameFor( + AnswerFormSpecificTableTypes::AnswerInputs + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -131,7 +135,9 @@ public function step_2(): void public function step_3(): void { - $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerOptions); + $table_name = $this->table_name_builder->getTableNameFor( + AnswerFormSpecificTableTypes::AnswerOptions + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -181,8 +187,8 @@ public function step_3(): void public function step_4(): void { $table_name = $this->table_name_builder->getTableNameFor( - TableTypes::Additional, - Persistence::COMBINATION_TABLE_IDENTIFIER + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationsTableIdentifier() ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ @@ -215,8 +221,8 @@ public function step_4(): void public function step_5(): void { $table_name = $this->table_name_builder->getTableNameFor( - TableTypes::Additional, - Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + AnswerFormSpecificTableTypes::Additional, + $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ @@ -254,7 +260,9 @@ public function step_5(): void public function step_6(): void { - $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Responses); + $table_name = $this->table_name_builder->getTableNameFor( + AnswerFormSpecificTableTypes::Responses + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 138ba06ace37..7f12a7d249dc 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -20,12 +20,20 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\Persistence\CoreTables; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\FeedbackTableTypes; +use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; +use ILIAS\Questions\Persistence\TableNameBuilder; class OverarchingQuestionTables implements \ilDatabaseUpdateSteps { protected \ilDBInterface $db; + public function __construct( + private readonly TableNameBuilder $basic_table_name_builder + ) { + } + #[\Override] public function prepare( \ilDBInterface $db @@ -35,7 +43,9 @@ public function prepare( public function step_1(): void { - $table_name = CoreTables::Questions->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + QuestionTableTypes::Questions + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -93,7 +103,9 @@ public function step_1(): void public function step_2(): void { - $table_name = CoreTables::AnswerForms->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + AnswerFormGenericTableTypes::AnswerForms + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -147,7 +159,9 @@ public function step_2(): void public function step_3(): void { - $table_name = CoreTables::Responses->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + QuestionTableTypes::Responses + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ @@ -178,7 +192,9 @@ public function step_3(): void public function step_4(): void { - $table_name = CoreTables::Linking->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + QuestionTableTypes::Linking + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'question_id' => [ @@ -210,7 +226,9 @@ public function step_4(): void public function step_5(): void { - $table_name = CoreTables::MigrationsTable->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + QuestionTableTypes::MigrationsTable + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'new_question_id' => [ @@ -244,7 +262,9 @@ public function step_5(): void public function step_6(): void { - $table_name = CoreTables::FeedbackGeneric->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + FeedbackTableTypes::FeedbackGeneric + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ @@ -284,7 +304,9 @@ public function step_6(): void public function step_7(): void { - $table_name = CoreTables::FeedbackSpecific->value; + $table_name = $this->basic_table_name_builder->getTableNameFor( + FeedbackTableTypes::FeedbackSpecific + ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 6f9e9a9265ee..28a1fa368903 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -22,11 +22,13 @@ use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert as AnswerFormMigrationInsert; -use ILIAS\Questions\Persistence\CoreTables; -use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; +use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; +use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup; @@ -51,6 +53,9 @@ class QuestionsMigration implements Migration public function __construct( private readonly PersistenceFactory $persistence_factory, + private readonly TableNameBuilder $question_table_name_builder, + private readonly QuestionTableDefinitions $question_table_definitions, + private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_form_migrations ) { $this->answer_form_migrations = array_reduce( @@ -165,10 +170,14 @@ public function step( #[\Override] public function getRemainingAmountOfSteps(): int { + $migration_table_name = $this->question_table_name_builder + ->getTableNameFor(QuestionTableTypes::MigrationsTable); + $query = $this->db->query( 'SELECT COUNT(question_id) cnt FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL - . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . "LEFT JOIN {$migration_table_name}" + . ' m ON q.question_id = m.old_question_id' . PHP_EOL . 'WHERE t.type_tag IN (' . implode( ', ', @@ -187,10 +196,14 @@ public function getRemainingAmountOfSteps(): int private function fetchValidRecord(): ?\stdClass { + $migration_table_name = $this->question_table_name_builder + ->getTableNameFor(QuestionTableTypes::MigrationsTable); + $query = $this->db->query( 'SELECT q.*, t.type_tag, s.sequence FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL - . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . "LEFT JOIN {$migration_table_name}" + . ' m ON q.question_id = m.old_question_id' . PHP_EOL . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' s ON q.question_id = s.question_fi' . PHP_EOL . 'WHERE t.type_tag IN (' . implode( @@ -247,9 +260,14 @@ private function cleanupAndMigrateOriginalId( private function loadAlreadyMigratedQuestions(): void { + $migration_table_name = $this->question_table_name_builder + ->getTableNameFor(QuestionTableTypes::MigrationsTable); + $linking_table_name = $this->question_table_name_builder + ->getTableNameFor(QuestionTableTypes::Linking); + $query = $this->db->query( - 'SELECT m.*, o.type FROM ' . CoreTables::MigrationsTable->value . ' m' . PHP_EOL - . 'JOIN ' . CoreTables::Linking->value . ' l' . PHP_EOL + "SELECT m.*, o.type FROM {$migration_table_name} m" . PHP_EOL + . "JOIN {$linking_table_name} l" . PHP_EOL . 'ON m.new_question_id = l.question_id' . PHP_EOL . 'JOIN object_data o ON l.obj_id = o.obj_id' . PHP_EOL ); @@ -297,8 +315,9 @@ private function buildInsertLinkingStatement( ?int $position ): Insert { return $this->persistence_factory->insert( - CoreTables::Linking->getColumns( - $this->persistence_factory + $this->question_table_definitions->getColumns( + $this->question_table_name_builder, + QuestionTableTypes::Linking ), [ $this->persistence_factory->value( @@ -327,8 +346,9 @@ private function buildInsertQuestionStatement( int $create_date ): Insert { return $this->persistence_factory->insert( - CoreTables::Questions->getColumns( - $this->persistence_factory + $this->question_table_definitions->getColumns( + $this->question_table_name_builder, + QuestionTableTypes::Questions ), [ $this->persistence_factory->value( @@ -376,8 +396,9 @@ private function buildInsertMigrationStatement( ?Uuid $new_question_id ): Insert { return $this->persistence_factory->insert( - CoreTables::MigrationsTable->getColumns( - $this->persistence_factory + $this->persistence_factory->getColumns( + $this->question_table_name_builder, + QuestionTableTypes::MigrationsTable ), [ $this->persistence_factory->value( @@ -409,6 +430,8 @@ private function buildMigrationInsert( $this->io, $this->uuid_factory, $this->persistence_factory, + $this->question_table_definitions, + $this->answer_form_generic_table_definitions, new TableNameBuilder( $answer_form_migration->getTableNameSpace() ), diff --git a/components/ILIAS/Questions/src/Setup/SetupTableNameBuilder.php b/components/ILIAS/Questions/src/Setup/SetupTableNameBuilder.php new file mode 100644 index 000000000000..e1d97883d442 --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/SetupTableNameBuilder.php @@ -0,0 +1,35 @@ + Date: Tue, 10 Mar 2026 08:15:23 +0100 Subject: [PATCH 078/108] Questions: Implement Feedback --- .../class.ilObjQuestionsGUI.php | 9 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 32 +- .../Legacy/PageEditor/QstsQuestionPage.php | 13 + .../PageEditor/class.QstsQuestionPageGUI.php | 14 +- .../PageEditor/class.ilPCAnswerFormGUI.php | 6 +- components/ILIAS/Questions/Questions.php | 27 +- .../src/AnswerForm/Capabilities/Action.php | 76 +++ .../AnswerForm/Capabilities/Capability.php | 14 +- .../src/AnswerForm/Capabilities/Factory.php | 40 ++ .../Capabilities/Feedback/Capability.php | 146 +++++ .../Capabilities/Feedback/Feedback.php | 309 ++++++++++- .../Capabilities/Feedback/Migration.php | 347 ++++++++++++ .../Capabilities/Feedback/Overview.php | 207 +++++++ .../Capabilities/Feedback/OverviewTable.php | 44 ++ .../Capabilities/Feedback/Repository.php | 145 +++-- .../Feedback/SpecificFeedback.php | 103 ++++ .../Feedback/TableDefinitions.php | 177 ++++++ ...{FeedbackTableTypes.php => TableTypes.php} | 4 +- .../Capabilities/Feedback/Types.php} | 18 +- .../Capabilities/Marking/Capability.php | 68 +++ .../Capabilities/Marking/Marking.php | 3 +- .../src/AnswerForm/Capabilities/Migration.php | 39 ++ .../Questions/src/AnswerForm/Definition.php | 2 +- .../src/AnswerForm/Migration/Migration.php | 12 +- .../Cloze/Capabilities/Feedback.php | 126 +++-- .../FeedbackOverviewDataRetrieval.php | 135 +++++ .../Capabilities/FeedbackOverviewTable.php | 506 ++++++++++++++++++ .../Cloze/Capabilities/Marking.php | 35 -- .../src/AnswerFormTypes/Cloze/Definition.php | 5 +- .../Cloze/Layout/FeedbackOverview.php | 358 ------------- .../Migration/BasicMigrationFunctions.php | 15 + .../Cloze/Migration/MigrationCloze.php | 122 +++-- .../Cloze/Migration/MigrationLongMenu.php | 10 +- .../Cloze/Migration/MigrationNumeric.php | 10 +- .../Cloze/Migration/MigrationTextSubset.php | 10 +- .../Properties/Combinations/Combinations.php | 6 +- .../Combinations/EditCombinations.php | 11 +- .../Properties/Combinations/MatchingValue.php | 2 +- .../Combinations/Overview.php} | 24 +- .../Cloze/Properties/Gaps/Gaps.php | 34 +- .../Cloze/Properties/Gaps/LongMenu.php | 4 +- .../Cloze/Properties/Gaps/Numeric.php | 42 +- .../Cloze/Properties/Gaps/Select.php | 4 +- .../Cloze/Properties/Gaps/Text.php | 4 +- .../Cloze/Properties/Gaps/Type.php | 58 +- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 9 +- .../ILIAS/Questions/src/Definitions/Range.php | 71 +++ .../src/Persistence/ManipulationType.php | 1 + .../src/Persistence/TableDefinitions.php | 4 +- .../Presentation/Definitions/Environment.php | 6 + .../Definitions/EnvironmentImplementation.php | 50 +- .../Presentation/Layout/QuestionsTable.php | 23 +- .../Questions/src/Presentation/Views/Edit.php | 156 +++--- .../ILIAS/Questions/src/Setup/Agent.php | 10 +- .../src/Setup/OverarchingQuestionTables.php | 32 +- .../src/Setup/QuestionsMigration.php | 26 +- 56 files changed, 2996 insertions(+), 768 deletions(-) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Factory.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/{FeedbackTableTypes.php => TableTypes.php} (85%) rename components/ILIAS/Questions/src/{AnswerFormTypes/Cloze/Properties/Combinations/InRange.php => AnswerForm/Capabilities/Feedback/Types.php} (63%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/FeedbackOverview.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/{Layout/CombinationsOverview.php => Properties/Combinations/Overview.php} (96%) create mode 100644 components/ILIAS/Questions/src/Definitions/Range.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 96fc466fafea..ff384f375286 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -20,9 +20,10 @@ use ILIAS\Questions\Administration\ConfigurationGUI; use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; @@ -56,7 +57,10 @@ public function __construct( $local_dic = LocalDIC::dic(); $this->units_repository = $local_dic[UnitsRepository::class]; - $this->edit_view = $local_dic[Edit::class]; + $this->edit_view = $local_dic[Edit::class] + ->withRequiredCapabilities([ + Feedback::class + ]); $this->configuration_repository = $local_dic[ConfigurationRepository::class]; $this->type = 'qsts'; @@ -128,7 +132,6 @@ public function viewQuestionsObject(): void $this->tpl->setContent( $this->edit_view->show( - $this->toolbar, $this->buildEditQuestionsBaseUri(), $this->object->getId(), $this->object->getRefId() diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 2bb86b72d955..ffe96a8f5292 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -23,6 +23,7 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Capabilities; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableDefinitions as FeedbackTableDefinitions; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; @@ -31,7 +32,6 @@ use ILIAS\Questions\Question\Persistence\Repository as QuestionsRepository; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; -use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -78,6 +78,23 @@ protected static function buildDIC(ILIASContainer $DIC): self => new AnswerFormGenericTableDefinitions( $c[PersistenceFactory::class] ); + $dic[Capabilities\Factory::class] = static fn($c): Capabilities\Factory + => new Capabilities\Factory([ + Capabilities\Feedback\Feedback::class => new Capabilities\Feedback\Capability( + $c[DataFactory::class]->text(), + new Capabilities\Feedback\Repository( + $DIC['ilDB'], + $DIC['refinery'], + $c[UuidFactory::class], + $c[DataFactory::class]->text(), + $c[PersistenceFactory::class], + new FeedbackTableDefinitions( + $c[PersistenceFactory::class] + ) + ) + ), + Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability() + ]); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( $c[UuidFactory::class], @@ -85,8 +102,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Definition::class] ] ); - $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository => - new QuestionsRepository( + $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository + => new QuestionsRepository( $DIC['ilDB'], $DIC['refinery'], $c[UuidFactory::class], @@ -116,6 +133,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilTabs'], $DIC->uiService(), $c[UuidFactory::class], + $c[Capabilities\Factory::class], $c[AnswerFormFactory::class], $c[QuestionsRepository::class], $c[LayoutFactory::class] @@ -183,7 +201,6 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Views\Edit::class] = static fn($c): Cloze\Views\Edit => new Cloze\Views\Edit( - $DIC['ilToolbar'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Views\EditGaps::class] @@ -197,8 +214,11 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Properties\Factory::class], $c[Cloze\TableDefinitions::class], [ - Capabilities\Marking::class => new Cloze\Capabilities\Marking(), - Capabilities\Feedback::class => new Cloze\Capabilities\Feedback() + Capabilities\Feedback\Feedback::class => new Cloze\Capabilities\Feedback( + $c[UuidFactory::class], + $c[DataFactory::class]->text() + ), + Capabilities\Marking\Marking::class => new Cloze\Capabilities\Marking() ], $c[Cloze\Views\Edit::class], $c[Cloze\Views\Participant::class] diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index cc8e263462ca..9f0d1b14a2ec 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -18,10 +18,12 @@ declare(strict_types=1); +use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\QuestionImplementation; class QstsQuestionPage extends ilPageObject { + private readonly Edit $edit_view; private readonly QuestionImplementation $question; #[\Override] @@ -30,6 +32,17 @@ public function getParentType(): string return 'qsts'; } + public function getEditView(): Edit + { + return $this->edit_view; + } + + public function setEditView( + Edit $edit_view + ): void { + $this->edit_view = $edit_view; + } + public function getQuestion(): QuestionImplementation { return $this->question; diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index dd338ed92682..1d312d1ad6cd 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Data\URI; @@ -32,12 +33,19 @@ class QstsQuestionPageGUI extends ilPageObjectGUI public function __construct( QuestionImplementation $question, - int $obj_id + int $obj_id, + ?Edit $edit_view = null ) { parent::__construct('qsts', $question->getPageId()); - $this->obj->setParentId($obj_id); - $this->obj->setQuestion($question); + $this->setEnabledPageFocus(false); + + $this->obj->setQuestion($question); + $this->obj->setParentId($obj_id); + + if ($edit_view !== null) { + $this->obj->setEditView($edit_view); + } } #[\Override] diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index 97db48fcfafe..f4aee1e50d2e 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -18,7 +18,6 @@ declare(strict_types=1); -use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Data\Factory as DataFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -31,7 +30,7 @@ class ilPCAnswerFormGUI extends ilPageContentGUI private readonly ilTabsGUI $tabs; private readonly UIRenderer $ui_renderer; private readonly DataFactory $data_factory; - private readonly Edit $edit_view; + private readonly ?Edit $edit_view; public function __construct( ilPageObject $pg_obj, @@ -44,8 +43,7 @@ public function __construct( $this->ui_renderer = $DIC['ui.renderer']; $this->data_factory = new DataFactory(); - $local_dic = LocalDIC::dic(); - $this->edit_view = $local_dic[Edit::class]; + $this->edit_view = $pg_obj->getEditView(); parent::__construct($pg_obj, $content_obj, $hier_id, $pc_id); } diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index e14a5cedb57c..8fc685442e02 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -20,13 +20,16 @@ namespace ILIAS; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Migration as FeedbackMigration; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableDefinitions as FeedbackTableDefinitions; +use ILIAS\Questions\AnswerForm\Capabilities\Migration as CapabilityMigration; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationCloze; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationLongMenu; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationNumeric; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationTextSubset; -use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions as ClozeTableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableSubNameSpace; @@ -46,6 +49,9 @@ public function init( array | \ArrayAccess &$internal, ): void { $define[] = AnswerFormDefinition::class; + $define[] = AnswerFormMigration::class; + $define[] = CapabilityMigration::class; + $contribute[AgentInterface::class] = static fn() => new Agent( $internal[PersistenceFactory::class], @@ -53,11 +59,12 @@ public function init( 'qsts', null ), - $seek[AnswerFormMigration::class] + $seek[AnswerFormMigration::class], + $seek[CapabilityMigration::class] ); $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze( - new TableDefinitions( + new ClozeTableDefinitions( $internal[PersistenceFactory::class], new TableSubNameSpace( 'ILIAS', @@ -68,7 +75,7 @@ public function init( ); $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu( - new TableDefinitions( + new ClozeTableDefinitions( $internal[PersistenceFactory::class], new TableSubNameSpace( 'ILIAS', @@ -78,7 +85,7 @@ public function init( ); $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric( - new TableDefinitions( + new ClozeTableDefinitions( $internal[PersistenceFactory::class], new TableSubNameSpace( 'ILIAS', @@ -89,7 +96,7 @@ public function init( ); $contribute[AnswerFormMigration::class] = static fn() => new MigrationTextSubset( - new TableDefinitions( + new ClozeTableDefinitions( $internal[PersistenceFactory::class], new TableSubNameSpace( 'ILIAS', @@ -97,6 +104,14 @@ public function init( ) ) ); + + $contribute[CapabilityMigration::class] = static fn() + => new FeedbackMigration( + new FeedbackTableDefinitions( + $internal[PersistenceFactory::class] + ) + ); + $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS( $this, diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php new file mode 100644 index 000000000000..38d32ffe125d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php @@ -0,0 +1,76 @@ + $available_capabilities + */ + public function __construct( + private readonly Capability $capability, + private readonly string $lang_var + ) { + } + + public function getCapability(): Capability + { + return $this->capability; + } + + public function addTab( + EnvironmentImplementation $environment, + \ilTabsGUI $tabs_gui, + Language $lng + ): void { + $action = $this->buildAction(); + $tabs_gui->addTab( + $action, + $lng->txt($this->lang_var), + $environment->withActionParameter($action) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + public function activateTab( + \ilTabsGUI $tabs_gui + ): void { + $action = $this->buildAction(); + $tabs_gui->activateTab($action); + } + + public function isThis( + string $action + ): bool { + return $action === $this->buildAction(); + } + + private function buildAction(): string + { + return self::ACTION_EDIT . '_' . md5($this->capability::class); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index 6111750a1804..674e90a774c4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -20,16 +20,24 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; +use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Renderable; -use ILIAS\Questions\Persistence\Storable; -interface Capability extends Storable +interface Capability { - public function isConfigured(): bool; + public function isAvailableFor( + Properties $answer_form_properties + ): bool; + + public function getEditAction(): ?Action; public function edit( Environment $environment ): self|Async|Renderable; + + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): void; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Factory.php new file mode 100644 index 000000000000..cbb0330100ea --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Factory.php @@ -0,0 +1,40 @@ + $available_capabilities + */ + public function __construct( + private readonly array $available_capabilities + ) { + } + + public function get( + string $class + ): ?Capability { + return $this->available_capabilities[$class] ?? null; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php new file mode 100644 index 000000000000..0000fff26e75 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php @@ -0,0 +1,146 @@ +getTypeGenericProperties() + ->getDefinition() + ->hasCapability( + Feedback::class + ); + } + + #[\Override] + public function getEditAction(): Action + { + return new Action( + $this, + 'feedback' + ); + } + + #[\Override] + public function edit( + Environment $environment + ): Async|Renderable { + $step = $environment->getStep(); + return match ($step) { + '' => $this->buildOverview($environment), + self::STEP_INSERT_LEGACY_TEXTS => $this->buildOverviewWithLegacyTexts( + $environment + ), + self::STEP_SAVE => $this->save($environment), + default => $this->buildOverview($environment)->doAction( + $this->repository, + $step + ) + }; + } + + #[\Override] + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): void { + $this->repository->store( + $answer_form_properties->getAnswerFormId(), + $this->repository->getFor( + $answer_form_properties->getAnswerFormId(), + $answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability(Feedback::class) + )->onAnswerFormUpdate( + $answer_form_properties + ) + ); + } + + private function buildOverview( + Environment $environment + ): Overview { + return new Overview( + $environment, + $this->text_factory, + $this->repository->getFor( + $environment->getAnswerFormId(), + $environment + ->getAnswerFormProperties() + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability(Feedback::class) + ), + $environment->withStepParameter( + self::STEP_SAVE + )->getUrlBuilder(), + $environment->withStepParameter( + self::STEP_INSERT_LEGACY_TEXTS + )->getUrlBuilder() + ); + } + + private function buildOverviewWithLegacyTexts( + Environment $environment + ): Overview { + return $this->buildOverview($environment) + ->withLegacyTextsAsValues(true); + } + + private function save( + Environment $environment + ): Overview { + $feedback = $this->buildOverview($environment)->processForm(); + if ($feedback instanceof Overview) { + return $feedback; + } + + $this->repository->store( + $environment->getAnswerFormId(), + $feedback + ); + + return $environment->redirectTo( + $environment->withDefaultStep()->getUrlBuilder() + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php index 7dcfab474ae3..ae8153ec3037 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php @@ -20,17 +20,314 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Persistence\Delete; +use ILIAS\Questions\Persistence\Factory as PersistenceFactory; +use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\Replace; +use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\Value; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Question\Response; +use ILIAS\Data\Text\Factory as TextFactory; +use ILIAS\Data\Text\Markdown; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\UI\Factory as UIFactory; -interface Feedback extends Capability +abstract class Feedback { - public function getGeneralFeedback( - Response $response - ): array; + private ?Markdown $feedback_best_response = null; + private string $feedback_best_response_legacy = ''; + private ?Markdown $feedback_other_response = null; + private string $feedback_other_response_legacy = ''; + + private array $specific_feedbacks = []; + + abstract public function getAdditionalInputs( + Language $lng, + UIFactory $ui_factory, + bool $set_legacy_texts_as_values + ): ?array; + + abstract public function getSpecificFeedbackTable( + Environment $environment + ): ?OverviewTable; - public function getSpecificFeedback( + abstract public function getSpecificFeedbackParticipantOutput( Response $response, string $answer_id ): array; + + abstract public function specificFeedbackInputsHaveLegacyTexts(): bool; + + abstract public function onAnswerFormUpdate( + Properties $answer_form_properties + ): static; + + public function withGenericFeedbackFromDatabase( + TextFactory $text_factory, + ?array $database_data + ): static { + if ($database_data === null) { + return $this; + } + $clone = clone $this; + $clone->feedback_best_response = $text_factory->markdown( + $database_data[0]['feedback_best_response'] + ); + $clone->feedback_best_response_legacy = $database_data[0]['feedback_best_response_legacy']; + $clone->feedback_other_response = $text_factory->markdown( + $database_data[0]['feedback_other_response'] + ); + $clone->feedback_other_response_legacy = $database_data[0]['feedback_other_response_legacy']; + return $clone; + } + + public function withSpecificFeedbackFromDatabase( + UuidFactory $uuid_factory, + TextFactory $text_factory, + ?array $database_data + ): static { + if ($database_data === null + || $database_data === []) { + return $this; + } + + $clone = clone $this; + foreach ($database_data as $feedback_data) { + $feedback = new SpecificFeedback( + $uuid_factory->fromString($feedback_data['id']), + $uuid_factory->fromString($feedback_data['answer_form_id']), + $uuid_factory->fromString($feedback_data['parent_id']), + $feedback_data['condition'], + $text_factory->markdown($feedback_data['feedback']), + $feedback_data['feedback_legacy'] + ); + + $clone->specific_feedbacks[$feedback_data['id']] = $feedback; + } + + return $clone; + } + + public function getFeedbackBestResponse(): ?Markdown + { + return $this->feedback_best_response; + } + + public function withFeedbackBestResponse( + Markdown $feedback_best_response + ): static { + $clone = clone $this; + $clone->feedback_best_response = $feedback_best_response; + return $clone; + } + + public function getFeedbackBestResponseLegacy(): string + { + return $this->feedback_best_response_legacy; + } + + public function getFeedbackOtherResponse(): ?Markdown + { + return $this->feedback_other_response; + } + + public function withFeedbackOtherResponse( + Markdown $feedback_other_response + ): static { + $clone = clone $this; + $clone->feedback_other_response = $feedback_other_response; + return $clone; + } + + public function getFeedbackOtherResponseLegacy(): string + { + return $this->feedback_other_response_legacy; + } + + public function hasLegacyTexts(): bool + { + return $this->feedback_best_response_legacy !== '' + || $this->feedback_other_response_legacy !== '' + || $this->specificFeedbackInputsHaveLegacyTexts(); + } + + public function getSpecificFeedbackForId( + Uuid $id + ): ?SpecificFeedback { + return $this->specific_feedbacks[$id->toString()]; + } + + public function getSpecificFeedbackForConditionOrNew( + UuidFactory $uuid_factory, + Uuid $answer_form_id, + Uuid $parent_id, + string $condition + ): SpecificFeedback { + $feedback = array_filter( + $this->specific_feedbacks, + fn(SpecificFeedback $v): bool => $v->getParentId()->toString() === $parent_id->toString() + && $v->getCondition() === $condition + ); + + return $feedback !== [] + ? current($feedback) + : new SpecificFeedback( + $uuid_factory->uuid4(), + $answer_form_id, + $parent_id, + $condition + ); + } + + public function getSpecificFeedbacks(): array + { + return $this->specific_feedbacks; + } + + public function withSpecificFeedback( + SpecificFeedback $specific_feedback + ): static { + $clone = clone $this; + $clone->specific_feedbacks[$specific_feedback->getId()->toString()] = $specific_feedback; + return $clone; + } + + public function withoutSpecificFeedback( + SpecificFeedback $specific_feedback + ): static { + $clone = clone $this; + unset($clone->specific_feedbacks[$specific_feedback->getId()->toString()]); + return $clone; + } + + public function getGenericFeedbackParticipantOutput( + Response $response + ): array { + + } + + public function toStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $feedback_table_definitions, + TableNameBuilder $feedback_table_names_builder, + Uuid $answer_form_id, + Manipulate $manipulate + ): Manipulate { + $manipulate_with_feedback = $manipulate + ->withAdditionalStatement( + $this->buildReplaceForGenericFeedback( + $persistence_factory, + $feedback_table_definitions, + $feedback_table_names_builder, + $answer_form_id + ) + )->withAdditionalStatement( + $this->buildDeleteForSpecificFeedback( + $persistence_factory, + $feedback_table_definitions, + $feedback_table_names_builder, + $answer_form_id + ) + ); + + $specific_feedback_replace = $this->buildReplaceForSpecificFeedback( + $persistence_factory, + $feedback_table_definitions, + $feedback_table_names_builder + ); + + if ($specific_feedback_replace === null) { + return $manipulate_with_feedback; + } + + return $manipulate_with_feedback->withAdditionalStatement( + $specific_feedback_replace + ); + } + + private function buildReplaceForGenericFeedback( + PersistenceFactory $persistence_factory, + TableDefinitions $feedback_table_definitions, + TableNameBuilder $feedback_table_names_builder, + Uuid $answer_form_id + ): Replace { + return $persistence_factory->replace( + $feedback_table_definitions->getColumns( + $feedback_table_names_builder, + TableTypes::FeedbackGeneric + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_best_response?->getRawRepresentation() ?? '' + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_best_response_legacy + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_other_response?->getRawRepresentation() ?? '' + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_other_response_legacy + ) + ] + ); + } + + private function buildDeleteForSpecificFeedback( + PersistenceFactory $persistence_factory, + TableDefinitions $feedback_table_definitions, + TableNameBuilder $feedback_table_names_builder, + Uuid $answer_form_id + ): Delete { + return $persistence_factory->delete( + $persistence_factory->table( + $feedback_table_names_builder, + TableTypes::FeedbackSpecific + ), + [ + $persistence_factory->where( + $feedback_table_definitions->getForeignKeyColumn( + $feedback_table_names_builder, + TableTypes::FeedbackSpecific + ), + new Value( + \ilDBConstants::T_TEXT, + $answer_form_id->toString() + ) + ) + ] + ); + } + + private function buildReplaceForSpecificFeedback( + PersistenceFactory $persistence_factory, + TableDefinitions $feedback_table_definitions, + TableNameBuilder $feedback_table_names_builder + ): ?Replace { + return array_reduce( + $this->specific_feedbacks, + fn(?Replace $c, SpecificFeedback $v) => $c === null + ? $persistence_factory->replace( + $feedback_table_definitions->getColumns( + $feedback_table_names_builder, + TableTypes::FeedbackSpecific + ), + $v->toStorage($persistence_factory) + ) : $c->withAdditionalValues( + $v->toStorage($persistence_factory) + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php new file mode 100644 index 000000000000..fd58898ec8e4 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php @@ -0,0 +1,347 @@ +table_definitions->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + PersistenceFactory $persistence_factory, + AnswerFormMigration $answer_form_migration, + MigrationInsert $migration_insert + ): ?MigrationInsert { + $generic_feedback_insert = $this->buildGenericFeedbackInsert( + $persistence_factory, + $migration_insert + ); + if ($generic_feedback_insert !== null) { + $migration_insert = $migration_insert + ->withAdditionalInsert( + $generic_feedback_insert + ); + } + + $specific_feedback_insert = $this->buildSpecificFeedbackInsert( + $persistence_factory, + $answer_form_migration, + $migration_insert + ); + if ($specific_feedback_insert !== null) { + $migration_insert = $migration_insert + ->withAdditionalInsert( + $specific_feedback_insert + ); + } + + return $migration_insert; + } + + private function fetchGenericFeedbackDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->queryF( + 'SELECT * FROM qpl_fb_generic WHERE question_fi = %s ORDER BY question_fi', + [\ilDBConstants::T_INTEGER], + [$old_question_id] + ); + + yield from $this->fetchFeedbacksForQuestions($query); + } + + private function fetchSpecificFeedbackDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->queryF( + 'SELECT * FROM qpl_fb_specific WHERE question_fi = %s ORDER BY question_fi', + [\ilDBConstants::T_INTEGER], + [$old_question_id] + ); + + yield from $this->fetchFeedbacksForQuestions($query); + } + + private function fetchFeedbacksForQuestions( + \ilDBStatement $query + ): \Generator { + $feedbacks_for_question = null; + while (($row = $db->fetchObject($query)) !== null) { + if ($feedbacks_for_question === null) { + $feedbacks_for_question = [$row]; + continue; + } + + if ($feedbacks_for_question['question_fi'] === $row['question_fi']) { + $feedbacks_for_question[] = $row; + continue; + } + + yield $feedbacks_for_question; + $feedbacks_for_question = [$row]; + } + + yield $feedbacks_for_question; + } + + private function buildGenericFeedbackInsert( + PersistenceFactory $persistence_factory, + MigrationInsert $migration_insert + ): Insert { + $insert = null; + foreach ($this->fetchGenericFeedbackDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $feedback_for_question) { + $insert = $this->addGenericFeedbackToInsert( + $persistence_factory, + $migration_insert, + $insert, + $feedback_for_question + ); + } + + return $insert; + } + + private function addGenericFeedbackToInsert( + PersistenceFactory $persistence_factory, + MigrationInsert $migration_insert, + Insert $insert, + array $feedback_for_question + ): Insert { + $values = $this->buildNewGenericFeedbackValuesFromOld( + $persistence_factory, + $migration_insert, + $feedback_for_question + ); + + if ($insert === null) { + return $persistence_factory->insert( + $this->table_definitions->getColumns( + $migration_insert->getTableNameBuilder(), + TableTypes::FeedbackGeneric + ), + $values + ); + } + + return $insert->withAdditionalValues($values); + } + + private function buildNewGenericFeedbackValuesFromOld( + PersistenceFactory $persistence_factory, + MigrationInsert $migration_insert, + array $feedback_for_question + ): array { + $feedback_best_response = ''; + $feedback_other_response = ''; + foreach ($feedback_for_question as $feedback_row) { + if ($feedback_row['correctness'] === '1') { + $feedback_best_response = $feedback_row['feedback']; + continue; + } + $feedback_other_response = $feedback_row['feedback']; + } + + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $migration_insert->getAnswerFormId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + '' + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->sanitizeLegacyText( + $migration_insert->getDb(), + $feedback_best_response, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + '' + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->sanitizeLegacyText( + $migration_insert->getDb(), + $feedback_other_response, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + ), + ]; + } + + private function buildSpecificFeedbackInsert( + PersistenceFactory $persistence_factory, + AnswerFormMigration $answer_form_migration, + MigrationInsert $migration_insert + ): Insert { + $insert = null; + foreach ($this->fetchSpecificFeedbackDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $feedback_for_question) { + $insert = $this->addSpecificFeedbackToInsert( + $persistence_factory, + $answer_form_migration, + $migration_insert, + $insert, + $feedback_for_question + ); + } + + return $insert; + } + + private function addSpecificFeedbackToInsert( + PersistenceFactory $persistence_factory, + AnswerFormMigration $answer_form_migration, + MigrationInsert $migration_insert, + Insert $insert, + array $feedback_for_question + ): Insert { + $values = $this->buildNewSpecificFeedbackValuesFromOld( + $persistence_factory, + $answer_form_migration, + $migration_insert, + $feedback_for_question + ); + + if ($values === null) { + return $insert; + } + + if ($insert === null) { + $insert = $persistence_factory->insert( + $this->table_definitions->getColumns( + $migration_insert->getTableNameBuilder(), + TableTypes::FeedbackSpecific + ), + array_shift($values) + ); + } + + return array_reduce( + $values, + fn(MigrationInsert $c, array $v): MigrationInsert + => $c->withAdditionalValues($v), + $insert + ); + } + + private function buildNewSpecificFeedbackValuesFromOld( + PersistenceFactory $persistence_factory, + AnswerFormMigration $answer_form_migration, + MigrationInsert $migration_insert, + array $feedback_for_question + ): ?array { + $parent_id = $answer_form_migration->getNewAnswerInputIdForOld( + $feedback_for_question + ); + $conditions = $answer_form_migration->getConditionForFeedbackFromOldValues( + $feedback_for_question['answer'], + $feedback_for_question['question'] + ) ?? ''; + + if ($parent_id === null + || $conditions === null) { + return null; + } + + return array_map( + fn(string $v): array => [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $migration_insert->getUuid()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $migration_insert->getAnswerFormId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $parent_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $v + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + '' + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->sanitizeLegacyText( + $migration_insert->getDb(), + $feedback_for_question['feedback'], + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + ), + ], + $conditions + ); + } + + private function buildRangeValue( + bool $is_numeric, + string $value + ): ?string { + if ($is_numeric === null) { + return null; + } + + if ($value === 'out_of_bounds') { + return Range::OutOfRange->value; + } + + return Range::InRange->value; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php new file mode 100644 index 000000000000..2ba2c1f86f3d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php @@ -0,0 +1,207 @@ +specific_feedback_table = $feedback->getSpecificFeedbackTable( + $environment + ); + } + + #[\Override] + public function render( + UIRenderer $ui_renderer + ): string { + $ui_factory = $this->environment->getUIFactory(); + $lng = $this->environment->getLanguage(); + + $content = []; + + if ($this->feedback->hasLegacyTexts()) { + $content[] = $ui_factory->messageBox()->info( + $lng->txt('insert_legacy_texts_info') + )->withButtons([ + $ui_factory->button()->standard( + $lng->txt('insert_legacy_texts'), + $this->insert_legacy_texts_uri->buildURI()->__toString() + ) + ]); + } + + $content[] = $this->form ?? $this->buildForm(); + + if ($this->specific_feedback_table !== null) { + $modal = $this->specific_feedback_table->getCreateModal( + $this->environment + ); + $content[] = $ui_factory->button()->standard( + $lng->txt('create_feedback'), + $modal->getShowSignal() + ); + $content[] = $modal; + $content[] = $this->specific_feedback_table->getTable( + $this->environment, + $this->feedback + )->withTitle( + $lng->txt('specific_feedback') + ); + } + + if ($this->modal !== null) { + $content[] = $this->modal; + } + return $ui_renderer->render($content); + } + + public function withLegacyTextsAsValues( + bool $replace_with_legacy_texts + ): self { + $clone = clone $this; + $clone->set_legacy_texts_as_values = $replace_with_legacy_texts; + return $clone; + } + + public function processForm(): Feedback|StandardForm + { + $this->form = $this->buildForm()->withRequest( + $this->environment->getHttpServices()->request() + ); + $data = $this->form->getData(); + + return $data === null + ? $this + : $data['feedback']; + } + + public function doAction( + Repository $repository, + string $action + ): Async|self { + $result = $this->specific_feedback_table->doAction( + $this->environment, + $this->feedback, + $action + ); + + if ($result instanceof Async) { + return $result; + } + + $clone = clone $this; + if ($result instanceof RoundTripModal) { + $clone->modal = $result->withOnLoad($result->getShowSignal()); + return $clone; + } + + $repository->store($this->environment->getAnswerFormId(), $result); + $clone->feedback = $result; + $clone->specific_feedback_table = $result->getSpecificFeedbackTable( + $this->environment + ); + return $clone; + } + + private function buildForm(): StandardForm + { + $if = $this->environment->getUIFactory()->input(); + $lng = $this->environment->getLanguage(); + + $inputs = [ + 'generic_feedback' => $if->field()->section( + [ + 'max_points' => $if->field()->markdown( + new \ilUIMarkdownPreviewGUI(), + $lng->txt('edit_feedback_max_points') + )->withValue( + $this->set_legacy_texts_as_values + ? $this->feedback->getFeedbackBestResponseLegacy() + : $this->feedback->getFeedbackBestResponse() + ?->getRawRepresentation() ?? '' + ), + 'not_max_points' => $if->field()->markdown( + new \ilUIMarkdownPreviewGUI(), + $lng->txt('edit_feedback_not_max_points') + )->withValue( + $this->set_legacy_texts_as_values + ? $this->feedback->getFeedbackOtherResponseLegacy() + : $this->feedback->getFeedbackOtherResponse() + ?->getRawRepresentation() ?? '' + ), + ], + $lng->txt('edit_generic_feedback') + ) + ]; + + $additional_inputs = $this->feedback->getAdditionalInputs( + $lng, + $this->environment->getUIFactory(), + $this->set_legacy_texts_as_values + ); + if ($additional_inputs !== null) { + $inputs['specific_feedback'] = $if->field()->section( + $additional_inputs, + $lng->txt('edit_specific_feedback') + ); + } + + return $if->container()->form()->standard( + $this->save_uri->buildURI()->__toString(), + [ + 'feedback' => $if->field()->group( + $inputs + )->withAdditionalTransformation( + $this->environment->getRefinery()->custom()->transformation( + fn(array $vs): Feedback => ($vs['specific_feedback'] ?? $this->feedback) + ->withFeedbackBestResponse( + $this->text_factory->markdown($vs['generic_feedback']['max_points']) + )->withFeedbackOtherResponse( + $this->text_factory->markdown($vs['generic_feedback']['not_max_points']) + ) + ) + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php new file mode 100644 index 000000000000..75c90a131f35 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php @@ -0,0 +1,44 @@ +feedback_table_names_builder = new TableNameBuilder( + QuestionRepository::COMPONENT_NAMESPACE, + null + ); } - /** - * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> - */ public function getFor( - Uuid $answer_form_id - ): \Generator { - foreach ($query = new Query( - $this->db, - $this->refinery, - QuestionRepository::COMPONENT_NAMESPACE - )->loadNextRecord() as $query_with_record) { - yield $this->retrieveQuestionFromQuery( - $query_with_record, - [] - ); + Uuid $answer_form_id, + Feedback $feedback + ): Feedback { + $database_values = $this + ->buildQuery() + ->withAdditionalWhere( + $this->persistence_factory->where( + $this->feedback_table_definitions->getIdColumn( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $answer_form_id->toString() + ) + ) + )->loadNextRecord( + $this->feedback_table_definitions->getIdColumn( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ) + )->current(); + + if ($database_values === null) { + return $feedback; } - } - /** - * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions - */ - public function create( - array $questions - ): void { - $this->store( - array_map( - fn(QuestionImplementation $v): QuestionImplementation => $v - ->withPageId($this->buildQuestionPage($v->getParentObjId())), - $questions + $feedback_with_generic_values = $database_values->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric ), - new Manipulate( - $this->db, - $this->answer_form_factory, - ManipulationType::Create + $this->refinery->custom()->transformation( + fn(array $vs): Feedback => $feedback->withGenericFeedbackFromDatabase( + $this->text_factory, + $vs + ) ) ); - } - /** - * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions - */ - public function update( - array $questions - ): void { - $this->store( - $questions, - new Manipulate( - $this->db, - $this->answer_form_factory, - ManipulationType::Update + return $feedback_with_generic_values->withSpecificFeedbackFromDatabase( + $this->uuid_factory, + $this->text_factory, + $database_values->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->feedback_table_names_builder, + TableTypes::FeedbackSpecific + ), + $this->refinery->identity() ) ); } - public function delete( - array $questions + public function store( + Uuid $answer_form_id, + Feedback $feedback ): void { - array_reduce( - $questions, - fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), + $feedback->toStorage( + $this->persistence_factory, + $this->feedback_table_definitions, + $this->feedback_table_names_builder, + $answer_form_id, new Manipulate( $this->db, - $this->answer_form_factory, - ManipulationType::Delete + ManipulationType::Replace, + QuestionRepository::COMPONENT_NAMESPACE ) )->run(); + } - foreach ($questions as $question) { - (new \QstsQuestionPage($question->getPageId()))->delete(); - } + private function buildQuery(): Query + { + return $this->feedback_table_definitions->completeQuery( + new Query( + $this->db, + $this->refinery, + QuestionRepository::COMPONENT_NAMESPACE, + $this->persistence_factory->table( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ) + ), + $this->feedback_table_definitions->getIdColumn( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ) + ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php new file mode 100644 index 000000000000..36c4e20b6767 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php @@ -0,0 +1,103 @@ +id; + } + + public function getParentId(): Uuid + { + return $this->parent_id; + } + + public function getCondition(): string + { + return $this->condition; + } + + public function getFeedbackText(): Markdown + { + return $this->feedback_text; + } + + public function withFeedbackText( + Markdown $text + ): self { + $clone = clone $this; + $clone->feedback_text = $text; + return $clone; + } + + public function toStorage( + PersistenceFactory $persistence_factory + ): array { + if ($this->feedback_text === null) { + throw new \UnexpectedValueException( + 'You cannot save a SpecificFeedback without a feedback text!' + ); + } + + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->parent_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->condition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_text->getRawRepresentation() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->feedback_legacy + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php new file mode 100644 index 000000000000..25a9be03189e --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php @@ -0,0 +1,177 @@ +persistence_factory->table( + $table_name_builder, + $table_type + ); + $column_identifiers = match($table_type) { + TableTypes::FeedbackGeneric => self::FEEDBACK_GENERIC_TABLE_COLUMNS, + TableTypes::FeedbackSpecific => self::FEEDBACK_SPECIFIC_TABLE_COLUMNS + }; + return array_map( + fn(string $v): Column => $this->persistence_factory->column( + $table, + $v + ), + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + #[\Override] + public function getIdColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return match($table_type) { + TableTypes::FeedbackGeneric => $this->persistence_factory->column( + $table, + self::FEEDBACK_GENERIC_TABLE_ID_COLUMN + ), + TableTypes::FeedbackSpecific => $this->persistence_factory->column( + $table, + self::FEEDBACK_SPECIFIC_TABLE_ID_COLUMN + ) + }; + } + + #[\Override] + public function getForeignKeyColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return match($table_type) { + TableTypes::FeedbackGeneric => $this->persistence_factory->column( + $table, + self::FEEDBACK_GENERIC_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::FeedbackSpecific => $this->persistence_factory->column( + $table, + self::FEEDBACK_SPECIFIC_TABLE_FOREIGN_KEY_COLUMN + ) + }; + } + + #[\Override] + public function completeQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_name_builder = $query->getTableNameBuilder(null); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + TableTypes::FeedbackGeneric + ) + ) + )->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + TableTypes::FeedbackSpecific + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $base_table_id_column, + $this->getForeignKeyColumn( + $table_name_builder, + TableTypes::FeedbackSpecific + ), + JoinType::Left + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $base_table_id_column + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php similarity index 85% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php index 9c4e3b20387d..56af7fc6fc4a 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/FeedbackTableTypes.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -use ILIAS\Questions\Persistence\TableTypes; +use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; -enum FeedbackTableTypes: string implements TableTypes +enum TableTypes: string implements TableTypesInterface { case FeedbackGeneric = 'feedback_generic'; case FeedbackSpecific = 'feedback_specific'; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php similarity index 63% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php index a32b62eff39c..20f8670e1d6d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php @@ -18,21 +18,17 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; +namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -use ILIAS\Language\Language; - -enum InRange: string +enum Types: string { - case InRange = 'i'; - case OutOfRange = 'o'; + case MaxPoints = 'max_points'; + case NotMaxPoints = 'not_max_points'; + case NothingSelected = 'nothing_selected'; - public function getLabel( + public function getTranslatedOptionName( Language $lng ): string { - return match($this) { - self::InRange => $lng->txt('in_range'), - self::OutOfRange => $lng->txt('out_of_range') - }; + return $lng->txt($this->value); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php new file mode 100644 index 000000000000..b819a828107b --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php @@ -0,0 +1,68 @@ +repository->store( + $answer_form_properties->getAnswerFormId(), + $answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability(Feedback::class) + ->onAnswerFormUpdate( + $answer_form_properties + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php index ba81dce2eed4..a29db9be1e1b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php @@ -20,10 +20,9 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\Response\Response; -interface Marking extends Capability +interface Marking { public function addAchievedPointsToResponse( Response $response diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php new file mode 100644 index 000000000000..a8497d4766af --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php @@ -0,0 +1,39 @@ +getStep()) { - self::STEP_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), - self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => - $this->addLegacyTextToBasicProperties($environment), - self::STEP_CONFIRMED_GAP_REMOVAL, - self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( - $environment->withPreservedTableRowIdsParameter() - ), - default => $this->forwardCmdToEditGaps( - $environment->withPreservedTableRowIdsParameter(), - $step + ): ?OverviewTable { + return new FeedbackOverviewTable( + $this->uuid_factory, + $this->text_factory, + new FeedbackOverviewDataRetrieval( + $this->uuid_factory, + $environment->getRefinery(), + $environment->getAnswerFormProperties(), + $this ) - }; + ); } #[\Override] - public function toStorage( - PersistenceFactory $persistence_factory, - Manipulate $manipulate - ): Manipulate { - return $manipulate; + public function getSpecificFeedbackParticipantOutput( + Response $response, + string $answer_id + ): array { + } #[\Override] - public function toDelete( - PersistenceFactory $persistence_factory, - Manipulate $manipulate - ): Manipulate { - return $manipulate; + public function specificFeedbackInputsHaveLegacyTexts(): bool + { + return false; } #[\Override] - public function getGeneralFeedback( - Response $response - ): array { + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): static { + $gaps = $answer_form_properties->getGaps(); + return array_reduce( + $this->getSpecificFeedbacks(), + function ( + Feedback $c, + SpecificFeedback $v + ) use ($gaps): self { + $gap = $gaps->getGapById($v->getParentId()); + if ($gap === null + || !$gap->getType()->isValidFeedbackCondition( + $this->uuid_factory, + $gap, + $v->getCondition() + ) + ) { + return $c->withoutSpecificFeedback($v); + } + + return $c; + }, + clone $this + ); } - #[\Override] - public function getSpecificFeedback( - Response $response, - string $answer_id - ): array { + public function buildSpecificFeedbackOverviewTableArray(): array + { + return array_reduce( + $this->getSpecificFeedbacks(), + function (array $c, SpecificFeedback $v): array { + $gap_id = $v->getParentId()->toString(); + $key = md5($v->getFeedbackText()->getRawRepresentation()); + if (!array_key_exists($gap_id, $c)) { + $c[$gap_id] = []; + } + if (!array_key_exists($key, $c[$gap_id])) { + $c[$gap_id][$key] = []; + } + $c[$gap_id][$key][] = $v; + return $c; + }, + [] + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php new file mode 100644 index 000000000000..0b57c2679a79 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php @@ -0,0 +1,135 @@ + + * } + */ + private readonly array $feedback; + + public function __construct( + private readonly UuidFactory $uuid_factory, + private readonly Refinery $refinery, + private readonly Properties $answer_form_properties, + Feedback $feedback + ) { + $this->feedback = $feedback->buildSpecificFeedbackOverviewTableArray(); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + foreach ($this->feedback as $gap_feedbacks) { + yield from $this->buildColumnValues( + $row_builder, + $gap_feedbacks + ); + } + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return count($this->feedback); + } + + public function getSpecificFeedbacksForRowId( + string $table_row_id + ): array { + $ids = explode('_', $table_row_id); + $feedbacks_for_gap = array_reduce( + $this->feedback[array_shift($ids)], + fn(array $c, array $v): array => [...$c, ...$v], + [] + ); + return array_filter( + $feedbacks_for_gap, + fn(SpecificFeedback $v): bool => in_array($v->getId(), $ids) + ); + } + + private function buildColumnValues( + DataRowBuilder $row_builder, + array $gap_feedbacks + ): \Generator { + foreach ($gap_feedbacks as $feedbacks) { + yield $this->buildTableRow( + $row_builder, + $feedbacks + ); + } + } + + private function buildTableRow( + DataRowBuilder $row_builder, + array $feedbacks + ): DataRow { + $gap_id = $feedbacks[0]->getParentId(); + $gap = $this->answer_form_properties->getGaps()->getGapById($gap_id); + + $keys = [$gap_id->toString()]; + $answer_options = []; + foreach ($feedbacks as $feedback) { + $keys[] = $feedback->getId()->toString(); + $answer_options[] = $gap->getType()->getLabelForValue( + $this->uuid_factory, + $gap, + $feedback->getCondition() + ); + } + + return $row_builder->buildDataRow( + implode('_', $keys), + [ + 'gap' => $gap->buildShortenedGapRepresentation(), + 'answer_option' => implode('
', $answer_options), + 'feedback' => $this->refinery->string()->markdown()->toHTML()->transform( + $feedbacks[0]->getFeedbackText() + ->getRawRepresentation() + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php new file mode 100644 index 000000000000..fef6c7f7f264 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php @@ -0,0 +1,506 @@ +getUIFactory(); + $lng = $environment->getLanguage(); + + return $ui_factory->modal()->roundtrip( + $lng->txt('create_feedback'), + null, + [ + 'gap' => $environment->getAnswerFormProperties()->getGaps() + ->buildGapsSelect( + $lng->txt('select_gap'), + $ui_factory->input()->field() + )->withAdditionalTransformation( + $environment->getRefinery()->custom()->transformation( + fn(string $v): Uuid => $this->uuid_factory->fromString($v) + ) + )->withRequired(true) + ], + $environment->withStepParameter( + self::STEP_ENTER_FEEDBACK + )->getUrlBuilder()->buildURI()->__toString() + )->withSubmitLabel($lng->txt('next')); + } + + #[\Override] + public function getTable( + Environment $environment, + Feedback $feedback + ): DataTable { + return $environment->getUIFactory()->table()->data( + $this->data_retrieval, + '', + $this->getColumns( + $environment->getUIFactory()->table()->column(), + $environment->getLanguage() + ) + )->withActions( + $this->getActions( + $environment + ) + )->withRequest($environment->getHttpServices()->request()); + } + + #[\Override] + public function doAction( + Environment $environment, + Feedback $feedback, + string $action + ): Async|RoundTripModal|Feedback { + return match($action) { + self::STEP_ENTER_FEEDBACK => $this->processSelectGapModal( + $environment, + $feedback + ), + self::STEP_EDIT_FEEDBACK => $this->editFeedback( + $environment->withPreservedTableRowIdsParameter(), + $feedback + ), + self::STEP_SAVE_FEEDBACK => $this->processEnterFeedbackModal( + $environment, + $feedback + ), + self::STEP_CONFIRM_DELETE_FEEDBACK => $this->confirmDeleteFeedback( + $environment, + $feedback + ), + self::STEP_DELETE_FEEDBACK => $this->deleteFeedback( + $environment, + $feedback + ) + }; + } + + private function processSelectGapModal( + Environment $environment, + Feedback $feedback + ): RoundTripModal { + $create_modal = $this->getCreateModal($environment) + ->withRequest($environment->getHttpServices()->request()); + $data = $create_modal->getData(); + if ($data === null) { + return $create_modal; + } + + $inputs_builder = $this->buildEnterFeedbackInputBuilder( + $environment, + $feedback, + $data['gap'] + ); + + $enter_feedback_modal = $this->buildEnterFeedbackModal( + $environment, + $inputs_builder + ); + + $inputs_builder + ->withCarry( + $data['gap']->toString() + )->persistCarry(); + + return $enter_feedback_modal; + } + + private function buildEnterFeedbackModal( + Environment $environment, + InputsBuilderSession $inputs_builder + ): RoundTripModal { + return $environment->getUIFactory()->modal()->roundtrip( + $environment->getLanguage()->txt('create_feedback'), + null, + [ + 'feedback' => $inputs_builder->getInputs() + ], + $environment->withStepParameter( + self::STEP_SAVE_FEEDBACK + )->getUrlBuilder()->buildURI()->__toString() + ); + } + + private function processEnterFeedbackModal( + Environment $environment, + Feedback $feedback + ): RoundTripModal|Feedback { + $specific_feedbacks = $this->getSpecifcFeedbacksFromQuery( + $environment + ); + + $gap_id = null; + if (isset($specific_feedbacks[0])) { + $gap_id = $specific_feedbacks[0]->getParentId(); + $environment = $environment->withPreservedTableRowIdsParameter(); + } + + $inputs_builder = $this->buildEnterFeedbackInputBuilder( + $environment, + $feedback, + $gap_id, + array_map( + fn(SpecificFeedback $v): string => $v->getCondition(), + $specific_feedbacks + ) + ); + + $modal = $this->buildEnterFeedbackModal( + $environment, + $inputs_builder + )->withRequest($environment->getHttpServices()->request()); + + $data = $modal->getData(); + if ($data === null) { + $inputs_builder->persistCarry(); + return $modal; + } + + return $data['feedback']; + } + + private function editFeedback( + Environment $environment, + Feedback $feedback + ): Async|Feedback { + $specific_feedbacks = $this->getSpecifcFeedbacksFromQuery( + $environment + ); + + return $environment->getPresentationFactory()->getAsync( + $this->buildEnterFeedbackModal( + $environment, + $this->buildEnterFeedbackInputBuilder( + $environment, + $feedback, + $specific_feedbacks[0]->getParentId(), + array_map( + fn(SpecificFeedback $v): string => $v->getCondition(), + $specific_feedbacks + ), + $specific_feedbacks[0]->getFeedbackText()->getRawRepresentation() + ) + ) + ); + } + + private function confirmDeleteFeedback( + Environment $environment, + Feedback $feedback + ): Async { + $pf = $environment->getPresentationFactory(); + $uf = $environment->getUIFactory(); + $lng = $environment->getLanguage(); + $selected_feedbacks = $this->data_retrieval->getSpecificFeedbacksForRowId( + $environment->getTableRowIds()[0] + ); + + if ($selected_feedbacks === []) { + return $pf->getAsync( + $uf->messageBox()->failure( + $lng->txt('ui_field_option_filter_no_selection') + ) + ); + } + + return $pf->getAsync( + $uf->modal()->interruptive( + $lng->txt('confirm'), + $lng->txt('confirm_delete_feedback'), + $environment + ->withStepParameter(self::STEP_DELETE_FEEDBACK) + ->getUrlBuilder() + ->buildURI() + ->__toString() + )->withAffectedItems([ + $uf->modal()->interruptiveItem()->standard( + $environment->getTableRowIds()[0], + $selected_feedbacks[0]->getFeedbackText() + ?->getRawRepresentation() ?? '' + ) + ]) + ); + } + + private function deleteFeedback( + Environment $environment, + Feedback $feedback + ): RoundTripModal|Feedback { + $uf = $environment->getUIFactory(); + $lng = $environment->getLanguage(); + $selected_feedbacks = $environment->getHttpServices()->wrapper()->post() + ->retrieve( + 'interruptive_items', + $environment->getRefinery()->custom()->transformation( + fn(array $vs): array => $this->data_retrieval + ->getSpecificFeedbacksForRowId($vs[0]) + ) + ); + + if ($selected_feedbacks === []) { + return $uf->modal()->roundtrip( + $lng->txt('error'), + $uf->messageBox()->failure( + $lng->txt('ui_field_option_filter_no_selection') + ) + ); + } + + return array_reduce( + $selected_feedbacks, + fn(Feedback $c, SpecificFeedback $v): Feedback => $c->withoutSpecificFeedback($v), + $feedback + ); + } + + private function buildEnterFeedbackInputBuilder( + Environment $environment, + Feedback $feedback, + ?Uuid $selected_gap = null, + array $selected_conditions = [], + string $feedback_text = '' + ): InputsBuilderSession { + return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( + self::KEY_GAP_ID, + $environment->getRefinery()->custom()->transformation( + function ( + ?string $v + ) use ( + $environment, + $feedback, + $selected_gap, + $selected_conditions, + $feedback_text + ): Section { + $gap_id = $selected_gap + ?? $this->uuid_factory->fromString($v); + + return $this->buildSpecificFeedbackSection( + $environment->getUIFactory(), + $environment->getRefinery(), + $environment->getLanguage(), + $environment->getAnswerFormProperties() + ->getGaps() + ->getGapById($gap_id), + $environment->isCapabilityRequired(Marking::class), + $feedback->getSpecificFeedbacks(), + $selected_conditions, + $feedback_text + )->withAdditionalTransformation( + $this->buildSpecificFeedbackTransformation( + $environment->getRefinery(), + $feedback, + $environment->getAnswerFormId(), + $gap_id, + $selected_conditions + ) + ); + } + ) + ); + } + + private function buildSpecificFeedbackSection( + UIFactory $ui_factory, + Refinery $refinery, + Language $lng, + Gap $gap, + bool $is_marking_required, + array $existing_specific_feedbacks, + array $selected_conditions, + string $feedback_text + ): Section { + $answer_options = $gap->getType()->getFeedbackSelectValues( + $gap, + $is_marking_required + ); + + foreach ($existing_specific_feedbacks as $feedback) { + if (!in_array($feedback->getCondition(), $selected_conditions)) { + unset($answer_options[$feedback->getCondition()]); + } + } + + return $ui_factory->input()->field()->section( + [ + 'answer_options' => $ui_factory->input()->field()->multiSelect( + $lng->txt('answer_options'), + $answer_options + )->withHasOptionFilter(true) + ->withRequired(true) + ->withValue($selected_conditions), + 'feedback' => $ui_factory->input()->field()->markdown( + new \ilUIMarkdownPreviewGUI(), + $lng->txt('feedback') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(string $v): Markdown => $this->text_factory->markdown($v) + ) + )->withRequired(true) + ->withValue($feedback_text) + ], + $lng->txt('enter_feedback') + ); + } + + private function buildSpecificFeedbackTransformation( + Refinery $refinery, + Feedback $feedback, + Uuid $answer_form_id, + Uuid $gap_id, + array $selected_conditions + ): CustomTransformation { + return $refinery->custom()->transformation( + function ( + array $vs + ) use ( + $feedback, + $answer_form_id, + $gap_id, + $selected_conditions + ): Feedback { + $feedback_with_removed_specific_feedbacks = array_reduce( + array_diff($selected_conditions, $vs['answer_options']), + fn(Feedback $c, string $v): Feedback => $c->withoutSpecificFeedback( + $c->getSpecificFeedbackForConditionOrNew( + $this->uuid_factory, + $answer_form_id, + $gap_id, + $v + ) + ), + $feedback + ); + + return array_reduce( + $vs['answer_options'], + fn(Feedback $c, string $v): Feedback => $c->withSpecificFeedback( + $c->getSpecificFeedbackForConditionOrNew( + $this->uuid_factory, + $answer_form_id, + $gap_id, + $v + )->withFeedbackText($vs['feedback']) + ), + $feedback_with_removed_specific_feedbacks + ); + } + ); + } + + private function getSpecifcFeedbacksFromQuery( + Environment $environment + ): array { + $selected_table_row_ids = $environment->getTableRowIds(); + + if ($selected_table_row_ids === []) { + return []; + } + + return $this->data_retrieval->getSpecificFeedbacksForRowId( + $selected_table_row_ids[0] + ); + } + + private function getColumns( + ColumnFactory $column_factory, + Language $lng + ): array { + return [ + 'gap' => $column_factory->text( + $lng->txt('gap') + )->withIsSortable(false), + 'answer_option' => $column_factory->text( + $lng->txt('answer_option') + )->withIsSortable(false), + 'feedback' => $column_factory->text( + $lng->txt('feedback') + )->withIsSortable(false) + ]; + } + + private function getActions( + Environment $environment + ): array { + $af = $environment->getUIFactory()->table()->action(); + $lng = $environment->getLanguage(); + + return [ + 'edit' => $af->single( + $lng->txt('edit'), + $environment->withStepParameter( + self::STEP_EDIT_FEEDBACK + )->getUrlBuilder(), + $environment->getTableRowIdToken() + )->withAsync(true), + 'delete' => $af->single( + $lng->txt('delete'), + $environment->withStepParameter( + self::STEP_CONFIRM_DELETE_FEEDBACK + )->getUrlBuilder(), + $environment->getTableRowIdToken() + )->withAsync(true), + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php index cf455f69a4bb..6c3fb2ede227 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php @@ -21,45 +21,10 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking as MarkingInterface; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\Manipulate; -use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Questions\Response\Response; class Marking implements MarkingInterface { - #[\Override] - public function isConfigured(): bool - { - return false; - } - - #[\Override] - public function edit( - Environment $environment - ): self|Async|EditForm|EditOverview { - return false; - } - - #[\Override] - public function toStorage( - PersistenceFactory $persistence_factory, - Manipulate $manipulate - ): Manipulate { - return $manipulate; - } - - #[\Override] - public function toDelete( - PersistenceFactory $persistence_factory, - Manipulate $manipulate - ): Manipulate { - return $manipulate; - } - #[\Override] public function addAchievedPointsToResponse( Response $response diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index 76d68ae18300..b71738d22eb0 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -21,7 +21,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; @@ -34,7 +33,7 @@ class Definition implements DefinitionInterface { /** - * @param array $available_capabilities + * @param array $available_capabilities */ public function __construct( private readonly PropertiesFactory $properties_factory, @@ -79,7 +78,7 @@ public function hasCapability( #[\Override] public function getCapability( string $capability_class_name - ): ?Capability { + ): mixed { return $this->available_capabilities[$capability_class_name]; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/FeedbackOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/FeedbackOverview.php deleted file mode 100644 index 73cf4c3ce4eb..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/FeedbackOverview.php +++ /dev/null @@ -1,358 +0,0 @@ -initializeModal($this->buildSetCombinationGapsModal()), - $this->buildTable() - ]; - if ($this->modal !== null) { - $content[] = $this->modal; - } - return $ui_renderer->render($content); - } - - #[\Override] - public function getRows( - DataRowBuilder $row_builder, - array $visible_column_ids, - Range $range, - Order $order, - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters - ): \Generator { - yield from $this->environment->getAnswerFormProperties() - ->getCombinations()->toTableRows($this->lng, $row_builder); - } - - #[\Override] - public function getTotalRowCount( - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters - ): ?int { - return $this->environment->getAnswerFormProperties() - ->getCombinations()->getNumberOfCombinations(); - } - - public function doAction(): Async|self|Properties - { - return match ($this->environment->getStep()) { - self::STEP_ADD_FEEDBACK => $this->processSetCombinationGapsModal(), - self::STEP_DELETE_FEEDBACK => $this->deleteCombination(), - self::STEP_SAVE => $this->processSetCombinationValues(), - default => $this->buildAction() - }; - } - - private function buildTable(): DataTable - { - return $this->ui_factory->table()->data( - $this, - $this->lng->txt('combinations'), - $this->getColumns() - )->withActions($this->getActions()) - ->withRequest($this->http->request()); - } - - private function initializeModal( - RoundTripModal $modal - ): RoundTripModal { - $this->toolbar->addComponent( - $this->ui_factory->button()->standard( - $this->lng->txt('add_combination'), - $modal->getShowSignal() - ) - ); - return $modal; - } - - private function getColumns(): array - { - $cf = $this->ui_factory->table()->column(); - return [ - 'gaps' => $cf->text($this->lng->txt('gaps')), - 'values' => $cf->text($this->lng->txt('values')), - 'available_points' => $cf->number($this->lng->txt('points'))->withDecimals(2) - ]; - } - - private function getActions(): array - { - $af = $this->ui_factory->table()->action(); - return [ - $af->single( - $this->lng->txt('edit'), - $this->environment - ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) - ->getUrlBuilder(), - $this->environment->getTableRowIdToken() - )->withAsync(true), - $af->single( - $this->lng->txt('delete'), - $this->environment - ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) - ->getUrlBuilder(), - $this->environment->getTableRowIdToken() - )->withAsync(true) - ]; - } - - private function buildAction(): Async - { - $affected_item = $this->environment->getAnswerFormProperties() - ->getCombinations()->getCombinationById( - $this->environment->getTableRowIds()[0] - ); - - if ($affected_item === null) { - return $this->buildNoItemsSelectedAsync(); - } - - return $this->environment->getPresentationFactory()->getAsync( - match ($this->environment->getStep()) { - self::STEP_JUMP_TO_SET_COMBINATION_VALUES => - $this->buildSetCombinationValuesModal( - $this->buildInputsBuilder($affected_item) - ), - self::STEP_CONFIRM_DELETE_COMBINATION => - $this->confirmDeleteCombination($affected_item) - } - ); - } - - private function buildNoItemsSelectedAsync(): Async - { - return new Async( - $this->http, - $this->ui_factory->messageBox()->failure('no_combination_selected') - ); - } - - private function buildSetCombinationGapsModal(): RoundTripModal - { - $properties = $this->environment->getAnswerFormProperties(); - $gaps = $properties->getGaps(); - return $this->ui_factory->modal()->roundtrip( - $this->lng->txt('add_combination'), - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $gaps, - $properties->getLegacyClozeText() - ), - [ - 'combination' => $gaps->buildGapsMultiSelect( - $this->lng->txt('select_gaps_for_combinations'), - $this->ui_factory->input()->field() - )->withRequired(true) - ->withAdditionalTransformation( - $this->refinery->custom()->constraint( - fn(array $v): bool => count($v) > 1, - $this->lng->txt('combination_needs_more_than_one') - ) - )->withAdditionalTransformation( - $this->refinery->custom()->transformation( - fn(array $v): Combination => $this->combinations_factory - ->buildNewCombination($gaps, $v) - ) - ) - ], - $this->environment - ->withStepParameter(self::STEP_ADD_FEEDBACK) - ->getUrlBuilder() - ->buildURI() - ->__toString() - )->withSubmitLabel($this->lng->txt('next')); - } - - private function processSetCombinationGapsModal(): self - { - $clone = clone $this; - - $set_gaps_modal = $clone->buildSetCombinationGapsModal() - ->withRequest($clone->http->request()); - $data = $set_gaps_modal->getData(); - - if ($data === null) { - $clone->modal = $set_gaps_modal->withOnLoad($set_gaps_modal->getShowSignal()); - return $clone; - } - - $set_values_modal = $clone->buildSetCombinationValuesModal( - $this->buildInputsBuilder($data['combination']) - ); - $clone->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); - return $clone; - } - - private function buildSetCombinationValuesModal( - InputsBuilder $inputs_builder - ): RoundTripModal { - $properties = $this->environment->getAnswerFormProperties(); - $gaps = $properties->getGaps(); - - return $this->ui_factory->modal()->roundtrip( - $this->lng->txt('edit'), - $properties->getClozeText()->buildPanelForEditing( - $this->ui_factory, - $this->lng, - $gaps, - $properties->getLegacyClozeText() - ), - [ - 'values_awarding_points' => $inputs_builder->getInputs() - ], - $this->environment->withStepParameter(self::STEP_SAVE) - ->getUrlBuilder() - ->buildURI() - ->__toString() - ); - } - - private function processSetCombinationValues(): self|Properties - { - $inputs_builder = $this->buildInputsBuilder(null); - $set_values_modal = $this->buildSetCombinationValuesModal($inputs_builder) - ->withRequest($this->http->request()); - $data = $set_values_modal->getData(); - if ($data === null) { - $this->modal = $this->initializeModal($set_values_modal) - ->withOnLoad($set_values_modal->getShowSignal()); - $inputs_builder->persistCarry(); - return $this; - } - - return $data['values_awarding_points']; - } - - private function confirmDeleteCombination( - Combination $affected_item - ): InterruptiveModal { - return $this->ui_factory->modal()->interruptive( - $this->lng->txt('confirm'), - $this->lng->txt('delete_combination'), - $this->environment->withStepParameter( - self::STEP_DELETE_FEEDBACK - )->getUrlBuilder() - ->withParameter( - $this->environment->getTableRowIdToken(), - [$affected_item->getId()->toString()] - )->buildURI()->__toString() - ); - } - - private function deleteCombination(): Properties - { - $combination_identifier = $this->environment->getTableRowIds(); - if ($combination_identifier === []) { - return $this->environment->getAnswerFormProperties(); - } - - /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ - $answer_form_properties = $this->environment->getAnswerFormProperties(); - - return $answer_form_properties->withCombinations( - $answer_form_properties->getCombinations()->withoutCombination( - $combination_identifier[0] - ) - ); - } - - private function buildInputsBuilder( - ?Combination $combination, - ): InputsBuilderSession { - $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), - $this->refinery->custom()->transformation( - function (?string $v) use ($combination): ?Section { - $properties = $this->environment->getAnswerFormProperties(); - if ($combination === null) { - $combination = $this->combinations_factory - ->buildCombinationFromCarryValue( - $v, - $properties - ); - } - - return $combination?->buildPointsInputs( - $this->ui_factory->input()->field(), - $this->refinery, - $this->lng, - $this->combinations_factory, - $properties - ); - } - ) - ); - - if ($combination === null) { - return $builder; - } - - $builder_with_string = $builder->withCarry($combination->buildCarryString()); - $builder_with_string->persistCarry(); - return $builder_with_string; - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index 543aba160492..2a6c270625df 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -35,6 +35,19 @@ trait BasicMigrationFunctions { use SanitizeLegacyText; + private ?array $answer_inputs_mapping = null; + + #[\Override] + public function getNewAnswerInputIdForOld( + int $id + ): ?Uuid { + if ($this->answer_inputs_mapping === null) { + return null; + } + + return $this->answer_inputs_mapping[$id] ?? null; + } + private function buildGapInsertStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, @@ -125,6 +138,8 @@ private function buildAnswerOptionInsertStatement( ?float $lower_limit, ?float $upper_limit ): Insert { + $this->answer_options_mapping[$position] = $answer_option_id; + if ($options_insert === null) { return $persistence_factory->insert( $table_definitions->getColumns( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index c43d4a569563..746c2f5d7b31 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -20,12 +20,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; +use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -37,6 +38,8 @@ class MigrationCloze implements Migration { use BasicMigrationFunctions; + private array $answer_options_mapping_for_feedback = []; + public function __construct( private readonly TableDefinitions $table_definitions, private readonly \EvalMath $math @@ -44,7 +47,7 @@ public function __construct( } #[\Override] - public function getOldQuestionIdentifier(): string + public function getOldQuestionTypeIdentifier(): string { return 'assClozeTest'; } @@ -80,6 +83,11 @@ public function completeMigrationInsert( if (!isset($answer_input_mapping[$db_row->gap_id])) { $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); $answer_options_mapping[$db_row->gap_id] = []; + $this->answer_options_mapping_for_feedback[$db_row->gap_id] = [ + 'is_numeric' => $db_row->cloze_type == \assClozeGap::TYPE_NUMERIC, + 'is_feedback_per_gap' => $db_row->feedback_mode === \ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION, + 'answer_options' => [] + ]; $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, $persistence_factory, @@ -102,6 +110,7 @@ public function completeMigrationInsert( 'is_numeric' => $db_row->cloze_type == \assClozeGap::TYPE_NUMERIC, 'answer_option_id' => $answer_option_id ]; + $this->answer_options_mapping_for_feedback[$db_row->gap_id]['answer_options'][$db_row->aorder] = $answer_option_id; $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, @@ -156,6 +165,46 @@ public function completeMigrationInsert( ); } + #[\Override] + public function getConditionsForFeedbackFromOldValues( + int $answer, + int $question + ): ?array { + $gap = $this->answer_options_mapping_for_feedback[$question]; + + if ($gap['is_feedback_per_gap'] && $answer !== -10 + || !$gap['is_feedback_per_gap'] && $answer === -10) { + return null; + } + + if ($answer === -10) { + return array_map( + fn(Uuid $v): string => $v->toString(), + $gap['answer_options'] + ); + } + + if ($answer === -1) { + return [Types::NothingSelected->value]; + } + + if ($gap['is_numeric']) { + return [$this->buildRangeValue(true, $answer)]; + } + + $answer_option_id = array_filter( + $gap['answer_options'], + fn(string $v): bool => $v == $answer, + ARRAY_FILTER_USE_KEY + ); + + if ($answer_option_id !== []) { + return [$answer_option_id->toString()]; + } + + return ['']; + } + private function fetchDBValues( \ilDBInterface $db, int $old_question_id @@ -219,8 +268,8 @@ private function addCombinationInsertStatements( $persistence_factory, $migration_insert->getTableNameBuilder(), $combinations_insert, - $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), - $migration_insert->getAnswerFormId()->toString(), + $combination_mapping[$db_row->combination_id . $db_row->row_id], + $migration_insert->getAnswerFormId(), $db_row->points ); } @@ -229,9 +278,9 @@ private function addCombinationInsertStatements( $persistence_factory, $migration_insert->getTableNameBuilder(), $combinations_to_answer_options_insert, - $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), - $answer_input_mapping[$db_row->gap_fi]->toString(), - $answer_option['answer_option_id']->toString(), + $combination_mapping[$db_row->combination_id . $db_row->row_id], + $answer_input_mapping[$db_row->gap_fi], + $answer_option['answer_option_id'], $this->buildRangeValue($answer_option['is_numeric'], $db_row->answer) ); } @@ -248,6 +297,12 @@ private function buildCombinationsInsert( Uuid $answer_form_id, float $points ): Insert { + $values = [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + ]; + if ($combinations_insert === null) { return $persistence_factory->insert( $this->table_definitions->getColumns( @@ -255,19 +310,11 @@ private function buildCombinationsInsert( AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationsTableIdentifier() ), - [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), - ] + $values ); } - return $combinations_insert->withAdditionalValues([ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), - ]); + return $combinations_insert->withAdditionalValues($values); } private function buildCombinationsToAnswerOptionsInsert( @@ -277,8 +324,15 @@ private function buildCombinationsToAnswerOptionsInsert( Uuid $combination_id, Uuid $gap_id, Uuid $answer_option_id, - InRange $in_range + ?Range $in_range ): Insert { + $values = [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range?->value) + ]; + if ($combinations_to_answer_options_insert === null) { return $persistence_factory->insert( $this->table_definitions->getColumns( @@ -286,21 +340,11 @@ private function buildCombinationsToAnswerOptionsInsert( AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), - [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) - ] + $values ); } - return $combinations_to_answer_options_insert->withAdditionalValues([ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), - $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) - ]); + return $combinations_to_answer_options_insert->withAdditionalValues($values); } private function buildNewGapTypeIdentifierFromOld( @@ -315,16 +359,24 @@ private function buildNewGapTypeIdentifierFromOld( private function buildRangeValue( bool $is_numeric, - string $value - ): ?string { - if ($is_numeric === null) { + string|int $value + ): ?Range { + if ($is_numeric === false) { return null; } if ($value === 'out_of_bounds') { - return InRange::OutOfRange->value; + return Range::OutOfRange; + } + + if ($value === \ilAssClozeTestFeedback::FB_NUMERIC_GAP_TOO_LOW_INDEX) { + return Range::BelowRange; + } + + if ($value === \ilAssClozeTestFeedback::FB_NUMERIC_GAP_TOO_HIGH_INDEX) { + return Range::AboveRange; } - return InRange::InRange->value; + return Range::InRange; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 3acb296defec..0b9039bf55c5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -38,7 +38,7 @@ public function __construct( } #[\Override] - public function getOldQuestionIdentifier(): string + public function getOldQuestionTypeIdentifier(): string { return 'assLongMenu'; } @@ -171,6 +171,14 @@ public function completeMigrationInsert( ->withAdditionalInsert($answer_options_insert); } + #[\Override] + public function getConditionsForFeedbackFromOldValues( + int $answer, + int $question + ): null { + return null; + } + private function fetchDBValues( \ilDBInterface $db, int $old_question_id diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index 97ba88451dd4..d420353b50bf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -42,7 +42,7 @@ public function __construct( } #[\Override] - public function getOldQuestionIdentifier(): string + public function getOldQuestionTypeIdentifier(): string { return 'assNumeric'; } @@ -120,6 +120,14 @@ public function completeMigrationInsert( ); } + #[\Override] + public function getConditionsForFeedbackFromOldValues( + int $answer, + int $question + ): null { + return null; + } + private function fetchDBValues( \ilDBInterface $db, int $old_question_id diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 495cff193a39..6d305dcf8eac 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -39,7 +39,7 @@ public function __construct( } #[\Override] - public function getOldQuestionIdentifier(): string + public function getOldQuestionTypeIdentifier(): string { return 'assTextSubset'; } @@ -134,6 +134,14 @@ public function completeMigrationInsert( ); } + #[\Override] + public function getConditionsForFeedbackFromOldValues( + int $answer, + int $question + ): null { + return null; + } + private function fetchDBValues( \ilDBInterface $db, int $old_question_id diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index e6d8e6f75d92..c9bb588d5a29 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -98,11 +98,9 @@ public function hasMatchingCombinationForAnswerOptionIds( return false; } - public function getEditView( - \ilToolbarGUI $toolbar - ): EditCombinations { + public function getEditView(): EditCombinations + { return new EditCombinations( - $toolbar, $this->combinations_factory ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php index fe4a08f21609..30d180d52c8e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; -use ILIAS\Questions\AnswerFormTypes\Cloze\Layout\CombinationsOverview; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -33,7 +32,6 @@ class EditCombinations private const string LANG_VAR_EDIT_COMBINATIONS = 'edit_combinations'; public function __construct( - private readonly \ilToolbarGUI $toolbar, private readonly Factory $combinations_factory ) { } @@ -59,7 +57,7 @@ public function show( self::STEP_EDIT_COMBINATIONS_OVERVIEW ); - $combinations_overview = $this->buildCombinationsOverview($environment); + $combinations_overview = $this->buildOverview($environment); $step = $environment->getStep(); if ($step === self::STEP_EDIT_COMBINATIONS_OVERVIEW @@ -70,11 +68,10 @@ public function show( return $combinations_overview->doAction(); } - private function buildCombinationsOverview( + private function buildOverview( Environment $environment - ): CombinationsOverview { - return new CombinationsOverview( - $this->toolbar, + ): Overview { + return new Overview( $environment, $this->combinations_factory ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index 6fe1ce934f69..df1c04c0e6d6 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -39,7 +39,7 @@ public function __construct( private readonly Uuid $combination_id, private readonly Gap $gap, private readonly ?AnswerOption $answer_option = null, - private readonly ?InRange $in_range = null + private readonly ?Range $in_range = null ) { } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php similarity index 96% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index 0d533ad8560c..41b67f0459aa 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Layout; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combination; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Factory as CombinationsFactory; @@ -38,7 +38,7 @@ use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Renderer as UIRenderer; -class CombinationsOverview implements DataRetrieval, Renderable +class Overview implements DataRetrieval, Renderable { private const string STEP_SAVE = 's'; private const string STEP_SET_COMBINATION_VALUES = 'scv'; @@ -49,7 +49,6 @@ class CombinationsOverview implements DataRetrieval, Renderable private ?RoundTripModal $modal = null; public function __construct( - private readonly \ilToolbarGUI $toolbar, private readonly Environment $environment, private readonly CombinationsFactory $combinations_factory ) { @@ -59,8 +58,13 @@ public function __construct( public function render( UIRenderer $ui_renderer ): string { + $modal = $this->buildSetCombinationGapsModal(); $content = [ - $this->initializeModal($this->buildSetCombinationGapsModal()), + $this->environment->getUIFactory()->button()->standard( + $this->environment->getLanguage()->txt('add_combination'), + $modal->getShowSignal() + ), + $modal, $this->buildTable() ]; if ($this->modal !== null) { @@ -116,18 +120,6 @@ private function buildTable(): DataTable ->withRequest($this->environment->getHttpServices()->request()); } - private function initializeModal( - RoundTripModal $modal - ): RoundTripModal { - $this->toolbar->addComponent( - $this->environment->getUIFactory()->button()->standard( - $this->environment->getLanguage()->txt('add_combination'), - $modal->getShowSignal() - ) - ); - return $modal; - } - private function getColumns(): array { $cf = $this->environment->getUIFactory()->table()->column(); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 59e5192a90aa..30795d1a5b39 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -33,8 +33,9 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\MultiSelect; +use ILIAS\UI\Component\Input\Field\Section; +use ILIAS\UI\Component\Input\Field\Select; use ILIAS\UI\Component\Table\DataRowBuilder; class Gaps @@ -297,20 +298,23 @@ function (array $c, Gap $v) use ($lng, $ff): array { ); } + public function buildGapsSelect( + string $label, + FieldFactory $ff + ): Select { + return $ff->select( + $label, + $this->buildOptionsArray() + ); + } + public function buildGapsMultiSelect( string $label, FieldFactory $ff ): MultiSelect { return $ff->multiSelect( $label, - array_reduce( - $this->gaps, - function (array $c, Gap $v): array { - $c[$v->getAnswerInputId()->toString()] = $v->buildShortenedGapName(); - return $c; - }, - [] - ) + $this->buildOptionsArray() ); } @@ -514,6 +518,18 @@ private function extractIdFromTagName( return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); } + private function buildOptionsArray(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->buildShortenedGapName(); + return $c; + }, + [] + ); + } + private function retrieveGapsForInputs( bool $is_in_creation_context, array $selected_gaps diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index 773e17b34c8b..6659ca1413b2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -37,11 +37,11 @@ class LongMenu extends Type public function __construct( Refinery $refinery, - private readonly Language $lng, + Language $lng, private readonly UIFactory $ui_factory, private readonly GlobalTemplate $global_tpl ) { - parent::__construct($refinery); + parent::__construct($refinery, $lng); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index 540a6f45b2f5..51b97f66414c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,9 +20,10 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\InRange; +use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -36,10 +37,10 @@ class Numeric extends Type public function __construct( Refinery $refinery, - private readonly Language $lng, + Language $lng, private readonly UIFactory $ui_factory ) { - parent::__construct($refinery); + parent::__construct($refinery, $lng); } #[\Override] @@ -152,10 +153,35 @@ public function getBuildGapTransformation( public function getCombinationsSelectValues( Gap $gap ): array { - $values = []; - foreach (InRange::cases() as $in_range) { - $values[$in_range->value] = $in_range->getLabel($this->lng); - } - return $values; + return [ + Range::InRange->value => Range::InRange->getLabel($this->lng), + Range::OutOfRange->value => Range::OutOfRange->getLabel($this->lng) + ]; + } + + #[\Override] + public function getFeedbackSelectValues( + Gap $gap, + bool $is_marking_required + ): array { + return $this->getCombinationsSelectValues($gap); + } + + #[\Override] + public function isValidFeedbackCondition( + UuidFactory $uuid_factory, + Gap $gap, + string $condition + ): bool { + return Range::tryFrom($condition) !== null; + } + + #[\Override] + public function getLabelForValue( + UuidFactory $uuid_factory, + Gap $gap, + string $value + ): string { + return Range::tryFrom($value)?->getLabel($this->lng) ?? ''; } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index bef4eeadf8f4..62a07425905d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -35,10 +35,10 @@ class Select extends Type public function __construct( Refinery $refinery, - private readonly Language $lng, + Language $lng, private readonly UIFactory $ui_factory ) { - parent::__construct($refinery); + parent::__construct($refinery, $lng); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 851b45ffc422..2234e052ff44 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -35,10 +35,10 @@ class Text extends Type public function __construct( Refinery $refinery, - private readonly Language $lng, + Language $lng, private readonly UIFactory $ui_factory ) { - parent::__construct($refinery); + parent::__construct($refinery, $lng); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index e9344e48b384..ba8cd0970b58 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -20,15 +20,21 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types as FeedbackTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\Definitions\Range; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; +use Ramsey\Uuid\Exception\InvalidUuidStringException; abstract class Type { public function __construct( - protected readonly Refinery $refinery + protected readonly Refinery $refinery, + protected readonly Language $lng ) { } @@ -62,6 +68,56 @@ public function getCombinationsSelectValues( ); } + public function getFeedbackSelectValues( + Gap $gap, + bool $is_marking_required + ): array { + $basic_select_values = $this->getCombinationsSelectValues($gap); + if (!$is_marking_required) { + return $basic_select_values; + } + return array_merge( + [ + FeedbackTypes::MaxPoints => FeedbackTypes::MaxPoints + ->getTranslatedOptionName($this->lng), + FeedbackTypes::NotMaxPoints => FeedbackTypes::NotMaxPoints + ->getTranslatedOptionName($this->lng), + FeedbackTypes::NothingSelected => FeedbackTypes::NothingSelected + ->getTranslatedOptionName($this->lng) + ], + $basic_select_values + ); + } + + public function isValidFeedbackCondition( + UuidFactory $uuid_factory, + Gap $gap, + string $condition + ): bool { + if (FeedbackTypes::tryFrom($condition) !== null + || Range::tryFrom($condition) !== null) { + return true; + } + + try { + return $gap->getAnswerOptions()->getAnswerOptionById( + $uuid_factory->fromString($condition) + ) !== null; + } catch (InvalidUuidStringException $e) { + return false; + } + } + + public function getLabelForValue( + UuidFactory $uuid_factory, + Gap $gap, + string $value + ): string { + return $gap->getAnswerOptions()->getAnswerOptionById( + $uuid_factory->fromString($value) + )->getTextValue(); + } + public function getAddPointsTransformation( Gap $gap ): Transformation { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 85bec54bc8d5..f08d8fa6a542 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -43,7 +43,6 @@ class Edit implements EditViewInterface private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; public function __construct( - private readonly \ilToolbarGUI $toolbar, private readonly PropertiesFactory $properties_factory, private readonly ClozeTextFactory $cloze_text_factory, private readonly EditGaps $edit_gaps @@ -76,9 +75,7 @@ public function edit( $combinations = $environment->getAnswerFormProperties()->getCombinations(); if ($combinations->areCombinationsEnabled()) { - $combinations->getEditView( - $this->toolbar - )->addCombinationsSubTab($environment); + $combinations->getEditView()->addCombinationsSubTab($environment); } if ($step === '') { @@ -112,9 +109,7 @@ public function other( ): Async|Renderable|Properties { return $environment ->getAnswerFormProperties() - ->getCombinations()->getEditView( - $this->toolbar - )->show($environment); + ->getCombinations()->getEditView()->show($environment); } private function startEditing( diff --git a/components/ILIAS/Questions/src/Definitions/Range.php b/components/ILIAS/Questions/src/Definitions/Range.php new file mode 100644 index 000000000000..5c398e065404 --- /dev/null +++ b/components/ILIAS/Questions/src/Definitions/Range.php @@ -0,0 +1,71 @@ + $lng->txt('in_range'), + self::OutOfRange => $lng->txt('out_of_range'), + self::BelowRange => $lng->txt('below_range'), + self::AboveRange => $lng->txt('above_range') + }; + } + + public function isValueInRange( + float $lower_limit, + float $upper_limit, + float $value + ): self { + if ($value < $lower_limit + || $value > $upper_limit) { + return self::OutOfRange; + } + + return self::InRange; + } + + public function checkValue( + float $lower_limit, + float $upper_limit, + float $value + ): self { + if ($value < $lower_limit) { + return self::BelowRange; + } + + if ($value > $upper_limit) { + return self::AboveRange; + } + + return self::InRange; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/ManipulationType.php b/components/ILIAS/Questions/src/Persistence/ManipulationType.php index 069a58b51d5f..8bca6030c258 100644 --- a/components/ILIAS/Questions/src/Persistence/ManipulationType.php +++ b/components/ILIAS/Questions/src/Persistence/ManipulationType.php @@ -24,5 +24,6 @@ enum ManipulationType { case Create; case Update; + case Replace; case Delete; } diff --git a/components/ILIAS/Questions/src/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/Persistence/TableDefinitions.php index 975382281285..00ce987ed94c 100644 --- a/components/ILIAS/Questions/src/Persistence/TableDefinitions.php +++ b/components/ILIAS/Questions/src/Persistence/TableDefinitions.php @@ -41,13 +41,13 @@ public function getIdColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, string $sub_table_identifier = '' - ): Column; + ): ?Column; public function getForeignKeyColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, string $sub_table_identifier = '' - ): Column; + ): ?Column; public function completeQuery( Query $query, diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 10416642efa6..cec5b7e35a82 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -65,6 +65,10 @@ public function withDefaultStep(): self; public function getEditability(): Editability; + public function isCapabilityRequired( + string $capability + ): bool; + public function isInCreationContext(): bool; /** @@ -86,4 +90,6 @@ public function withStepParameter( ): self; public function withPreservedTableRowIdsParameter(): self; + + public function redirectTo(URLBuilder $target): void; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php index 8b5b5bdbb5e6..fc3163143c03 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php @@ -77,6 +77,7 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly Factory $presentation_factory, private readonly Editability $editability, + private readonly array $required_capabilities, URI $base_uri, private readonly int $obj_id ) { @@ -184,6 +185,16 @@ public function getEditability(): Editability return $this->editability; } + #[\Override] + public function isCapabilityRequired( + string $capability + ): bool { + return array_key_exists( + $capability, + $this->required_capabilities + ); + } + #[\Override] public function isInCreationContext(): bool { @@ -271,6 +282,14 @@ public function withPreservedTableRowIdsParameter(): self return $clone; } + #[\Override] + public function redirectTo(URLBuilder $target): void + { + $this->ctrl->redirectToURL( + $target->buildURI()->__toString() + ); + } + public function withIsInCreationContext( bool $is_in_creation_context ): self { @@ -433,9 +452,12 @@ public function isCreateModeSimple(): bool ); } + /** + * + * @param array<\ILIAS\Questions\AnswerForm\Capabilities\Action> $additional_actions + */ public function setEditAnswerFormTabs( - string $cmd_feedback, - string $cmd_content_for_repetition + array $additional_actions ): void { $this->tabs_gui->addTab( self::TAB_ID_ANSWER_FORM, @@ -443,23 +465,13 @@ public function setEditAnswerFormTabs( $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() ); - $this->tabs_gui->addTab( - $cmd_feedback, - $this->lng->txt('feedback'), - $this->withActionParameter($cmd_feedback) - ->getUrlBuilder() - ->buildURI() - ->__toString() - ); - - $this->tabs_gui->addTab( - $cmd_content_for_repetition, - $this->lng->txt('suggested_solution'), - $this->withActionParameter($cmd_content_for_repetition) - ->getUrlBuilder() - ->buildURI() - ->__toString() - ); + foreach ($additional_actions as $action) { + $action->addTab( + $this, + $this->tabs_gui, + $this->lng + ); + } $this->tabs_gui->addSubTab( self::TAB_ID_ANSWER_FORM, diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index ef2a66328e86..a84cb29c96cf 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -29,10 +29,13 @@ use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\Component\Button\Primary as PrimaryButton; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; class QuestionsTable implements Renderable, DataRetrieval { + private ?PrimaryButton $create_question_button = null; + public function __construct( private readonly \ilUIService $ui_service, private readonly AnswerFormFactory $answer_form_factory, @@ -45,7 +48,25 @@ public function __construct( public function render( UIRenderer $ui_renderer ): string { - return $ui_renderer->render($this->buildContent()); + + $rendered_content = ''; + + if ($this->create_question_button !== null) { + $toolbar = new \ilToolbarGUI(); + $toolbar->addComponent( + $this->create_question_button + ); + $rendered_content = $toolbar->getHTML(); + } + return $rendered_content . $ui_renderer->render($this->buildContent()); + } + + public function withCreateQuestionButton( + PrimaryButton $create_question_button + ): self { + $clone = clone $this; + $clone->create_question_button = $create_question_button; + return $clone; } #[\Override] diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 51e8f474af84..d58e8ffec38c 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,8 +20,9 @@ namespace ILIAS\Questions\Presentation\Views; +use ILIAS\Questions\AnswerForm\Capabilities\Action; use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\Factory as CapabilitesFactory; use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; @@ -60,8 +61,6 @@ class Edit public const string ACTION_DELETE_QUESTIONS = 'delete'; private const string ACTION_CREATE_ANSWER_FORM = 'create_af'; public const string ACTION_OTHER_ANSWER_FORM = 'other_af'; - private const string ACTION_EDIT_FEEDBACK = 'edit_f'; - private const string ACTION_EDIT_CONTENT_FOR_REPETITION = 'edit_cfr'; private array $required_capabilities = []; private Editability $editability = Editability::Full; @@ -82,6 +81,7 @@ public function __construct( private readonly \ilTabsGUI $tabs_gui, private readonly \ilUIService $ui_services, private readonly UuidFactory $uuid_factory, + private readonly CapabilitesFactory $capabilities_factory, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, private readonly LayoutFactory $definitions_factory @@ -91,9 +91,10 @@ public function __construct( public function withRequiredCapabilities( array $capability_class_names ): self { - $this->checkCapabilities($capability_class_names); $clone = clone $this; - $clone->required_capabilities = $capability_class_names; + $clone->required_capabilities = $this->buildCapabilities( + $capability_class_names + ); return $clone; } @@ -114,7 +115,6 @@ public function withOrderingEnabled( } public function show( - \ilToolbarGUI $toolbar, URI $base_uri, int $obj_id, int $ref_id @@ -135,7 +135,7 @@ public function show( ), self::ACTION_EDIT_QUESTION => $this->editQuestion($environment), self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), - default => $this->showTable($toolbar, $environment) + default => $this->showTable($environment) }; } @@ -170,7 +170,8 @@ public function forwardPageCmds( $this->questions_repository->getForQuestionId( $environment->getQuestionId() ), - $obj_id + $obj_id, + $this )->withReturnURI( $environment ->withActionParameter(self::ACTION_EDIT_QUESTION) @@ -241,28 +242,57 @@ public function editAnswerForm( QuestionImplementation $question, AnswerFormProperties $answer_form_properties, Definition $type_definition - ): Renderable { + ): Async|Renderable { $environment = $this->buildEnvironment( $base_uri, $obj_id )->withAnswerFormProperties($answer_form_properties) ->withQuestionIdParameter($question->getId()); + $capability_actions = array_filter( + array_map( + fn(Capability $v): ?Action => $v->getEditAction(), + $this->required_capabilities + ) + ); + $environment->setEditAnswerFormTabs( - self::ACTION_EDIT_FEEDBACK, - self::ACTION_EDIT_CONTENT_FOR_REPETITION + $capability_actions ); - [$environment, $next] = $this->doEditAction( - $environment, - $type_definition + $action = $environment->getAction(); + + $capability_action = array_filter( + $capability_actions, + fn(Action $v): bool => $v->isThis($action) ); + if ($capability_action !== []) { + $capability_action[0]->activateTab($this->tabs_gui); + return $capability_action[0]->getCapability()->edit( + $environment->withActionParameter($action) + ); + } + + $edit_view = $type_definition->getEditView(); + + if ($action === self::ACTION_OTHER_ANSWER_FORM) { + $environment = $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM); + $next = $edit_view->other($environment); + } else { + $next = $edit_view->edit($environment); + } - if ($next instanceof Renderable) { + if (!($next instanceof AnswerFormProperties)) { return $next; } - $this->storeEditResult($next); + $this->questions_repository->update( + [$question->withAnswerFormProperties($next)] + ); + + foreach ($this->required_capabilities as $capability) { + $capability->onAnswerFormUpdate($next); + } $this->ctrl->redirectToURL( $environment->getUrlBuilder()->buildURI()->__toString() @@ -378,10 +408,14 @@ private function deleteQuestions( } private function showTable( - \ilToolbarGUI $toolbar, EnvironmentImplementation $environment ): QuestionsTable { - $toolbar->addComponent( + return new QuestionsTable( + $this->ui_services, + $this->answer_form_factory, + $this->questions_repository, + $environment + )->withCreateQuestionButton( $this->ui_factory->button()->primary( $this->lng->txt('create'), $environment->withActionParameter(self::ACTION_CREATE_QUESTION) @@ -390,13 +424,6 @@ private function showTable( ->__toString() ) ); - - return new QuestionsTable( - $this->ui_services, - $this->answer_form_factory, - $this->questions_repository, - $environment - ); } private function processCreateAnswerForm( @@ -470,56 +497,6 @@ private function forwardCreateAnswerFormCmd( ); } - /** - * @return array{ - * \ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation, - * \ILIAS\Questions\AnswerForm\Definition - * } - */ - private function doEditAction( - EnvironmentImplementation $environment, - Definition $type_definition - ): array { - $action = $environment->getAction(); - if ($action === self::ACTION_EDIT_FEEDBACK) { - $environment = $environment->withActionParameter( - self::ACTION_OTHER_ANSWER_FORM - ); - return [ - $environment, - $type_definition->getCapability(Feedback::class)->edit($environment) - ]; - } - - - if ($action === self::ACTION_OTHER_ANSWER_FORM) { - $environment = $environment->withActionParameter( - self::ACTION_OTHER_ANSWER_FORM - ); - return [ - $environment, - $type_definition->getEditView()->other($environment) - ]; - } - - return [ - $environment, - $type_definition->getEditView()->edit($environment) - ]; - } - - private function storeEditResult( - Capability|AnswerFormProperties $result - ): void { - if ($result instanceof Capability) { - $this->questions_repository->storeCapability($result); - } - - $this->questions_repository->update( - [$question->withAnswerFormProperties($result)] - ); - } - private function initializeEditMode( EnvironmentImplementation $environment ): void { @@ -659,17 +636,25 @@ private function buildAffectedItems( return $affected_items; } - private function checkCapabilities( + /** + * @param list $capabilities + * @return list<\ILIAS\Questions\AnswerForm\Capabilities\Capability> + */ + private function buildCapabilities( array $capabilities - ): void { - foreach ($capabilities as $capability) { - if (!$this->questions_repository->capabilityExists($capability)) { - throw new \InvalidArgumentException( - 'All provided capabilities must implement ' - . 'ILIAS\Questions\AnswerForm\Capabilities\Capability.' - ); - } - } + ): array { + return array_map( + function (string $v): Capability { + $capability = $this->capabilities_factory->get($v); + if ($capability === null) { + throw new \InvalidArgumentException( + "The capability {$v} does not exist." + ); + } + return $capability; + }, + $capabilities + ); } private function deleteSelectedQuestions( @@ -702,6 +687,7 @@ private function buildEnvironment( $this->uuid_factory, $this->definitions_factory, $this->editability, + $this->required_capabilities, $base_uri, $obj_id ); diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index ad55883a11f6..8f0ac87daa7f 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -39,10 +39,15 @@ class Agent implements SetupAgent { use HasNoNamedObjective; + /** + * @param array<\ILIAS\Questions\AnswerForm\Migration\Migration> $answer_form_migrations + * @param array<\ILIAS\Questions\AnswerForm\Capabilities\Migration> $capability_migrations + */ public function __construct( private readonly PersistenceFactory $persistence_factory, private readonly TableNameBuilder $question_table_name_builder, - private readonly array $answer_form_migrations + private readonly array $answer_form_migrations, + private readonly array $capability_migrations ) { } @@ -129,7 +134,8 @@ public function getMigrations(): array new AnswerFormGenericTableDefinitions( $this->persistence_factory ), - $this->answer_form_migrations + $this->answer_form_migrations, + $this->capability_migrations ) ]; } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 7f12a7d249dc..7ab9a2678af5 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -21,7 +21,7 @@ namespace ILIAS\Questions\Setup; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\FeedbackTableTypes; +use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableTypes as FeedbackTableTypes; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -309,6 +309,11 @@ public function step_7(): void ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], 'answer_form_id' => [ 'type' => \ilDBConstants::T_TEXT, 'length' => 64, @@ -321,13 +326,9 @@ public function step_7(): void ], 'condition' => [ 'type' => \ilDBConstants::T_TEXT, - 'length' => 16, + 'length' => 64, 'notnull' => false ], - 'feedback_best_response_legacy' => [ - 'type' => \ilDBConstants::T_CLOB, - 'notnull' => true - ], 'feedback' => [ 'type' => \ilDBConstants::T_CLOB, 'notnull' => true @@ -340,6 +341,16 @@ public function step_7(): void } if (!$this->db->primaryExistsByFields( + $table_name, + ['id'] + )) { + $this->db->addPrimaryKey( + $table_name, + ['id'], + ); + } + + if (!$this->db->indexExistsByFields( $table_name, [ 'answer_form_id', @@ -347,13 +358,8 @@ public function step_7(): void 'condition' ] )) { - $this->db->addPrimaryKey( - $table_name, - [ - 'answer_form_id', - 'parent_id', - 'condition' - ], + $this->db->manipulate( + "CREATE UNIQUE INDEX apc_idx ON {$table_name} (`answer_form_id`, `parent_id`, `condition`)" ); } } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 28a1fa368903..c7ca85d482b6 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Setup; +use ILIAS\Questions\AnswerForm\Capabilities\Migration as CapabilityMigration; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert as AnswerFormMigrationInsert; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; @@ -51,17 +52,22 @@ class QuestionsMigration implements Migration private ?array $allready_migrated_questions = null; private ?array $allready_migrated_questions_in_qpls = null; + /** + * @param array<\ILIAS\Questions\AnswerForm\Migration\Migration> $answer_form_migrations + * @param array<\ILIAS\Questions\AnswerForm\Capabilities\Migration> $capability_migrations + */ public function __construct( private readonly PersistenceFactory $persistence_factory, private readonly TableNameBuilder $question_table_name_builder, private readonly QuestionTableDefinitions $question_table_definitions, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - array $answer_form_migrations + array $answer_form_migrations, + private readonly array $capability_migrations ) { $this->answer_form_migrations = array_reduce( $answer_form_migrations, function (array $c, AnswerFormMigration $v): array { - $c[$v->getOldQuestionIdentifier()] = $v; + $c[$v->getOldQuestionTypeIdentifier()] = $v; return $c; }, [] @@ -123,6 +129,7 @@ public function step( $migration_insert = $answer_form_migration->completeMigrationInsert( $environment, + $this->persistence_factory, $this->buildMigrationInsert( $answer_form_migration, [ @@ -163,7 +170,16 @@ public function step( return; } - $migration_insert->run(); + array_reduce( + $this->capability_migrations, + fn(AnswerFormMigrationInsert $c, CapabilityMigration $v): AnswerFormMigration + => $v->completeMigrationInsert( + $environment, + $this->persistence_factory, + $migration_insert + ), + $migration_insert + )->run(); $this->io->inform("{$new_question_id->toString()} successfully migrated."); } @@ -182,7 +198,7 @@ public function getRemainingAmountOfSteps(): int . implode( ', ', array_map( - fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionTypeIdentifier()}'", $this->answer_form_migrations ) ) . ')' . PHP_EOL @@ -209,7 +225,7 @@ private function fetchValidRecord(): ?\stdClass . implode( ', ', array_map( - fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionTypeIdentifier()}'", $this->answer_form_migrations ) ) . ')' . PHP_EOL From 49d738ec0c6348d98a97c0aaae84d4ac7f931507 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 13 Mar 2026 14:48:22 +0100 Subject: [PATCH 079/108] Questions: Remove Unneeded Parameter --- .../Questions/src/AnswerForm/Migration/MigrationInsert.php | 2 -- components/ILIAS/Questions/src/Setup/QuestionsMigration.php | 1 - 2 files changed, 3 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index abbc76d2a9ad..888e96e27cc7 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -22,7 +22,6 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; -use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -43,7 +42,6 @@ public function __construct( private readonly IOWrapper $io, private readonly UuidFactory $uuid_factory, private readonly PersistenceFactory $persistence_factory, - private readonly QuestionTableDefinitions $question_table_definitions, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, private readonly TableNameBuilder $table_name_builder, private array $inserts, diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index c7ca85d482b6..168b23d0b264 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -446,7 +446,6 @@ private function buildMigrationInsert( $this->io, $this->uuid_factory, $this->persistence_factory, - $this->question_table_definitions, $this->answer_form_generic_table_definitions, new TableNameBuilder( $answer_form_migration->getTableNameSpace() From 4985da960dc4a82eaae71e3109f5b5cbc9363926 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 13 Mar 2026 16:33:46 +0100 Subject: [PATCH 080/108] Questions: Always use FormActions for CSRF-Checks --- .../Legacy/Administration/class.ilObjQuestionsGUI.php | 2 +- .../Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index ff384f375286..7a4348677fc3 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -172,7 +172,7 @@ protected function getTabs(): void private function buildEditQuestionsBaseUri(): URI { return $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'viewQuestions') ); } } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index f4aee1e50d2e..8de7fcddfd8a 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -61,7 +61,7 @@ public function insertCmd(): void $this->tpl->setContent( $this->edit_view->createAnswerForm( $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'insert') ), $this->pg_obj->getParentId(), $this->pg_obj->getQuestion(), @@ -81,7 +81,7 @@ public function editCmd(): void $this->tpl->setContent( $this->edit_view->editAnswerForm( $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'edit') + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'edit') ), $this->pg_obj->getParentId(), $question, From bdddd07ab5bed8bbbbfe2261ed6b1685d18fc716 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 13 Mar 2026 16:34:37 +0100 Subject: [PATCH 081/108] Questions: Remove Unneeded Include --- components/ILIAS/Questions/src/AnswerForm/Definition.php | 1 - 1 file changed, 1 deletion(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 033c287b4fbe..3c95d98d4cab 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerForm; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\AnswerForm\Views\Participant; use ILIAS\Questions\Persistence\Query; From efdade6439674fc7bebcdcbb2684d4de2f1347b1 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 31 Mar 2026 09:56:41 +0200 Subject: [PATCH 082/108] Questions: Implement Suggested Learning Content --- .../class.ilObjQuestionsGUI.php | 4 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 30 +- .../PageEditor/class.QstsQuestionPageGUI.php | 1 + components/ILIAS/Questions/Questions.php | 8 + .../Capabilities/Feedback/Repository.php | 4 +- .../SuggestedLearningContent/Content.php | 188 +++++++++ .../SuggestedLearningContent/Migration.php | 276 +++++++++++++ .../NodeRetrieval.php | 277 +++++++++++++ .../SuggestedLearningContent/Overview.php | 375 ++++++++++++++++++ .../SuggestedLearningContent/Repository.php | 173 ++++++++ .../SuggestedLearningContent/Stakeholder.php | 39 ++ .../SuggestedLearningContent.php | 93 +++++ .../TableDefinitions.php | 131 ++++++ .../SuggestedLearningContent/TableTypes.php | 28 ++ .../SuggestedLearningContent/Types.php | 295 ++++++++++++++ .../Capabilities/FeedbackOverviewTable.php | 5 +- .../Properties/Combinations/Overview.php | 4 +- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 3 +- .../AnswerFormTypes/Cloze/Views/EditGaps.php | 5 +- .../UploadAnswerOptions.php} | 0 .../ILIAS/Questions/src/Persistence/Query.php | 26 +- .../src/Presentation/Definitions/Actor.php | 34 ++ .../src/Presentation/Layout/Async.php | 9 +- .../src/Presentation/Layout/EditForm.php | 1 + .../src/Presentation/Layout/Factory.php | 32 +- .../Layout/{ => Tools}/InputsBuilder.php | 2 +- .../{ => Tools}/InputsBuilderSession.php | 13 +- .../Layout/Tools/UploadHandler.php | 364 +++++++++++++++++ .../Questions/src/Presentation/Views/Edit.php | 22 +- .../src/Question/Persistence/Repository.php | 12 +- .../src/Setup/OverarchingQuestionTables.php | 36 ++ 31 files changed, 2436 insertions(+), 54 deletions(-) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Stakeholder.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableTypes.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Types.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/{Properties/Gaps/class.UploadAnswerOptionsGUI.php => Views/UploadAnswerOptions.php} (100%) create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/Actor.php rename components/ILIAS/Questions/src/Presentation/Layout/{ => Tools}/InputsBuilder.php (92%) rename components/ILIAS/Questions/src/Presentation/Layout/{ => Tools}/InputsBuilderSession.php (86%) create mode 100644 components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 7a4348677fc3..a80c30c94f2f 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -20,6 +20,7 @@ use ILIAS\Questions\Administration\ConfigurationGUI; use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Questions\Legacy\LocalDIC; @@ -59,7 +60,8 @@ public function __construct( $this->units_repository = $local_dic[UnitsRepository::class]; $this->edit_view = $local_dic[Edit::class] ->withRequiredCapabilities([ - Feedback::class + Feedback::class, + SuggestedLearningContent::class ]); $this->configuration_repository = $local_dic[ConfigurationRepository::class]; diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index ffe96a8f5292..9c6028934fb7 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -23,7 +23,6 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableDefinitions as FeedbackTableDefinitions; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; @@ -59,6 +58,10 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[UuidFactory::class] = static fn($c): UuidFactory => new UuidFactory(); $dic[MustacheEngine::class] = static fn($c): MustacheEngine => new MustacheEngine(['escape' => static fn($v) => $v]); + $dic[\ilFileServicesFilenameSanitizer::class] = static fn($c): \ilFileServicesFilenameSanitizer + => new \ilFileServicesFilenameSanitizer( + $DIC->fileServiceSettings() + ); $dic[ConfigurationRepository::class] = static fn($c): ConfigurationRepository => new ConfigurationRepository( @@ -88,11 +91,28 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[UuidFactory::class], $c[DataFactory::class]->text(), $c[PersistenceFactory::class], - new FeedbackTableDefinitions( + new Capabilities\Feedback\TableDefinitions( $c[PersistenceFactory::class] ) ) ), + Capabilities\SuggestedLearningContent\SuggestedLearningContent::class + => new Capabilities\SuggestedLearningContent\SuggestedLearningContent( + $DIC['ilCtrl'], + $DIC['rbacsystem'], + $DIC['tree'], + $DIC['static_url'], + $DIC['user']->getLoggedInUser(), + new Capabilities\SuggestedLearningContent\Repository( + $DIC['ilDB'], + $DIC['refinery'], + $DIC['resource_storage'], + $c[PersistenceFactory::class], + new Capabilities\SuggestedLearningContent\TableDefinitions( + $c[PersistenceFactory::class] + ) + ) + ), Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability() ]); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory @@ -116,7 +136,11 @@ protected static function buildDIC(ILIASContainer $DIC): self new LayoutFactory( $DIC['ui.factory'], $DIC['http'], - $DIC['lng'] + $DIC['lng'], + $DIC['resource_storage'], + $DIC['filesystem']->temp(), + $DIC['upload'], + $c[\ilFileServicesFilenameSanitizer::class] ); $dic[Edit::class] = static fn($c): Edit => new Edit( $DIC['lng'], diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index 1d312d1ad6cd..6bd28a146887 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -26,6 +26,7 @@ * * @ilCtrl_Calls QstsQuestionPageGUI: ilPageEditorGUI, ilEditClipboardGUI * @ilCtrl_Calls QstsQuestionPageGUI: ilPublicUserProfileGUI, ilCommentGUI + * @ilCtrl_Calls QstsQuestionPageGUI: ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\UploadHandlerGUI */ class QstsQuestionPageGUI extends ilPageObjectGUI { diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 8fc685442e02..bb99c6fe2e72 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -23,6 +23,8 @@ use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Migration as FeedbackMigration; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableDefinitions as FeedbackTableDefinitions; use ILIAS\Questions\AnswerForm\Capabilities\Migration as CapabilityMigration; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\Migration as SuggestedLearningContentMigration; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\TableDefinitions as SuggestedLearningContentTableDefinitions; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationCloze; @@ -111,6 +113,12 @@ public function init( $internal[PersistenceFactory::class] ) ); + $contribute[CapabilityMigration::class] = static fn() + => new SuggestedLearningContentMigration( + new SuggestedLearningContentTableDefinitions( + $internal[PersistenceFactory::class] + ) + ); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php index 18b086d93ae2..a86fb948d170 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php @@ -68,12 +68,12 @@ public function getFor( $answer_form_id->toString() ) ) - )->loadNextRecord( + )->withGroupBy( $this->feedback_table_definitions->getIdColumn( $this->feedback_table_names_builder, TableTypes::FeedbackGeneric ) - )->current(); + )->loadNextRecord()->current(); if ($database_values === null) { return $feedback; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php new file mode 100644 index 000000000000..1fe94cce976c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php @@ -0,0 +1,188 @@ +target_ref_id = $decoded_content[self::KEY_TARGET_REF_ID] ?? null; + $this->sub_object_id = $decoded_content[self::KEY_SUB_OBJECT_ID] ?? null; + $this->rid = $this->irss->manage()->find( + $decoded_content[self::KEY_RID] ?? '' + ); + $this->file_title = $decoded_content[self::KEY_FILE_TITLE] ?? ''; + + } + + public function getAnswerFormId(): Uuid + { + return $this->answer_form_id; + } + + public function getType(): Types + { + return $this->type; + } + + public function withType( + Types $type + ): self { + $clone = clone $this; + $clone->type = $type; + return $clone; + } + + public function getTargetRefId(): ?int + { + return $this->target_ref_id; + } + + public function withTargetRefId( + int $target_ref_id + ): self { + $clone = clone $this; + $clone->target_ref_id = $target_ref_id; + return $clone; + } + + public function withSubObjectId( + int $sub_object_id + ): self { + $clone = clone $this; + $clone->sub_object_id = $sub_object_id; + return $clone; + } + + public function withFileInfo( + array $file_info + ): self { + $clone = clone $this; + $clone->rid = $this->irss->manage()->find($file_info[self::KEY_RID][0]); + $clone->file_title = $file_info[self::KEY_FILE_TITLE]; + return $clone; + } + + public function getContentForPresentation( + \ilCtrl $ctrl, + StaticURLServices $static_url, + Environment $environment + ): ?StandardLink { + return $this->type->present( + $ctrl, + $static_url, + $this->irss, + $environment, + $this->file_title, + $this->rid, + $this->target_ref_id, + $this->sub_object_id + ); + } + + public function getListing( + \ilCtrl $ctrl, + StaticURLServices $static_url, + Environment $environment + ): array { + $lng = $environment->getLanguage(); + + return [ + $lng->txt('type') => $this->type->getTranslatedOptionName($lng), + $lng->txt('content') => $this + ->getContentForPresentation( + $ctrl, + $static_url, + $environment + ) ?? '' + ]; + } + + public function toStorage( + PersistenceFactory $persistence_factory + ): array { + if ($this->type === null) { + throw new \UnexpectedValueException( + 'You cannot save a Suggested Learnign Content without a Type!' + ); + } + + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->type->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + json_encode($this->buildContentArray()) + ) + ]; + } + + private function buildContentArray(): array + { + $array = []; + if ($this->target_ref_id !== null) { + $array[self::KEY_TARGET_REF_ID] = $this->target_ref_id; + } + + if ($this->sub_object_id !== null) { + $array[self::KEY_SUB_OBJECT_ID] = $this->sub_object_id; + } + + if ($this->rid !== null) { + $array[self::KEY_RID] = $this->rid->serialize(); + } + + if ($this->file_title !== '') { + $array[self::KEY_FILE_TITLE] = $this->file_title; + } + + return $array; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php new file mode 100644 index 000000000000..2e46fdcbf466 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php @@ -0,0 +1,276 @@ +table_definitions->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + PersistenceFactory $persistence_factory, + AnswerFormMigration $answer_form_migration, + MigrationInsert $migration_insert + ): ?MigrationInsert { + if ($this->old_repository === null) { + $this->old_repository = new SuggestedSolutionsDatabaseRepository( + $migration_insert->getDb() + ); + $this->irss_migration_helper = new \ilResourceStorageMigrationHelper( + new Stakeholder(), + $environment + ); + } + + $old_suggested_learning_content = $this->old_repository->selectFor( + $migration_insert->getOldQuestionId() + )[0] ?? null; + + if ($old_suggested_learning_content === null) { + return null; + } + + $insert = $this->buildInsertFromOldSuggestedSolution( + $migration_insert->getAnswerFormId(), + $old_suggested_learning_content + ); + + if ($insert === null) { + return null; + } + + return $migration_insert->withAdditionalInsert($insert); + } + + private function buildInsertFromOldSuggestedSolution( + MigrationInsert $migration_insert, + SuggestedSolution $old_values + ): ?Insert { + $type = $this->buildNewTypeFromOld($old_values->getType())->value; + $content = $this->buildContentStringFromOldSuggesteSolution( + $migration_insert->getDb(), + $type, + $old_values + ); + + if ($content === null) { + return null; + } + + $pf = $migration_insert->getPersistenceFactory(); + return $pf->insert( + $this->table_definitions->getColumns( + $migration_insert->getTableNameBuilder(), + TableTypes::SuggestedLearningContent + ), + [ + $pf->value( + \ilDBConstants::T_TEXT, + $migration_insert->getAnswerFormId()->toString() + ), + $pf->value( + \ilDBConstants::T_TEXT, + $type + ), + $pf->value( + \ilDBConstants::T_TEXT, + json_encode($content) + ), + ] + ); + + } + + private function buildNewTypeFromOld( + string $old_type + ): Types { + return match($old_type) { + SuggestedSolution::TYPE_FILE => Types::File, + SuggestedSolution::TYPE_LM => Types::LearningModule, + SuggestedSolution::TYPE_LM_PAGE => Types::LearningModulePage, + SuggestedSolution::TYPE_LM_CHAPTER => Types::LearningModuleChapter, + SuggestedSolution::TYPE_GLOSARY_TERM => Types::GlossaryTerm + }; + } + + private function buildContentStringFromOldSuggesteSolution( + \ilDBInterface $db, + Types $type, + SuggestedSolution $old_values + ): ?array { + $old_parent_and_owner = $this->fetchQuestionParentAndOwnerFromDB( + $db, + $old_values->getQuestionId() + ); + if ($old_values instanceof SuggestedSolutionFile) { + $rid = $this->irss_migration_helper->movePathToStorage( + $this->buildFilePath( + $old_parent_and_owner->obj_fi, + $old_values + ), + $this->determineNewOwner( + $old_parent_and_owner->owner + ) + ); + + if ($rid === null) { + return null; + } + + return [ + Content::KEY_FILE_TITLE => $old_values->getTitle() === $old_values->getFilename() + ? '' + : $old_values->getTitle(), + Content::KEY_RID => $rid->serialize() + ]; + } + + $id = $this->parseInternalLink( + $old_values->getInternalLink() + ); + + if ($id === null) { + return null; + } + + if ($type === Types::LearningModule) { + return [ + Content::KEY_TARGET_REF_ID => $id + ]; + } + + $target_ref_id = $this->fetchTargetRefIdFromDB( + $db, + $type, + $id + ); + + if ($target_ref_id === null) { + return null; + } + + return [ + Content::KEY_TARGET_REF_ID => $target_ref_id, + Content::KEY_SUB_OBJECT_ID => $id + ]; + } + + private function fetchQuestionParentAndOwnerFromDB( + \ilDBInterface $db, + int $old_question_id + ): \stdClass { + return $db->fetchObject( + $db->queryF( + 'SELECT obj_fi, owner FROM qpl_questions WHERE id = %s', + [\ilDBConstants::T_INTEGER], + [$old_question_id] + ) + ); + } + + private function fetchTargetRefIdFromDB( + \ilDBInterface $db, + Types $type, + int $sub_object_id + ): ?int { + if ($type === Types::LearningModulePage + || $type === Types::LearningModuleChapter) { + return $db->fetchObject( + $db->queryF( + 'SELECT object_reference.ref_id FROM lm_data' . PHP_EOL + . 'INNER JOIN object_reference' . PHP_EOL + . 'ON lm_data.lm_id = object_reference.obj_id' . PHP_EOL + . 'WHERE lm_data.obj_id = %s', + [\ilDBConstants::T_INTEGER], + [$sub_object_id] + ) + )?->lm_id; + } + + return $db->fetchObject( + $db->queryF( + 'SELECT object_reference.ref_id FROM glossary_term' . PHP_EOL + . 'INNER JOIN object_reference' . PHP_EOL + . 'ON glossary_term.glo_id = object_reference.obj_id' . PHP_EOL + . 'WHERE glossary_term.id = %s', + [\ilDBConstants::T_INTEGER], + [$sub_object_id] + ) + )?->glo_id; + } + + private function determineNewOwner( + int $owner + ): int { + if ($owner > 0) { + return $owner; + } + + return $this->irss_migration_helper + ->getStakeholder() + ->getOwnerOfNewResources(); + } + + private function buildFilePath( + int $parent_id, + SuggestedSolutionFile $old_values + ): string { + return \ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) + . "/assessment/{$parent_id}/{$old_values->getQuestionId()}" + . "/solution/{$old_values->getFilename()}"; + } + + private function parseInternalLink( + string $link + ): ?int { + if (preg_match("/il__\w+_(\d+)/", $link, $matches) !== 1) { + return null; + } + + return (int) $matches[1]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php new file mode 100644 index 000000000000..886d82d55233 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php @@ -0,0 +1,277 @@ + + */ + private readonly array $requested_types; + + public function __construct( + private readonly \ilRbacSystem $rbac_system, + private readonly \ilTree $tree, + private readonly Environment $environment, + private readonly string $requested_type + ) { + [$this->url_builder, $this->node_parameter_token] = $environment + ->withStepParameter(self::STEP_RETRIEVE_NODES) + ->getUrlBuilder() + ->acquireParameter( + self::PARAMETER_NAMESPACE, + self::PARAMETER_ID_STRING_NODE + ); + + $this->requested_types = array_merge( + self::CONTAINER_CONTENT_TYPES, + [$requested_type] + ); + } + + #[\Override] + public function getNodes( + NodeFactory $node_factory, + IconFactory $icon_factory, + array $sync_node_id_whitelist = [], + ?string $parent_id = null + ): \Generator { + if ($parent_id === null) { + $parent_id = ROOT_FOLDER_ID; + } + + yield from $this->buildFilteredNodes( + $node_factory, + $icon_factory, + $this->tree->getChildsByTypeFilter( + $parent_id, + $this->requested_types + ) + ); + } + + #[\Override] + public function getNodesAsLeaf( + NodeFactory $node_factory, + IconFactory $icon_factory, + array $node_ids, + ): \Generator { + foreach ($node_ids as $node_id) { + $node = $this->tree->getNodeData((int) $node_id); + yield $node_factory->leaf( + explode('.', $node['path'] ?? ''), + $node['title'] ?? $this->environment->getLanguage()->txt('invalid') + ); + } + } + + #[\Override] + public function can( + string $step + ): bool { + return $step === self::STEP_RETRIEVE_NODES + && $this->environment->getHttpServices()->wrapper()->query()->has( + $this->node_parameter_token->getName() + ); + } + + #[\Override] + public function do( + string $action + ): Async { + $node_id = $this->retrieveNodeIdFromQuery(); + + $response = ''; + if ($node_id !== null) { + $response = iterator_to_array( + $this->buildFilteredNodes( + $this->environment->getUIFactory()->input()->field()->node(), + $this->environment->getUIFactory()->symbol()->icon(), + $this->tree->getChildsByTypeFilter( + $node_id, + $this->requested_types + ) + ) + ); + } + + return $this->environment->getPresentationFactory()->getAsync($response); + } + + public function buildValidNodeConstraint(): Constraint + { + return $this->environment->getRefinery()->custom()->constraint( + function (array $v): bool { + if (!isset($v[0])) { + return false; + } + $data = $this->tree->getNodeData((int) $v[0]); + return $data['type'] === $this->requested_type; + }, + function (\Closure $txt, array $v): string { + if (!isset($v[0])) { + return $txt('required'); + } + $data = $this->tree->getNodeData((int) $v[0]); + return sprintf( + $txt('invalid_node_selected'), + $txt("obj_{$data['type']}") + ); + } + ); + } + + private function buildFilteredNodes( + NodeFactory $node_factory, + IconFactory $icon_factory, + array $nodes + ): \Generator { + foreach ($nodes as $node) { + $is_visible = $this->rbac_system->checkAccess('visible', $node['ref_id']); + $is_container = in_array($node['type'], self::CONTAINER_CONTENT_TYPES); + if (!$is_container && !$is_visible) { + continue; + } + + if (!$is_container) { + yield $node_factory->leaf( + explode('.', $node['path']), + $node['title'], + $this->getIconForNodeArray( + $icon_factory, + $node + ) + ); + continue; + } + + $is_readable = $this->rbac_system->checkAccess('read', $node['ref_id']); + if ($is_visible && $is_readable && $node['parent'] === ROOT_FOLDER_ID) { + yield $node_factory->branch( + explode('.', $node['path']), + $node['title'], + $this->getIconForNodeArray( + $icon_factory, + $node + ), + ...$this->buildFilteredNodes( + $node_factory, + $icon_factory, + $this->tree->getChildsByTypeFilter( + $node['ref_id'], + $this->requested_types + ) + ) + ); + continue; + } + + if ($is_visible && $is_readable) { + yield $node_factory->async( + $this->getAsyncNodeRenderUrl($node['ref_id']), + explode('.', $node['path']), + $node['title'], + $this->getIconForNodeArray( + $icon_factory, + $node + ) + ); + continue; + } + + if ($is_readable) { + yield from $this->buildFilteredNodes( + $node_factory, + $icon_factory, + $this->tree->getChildsByTypeFilter( + $node['ref_id'], + $this->requested_types + ) + ); + } + } + } + + private function getAsyncNodeRenderUrl( + int $node_id + ): URI { + return $this->url_builder->withParameter( + $this->node_parameter_token, + (string) $node_id + )->buildURI(); + } + + private function retrieveNodeIdFromQuery(): ?int + { + $refinery = $this->environment->getRefinery(); + + return $this->environment + ->getHttpServices() + ->wrapper() + ->query() + ->retrieve( + $this->node_parameter_token->getName(), + $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always(null) + ]) + ); + } + + private function getIconForNodeArray( + IconFactory $icon_factory, + array $node + ): StandardIcon { + return $icon_factory->standard( + $node['type'], + $this->environment->getLanguage()->txt( + "obj_{$node['type']}" + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php new file mode 100644 index 000000000000..8e4ad4206182 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php @@ -0,0 +1,375 @@ +render([ + $this->buildPanel() + ]); + } + + public function doAction( + string $action + ): Async { + return match($action) { + self::STEP_SELECT_TYPE => $this->buildPromptShowAsync( + $this->buildSelectTypeForm() + ), + self::STEP_SELECT_CONTENT => $this->processSelectTypeForm(), + self::STEP_SAVE_CONTENT => $this->processSelectContentForm(), + self::STEP_SAVE_SUB_CONTENT => $this->processSelectSubContentForm(), + default => $this->forwardActionToActors($action) + }; + } + + private function buildPrompt(): Prompt + { + return $this->environment->getUIFactory()->prompt()->standard( + $this->environment->withStepParameter( + self::STEP_SELECT_TYPE + )->getUrlBuilder()->buildURI() + ); + } + + private function buildPanel(): StandardPanel + { + $content = [ + $this->environment->getUIFactory()->listing()->descriptive( + $this->repository->getFor( + $this->environment->getAnswerFormId() + )->getListing( + $this->ctrl, + $this->static_url, + $this->environment + ) + ) + ]; + + if ($this->environment->getEditability() === Editability::Full) { + $prompt = $this->buildPrompt(); + $content[] = $prompt; + $content[] = $this->environment->getUIFactory()->button()->standard( + $this->environment->getLanguage()->txt('edit'), + $prompt->getShowSignal() + ); + } + + return $this->environment->getUIFactory()->panel()->standard( + $this->environment->getLanguage()->txt('suggested_learning_content'), + $content + ); + } + + private function forwardActionToActors( + string $action + ): Async { + $node_retrieval = new NodeRetrieval( + $this->rbac_system, + $this->tree, + $this->environment, + $this->retrieveReferencedObjectTypeFromCarry() + ); + + if ($node_retrieval->can($action)) { + return $node_retrieval->do($action); + } + + $upload_handler = $this->environment->getPresentationFactory()->getUploadHandler( + $this->environment, + new Stakeholder( + $this->current_user->getId() + ) + ); + + if ($upload_handler->can($action)) { + return $upload_handler->do($action); + } + + throw new \InvalidArgumentException( + "No actor found that can '{$action}'" + ); + } + + private function buildSelectTypeForm(): StandardForm + { + $uf = $this->environment->getUIFactory(); + + return $uf->input()->container()->form()->standard( + $this->environment->withStepParameter( + self::STEP_SELECT_CONTENT + )->getUrlBuilder()->buildURI()->__toString(), + [ + 'type' => $this->buildTypeSelect() + ] + )->withSubmitLabel( + $this->environment->getLanguage()->txt('next') + ); + } + + private function processSelectTypeForm(): Async + { + $form = $this->buildSelectTypeForm()->withRequest( + $this->environment->getHttpServices()->request() + ); + $data = $form->getData(); + if ($data === null) { + return $this->buildPromptShowAsync($form); + ; + } + + if ($data['type'] === Types::None) { + $this->repository->delete( + $this->environment->getAnswerFormId() + ); + return $this->buildRedirectToOverviewAsync(); + } + + $inputs_builder = $this->buildInputsBuilderSelectContentForm() + ->withCarry($data['type']->value); + $inputs_builder->persistCarry(); + + return $this->buildPromptShowAsync( + $this->buildSelectContentForm($inputs_builder) + ); + } + + private function buildSelectContentForm( + InputsBuilderSession $inputs_builder + ): StandardForm { + $uf = $this->environment->getUIFactory(); + + $form = $uf->input()->container()->form()->standard( + $this->environment->withStepParameter( + self::STEP_SAVE_CONTENT + )->getUrlBuilder()->buildURI()->__toString(), + [ + 'content' => $inputs_builder->getInputs() + ] + ); + + $type = $inputs_builder->retrieveCarry( + $this->environment->getRefinery()->custom()->transformation( + fn(string $v): Types => Types::tryFrom($v) + ) + ); + if ($type->hasSelectContentSubForm()) { + return $form->withSubmitLabel( + $this->environment->getLanguage()->txt('next') + ); + } + return $form; + } + + private function processSelectContentForm(): Async + { + $select_content_inputs_builder = $this->buildInputsBuilderSelectContentForm(); + $form = $this->buildSelectContentForm( + $select_content_inputs_builder + )->withRequest( + $this->environment->getHttpServices()->request() + ); + $data = $form->getData(); + if ($data === null) { + $select_content_inputs_builder->persistCarry(); + return $this->buildPromptShowAsync($form); + } + + if (!$data['content']->getType()->hasSelectContentSubForm()) { + $this->repository->store($data['content']); + return $this->buildRedirectToOverviewAsync(); + } + + $select_sub_content_inputs_builder = $this + ->buildInputsBuilderSelectSubContentForm() + ->withCarry( + json_encode([ + 'type' => $data['content']->getType()->value, + 'target_ref_id' => $data['content']->getTargetRefId() + ]) + ); + $select_sub_content_inputs_builder->persistCarry(); + + return $this->buildPromptShowAsync( + $this->buildSelectSubContentForm($select_sub_content_inputs_builder) + ); + } + + private function buildSelectSubContentForm( + InputsBuilderSession $inputs_builder + ): StandardForm { + $uf = $this->environment->getUIFactory(); + + $form = $uf->input()->container()->form()->standard( + $this->environment->withStepParameter( + self::STEP_SAVE_SUB_CONTENT + )->getUrlBuilder()->buildURI()->__toString(), + [ + 'content' => $inputs_builder->getInputs() + ] + ); + return $form; + } + + private function processSelectSubContentForm(): Async + { + $inputs_builder = $this->buildInputsBuilderSelectSubContentForm(); + $form = $this->buildSelectSubContentForm( + $inputs_builder + )->withRequest( + $this->environment->getHttpServices()->request() + ); + $data = $form->getData(); + if ($data === null) { + $inputs_builder->persistCarry(); + return $this->buildPromptShowAsync($form); + } + + $this->repository->store($data['content']); + return $this->buildRedirectToOverviewAsync(); + } + + private function buildTypeSelect(): Select + { + return $this->environment->getUIFactory()->input()->field()->select( + $this->environment->getLanguage()->txt('type'), + array_reduce( + Types::cases(), + function (array $c, Types $v): array { + $c[$v->value] = $v->getTranslatedOptionName($this->environment->getLanguage()); + return $c; + }, + [] + ) + )->withAdditionalTransformation( + $this->environment->getRefinery()->custom()->transformation( + fn(string $v): Types => Types::tryFrom($v) ?? Types::None + ) + )->withRequired(true); + } + + private function buildInputsBuilderSelectContentForm(): InputsBuilderSession + { + return $this->environment->getPresentationFactory() + ->getSessionBasedInputsBuilder( + $this->environment->getRefinery()->custom()->transformation( + fn(string $v): Section + => Types::tryFrom($v)->buildContentInput( + $this->repository, + $this->rbac_system, + $this->tree, + $this->environment, + $this->current_user->getId() + ) + ) + ); + } + + private function buildInputsBuilderSelectSubContentForm(): InputsBuilderSession + { + return $this->environment->getPresentationFactory() + ->getSessionBasedInputsBuilder( + $this->environment->getRefinery()->custom()->transformation( + function (string $v): Section { + $v_array = json_decode($v, true); + return Types::tryFrom($v_array['type'])->buildSubContentInput( + $this->repository, + $this->rbac_system, + $this->environment, + $v_array['target_ref_id'] + ); + } + ) + ); + } + + private function buildPromptShowAsync( + StandardForm $form + ): Async { + $lng = $this->environment->getLanguage(); + + return $this->environment->getPresentationFactory()->getAsync( + $this->environment->getUIFactory()->prompt()->state()->show( + $form + )->withTitle( + $lng->txt('edit_suggested_solution') + ) + ); + } + + private function buildRedirectToOverviewAsync(): Async + { + return $this->environment->getPresentationFactory()->getAsync( + $this->environment->getUIFactory()->prompt()->state()->redirect( + $this->environment + ->withDefaultStep() + ->getUrlBuilder() + ->buildURI() + ) + ); + } + + private function retrieveReferencedObjectTypeFromCarry(): string + { + $inputs_builder = $this->buildInputsBuilderSelectContentForm(); + $inputs_builder->persistCarry(); + + return $inputs_builder->retrieveCarry( + $this->environment->getRefinery()->custom()->transformation( + fn(string $v): string => Types::tryFrom($v) + ->getReferencedObjectType() + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php new file mode 100644 index 000000000000..83892b031141 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php @@ -0,0 +1,173 @@ +table_names_builder = new TableNameBuilder( + QuestionRepository::COMPONENT_NAMESPACE, + null + ); + } + + public function getNew( + Uuid $answer_form_id, + Types $type + ): Content { + return new Content( + $this->irss, + $answer_form_id, + $type, + '' + ); + } + + public function getFor( + Uuid $answer_form_id, + ): Content { + $database_values = $this->buildQuery( + $answer_form_id + )->loadNextRecord()->current(); + + if ($database_values === null) { + return $this->getNew( + $answer_form_id, + Types::None + ); + } + + return $database_values->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ), + $this->refinery->custom()->transformation( + fn(array $vs): Content => new Content( + $this->irss, + $answer_form_id, + Types::tryFrom($vs[0]['type']), + $vs[0]['content'] + ) + ) + ); + } + + public function store( + Content $content + ): void { + (new Manipulate( + $this->db, + ManipulationType::Replace, + QuestionRepository::COMPONENT_NAMESPACE + ))->withAdditionalStatement( + $this->persistence_factory->replace( + $this->table_definitions->getColumns( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ), + $content->toStorage( + $this->persistence_factory + ) + ) + )->run(); + } + + public function delete( + Uuid $answer_form_id + ): void { + (new Manipulate( + $this->db, + ManipulationType::Delete, + QuestionRepository::COMPONENT_NAMESPACE + ))->withAdditionalStatement( + $this->persistence_factory->delete( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ), + [ + $this->persistence_factory->where( + $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $answer_form_id->toString() + ) + ) + ] + ) + )->run(); + } + + private function buildQuery( + Uuid $answer_form_id + ): Query { + return $this->table_definitions->completeQuery( + new Query( + $this->db, + $this->refinery, + QuestionRepository::COMPONENT_NAMESPACE, + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ) + ), + $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ) + )->withAdditionalWhere( + $this->persistence_factory->where( + $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::SuggestedLearningContent + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $answer_form_id->toString() + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Stakeholder.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Stakeholder.php new file mode 100644 index 000000000000..2c5c79794ba8 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Stakeholder.php @@ -0,0 +1,39 @@ + + */ +class Stakeholder extends AbstractResourceStakeholder +{ + public function getId(): string + { + return 'questions_suggested_learning_content'; + } + + public function getOwnerOfNewResources(): int + { + return $this->default_owner; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php new file mode 100644 index 000000000000..5987bd834bfc --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php @@ -0,0 +1,93 @@ +getStep(); + $overview = $this->buildOverview($environment); + if ($step === '') { + return $overview; + } + + return $overview->doAction( + $step + ); + } + + #[\Override] + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): void { + } + + private function buildOverview( + Environment $environment + ): Overview { + return new Overview( + $this->ctrl, + $this->rbac_system, + $this->tree, + $this->current_user, + $this->static_url, + $environment, + $this->repository + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php new file mode 100644 index 000000000000..fd23e5406b6d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php @@ -0,0 +1,131 @@ +persistence_factory->table( + $table_name_builder, + $table_type + ); + return array_map( + fn(string $v): Column => $this->persistence_factory->column( + $table, + $v + ), + array_values( + array_filter( + self::SUGGESTED_LEARNING_CONTENT_TABLE_COLUMNS, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + #[\Override] + public function getIdColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return $this->persistence_factory->column( + $table, + self::SUGGESTED_LEARNING_CONTENT_TABLE_ID_COLUMN + ); + } + + #[\Override] + public function getForeignKeyColumn( + TableNameBuilder $table_name_builder, + TableTypesInterface $table_type, + string $sub_table_identifier = '' + ): Column { + $table = $this->persistence_factory->table( + $table_name_builder, + $table_type + ); + + return $this->persistence_factory->column( + $table, + self::SUGGESTED_LEARNING_CONTENT_TABLE_FOREIGN_KEY_COLUMN + ); + } + + #[\Override] + public function completeQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_name_builder = $query->getTableNameBuilder(null); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_name_builder, + TableTypes::SuggestedLearningContent + ) + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $base_table_id_column + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableTypes.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableTypes.php new file mode 100644 index 000000000000..dd42118ae452 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableTypes.php @@ -0,0 +1,28 @@ + $lng->txt('glossary_term'), + default => $lng->txt($this->value) + }; + } + + public function present( + \ilCtrl $ctrl, + StaticURLServices $static_url, + IRSS $irss, + Environment $environment, + string $file_title, + ?ResourceIdentification $rid, + ?int $target_ref_id, + ?int $sub_object_id + ): ?StandardLink { + $lf = $environment->getUIFactory()->link(); + $lng = $environment->getLanguage(); + return match($this) { + self::File => $lf->standard( + $file_title === '' + ? $irss->manage()->getResource($rid)->getCurrentRevision()->getTitle() + : $file_title, + $irss->consume()->src($rid)->getSrc(true) + ), + self::LearningModule => $this->buildLinkToLearningModule( + $ctrl, + $lng, + $lf, + $target_ref_id + ), + self::LearningModulePage, + self::LearningModuleChapter, + self::GlossaryTerm => $lf->standard( + $lng->txt('show'), + $static_url->builder()->build( + $this->value, + null, + [$sub_object_id] + )->__toString() + ), + self::None => null + }; + } + + public function getReferencedObjectType(): string + { + return match ($this) { + self::LearningModule, + self::LearningModuleChapter, + self::LearningModulePage => 'lm', + self::GlossaryTerm => 'glo', + default => '' + }; + } + + public function hasSelectContentSubForm(): bool + { + return $this !== self::LearningModule && $this !== self::File; + } + + public function buildContentInput( + Repository $repository, + \ilRbacSystem $rbac_system, + \ilTree $tree, + Environment $environment, + int $current_user_id + ): Section { + return match($this) { + self::File => $this->buildUploadFileInput( + $repository, + $environment, + $current_user_id + ), + default => $this->buildSelectObjectInput( + $repository, + $rbac_system, + $tree, + $environment, + $this->getReferencedObjectType() + ) + }; + } + + public function buildSubContentInput( + Repository $repository, + \ilRbacSystem $rbac_system, + Environment $environment, + int $target_ref_id + ): Section { + if (!$this->hasSelectContentSubForm()) { + throw new InvalidArgumentException( + 'This type of SuggestedLearningContent does not provide a SubContentInput.' + ); + } + + $ff = $environment->getUIFactory()->input()->field(); + $lng = $environment->getLanguage(); + + return $ff->section( + [ + 'sub_object' => $ff->select( + $this->getTranslatedOptionName($lng), + $this->buildSubContentOptions( + $rbac_system, + $target_ref_id + ) + )->withRequired(true) + ], + $lng->txt('select_target') + )->withAdditionalTransformation( + $environment->getRefinery()->custom()->transformation( + fn(array $vs): Content => $repository->getNew( + $environment->getAnswerFormId(), + $this + )->withTargetRefId($target_ref_id) + ->withSubObjectId((int) $vs['sub_object']) + ) + ); + } + + private function buildLinkToLearningModule( + \ilCtrl $ctrl, + Language $lng, + LinkFactory $link_factory, + int $target_ref_id + ): StandardLink { + $ctrl->setParameterByClass( + \ilLMPresentationGUI::class, + 'ref_id', + $target_ref_id + ); + $link = $link_factory->standard( + $lng->txt('show'), + $ctrl->getLinkTargetByClass( + [ + \ilLMPresentationGUI::class + ], + ) + ); + $ctrl->clearParameterByClass( + \ilLMPresentationGUI::class, + 'ref_id' + ); + return $link; + } + + private function buildUploadFileInput( + Repository $repository, + Environment $environment, + int $current_user_id + ): Section { + $ff = $environment->getUIFactory()->input()->field(); + $lng = $environment->getLanguage(); + + return $ff->section( + [ + Content::KEY_FILE_TITLE => $ff->text( + $lng->txt('title'), + $lng->txt('if_no_title_then_filename') + ), + Content::KEY_RID => $ff->file( + $environment->getPresentationFactory()->getUploadHandler( + $environment, + new Stakeholder($current_user_id) + ), + $lng->txt('file') + )->withRequired(true) + ], + $lng->txt('upload_file') + )->withAdditionalTransformation( + $environment->getRefinery()->custom()->transformation( + fn(array $vs): Content => $repository->getNew( + $environment->getAnswerFormId(), + $this + )->withFileInfo($vs) + ) + ); + } + + private function buildSelectObjectInput( + Repository $repository, + \ilRbacSystem $rbac_system, + \ilTree $tree, + Environment $environment, + string $type + ): Section { + $ff = $environment->getUIFactory()->input()->field(); + $lng = $environment->getLanguage(); + + $node_retrieval = new NodeRetrieval( + $rbac_system, + $tree, + $environment, + $type + ); + + return $ff->section( + [ + 'object' => $ff->treeSelect( + $node_retrieval, + $lng->txt('select') + )->withAdditionalTransformation( + $node_retrieval->buildValidNodeConstraint() + ) + ], + $environment->getLanguage()->txt('select_object') + )->withAdditionalTransformation( + $environment->getRefinery()->custom()->transformation( + fn(array $vs): Content => $repository->getNew( + $environment->getAnswerFormId(), + $this + )->withTargetRefId((int) $vs['object'][0]) + ) + ); + } + + private function buildSubContentOptions( + \ilRbacSystem $rbac_system, + int $target_ref_id + ): array { + if (!$rbac_system->checkAccess('read', $target_ref_id)) { + return []; + } + + return match ($this) { + self::GlossaryTerm => array_reduce( + (new \ilObjGlossary($target_ref_id))->getTermList(), + function (array $c, array $v): array { + $c[$v['id']] = $v['term']; + return $c; + }, + [] + ), + self::LearningModulePage, + self::LearningModuleChapter => array_reduce( + \ilLMObject::getObjectList( + \ilObject::_lookupObjectId($target_ref_id), + $this === self::LearningModuleChapter + ? 'st' + : 'pg' + ), + function (array $c, array $v): array { + $c[$v['obj_id']] = $v['title']; + return $c; + }, + [] + ) + }; + + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php index fef6c7f7f264..6a5dcba24eab 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php @@ -27,7 +27,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Data\Text\Factory as TextFactory; use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -50,8 +50,6 @@ class FeedbackOverviewTable implements OverviewTable private const string STEP_DELETE_FEEDBACK = 'df'; private const string STEP_SAVE_FEEDBACK = 'sf'; - private const string KEY_GAP_ID = 'cap_id'; - public function __construct( private readonly UuidFactory $uuid_factory, private readonly TextFactory $text_factory, @@ -322,7 +320,6 @@ private function buildEnterFeedbackInputBuilder( string $feedback_text = '' ): InputsBuilderSession { return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( - self::KEY_GAP_ID, $environment->getRefinery()->custom()->transformation( function ( ?string $v diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index 41b67f0459aa..df307a5c7ddf 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -25,8 +25,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\InputsBuilder; -use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilder; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Data\Range; use ILIAS\Data\Order; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index f08d8fa6a542..81c56857e1e9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -29,7 +29,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; -use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; @@ -264,7 +264,6 @@ private function buildInputsBuilderForBasicInputs( ): InputsBuilderSession { $inputs_builder = $environment->getPresentationFactory() ->getSessionBasedInputsBuilder( - $environment->getAnswerFormId()->toString(), $environment->getRefinery()->custom()->transformation( fn(?string $carry): Section => $this->properties_factory ->fromCarry( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php index d83f18bc458b..5a7eb2004313 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -25,7 +25,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\InputsBuilderSession; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Refinery\Custom\Transformation as CustomTransformation; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\URLBuilder; @@ -211,7 +211,6 @@ private function buildInputsBuilderForTypesForm( Environment $environment ): InputsBuilderSession { return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $environment->getAnswerFormProperties()->getAnswerFormId()->toString(), $environment->getRefinery()->custom()->transformation( function (?string $carry) use ($environment): Section { $properties_from_carry = $this->properties_factory @@ -337,7 +336,6 @@ private function buildInputsBuilderForAnswerOptionsForm( Properties $properties ): InputsBuilderSession { return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $properties->getAnswerFormId()->toString(), $environment->getRefinery()->custom()->transformation( function (?string $carry) use ( $environment, @@ -449,7 +447,6 @@ private function buildInputsBuilderForPointsForm( Properties $properties ): InputsBuilderSession { return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $properties->getAnswerFormId()->toString(), $environment->getRefinery()->custom()->transformation( function (?string $carry) use ( $environment, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php similarity index 100% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 7cd20865a70a..eec66941b48e 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -32,6 +32,8 @@ class Query private array $order = []; private ?Range $range = null; + private ?Column $group_by = null; + private array $binding_types = []; private array $binding_values = []; @@ -44,8 +46,8 @@ class Query public function __construct( private readonly \ilDBInterface $db, private readonly Refinery $refinery, - private string $component_name_space, - private Table $base_table + private readonly string $component_name_space, + private readonly Table $base_table ) { } @@ -103,9 +105,16 @@ public function withRange( return $clone; } - public function loadNextRecord( - ?Column $group_by - ): \Generator { + public function withGroupBy( + Column $group_by + ): self { + $clone = clone $this; + $clone->group_by = $group_by; + return $clone; + } + + public function loadNextRecord(): \Generator + { $result = $this->toSql(); $this->current_record = [$this->db->fetchAssoc($result)]; @@ -113,14 +122,15 @@ public function loadNextRecord( return null; } - if ($group_by === null) { + if ($this->group_by === null) { + yield $this; yield from $this->loadNextRecordUngrouped($result); return; } yield from $this->loadNextRecordGrouped( $result, - $group_by->getColumnAlias() + $this->group_by->getColumnAlias() ); } @@ -223,7 +233,7 @@ private function loadNextRecordUngrouped( \ilDBStatement $result ): \Generator { while (($db_record = $this->db->fetchAssoc($result)) !== null) { - $this->current_record = $db_record; + $this->current_record[0] = $db_record; yield $this; } } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php new file mode 100644 index 000000000000..7b3ad84e661a --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php @@ -0,0 +1,34 @@ +content) + ? $this->content + : $ui_renderer->renderAsync($this->content); + $this->http->saveResponse( $this->http->response()->withBody( Streams::ofString( - $ui_renderer->renderAsync($this->content) + $rendered_content ) ) ); diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 469b158eaaf0..3a5b213a1c60 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Presentation\Layout; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilder; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\MessageBox\MessageBox; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index 77dcb48782fa..61c1ad2e370b 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -21,10 +21,18 @@ namespace ILIAS\Questions\Presentation\Layout; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilder; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; +use ILIAS\Questions\Presentation\Layout\Tools\UploadHandler; +use ILIAS\Filesystem\Filesystem; +use ILIAS\FileUpload\FileUpload; use ILIAS\HTTP\Services as HttpService; use ILIAS\Language\Language; use ILIAS\Refinery\Transformation; +use ILIAS\ResourceStorage\Services as IRSS; +use ILIAS\ResourceStorage\Stakeholder\ResourceStakeholder; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Prompt\State\State; use ILIAS\UI\Component\Input\Input; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; @@ -36,7 +44,11 @@ class Factory public function __construct( private readonly UIFactory $ui_factory, private readonly HttpService $http, - private readonly Language $lng + private readonly Language $lng, + private readonly IRSS $irss, + private readonly Filesystem $temp_filesystem, + private readonly FileUpload $upload, + private readonly \ilFileServicesFilenameSanitizer $sanitizer, ) { } @@ -67,7 +79,7 @@ public function getEditForm( } public function getAsync( - InterruptiveModal|RoundTripModal|MessageBox $content + InterruptiveModal|RoundTripModal|MessageBox|State|array|string $content ): Async { return new Async( $this->http, @@ -76,15 +88,27 @@ public function getAsync( } public function getSessionBasedInputsBuilder( - string $storage_key, Transformation $to_inputs ): InputsBuilderSession { return new InputsBuilderSession( - $storage_key, $to_inputs ); } + public function getUploadHandler( + Environment $environment, + ResourceStakeholder $stakeholder + ): UploadHandler { + return new UploadHandler( + $this->irss, + $this->temp_filesystem, + $this->upload, + $this->sanitizer, + $stakeholder, + $environment + ); + } + public function getUIFactory(): UIFactory { return $this->ui_factory; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php b/components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilder.php similarity index 92% rename from components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php rename to components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilder.php index 10bcd3ec1389..65adcb355ab4 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilder.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout; +namespace ILIAS\Questions\Presentation\Layout\Tools; use ILIAS\UI\Component\Input\Field\Section; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php b/components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilderSession.php similarity index 86% rename from components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php rename to components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilderSession.php index ee466a5b4c3f..818d63a28e81 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilderSession.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Tools/InputsBuilderSession.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Layout; +namespace ILIAS\Questions\Presentation\Layout\Tools; use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Section; @@ -29,6 +29,8 @@ */ class InputsBuilderSession implements InputsBuilder { + private const string STORAGE_KEY = 'inputs_builder_carry'; + private ?string $carry = null; /** @@ -37,7 +39,6 @@ class InputsBuilderSession implements InputsBuilder * the value will be null. */ public function __construct( - private readonly string $storage_key, private readonly Transformation $to_inputs ) { } @@ -55,12 +56,12 @@ public function persistCarry(): void if ($this->carry === null) { $this->loadCarryFromSessionAndClear(); } - \ilSession::set($this->storage_key, $this->carry); + \ilSession::set(self::STORAGE_KEY, $this->carry); } public function resetCarry(): void { - \ilSession::clear($this->storage_key); + \ilSession::clear(self::STORAGE_KEY); } public function retrieveCarry( @@ -83,7 +84,7 @@ public function getInputs(): Section private function loadCarryFromSessionAndClear(): void { - $this->carry = \ilSession::get($this->storage_key); - \ilSession::clear($this->storage_key); + $this->carry = \ilSession::get(self::STORAGE_KEY); + \ilSession::clear(self::STORAGE_KEY); } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php b/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php new file mode 100644 index 000000000000..e3ee24e168c6 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php @@ -0,0 +1,364 @@ +environment + ->withStepParameter(self::STEP_UPLOAD) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getFileRemovalURL(): string + { + return $this->environment + ->withStepParameter(self::STEP_REMOVE) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getExistingFileInfoURL(): string + { + return $this->environment + ->withStepParameter(self::STEP_INFO) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getInfoForExistingFiles( + array $file_ids + ): array { + return array_map( + fn($file_id): FileInfoResult => $this->getInfoResult($file_id), + $file_ids + ); + } + + #[\Override] + public function getInfoResult( + string $identifier + ): ?FileInfoResult { + $title = $mime = 'unknown'; + $size = 0; + $id = $this->irss->manage()->find($identifier); + if ($id !== null) { + $revision = $this->irss->manage()->getCurrentRevision($id)->getInformation(); + $title = $revision->getTitle(); + $size = $revision->getSize(); + $mime = $revision->getMimeType(); + } + + return new BasicFileInfoResult( + $this->getFileIdentifierParameterName(), + $identifier, + $title, + $size, + $mime + ); + } + + #[\Override] + public function supportsChunkedUploads(): bool + { + return true; + } + + #[\Override] + public function can( + string $step + ): bool { + $has_file_identifier = $this->hasFileIdentifier(); + + return $step === self::STEP_UPLOAD + || $step === self::STEP_REMOVE && $has_file_identifier + || $step === self::STEP_INFO && $has_file_identifier; + } + + #[\Override] + public function do( + string $action + ): Async { + $response = match($action) { + self::STEP_UPLOAD => $this->upload(), + self::STEP_REMOVE => $this->remove(), + self::STEP_INFO => $this->info(), + default => '' + }; + + return $this->environment->getPresentationFactory()->getAsync($response); + } + + private function upload(): string + { + try { + $this->readChunkedInformation(); + return json_encode($this->getUploadResult()); + } catch (\Throwable $t) { + return json_encode( + new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + BasicHandlerResult::STATUS_FAILED, + '', + $t->getMessage() + ) + ); + } + } + + private function remove(): string + { + return json_encode( + $this->getRemoveResult( + $this->retrieveFileIdentifier() + ) + ); + } + + private function info(): string + { + return json_encode( + $this->getInfoResult( + $this->retrieveFileIdentifier() + ) + ); + } + + private function readChunkedInformation(): void + { + $body = $this->environment->getHttpServices()->request()->getParsedBody(); + $this->chunk_id = $body['dzuuid'] ?? null; + $this->amount_of_chunks = (int) ($body['dztotalchunkcount'] ?? 0); + $this->chunk_index = (int) ($body['dzchunkindex'] ?? 0); + $this->chunk_total_size = (int) ($body['dztotalfilesize'] ?? 0); + $this->is_chunked = ($this->chunk_id !== null && $this->amount_of_chunks > 0); + } + + private function getUploadResult(): HandlerResult + { + $this->upload->process(); + + $result_array = $this->upload->getResults(); + $result = end($result_array); + + $identifier = ''; + $status = HandlerResult::STATUS_FAILED; + $message = $this->environment->getLanguage()->txt( + 'msg_info_blacklisted' + ); + if ($result instanceof UploadResult && $result->isOK()) { + if ($this->is_chunked) { + return $this->processChunckedUpload($result); + } + + $identifier = $this->irss->manage()->upload( + $result, + $this->stakeholder + )->serialize(); + $status = HandlerResult::STATUS_OK; + $message = "file upload OK"; + } + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + $status, + $identifier, + $message + ); + } + + private function getRemoveResult( + string $identifier + ): HandlerResult { + $status = HandlerResult::STATUS_OK; + $message = "file with identifier '{$identifier}' doesn't exist, nothing to do."; + + $id = $this->irss->manage()->find($identifier); + if ($id !== null) { + $this->irss->manage()->remove( + $id, + $this->stakeholder + ); + $status = HandlerResult::STATUS_OK; + $message = "file removal OK"; + } + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + $status, + $identifier, + $message + ); + } + + private function processChunckedUpload( + UploadResult $result + ): HandlerResult { + $temp_path = $this->sanitizer->sanitize( + "{$this->chunk_id}/{$result->getName()}" + ); + + try { + $this->writeChunkedTempFile( + $result, + $temp_path + ); + } catch (\Throwable $t) { + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_FAILED, + '', + $t->getMessage() + ); + } + + if (($this->chunk_index + 1) === $this->amount_of_chunks) { + return $this->storeChunkedUpload( + $result, + $temp_path + ); + } + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_PARTIAL, + '', + 'chunk upload OK' + ); + } + + private function writeChunkedTempFile( + UploadResult $result, + string $temp_path + ): void { + if ($this->temp_filesystem->has($temp_path)) { + fwrite( + fopen( + $this->temp_filesystem->readStream($temp_path) + ->getMetadata()['uri'], + 'ab' + ), + file_get_contents( + $result->getPath() + ) + ); + return; + } + + $this->temp_filesystem->write( + $temp_path, + file_get_contents( + $result->getPath() + ) + ); + } + + private function storeChunkedUpload( + UploadResult $result, + string $temp_path + ): HandlerResult { + $id = $this->irss->manage()->stream( + $this->temp_filesystem->readStream($temp_path), + $this->stakeholder, + $result->getName() + ); + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $id->serialize(), + 'file upload OK' + ); + } + + private function hasFileIdentifier(): bool + { + return $this->environment + ->getHttpServices() + ->wrapper() + ->query() + ->has($this->getFileIdentifierParameterName()); + } + + private function retrieveFileIdentifier(): string + { + if (!$this->hasFileIdentifier()) { + return ''; + } + + return $this->environment + ->getHttpServices() + ->wrapper() + ->query() + ->retrieve( + $this->getFileIdentifierParameterName(), + $this->environment->getRefinery()->kindlyTo()->string() + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index d58e8ffec38c..8987ec8b19ba 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -174,10 +174,10 @@ public function forwardPageCmds( $this )->withReturnURI( $environment - ->withActionParameter(self::ACTION_EDIT_QUESTION) - ->withQuestionIdParameter($environment->getQuestionId()) - ->getUrlBuilder() - ->buildURI() + ->withActionParameter(self::ACTION_EDIT_QUESTION) + ->withQuestionIdParameter($environment->getQuestionId()) + ->getUrlBuilder() + ->buildURI() ) ) ); @@ -262,13 +262,15 @@ public function editAnswerForm( $action = $environment->getAction(); - $capability_action = array_filter( - $capability_actions, - fn(Action $v): bool => $v->isThis($action) + $capability_action = current( + array_filter( + $capability_actions, + fn(Action $v): bool => $v->isThis($action) + ) ); - if ($capability_action !== []) { - $capability_action[0]->activateTab($this->tabs_gui); - return $capability_action[0]->getCapability()->edit( + if ($capability_action !== false) { + $capability_action->activateTab($this->tabs_gui); + return $capability_action->getCapability()->edit( $environment->withActionParameter($action) ); } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 4615589fba7f..f0163f1292e7 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -74,9 +74,9 @@ public function getNew( */ public function getQuestionDataOnlyForAllQuestions(): \Generator { - foreach ($this->buildQuestionsQuery()->loadNextRecord( + foreach ($this->buildQuestionsQuery()->withGroupBy( $this->buildGroupByColumn() - ) as $query_with_record) { + )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, [] @@ -105,7 +105,9 @@ public function getQuestionDataOnlyForQuestionIds( ), Operator::In ) - )->loadNextRecord($this->buildGroupByColumn()) as $query_with_record) { + )->withGroupBy( + $this->buildGroupByColumn() + )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, [] @@ -237,9 +239,9 @@ private function getForBaseQuery( $query ); - foreach ($query_with_answer_forms->loadNextRecord( + foreach ($query_with_answer_forms->withGroupBy( $this->buildGroupByColumn() - ) as $query_with_record) { + )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, $this->retrieveAnswerFormsFromQuery($query_with_record) diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 7ab9a2678af5..55e9a878106b 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\Setup; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\TableTypes as SuggestedContentTableTypes; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableTypes as FeedbackTableTypes; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -363,4 +364,39 @@ public function step_7(): void ); } } + + public function step_8(): void + { + $table_name = $this->basic_table_name_builder->getTableNameFor( + SuggestedContentTableTypes::SuggestedLearningContent + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'type' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => false + ], + 'content' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields( + $table_name, + ['answer_form_id'] + )) { + $this->db->addPrimaryKey( + $table_name, + ['answer_form_id'], + ); + } + } } From d7de02a433ed7e798ff3d3f9ca757b7eddf83dd3 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 3 Apr 2026 15:48:36 +0200 Subject: [PATCH 083/108] Questions: Remove PersistenceFactory in Migration --- .../src/AnswerForm/Capabilities/Feedback/Migration.php | 5 ++--- .../Questions/src/AnswerForm/Capabilities/Migration.php | 2 -- .../Capabilities/SuggestedLearningContent/Migration.php | 1 - .../Questions/src/AnswerForm/Migration/Migration.php | 2 -- .../AnswerFormTypes/Cloze/Migration/MigrationCloze.php | 9 ++++----- .../Cloze/Migration/MigrationLongMenu.php | 8 +++----- .../AnswerFormTypes/Cloze/Migration/MigrationNumeric.php | 8 +++----- .../Cloze/Migration/MigrationTextSubset.php | 8 +++----- .../ILIAS/Questions/src/Setup/QuestionsMigration.php | 2 -- 9 files changed, 15 insertions(+), 30 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php index fd58898ec8e4..a081f533ba98 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php @@ -48,12 +48,11 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert ): ?MigrationInsert { $generic_feedback_insert = $this->buildGenericFeedbackInsert( - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert ); if ($generic_feedback_insert !== null) { @@ -64,7 +63,7 @@ public function completeMigrationInsert( } $specific_feedback_insert = $this->buildSpecificFeedbackInsert( - $persistence_factory, + $migration_insert->getPersistenceFactory(), $answer_form_migration, $migration_insert ); diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php index a8497d4766af..8d04438ea011 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Migration.php @@ -22,7 +22,6 @@ use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -32,7 +31,6 @@ public function getTableNameSpace(): TableNameSpace; public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert ): ?MigrationInsert; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php index 2e46fdcbf466..a762f1a67dd2 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php @@ -52,7 +52,6 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert ): ?MigrationInsert { diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php index b8b642024060..c2643a4a8ed6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerForm\Migration; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\Environment; @@ -39,7 +38,6 @@ public function getTableNameSpace(): TableNameSpace; public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 746c2f5d7b31..14516cb16d1d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -67,7 +67,6 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_input_mapping = []; @@ -90,7 +89,7 @@ public function completeMigrationInsert( ]; $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_mapping[$db_row->gap_id], @@ -114,7 +113,7 @@ public function completeMigrationInsert( $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer_option_id, @@ -133,7 +132,7 @@ public function completeMigrationInsert( if ($db_row->combinations_enabled) { $migration_insert = $this->addCombinationInsertStatements( - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert, $answer_input_mapping, $answer_options_mapping @@ -144,7 +143,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 0b9039bf55c5..9830dd3ef6e0 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -24,7 +24,6 @@ use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -58,7 +57,6 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_form_id = $migration_insert->getAnswerFormId(); @@ -77,7 +75,7 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $answer_input_id, @@ -136,7 +134,7 @@ public function completeMigrationInsert( ) as $answer) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $answer['answer_option_id'], @@ -153,7 +151,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, $this->buildScoringIdenticalFromOld($db_row->identical_scoring), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index d420353b50bf..cb555f9ac174 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -26,7 +26,6 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -62,7 +61,6 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $db_row = $this->fetchDBValues( @@ -79,7 +77,7 @@ public function completeMigrationInsert( return $migration_insert->withAdditionalInsert( $this->buildGapInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), null, $gap_id, @@ -95,7 +93,7 @@ public function completeMigrationInsert( )->withAdditionalInsert( $this->buildAnswerOptionInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), null, $migration_insert->getUuid(), @@ -109,7 +107,7 @@ public function completeMigrationInsert( )->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::ScoreAll, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 6d305dcf8eac..7d5da8dcc3dd 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -25,7 +25,6 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; @@ -59,7 +58,6 @@ public function getTableNameSpace(): TableNameSpace #[\Override] public function completeMigrationInsert( Environment $environment, - PersistenceFactory $persistence_factory, MigrationInsert $migration_insert ): ?MigrationInsert { $answer_form_id = $migration_insert->getAnswerFormId(); @@ -78,7 +76,7 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $gaps_insert, $gap_id, @@ -99,7 +97,7 @@ public function completeMigrationInsert( foreach ($gaps as $gap_id) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_options_insert, $migration_insert->getUuid(), @@ -121,7 +119,7 @@ public function completeMigrationInsert( ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, - $persistence_factory, + $migration_insert->getPersistenceFactory(), $migration_insert->getTableNameBuilder(), $answer_form_id, ScoringIdentical::OnlyScoreDistinct, diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 168b23d0b264..62bfce755246 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -129,7 +129,6 @@ public function step( $migration_insert = $answer_form_migration->completeMigrationInsert( $environment, - $this->persistence_factory, $this->buildMigrationInsert( $answer_form_migration, [ @@ -175,7 +174,6 @@ public function step( fn(AnswerFormMigrationInsert $c, CapabilityMigration $v): AnswerFormMigration => $v->completeMigrationInsert( $environment, - $this->persistence_factory, $migration_insert ), $migration_insert From e7b5a43ff965daaa997566ad4d24d03d5f75d917 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 3 Apr 2026 15:50:05 +0200 Subject: [PATCH 084/108] Questions: Rename EnvironmentImplementation --- .../ConfigurationRepository.php | 4 +-- .../src/AnswerForm/Capabilities/Action.php | 4 +-- ...lementation.php => DefaultEnvironment.php} | 2 +- .../Presentation/Layout/QuestionsTable.php | 4 +-- .../Questions/src/Presentation/Views/Edit.php | 32 +++++++++---------- .../src/Question/QuestionImplementation.php | 6 ++-- .../Questions/src/Question/Views/Edit.php | 16 +++++----- 7 files changed, 34 insertions(+), 34 deletions(-) rename components/ILIAS/Questions/src/Presentation/Definitions/{EnvironmentImplementation.php => DefaultEnvironment.php} (99%) diff --git a/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php index 34ddc3dd9a1a..fa9e33a3ee4e 100755 --- a/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php +++ b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Administration; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -55,7 +55,7 @@ public function getGlobalCreateMode(): CreateModes } public function isCreateModeSimple( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): bool { return $this->isCreateModeChangeableByUser() && $environment->isCreateModeSimple() || $this->getGlobalCreateMode() === CreateModes::Simple; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php index 38d32ffe125d..078c6bc81b01 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Language\Language; class Action @@ -41,7 +41,7 @@ public function getCapability(): Capability } public function addTab( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, \ilTabsGUI $tabs_gui, Language $lng ): void { diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php similarity index 99% rename from components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php rename to components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index fc3163143c03..e5d196d4f200 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/EnvironmentImplementation.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -34,7 +34,7 @@ use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; -class EnvironmentImplementation implements Environment +class DefaultEnvironment implements Environment { private const array QUERY_PARAMETER_NAME_SPACE = ['q']; private const string TOKEN_STRING_ACTION = 'a'; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index a84cb29c96cf..1d544095ea09 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -21,7 +21,7 @@ namespace ILIAS\Questions\Presentation\Layout; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\Range; @@ -40,7 +40,7 @@ public function __construct( private readonly \ilUIService $ui_service, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, - private readonly EnvironmentImplementation $environment + private readonly DefaultEnvironment $environment ) { $environment->getLanguage()->loadLanguageModule('qpl'); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 8987ec8b19ba..e282385daedc 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -34,7 +34,7 @@ use ILIAS\Questions\Presentation\Layout\Renderable; use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; use ILIAS\Questions\Question\Persistence\Repository; @@ -302,7 +302,7 @@ public function editAnswerForm( } private function createQuestion( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm { $this->initializeEditMode($environment); $this->tabs_gui->setBackTarget( @@ -338,7 +338,7 @@ private function createQuestion( } private function editQuestion( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm { $this->initializeEditMode($environment); $this->tabs_gui->setBackTarget( @@ -375,7 +375,7 @@ private function editQuestion( } private function deleteQuestions( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): Async { $question_ids = $environment->getQuestionIds(); @@ -410,7 +410,7 @@ private function deleteQuestions( } private function showTable( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): QuestionsTable { return new QuestionsTable( $this->ui_services, @@ -429,7 +429,7 @@ private function showTable( } private function processCreateAnswerForm( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, QuestionImplementation $question, \ilPCAnswerForm $content_object ): EditForm { @@ -467,7 +467,7 @@ private function processCreateAnswerForm( } private function forwardCreateAnswerFormCmd( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, QuestionImplementation $question, \ilPCAnswerForm $content_object, AnswerFormEditView $answer_form_edit_view @@ -500,7 +500,7 @@ private function forwardCreateAnswerFormCmd( } private function initializeEditMode( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): void { $this->tabs_gui->clearTargets(); @@ -526,7 +526,7 @@ private function initializeEditMode( } private function buildQuestionListSlate( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): LegacySlate { return $this->ui_factory->mainControls()->slate()->legacy( $this->lng->txt('mainbar_button_label_questionlist'), @@ -545,7 +545,7 @@ private function buildQuestionListSlate( } private function buildItemGroupForQuestionListSlate( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): ItemGroup { return $this->ui_factory->item()->group( '', @@ -569,7 +569,7 @@ private function builEditLinksForQuestionListSlate( } private function buildEditStartView( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, QuestionImplementation $question ): EditForm { return $question->getEditView( @@ -583,7 +583,7 @@ private function buildEditStartView( } private function buildCreateAnswerForm( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm { $if = $this->ui_factory->input(); @@ -679,8 +679,8 @@ private function deleteSelectedQuestions( private function buildEnvironment( URI $base_uri, int $obj_id - ): EnvironmentImplementation { - return new EnvironmentImplementation( + ): DefaultEnvironment { + return new DefaultEnvironment( $this->ctrl, $this->http, $this->refinery, @@ -696,7 +696,7 @@ private function buildEnvironment( } private function buildAfterQuestionCreationRedirectUri( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, CreateModes $create_mode, Uuid $question_uuid ): string { @@ -723,7 +723,7 @@ private function buildAfterQuestionCreationRedirectUri( } private function buildAfterAnswerFormCreationRedirectUri( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, ): string { if (!$this->configuration_repository->isCreateModeSimple($environment)) { return $this->ctrl->getLinkTargetByClass( diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php index 0ccc40dd4c29..0bde88619952 100644 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ b/components/ILIAS/Questions/src/Question/QuestionImplementation.php @@ -32,7 +32,7 @@ use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; @@ -281,7 +281,7 @@ public function getParticipantView(): Views\Participant public function toEditLink( LinkFactory $link_factory, - EnvironmentImplementation $environment + DefaultEnvironment $environment ): StandardLink { return $link_factory->standard( $this->title, @@ -294,7 +294,7 @@ public function toEditLink( public function toTableRow( DataRowBuilder $row_builder, - EnvironmentImplementation $environment + DefaultEnvironment $environment ): DataRow { return $row_builder->buildDataRow( $this->id->toString(), diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index cda73a72a87f..f34595129c61 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -22,7 +22,7 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Definitions\EnvironmentImplementation; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; @@ -47,7 +47,7 @@ public function __construct( } public function create( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm|Question { return match ($environment->getStep()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesCreateForm( @@ -58,7 +58,7 @@ public function create( } public function edit( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, Participant $participant_view ): EditForm|Question { return match ($environment->getStep()) { @@ -73,7 +73,7 @@ public function edit( } private function buildBasicPropertiesCreateForm( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm { $ff = $environment->getUIFactory()->input()->field(); @@ -119,7 +119,7 @@ private function buildBasicPropertiesCreateForm( } private function processBasicPropertiesCreateForm( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm|Question { $form = $this->buildBasicPropertiesCreateForm( $environment @@ -132,7 +132,7 @@ private function processBasicPropertiesCreateForm( } private function buildBasicPropertiesEditingForm( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm { return $environment->getPresentationFactory()->getEditForm( $environment->getUIFactory()->input()->field()->section( @@ -157,7 +157,7 @@ private function buildBasicPropertiesEditingForm( } private function processBasicPropertiesEditingForm( - EnvironmentImplementation $environment + DefaultEnvironment $environment ): EditForm|Question { $form = $this->buildBasicPropertiesEditingForm( $environment @@ -225,7 +225,7 @@ function (array $vs): QuestionImplementation { } private function buildPreviewPanel( - EnvironmentImplementation $environment, + DefaultEnvironment $environment, Participant $participant_view ): StandardPanel { $environment->preserveParametersForPageEditorCmds(); From c3662c8c42636c28226bc502c56ac871d9e40d24 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 3 Apr 2026 15:56:30 +0200 Subject: [PATCH 085/108] Questions: Rename QuestionClass and Interface --- .../Legacy/PageEditor/QstsQuestionPage.php | 8 +- .../PageEditor/class.QstsQuestionPageGUI.php | 4 +- .../PageEditor/class.ilPCAnswerForm.php | 2 +- .../PageEditor/class.ilPCAnswerFormGUI.php | 2 +- components/ILIAS/Questions/src/Collector.php | 4 +- .../Questions/src/Presentation/Views/Edit.php | 12 +- .../src/Question/Persistence/Repository.php | 22 +- .../src/Question/PublicQuestionInterface.php | 28 + .../ILIAS/Questions/src/Question/Question.php | 799 ++++++++++++++++- .../src/Question/QuestionImplementation.php | 821 ------------------ .../Questions/src/Question/Views/Edit.php | 5 +- .../src/Question/Views/Participant.php | 4 +- .../classes/class.ilAssQuestionPage.php | 8 +- 13 files changed, 859 insertions(+), 860 deletions(-) create mode 100644 components/ILIAS/Questions/src/Question/PublicQuestionInterface.php delete mode 100644 components/ILIAS/Questions/src/Question/QuestionImplementation.php diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 9f0d1b14a2ec..8d70bf7e1339 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -19,12 +19,12 @@ declare(strict_types=1); use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; class QstsQuestionPage extends ilPageObject { private readonly Edit $edit_view; - private readonly QuestionImplementation $question; + private readonly Question $question; #[\Override] public function getParentType(): string @@ -43,13 +43,13 @@ public function setEditView( $this->edit_view = $edit_view; } - public function getQuestion(): QuestionImplementation + public function getQuestion(): Question { return $this->question; } public function setQuestion( - QuestionImplementation $question + Question $question ): void { $this->question = $question; } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index 6bd28a146887..541b0e08e158 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -19,7 +19,7 @@ declare(strict_types=1); use ILIAS\Questions\Presentation\Views\Edit; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; use ILIAS\Data\URI; /** @@ -33,7 +33,7 @@ class QstsQuestionPageGUI extends ilPageObjectGUI private URI $return_uri; public function __construct( - QuestionImplementation $question, + Question $question, int $obj_id, ?Edit $edit_view = null ) { diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 0f42da2ed931..d370ed5a96f5 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -86,7 +86,7 @@ public static function afterPageUpdate( $dom_util = $DIC->copage()->internal()->domain()->domUtil(); $question_repository = LocalDIC::dic()[Repository::class]; - /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + /** @var \ILIAS\Questions\Question\Question $question */ $question = $page->getQuestion(); $answer_forms = []; diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index 8de7fcddfd8a..3ad7d3715e3f 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -72,7 +72,7 @@ public function insertCmd(): void public function editCmd(): void { - /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + /** @var \ILIAS\Questions\Question\Question $question */ $question = $this->pg_obj->getQuestion(); $answer_form_properties = $question->getAnswerFormPropertiesByIdString( $this->getContentObject()->getAnswerFormIdStringFromAttribute() diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php index 28fc8b6ad88b..d5143cc9d536 100644 --- a/components/ILIAS/Questions/src/Collector.php +++ b/components/ILIAS/Questions/src/Collector.php @@ -21,7 +21,7 @@ namespace ILIAS\Questions; use ILIAS\Questions\Question\Persistence\Repository; -use ILIAS\Questions\Question\Question; +use ILIAS\Questions\Question\PublicQuestionInterface; use ILIAS\Data\UUID\Uuid; class Collector @@ -44,7 +44,7 @@ public function withRequiredCapabilities( public function getQuestionsForId( Uuid $id - ): ?Question { + ): ?PublicQuestionInterface { return $this->repository->getForQuestionId($id); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index e282385daedc..42e0cd757b7b 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -38,7 +38,7 @@ use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; use ILIAS\Questions\Question\Persistence\Repository; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -186,7 +186,7 @@ public function forwardPageCmds( public function createAnswerForm( URI $base_uri, int $obj_id, - QuestionImplementation $question, + Question $question, \ilPCAnswerForm $content_object ): EditForm { $environment = $this->buildEnvironment( @@ -239,7 +239,7 @@ public function createAnswerForm( public function editAnswerForm( URI $base_uri, int $obj_id, - QuestionImplementation $question, + Question $question, AnswerFormProperties $answer_form_properties, Definition $type_definition ): Async|Renderable { @@ -430,7 +430,7 @@ private function showTable( private function processCreateAnswerForm( DefaultEnvironment $environment, - QuestionImplementation $question, + Question $question, \ilPCAnswerForm $content_object ): EditForm { $form = $this->buildCreateAnswerForm($environment) @@ -468,7 +468,7 @@ private function processCreateAnswerForm( private function forwardCreateAnswerFormCmd( DefaultEnvironment $environment, - QuestionImplementation $question, + Question $question, \ilPCAnswerForm $content_object, AnswerFormEditView $answer_form_edit_view ): ?EditForm { @@ -570,7 +570,7 @@ private function builEditLinksForQuestionListSlate( private function buildEditStartView( DefaultEnvironment $environment, - QuestionImplementation $question + Question $question ): EditForm { return $question->getEditView( $this->configuration_repository, diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index f0163f1292e7..ce13d22253da 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -25,7 +25,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; use ILIAS\Questions\Question\Definitions\Lifecycle; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Operator; @@ -62,8 +62,8 @@ public function __construct( public function getNew( int $parent_obj_id - ): QuestionImplementation { - return new QuestionImplementation( + ): Question { + return new Question( $this->buildAvailableUuid(), $parent_obj_id ); @@ -117,7 +117,7 @@ public function getQuestionDataOnlyForQuestionIds( public function getForQuestionId( Uuid $question_id - ): ?QuestionImplementation { + ): ?Question { return $this->getForBaseQuery( $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( @@ -173,7 +173,7 @@ public function create( ): void { $this->store( array_map( - fn(QuestionImplementation $v): QuestionImplementation => $v + fn(Question $v): Question => $v ->withPageId($this->buildQuestionPage($v->getParentObjId())), $questions ), @@ -202,7 +202,7 @@ public function delete( ): void { array_reduce( $questions, - fn(Manipulate $c, QuestionImplementation $v): Manipulate + fn(Manipulate $c, Question $v): Manipulate => $v->toDelete( $c, $this->persistence_factory, @@ -255,7 +255,7 @@ private function getForBaseQuery( private function retrieveQuestionFromQuery( Query $query, array $answer_forms - ): QuestionImplementation { + ): Question { $linking_info = $query->retrieveCurrentRecord( $this->persistence_factory->table( $this->question_table_names_builder, @@ -270,7 +270,7 @@ private function retrieveQuestionFromQuery( TableTypes::Questions, ), $this->refinery->custom()->transformation( - fn(array $vs): QuestionImplementation => new QuestionImplementation( + fn(array $vs): Question => new Question( $this->uuid_factory->fromString($vs[0]['id']), $linking_info[0]['obj_id'], $linking_info[0]['position'], @@ -368,7 +368,7 @@ private function store( ): void { array_reduce( $questions, - fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toStorage( + fn(Manipulate $c, Question $v): Manipulate => $v->toStorage( $c, $this->persistence_factory, $this->question_table_definitions, @@ -469,8 +469,8 @@ private function getNextAvailableQuestionPageId(): int } private function migrateQuestionPage( - QuestionImplementation $question - ): QuestionImplementation { + Question $question + ): Question { $table_name = $this->question_table_names_builder ->getTableNameFor(TableTypes::MigrationsTable); diff --git a/components/ILIAS/Questions/src/Question/PublicQuestionInterface.php b/components/ILIAS/Questions/src/Question/PublicQuestionInterface.php new file mode 100644 index 000000000000..b6338f329375 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/PublicQuestionInterface.php @@ -0,0 +1,28 @@ +answer_forms = array_reduce( + $answer_forms, + function (array $c, AnswerFormProperties $v): array { + $c[$v->getAnswerFormId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function getId(): ?Uuid + { + return $this->id; + } + + public function getParentObjId(): int + { + return $this->parent_obj_id; + } + + public function withParentObjId( + int $parent_obj_id + ): self { + $clone = clone $this; + $clone->parent_obj_id = $parent_obj_id; + $clone->linking_information_updated = true; + return $clone; + } + + public function withPosition( + int $position + ): self { + $clone = clone $this; + $clone->position = $position; + $clone->linking_information_updated = true; + return $clone; + } + + public function getPageId(): ?int + { + return $this->page_id; + } + + public function withPageId( + int $page_id + ): self { + $clone = clone $this; + $clone->page_id = $page_id; + $clone->page_id_updated = true; + return $clone; + } + + public function getTitle(): string + { + return $this->title; + } + + public function withTitle( + string $title + ): self { + $clone = clone $this; + $clone->title = $title; + $clone->self_updated = true; + return $clone; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function withAuthor( + string $author + ): self { + $clone = clone $this; + $clone->author = $author; + $clone->self_updated = true; + return $clone; + } + + public function getLifecycle(): Lifecycle + { + return $this->lifecycle; + } + + public function withLifecycle( + Lifecycle $lifecycle + ): self { + $clone = clone $this; + $clone->lifecycle = $lifecycle; + $clone->self_updated = true; + return $clone; + } + + public function getRemarks(): string + { + return $this->remarks; + } + + public function withRemarks( + string $remarks + ): self { + $clone = clone $this; + $clone->remarks = $remarks; + $clone->self_updated = true; + return $clone; + } + + public function getOriginalId(): ?Uuid + { + return $this->original_id; + } + + public function withOriginalId( + Uuid $original_id + ): self { + $clone = clone $this; + $clone->original_id = $original_id; + $clone->self_updated = true; + return $clone; + } + + public function getLastUpdate(): ?\DateTimeImmutable + { + return $this->last_update; + } + + public function getCreated(): ?\DateTimeImmutable + { + return $this->created; + } + + public function getAnswerFormProperties(): array + { + return $this->answer_forms; + } + + public function getAnswerFormPropertiesByIdString( + string $form_id + ): ?AnswerFormProperties { + return $this->answer_forms[$form_id] ?? null; + } + + public function withAnswerFormProperties( + AnswerFormProperties $answer_form + ): self { + $clone = clone $this; + $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; + $clone->updated_answer_forms[] = $answer_form; + return $clone; + } + + public function withoutDeletedAnswerForms( + array $found_answer_form_ids + ): self { + $clone = clone $this; + foreach (array_keys($this->answer_forms) as $answer_form_id) { + if (!in_array($answer_form_id, $found_answer_form_ids)) { + $clone->deleted_answer_forms[] = $clone->answer_forms[$answer_form_id]; + unset($clone->answer_forms[$answer_form_id]); + } + } + + return $clone; + } + + /** + * Checks whether the question is a clone of another question or not + */ + public function isClone(): bool + { + return $this->original_id !== null; + } + + public function getCreateMode(): ?CreateModes + { + return $this->create_mode; + } + + public function withCreateMode( + CreateModes $create_mode + ): self { + $clone = clone $this; + $clone->create_mode = $create_mode; + return $clone; + } + + public function getEditView( + ConfigurationRepository $configuration_repository, + \ilObjUser $current_user, + \ilCtrl $ctrl + ): Views\Edit { + return new Views\Edit( + $configuration_repository, + $current_user, + $ctrl, + $this + ); + } + + #[\Override] + public function getParticipantView(): Views\Participant + { + return new Views\Participant( + $this + ); + } + + public function toEditLink( + LinkFactory $link_factory, + DefaultEnvironment $environment + ): StandardLink { + return $link_factory->standard( + $this->title, + $environment->withQuestionIdParameter($this->id) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + public function toTableRow( + DataRowBuilder $row_builder, + DefaultEnvironment $environment + ): DataRow { + return $row_builder->buildDataRow( + $this->id->toString(), + [ + 'title' => $environment->getUIFactory()->link()->standard( + $this->title, + $environment->withQuestionIdParameter( + $this->id + )->getUrlBuilder() + ->buildURI() + ->__toString() + ) + ] + ); + } + + public function toStorage( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + ): Manipulate { + return $manipulate->getManipulationType() === ManipulationType::Create + ? $this->addInsertStatementsToManipulation( + $manipulate, + $persistence_factory, + $question_tables_definitions + ) : $this->addUpdateStatementsToManipulation( + $manipulate, + $persistence_factory, + $question_tables_definitions, + $answer_form_generic_table_definitions + ); + } + + public function toDelete( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + ): Manipulate { + $table_name_builder = $manipulate->getTableNameBuilder(null); + + return $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate->withAdditionalStatement( + $this->buildDeleteQuestionStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + )->withAdditionalStatement( + $this->buildDeleteLinkingStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + )->withAdditionalStatement( + $this->buildDeleteMigrationStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + ), + $persistence_factory, + $answer_form_generic_table_definitions, + $this->answer_forms + ); + } + + private function addInsertStatementsToManipulation( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions + ): Manipulate { + if ($this->created === null) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildInsertLinkingStatement( + $persistence_factory, + $question_tables_definitions, + $manipulate->getTableNameBuilder(null) + ) + )->withAdditionalStatement( + $this->buildInsertQuestionStatement( + $persistence_factory, + $question_tables_definitions, + $manipulate->getTableNameBuilder(null) + ) + ); + } + + if ($this->updated_answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + if ($this->answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->answer_forms + ); + } + + return $manipulate; + } + + private function addUpdateStatementsToManipulation( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + ): Manipulate { + $table_name_builder = $manipulate->getTableNameBuilder(null); + + if ($this->linking_information_updated) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildUpdateLinkingStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + ); + } + + if ($this->self_updated) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdateQuestionStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + ); + } + + if ($this->page_id) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdatePageIdStatement( + $persistence_factory, + $question_tables_definitions, + $table_name_builder + ) + ); + } + + if ($this->deleted_answer_forms !== []) { + $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, + $this->deleted_answer_forms + ); + } + + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, + $this->updated_answer_forms + ); + } + + private function addAnswerFormStatementsToManipulate( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, + array $answer_forms + ): Manipulate { + return array_reduce( + $answer_forms, + function ( + Manipulate $c, + AnswerFormProperties $v + ) use ( + $persistence_factory, + $answer_form_generic_table_definitions + ): Manipulate { + $manipulate_with_generic_properties = $v->getTypeGenericProperties() + ->toStorage( + $persistence_factory, + $answer_form_generic_table_definitions, + $c + ); + + return $v->toStorage( + $persistence_factory, + $manipulate_with_generic_properties + ); + }, + $manipulate + ); + } + + private function addDeleteAnswerFormsStatementsToManipulate( + Manipulate $manipulate, + PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, + array $answer_forms_to_delete + ): Manipulate { + return array_reduce( + $answer_forms_to_delete, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( + $v->getTypeGenericProperties()->toDelete( + $persistence_factory, + $answer_form_generic_table_definitions, + $c, + ) + ), + $manipulate + ); + } + + + + private function buildInsertLinkingStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder + ): Insert { + return $persistence_factory->insert( + $question_tables_definitions->getColumns( + $table_name_builder, + QuestionTableTypes::Linking + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) + ] + ); + } + + private function buildInsertQuestionStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder + ): Insert { + return $persistence_factory->insert( + $question_tables_definitions->getColumns( + $table_name_builder, + QuestionTableTypes::Questions + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ] + ); + } + + private function buildUpdateLinkingStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder + ): Update { + $table_type = QuestionTableTypes::Linking; + return $persistence_factory->update( + $question_tables_definitions->getColumns( + $table_name_builder, + $table_type, + '', + [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) + ], + [ + $persistence_factory->where( + $table_type->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildUpdateQuestionStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder + ): Update { + $table_type = QuestionTableTypes::Questions; + return $persistence_factory->update( + $question_tables_definitions->getColumns( + $table_name_builder, + $table_type, + '', + [ + 'id', + 'page_id', + 'created' + ] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ], + [ + $persistence_factory->where( + $question_tables_definitions->getIdColumn( + $table_name_builder, + $table_type + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildDeleteQuestionStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $question_tables_definitions, + TableNameBuilder $table_name_builder + ): Delete { + $table_type = QuestionTableTypes::Questions; + return $persistence_factory->delete( + $persistence_factory->table( + $table_name_builder, + $table_type + ), + [ + $persistence_factory->where( + $question_tables_definitions->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_type + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildDeleteLinkingStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder + ): Delete { + $table_type = QuestionTableTypes::Linking; + return $persistence_factory->delete( + $persistence_factory->table( + $table_name_builder, + $table_type + ), + [ + $persistence_factory->where( + $table_definitions->getIdColumn( + $table_name_builder, + $table_type + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after + * this MUST go! + */ + private function buildDeleteMigrationStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder + ): Delete { + $table_type = QuestionTableTypes::MigrationsTable; + return $persistence_factory->delete( + $persistence_factory->table( + $table_name_builder, + $table_type + ), + [ + $persistence_factory->where( + $table_definitions->getIdColumn( + $table_name_builder, + $table_type + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after + * this a question MUST never change the page assigned to it after its creation! + */ + private function buildUpdatePageIdStatement( + PersistenceFactory $persistence_factory, + QuestionTableDefinitions $table_definitions, + TableNameBuilder $table_name_builder + ): Update { + $table_type = QuestionTableTypes::Questions; + return $persistence_factory->update( + [ + $persistence_factory->column( + $persistence_factory->table( + $table_name_builder, + $table_type + ), + 'page_id' + ), + $persistence_factory->column( + $persistence_factory->table( + $table_name_builder, + $table_type + ), + 'last_update' + ) + ], + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ], + [ + $persistence_factory->where( + $table_definitions->getIdColumn( + $table_name_builder, + $table_type + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } } diff --git a/components/ILIAS/Questions/src/Question/QuestionImplementation.php b/components/ILIAS/Questions/src/Question/QuestionImplementation.php deleted file mode 100644 index 0bde88619952..000000000000 --- a/components/ILIAS/Questions/src/Question/QuestionImplementation.php +++ /dev/null @@ -1,821 +0,0 @@ -answer_forms = array_reduce( - $answer_forms, - function (array $c, AnswerFormProperties $v): array { - $c[$v->getAnswerFormId()->toString()] = $v; - return $c; - }, - [] - ); - } - - public function getId(): ?Uuid - { - return $this->id; - } - - public function getParentObjId(): int - { - return $this->parent_obj_id; - } - - public function withParentObjId( - int $parent_obj_id - ): self { - $clone = clone $this; - $clone->parent_obj_id = $parent_obj_id; - $clone->linking_information_updated = true; - return $clone; - } - - public function withPosition( - int $position - ): self { - $clone = clone $this; - $clone->position = $position; - $clone->linking_information_updated = true; - return $clone; - } - - public function getPageId(): ?int - { - return $this->page_id; - } - - public function withPageId( - int $page_id - ): self { - $clone = clone $this; - $clone->page_id = $page_id; - $clone->page_id_updated = true; - return $clone; - } - - public function getTitle(): string - { - return $this->title; - } - - public function withTitle( - string $title - ): self { - $clone = clone $this; - $clone->title = $title; - $clone->self_updated = true; - return $clone; - } - - public function getAuthor(): string - { - return $this->author; - } - - public function withAuthor( - string $author - ): self { - $clone = clone $this; - $clone->author = $author; - $clone->self_updated = true; - return $clone; - } - - public function getLifecycle(): Lifecycle - { - return $this->lifecycle; - } - - public function withLifecycle( - Lifecycle $lifecycle - ): self { - $clone = clone $this; - $clone->lifecycle = $lifecycle; - $clone->self_updated = true; - return $clone; - } - - public function getRemarks(): string - { - return $this->remarks; - } - - public function withRemarks( - string $remarks - ): self { - $clone = clone $this; - $clone->remarks = $remarks; - $clone->self_updated = true; - return $clone; - } - - public function getOriginalId(): ?Uuid - { - return $this->original_id; - } - - public function withOriginalId( - Uuid $original_id - ): self { - $clone = clone $this; - $clone->original_id = $original_id; - $clone->self_updated = true; - return $clone; - } - - public function getLastUpdate(): ?\DateTimeImmutable - { - return $this->last_update; - } - - public function getCreated(): ?\DateTimeImmutable - { - return $this->created; - } - - public function getAnswerFormProperties(): array - { - return $this->answer_forms; - } - - public function getAnswerFormPropertiesByIdString( - string $form_id - ): ?AnswerFormProperties { - return $this->answer_forms[$form_id] ?? null; - } - - public function withAnswerFormProperties( - AnswerFormProperties $answer_form - ): self { - $clone = clone $this; - $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; - $clone->updated_answer_forms[] = $answer_form; - return $clone; - } - - public function withoutDeletedAnswerForms( - array $found_answer_form_ids - ): self { - $clone = clone $this; - foreach (array_keys($this->answer_forms) as $answer_form_id) { - if (!in_array($answer_form_id, $found_answer_form_ids)) { - $clone->deleted_answer_forms[] = $clone->answer_forms[$answer_form_id]; - unset($clone->answer_forms[$answer_form_id]); - } - } - - return $clone; - } - - /** - * Checks whether the question is a clone of another question or not - */ - public function isClone(): bool - { - return $this->original_id !== null; - } - - public function getCreateMode(): ?CreateModes - { - return $this->create_mode; - } - - public function withCreateMode( - CreateModes $create_mode - ): self { - $clone = clone $this; - $clone->create_mode = $create_mode; - return $clone; - } - - public function getEditView( - ConfigurationRepository $configuration_repository, - \ilObjUser $current_user, - \ilCtrl $ctrl - ): Views\Edit { - return new Views\Edit( - $configuration_repository, - $current_user, - $ctrl, - $this - ); - } - - #[\Override] - public function getParticipantView(): Views\Participant - { - return new Views\Participant( - $this - ); - } - - public function toEditLink( - LinkFactory $link_factory, - DefaultEnvironment $environment - ): StandardLink { - return $link_factory->standard( - $this->title, - $environment->withQuestionIdParameter($this->id) - ->getUrlBuilder() - ->buildURI() - ->__toString() - ); - } - - public function toTableRow( - DataRowBuilder $row_builder, - DefaultEnvironment $environment - ): DataRow { - return $row_builder->buildDataRow( - $this->id->toString(), - [ - 'title' => $environment->getUIFactory()->link()->standard( - $this->title, - $environment->withQuestionIdParameter( - $this->id - )->getUrlBuilder() - ->buildURI() - ->__toString() - ) - ] - ); - } - - public function toStorage( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions - ): Manipulate { - return $manipulate->getManipulationType() === ManipulationType::Create - ? $this->addInsertStatementsToManipulation( - $manipulate, - $persistence_factory, - $question_tables_definitions - ) : $this->addUpdateStatementsToManipulation( - $manipulate, - $persistence_factory, - $question_tables_definitions, - $answer_form_generic_table_definitions - ); - } - - public function toDelete( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions - ): Manipulate { - $table_name_builder = $manipulate->getTableNameBuilder(null); - - return $this->addDeleteAnswerFormsStatementsToManipulate( - $manipulate->withAdditionalStatement( - $this->buildDeleteQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - )->withAdditionalStatement( - $this->buildDeleteLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - )->withAdditionalStatement( - $this->buildDeleteMigrationStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - ), - $persistence_factory, - $answer_form_generic_table_definitions, - $this->answer_forms - ); - } - - private function addInsertStatementsToManipulation( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions - ): Manipulate { - if ($this->created === null) { - $manipulate = $manipulate - ->withAdditionalStatement( - $this->buildInsertLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $manipulate->getTableNameBuilder(null) - ) - )->withAdditionalStatement( - $this->buildInsertQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $manipulate->getTableNameBuilder(null) - ) - ); - } - - if ($this->updated_answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $this->updated_answer_forms - ); - } - - if ($this->answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $this->answer_forms - ); - } - - return $manipulate; - } - - private function addUpdateStatementsToManipulation( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions - ): Manipulate { - $table_name_builder = $manipulate->getTableNameBuilder(null); - - if ($this->linking_information_updated) { - $manipulate = $manipulate - ->withAdditionalStatement( - $this->buildUpdateLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - ); - } - - if ($this->self_updated) { - $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdateQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - ); - } - - if ($this->page_id) { - $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdatePageIdStatement( - $persistence_factory, - $question_tables_definitions, - $table_name_builder - ) - ); - } - - if ($this->deleted_answer_forms !== []) { - $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->deleted_answer_forms - ); - } - - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->updated_answer_forms - ); - } - - private function addAnswerFormStatementsToManipulate( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - array $answer_forms - ): Manipulate { - return array_reduce( - $answer_forms, - function ( - Manipulate $c, - AnswerFormProperties $v - ) use ( - $persistence_factory, - $answer_form_generic_table_definitions - ): Manipulate { - $manipulate_with_generic_properties = $v->getTypeGenericProperties() - ->toStorage( - $persistence_factory, - $answer_form_generic_table_definitions, - $c - ); - - return $v->toStorage( - $persistence_factory, - $manipulate_with_generic_properties - ); - }, - $manipulate - ); - } - - private function addDeleteAnswerFormsStatementsToManipulate( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - array $answer_forms_to_delete - ): Manipulate { - return array_reduce( - $answer_forms_to_delete, - fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( - $v->getTypeGenericProperties()->toDelete( - $persistence_factory, - $answer_form_generic_table_definitions, - $c, - ) - ), - $manipulate - ); - } - - - - private function buildInsertLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder - ): Insert { - return $persistence_factory->insert( - $question_tables_definitions->getColumns( - $table_name_builder, - QuestionTableTypes::Linking - ), - [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->parent_obj_id - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->position - ) - ] - ); - } - - private function buildInsertQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder - ): Insert { - return $persistence_factory->insert( - $question_tables_definitions->getColumns( - $table_name_builder, - QuestionTableTypes::Questions - ), - [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->page_id - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->title - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->author - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->lifecycle->value - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->remarks - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->original_id?->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - time() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - time() - ) - ] - ); - } - - private function buildUpdateLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder - ): Update { - $table_type = QuestionTableTypes::Linking; - return $persistence_factory->update( - $question_tables_definitions->getColumns( - $table_name_builder, - $table_type, - '', - [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] - ), - [ - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->parent_obj_id - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->position - ) - ], - [ - $persistence_factory->where( - $table_type->getIdColumn( - $persistence_factory - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildUpdateQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder - ): Update { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->update( - $question_tables_definitions->getColumns( - $table_name_builder, - $table_type, - '', - [ - 'id', - 'page_id', - 'created' - ] - ), - [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->title - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->author - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->lifecycle->value - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->remarks - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->original_id?->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - time() - ) - ], - [ - $persistence_factory->where( - $question_tables_definitions->getIdColumn( - $table_name_builder, - $table_type - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildDeleteQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder - ): Delete { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->delete( - $persistence_factory->table( - $table_name_builder, - $table_type - ), - [ - $persistence_factory->where( - $question_tables_definitions->getIdColumn( - $persistence_factory, - $table_name_builder, - $table_type - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildDeleteLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder - ): Delete { - $table_type = QuestionTableTypes::Linking; - return $persistence_factory->delete( - $persistence_factory->table( - $table_name_builder, - $table_type - ), - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_name_builder, - $table_type - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - /** - * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after - * this MUST go! - */ - private function buildDeleteMigrationStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder - ): Delete { - $table_type = QuestionTableTypes::MigrationsTable; - return $persistence_factory->delete( - $persistence_factory->table( - $table_name_builder, - $table_type - ), - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_name_builder, - $table_type - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - /** - * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after - * this a question MUST never change the page assigned to it after its creation! - */ - private function buildUpdatePageIdStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder - ): Update { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->update( - [ - $persistence_factory->column( - $persistence_factory->table( - $table_name_builder, - $table_type - ), - 'page_id' - ), - $persistence_factory->column( - $persistence_factory->table( - $table_name_builder, - $table_type - ), - 'last_update' - ) - ], - [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->page_id - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - time() - ) - ], - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_name_builder, - $table_type - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } -} diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index f34595129c61..72aa523a30b2 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -24,7 +24,6 @@ use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Question\Question; -use ILIAS\Questions\Question\QuestionImplementation; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Language\Language; @@ -41,7 +40,7 @@ public function __construct( private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, private readonly \ilCtrl $ctrl, - private readonly QuestionImplementation $question + private readonly Question $question ) { } @@ -208,7 +207,7 @@ private function buildAddBasicPropertiesToQuestionTrafo( Refinery $refinery ): Transformation { return $refinery->custom()->transformation( - function (array $vs): QuestionImplementation { + function (array $vs): Question { $question = $this->question ->withTitle($vs['title']) ->withAuthor($vs['author']) diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index f36881863239..a6eeec176824 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Question\Views; -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; class Participant { @@ -30,7 +30,7 @@ class Participant private bool $show_correct_solution = false; public function __construct( - private readonly QuestionImplementation $question + private readonly Question $question ) { } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index 8f3a80954dea..3fab06b00082 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -18,12 +18,12 @@ declare(strict_types=1); -use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Questions\Question\Question; use ILIAS\Data\UUID\Uuid; class ilAssQuestionPage extends ilPageObject { - private readonly QuestionImplementation $question; + private readonly Question $question; /** * Get parent type @@ -35,14 +35,14 @@ public function getParentType(): string } public function setQuestion( - QuestionImplementation $question + Question $question ): void { $this->question = $question; } public function copyToAnswerForm( int $new_id, - QuestionImplementation $question + Question $question ): void { $this->buildDom(); $this->migrateQuestionElementToAnswerForm(); From c9d53a8a94fb2b4ff94b06417ea87ed5054d35ff Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 3 Apr 2026 17:31:33 +0200 Subject: [PATCH 086/108] Questions: Rename Step to SubAction --- .../Capabilities/Feedback/Capability.php | 14 +-- .../NodeRetrieval.php | 8 +- .../SuggestedLearningContent/Overview.php | 34 +++--- .../Capabilities/FeedbackOverviewTable.php | 38 +++---- .../Cloze/Layout/OverviewTable.php | 6 +- .../Properties/Combinations/Combination.php | 2 +- .../Combinations/EditCombinations.php | 14 +-- .../Properties/Combinations/Overview.php | 36 +++---- .../Cloze/Properties/Gaps/Numeric.php | 8 +- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 50 ++++----- .../AnswerFormTypes/Cloze/Views/EditGaps.php | 102 +++++++++--------- .../src/Presentation/Definitions/Actor.php | 2 +- .../Definitions/DefaultEnvironment.php | 44 ++++---- .../Presentation/Definitions/Environment.php | 12 +-- .../Layout/Tools/UploadHandler.php | 26 ++--- .../Questions/src/Presentation/Views/Edit.php | 12 +-- .../Questions/src/Question/Views/Edit.php | 8 +- 17 files changed, 208 insertions(+), 208 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php index 0000fff26e75..185e0e405ea0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php @@ -30,8 +30,8 @@ class Capability implements CapabilityInterface { - private const string STEP_SAVE = 's'; - private const string STEP_INSERT_LEGACY_TEXTS = 'ilt'; + private const string SUB_ACTION_SAVE = 's'; + private const string SUB_ACTION_INSERT_LEGACY_TEXTS = 'ilt'; public function __construct( private readonly TextFactory $text_factory, @@ -110,11 +110,11 @@ private function buildOverview( ->getDefinition() ->getCapability(Feedback::class) ), - $environment->withStepParameter( - self::STEP_SAVE + $environment->withSubActionParameter( + self::SUB_ACTION_SAVE )->getUrlBuilder(), - $environment->withStepParameter( - self::STEP_INSERT_LEGACY_TEXTS + $environment->withSubActionParameter( + self::SUB_ACTION_INSERT_LEGACY_TEXTS )->getUrlBuilder() ); } @@ -140,7 +140,7 @@ private function save( ); return $environment->redirectTo( - $environment->withDefaultStep()->getUrlBuilder() + $environment->withDefaultSubAction()->getUrlBuilder() ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php index 886d82d55233..fa3068ef0410 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php @@ -34,7 +34,7 @@ class NodeRetrieval implements NodeRetrievalInterface, Actor { - private const string STEP_RETRIEVE_NODES = 'rn'; + private const string SUB_ACTION_RETRIEVE_NODES = 'rn'; private const array PARAMETER_NAMESPACE = ['q', 'nr']; private const string PARAMETER_ID_STRING_NODE = 'n'; @@ -61,7 +61,7 @@ public function __construct( private readonly string $requested_type ) { [$this->url_builder, $this->node_parameter_token] = $environment - ->withStepParameter(self::STEP_RETRIEVE_NODES) + ->withSubActionParameter(self::SUB_ACTION_RETRIEVE_NODES) ->getUrlBuilder() ->acquireParameter( self::PARAMETER_NAMESPACE, @@ -112,9 +112,9 @@ public function getNodesAsLeaf( #[\Override] public function can( - string $step + string $sub_action ): bool { - return $step === self::STEP_RETRIEVE_NODES + return $sub_action === self::SUB_ACTION_RETRIEVE_NODES && $this->environment->getHttpServices()->wrapper()->query()->has( $this->node_parameter_token->getName() ); diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php index 8e4ad4206182..b036d5d6317b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php @@ -35,10 +35,10 @@ class Overview implements Renderable { - private const string STEP_SELECT_TYPE = 'st'; - private const string STEP_SELECT_CONTENT = 'sc'; - private const string STEP_SAVE_CONTENT = 'sac'; - private const string STEP_SAVE_SUB_CONTENT = 'ssc'; + private const string SUB_ACTION_SELECT_TYPE = 'st'; + private const string SUB_ACTION_SELECT_CONTENT = 'sc'; + private const string SUB_ACTION_SAVE_CONTENT = 'sac'; + private const string SUB_ACTION_SAVE_SUB_CONTENT = 'ssc'; public function __construct( private readonly \ilCtrl $ctrl, @@ -64,12 +64,12 @@ public function doAction( string $action ): Async { return match($action) { - self::STEP_SELECT_TYPE => $this->buildPromptShowAsync( + self::SUB_ACTION_SELECT_TYPE => $this->buildPromptShowAsync( $this->buildSelectTypeForm() ), - self::STEP_SELECT_CONTENT => $this->processSelectTypeForm(), - self::STEP_SAVE_CONTENT => $this->processSelectContentForm(), - self::STEP_SAVE_SUB_CONTENT => $this->processSelectSubContentForm(), + self::SUB_ACTION_SELECT_CONTENT => $this->processSelectTypeForm(), + self::SUB_ACTION_SAVE_CONTENT => $this->processSelectContentForm(), + self::SUB_ACTION_SAVE_SUB_CONTENT => $this->processSelectSubContentForm(), default => $this->forwardActionToActors($action) }; } @@ -77,8 +77,8 @@ public function doAction( private function buildPrompt(): Prompt { return $this->environment->getUIFactory()->prompt()->standard( - $this->environment->withStepParameter( - self::STEP_SELECT_TYPE + $this->environment->withSubActionParameter( + self::SUB_ACTION_SELECT_TYPE )->getUrlBuilder()->buildURI() ); } @@ -147,8 +147,8 @@ private function buildSelectTypeForm(): StandardForm $uf = $this->environment->getUIFactory(); return $uf->input()->container()->form()->standard( - $this->environment->withStepParameter( - self::STEP_SELECT_CONTENT + $this->environment->withSubActionParameter( + self::SUB_ACTION_SELECT_CONTENT )->getUrlBuilder()->buildURI()->__toString(), [ 'type' => $this->buildTypeSelect() @@ -191,8 +191,8 @@ private function buildSelectContentForm( $uf = $this->environment->getUIFactory(); $form = $uf->input()->container()->form()->standard( - $this->environment->withStepParameter( - self::STEP_SAVE_CONTENT + $this->environment->withSubActionParameter( + self::SUB_ACTION_SAVE_CONTENT )->getUrlBuilder()->buildURI()->__toString(), [ 'content' => $inputs_builder->getInputs() @@ -252,8 +252,8 @@ private function buildSelectSubContentForm( $uf = $this->environment->getUIFactory(); $form = $uf->input()->container()->form()->standard( - $this->environment->withStepParameter( - self::STEP_SAVE_SUB_CONTENT + $this->environment->withSubActionParameter( + self::SUB_ACTION_SAVE_SUB_CONTENT )->getUrlBuilder()->buildURI()->__toString(), [ 'content' => $inputs_builder->getInputs() @@ -353,7 +353,7 @@ private function buildRedirectToOverviewAsync(): Async return $this->environment->getPresentationFactory()->getAsync( $this->environment->getUIFactory()->prompt()->state()->redirect( $this->environment - ->withDefaultStep() + ->withDefaultSubAction() ->getUrlBuilder() ->buildURI() ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php index 6a5dcba24eab..2f5fb13cf000 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php @@ -44,11 +44,11 @@ class FeedbackOverviewTable implements OverviewTable { - private const string STEP_ENTER_FEEDBACK = 'ef'; - private const string STEP_EDIT_FEEDBACK = 'edf'; - private const string STEP_CONFIRM_DELETE_FEEDBACK = 'cdf'; - private const string STEP_DELETE_FEEDBACK = 'df'; - private const string STEP_SAVE_FEEDBACK = 'sf'; + private const string SUB_ACTION_ENTER_FEEDBACK = 'ef'; + private const string SUB_ACTION_EDIT_FEEDBACK = 'edf'; + private const string SUB_ACTION_CONFIRM_DELETE_FEEDBACK = 'cdf'; + private const string SUB_ACTION_DELETE_FEEDBACK = 'df'; + private const string SUB_ACTION_SAVE_FEEDBACK = 'sf'; public function __construct( private readonly UuidFactory $uuid_factory, @@ -79,8 +79,8 @@ public function getCreateModal( ) )->withRequired(true) ], - $environment->withStepParameter( - self::STEP_ENTER_FEEDBACK + $environment->withSubActionParameter( + self::SUB_ACTION_ENTER_FEEDBACK )->getUrlBuilder()->buildURI()->__toString() )->withSubmitLabel($lng->txt('next')); } @@ -111,23 +111,23 @@ public function doAction( string $action ): Async|RoundTripModal|Feedback { return match($action) { - self::STEP_ENTER_FEEDBACK => $this->processSelectGapModal( + self::SUB_ACTION_ENTER_FEEDBACK => $this->processSelectGapModal( $environment, $feedback ), - self::STEP_EDIT_FEEDBACK => $this->editFeedback( + self::SUB_ACTION_EDIT_FEEDBACK => $this->editFeedback( $environment->withPreservedTableRowIdsParameter(), $feedback ), - self::STEP_SAVE_FEEDBACK => $this->processEnterFeedbackModal( + self::SUB_ACTION_SAVE_FEEDBACK => $this->processEnterFeedbackModal( $environment, $feedback ), - self::STEP_CONFIRM_DELETE_FEEDBACK => $this->confirmDeleteFeedback( + self::SUB_ACTION_CONFIRM_DELETE_FEEDBACK => $this->confirmDeleteFeedback( $environment, $feedback ), - self::STEP_DELETE_FEEDBACK => $this->deleteFeedback( + self::SUB_ACTION_DELETE_FEEDBACK => $this->deleteFeedback( $environment, $feedback ) @@ -174,8 +174,8 @@ private function buildEnterFeedbackModal( [ 'feedback' => $inputs_builder->getInputs() ], - $environment->withStepParameter( - self::STEP_SAVE_FEEDBACK + $environment->withSubActionParameter( + self::SUB_ACTION_SAVE_FEEDBACK )->getUrlBuilder()->buildURI()->__toString() ); } @@ -267,7 +267,7 @@ private function confirmDeleteFeedback( $lng->txt('confirm'), $lng->txt('confirm_delete_feedback'), $environment - ->withStepParameter(self::STEP_DELETE_FEEDBACK) + ->withSubActionParameter(self::SUB_ACTION_DELETE_FEEDBACK) ->getUrlBuilder() ->buildURI() ->__toString() @@ -486,15 +486,15 @@ private function getActions( return [ 'edit' => $af->single( $lng->txt('edit'), - $environment->withStepParameter( - self::STEP_EDIT_FEEDBACK + $environment->withSubActionParameter( + self::SUB_ACTION_EDIT_FEEDBACK )->getUrlBuilder(), $environment->getTableRowIdToken() )->withAsync(true), 'delete' => $af->single( $lng->txt('delete'), - $environment->withStepParameter( - self::STEP_CONFIRM_DELETE_FEEDBACK + $environment->withSubActionParameter( + self::SUB_ACTION_CONFIRM_DELETE_FEEDBACK )->getUrlBuilder(), $environment->getTableRowIdToken() )->withAsync(true), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index 9f49ed4d87ba..4a7073ce6dbd 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -99,21 +99,21 @@ private function getActions(): array 'edit_gaps' => $taf->standard( $this->environment->getLanguage()->txt('edit_gaps'), $this->environment - ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_GAP_TYPES) + ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_answer_options' => $taf->standard( $this->environment->getLanguage()->txt('edit_answer_options'), $this->environment - ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_ANSWER_OPTIONS) + ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ), 'edit_points' => $taf->standard( $this->environment->getLanguage()->txt('edit_available_points'), $this->environment - ->withStepParameter(EditGaps::STEP_JUMP_TO_ASSIGN_POINTS) + ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_ASSIGN_POINTS) ->getUrlBuilder(), $this->environment->getTableRowIdToken() ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 43f1dddf8cef..30f00be758ac 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -262,7 +262,7 @@ public function buildPointsInputs( ), 'points' => $field_factory->numeric( $lng->txt('points') - )->withStepSize(0.01) + )->withSubActionSize(0.01) ->withRequired(true) ->withValue($this->getAvailablePoints()) ], diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php index 30d180d52c8e..ab390aecd6a8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -27,7 +27,7 @@ class EditCombinations { - private const string STEP_EDIT_COMBINATIONS_OVERVIEW = 'eco'; + private const string SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW = 'eco'; private const string LANG_VAR_EDIT_COMBINATIONS = 'edit_combinations'; @@ -40,7 +40,7 @@ public function addCombinationsSubTab( Environment $environment ): void { $environment->addEditAnswerFormSubTab( - self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW, self::LANG_VAR_EDIT_COMBINATIONS ); } @@ -49,19 +49,19 @@ public function show( Environment $environment ): Async|Renderable|Properties { $environment->addEditAnswerFormSubTab( - self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW, self::LANG_VAR_EDIT_COMBINATIONS ); $environment->activateEditAnswerFormSubTab( - self::STEP_EDIT_COMBINATIONS_OVERVIEW + self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW ); $combinations_overview = $this->buildOverview($environment); - $step = $environment->getStep(); - if ($step === self::STEP_EDIT_COMBINATIONS_OVERVIEW - || $step === '') { + $sub_action = $environment->getSubAction(); + if ($sub_action === self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW + || $sub_action === '') { return $combinations_overview; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index df307a5c7ddf..4e6ba7467a89 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -40,11 +40,11 @@ class Overview implements DataRetrieval, Renderable { - private const string STEP_SAVE = 's'; - private const string STEP_SET_COMBINATION_VALUES = 'scv'; - private const string STEP_JUMP_TO_SET_COMBINATION_VALUES = 'jscv'; - private const string STEP_DELETE_COMBINATION = 'dc'; - private const string STEP_CONFIRM_DELETE_COMBINATION = 'cdc'; + private const string SUB_ACTION_SAVE = 's'; + private const string SUB_ACTION_SET_COMBINATION_VALUES = 'scv'; + private const string SUB_ACTION_JUMP_TO_SET_COMBINATION_VALUES = 'jscv'; + private const string SUB_ACTION_DELETE_COMBINATION = 'dc'; + private const string SUB_ACTION_CONFIRM_DELETE_COMBINATION = 'cdc'; private ?RoundTripModal $modal = null; @@ -102,10 +102,10 @@ public function getTotalRowCount( public function doAction(): Async|self|Properties { - return match ($this->environment->getStep()) { - self::STEP_SET_COMBINATION_VALUES => $this->processSetCombinationGapsModal(), - self::STEP_DELETE_COMBINATION => $this->deleteCombination(), - self::STEP_SAVE => $this->processSetCombinationValues(), + return match ($this->environment->getSubAction()) { + self::SUB_ACTION_SET_COMBINATION_VALUES => $this->processSetCombinationGapsModal(), + self::SUB_ACTION_DELETE_COMBINATION => $this->deleteCombination(), + self::SUB_ACTION_SAVE => $this->processSetCombinationValues(), default => $this->buildAction() }; } @@ -137,14 +137,14 @@ private function getActions(): array $af->single( $this->environment->getLanguage()->txt('edit'), $this->environment - ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) + ->withSubActionParameter(self::SUB_ACTION_JUMP_TO_SET_COMBINATION_VALUES) ->getUrlBuilder(), $this->environment->getTableRowIdToken() )->withAsync(true), $af->single( $this->environment->getLanguage()->txt('delete'), $this->environment - ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) + ->withSubActionParameter(self::SUB_ACTION_CONFIRM_DELETE_COMBINATION) ->getUrlBuilder(), $this->environment->getTableRowIdToken() )->withAsync(true) @@ -163,12 +163,12 @@ private function buildAction(): Async } return $this->environment->getPresentationFactory()->getAsync( - match ($this->environment->getStep()) { - self::STEP_JUMP_TO_SET_COMBINATION_VALUES => + match ($this->environment->getSubAction()) { + self::SUB_ACTION_JUMP_TO_SET_COMBINATION_VALUES => $this->buildSetCombinationValuesModal( $this->buildInputsBuilder($affected_item) ), - self::STEP_CONFIRM_DELETE_COMBINATION => + self::SUB_ACTION_CONFIRM_DELETE_COMBINATION => $this->confirmDeleteCombination($affected_item) } ); @@ -213,7 +213,7 @@ private function buildSetCombinationGapsModal(): RoundTripModal ) ], $this->environment - ->withStepParameter(self::STEP_SET_COMBINATION_VALUES) + ->withSubActionParameter(self::SUB_ACTION_SET_COMBINATION_VALUES) ->getUrlBuilder() ->buildURI() ->__toString() @@ -257,7 +257,7 @@ private function buildSetCombinationValuesModal( [ 'values_awarding_points' => $inputs_builder->getInputs() ], - $this->environment->withStepParameter(self::STEP_SAVE) + $this->environment->withSubActionParameter(self::SUB_ACTION_SAVE) ->getUrlBuilder() ->buildURI() ->__toString() @@ -286,8 +286,8 @@ private function confirmDeleteCombination( return $this->environment->getUIFactory()->modal()->interruptive( $this->environment->getLanguage()->txt('confirm'), $this->environment->getLanguage()->txt('delete_combination'), - $this->environment->withStepParameter( - self::STEP_DELETE_COMBINATION + $this->environment->withSubActionParameter( + self::SUB_ACTION_DELETE_COMBINATION )->getUrlBuilder() ->withParameter( $this->environment->getTableRowIdToken(), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index 51b97f66414c..a92d3cd373ed 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -33,7 +33,7 @@ class Numeric extends Type { - private const float DEFAULT_STEP_SIZE = 0.0001; + private const float DEFAULT_SUB_ACTION_SIZE = 0.0001; public function __construct( Refinery $refinery, @@ -77,16 +77,16 @@ public function getEditAnswerOptionsInputs( $ff = $this->ui_factory->input()->field(); return [ 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) - ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withRequired(true) ->withValue($answer_option->getLowerLimit()), 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) - ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withValue($answer_option->getUpperLimit()), 'step_size' => $ff->numeric($this->lng->txt('step_size')) ->withStepSize(0.000001) ->withRequired(true) - ->withValue($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withValue($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ]; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 81c56857e1e9..03cad58d1128 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -37,10 +37,10 @@ class Edit implements EditViewInterface { - private const string STEP_EDIT_BASIC_PROPERTIES = 'ebp'; - private const string STEP_PROCESS_BASIC_PROPERTIES = 'pbp'; - private const string STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES = 'altbp'; - private const string STEP_CONFIRMED_GAP_REMOVAL = 'cgr'; + private const string SUB_ACTION_EDIT_BASIC_PROPERTIES = 'ebp'; + private const string SUB_ACTION_PROCESS_BASIC_PROPERTIES = 'pbp'; + private const string SUB_ACTION_ADD_LEGACY_TEXT_BASIC_PROPERTIES = 'altbp'; + private const string SUB_ACTION_CONFIRMED_GAP_REMOVAL = 'cgr'; public function __construct( private readonly PropertiesFactory $properties_factory, @@ -53,16 +53,16 @@ public function __construct( public function create( Environment $environment ): EditForm|Properties { - $step = $environment->getStep(); + $sub_action = $environment->getSubAction(); - return match($step) { + return match($sub_action) { '' => $this->startEditing($environment), - self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + self::SUB_ACTION_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( $environment ), default => $this->forwardCmdToEditGaps( $environment, - $step + $sub_action ) }; } @@ -71,34 +71,34 @@ public function create( public function edit( Environment $environment ): EditOverview|EditForm|Properties { - $step = $environment->getStep(); + $sub_action = $environment->getSubAction(); $combinations = $environment->getAnswerFormProperties()->getCombinations(); if ($combinations->areCombinationsEnabled()) { $combinations->getEditView()->addCombinationsSubTab($environment); } - if ($step === '') { + if ($sub_action === '') { return $environment->getPresentationFactory()->getEditOverview( $environment, - $environment->withStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + $environment->withSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) ->getUrlBuilder() ); } $environment->setEditAnswerFormBackTarget(); - return match ($step) { - self::STEP_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), - self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => + return match ($sub_action) { + self::SUB_ACTION_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), + self::SUB_ACTION_ADD_LEGACY_TEXT_BASIC_PROPERTIES => $this->addLegacyTextToBasicProperties($environment), - self::STEP_CONFIRMED_GAP_REMOVAL, - self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + self::SUB_ACTION_CONFIRMED_GAP_REMOVAL, + self::SUB_ACTION_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( $environment->withPreservedTableRowIdsParameter() ), default => $this->forwardCmdToEditGaps( $environment->withPreservedTableRowIdsParameter(), - $step + $sub_action ) }; } @@ -130,9 +130,9 @@ private function startEditing( private function forwardCmdToEditGaps( Environment $environment, - string $step + string $sub_action ): EditForm|Properties { - $processed_form = $this->edit_gaps->call($environment, $step); + $processed_form = $this->edit_gaps->call($environment, $sub_action); if (is_string($processed_form)) { $inputs_builder = $this->buildInputsBuilderForBasicInputs( $environment, @@ -168,8 +168,8 @@ private function buildBasicEditingForm( && $properties->getLegacyClozeText() !== '' && $properties->getClozeText()->getRawRepresentation() === '') { return $editing_form->withInsertLegacyTextsButton( - $environment->withStepParameter( - self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES + $environment->withSubActionParameter( + self::SUB_ACTION_ADD_LEGACY_TEXT_BASIC_PROPERTIES )->getUrlBuilder() ); } @@ -218,7 +218,7 @@ private function processBasicEditingForm( $new_gaps = $data->getGaps(); $old_gaps = $environment->getAnswerFormProperties()->getGaps(); - if ($environment->getStep() !== self::STEP_CONFIRMED_GAP_REMOVAL) { + if ($environment->getSubAction() !== self::SUB_ACTION_CONFIRMED_GAP_REMOVAL) { $removed_gaps = $new_gaps->getRemovedGaps($old_gaps); if ($removed_gaps !== []) { return $form->withConfirmation( @@ -250,7 +250,7 @@ private function buildEditFormForBasicInputs( return $environment->getPresentationFactory()->getEditForm( $inputs_builder, $environment - ->withStepParameter(self::STEP_PROCESS_BASIC_PROPERTIES) + ->withSubActionParameter(self::SUB_ACTION_PROCESS_BASIC_PROPERTIES) ->getUrlBuilder(), null, false @@ -297,8 +297,8 @@ private function buildRemovedGapsConfirmation( return $environment->getUIFactory()->modal()->interruptive( $environment->getLanguage()->txt('confirm'), $environment->getLanguage()->txt('confirm_remove_gaps'), - $environment->withStepParameter( - self::STEP_CONFIRMED_GAP_REMOVAL + $environment->withSubActionParameter( + self::SUB_ACTION_CONFIRMED_GAP_REMOVAL )->getUrlBuilder()->buildURI()->__toString() )->withAffectedItems( array_map( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php index 5a7eb2004313..0b20d61d42d7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -32,19 +32,19 @@ class EditGaps { - private const string STEP_BACK_TO_EDIT_BASIC_PROPERTIES = 'bebp'; - private const string STEP_SET_GAP_TYPES = 'sgt'; - private const string STEP_BACK_TO_SET_GAP_TYPES = 'bsgt'; - public const string STEP_JUMP_TO_SET_GAP_TYPES = 'jsgt'; - private const string STEP_SET_ANSWER_OPTIONS = 'sao'; - private const string STEP_BACK_TO_SET_ANSWER_OPTIONS = 'bsao'; - public const string STEP_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; - private const string STEP_ASSIGN_POINTS = 'ap'; - public const string STEP_JUMP_TO_ASSIGN_POINTS = 'jap'; - private const string STEP_SAVE = 's'; - - private string $step; - private ?string $start_step; + private const string SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES = 'bebp'; + private const string SUB_ACTION_SET_GAP_TYPES = 'sgt'; + private const string SUB_ACTION_BACK_TO_SET_GAP_TYPES = 'bsgt'; + public const string SUB_ACTION_JUMP_TO_SET_GAP_TYPES = 'jsgt'; + private const string SUB_ACTION_SET_ANSWER_OPTIONS = 'sao'; + private const string SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS = 'bsao'; + public const string SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; + private const string SUB_ACTION_ASSIGN_POINTS = 'ap'; + public const string SUB_ACTION_JUMP_TO_ASSIGN_POINTS = 'jap'; + private const string SUB_ACTION_SAVE = 's'; + + private string $sub_action; + private ?string $start_sub_action; public function __construct( private readonly PropertiesFactory $properties_factory, @@ -54,52 +54,52 @@ public function __construct( public function call( Environment $environment, - string $step = self::STEP_SET_GAP_TYPES + string $sub_action = self::SUB_ACTION_SET_GAP_TYPES ): EditForm|Properties|string { - $step_array = explode('_', $step); - $this->step = $step_array[0]; - $this->start_step = $this->determineStartStepFromStep( - $step_array[1] ?? null + $sub_action_array = explode('_', $sub_action); + $this->sub_action = $sub_action_array[0]; + $this->start_sub_action = $this->determineStartSubActionFromSubAction( + $sub_action_array[1] ?? null ); - return match ($this->step) { - self::STEP_SET_GAP_TYPES, - self::STEP_JUMP_TO_SET_GAP_TYPES + return match ($this->sub_action) { + self::SUB_ACTION_SET_GAP_TYPES, + self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesFormWithCarry( $environment, $environment->getAnswerFormProperties() ), - self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + self::SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES => $this->backToEditBasicProperties( $environment ), - self::STEP_BACK_TO_SET_GAP_TYPES + self::SUB_ACTION_BACK_TO_SET_GAP_TYPES => $this->backToGapTypesForm( $environment ), - self::STEP_SET_ANSWER_OPTIONS + self::SUB_ACTION_SET_ANSWER_OPTIONS => $this->forwardToAnswerOptionsForm( $environment ), - self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsFormWithCarry( $environment, $environment->getAnswerFormProperties() ), - self::STEP_BACK_TO_SET_ANSWER_OPTIONS + self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS => $this->backToSetAnswerOptionsForm( $environment ), - self::STEP_ASSIGN_POINTS + self::SUB_ACTION_ASSIGN_POINTS => $this->forwardToAssignPointsForm( $environment ), - self::STEP_JUMP_TO_ASSIGN_POINTS + self::SUB_ACTION_JUMP_TO_ASSIGN_POINTS => $this->buildAssignPointsFormWithCarry( $environment, $environment->getAnswerFormProperties() ), - self::STEP_SAVE + self::SUB_ACTION_SAVE => $this->processAssignPointsForm( $environment ) @@ -162,14 +162,14 @@ private function buildGapTypesForm( $inputs_builder, $this->buildPostTarget( $environment, - self::STEP_SET_ANSWER_OPTIONS + self::SUB_ACTION_SET_ANSWER_OPTIONS ), - $this->step === self::STEP_JUMP_TO_SET_GAP_TYPES - || $this->step === $this->start_step + $this->sub_action === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES + || $this->sub_action === $this->start_sub_action ? null : $this->buildPostTarget( $environment, - self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + self::SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES ), false )->withContentBeforeForm( @@ -289,14 +289,14 @@ private function buildAnswerOptionsForm( $inputs_builder, $this->buildPostTarget( $environment, - self::STEP_ASSIGN_POINTS + self::SUB_ACTION_ASSIGN_POINTS ), - $this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS - || $this->step === $this->start_step + $this->sub_action === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS + || $this->sub_action === $this->start_sub_action ? null : $this->buildPostTarget( $environment, - self::STEP_BACK_TO_SET_GAP_TYPES + self::SUB_ACTION_BACK_TO_SET_GAP_TYPES ), false )->withContentBeforeForm( @@ -401,13 +401,13 @@ private function buildAssignPointsForm( $inputs_builder, $this->buildPostTarget( $environment, - self::STEP_SAVE + self::SUB_ACTION_SAVE ), - $this->step === self::STEP_JUMP_TO_ASSIGN_POINTS + $this->sub_action === self::SUB_ACTION_JUMP_TO_ASSIGN_POINTS ? null : $this->buildPostTarget( $environment, - self::STEP_BACK_TO_SET_ANSWER_OPTIONS + self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS ), true )->withContentBeforeForm( @@ -474,26 +474,26 @@ private function buildPostTarget( Environment $environment, string $next_step ): URLBuilder { - if ($this->start_step !== null) { - $next_step = "{$next_step}_{$this->start_step}"; + if ($this->start_sub_action !== null) { + $next_step = "{$next_step}_{$this->start_sub_action}"; } - return $environment->withStepParameter($next_step)->getUrlBuilder(); + return $environment->withSubActionParameter($next_step)->getUrlBuilder(); } - private function determineStartStepFromStep( - ?string $start_step_from_get + private function determineStartSubActionFromSubAction( + ?string $start_sub_action_from_get ): ?string { - if ($start_step_from_get !== null) { - return $start_step_from_get; + if ($start_sub_action_from_get !== null) { + return $start_sub_action_from_get; } - if ($this->step === self::STEP_JUMP_TO_SET_GAP_TYPES) { - return self::STEP_BACK_TO_SET_GAP_TYPES; + if ($this->sub_action === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) { + return self::SUB_ACTION_BACK_TO_SET_GAP_TYPES; } - if ($this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS) { - return self::STEP_BACK_TO_SET_ANSWER_OPTIONS; + if ($this->sub_action === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) { + return self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS; } return null; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php index 7b3ad84e661a..8dc766aadd26 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php @@ -25,7 +25,7 @@ interface Actor { public function can( - string $step + string $sub_action ): bool; public function do( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index e5d196d4f200..d0b6bd5805a6 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -38,7 +38,7 @@ class DefaultEnvironment implements Environment { private const array QUERY_PARAMETER_NAME_SPACE = ['q']; private const string TOKEN_STRING_ACTION = 'a'; - private const string TOKEN_STRING_STEP = 's'; + private const string TOKEN_STRING_SUB_ACTION = 's'; private const string TOKEN_STRING_QUESTION_ID = 'q'; private const string TOKEN_STRING_TABLE_ROW_ID = 'r'; private const string TOKEN_STRING_TYPE_HASH = 't'; @@ -54,12 +54,12 @@ class DefaultEnvironment implements Environment private ?Properties $answer_form_properties = null; - private bool $default_step = false; + private bool $default_sub_action = false; private bool $is_in_creation_context = false; private URLBuilder $url_builder; private readonly URLBuilderToken $action_token; - private readonly URLBuilderToken $step_token; + private readonly URLBuilderToken $sub_action_token; private readonly URLBuilderToken $question_id_token; private readonly URLBuilderToken $table_row_token; private ?URLBuilderToken $type_hash_token = null; @@ -120,13 +120,13 @@ public function setEditAnswerFormBackTarget(): void #[\Override] public function addEditAnswerFormSubTab( - string $step, + string $sub_action, string $language_variable ): void { $this->tabs_gui->addSubTab( - $step, + $sub_action, $this->lng->txt($language_variable), - $this->withStepParameter($step) + $this->withSubActionParameter($sub_action) ->withActionParameter(Edit::ACTION_OTHER_ANSWER_FORM) ->getUrlBuilder() ->buildURI() @@ -136,9 +136,9 @@ public function addEditAnswerFormSubTab( #[\Override] public function activateEditAnswerFormSubTab( - string $step + string $sub_action ): void { - $this->tabs_gui->activateSubTab($step); + $this->tabs_gui->activateSubTab($sub_action); } #[\Override] @@ -154,29 +154,29 @@ public function getUrlBuilder(): URLBuilder } #[\Override] - public function withStepParameter( - string $step + public function withSubActionParameter( + string $sub_action ): self { $clone = clone $this; $clone->url_builder = $this->url_builder - ->withParameter($this->step_token, $step); + ->withParameter($this->sub_action_token, $sub_action); return $clone; } #[\Override] - public function withDefaultStep(): self + public function withDefaultSubAction(): self { $clone = clone $this; - $clone->default_step = true; + $clone->default_sub_action = true; return $clone; } #[\Override] - public function getStep(): string + public function getSubAction(): string { - return $this->default_step + return $this->default_sub_action ? '' - : $this->retrieveStringValueForToken($this->step_token, self::TOKEN_STRING_STEP); + : $this->retrieveStringValueForToken($this->sub_action_token, self::TOKEN_STRING_SUB_ACTION); } #[\Override] @@ -462,7 +462,7 @@ public function setEditAnswerFormTabs( $this->tabs_gui->addTab( self::TAB_ID_ANSWER_FORM, $this->lng->txt('answer_form'), - $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + $this->withDefaultSubAction()->getUrlBuilder()->buildURI()->__toString() ); foreach ($additional_actions as $action) { @@ -476,7 +476,7 @@ public function setEditAnswerFormTabs( $this->tabs_gui->addSubTab( self::TAB_ID_ANSWER_FORM, $this->lng->txt('overview'), - $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + $this->withDefaultSubAction()->getUrlBuilder()->buildURI()->__toString() ); $this->tabs_gui->activateTab(self::TAB_ID_ANSWER_FORM); @@ -539,7 +539,7 @@ private function setQuestionIdParamterForPageEditorCmds( private function buildEditAnswerFormBackUrl(): URLBuilder { if (!$this->is_in_creation_context) { - return $this->withDefaultStep()->getUrlBuilder(); + return $this->withDefaultSubAction()->getUrlBuilder(); } if (!$this->isCreateModeSimple()) { @@ -557,7 +557,7 @@ private function buildEditAnswerFormBackUrl(): URLBuilder $this->action_token, Edit::ACTION_DELETE_QUESTIONS )->withParameter( - $this->step_token, + $this->sub_action_token, Edit::ACTION_DELETE_QUESTIONS )->withParameter( $this->table_row_token, @@ -571,14 +571,14 @@ private function acquireURLBuilderAndParameters( [ $this->url_builder, $this->action_token, - $this->step_token, + $this->sub_action_token, $this->question_id_token, $this->table_row_token ] = (new URLBuilder($base_uri)) ->acquireParameters( self::QUERY_PARAMETER_NAME_SPACE, self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_STEP, + self::TOKEN_STRING_SUB_ACTION, self::TOKEN_STRING_QUESTION_ID, self::TOKEN_STRING_TABLE_ROW_ID ); diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index cec5b7e35a82..f8ca23f7e1ee 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -43,12 +43,12 @@ public function getUIFactory(): UIFactory; public function setEditAnswerFormBackTarget(): void; public function addEditAnswerFormSubTab( - string $step, + string $sub_action, string $text ): void; public function activateEditAnswerFormSubTab( - string $step + string $sub_action ): void; public function getPresentationFactory(): Factory; @@ -59,9 +59,9 @@ public function getTableRowIdToken(): URLBuilderToken; public function getTableRowIds(): array; - public function getStep(): string; + public function getSubAction(): string; - public function withDefaultStep(): self; + public function withDefaultSubAction(): self; public function getEditability(): Editability; @@ -85,8 +85,8 @@ public function withAnswerFormProperties( Properties $properties ): self; - public function withStepParameter( - string $step + public function withSubActionParameter( + string $sub_action ): self; public function withPreservedTableRowIdsParameter(): self; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php b/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php index e3ee24e168c6..7aa22a75af85 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Tools/UploadHandler.php @@ -35,9 +35,9 @@ class UploadHandler implements UploadHandlerInterface, Actor { - private const string STEP_UPLOAD = 'uhu'; - private const string STEP_REMOVE = 'uhr'; - private const string STEP_INFO = 'uhi'; + private const string SUB_ACTION_UPLOAD = 'uhu'; + private const string SUB_ACTION_REMOVE = 'uhr'; + private const string SUB_ACTION_INFO = 'uhi'; private bool $is_chunked = false; private int $chunk_index = 0; @@ -65,7 +65,7 @@ public function getFileIdentifierParameterName(): string public function getUploadURL(): string { return $this->environment - ->withStepParameter(self::STEP_UPLOAD) + ->withSubActionParameter(self::SUB_ACTION_UPLOAD) ->getUrlBuilder() ->buildURI() ->__toString(); @@ -75,7 +75,7 @@ public function getUploadURL(): string public function getFileRemovalURL(): string { return $this->environment - ->withStepParameter(self::STEP_REMOVE) + ->withSubActionParameter(self::SUB_ACTION_REMOVE) ->getUrlBuilder() ->buildURI() ->__toString(); @@ -85,7 +85,7 @@ public function getFileRemovalURL(): string public function getExistingFileInfoURL(): string { return $this->environment - ->withStepParameter(self::STEP_INFO) + ->withSubActionParameter(self::SUB_ACTION_INFO) ->getUrlBuilder() ->buildURI() ->__toString(); @@ -132,13 +132,13 @@ public function supportsChunkedUploads(): bool #[\Override] public function can( - string $step + string $sub_action ): bool { $has_file_identifier = $this->hasFileIdentifier(); - return $step === self::STEP_UPLOAD - || $step === self::STEP_REMOVE && $has_file_identifier - || $step === self::STEP_INFO && $has_file_identifier; + return $sub_action === self::SUB_ACTION_UPLOAD + || $sub_action === self::SUB_ACTION_REMOVE && $has_file_identifier + || $sub_action === self::SUB_ACTION_INFO && $has_file_identifier; } #[\Override] @@ -146,9 +146,9 @@ public function do( string $action ): Async { $response = match($action) { - self::STEP_UPLOAD => $this->upload(), - self::STEP_REMOVE => $this->remove(), - self::STEP_INFO => $this->info(), + self::SUB_ACTION_UPLOAD => $this->upload(), + self::SUB_ACTION_REMOVE => $this->remove(), + self::SUB_ACTION_INFO => $this->info(), default => '' }; diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 42e0cd757b7b..68f1f603d654 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -307,7 +307,7 @@ private function createQuestion( $this->initializeEditMode($environment); $this->tabs_gui->setBackTarget( $this->lng->txt('cancel'), - $environment->withDefaultStep()->getUrlBuilder() + $environment->withDefaultSubAction()->getUrlBuilder() ->buildURI() ->__toString() ); @@ -343,7 +343,7 @@ private function editQuestion( $this->initializeEditMode($environment); $this->tabs_gui->setBackTarget( $this->lng->txt('back'), - $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + $environment->withDefaultSubAction()->getUrlBuilder()->buildURI()->__toString() ); $question_id = $environment->getQuestionId(); @@ -368,7 +368,7 @@ private function editQuestion( $this->questions_repository->update([$edit]); return $this->buildEditStartView( $environment_with_question_parameter - ->withDefaultStep() + ->withDefaultSubAction() ->withActionParameter(self::ACTION_EDIT_QUESTION), $edit ); @@ -387,7 +387,7 @@ private function deleteQuestions( ); } - if ($environment->getStep() === self::ACTION_DELETE_QUESTIONS) { + if ($environment->getSubAction() === self::ACTION_DELETE_QUESTIONS) { $this->deleteSelectedQuestions($question_ids); $this->ctrl->redirectToURL( $environment->getUrlBuilder()->buildURI()->__toString() @@ -400,7 +400,7 @@ private function deleteQuestions( $this->lng->txt('qpl_confirm_delete_questions'), $environment->withActionParameter( self::ACTION_DELETE_QUESTIONS - )->withStepParameter( + )->withSubActionParameter( self::ACTION_DELETE_QUESTIONS )->getUrlBuilder()->buildURI()->__toString() )->withAffectedItems( @@ -702,7 +702,7 @@ private function buildAfterQuestionCreationRedirectUri( ): string { if ($create_mode !== CreateModes::Simple) { return $environment - ->withDefaultStep() + ->withDefaultSubAction() ->withActionParameter(self::ACTION_EDIT_QUESTION) ->withQuestionIdParameter($question_uuid) ->getUrlBuilder() diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 72aa523a30b2..b510680fb04c 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -48,7 +48,7 @@ public function __construct( public function create( DefaultEnvironment $environment ): EditForm|Question { - return match ($environment->getStep()) { + return match ($environment->getSubAction()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesCreateForm( $environment ), @@ -60,7 +60,7 @@ public function edit( DefaultEnvironment $environment, Participant $participant_view ): EditForm|Question { - return match ($environment->getStep()) { + return match ($environment->getSubAction()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesEditingForm( $environment ), @@ -110,7 +110,7 @@ private function buildBasicPropertiesCreateForm( $environment->getLanguage()->txt('edit_basic_form_properties') ), $environment - ->withStepParameter(self::CMD_SAVE_QUESTION) + ->withSubActionParameter(self::CMD_SAVE_QUESTION) ->getUrlBuilder(), null, false @@ -148,7 +148,7 @@ private function buildBasicPropertiesEditingForm( $this->buildBasicPropertiesBasicValuesArray() ), $environment - ->withStepParameter(self::CMD_SAVE_QUESTION) + ->withSubActionParameter(self::CMD_SAVE_QUESTION) ->getUrlBuilder(), null, true From 6f1e90ce34a54ebd38e3bc5973535c9f73332848 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Sat, 4 Apr 2026 11:20:13 +0200 Subject: [PATCH 087/108] Questions: Move AnswerOptionsUpload to Actor --- .../ILIAS/Questions/Legacy/LocalDIC.php | 1 + .../Questions/src/AnswerForm/Views/Edit.php | 4 +- .../Cloze/Properties/Gaps/Gap.php | 16 +- .../Cloze/Properties/Gaps/Gaps.php | 25 +-- .../Cloze/Properties/Gaps/LongMenu.php | 10 +- .../Cloze/Properties/Gaps/Numeric.php | 6 +- .../Cloze/Properties/Gaps/Select.php | 4 + .../Cloze/Properties/Gaps/Text.php | 4 + .../Cloze/Properties/Gaps/Type.php | 4 + .../src/AnswerFormTypes/Cloze/Views/Edit.php | 12 +- .../AnswerFormTypes/Cloze/Views/EditGaps.php | 23 ++- .../Cloze/Views/UploadAnswerOptions.php | 195 ++++++++++++++---- 12 files changed, 226 insertions(+), 78 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 9c6028934fb7..5f98709587e6 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -220,6 +220,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps => new Cloze\Views\EditGaps( + $DIC['upload'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] ); diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index ce0c6bf98fd0..a9d5d52f2588 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -31,11 +31,11 @@ interface Edit { public function create( Environment $environment - ): EditForm|Properties; + ): EditForm|Async|Properties; public function edit( Environment $environment - ): EditOverview|EditForm|Properties; + ): EditOverview|EditForm|Async|Properties; public function other( Environment $environment diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 76786fca4d76..95ec67be3919 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -27,7 +27,9 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -310,12 +312,16 @@ public function buildParticipantViewLegacyInput(): string } public function getEditAnswerOptionsSection( - Language $lng, - FieldFactory $ff + FileUpload $file_upload, + Environment $environment ): Section { - $section = $ff->section( - $this->getType()->getEditAnswerOptionsInputs($this), - "{$this->buildShortenedGapName()} ({$lng->txt("{$this->getType()->getIdentifier()}_gap")})" + $section = $environment->getUIFactory()->input()->field()->section( + $this->getType()->getEditAnswerOptionsInputs( + $file_upload, + $environment, + $this + ), + "{$this->buildShortenedGapName()} ({$environment->getLanguage()->txt("{$this->getType()->getIdentifier()}_gap")})" ); $edit_section_constraint = $this->getType()->getEditAnswerOptionsSectionConstraint(); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 30795d1a5b39..4bed2904d853 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -22,14 +22,16 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Junctor; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Operator; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -227,28 +229,27 @@ function (array $c, Gap $v) use ($ff, $available_gap_types): array { } public function buildAnswerOptionsInputs( - Language $lng, - FieldFactory $ff, + FileUpload $file_upload, + Environment $environment, Properties $properties, - bool $is_in_creation_context, - array $selected_gaps ): Section { - return $ff->section( + + return $environment->getUIFactory()->input()->field()->section( array_reduce( $this->retrieveGapsForInputs( - $is_in_creation_context, - $selected_gaps + $environment->isInCreationContext(), + $environment->getTableRowIds() ), - function (array $c, Gap $v) use ($lng, $ff): array { + function (array $c, Gap $v) use ($environment, $file_upload): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditAnswerOptionsSection( - $lng, - $ff + $file_upload, + $environment ); return $c; }, [] ), - $lng->txt('add_answer_options') + $environment->getLanguage()->txt('add_answer_options') )->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(array $vs): Properties => $properties->withGaps( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index 6659ca1413b2..b9e7e59f5563 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -22,7 +22,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Views\UploadAnswerOptions; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\FileUpload\MimeType; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -82,6 +85,8 @@ public function getParticipantViewLegacyInput( #[\Override] public function getEditAnswerOptionsInputs( + FileUpload $file_upload, + Environment $environment, Gap $gap ): array { $ff = $this->ui_factory->input()->field(); @@ -91,7 +96,10 @@ public function getEditAnswerOptionsInputs( [] )->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'upload_answer_options' => $ff->file( - new UploadAnswerOptionsGUI(), + new UploadAnswerOptions( + $file_upload, + $environment + ), $this->lng->txt('upload_answer_options'), $this->lng->txt('upload_answer_options_info') )->withAcceptedMimeTypes(self::ACCEPTED_MIME_TYPES), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index a92d3cd373ed..a16e242a15d2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,10 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\Definitions\Range; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -70,6 +72,8 @@ public function getParticipantViewLegacyInput( #[\Override] public function getEditAnswerOptionsInputs( + FileUpload $file_upload, + Environment $environment, Gap $gap ): array { $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 62a07425905d..3f616bdccb3b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -22,6 +22,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -90,6 +92,8 @@ public function getParticipantViewLegacyInput( #[\Override] public function getEditAnswerOptionsInputs( + FileUpload $file_upload, + Environment $environment, Gap $gap ): array { $ff = $this->ui_factory->input()->field(); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 2234e052ff44..3a55ba341ca4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -23,6 +23,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\Definitions\TextMatchingOptions; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -74,6 +76,8 @@ public function getParticipantViewLegacyInput( #[\Override] public function getEditAnswerOptionsInputs( + FileUpload $file_upload, + Environment $environment, Gap $gap ): array { $ff = $this->ui_factory->input()->field(); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index ba8cd0970b58..36fe05d28a1d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -22,8 +22,10 @@ use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types as FeedbackTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Definitions\Range; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -45,6 +47,8 @@ abstract public function getParticipantViewLegacyInput( ): string; abstract public function getEditAnswerOptionsInputs( + FileUpload $file_upload, + Environment $environment, Gap $gap ): array; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 03cad58d1128..73f2e5e53ede 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -52,7 +52,7 @@ public function __construct( #[\Override] public function create( Environment $environment - ): EditForm|Properties { + ): EditForm|Async|Properties { $sub_action = $environment->getSubAction(); return match($sub_action) { @@ -70,7 +70,7 @@ public function create( #[\Override] public function edit( Environment $environment - ): EditOverview|EditForm|Properties { + ): EditOverview|EditForm|Async|Properties { $sub_action = $environment->getSubAction(); $combinations = $environment->getAnswerFormProperties()->getCombinations(); @@ -131,8 +131,8 @@ private function startEditing( private function forwardCmdToEditGaps( Environment $environment, string $sub_action - ): EditForm|Properties { - $processed_form = $this->edit_gaps->call($environment, $sub_action); + ): EditForm|Async|Properties { + $processed_form = $this->edit_gaps->do($environment, $sub_action); if (is_string($processed_form)) { $inputs_builder = $this->buildInputsBuilderForBasicInputs( $environment, @@ -196,7 +196,7 @@ private function addLegacyTextToBasicProperties( private function processBasicEditingForm( Environment $environment - ): EditForm|Properties { + ): EditForm|Async|Properties { $inputs_builder = $this->buildInputsBuilderForBasicInputs( $environment, false, @@ -234,7 +234,7 @@ private function processBasicEditingForm( return $data; } - return $this->edit_gaps->call( + return $this->edit_gaps->do( $environment->withAnswerFormProperties( $data->withGaps( $data->getGaps()->withMarkedIncompleteGaps() diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php index 0b20d61d42d7..7c80f802a765 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -24,8 +24,10 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; +use ILIAS\FileUpload\FileUpload; use ILIAS\Refinery\Custom\Transformation as CustomTransformation; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\URLBuilder; @@ -47,21 +49,30 @@ class EditGaps private ?string $start_sub_action; public function __construct( + private readonly FileUpload $file_upload, private readonly PropertiesFactory $properties_factory, private readonly GapFactory $gap_factory ) { } - public function call( + public function do( Environment $environment, string $sub_action = self::SUB_ACTION_SET_GAP_TYPES - ): EditForm|Properties|string { + ): EditForm|Async|Properties|string { $sub_action_array = explode('_', $sub_action); $this->sub_action = $sub_action_array[0]; $this->start_sub_action = $this->determineStartSubActionFromSubAction( $sub_action_array[1] ?? null ); + $upload_handler = new UploadAnswerOptions( + $this->file_upload, + $environment + ); + if ($upload_handler->can($sub_action)) { + return $upload_handler->do($sub_action); + } + return match ($this->sub_action) { self::SUB_ACTION_SET_GAP_TYPES, self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES @@ -348,11 +359,9 @@ function (?string $carry) use ( ); return $properties_from_carry->getGaps() ->buildAnswerOptionsInputs( - $environment->getLanguage(), - $environment->getUIFactory()->input()->field(), - $properties_from_carry, - $environment->isInCreationContext(), - $environment->getTableRowIds() + $this->file_upload, + $environment, + $properties_from_carry ); } ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php index 94c0f296d8a9..08d3cd9d60cd 100755 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php @@ -18,24 +18,123 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; -use ILIAS\FileUpload\Handler\AbstractCtrlAwareUploadHandler; +use ILIAS\Questions\Presentation\Definitions\Actor; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\FileUpload\Handler\BasicFileInfoResult; use ILIAS\FileUpload\Handler\BasicHandlerResult; use ILIAS\FileUpload\Handler\FileInfoResult; use ILIAS\FileUpload\Handler\HandlerResult; use ILIAS\FileUpload\DTO\UploadResult; +use ILIAS\FileUpload\FileUpload; +use ILIAS\UI\Component\Input\Field\UploadHandler; -/** - * - * @author Stephan Kergomard - * @ilCtrl_isCalledBy ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI: ilObjQuestionsGUI - */ -class UploadAnswerOptionsGUI extends AbstractCtrlAwareUploadHandler +class UploadAnswerOptions implements UploadHandler, Actor { + private const string SUB_ACTION_UPLOAD = 'uhu'; + private const string SUB_ACTION_REMOVE = 'uhr'; + private const string SUB_ACTION_INFO = 'uhi'; + + public function __construct( + private readonly FileUpload $upload, + private readonly Environment $environment + ) { + } + + + #[\Override] + public function getFileIdentifierParameterName(): string + { + return self::DEFAULT_FILE_ID_PARAMETER; + } + + #[\Override] + public function getUploadURL(): string + { + return $this->environment + ->withSubActionParameter(self::SUB_ACTION_UPLOAD) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getFileRemovalURL(): string + { + return $this->environment + ->withSubActionParameter(self::SUB_ACTION_REMOVE) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getExistingFileInfoURL(): string + { + return $this->environment + ->withSubActionParameter(self::SUB_ACTION_INFO) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + #[\Override] + public function getInfoForExistingFiles( + array $file_ids + ): array { + return array_map( + fn($file_id): FileInfoResult => $this->getInfoResult($file_id), + $file_ids + ); + } + + #[\Override] + public function getInfoResult( + string $identifier + ): ?FileInfoResult { + return new BasicFileInfoResult( + $this->getFileIdentifierParameterName(), + $identifier, + 'unknown', + 0, + 'unknown' + ); + } + #[\Override] - protected function getUploadResult(): HandlerResult + public function supportsChunkedUploads(): bool + { + return false; + } + + #[\Override] + public function can( + string $sub_action + ): bool { + $has_file_identifier = $this->hasFileIdentifier(); + + return $sub_action === self::SUB_ACTION_UPLOAD + || $sub_action === self::SUB_ACTION_REMOVE && $has_file_identifier + || $sub_action === self::SUB_ACTION_INFO && $has_file_identifier; + } + + #[\Override] + public function do( + string $action + ): Async { + $response = match($action) { + self::SUB_ACTION_UPLOAD => $this->upload(), + self::SUB_ACTION_REMOVE => $this->remove(), + self::SUB_ACTION_INFO => $this->info(), + default => '' + }; + + return $this->environment->getPresentationFactory()->getAsync($response); + } + + private function upload(): string { $this->upload->process(); @@ -54,51 +153,59 @@ protected function getUploadResult(): HandlerResult $content = base64_encode(file_get_contents($result->getPath())); unlink($result->getPath()); - return new BasicHandlerResult( - $this->getFileIdentifierParameterName(), - HandlerResult::STATUS_OK, - $content, - 'file upload OK' + return json_encode( + new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $content, + 'file upload OK' + ) ); } - #[\Override] - protected function getRemoveResult( - string $identifier - ): HandlerResult { - return new BasicHandlerResult( - $this->getFileIdentifierParameterName(), - HandlerResult::STATUS_OK, - $identifier, - 'We just don\'t do anything here.' + private function remove(): string + { + return json_encode( + new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $this->retrieveFileIdentifier(), + 'We just don\'t do anything here.' + ) ); } - #[\Override] - public function getInfoResult( - string $identifier - ): ?FileInfoResult { - return new BasicFileInfoResult( - $this->getFileIdentifierParameterName(), - $identifier, - 'unknown', - 0, - 'unknown' + private function info(): string + { + return json_encode( + $this->getInfoResult( + $this->retrieveFileIdentifier() + ) ); } - #[\Override] - /** - * @return \ILIAS\FileUpload\Handler\BasicFileInfoResult[] - */ - public function getInfoForExistingFiles( - array $file_ids - ): array { - $info_results = []; - foreach ($file_ids as $identifier) { - $info_results[] = $this->getInfoResult($identifier); + private function hasFileIdentifier(): bool + { + return $this->environment + ->getHttpServices() + ->wrapper() + ->query() + ->has($this->getFileIdentifierParameterName()); + } + + private function retrieveFileIdentifier(): string + { + if (!$this->hasFileIdentifier()) { + return ''; } - return $info_results; + return $this->environment + ->getHttpServices() + ->wrapper() + ->query() + ->retrieve( + $this->getFileIdentifierParameterName(), + $this->environment->getRefinery()->kindlyTo()->string() + ); } } From c73cd15583e899e6b8aa521eca9768d711fcea8f Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Sat, 4 Apr 2026 11:54:35 +0200 Subject: [PATCH 088/108] Questions: Move ilDBConstants to FieldDefinition --- .../Capabilities/Feedback/Feedback.php | 13 +-- .../Capabilities/Feedback/Migration.php | 27 ++++--- .../Capabilities/Feedback/Repository.php | 3 +- .../Feedback/SpecificFeedback.php | 13 +-- .../SuggestedLearningContent/Migration.php | 13 +-- .../SuggestedLearningContent/Repository.php | 5 +- .../AnswerForm/Migration/MigrationInsert.php | 17 ++-- .../src/AnswerForm/TypeGenericProperties.php | 29 +++---- .../Migration/BasicMigrationFunctions.php | 39 ++++----- .../Cloze/Migration/MigrationCloze.php | 15 ++-- .../Properties/Combinations/Combination.php | 11 +-- .../Properties/Combinations/MatchingValue.php | 9 ++- .../Gaps/AnswerOptions/AnswerOption.php | 15 ++-- .../Gaps/AnswerOptions/AnswerOptions.php | 3 +- .../Cloze/Properties/Gaps/Gap.php | 19 ++--- .../Cloze/Properties/Gaps/Gaps.php | 7 +- .../Cloze/Properties/Properties.php | 15 ++-- .../ILIAS/Questions/src/Persistence/Value.php | 2 +- .../src/Question/Persistence/Repository.php | 11 +-- .../ILIAS/Questions/src/Question/Question.php | 57 ++++++------- .../src/Setup/ClozeQuestionTables.php | 61 +++++++------- .../src/Setup/OverarchingQuestionTables.php | 81 ++++++++++--------- .../src/Setup/QuestionsMigration.php | 31 +++---- 23 files changed, 259 insertions(+), 237 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php index ae8153ec3037..f01ba83be273 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php @@ -33,6 +33,7 @@ use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; use ILIAS\UI\Factory as UIFactory; @@ -262,23 +263,23 @@ private function buildReplaceForGenericFeedback( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $answer_form_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_best_response?->getRawRepresentation() ?? '' ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_best_response_legacy ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_other_response?->getRawRepresentation() ?? '' ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_other_response_legacy ) ] @@ -303,7 +304,7 @@ private function buildDeleteForSpecificFeedback( TableTypes::FeedbackSpecific ), new Value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $answer_form_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php index a081f533ba98..011b9b9b6e38 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php @@ -28,6 +28,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Database\FieldDefinition; use ILIAS\Setup\Environment; class Migration implements MigrationInterface @@ -83,7 +84,7 @@ private function fetchGenericFeedbackDBValues( ): \Generator { $query = $db->queryF( 'SELECT * FROM qpl_fb_generic WHERE question_fi = %s ORDER BY question_fi', - [\ilDBConstants::T_INTEGER], + [FieldDefinition::T_INTEGER], [$old_question_id] ); @@ -96,7 +97,7 @@ private function fetchSpecificFeedbackDBValues( ): \Generator { $query = $db->queryF( 'SELECT * FROM qpl_fb_specific WHERE question_fi = %s ORDER BY question_fi', - [\ilDBConstants::T_INTEGER], + [FieldDefinition::T_INTEGER], [$old_question_id] ); @@ -187,15 +188,15 @@ private function buildNewGenericFeedbackValuesFromOld( return [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $migration_insert->getAnswerFormId()->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, '' ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->sanitizeLegacyText( $migration_insert->getDb(), $feedback_best_response, @@ -203,11 +204,11 @@ private function buildNewGenericFeedbackValuesFromOld( ) ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, '' ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->sanitizeLegacyText( $migration_insert->getDb(), $feedback_other_response, @@ -297,27 +298,27 @@ private function buildNewSpecificFeedbackValuesFromOld( return array_map( fn(string $v): array => [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $migration_insert->getUuid()->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $migration_insert->getAnswerFormId()->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $parent_id ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $v ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, '' ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->sanitizeLegacyText( $migration_insert->getDb(), $feedback_for_question['feedback'], diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php index a86fb948d170..ac8e95d2c8da 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php @@ -31,6 +31,7 @@ use ILIAS\Data\Text\Factory as TextFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; class Repository @@ -64,7 +65,7 @@ public function getFor( TableTypes::FeedbackGeneric ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $answer_form_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php index 36c4e20b6767..d7a84116ca3c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php @@ -23,6 +23,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; class SpecificFeedback { @@ -75,27 +76,27 @@ public function toStorage( return [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->parent_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->condition ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_text->getRawRepresentation() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->feedback_legacy ) ]; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php index a762f1a67dd2..89c0147f5361 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php @@ -29,6 +29,7 @@ use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolution; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionFile; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; +use ILIAS\Database\FieldDefinition; use ILIAS\Setup\Environment; class Migration implements MigrationInterface @@ -108,15 +109,15 @@ private function buildInsertFromOldSuggestedSolution( ), [ $pf->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $migration_insert->getAnswerFormId()->toString() ), $pf->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $type ), $pf->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, json_encode($content) ), ] @@ -205,7 +206,7 @@ private function fetchQuestionParentAndOwnerFromDB( return $db->fetchObject( $db->queryF( 'SELECT obj_fi, owner FROM qpl_questions WHERE id = %s', - [\ilDBConstants::T_INTEGER], + [FieldDefinition::T_INTEGER], [$old_question_id] ) ); @@ -224,7 +225,7 @@ private function fetchTargetRefIdFromDB( . 'INNER JOIN object_reference' . PHP_EOL . 'ON lm_data.lm_id = object_reference.obj_id' . PHP_EOL . 'WHERE lm_data.obj_id = %s', - [\ilDBConstants::T_INTEGER], + [FieldDefinition::T_INTEGER], [$sub_object_id] ) )?->lm_id; @@ -236,7 +237,7 @@ private function fetchTargetRefIdFromDB( . 'INNER JOIN object_reference' . PHP_EOL . 'ON glossary_term.glo_id = object_reference.obj_id' . PHP_EOL . 'WHERE glossary_term.id = %s', - [\ilDBConstants::T_INTEGER], + [FieldDefinition::T_INTEGER], [$sub_object_id] ) )?->glo_id; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php index 83892b031141..234569797dc3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php @@ -29,6 +29,7 @@ use ILIAS\Data\Order as DataOrder; use ILIAS\Data\Range as DataRange; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; use ILIAS\ResourceStorage\Services as IRSS; @@ -131,7 +132,7 @@ public function delete( TableTypes::SuggestedLearningContent ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $answer_form_id->toString() ) ) @@ -164,7 +165,7 @@ private function buildQuery( TableTypes::SuggestedLearningContent ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $answer_form_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index 888e96e27cc7..e5b312acc396 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -27,6 +27,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Setup\CLI\IOWrapper; class MigrationInsert @@ -175,37 +176,37 @@ private function buildCoreAnswerFormInsertStatement(): Insert ), [ $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->definition_class ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->new_question_id->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->available_points ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->image_size ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->shuffle_answer_options === null ? null : ($this->shuffle_answer_options ? 1 : 0) ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->additional_text ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->additional_text_legacy ) ] diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 455d8326e852..a8a72897db44 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\Update; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; class TypeGenericProperties { @@ -134,7 +135,7 @@ public function toDelete( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ) ) @@ -155,35 +156,35 @@ private function buildInsertStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->definition::class ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->question_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->available_points ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->image_size ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->getShuffleAnswerOptionsForStorage() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->additional_text ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->additional_text_legacy ) ] @@ -211,19 +212,19 @@ private function buildUpdateStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->available_points ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->image_size ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->getShuffleAnswerOptionsForStorage() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->additional_text ) @@ -235,7 +236,7 @@ private function buildUpdateStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index 2a6c270625df..b73a0e3f1185 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -30,6 +30,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; trait BasicMigrationFunctions { @@ -113,15 +114,15 @@ private function buildGapValuesForInsert( ?int $shuffle ): array { return [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), - $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_type), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $max_chars), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $step_size), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $matching_options?->value), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $min_autocomplete), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $shuffle) + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(FieldDefinition::T_INTEGER, $position), + $persistence_factory->value(FieldDefinition::T_TEXT, $gap_type), + $persistence_factory->value(FieldDefinition::T_INTEGER, $max_chars), + $persistence_factory->value(FieldDefinition::T_FLOAT, $step_size), + $persistence_factory->value(FieldDefinition::T_INTEGER, $matching_options?->value), + $persistence_factory->value(FieldDefinition::T_INTEGER, $min_autocomplete), + $persistence_factory->value(FieldDefinition::T_INTEGER, $shuffle) ]; } @@ -184,14 +185,14 @@ private function buildOptionValuesForInsert( ?float $upper_limit ): array { return [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), - $persistence_factory->value(\ilDBConstants::T_TEXT, $text_value), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $lower_limit), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_option_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(FieldDefinition::T_INTEGER, $position), + $persistence_factory->value(FieldDefinition::T_TEXT, $text_value), + $persistence_factory->value(FieldDefinition::T_FLOAT, $points), + $persistence_factory->value(FieldDefinition::T_FLOAT, $lower_limit), $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $lower_limit !== $upper_limit ? $upper_limit : null @@ -213,9 +214,9 @@ private function buildAnswerFormInsertStatement( AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $scoring_identical->value), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $combinations_enabled) + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $scoring_identical->value), + $persistence_factory->value(FieldDefinition::T_INTEGER, $combinations_enabled) ] ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 14516cb16d1d..103e73310a17 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -32,6 +32,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Setup\Environment; class MigrationCloze implements Migration @@ -297,9 +298,9 @@ private function buildCombinationsInsert( float $points ): Insert { $values = [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + $persistence_factory->value(FieldDefinition::T_TEXT, $combination_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(FieldDefinition::T_FLOAT, $points), ]; if ($combinations_insert === null) { @@ -326,10 +327,10 @@ private function buildCombinationsToAnswerOptionsInsert( ?Range $in_range ): Insert { $values = [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range?->value) + $persistence_factory->value(FieldDefinition::T_TEXT, $combination_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $gap_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_option_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $in_range?->value) ]; if ($combinations_to_answer_options_insert === null) { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 30f00be758ac..afe01f2abfd9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -159,9 +160,9 @@ private function buildReplace( $table_definitions->getCombinationsTableIdentifier() ), [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $this->id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->available_points) + $persistence_factory->value(FieldDefinition::T_TEXT, $this->id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(FieldDefinition::T_FLOAT, $this->available_points) ] ); } @@ -186,7 +187,7 @@ private function buildDelete( $table_definitions->getCombinationsTableIdentifier() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -214,7 +215,7 @@ private function buildDeleteForLinkedValues( $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index df1c04c0e6d6..24a59608e5a8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Language\Language; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; class MatchingValue { @@ -92,19 +93,19 @@ public function toStorage( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->combination_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->gap->getAnswerInputId()->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_option->getAnswerOptionId()->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->in_range?->value ) ] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index 96ce83eb3252..958a777c27c4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -23,6 +23,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; class AnswerOption { @@ -159,31 +160,31 @@ private function buildValuesForGapReplace( ): array { return [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_option_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_input_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->position ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->text_value ), $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->available_points ), $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->lower_limit ), $persistence_factory->value( - \ilDBConstants::T_FLOAT, + FieldDefinition::T_FLOAT, $this->upper_limit ) ]; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 14115b191fb9..e27f32873aca 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -27,6 +27,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -285,7 +286,7 @@ public function buildDelete( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_input_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 95ec67be3919..741a1bb77857 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -388,16 +389,16 @@ private function buildValuesForGapReplace( PersistenceFactory $persistence_factory ): array { return [ - $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), - $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->position), - $persistence_factory->value(\ilDBConstants::T_TEXT, $this->type->getIdentifier()), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->max_chars), - $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->step_size), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), - $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), + $persistence_factory->value(FieldDefinition::T_TEXT, $this->answer_input_id->toString()), + $persistence_factory->value(FieldDefinition::T_TEXT, $this->answer_form_id->toString()), + $persistence_factory->value(FieldDefinition::T_INTEGER, $this->position), + $persistence_factory->value(FieldDefinition::T_TEXT, $this->type->getIdentifier()), + $persistence_factory->value(FieldDefinition::T_INTEGER, $this->max_chars), + $persistence_factory->value(FieldDefinition::T_FLOAT, $this->step_size), + $persistence_factory->value(FieldDefinition::T_INTEGER, $this->text_matching_method?->value), + $persistence_factory->value(FieldDefinition::T_INTEGER, $this->min_autocomplete), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->shuffle_answer_options === null ? null : ($this->shuffle_answer_options ? 1 : 0) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 4bed2904d853..bca52aabb28b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -31,6 +31,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; @@ -451,7 +452,7 @@ private function buildDeleteForRemovedGaps( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ) ), @@ -461,7 +462,7 @@ private function buildDeleteForRemovedGaps( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, array_map( fn(Gap $v): string => $v->getAnswerInputId()->toString(), $this->gaps @@ -494,7 +495,7 @@ private function buildDeleteForDeletionOfAnswerForm( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ), ) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index b4c75ed1b919..ea51a86cba05 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -40,6 +40,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; @@ -347,15 +348,15 @@ private function buildInsertAnswerFormStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->scoring_identical->value ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->combinations->areCombinationsEnabled() ? 1 : 0 ) ] @@ -377,11 +378,11 @@ private function buildUpdateAnswerFormStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->scoring_identical->value ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->combinations->areCombinationsEnabled() ? 1 : 0 ) ], @@ -392,7 +393,7 @@ private function buildUpdateAnswerFormStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ) ) @@ -436,7 +437,7 @@ private function buildDeleteAnswerFormStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->answer_form_id->toString() ) ) diff --git a/components/ILIAS/Questions/src/Persistence/Value.php b/components/ILIAS/Questions/src/Persistence/Value.php index bb986140fd4c..3ed7a8107435 100644 --- a/components/ILIAS/Questions/src/Persistence/Value.php +++ b/components/ILIAS/Questions/src/Persistence/Value.php @@ -23,7 +23,7 @@ class Value { /* - * @param $type Type definition as provided by \ilDBConstants + * @param string $type Type definition as provided by `\ILIAS\Database\FieldDefinition`; */ public function __construct( private readonly string $type, diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index ce13d22253da..7be1c67ef476 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -37,6 +37,7 @@ use ILIAS\Data\Order as DataOrder; use ILIAS\Data\Range as DataRange; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; class Repository @@ -97,7 +98,7 @@ public function getQuestionDataOnlyForQuestionIds( TableTypes::Questions ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, array_map( fn(Uuid $v): string => $v->toString(), $question_ids @@ -126,7 +127,7 @@ public function getForQuestionId( TableTypes::Questions ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $question_id->toString() ), Operator::Equal @@ -152,7 +153,7 @@ public function getForQuestionIds( TableTypes::Questions ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, array_map( fn(Uuid $v): string => $v->toString(), $question_ids @@ -349,7 +350,7 @@ private function getAnswerFormTypesForQuestionIds( 'question_id', $question_ids, false, - \ilDBConstants::T_TEXT + FieldDefinition::T_TEXT )}" ); $answer_form_types = []; @@ -477,7 +478,7 @@ private function migrateQuestionPage( $old_page_id = $this->db->fetchObject( $this->db->query( "SELECT old_question_id FROM {$table_name}" . PHP_EOL - . "WHERE new_question_id = {$this->db->quote($question->getId(), \ilDBConstants::T_TEXT)}" + . "WHERE new_question_id = {$this->db->quote($question->getId(), FieldDefinition::T_TEXT)}" ) )->old_question_id; diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index eb89397d7a9f..1daef99539ae 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -36,6 +36,7 @@ use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -523,15 +524,15 @@ private function buildInsertLinkingStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->parent_obj_id ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->position ) ] @@ -550,39 +551,39 @@ private function buildInsertQuestionStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->page_id ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->title ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->author ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->lifecycle->value ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->remarks ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->original_id?->toString() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, time() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, time() ) ] @@ -604,11 +605,11 @@ private function buildUpdateLinkingStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->parent_obj_id ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $this->position ) ], @@ -618,7 +619,7 @@ private function buildUpdateLinkingStatement( $persistence_factory ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -645,27 +646,27 @@ private function buildUpdateQuestionStatement( ), [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->title ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->author ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->lifecycle->value ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->remarks ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->original_id?->toString() ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, time() ) ], @@ -676,7 +677,7 @@ private function buildUpdateQuestionStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -703,7 +704,7 @@ private function buildDeleteQuestionStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -729,7 +730,7 @@ private function buildDeleteLinkingStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -759,7 +760,7 @@ private function buildDeleteMigrationStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) @@ -796,11 +797,11 @@ private function buildUpdatePageIdStatement( ], [ $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->page_id ), $persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, time() ) ], @@ -811,7 +812,7 @@ private function buildUpdatePageIdStatement( $table_type ), $persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $this->id->toString() ) ) diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index 29707fcab9f0..bf8de6be0a6e 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Database\FieldDefinition; class ClozeQuestionTables implements \ilDatabaseUpdateSteps { @@ -48,17 +49,17 @@ public function step_1(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'scoring_identical_responses' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 32, 'notnull' => true ], 'combinations_enabled' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 1, 'notnull' => true ] @@ -78,46 +79,46 @@ public function step_2(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'position' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => true ], 'gap_type' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 32, 'notnull' => true ], 'max_chars' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => false ], 'step_size' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ], 'text_matching_method' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 32, 'notnull' => false ], 'min_autocomplete' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => false ], 'shuffle_answer_options' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 1, 'notnull' => false ] @@ -141,35 +142,35 @@ public function step_3(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_input_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'position' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => true ], 'text_value' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 4000, 'notnull' => false ], 'lower_limit' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ], 'upper_limit' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ], 'points' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => true ] ]); @@ -193,17 +194,17 @@ public function step_4(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'points' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ] ]); @@ -227,22 +228,22 @@ public function step_5(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'combination_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'gap_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_option_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'in_range' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 16, 'notnull' => false ] @@ -266,22 +267,22 @@ public function step_6(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_input_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'selected_answer_option' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => false ], 'text' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 4000, 'notnull' => true ] diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index 55e9a878106b..c8b33900fb1a 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -25,6 +25,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableTypes as FeedbackTableTypes; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Database\FieldDefinition; class OverarchingQuestionTables implements \ilDatabaseUpdateSteps { @@ -50,47 +51,47 @@ public function step_1(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'page_id' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 4, 'notnull' => true ], 'title' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 512, 'notnull' => true ], 'author' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 512, 'notnull' => false ], 'lifecycle' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 16, 'notnull' => true ], 'remarks' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 4000, 'notnull' => false ], 'original_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => false ], 'last_update' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 8, 'notnull' => true ], 'created' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 8, 'notnull' => true ], @@ -110,40 +111,40 @@ public function step_2(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'type' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 4000, 'notnull' => true ], 'question_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'available_points' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ], 'image_size' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => false ], 'shuffle_answer_options' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 1, 'notnull' => false ], 'additional_text' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ], 'additional_text_legacy' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ] ]); @@ -166,17 +167,17 @@ public function step_3(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'question_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'reached_points' => [ - 'type' => \ilDBConstants::T_FLOAT, + 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ] ]); @@ -199,17 +200,17 @@ public function step_4(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'question_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'obj_id' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 4, 'notnull' => true ], 'position' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 2, 'notnull' => false ] @@ -233,17 +234,17 @@ public function step_5(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'new_question_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'old_question_id' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 4, 'notnull' => false ], 'success' => [ - 'type' => \ilDBConstants::T_INTEGER, + 'type' => FieldDefinition::T_INTEGER, 'length' => 1, 'notnull' => true ] @@ -269,24 +270,24 @@ public function step_6(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'feedback_best_response' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ], 'feedback_best_response_legacy' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ], 'feedback_other_response' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ], 'feedback_other_response_legacy' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ] ]); @@ -311,31 +312,31 @@ public function step_7(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'parent_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'condition' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => false ], 'feedback' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ], 'feedback_legacy' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ] ]); @@ -373,17 +374,17 @@ public function step_8(): void if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ 'answer_form_id' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], 'type' => [ - 'type' => \ilDBConstants::T_TEXT, + 'type' => FieldDefinition::T_TEXT, 'length' => 32, 'notnull' => false ], 'content' => [ - 'type' => \ilDBConstants::T_CLOB, + 'type' => FieldDefinition::T_CLOB, 'notnull' => true ] ]); diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 62bfce755246..ecb28a23c26e 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -32,6 +32,7 @@ use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; +use ILIAS\Database\FieldDefinition; use ILIAS\Setup; use ILIAS\Setup\CLI\IOWrapper; use ILIAS\Setup\Environment; @@ -335,15 +336,15 @@ private function buildInsertLinkingStatement( ), [ $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $new_question_id->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $obj_id ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $position ) ] @@ -366,39 +367,39 @@ private function buildInsertQuestionStatement( ), [ $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $id->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, 0 ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $title ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $author ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $lifecycle->value ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $remarks ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $original_id?->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, time() ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $create_date ) ] @@ -416,15 +417,15 @@ private function buildInsertMigrationStatement( ), [ $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $old_question_id ), $this->persistence_factory->value( - \ilDBConstants::T_TEXT, + FieldDefinition::T_TEXT, $new_question_id?->toString() ), $this->persistence_factory->value( - \ilDBConstants::T_INTEGER, + FieldDefinition::T_INTEGER, $new_question_id === null ? '0' : '1' From ea9f3c66237f5898953e30a8b0981d052ceb49da Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 3 Apr 2026 17:18:29 +0200 Subject: [PATCH 089/108] Questions: Move Marking to Capability --- .../class.ilObjQuestionsGUI.php | 6 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 10 +- .../src/AnswerForm/Capabilities/Action.php | 76 ----- .../AnswerForm/Capabilities/ActionWithTab.php | 77 +++++ .../Capabilities/AdditionalFormStepAction.php | 221 ++++++++++++++ .../AnswerForm/Capabilities/Capability.php | 13 +- .../src/AnswerForm/Capabilities/Edit.php | 205 +++++++++++++ .../Capabilities/Feedback/Capability.php | 59 ++-- .../Capabilities/Marking/Capability.php | 52 ++-- .../Capabilities/Marking/Marking.php | 6 + .../SuggestedLearningContent.php | 51 +++- .../Questions/src/AnswerForm/Views/Edit.php | 4 + .../Cloze/Capabilities/Marking.php | 34 +++ .../Cloze/Layout/OverviewTable.php | 49 ++-- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 29 +- .../AnswerFormTypes/Cloze/Views/EditGaps.php | 276 +++++------------- .../src/Presentation/Definitions/Actor.php | 2 +- .../Definitions/DefaultEnvironment.php | 227 ++++++++++---- .../Presentation/Definitions/Environment.php | 10 + .../src/Presentation/Layout/EditForm.php | 42 +-- .../src/Presentation/Layout/Factory.php | 6 +- .../Questions/src/Presentation/Views/Edit.php | 240 ++++++++++----- .../Questions/src/Question/Views/Edit.php | 8 +- 23 files changed, 1154 insertions(+), 549 deletions(-) delete mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/AdditionalFormStepAction.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index a80c30c94f2f..9580b6e3bc75 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -20,8 +20,9 @@ use ILIAS\Questions\Administration\ConfigurationGUI; use ILIAS\Questions\Administration\ConfigurationRepository; -use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; +use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; @@ -61,7 +62,8 @@ public function __construct( $this->edit_view = $local_dic[Edit::class] ->withRequiredCapabilities([ Feedback::class, - SuggestedLearningContent::class + SuggestedLearningContent::class, + Marking::class ]); $this->configuration_repository = $local_dic[ConfigurationRepository::class]; diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 5f98709587e6..bff8a122c20b 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -115,6 +115,10 @@ protected static function buildDIC(ILIASContainer $DIC): self ), Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability() ]); + $dic[Capabilities\Edit::class] = fn($c): Capabilities\Edit + => new Capabilities\Edit( + $c[Capabilities\Factory::class] + ); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( $c[UuidFactory::class], @@ -157,7 +161,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilTabs'], $DIC->uiService(), $c[UuidFactory::class], - $c[Capabilities\Factory::class], + $c[Capabilities\Edit::class], $c[AnswerFormFactory::class], $c[QuestionsRepository::class], $c[LayoutFactory::class] @@ -243,7 +247,9 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[UuidFactory::class], $c[DataFactory::class]->text() ), - Capabilities\Marking\Marking::class => new Cloze\Capabilities\Marking() + Capabilities\Marking\Marking::class => new Cloze\Capabilities\Marking( + $c[Cloze\Properties\Factory::class] + ) ], $c[Cloze\Views\Edit::class], $c[Cloze\Views\Participant::class] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php deleted file mode 100644 index 078c6bc81b01..000000000000 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Action.php +++ /dev/null @@ -1,76 +0,0 @@ - $available_capabilities - */ - public function __construct( - private readonly Capability $capability, - private readonly string $lang_var - ) { - } - - public function getCapability(): Capability - { - return $this->capability; - } - - public function addTab( - DefaultEnvironment $environment, - \ilTabsGUI $tabs_gui, - Language $lng - ): void { - $action = $this->buildAction(); - $tabs_gui->addTab( - $action, - $lng->txt($this->lang_var), - $environment->withActionParameter($action) - ->getUrlBuilder() - ->buildURI() - ->__toString() - ); - } - - public function activateTab( - \ilTabsGUI $tabs_gui - ): void { - $action = $this->buildAction(); - $tabs_gui->activateTab($action); - } - - public function isThis( - string $action - ): bool { - return $action === $this->buildAction(); - } - - private function buildAction(): string - { - return self::ACTION_EDIT . '_' . md5($this->capability::class); - } -} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php new file mode 100644 index 000000000000..bd5d3af375b9 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php @@ -0,0 +1,77 @@ +do->__invoke($environment); + } + + public function getIdentifier(): string + { + return self::PREFIX . '_' . md5($this->capability::class); + } + + public function addTab( + Environment $environment, + \ilTabsGUI $tabs_gui + ): void { + $action = $this->getIdentifier(); + $tabs_gui->addTab( + $action, + $environment->getLanguage()->txt($this->lang_var), + $environment->withActionParameter($action) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + public function activateTab( + \ilTabsGUI $tabs_gui + ): void { + $tabs_gui->activateTab( + $this->buildIdentifier() + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AdditionalFormStepAction.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AdditionalFormStepAction.php new file mode 100644 index 000000000000..e4e31d29e14c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AdditionalFormStepAction.php @@ -0,0 +1,221 @@ +getSubAction(); + $environment_with_preserved_parameters = $environment + ->withPreservedTableRowIdsParameter() + ->withPreservedFormStartSubActionParameter(); + return match ($sub_action) { + self::SUB_ACTION_SAVE => $this->forwardToNextForm( + $environment_with_preserved_parameters + ), + self::SUB_ACTION_BACK => $this->backToPreviousForm( + $environment_with_preserved_parameters + ), + default => $this->buildFormWithCarry( + $environment_with_preserved_parameters + ) + }; + } + + public function getIdentifier(): string + { + return self::PREFIX . '_' . md5($this->capability::class); + } + + private function toPrevious( + DefaultEnvironment $envrionment + ): Async|EditForm { + if ($this->previous instanceof AnswerFormEditView) { + return $this->previous + ->backToLastEditCommand($envrionment->withActionParameter('')) + ->withIsFinalStep(false); + } + + return $this->previous->getAnswerFormEditAdditionalStep()->do( + $envrionment + ); + } + + public function withPrevious( + Capability|AnswerFormEditView $previous + ): self { + $clone = clone $this; + $clone->previous = $previous; + return $clone; + } + + private function toNext( + DefaultEnvironment $environment + ): Async|EditForm|Properties { + if ($this->next === null) { + return $environment->getAnswerFormProperties(); + } + + return $this->next->getAnswerFormEditAdditionalStep()->do( + $environment + ); + } + + public function withNext( + ?Capability $next + ): self { + $clone = clone $this; + $clone->next = $next; + return $clone; + } + + public function getAsTableAction( + DefaultEnvironment $environment + ): TableAction { + return $environment->getUIFactory()->table()->action()->standard( + $environment->getLanguage()->txt($this->lang_var), + $environment->withActionParameter( + $this->getIdentifier() + )->getUrlBuilder(), + $environment->getTableRowIdToken() + ); + } + + private function buildFormWithCarry( + DefaultEnvironment $environment + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + + $inputs_builder_for_points = $this->retrieve_inputs_builder->__invoke( + $environment + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder_for_points->persistCarry(); + + return $this->buildForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder_for_points + ); + } + + private function buildForm( + DefaultEnvironment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $environment->withSubActionParameter( + self::SUB_ACTION_SAVE + )->getUrlBuilder(), + $environment->getFormStartSubAction() === '' + ? null + : $environment->withSubActionParameter( + self::SUB_ACTION_BACK + )->getUrlBuilder() + )->withIsFinalStep(true) + ->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $environment->getUIFactory(), + $environment->getLanguage(), + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function forwardToNextForm( + DefaultEnvironment $environment + ): EditForm|Properties { + $processed_form = $this->processForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->toNext( + $environment->withAnswerFormProperties($processed_form) + ); + } + + private function backToPreviousForm( + DefaultEnvironment $environment + ): EditForm { + $processed_form = $this->processForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->toPrevious( + $environment->withAnswerFormProperties($processed_form) + ); + } + + private function processForm( + DefaultEnvironment $environment + ): EditForm|Properties { + $inputs_builder = $this->retrieve_inputs_builder->__invoke($environment); + $form = $this->buildForm( + $environment, + $inputs_builder + )->withRequest($environment->getHttpServices()->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder->persistCarry(); + return $form; + } + + return $data; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index 674e90a774c4..42b7b8881217 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -21,9 +21,6 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; interface Capability { @@ -31,11 +28,13 @@ public function isAvailableFor( Properties $answer_form_properties ): bool; - public function getEditAction(): ?Action; + public function providesAnswerFormEditAdditionalTab(): bool; - public function edit( - Environment $environment - ): self|Async|Renderable; + public function getAnswerFormEditAdditionalTab(): ?ActionWithTab; + + public function providesAnswerFormEditAdditionalStep(): bool; + + public function getAnswerFormEditAdditionalStep(): ?AdditionalFormStepAction; public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php new file mode 100644 index 000000000000..a0207454163d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php @@ -0,0 +1,205 @@ +required_capabilites; + } + + public function withRequiredCapabilities( + array $capability_class_names + ): self { + $clone = clone $this; + [ + 'required_capabilities' => $clone->required_capabilites, + 'required_actions_with_tab' => $clone->required_actions_with_tab, + 'required_form_step_actions' => $clone->required_form_step_actions + ] = $clone->buildRequiredCapabilitiesAndActions( + $capability_class_names + ); + return $clone; + } + + public function onAnswerFormUpdate( + AnswerFormProperties $properties + ): void { + foreach ($this->required_capabilites as $capability) { + $capability->onAnswerFormUpdate($properties); + } + } + + public function edit( + DefaultEnvironment $environment, + AnswerFormEditView $edit_view, + string $action_from_get +<<<<<<< HEAD + ): Async|Renderable|AnswerFormProperties|null { + $action = $this->getCurrentActionFromActionsArray( + $this->required_actions_with_tab, + $action_from_get + ); + +======= + ): EditForm|AnswerFormProperties|null { + $environment->setEditAnswerFormTabs( + $this->required_actions_with_tab + ); + + $action = $this->required_actions_with_tab[$action_from_get] ?? null; +>>>>>>> 4a2133216e4 (Questions: Move Marking to Capability) + if ($action !== null) { + $action->activateTab($this->tabs_gui); + } else { +<<<<<<< HEAD + $action = $this->addPreviousAndNextStepToFormAction( + $environment, +======= + $environment->setEditAnswerFormBackTarget(); + $action = $this->retrieveNextFormStepFromActionIdentifier( +>>>>>>> 4a2133216e4 (Questions: Move Marking to Capability) + $edit_view, + $action_from_get + ); + } + + if ($action === null) { + return null; + } + + $environment->setEditAnswerFormBackTarget(); + return $action->do( + $environment->withActionParameter($action_from_get) + ); + } + + public function providesAnswerFormEditAdditionalSteps(): bool + { + return array_filter( + $this->required_capabilites, + fn(Capability $v): bool => $v->providesAnswerFormEditAdditionalStep() + ) !== []; + } + + public function doFirstFormStepAction( + DefaultEnvironment $environment, + AnswerFormEditView $edit_view + ): EditForm|AnswerFormProperties { + if ($this->required_form_step_actions === []) { + return $environment->getAnswerFormProperties(); + } + + $keys = array_keys($this->required_form_step_actions); + + return $this->required_form_step_actions[$keys[0]] + ->withPrevious( + $edit_view + )->withNext( + isset($keys[1]) + ? $this->required_form_step_actions[$keys[1]] + : null + )->do( + $environment->withActionParameter($keys[0]) + ); + } + + private function retrieveNextFormStepFromActionIdentifier( + AnswerFormEditView $edit_view, + string $action_identifier + ): ?AdditionalFormStepAction { + $action = $this->required_form_step_actions[$action_identifier] ?? null; + if ($action === null) { + return null; + } + + $keys = array_keys($this->required_form_step_actions); + $current_index = array_search($action_identifier, $keys); + + $next_index = $current_index + 1; + if (isset($keys[$next_index])) { + $action = $action->withNext( + $this->required_form_step_actions[$next_index] + ); + } + + return $action->withPrevious( + $current_index > 0 + ? $this->required_form_step_actions[$keys[$current_index - 1]] + : $edit_view + ); + } + + /** + * @param list $capabilities + * @return list<\ILIAS\Questions\AnswerForm\Capabilities\Capability> + */ + private function buildRequiredCapabilitiesAndActions( + array $capabilities + ): array { + return array_reduce( + $capabilities, + function (array $c, string $v): array { + $capability = $this->factory->get($v); + if ($capability === null) { + throw new \InvalidArgumentException( + "The capability {$v} does not exist." + ); + } + $c['required_capabilities'][] = $capability; + + $action_with_tab = $capability->getAnswerFormEditAdditionalTab(); + if ($action_with_tab !== null) { + $c['required_actions_with_tab'][$action_with_tab->getIdentifier()] = $action_with_tab; + } + + $form_step_action = $capability->getAnswerFormEditAdditionalStep(); + if ($form_step_action !== null) { + $c['required_form_step_actions'][$form_step_action->getIdentifier()] = $form_step_action; + } + + return $c; + }, + [ + 'required_capabilities' => [], + 'required_actions_with_tab' => [], + 'required_form_step_actions' => [] + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php index 185e0e405ea0..a4d870cc2364 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\Action; +use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; @@ -52,30 +52,31 @@ public function isAvailableFor( } #[\Override] - public function getEditAction(): Action + public function providesAnswerFormEditAdditionalTab(): bool { - return new Action( + return true; + } + + #[\Override] + public function getAnswerFormEditAdditionalTab(): ActionWithTab + { + return new ActionWithTab( $this, - 'feedback' + 'feedback', + $this->buildDoEditActionClosure() ); } #[\Override] - public function edit( - Environment $environment - ): Async|Renderable { - $step = $environment->getStep(); - return match ($step) { - '' => $this->buildOverview($environment), - self::STEP_INSERT_LEGACY_TEXTS => $this->buildOverviewWithLegacyTexts( - $environment - ), - self::STEP_SAVE => $this->save($environment), - default => $this->buildOverview($environment)->doAction( - $this->repository, - $step - ) - }; + public function providesAnswerFormEditAdditionalStep(): bool + { + return false; + } + + #[\Override] + public function getAnswerFormEditAdditionalStep(): null + { + return null; } #[\Override] @@ -96,6 +97,26 @@ public function onAnswerFormUpdate( ); } + private function buildDoEditActionClosure(): \Closure + { + return function ( + Environment $environment + ): Async|Renderable { + $sub_action = $environment->getSubAction(); + return match ($sub_action) { + '' => $this->buildOverview($environment), + self::STEP_INSERT_LEGACY_TEXTS => $this->buildOverviewWithLegacyTexts( + $environment + ), + self::STEP_SAVE => $this->save($environment), + default => $this->buildOverview($environment)->doAction( + $this->repository, + $sub_action + ) + }; + }; + } + private function buildOverview( Environment $environment ): Overview { diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php index b819a828107b..d4e45cd07295 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php @@ -20,12 +20,11 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Questions\Response\Response; class Capability implements CapabilityInterface @@ -34,35 +33,52 @@ class Capability implements CapabilityInterface public function isAvailableFor( Properties $answer_form_properties ): bool { + return $answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->hasCapability( + Marking::class + ) + && $answer_form_properties + ->getTypeGenericProperties() + ->getAvailablePoints() !== null; + } + + #[\Override] + public function providesAnswerFormEditAdditionalTab(): bool + { return false; } #[\Override] - public function getEditAction(): null + public function getAnswerFormEditAdditionalTab(): null { return null; } #[\Override] - public function edit( - Environment $environment - ): self|Async|EditForm|EditOverview { - return false; + public function providesAnswerFormEditAdditionalStep(): bool + { + return true; + } + + #[\Override] + public function getAnswerFormEditAdditionalStep(): AdditionalFormStepAction + { + return new AdditionalFormStepAction( + $this, + 'edit_available_points', + fn(Environment $v): InputsBuilderSession + => $v->getAnswerFormProperties() + ->getDefinition() + ->getCapability(Marking::class) + ->getEditFormInputsBuilder($v) + ); } #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties ): void { - $this->repository->store( - $answer_form_properties->getAnswerFormId(), - $answer_form_properties - ->getTypeGenericProperties() - ->getDefinition() - ->getCapability(Feedback::class) - ->onAnswerFormUpdate( - $answer_form_properties - ) - ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php index a29db9be1e1b..4b30fded4799 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php @@ -20,10 +20,16 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; +use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Response\Response; interface Marking { + public function getEditFormInputsBuilder( + Environment $environment, + ): InputsBuilderSession; + public function addAchievedPointsToResponse( Response $response ): Response; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php index 5987bd834bfc..fe8970234d0c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent; -use ILIAS\Questions\AnswerForm\Capabilities\Action; +use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; @@ -48,27 +48,31 @@ public function isAvailableFor( } #[\Override] - public function getEditAction(): Action + public function providesAnswerFormEditAdditionalTab(): bool { - return new Action( + return true; + } + + #[\Override] + public function getAnswerFormEditAdditionalTab(): ActionWithTab + { + return new ActionWithTab( $this, - 'suggested_learning_content' + 'suggested_learning_content', + $this->buildDoEditActionClosure() ); } #[\Override] - public function edit( - Environment $environment - ): Async|Renderable { - $step = $environment->getStep(); - $overview = $this->buildOverview($environment); - if ($step === '') { - return $overview; - } + public function providesAnswerFormEditAdditionalStep(): bool + { + return false; + } - return $overview->doAction( - $step - ); + #[\Override] + public function getAnswerFormEditAdditionalStep(): null + { + return null; } #[\Override] @@ -77,6 +81,23 @@ public function onAnswerFormUpdate( ): void { } + private function buildDoEditActionClosure(): \Closure + { + return function ( + Environment $environment + ): Async|Renderable { + $sub_action = $environment->getSubAction(); + $overview = $this->buildOverview($environment); + if ($sub_action === '') { + return $overview; + } + + return $overview->doAction( + $sub_action + ); + }; + } + private function buildOverview( Environment $environment ): Overview { diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index a9d5d52f2588..1b5bba381c68 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -40,4 +40,8 @@ public function edit( public function other( Environment $environment ): Async|Renderable|Properties; + + public function backToLastEditCommand( + Environment $environment + ): EditForm; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php index 6c3fb2ede227..56b6ad938080 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php @@ -21,10 +21,44 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking as MarkingInterface; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Questions\Response\Response; +use ILIAS\UI\Component\Input\Field\Section; class Marking implements MarkingInterface { + public function __construct( + private readonly PropertiesFactory $properties_factory + ) { + } + + #[\Override] + public function getEditFormInputsBuilder( + Environment $environment, + ): InputsBuilderSession { + return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( + $environment->getRefinery()->custom()->transformation( + function (?string $carry) use ($environment): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $environment->getAnswerFormProperties(), + $carry + ); + return $properties_from_carry->getGaps() + ->buildPointInputs( + $environment->getLanguage(), + $environment->getUIFactory()->input()->field(), + $properties_from_carry, + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + } + ) + ); + } + #[\Override] public function addAchievedPointsToResponse( Response $response diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index 4a7073ce6dbd..e595a2da1287 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -24,6 +24,7 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\Range; use ILIAS\Data\Order; +use ILIAS\UI\Component\Table\Action\Action as TableAction; use ILIAS\UI\Component\Table\Data as DataTable; use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -95,28 +96,30 @@ private function getColums(): array private function getActions(): array { $taf = $this->environment->getUIFactory()->table()->action(); - return [ - 'edit_gaps' => $taf->standard( - $this->environment->getLanguage()->txt('edit_gaps'), - $this->environment - ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) - ->getUrlBuilder(), - $this->environment->getTableRowIdToken() - ), - 'edit_answer_options' => $taf->standard( - $this->environment->getLanguage()->txt('edit_answer_options'), - $this->environment - ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) - ->getUrlBuilder(), - $this->environment->getTableRowIdToken() - ), - 'edit_points' => $taf->standard( - $this->environment->getLanguage()->txt('edit_available_points'), - $this->environment - ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_ASSIGN_POINTS) - ->getUrlBuilder(), - $this->environment->getTableRowIdToken() - ) - ]; + return array_reduce( + $this->environment->getAnswerFormTableActionsForRequiredCapabilities(), + function (array $c, TableAction $v): array { + $c[] = $v; + return $c; + }, + [ + 'edit_gaps' => $taf->standard( + $this->environment->getLanguage()->txt('edit_gaps'), + $this->environment + ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) + ->withFormStartSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ), + 'edit_answer_options' => $taf->standard( + $this->environment->getLanguage()->txt('edit_answer_options'), + $this->environment + ->withSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) + ->withFormStartSubActionParameter(EditGaps::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ) + ] + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 73f2e5e53ede..400751735a31 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -61,8 +61,7 @@ public function create( $environment ), default => $this->forwardCmdToEditGaps( - $environment, - $sub_action + $environment ) }; } @@ -97,8 +96,9 @@ public function edit( $environment->withPreservedTableRowIdsParameter() ), default => $this->forwardCmdToEditGaps( - $environment->withPreservedTableRowIdsParameter(), - $sub_action + $environment + ->withPreservedTableRowIdsParameter() + ->withPreservedFormStartSubActionParameter() ) }; } @@ -112,6 +112,16 @@ public function other( ->getCombinations()->getEditView()->show($environment); } + #[\Override] + public function backToLastEditCommand( + Environment $environment + ): EditForm { + return $this->edit_gaps->do( + $environment, + EditGaps::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS + ); + } + private function startEditing( Environment $environment ): EditForm { @@ -129,10 +139,12 @@ private function startEditing( } private function forwardCmdToEditGaps( - Environment $environment, - string $sub_action + Environment $environment ): EditForm|Async|Properties { - $processed_form = $this->edit_gaps->do($environment, $sub_action); + $processed_form = $this->edit_gaps->do( + $environment, + $environment->getSubAction() + ); if (is_string($processed_form)) { $inputs_builder = $this->buildInputsBuilderForBasicInputs( $environment, @@ -252,8 +264,7 @@ private function buildEditFormForBasicInputs( $environment ->withSubActionParameter(self::SUB_ACTION_PROCESS_BASIC_PROPERTIES) ->getUrlBuilder(), - null, - false + null ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php index 7c80f802a765..38f8aff882d2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -39,14 +39,10 @@ class EditGaps private const string SUB_ACTION_BACK_TO_SET_GAP_TYPES = 'bsgt'; public const string SUB_ACTION_JUMP_TO_SET_GAP_TYPES = 'jsgt'; private const string SUB_ACTION_SET_ANSWER_OPTIONS = 'sao'; - private const string SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS = 'bsao'; + public const string SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS = 'bsao'; public const string SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS = 'jsao'; - private const string SUB_ACTION_ASSIGN_POINTS = 'ap'; public const string SUB_ACTION_JUMP_TO_ASSIGN_POINTS = 'jap'; - private const string SUB_ACTION_SAVE = 's'; - - private string $sub_action; - private ?string $start_sub_action; + private const string SUB_ACTION_PROCESS_SET_ANSWER_OPTIONS = 'psao'; public function __construct( private readonly FileUpload $file_upload, @@ -59,12 +55,6 @@ public function do( Environment $environment, string $sub_action = self::SUB_ACTION_SET_GAP_TYPES ): EditForm|Async|Properties|string { - $sub_action_array = explode('_', $sub_action); - $this->sub_action = $sub_action_array[0]; - $this->start_sub_action = $this->determineStartSubActionFromSubAction( - $sub_action_array[1] ?? null - ); - $upload_handler = new UploadAnswerOptions( $this->file_upload, $environment @@ -73,54 +63,52 @@ public function do( return $upload_handler->do($sub_action); } - return match ($this->sub_action) { + return match ($sub_action) { self::SUB_ACTION_SET_GAP_TYPES, self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES => $this->buildGapTypesFormWithCarry( $environment, - $environment->getAnswerFormProperties() + $environment->getAnswerFormProperties(), + $sub_action ), self::SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES => $this->backToEditBasicProperties( - $environment + $environment, + $sub_action ), self::SUB_ACTION_BACK_TO_SET_GAP_TYPES => $this->backToGapTypesForm( - $environment + $environment, + $sub_action ), self::SUB_ACTION_SET_ANSWER_OPTIONS => $this->forwardToAnswerOptionsForm( - $environment + $environment, + $sub_action ), + self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS, self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS => $this->buildAnswerOptionsFormWithCarry( $environment, - $environment->getAnswerFormProperties() - ), - self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS - => $this->backToSetAnswerOptionsForm( - $environment - ), - self::SUB_ACTION_ASSIGN_POINTS - => $this->forwardToAssignPointsForm( - $environment + $environment->getAnswerFormProperties(), + $sub_action ), - self::SUB_ACTION_JUMP_TO_ASSIGN_POINTS - => $this->buildAssignPointsFormWithCarry( + self::SUB_ACTION_PROCESS_SET_ANSWER_OPTIONS + => $this->processAnswerOptionsForm( $environment, - $environment->getAnswerFormProperties() - ), - self::SUB_ACTION_SAVE - => $this->processAssignPointsForm( - $environment + $sub_action ) }; } private function backToEditBasicProperties( - Environment $environment + Environment $environment, + string $sub_action ): EditForm|string { - $processed_form = $this->processGapTypesForm($environment); + $processed_form = $this->processGapTypesForm( + $environment, + $sub_action + ); if ($processed_form instanceof EditForm) { return $processed_form; } @@ -129,22 +117,28 @@ private function backToEditBasicProperties( } private function backToGapTypesForm( - Environment $environment + Environment $environment, + string $sub_action ): EditForm { - $processed_form = $this->processAnswerOptionsForm($environment); + $processed_form = $this->processAnswerOptionsForm( + $environment, + $sub_action + ); if ($processed_form instanceof EditForm) { return $processed_form; } return $this->buildGapTypesFormWithCarry( $environment, - $processed_form + $processed_form, + $sub_action ); } private function buildGapTypesFormWithCarry( Environment $environment, - Properties $properties + Properties $properties, + string $sub_action ): EditForm { $inputs_builder = $this->buildInputsBuilderForTypesForm( $environment @@ -156,13 +150,15 @@ private function buildGapTypesFormWithCarry( return $this->buildGapTypesForm( $environment, - $inputs_builder + $inputs_builder, + $sub_action ); } private function buildGapTypesForm( Environment $environment, - InputsBuilderSession $inputs_builder + InputsBuilderSession $inputs_builder, + string $sub_action ): EditForm { /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ $properties = $environment->getAnswerFormProperties(); @@ -175,14 +171,13 @@ private function buildGapTypesForm( $environment, self::SUB_ACTION_SET_ANSWER_OPTIONS ), - $this->sub_action === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES - || $this->sub_action === $this->start_sub_action + $sub_action === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES + || $environment->getFormStartSubAction() === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES ? null : $this->buildPostTarget( $environment, self::SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES - ), - false + ) )->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( $environment->getUIFactory(), @@ -194,7 +189,8 @@ private function buildGapTypesForm( } private function processGapTypesForm( - Environment $environment + Environment $environment, + string $sub_action ): EditForm|Properties { $inputs_builder_for_types = $this->buildInputsBuilderForTypesForm( $environment @@ -206,7 +202,8 @@ private function processGapTypesForm( $form = $this->buildGapTypesForm( $environment->withAnswerFormProperties($properties), - $inputs_builder_for_types + $inputs_builder_for_types, + $sub_action )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); @@ -245,36 +242,28 @@ function (?string $carry) use ($environment): Section { } private function forwardToAnswerOptionsForm( - Environment $environment + Environment $environment, + string $sub_action ): EditForm { - $processed_form = $this->processGapTypesForm($environment); - if ($processed_form instanceof EditForm) { - return $processed_form; - } - - return $this->buildAnswerOptionsFormWithCarry( + $processed_form = $this->processGapTypesForm( $environment, - $processed_form + $sub_action ); - } - - private function backToSetAnswerOptionsForm( - Environment $environment - ): EditForm { - $processed_form = $this->processAssignPointsForm($environment); if ($processed_form instanceof EditForm) { return $processed_form; } return $this->buildAnswerOptionsFormWithCarry( $environment, - $processed_form + $processed_form, + $sub_action ); } private function buildAnswerOptionsFormWithCarry( Environment $environment, - Properties $properties + Properties $properties, + string $sub_action ): EditForm { $inputs_builder = $this->buildInputsBuilderForAnswerOptionsForm( $environment, @@ -287,30 +276,32 @@ private function buildAnswerOptionsFormWithCarry( return $this->buildAnswerOptionsForm( $environment->withAnswerFormProperties($properties), - $inputs_builder + $inputs_builder, + $sub_action ); } private function buildAnswerOptionsForm( Environment $environment, - InputsBuilderSession $inputs_builder + InputsBuilderSession $inputs_builder, + string $sub_action ): EditForm { $properties = $environment->getAnswerFormProperties(); return $environment->getPresentationFactory()->getEditForm( $inputs_builder, $this->buildPostTarget( $environment, - self::SUB_ACTION_ASSIGN_POINTS + self::SUB_ACTION_PROCESS_SET_ANSWER_OPTIONS ), - $this->sub_action === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS - || $this->sub_action === $this->start_sub_action + $sub_action === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS + || $environment->getFormStartSubAction() === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS ? null : $this->buildPostTarget( $environment, self::SUB_ACTION_BACK_TO_SET_GAP_TYPES - ), - false - )->withContentBeforeForm( + ) + )->withIsFinalStep(true) + ->withContentBeforeForm( $properties->getClozeText()->buildPanelForEditing( $environment->getUIFactory(), $environment->getLanguage(), @@ -321,7 +312,8 @@ private function buildAnswerOptionsForm( } private function processAnswerOptionsForm( - Environment $environment + Environment $environment, + string $sub_action ): EditForm|Properties { $inputs_builder_for_options = $this->buildInputsBuilderForAnswerOptionsForm( $environment, @@ -330,7 +322,8 @@ private function processAnswerOptionsForm( $form = $this->buildAnswerOptionsForm( $environment, - $inputs_builder_for_options + $inputs_builder_for_options, + $sub_action )->withRequest($environment->getHttpServices()->request()); $data = $form->getData(); @@ -368,144 +361,13 @@ function (?string $carry) use ( ); } - private function forwardToAssignPointsForm( - Environment $environment - ): EditForm { - $processed_form = $this->processAnswerOptionsForm($environment); - if ($processed_form instanceof EditForm) { - return $processed_form; - } - - return $this->buildAssignPointsFormWithCarry( - $environment, - $processed_form - ); - } - - private function buildAssignPointsFormWithCarry( - Environment $environment, - Properties $properties - ): EditForm { - $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( - $environment, - $properties - )->withCarry( - $properties->toCarry() - ); - - $inputs_builder_for_points->persistCarry(); - - return $this->buildAssignPointsForm( - $environment->withAnswerFormProperties($properties), - $inputs_builder_for_points - ); - } - - private function buildAssignPointsForm( - Environment $environment, - InputsBuilderSession $inputs_builder - ): EditForm { - $properties = $environment->getAnswerFormProperties(); - return $environment->getPresentationFactory()->getEditForm( - $inputs_builder, - $this->buildPostTarget( - $environment, - self::SUB_ACTION_SAVE - ), - $this->sub_action === self::SUB_ACTION_JUMP_TO_ASSIGN_POINTS - ? null - : $this->buildPostTarget( - $environment, - self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS - ), - true - )->withContentBeforeForm( - $properties->getClozeText()->buildPanelForEditing( - $environment->getUIFactory(), - $environment->getLanguage(), - $properties->getGaps(), - $properties->getLegacyClozeText() - ) - ); - } - - private function processAssignPointsForm( - Environment $environment - ): EditForm|Properties { - $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( - $environment, - $environment->getAnswerFormProperties() - ); - - $form = $this->buildAssignPointsForm( - $environment, - $inputs_builder_for_points - )->withRequest($environment->getHttpServices()->request()); - - $data = $form->getData(); - if ($data === null) { - $inputs_builder_for_points->persistCarry(); - return $form; - } - - return $data; - } - - private function buildInputsBuilderForPointsForm( - Environment $environment, - Properties $properties - ): InputsBuilderSession { - return $environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $environment->getRefinery()->custom()->transformation( - function (?string $carry) use ( - $environment, - $properties - ): Section { - $properties_from_carry = $this->properties_factory - ->fromCarry( - $properties, - $carry - ); - return $properties_from_carry->getGaps() - ->buildPointInputs( - $environment->getLanguage(), - $environment->getUIFactory()->input()->field(), - $properties_from_carry, - $environment->isInCreationContext(), - $environment->getTableRowIds() - ); - } - ) - ); - } - private function buildPostTarget( Environment $environment, string $next_step ): URLBuilder { - if ($this->start_sub_action !== null) { - $next_step = "{$next_step}_{$this->start_sub_action}"; - } - - return $environment->withSubActionParameter($next_step)->getUrlBuilder(); - } - - private function determineStartSubActionFromSubAction( - ?string $start_sub_action_from_get - ): ?string { - if ($start_sub_action_from_get !== null) { - return $start_sub_action_from_get; - } - - if ($this->sub_action === self::SUB_ACTION_JUMP_TO_SET_GAP_TYPES) { - return self::SUB_ACTION_BACK_TO_SET_GAP_TYPES; - } - - if ($this->sub_action === self::SUB_ACTION_JUMP_TO_SET_ANSWER_OPTIONS) { - return self::SUB_ACTION_BACK_TO_SET_ANSWER_OPTIONS; - } - - return null; + return $environment + ->withSubActionParameter($next_step) + ->getUrlBuilder(); } private function buildRetrievePropertiesTransformation( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php index 8dc766aadd26..b77e2ee1b97e 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Actor.php @@ -25,7 +25,7 @@ interface Actor { public function can( - string $sub_action + string $action ): bool; public function do( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index d0b6bd5805a6..36845b35222a 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Presentation\Definitions; +use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; use ILIAS\Questions\Presentation\Views\Edit; @@ -38,12 +39,13 @@ class DefaultEnvironment implements Environment { private const array QUERY_PARAMETER_NAME_SPACE = ['q']; private const string TOKEN_STRING_ACTION = 'a'; - private const string TOKEN_STRING_SUB_ACTION = 's'; + private const string TOKEN_STRING_SUB_ACTION = 'sa'; private const string TOKEN_STRING_QUESTION_ID = 'q'; private const string TOKEN_STRING_TABLE_ROW_ID = 'r'; private const string TOKEN_STRING_TYPE_HASH = 't'; private const string TOKEN_STRING_ANSWER_FORM_ID = 'af'; private const string TOKEN_STRING_CREATE_MODE = 'cm'; + private const string TOKEN_STRING_FORM_START_SUB_ACTION = 'fssa'; private const string TOKEN_STRING_CREATE_AND_NEW = 'can'; private const string PARAMETER_STRING_HIER_ID = 'hier_id'; @@ -58,13 +60,14 @@ class DefaultEnvironment implements Environment private bool $is_in_creation_context = false; private URLBuilder $url_builder; - private readonly URLBuilderToken $action_token; - private readonly URLBuilderToken $sub_action_token; - private readonly URLBuilderToken $question_id_token; private readonly URLBuilderToken $table_row_token; + private readonly URLBuilderToken $question_id_token; + private ?URLBuilderToken $sub_action_token = null; + private ?URLBuilderToken $action_token = null; private ?URLBuilderToken $type_hash_token = null; private ?URLBuilderToken $answer_form_id_token = null; private ?URLBuilderToken $create_mode_token = null; + private ?URLBuilderToken $form_start_sub_action_token = null; private ?array $table_row_ids = null; @@ -158,8 +161,17 @@ public function withSubActionParameter( string $sub_action ): self { $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->sub_action_token, $sub_action); + if ($clone->sub_action_token === null) { + [ + $clone->url_builder, + $clone->sub_action_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_SUB_ACTION + ); + } + $clone->url_builder = $clone->url_builder + ->withParameter($clone->sub_action_token, $sub_action); return $clone; } @@ -174,9 +186,22 @@ public function withDefaultSubAction(): self #[\Override] public function getSubAction(): string { - return $this->default_sub_action - ? '' - : $this->retrieveStringValueForToken($this->sub_action_token, self::TOKEN_STRING_SUB_ACTION); + if ($this->default_sub_action) { + return ''; + } + + $sub_action_token = $this->sub_action_token; + if ($sub_action_token === null) { + [,$sub_action_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_SUB_ACTION + ); + } + + return $this->retrieveStringValueForToken( + $sub_action_token, + self::TOKEN_STRING_SUB_ACTION + ); } #[\Override] @@ -195,6 +220,25 @@ public function isCapabilityRequired( ); } + #[\Override] + public function getAnswerFormTableActionsForRequiredCapabilities(): array + { + return array_reduce( + $this->required_capabilities, + function (array $c, Capability $v): array { + $action = $v->getAnswerFormEditAdditionalStep($this); + if ($action !== null) { + $c[] = $action->getAsTableAction( + $this->withActionParameter($action->getIdentifier()) + ); + } + + return $c; + }, + [] + ); + } + #[\Override] public function isInCreationContext(): bool { @@ -253,23 +297,23 @@ public function getTableRowIdToken(): URLBuilderToken #[\Override] public function getTableRowIds(): array { - if ($this->table_row_ids !== null) { - return $this->table_row_ids; + if ($this->table_row_ids === null) { + $this->table_row_ids = $this->http->wrapper()->query()->retrieve( + $this->table_row_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + fn($v): string => $v !== '' + ? $this->refinery->kindlyTo()->string()->transform($v) + : throw new \UnexpectedValueException() + ) + ), + $this->refinery->always([]) + ]) + ); } - return $this->table_row_ids = $this->http->wrapper()->query()->retrieve( - $this->table_row_token->getName(), - $this->refinery->byTrying([ - $this->refinery->kindlyTo()->listOf( - $this->refinery->custom()->transformation( - fn($v): string => $v !== '' - ? $this->refinery->kindlyTo()->string()->transform($v) - : throw new \UnexpectedValueException() - ) - ), - $this->refinery->always([]) - ]) - ); + return $this->table_row_ids; } #[\Override] @@ -305,15 +349,31 @@ public function getObjId(): int public function getAction(): string { - return $this->retrieveStringValueForToken($this->action_token); + $action_token = $this->action_token; + if ($action_token === null) { + [,$action_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION + ); + } + return $this->retrieveStringValueForToken($action_token); } public function withActionParameter( string $action ): self { $clone = clone $this; - $clone->url_builder = $this->url_builder - ->withParameter($this->action_token, $action); + if ($clone->action_token === null) { + [ + $clone->url_builder, + $clone->action_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION + ); + } + $clone->url_builder = $clone->url_builder + ->withParameter($clone->action_token, $action); return $clone; } @@ -330,13 +390,15 @@ public function withAnswerFormTypeHashParameter( string $type_hash ): self { $clone = clone $this; - [ - $clone->url_builder, - $clone->type_hash_token - ] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_TYPE_HASH - ); + if ($clone->type_hash_token === null) { + [ + $clone->url_builder, + $clone->type_hash_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + } $clone->url_builder = $clone->url_builder ->withParameter($clone->type_hash_token, $type_hash); @@ -347,14 +409,15 @@ public function withAnswerFormIdParameter( Uuid $answer_form_id ): self { $clone = clone $this; - [ - $clone->url_builder, - $clone->answer_form_id_token - ] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ANSWER_FORM_ID - ); - + if ($clone->answer_form_id_token === null) { + [ + $clone->url_builder, + $clone->answer_form_id_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ANSWER_FORM_ID + ); + } $clone->url_builder = $clone->url_builder->withParameter( $clone->answer_form_id_token, $answer_form_id->toString() @@ -365,20 +428,52 @@ public function withAnswerFormIdParameter( public function withCreateModeParameter(): self { $clone = clone $this; - - [ - $clone->url_builder, - $clone->create_mode_token - ] = $this->url_builder->acquireParameter( - self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_CREATE_MODE - ); + if ($clone->create_mode_token === null) { + [ + $clone->url_builder, + $clone->create_mode_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + } $clone->url_builder = $clone->url_builder ->withParameter($clone->create_mode_token, '1'); return $clone; } + #[\Override] + public function withFormStartSubActionParameter( + string $sub_action + ): self { + $clone = clone $this; + if ($clone->form_start_sub_action_token === null) { + [ + $clone->url_builder, + $clone->form_start_sub_action_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_FORM_START_SUB_ACTION + ); + } + + $clone->url_builder = $clone->url_builder + ->withParameter( + $clone->form_start_sub_action_token, + $sub_action + ); + return $clone; + } + + #[\Override] + public function withPreservedFormStartSubActionParameter(): self + { + return $this->withFormStartSubActionParameter( + $this->getFormStartSubAction() + ); + } + public function getQuestionId(): ?Uuid { return $this->http->wrapper()->query()->retrieve( @@ -437,6 +532,18 @@ public function getTypeClassHash(): string return $this->retrieveStringValueForToken($type_hash_token); } + public function getFormStartSubAction(): string + { + $form_start_command_token = $this->form_start_sub_action_token; + if ($form_start_command_token === null) { + [,$form_start_command_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_FORM_START_SUB_ACTION + ); + } + return $this->retrieveStringValueForToken($form_start_command_token); + } + public function isCreateModeSimple(): bool { $create_mode_token = $this->create_mode_token; @@ -553,13 +660,11 @@ private function buildEditAnswerFormBackUrl(): URLBuilder ); } - return $this->url_builder->withParameter( - $this->action_token, + return $this->withActionParameter( Edit::ACTION_DELETE_QUESTIONS - )->withParameter( - $this->sub_action_token, + )->withSubActionParameter( Edit::ACTION_DELETE_QUESTIONS - )->withParameter( + )->getUrlBuilder()->withParameter( $this->table_row_token, [$this->getQuestionId()->toString()] ); @@ -570,17 +675,13 @@ private function acquireURLBuilderAndParameters( ): void { [ $this->url_builder, - $this->action_token, - $this->sub_action_token, - $this->question_id_token, - $this->table_row_token + $this->table_row_token, + $this->question_id_token ] = (new URLBuilder($base_uri)) ->acquireParameters( self::QUERY_PARAMETER_NAME_SPACE, - self::TOKEN_STRING_ACTION, - self::TOKEN_STRING_SUB_ACTION, - self::TOKEN_STRING_QUESTION_ID, - self::TOKEN_STRING_TABLE_ROW_ID + self::TOKEN_STRING_TABLE_ROW_ID, + self::TOKEN_STRING_QUESTION_ID ); } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index f8ca23f7e1ee..0714f978b9bc 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -69,6 +69,8 @@ public function isCapabilityRequired( string $capability ): bool; + public function getAnswerFormTableActionsForRequiredCapabilities(): array; + public function isInCreationContext(): bool; /** @@ -89,6 +91,14 @@ public function withSubActionParameter( string $sub_action ): self; + public function getFormStartSubAction(): string; + + public function withFormStartSubActionParameter( + string $form_start_command + ): self; + + public function withPreservedFormStartSubActionParameter(): self; + public function withPreservedTableRowIdsParameter(): self; public function redirectTo(URLBuilder $target): void; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 3a5b213a1c60..36554e638e73 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -39,6 +39,8 @@ class EditForm implements Renderable private StandardForm $form; + private bool $is_final_step = false; + private ?StandardPanel $content_before_form = null; private ?StandardPanel $content_after_form = null; private ?InterruptiveModal $confirmation = null; @@ -49,14 +51,12 @@ public function __construct( private readonly Language $lng, Input|InputsBuilder $inputs, private URLBuilder $default_form_action, - ?URLBuilder $back_form_action, - private bool $is_final_step + ?URLBuilder $back_form_action ) { $this->form = $this->buildForm( $inputs, $default_form_action, - $back_form_action, - $is_final_step + $back_form_action ); } @@ -72,6 +72,14 @@ public function isFinalStep(): bool return $this->is_final_step; } + public function withIsFinalStep( + bool $is_final_step + ): self { + $clone = clone $this; + $clone->is_final_step = $is_final_step; + return $clone; + } + public function withContentBeforeForm( StandardPanel $content ): self { @@ -167,7 +175,11 @@ function ($id) { $content[] = $this->insert_legacy_text_button; } - $content[] = $this->form; + $content[] = $this->is_final_step + ? $this->form + : $this->form->withSubmitLabel( + $this->lng->txt('next') + ); if ($this->content_after_form !== null) { $content[] = $this->content_after_form; @@ -179,8 +191,7 @@ function ($id) { private function buildForm( Input|InputsBuilder $inputs, URLBuilder $default_form_action, - ?URLBuilder $back_form_action, - bool $is_final_step + ?URLBuilder $back_form_action ): StandardForm { if ($inputs instanceof InputsBuilder) { $inputs = $inputs->getInputs(); @@ -192,20 +203,13 @@ private function buildForm( ] ); - if ($back_form_action !== null) { - $form = $form->withAdditionalFormAction( - $back_form_action->buildURI()->__toString(), - $this->lng->txt('previous') - ); - } - - $submit_action_label = 'next'; - if ($is_final_step) { - $submit_action_label = 'save'; + if ($back_form_action === null) { + return $form; } - return $form->withSubmitLabel( - $this->lng->txt($submit_action_label) + return $form->withAdditionalFormAction( + $back_form_action->buildURI()->__toString(), + $this->lng->txt('previous') ); } } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php index 61c1ad2e370b..aead15450bb7 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -65,16 +65,14 @@ public function getEditOverview( public function getEditForm( Input|InputsBuilder $main_section_inputs, URLBuilder $default_form_action, - ?URLBuilder $back_form_action, - bool $is_final_step + ?URLBuilder $back_form_action ): EditForm { return new EditForm( $this->ui_factory, $this->lng, $main_section_inputs, $default_form_action, - $back_form_action, - $is_final_step + $back_form_action ); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 68f1f603d654..d591f29b22d9 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,9 +20,7 @@ namespace ILIAS\Questions\Presentation\Views; -use ILIAS\Questions\AnswerForm\Capabilities\Action; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\Capabilities\Factory as CapabilitesFactory; +use ILIAS\Questions\AnswerForm\Capabilities\Edit as CapabilitiesEditView; use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; @@ -62,7 +60,6 @@ class Edit private const string ACTION_CREATE_ANSWER_FORM = 'create_af'; public const string ACTION_OTHER_ANSWER_FORM = 'other_af'; - private array $required_capabilities = []; private Editability $editability = Editability::Full; private bool $ordering_enabled = false; @@ -81,7 +78,7 @@ public function __construct( private readonly \ilTabsGUI $tabs_gui, private readonly \ilUIService $ui_services, private readonly UuidFactory $uuid_factory, - private readonly CapabilitesFactory $capabilities_factory, + private CapabilitiesEditView $capabilities_edit_view, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, private readonly LayoutFactory $definitions_factory @@ -92,9 +89,8 @@ public function withRequiredCapabilities( array $capability_class_names ): self { $clone = clone $this; - $clone->required_capabilities = $this->buildCapabilities( - $capability_class_names - ); + $clone->capabilities_edit_view = $this->capabilities_edit_view + ->withRequiredCapabilities($capability_class_names); return $clone; } @@ -193,10 +189,8 @@ public function createAnswerForm( $base_uri, $obj_id )->withIsInCreationContext(true) - ->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) ->withQuestionIdParameter($question->getId()); - $environment->setEditAnswerFormBackTarget(); if ($this->configuration_repository->isCreateModeSimple($environment)) { @@ -249,55 +243,63 @@ public function editAnswerForm( )->withAnswerFormProperties($answer_form_properties) ->withQuestionIdParameter($question->getId()); - $capability_actions = array_filter( - array_map( - fn(Capability $v): ?Action => $v->getEditAction(), - $this->required_capabilities - ) - ); - - $environment->setEditAnswerFormTabs( - $capability_actions - ); - $action = $environment->getAction(); + $edit_view = $type_definition->getEditView(); - $capability_action = current( - array_filter( - $capability_actions, - fn(Action $v): bool => $v->isThis($action) - ) + $from_capabilites = $this->capabilities_edit_view->edit( + $environment, + $edit_view, + $action ); - if ($capability_action !== false) { - $capability_action->activateTab($this->tabs_gui); - return $capability_action->getCapability()->edit( - $environment->withActionParameter($action) + + if ($from_capabilites instanceof AnswerFormProperties) { + $this->updateAnswerFormAndRedirect( + $environment, + $question, + $from_capabilites ); } - $edit_view = $type_definition->getEditView(); + if ($from_capabilites !== null) { + return $from_capabilites; + } + + $environment->setEditAnswerFormTabs( + $this->capabilities_edit_view->getRequiredCapabilities() + ); if ($action === self::ACTION_OTHER_ANSWER_FORM) { - $environment = $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM); - $next = $edit_view->other($environment); - } else { - $next = $edit_view->edit($environment); + return $this->processOtherAnswerFormAction( + $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM), + $question, + $edit_view + ); } - if (!($next instanceof AnswerFormProperties)) { - return $next; + $from_edit_view = $edit_view->edit($environment); + if ($from_edit_view instanceof EditForm + && $this->capabilities_edit_view->providesAnswerFormEditAdditionalSteps()) { + return $from_edit_view->withIsFinalStep(false); } - $this->questions_repository->update( - [$question->withAnswerFormProperties($next)] - ); + if (!($from_edit_view instanceof AnswerFormProperties)) { + return $from_edit_view; + } - foreach ($this->required_capabilities as $capability) { - $capability->onAnswerFormUpdate($next); + $return_form_step_actions = $this->capabilities_edit_view + ->doFirstFormStepAction( + $environment->withAnswerFormProperties($from_edit_view), + $edit_view + ); + if ($return_form_step_actions instanceof Async + || $return_form_step_actions instanceof Renderable) { + return $return_form_step_actions; } - $this->ctrl->redirectToURL( - $environment->getUrlBuilder()->buildURI()->__toString() + $this->updateAnswerFormAndRedirect( + $environment, + $question, + $from_edit_view ); } @@ -472,31 +474,116 @@ private function forwardCreateAnswerFormCmd( \ilPCAnswerForm $content_object, AnswerFormEditView $answer_form_edit_view ): ?EditForm { - $create = $answer_form_edit_view->create( + $action = $environment->getAction(); + + $from_capabilites = $this->capabilities_edit_view->edit( + $environment, + $answer_form_edit_view, + $action + ); + + if ($from_capabilites instanceof EditForm) { + return $this->addSaveAndNewToAnswerFormCreateIfNeeded( + $environment, + $from_capabilites + ); + } + + if ($from_capabilites instanceof AnswerFormProperties) { + $this->createAnswerFormAndRedirect( + $environment, + $question->withAnswerFormProperties($from_capabilites), + $content_object + ); + } + + $from_edit_view = $answer_form_edit_view->create( $environment->withAnswerFormIdParameter( $environment->getAnswerFormId() ) ); - if ($create instanceof EditForm) { - return $create->isFinalStep() - ? $create->withAdditionalAction( - $environment->buildURLBuilderTokenForCreateAndNew(), - '1', - $this->lng->txt('save_and_new') - ) : $create; + if ($from_edit_view instanceof EditForm) { + return $this->addSaveAndNewToAnswerFormCreateIfNeeded( + $environment, + $this->capabilities_edit_view->providesAnswerFormEditAdditionalSteps() + ? $from_edit_view->withIsFinalStep(false) + : $from_edit_view + ); } + $from_capabilities_first_step = $this->capabilities_edit_view + ->doFirstFormStepAction( + $environment->withAnswerFormProperties($from_edit_view), + $answer_form_edit_view + ); + + if ($from_capabilities_first_step instanceof EditForm) { + return $this->addSaveAndNewToAnswerFormCreateIfNeeded( + $environment, + $from_capabilities_first_step + ); + } + + $this->createAnswerFormAndRedirect( + $environment, + $question->withAnswerFormProperties($from_capabilities_first_step), + $content_object + ); + } + + private function processOtherAnswerFormAction( + Environment $environment, + Question $question, + AnswerFormEditView $edit_view + ) { + $from_edit_view = $edit_view->other($environment); + + if (!($from_edit_view instanceof AnswerFormProperties)) { + return $from_edit_view; + } + + $this->updateAnswerFormAndRedirect( + $environment, + $question, + $from_edit_view + ); + } + + private function updateAnswerFormAndRedirect( + Environment $environment, + Question $question, + AnswerFormProperties $properties + ): never { + $this->questions_repository->update( + [$question->withAnswerFormProperties($properties)] + ); + + $this->capabilities_edit_view->onAnswerFormUpdate($properties); + + $this->ctrl->redirectToURL( + $environment->getUrlBuilder()->buildURI()->__toString() + ); + } + + private function createAnswerFormAndRedirect( + Environment $environment, + Question $question, + \ilPCAnswerForm $content_object + ): never { $this->questions_repository->create( - [$question->withAnswerFormProperties($create)] + [$question] ); - $content_object->create($create->getAnswerFormId()); + $content_object->create( + $properties->getAnswerFormId() + ); $content_object->getPage()->update(); $this->ctrl->redirectToURL( $this->buildAfterAnswerFormCreationRedirectUri($environment) ); + } private function initializeEditMode( @@ -611,9 +698,10 @@ private function buildCreateAnswerForm( ], $this->lng->txt('create_answer_form') ), - $environment->getUrlBuilder(), - null, - false + $environment->withActionParameter( + self::ACTION_CREATE_ANSWER_FORM + )->getUrlBuilder(), + null ); } @@ -638,27 +726,6 @@ private function buildAffectedItems( return $affected_items; } - /** - * @param list $capabilities - * @return list<\ILIAS\Questions\AnswerForm\Capabilities\Capability> - */ - private function buildCapabilities( - array $capabilities - ): array { - return array_map( - function (string $v): Capability { - $capability = $this->capabilities_factory->get($v); - if ($capability === null) { - throw new \InvalidArgumentException( - "The capability {$v} does not exist." - ); - } - return $capability; - }, - $capabilities - ); - } - private function deleteSelectedQuestions( array $question_ids ): void { @@ -676,6 +743,21 @@ private function deleteSelectedQuestions( $this->questions_repository->delete($questions_to_delete); } + private function addSaveAndNewToAnswerFormCreateIfNeeded( + Environment $environment, + EditForm $edit_form + ): EditForm { + if ($edit_form->isFinalStep() && $environment->isCreateModeSimple()) { + $edit_form = $edit_form->withAdditionalAction( + $environment->buildURLBuilderTokenForCreateAndNew(), + '1', + $this->lng->txt('save_and_new') + ); + } + + return $edit_form; + } + private function buildEnvironment( URI $base_uri, int $obj_id @@ -689,7 +771,7 @@ private function buildEnvironment( $this->uuid_factory, $this->definitions_factory, $this->editability, - $this->required_capabilities, + $this->capabilities_edit_view->getRequiredCapabilities(), $base_uri, $obj_id ); diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index b510680fb04c..cb026eca4f99 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -112,8 +112,7 @@ private function buildBasicPropertiesCreateForm( $environment ->withSubActionParameter(self::CMD_SAVE_QUESTION) ->getUrlBuilder(), - null, - false + null ); } @@ -150,9 +149,8 @@ private function buildBasicPropertiesEditingForm( $environment ->withSubActionParameter(self::CMD_SAVE_QUESTION) ->getUrlBuilder(), - null, - true - ); + null + )->withIsFinalStep(true); } private function processBasicPropertiesEditingForm( From 2cd46a2144ca4c338ffa8c7f437e5668fb3bd882 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 8 Apr 2026 15:02:11 +0200 Subject: [PATCH 090/108] Questions: Add Missing Parameters to Function Call --- .../src/Question/Persistence/Repository.php | 3 +-- .../ILIAS/Questions/src/Question/Question.php | 17 +++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 7be1c67ef476..db83c8a261f8 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -207,8 +207,7 @@ public function delete( => $v->toDelete( $c, $this->persistence_factory, - $this->question_table_definitions, - $this->answer_form_generic_table_definitions + $this->question_table_definitions ), $this->buildManipulate( ManipulationType::Delete diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 1daef99539ae..69de653e224b 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -322,7 +322,8 @@ public function toStorage( ? $this->addInsertStatementsToManipulation( $manipulate, $persistence_factory, - $question_tables_definitions + $question_tables_definitions, + $answer_form_generic_table_definitions ) : $this->addUpdateStatementsToManipulation( $manipulate, $persistence_factory, @@ -334,8 +335,7 @@ public function toStorage( public function toDelete( Manipulate $manipulate, PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + QuestionTableDefinitions $question_tables_definitions ): Manipulate { $table_name_builder = $manipulate->getTableNameBuilder(null); @@ -360,7 +360,6 @@ public function toDelete( ) ), $persistence_factory, - $answer_form_generic_table_definitions, $this->answer_forms ); } @@ -368,7 +367,8 @@ public function toDelete( private function addInsertStatementsToManipulation( Manipulate $manipulate, PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions ): Manipulate { if ($this->created === null) { $manipulate = $manipulate @@ -390,6 +390,8 @@ private function addInsertStatementsToManipulation( if ($this->updated_answer_forms !== []) { return $this->addAnswerFormStatementsToManipulate( $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, $this->updated_answer_forms ); } @@ -397,6 +399,8 @@ private function addInsertStatementsToManipulation( if ($this->answer_forms !== []) { return $this->addAnswerFormStatementsToManipulate( $manipulate, + $persistence_factory, + $answer_form_generic_table_definitions, $this->answer_forms ); } @@ -447,7 +451,6 @@ private function addUpdateStatementsToManipulation( $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate, $persistence_factory, - $answer_form_generic_table_definitions, $this->deleted_answer_forms ); } @@ -494,7 +497,6 @@ function ( private function addDeleteAnswerFormsStatementsToManipulate( Manipulate $manipulate, PersistenceFactory $persistence_factory, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_forms_to_delete ): Manipulate { return array_reduce( @@ -502,7 +504,6 @@ private function addDeleteAnswerFormsStatementsToManipulate( fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( $v->getTypeGenericProperties()->toDelete( $persistence_factory, - $answer_form_generic_table_definitions, $c, ) ), From 38b5c9ec21c0408755a94a07b8256443cd7efe6c Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 8 Apr 2026 17:15:13 +0200 Subject: [PATCH 091/108] Questions: Fix Tabs Handling --- .../src/AnswerForm/Capabilities/Edit.php | 18 ++---------------- .../Questions/src/Presentation/Views/Edit.php | 6 ++---- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php index a0207454163d..11e51aedaac6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php @@ -66,34 +66,21 @@ public function onAnswerFormUpdate( } public function edit( + \ilTabsGUI $tabs_gui, DefaultEnvironment $environment, AnswerFormEditView $edit_view, string $action_from_get -<<<<<<< HEAD - ): Async|Renderable|AnswerFormProperties|null { - $action = $this->getCurrentActionFromActionsArray( - $this->required_actions_with_tab, - $action_from_get - ); - -======= ): EditForm|AnswerFormProperties|null { $environment->setEditAnswerFormTabs( $this->required_actions_with_tab ); $action = $this->required_actions_with_tab[$action_from_get] ?? null; ->>>>>>> 4a2133216e4 (Questions: Move Marking to Capability) if ($action !== null) { - $action->activateTab($this->tabs_gui); + $action->activateTab($tabs_gui); } else { -<<<<<<< HEAD - $action = $this->addPreviousAndNextStepToFormAction( - $environment, -======= $environment->setEditAnswerFormBackTarget(); $action = $this->retrieveNextFormStepFromActionIdentifier( ->>>>>>> 4a2133216e4 (Questions: Move Marking to Capability) $edit_view, $action_from_get ); @@ -103,7 +90,6 @@ public function edit( return null; } - $environment->setEditAnswerFormBackTarget(); return $action->do( $environment->withActionParameter($action_from_get) ); diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index d591f29b22d9..1f1ed606db61 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -247,6 +247,7 @@ public function editAnswerForm( $edit_view = $type_definition->getEditView(); $from_capabilites = $this->capabilities_edit_view->edit( + $this->tabs_gui, $environment, $edit_view, $action @@ -264,10 +265,6 @@ public function editAnswerForm( return $from_capabilites; } - $environment->setEditAnswerFormTabs( - $this->capabilities_edit_view->getRequiredCapabilities() - ); - if ($action === self::ACTION_OTHER_ANSWER_FORM) { return $this->processOtherAnswerFormAction( $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM), @@ -477,6 +474,7 @@ private function forwardCreateAnswerFormCmd( $action = $environment->getAction(); $from_capabilites = $this->capabilities_edit_view->edit( + $this->tabs_gui, $environment, $answer_form_edit_view, $action From e33c3b9c1f63a383f1af93195398d9f3243a908a Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 6 May 2026 15:17:52 +0200 Subject: [PATCH 092/108] Questions: Add Filter, Range, and Order to Table --- .../Questions/src/AnswerForm/Factory.php | 2 +- .../ILIAS/Questions/src/Persistence/Query.php | 2 +- .../Definitions/OverviewTableColumns.php | 98 ++++++++++ .../Presentation/Layout/QuestionsTable.php | 64 +++---- .../Questions/src/Presentation/Views/Edit.php | 4 +- .../src/Question/Persistence/Repository.php | 172 ++++++++++++++---- .../ILIAS/Questions/src/Question/Question.php | 41 +++-- 7 files changed, 301 insertions(+), 82 deletions(-) create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php diff --git a/components/ILIAS/Questions/src/AnswerForm/Factory.php b/components/ILIAS/Questions/src/AnswerForm/Factory.php index 98e9545862a4..3e34366fe656 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Factory.php +++ b/components/ILIAS/Questions/src/AnswerForm/Factory.php @@ -89,7 +89,7 @@ public function getHashedClass(string $class): string return md5($class); } - public function buildTypeDefinitionFromSelectValue( + public function getTypeDefinitionFromSelectValue( string $value ): Definition { $type_definition = $this->available_answer_form_types[$value] ?? null; diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index eec66941b48e..8f8599cfbd91 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -194,7 +194,7 @@ function (?string $c, Where $v): string { return "WHERE {$v->toSql()}" . PHP_EOL; } - return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + return "{$c}{$v->getLogicalOperator()->value} {$v->toSql()}" . PHP_EOL; } ) ?? ''; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php new file mode 100644 index 000000000000..875ff8edd89d --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php @@ -0,0 +1,98 @@ +value + => $column_factory->link($lng->txt('title')), + OverviewTableColumns::AnswerFormTypes->value + => $column_factory->text( + $lng->txt('contained_types') + )->withIsOptional(true, true) + ->withIsSortable(false), + ]; + } + + public static function getFilderInputs( + Language $lng, + FieldFactory $field_factory, + array $answer_form_types_array_for_select + ): array { + return [ + self::Title->value => $field_factory->text( + $lng->txt('title') + ), + self::AnswerFormTypes->value => $field_factory->multiSelect( + $lng->txt('contains_type'), + $answer_form_types_array_for_select + )->withRequired(true), + ]; + } + + public function getDatabaseColumn( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): ?Column { + return $persistence_factory->column( + $persistence_factory->table( + $table_name_builder, + match($this) { + self::Title => TableTypes::Questions, + self::AnswerFormTypes => AnswerFormGenericTableTypes::AnswerForms + } + ), + $this->value + ); + } + + public function transformFilterValue( + AnswerFormFactory $answer_form_factory, + mixed $value + ): mixed { + return match($this) { + self::AnswerFormTypes => array_map( + fn(string $v): string => $answer_form_factory + ->getTypeDefinitionFromSelectValue($v)::class, + $value + ), + default => $value + }; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 1d544095ea09..3736adcb0295 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; +use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\Range; @@ -82,7 +83,11 @@ public function getRows( $environment_with_action = $this->environment->withActionParameter( Edit::ACTION_EDIT_QUESTION ); - foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { + foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions( + $range, + $order, + $filter_data + ) as $question) { yield $question->toTableRow( $row_builder, $environment_with_action @@ -96,44 +101,43 @@ public function getTotalRowCount( mixed $filter_data, mixed $additional_parameters ): ?int { - return 0; + return $this->questions_repository->getQuestionsCount(); } private function buildContent(): array { + $filter = $this->buildFilter( + $this->environment->getUrlBuilder()->buildURI()->__toString() + ); + return [ - $this->buildFilter($this->environment->getUrlBuilder()->buildURI()->__toString()), + $filter, $this->environment->getUIFactory()->table()->data( $this, $this->environment->getLanguage()->txt('questions'), - $this->getColums(), + OverviewTableColumns::getTableColums( + $this->environment->getLanguage(), + $this->environment->getUIFactory()->table()->column() + ), )->withActions( $this->getActions() )->withRange(new Range(0, 20)) - ->withRequest($this->environment->getHttpServices()->request()) + ->withFilter( + $this->ui_service->filter()->getData($filter) + )->withRequest($this->environment->getHttpServices()->request()) ]; } private function buildFilter( string $action ): Filter { - $question_type_options = [ - '' => $this->environment->getLanguage()->txt('filter_all_question_types') - ]; - - $field_factory = $this->environment->getUIFactory()->input()->field(); - $filter_inputs = [ - 'title' => $field_factory->text( - $this->environment->getLanguage()->txt('title') - ), - 'contains_type' => $field_factory->select( - $this->environment->getLanguage()->txt('contains_type'), - $question_type_options + $this->answer_form_factory - ->getAnswerFormTypesArrayForSelect( - $this->environment->getLanguage() - ) - ), - ]; + $filter_inputs = OverviewTableColumns::getFilderInputs( + $this->environment->getLanguage(), + $this->environment->getUIFactory()->input()->field(), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect( + $this->environment->getLanguage() + ) + ); $active = array_fill(0, count($filter_inputs), true); @@ -145,19 +149,11 @@ private function buildFilter( true, true ); - return $filter; - } - - private function getColums(): array - { - $f = $this->environment->getUIFactory()->table()->column(); - return [ - 'title' => $f->link($this->environment->getLanguage()->txt('title')), - 'type' => $f->text( - $this->environment->getLanguage()->txt('question_type') - )->withIsOptional(true, true), - ]; + $request = $this->environment->getHttpServices()->request(); + return $request->getMethod() === 'POST' + ? $filter->withRequest($request) + : $filter; } private function getActions(): array diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 1f1ed606db61..3f92cdde9177 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -201,7 +201,7 @@ public function createAnswerForm( if ($answer_form_type_class_hash !== '') { $type_definition = $this->answer_form_factory - ->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash); + ->getTypeDefinitionFromSelectValue($answer_form_type_class_hash); return $this->forwardCreateAnswerFormCmd( $environment->withAnswerFormProperties( @@ -690,7 +690,7 @@ private function buildCreateAnswerForm( ->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(string $v): ?Definition => $this->answer_form_factory - ->buildTypeDefinitionFromSelectValue($v) + ->getTypeDefinitionFromSelectValue($v) ) ) ], diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index db83c8a261f8..f8e70994947a 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -29,10 +29,13 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Operator; +use ILIAS\Questions\Persistence\Order; +use ILIAS\Questions\Persistence\OrderDirection; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\Order as DataOrder; use ILIAS\Data\Range as DataRange; @@ -44,7 +47,7 @@ class Repository { public const string COMPONENT_NAMESPACE = 'qsts'; - private readonly TableNameBuilder $question_table_names_builder; + private readonly TableNameBuilder $core_table_names_builder; public function __construct( private readonly \ilDBInterface $db, @@ -55,7 +58,7 @@ public function __construct( private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, private readonly AnswerFormFactory $answer_form_factory ) { - $this->question_table_names_builder = new TableNameBuilder( + $this->core_table_names_builder = new TableNameBuilder( self::COMPONENT_NAMESPACE, null ); @@ -73,18 +76,47 @@ public function getNew( /** * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ - public function getQuestionDataOnlyForAllQuestions(): \Generator - { - foreach ($this->buildQuestionsQuery()->withGroupBy( + public function getQuestionDataOnlyForAllQuestions( + ?DataRange $range = null, + ?DataOrder $order = null, + array $filter_data = [] + ): \Generator { + $questions_query = $this->addFilterToQuery( + $this->buildQuestionsQuery( + $this->buildMainOrder($order) + ), + $filter_data + ); + + if ($range !== null) { + $questions_query = $questions_query->withRange($range); + } + + + + foreach ($questions_query->withGroupBy( $this->buildGroupByColumn() )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, - [] + $this->retrieveAnswerFormsFromQuery($query_with_record, true) ); } } + public function getQuestionsCount(): int + { + $id_column = $this->question_table_definitions->getIdColumn( + $this->core_table_names_builder, + TableTypes::Questions + ); + return $this->db->fetchObject( + $this->db->query( + "SELECT count({$id_column->getColumnString()}) cnt FROM {$id_column->getTableName()}", + ) + )->cnt; + } + /** * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> */ @@ -94,7 +126,7 @@ public function getQuestionDataOnlyForQuestionIds( foreach ($this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( $this->question_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions ), $this->persistence_factory->value( @@ -111,7 +143,7 @@ public function getQuestionDataOnlyForQuestionIds( )->loadNextRecord() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, - [] + $this->retrieveAnswerFormsFromQuery($query_with_record, true) ); } } @@ -123,7 +155,7 @@ public function getForQuestionId( $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( $this->question_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions ), $this->persistence_factory->value( @@ -149,7 +181,7 @@ public function getForQuestionIds( $this->buildQuestionsQuery()->withAdditionalWhere( $this->persistence_factory->where( $this->question_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions ), $this->persistence_factory->value( @@ -232,7 +264,7 @@ private function getForBaseQuery( fn(Query $c, AnswerFormDefinition $v) => $v->getTableDefinitions()->completeQuery( $c, $this->answer_form_generic_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, AnswerFormGenericTableTypes::AnswerForms ) ), @@ -258,7 +290,7 @@ private function retrieveQuestionFromQuery( ): Question { $linking_info = $query->retrieveCurrentRecord( $this->persistence_factory->table( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Linking ), $this->refinery->identity() @@ -266,7 +298,7 @@ private function retrieveQuestionFromQuery( $question = $query->retrieveCurrentRecord( $this->persistence_factory->table( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions, ), $this->refinery->custom()->transformation( @@ -300,15 +332,16 @@ private function retrieveQuestionFromQuery( * @return array<\ILIAS\Questions\AnswerForms\Properties> */ private function retrieveAnswerFormsFromQuery( - Query $query + Query $query, + bool $only_generic_data = false ): array { return $query->retrieveCurrentRecord( $this->persistence_factory->table( - $this->question_table_names_builder, + $this->core_table_names_builder, AnswerFormGenericTableTypes::AnswerForms ), $this->refinery->custom()->transformation( - function (array $vs) use ($query): array { + function (array $vs) use ($query, $only_generic_data): array { if (count($vs) === 1 && $vs[0]['type'] === null) { return []; } @@ -324,7 +357,7 @@ function (array $vs) use ($query): array { ->getDefinitionForClass($data_set['type']); $answer_forms[] = $definition->buildProperties( $this->answer_form_factory->buildTypeGenericPropertiesFromDatabase($data_set), - $query + $only_generic_data ? null : $query ); } return $answer_forms; @@ -340,7 +373,7 @@ function (array $vs) use ($query): array { private function getAnswerFormTypesForQuestionIds( array $question_ids ): array { - $table_name = $this->question_table_names_builder + $table_name = $this->core_table_names_builder ->getTableNameFor(AnswerFormGenericTableTypes::AnswerForms); $query = $this->db->query( @@ -378,28 +411,79 @@ private function store( )->run(); } - private function buildQuestionsQuery(): Query - { + private function buildQuestionsQuery( + ?Order $main_sort_order = null + ): Query { + $base_query = new Query( + $this->db, + $this->refinery, + self::COMPONENT_NAMESPACE, + $this->persistence_factory->table( + $this->core_table_names_builder, + TableTypes::Linking + ) + ); + + if ($main_sort_order !== null) { + $base_query = $base_query->withAdditionalOrder($main_sort_order); + } + return $this->answer_form_generic_table_definitions->completeQuery( $this->question_table_definitions->completeQuery( - new Query( - $this->db, - $this->refinery, - self::COMPONENT_NAMESPACE, - $this->persistence_factory->table( - $this->question_table_names_builder, - TableTypes::Linking - ) - ), + $base_query, null ), $this->question_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions ) ); } + private function addFilterToQuery( + Query $question_query, + array $filter_data + ): Query { + foreach ($filter_data as $key => $value) { + if ($value === null || $value === '' || $value === []) { + continue; + } + + $column_definition = OverviewTableColumns::tryFrom($key); + + $column = $column_definition?->getDatabaseColumn( + $this->persistence_factory, + $this->core_table_names_builder, + $this->answer_form_factory + ); + if ($column === null) { + continue; + } + + $operator = Operator::Equal; + if (is_string($value)) { + $operator = Operator::Like; + $value = "%{$value}%"; + } + + $question_query = $question_query->withAdditionalWhere( + $this->persistence_factory->where( + $column, + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $column_definition->transformFilterValue( + $this->answer_form_factory, + $value + ) + ), + $operator + ) + ); + } + + return $question_query; + } + private function buildManipulate( ManipulationType $manipulation_type ): Manipulate { @@ -410,10 +494,32 @@ private function buildManipulate( ); } + private function buildMainOrder( + ?DataOrder $order + ): ?Order { + if ($order === null) { + return null; + } + + $order_array = $order->get(); + + return $this->persistence_factory->order( + OverviewTableColumns::tryFrom( + array_key_first($order_array) + )->getDatabaseColumn( + $this->persistence_factory, + $this->core_table_names_builder + ), + OrderDirection::tryFrom( + array_shift($order_array) + ) + ); + } + private function buildGroupByColumn(): Column { return $this->question_table_definitions->getIdColumn( - $this->question_table_names_builder, + $this->core_table_names_builder, TableTypes::Questions ); } @@ -431,7 +537,7 @@ private function buildAvailableUuid(): Uuid private function checkAvailabilityOfId( Uuid $uuid ): bool { - $table_name = $this->question_table_names_builder + $table_name = $this->core_table_names_builder ->getTableNameFor(TableTypes::Questions); return $this->db->fetchObject( @@ -471,7 +577,7 @@ private function getNextAvailableQuestionPageId(): int private function migrateQuestionPage( Question $question ): Question { - $table_name = $this->question_table_names_builder + $table_name = $this->core_table_names_builder ->getTableNameFor(TableTypes::MigrationsTable); $old_page_id = $this->db->fetchObject( diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 69de653e224b..ed100708aa17 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -33,6 +33,7 @@ use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; +use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; @@ -68,9 +69,7 @@ public function __construct( private ?Uuid $original_id = null, private ?\DateTimeImmutable $last_update = null, private readonly ?\DateTimeImmutable $created = null, - array $answer_forms = [], - private ?Taxonomies $taxonomies = null, - private ?ContentForRecapitulation $content_for_recapitulation = null + array $answer_forms = [] ) { $this->answer_forms = array_reduce( $answer_forms, @@ -300,14 +299,34 @@ public function toTableRow( return $row_builder->buildDataRow( $this->id->toString(), [ - 'title' => $environment->getUIFactory()->link()->standard( - $this->title, - $environment->withQuestionIdParameter( - $this->id - )->getUrlBuilder() - ->buildURI() - ->__toString() - ) + OverviewTableColumns::Title->value => $environment + ->getUIFactory()->link()->standard( + $this->title, + $environment->withQuestionIdParameter( + $this->id + )->getUrlBuilder() + ->buildURI() + ->__toString() + ), + OverviewTableColumns::AnswerFormTypes->value => implode( + '
', + array_reduce( + $this->answer_forms, + function ( + array $c, + AnswerFormProperties $v + ) use ($environment): array { + $type_label = $v->getDefinition()->getLabel( + $environment->getLanguage() + ); + if (!in_array($type_label, $c)) { + $c[] = $type_label; + } + return $c; + }, + [] + ) + ) ] ); } From 8cbab06d3049f519eb44d183a5dde7c194ce45c2 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 8 May 2026 13:32:42 +0200 Subject: [PATCH 093/108] Questions: Move From Renderable to Viewable --- .../class.ilObjQuestionsGUI.php | 11 +-- .../PageEditor/class.ilPCAnswerFormGUI.php | 36 ++++---- .../AnswerForm/Capabilities/ActionWithTab.php | 4 +- .../Capabilities/Feedback/Capability.php | 4 +- .../Capabilities/Feedback/Overview.php | 12 ++- .../SuggestedLearningContent/Overview.php | 61 ++++++-------- .../SuggestedLearningContent.php | 4 +- .../Questions/src/AnswerForm/Views/Edit.php | 4 +- .../Combinations/EditCombinations.php | 4 +- .../Properties/Combinations/Overview.php | 12 ++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 4 +- .../src/Presentation/Layout/Async.php | 4 +- .../src/Presentation/Layout/EditForm.php | 83 +++++++++---------- .../src/Presentation/Layout/EditOverview.php | 12 +-- .../Presentation/Layout/QuestionsTable.php | 23 +++-- .../Layout/{Renderable.php => Viewable.php} | 12 +-- .../Questions/src/Presentation/Views/Edit.php | 73 +++++++++------- .../ILIAS/Questions/src/Question/Question.php | 11 ++- 18 files changed, 190 insertions(+), 184 deletions(-) rename components/ILIAS/Questions/src/Presentation/Layout/{Renderable.php => Viewable.php} (79%) diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 9580b6e3bc75..260afefa98f8 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -135,11 +135,12 @@ public function viewQuestionsObject(): void $this->tabs_gui->activateTab('questions'); $this->tpl->setContent( - $this->edit_view->show( - $this->buildEditQuestionsBaseUri(), - $this->object->getId(), - $this->object->getRefId() - )->render($this->ui_renderer) + $this->ui_renderer->render( + $this->edit_view->getUI( + $this->buildEditQuestionsBaseUri(), + $this->object->getRefId() + ) + ) ); } diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php index 3ad7d3715e3f..4edb44721193 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -59,14 +59,15 @@ public function insertCmd(): void $content_obj = new ilPCAnswerForm($this->pg_obj); $content_obj->setHierId($this->hier_id); $this->tpl->setContent( - $this->edit_view->createAnswerForm( - $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'insert') - ), - $this->pg_obj->getParentId(), - $this->pg_obj->getQuestion(), - $content_obj - )->render($this->ui_renderer) + $this->ui_renderer->render( + $this->edit_view->getCreateAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'insert') + ), + $this->pg_obj->getQuestion(), + $content_obj + ) + ) ); } @@ -79,15 +80,16 @@ public function editCmd(): void ); $this->tpl->setContent( - $this->edit_view->editAnswerForm( - $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'edit') - ), - $this->pg_obj->getParentId(), - $question, - $answer_form_properties, - $answer_form_properties->getDefinition() - )->render($this->ui_renderer) + $this->ui_renderer->render( + $this->edit_view->getEditAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'edit') + ), + $question, + $answer_form_properties, + $answer_form_properties->getDefinition() + ) + ) ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php index bd5d3af375b9..8cc9584c9cd3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php @@ -22,7 +22,7 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; class ActionWithTab { @@ -43,7 +43,7 @@ public function __construct( public function do( Environment $environment - ): Async|Renderable { + ): Async|Viewable { return $this->do->__invoke($environment); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php index a4d870cc2364..49f5b836384c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php @@ -25,7 +25,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Text\Factory as TextFactory; class Capability implements CapabilityInterface @@ -101,7 +101,7 @@ private function buildDoEditActionClosure(): \Closure { return function ( Environment $environment - ): Async|Renderable { + ): Async|Viewable { $sub_action = $environment->getSubAction(); return match ($sub_action) { '' => $this->buildOverview($environment), diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php index 2ba2c1f86f3d..5822908e0d99 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php @@ -22,14 +22,13 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Text\Factory as TextFactory; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; -use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\URLBuilder; -class Overview implements Renderable +class Overview implements Viewable { private bool $set_legacy_texts_as_values = false; @@ -51,9 +50,8 @@ public function __construct( } #[\Override] - public function render( - UIRenderer $ui_renderer - ): string { + public function getUI(): array + { $ui_factory = $this->environment->getUIFactory(); $lng = $this->environment->getLanguage(); @@ -92,7 +90,7 @@ public function render( if ($this->modal !== null) { $content[] = $this->modal; } - return $ui_renderer->render($content); + return $content; } public function withLegacyTextsAsValues( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php index b036d5d6317b..f98a653a7fac 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php @@ -23,7 +23,7 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\StaticURL\Services as StaticURLServices; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; @@ -31,9 +31,8 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Panel\Standard as StandardPanel; use ILIAS\UI\Component\Prompt\Prompt; -use ILIAS\UI\Renderer as UIRenderer; -class Overview implements Renderable +class Overview implements Viewable { private const string SUB_ACTION_SELECT_TYPE = 'st'; private const string SUB_ACTION_SELECT_CONTENT = 'sc'; @@ -52,38 +51,7 @@ public function __construct( } #[\Override] - public function render( - UIRenderer $ui_renderer - ): string { - return $ui_renderer->render([ - $this->buildPanel() - ]); - } - - public function doAction( - string $action - ): Async { - return match($action) { - self::SUB_ACTION_SELECT_TYPE => $this->buildPromptShowAsync( - $this->buildSelectTypeForm() - ), - self::SUB_ACTION_SELECT_CONTENT => $this->processSelectTypeForm(), - self::SUB_ACTION_SAVE_CONTENT => $this->processSelectContentForm(), - self::SUB_ACTION_SAVE_SUB_CONTENT => $this->processSelectSubContentForm(), - default => $this->forwardActionToActors($action) - }; - } - - private function buildPrompt(): Prompt - { - return $this->environment->getUIFactory()->prompt()->standard( - $this->environment->withSubActionParameter( - self::SUB_ACTION_SELECT_TYPE - )->getUrlBuilder()->buildURI() - ); - } - - private function buildPanel(): StandardPanel + public function getUI(): StandardPanel { $content = [ $this->environment->getUIFactory()->listing()->descriptive( @@ -112,6 +80,29 @@ private function buildPanel(): StandardPanel ); } + public function doAction( + string $action + ): Async { + return match($action) { + self::SUB_ACTION_SELECT_TYPE => $this->buildPromptShowAsync( + $this->buildSelectTypeForm() + ), + self::SUB_ACTION_SELECT_CONTENT => $this->processSelectTypeForm(), + self::SUB_ACTION_SAVE_CONTENT => $this->processSelectContentForm(), + self::SUB_ACTION_SAVE_SUB_CONTENT => $this->processSelectSubContentForm(), + default => $this->forwardActionToActors($action) + }; + } + + private function buildPrompt(): Prompt + { + return $this->environment->getUIFactory()->prompt()->standard( + $this->environment->withSubActionParameter( + self::SUB_ACTION_SELECT_TYPE + )->getUrlBuilder()->buildURI() + ); + } + private function forwardActionToActors( string $action ): Async { diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php index fe8970234d0c..c7d7b4fc6f0e 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php @@ -25,7 +25,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\StaticURL\Services as StaticURLServices; class SuggestedLearningContent implements CapabilityInterface @@ -85,7 +85,7 @@ private function buildDoEditActionClosure(): \Closure { return function ( Environment $environment - ): Async|Renderable { + ): Async|Viewable { $sub_action = $environment->getSubAction(); $overview = $this->buildOverview($environment); if ($sub_action === '') { diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index 1b5bba381c68..c34071e081eb 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -25,7 +25,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; interface Edit { @@ -39,7 +39,7 @@ public function edit( public function other( Environment $environment - ): Async|Renderable|Properties; + ): Async|Viewable|Properties; public function backToLastEditCommand( Environment $environment diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php index ab390aecd6a8..c584a585bb2c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -23,7 +23,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; class EditCombinations { @@ -47,7 +47,7 @@ public function addCombinationsSubTab( public function show( Environment $environment - ): Async|Renderable|Properties { + ): Async|Viewable|Properties { $environment->addEditAnswerFormSubTab( self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW, self::LANG_VAR_EDIT_COMBINATIONS diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index 4e6ba7467a89..e128c0f36b3e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -27,7 +27,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilder; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Range; use ILIAS\Data\Order; use ILIAS\UI\Component\Input\Field\Section; @@ -36,9 +36,8 @@ use ILIAS\UI\Component\Table\Data as DataTable; use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRowBuilder; -use ILIAS\UI\Renderer as UIRenderer; -class Overview implements DataRetrieval, Renderable +class Overview implements Viewable, DataRetrieval { private const string SUB_ACTION_SAVE = 's'; private const string SUB_ACTION_SET_COMBINATION_VALUES = 'scv'; @@ -55,9 +54,8 @@ public function __construct( } #[\Override] - public function render( - UIRenderer $ui_renderer - ): string { + public function getUI(): array + { $modal = $this->buildSetCombinationGapsModal(); $content = [ $this->environment->getUIFactory()->button()->standard( @@ -70,7 +68,7 @@ public function render( if ($this->modal !== null) { $content[] = $this->modal; } - return $ui_renderer->render($content); + return $content; } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 400751735a31..8e57ac3faac9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -30,7 +30,7 @@ use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\InterruptiveItem\Standard as InterruptiveItem; @@ -106,7 +106,7 @@ public function edit( #[\Override] public function other( Environment $environment - ): Async|Renderable|Properties { + ): Async|Viewable|Properties { return $environment ->getAnswerFormProperties() ->getCombinations()->getEditView()->show($environment); diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Async.php b/components/ILIAS/Questions/src/Presentation/Layout/Async.php index 72181b2ee9ed..b61cd8c18b9e 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Async.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Async.php @@ -22,10 +22,10 @@ use ILIAS\Filesystem\Stream\Streams; use ILIAS\HTTP\Services as HttpService; -use ILIAS\UI\Component\Prompt\State\State; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; +use ILIAS\UI\Component\Prompt\State\State; use ILIAS\UI\Renderer as UIRenderer; class Async @@ -53,6 +53,4 @@ public function render( $this->http->sendResponse(); $this->http->close(); } - - } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php index 36554e638e73..efe26faeb70a 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -28,12 +28,11 @@ use ILIAS\UI\Component\Input\Input; use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal; use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; use Psr\Http\Message\ServerRequestInterface; -class EditForm implements Renderable +class EditForm implements Viewable { private const string MAIN_SECTION_NAME = 'form'; @@ -61,10 +60,43 @@ public function __construct( } #[\Override] - public function render( - UIRenderer $ui_renderer - ): string { - return $ui_renderer->render($this->buildContent()); + public function getUI(): array + { + $content = []; + + if ($this->content_before_form !== null) { + $content[] = $this->content_before_form; + } + + if ($this->confirmation !== null) { + $content[] = $this->confirmation->withOnLoad( + $this->confirmation->getShowSignal() + )->withAdditionalOnLoadCode( + function ($id) { + return "var button = {$id}.querySelector('input[type=\"submit\"]'); " + . "button.addEventListener('click', (e) => {e.preventDefault();" + . 'const form = button.closest("dialog").nextElementSibling;' + . "form.action = '{$this->confirmation->getFormAction()}';" + . 'form.submit();});'; + } + ); + } + + if ($this->insert_legacy_text_button !== null) { + $content[] = $this->insert_legacy_text_button; + } + + $content[] = $this->is_final_step + ? $this->form + : $this->form->withSubmitLabel( + $this->lng->txt('next') + ); + + if ($this->content_after_form !== null) { + $content[] = $this->content_after_form; + } + + return $content; } public function isFinalStep(): bool @@ -149,45 +181,6 @@ public function withAdditionalAction( return $clone; } - private function buildContent(): array - { - $content = []; - - if ($this->content_before_form !== null) { - $content[] = $this->content_before_form; - } - - if ($this->confirmation !== null) { - $content[] = $this->confirmation->withOnLoad( - $this->confirmation->getShowSignal() - )->withAdditionalOnLoadCode( - function ($id) { - return "var button = {$id}.querySelector('input[type=\"submit\"]'); " - . "button.addEventListener('click', (e) => {e.preventDefault();" - . 'const form = button.closest("dialog").nextElementSibling;' - . "form.action = '{$this->confirmation->getFormAction()}';" - . 'form.submit();});'; - } - ); - } - - if ($this->insert_legacy_text_button !== null) { - $content[] = $this->insert_legacy_text_button; - } - - $content[] = $this->is_final_step - ? $this->form - : $this->form->withSubmitLabel( - $this->lng->txt('next') - ); - - if ($this->content_after_form !== null) { - $content[] = $this->content_after_form; - } - - return $content; - } - private function buildForm( Input|InputsBuilder $inputs, URLBuilder $default_form_action, diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index 93e1fbc3d10e..70913bd458ed 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -23,10 +23,9 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\UI\Component\Panel\Standard as StandardPanel; -use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\URLBuilder; -class EditOverview implements Renderable +class EditOverview implements Viewable { public function __construct( private readonly Environment $environment, @@ -34,13 +33,8 @@ public function __construct( ) { } - public function render( - UIRenderer $ui_renderer - ): string { - return $ui_renderer->render($this->buildContent()); - } - - private function buildContent(): array + #[\Override] + public function getUI(): array { return [ $this->buildBasicAnswerFormPanel(), diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 3736adcb0295..71f8737614b6 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -33,7 +33,7 @@ use ILIAS\UI\Component\Button\Primary as PrimaryButton; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; -class QuestionsTable implements Renderable, DataRetrieval +class QuestionsTable implements Viewable, DataRetrieval { private ?PrimaryButton $create_question_button = null; @@ -46,20 +46,27 @@ public function __construct( $environment->getLanguage()->loadLanguageModule('qpl'); } - public function render( - UIRenderer $ui_renderer - ): string { + #[\Override] + public function getUI(): array + { - $rendered_content = ''; + $content = []; if ($this->create_question_button !== null) { $toolbar = new \ilToolbarGUI(); $toolbar->addComponent( $this->create_question_button ); - $rendered_content = $toolbar->getHTML(); + $content = [ + $this->environment->getUIFactory()->legacy()->content( + $toolbar->getHTML() + ) + ]; } - return $rendered_content . $ui_renderer->render($this->buildContent()); + + $content[] = $this->buildTable(); + + return $content; } public function withCreateQuestionButton( @@ -104,7 +111,7 @@ public function getTotalRowCount( return $this->questions_repository->getQuestionsCount(); } - private function buildContent(): array + private function buildTable(): array { $filter = $this->buildFilter( $this->environment->getUrlBuilder()->buildURI()->__toString() diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php b/components/ILIAS/Questions/src/Presentation/Layout/Viewable.php similarity index 79% rename from components/ILIAS/Questions/src/Presentation/Layout/Renderable.php rename to components/ILIAS/Questions/src/Presentation/Layout/Viewable.php index 522b6d0def13..a2cf6f879584 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/Viewable.php @@ -20,11 +20,13 @@ namespace ILIAS\Questions\Presentation\Layout; -use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\Component\Component; -interface Renderable +interface Viewable { - public function render( - UIRenderer $ui_renderer - ): string; + /** + * + * @return list + */ + public function getUI(): array|Component; } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 3f92cdde9177..959b214ec893 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -29,7 +29,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; -use ILIAS\Questions\Presentation\Layout\Renderable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; @@ -47,6 +47,7 @@ use ILIAS\HTTP\Services as HTTP; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\Component\Component; use ILIAS\UI\Component\Item\Group as ItemGroup; use ILIAS\UI\Component\MainControls\Slate\Legacy as LegacySlate; use ILIAS\Style\Content\Service as ContentStyle; @@ -110,11 +111,11 @@ public function withOrderingEnabled( return $clone; } - public function show( + public function getUI( URI $base_uri, int $obj_id, int $ref_id - ): Async|QuestionsTable|EditForm { + ): array|Component { $this->content_style->gui()->addCss( $this->global_tpl, $ref_id @@ -125,7 +126,7 @@ public function show( $obj_id ); - return match($environment->getAction()) { + $view = match($environment->getAction()) { self::ACTION_CREATE_QUESTION => $this->createQuestion( $environment->withIsInCreationContext(true) ), @@ -133,6 +134,12 @@ public function show( self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), default => $this->showTable($environment) }; + + if ($view instanceof Async) { + $view->render($this->ui_renderer); + } + + return $view->getUI(); } public function forwardPageCmds( @@ -179,17 +186,15 @@ public function forwardPageCmds( ); } - public function createAnswerForm( + public function getCreateAnswerForm( URI $base_uri, int $obj_id, Question $question, \ilPCAnswerForm $content_object - ): EditForm { - $environment = $this->buildEnvironment( - $base_uri, - $obj_id - )->withIsInCreationContext(true) - ->withQuestionIdParameter($question->getId()); + ): array|Component { + $environment = $this->buildEnvironment($base_uri) + ->withIsInCreationContext(true) + ->withQuestionIdParameter($question->getId()); $environment->setEditAnswerFormBackTarget(); @@ -230,18 +235,16 @@ public function createAnswerForm( }; } - public function editAnswerForm( + public function getEditAnswerForm( URI $base_uri, int $obj_id, Question $question, AnswerFormProperties $answer_form_properties, Definition $type_definition - ): Async|Renderable { - $environment = $this->buildEnvironment( - $base_uri, - $obj_id - )->withAnswerFormProperties($answer_form_properties) - ->withQuestionIdParameter($question->getId()); + ): array|Component { + $environment = $this->buildEnvironment($base_uri) + ->withAnswerFormProperties($answer_form_properties) + ->withQuestionIdParameter($question->getId()); $action = $environment->getAction(); $edit_view = $type_definition->getEditView(); @@ -261,8 +264,12 @@ public function editAnswerForm( ); } - if ($from_capabilites !== null) { - return $from_capabilites; + if ($from_capabilites instanceof Async) { + $from_capabilites->render($this->ui_renderer); + } + + if ($from_capabilites instanceof Viewable) { + return $from_capabilites->getUI(); } if ($action === self::ACTION_OTHER_ANSWER_FORM) { @@ -276,11 +283,15 @@ public function editAnswerForm( $from_edit_view = $edit_view->edit($environment); if ($from_edit_view instanceof EditForm && $this->capabilities_edit_view->providesAnswerFormEditAdditionalSteps()) { - return $from_edit_view->withIsFinalStep(false); + return $from_edit_view->withIsFinalStep(false)->getUI(); } - if (!($from_edit_view instanceof AnswerFormProperties)) { - return $from_edit_view; + if ($from_edit_view instanceof Async) { + $from_edit_view->render($this->ui_renderer); + } + + if ($from_edit_view instanceof Viewable) { + return $from_edit_view->getUI(); } $return_form_step_actions = $this->capabilities_edit_view @@ -288,9 +299,13 @@ public function editAnswerForm( $environment->withAnswerFormProperties($from_edit_view), $edit_view ); - if ($return_form_step_actions instanceof Async - || $return_form_step_actions instanceof Renderable) { - return $return_form_step_actions; + + if ($return_form_step_actions instanceof Async) { + $return_form_step_actions->render($this->ui_renderer); + } + + if ($return_form_step_actions instanceof Viewable) { + return $return_form_step_actions->getUI(); } $this->updateAnswerFormAndRedirect( @@ -353,7 +368,8 @@ private function editQuestion( $edit = $question->getEditView( $this->configuration_repository, $this->current_user, - $this->ctrl + $this->ctrl, + $this->ui_renderer )->edit( $environment_with_question_parameter ->withActionParameter(self::ACTION_EDIT_QUESTION), @@ -660,7 +676,8 @@ private function buildEditStartView( return $question->getEditView( $this->configuration_repository, $this->current_user, - $this->ctrl + $this->ctrl, + $this->ui_renderer )->edit( $environment, $question->getParticipantView() diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index ed100708aa17..9464b891d25b 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -42,6 +42,8 @@ use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Component\Table\DataRow; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; class Question implements PublicQuestionInterface { @@ -261,19 +263,22 @@ public function withCreateMode( public function getEditView( ConfigurationRepository $configuration_repository, \ilObjUser $current_user, - \ilCtrl $ctrl + \ilCtrl $ctrl, + UIRenderer $ui_renderer ): Views\Edit { return new Views\Edit( $configuration_repository, $current_user, $ctrl, + $ui_renderer, $this ); } #[\Override] - public function getParticipantView(): Views\Participant - { + public function getParticipantView( + UIFactory $ui_factory + ): Views\Participant { return new Views\Participant( $this ); From 45d932ed523c29c961197b6c5ea13cfd81e98db7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 8 May 2026 15:26:54 +0200 Subject: [PATCH 094/108] Questions: Clarify TabeDefinitions --- .../Feedback/TableDefinitions.php | 31 +++------- .../TableDefinitions.php | 39 ++----------- .../Questions/src/AnswerForm/Definition.php | 2 +- .../AnswerFormGenericTableDefinitions.php | 15 ++--- .../Persistence/TableDefinitions.php | 32 +++++++++++ .../src/AnswerForm/TypeGenericProperties.php | 1 - .../src/AnswerFormTypes/Cloze/Definition.php | 2 +- .../Cloze/TableDefinitions.php | 8 +-- .../src/Persistence/TableDefinitions.php | 56 ------------------- .../Definitions/ForImmediateStorage.php | 34 +++++++++++ .../src/Question/Persistence/Repository.php | 9 +-- .../Question/Persistence/TableDefinitions.php | 56 ++++--------------- .../ILIAS/Questions/src/Question/Question.php | 8 +-- .../ILIAS/Questions/src/Setup/Agent.php | 4 +- 14 files changed, 106 insertions(+), 191 deletions(-) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php delete mode 100644 components/ILIAS/Questions/src/Persistence/TableDefinitions.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/ForImmediateStorage.php diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php index 25a9be03189e..9471dde44e43 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php @@ -23,13 +23,12 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; -class TableDefinitions implements TableDefinitionsInterface +class TableDefinitions { private const string FEEDBACK_GENERIC_TABLE_ID_COLUMN = 'answer_form_id'; private const string FEEDBACK_GENERIC_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; @@ -57,46 +56,35 @@ public function __construct( ) { } - #[\Override] public function getTableSubNameSpace(): ?TableSubNameSpace { return null; } - #[\Override] public function getColumns( TableNameBuilder $table_name_builder, TableTypesInterface $table_type, - string $sub_table_identifier = '', - array $columns_to_skip = [] ): array { $table = $this->persistence_factory->table( $table_name_builder, $table_type ); - $column_identifiers = match($table_type) { - TableTypes::FeedbackGeneric => self::FEEDBACK_GENERIC_TABLE_COLUMNS, - TableTypes::FeedbackSpecific => self::FEEDBACK_SPECIFIC_TABLE_COLUMNS - }; + return array_map( fn(string $v): Column => $this->persistence_factory->column( $table, $v ), - array_values( - array_filter( - $column_identifiers, - fn(string $v) => !in_array($v, $columns_to_skip) - ) - ) + match($table_type) { + TableTypes::FeedbackGeneric => self::FEEDBACK_GENERIC_TABLE_COLUMNS, + TableTypes::FeedbackSpecific => self::FEEDBACK_SPECIFIC_TABLE_COLUMNS + } ); } - #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' + TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( $table_name_builder, @@ -115,11 +103,9 @@ public function getIdColumn( }; } - #[\Override] public function getForeignKeyColumn( TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' + TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( $table_name_builder, @@ -138,7 +124,6 @@ public function getForeignKeyColumn( }; } - #[\Override] public function completeQuery( Query $query, ?Column $base_table_id_column diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php index fd23e5406b6d..b66db76b69bf 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php @@ -22,16 +22,14 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; -class TableDefinitions implements TableDefinitionsInterface +class TableDefinitions { private const string SUGGESTED_LEARNING_CONTENT_TABLE_ID_COLUMN = 'answer_form_id'; - private const string SUGGESTED_LEARNING_CONTENT_TABLE_FOREIGN_KEY_COLUMN = 'answer_form_id'; private const array SUGGESTED_LEARNING_CONTENT_TABLE_COLUMNS = [ 'answer_form_id', 'type', @@ -43,18 +41,14 @@ public function __construct( ) { } - #[\Override] public function getTableSubNameSpace(): ?TableSubNameSpace { return null; } - #[\Override] public function getColumns( TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '', - array $columns_to_skip = [] + TableTypesInterface $table_type ): array { $table = $this->persistence_factory->table( $table_name_builder, @@ -65,20 +59,13 @@ public function getColumns( $table, $v ), - array_values( - array_filter( - self::SUGGESTED_LEARNING_CONTENT_TABLE_COLUMNS, - fn(string $v) => !in_array($v, $columns_to_skip) - ) - ) + self::SUGGESTED_LEARNING_CONTENT_TABLE_COLUMNS ); } - #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' + TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( $table_name_builder, @@ -91,24 +78,6 @@ public function getIdColumn( ); } - #[\Override] - public function getForeignKeyColumn( - TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' - ): Column { - $table = $this->persistence_factory->table( - $table_name_builder, - $table_type - ); - - return $this->persistence_factory->column( - $table, - self::SUGGESTED_LEARNING_CONTENT_TABLE_FOREIGN_KEY_COLUMN - ); - } - - #[\Override] public function completeQuery( Query $query, ?Column $base_table_id_column diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 3c95d98d4cab..061968baa540 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -23,7 +23,7 @@ use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\AnswerForm\Views\Participant; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableDefinitions; +use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions; use ILIAS\Language\Language; interface Definition diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php index 323a2951ac21..61059ef661f4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php @@ -23,7 +23,7 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; +use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -49,17 +49,14 @@ public function __construct( ) { } - #[\Override] public function getTableSubNameSpace(): ?TableSubNameSpace { return null; } - #[\Override] public function getColumns( TableNameBuilder $table_name_builder, TableTypes $table_type, - string $sub_table_identifier = '', array $columns_to_skip = [] ): array { return array_map( @@ -79,11 +76,9 @@ public function getColumns( ); } - #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, - TableTypes $table_type, - string $sub_table_identifier = '' + TableTypes $table_type ): Column { return $this->persistence_factory->column( $this->persistence_factory->table( @@ -94,11 +89,9 @@ public function getIdColumn( ); } - #[\Override] public function getForeignKeyColumn( TableNameBuilder $table_name_builder, - TableTypes $table_type, - string $sub_table_identifier = '' + TableTypes $table_type ): Column { return $this->persistence_factory->column( $this->persistence_factory->table( @@ -110,7 +103,7 @@ public function getForeignKeyColumn( } #[\Override] - public function completeQuery( + public function completeQuestionQuery( Query $query, ?Column $base_table_id_column ): Query { diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php new file mode 100644 index 000000000000..94260ca7713b --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php @@ -0,0 +1,32 @@ +getColumns( $table_name_builder, $table_type, - '', [ 'id', 'type', diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index b71738d22eb0..8914f7ebc041 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -27,7 +27,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableDefinitions; +use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions; use ILIAS\Language\Language; class Definition implements DefinitionInterface diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php index 813eddba6b8a..3796515bc0ab 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php @@ -24,7 +24,7 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; +use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -90,13 +90,11 @@ public function __construct( ) { } - #[\Override] public function getTableSubNameSpace(): TableSubNameSpace { return $this->table_name_specifiers; } - #[\Override] public function getColumns( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -131,7 +129,6 @@ public function getColumns( ); } - #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -163,7 +160,6 @@ public function getIdColumn( ); } - #[\Override] public function getForeignKeyColumn( TableNameBuilder $table_name_builder, TableTypes $table_type, @@ -198,7 +194,7 @@ public function getForeignKeyColumn( } #[\Override] - public function completeQuery( + public function completeQuestionQuery( Query $query, ?Column $answer_form_id_column ): Query { diff --git a/components/ILIAS/Questions/src/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/Persistence/TableDefinitions.php deleted file mode 100644 index 00ce987ed94c..000000000000 --- a/components/ILIAS/Questions/src/Persistence/TableDefinitions.php +++ /dev/null @@ -1,56 +0,0 @@ -getAnswerFormTypesForQuestionIds($question_ids), - fn(Query $c, AnswerFormDefinition $v) => $v->getTableDefinitions()->completeQuery( + fn(Query $c, AnswerFormDefinition $v) => $v->getTableDefinitions()->completeQuestionQuery( $c, $this->answer_form_generic_table_definitions->getIdColumn( $this->core_table_names_builder, @@ -428,11 +428,8 @@ private function buildQuestionsQuery( $base_query = $base_query->withAdditionalOrder($main_sort_order); } - return $this->answer_form_generic_table_definitions->completeQuery( - $this->question_table_definitions->completeQuery( - $base_query, - null - ), + return $this->answer_form_generic_table_definitions->completeQuestionQuery( + $this->question_table_definitions->completeLoadQuestionQuery($base_query), $this->question_table_definitions->getIdColumn( $this->core_table_names_builder, TableTypes::Questions diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php index 7cd687d4bcfc..7e19071a152f 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php +++ b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php @@ -23,13 +23,12 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; -class TableDefinitions implements TableDefinitionsInterface +class TableDefinitions { private const string QUESTION_TABLE_ID_COLUMN = 'id'; private const array QUESTION_TABLE_COLUMNS = [ @@ -65,28 +64,21 @@ public function __construct( ) { } - #[\Override] public function getTableSubNameSpace(): ?TableSubNameSpace { return null; } - #[\Override] public function getColumns( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type, - string $sub_table_identifier = '', array $columns_to_skip = [] ): array { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); - $column_identifiers = match($table_type) { - TableTypes::Questions => self::QUESTION_TABLE_COLUMNS, - TableTypes::Linking => self::LINKING_TABLE_COLUMNS, - TableTypes::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS - }; + return array_map( fn(string $v): Column => $this->persistence_factory->column( $table, @@ -94,18 +86,20 @@ public function getColumns( ), array_values( array_filter( - $column_identifiers, + match($table_type) { + TableTypes::Questions => self::QUESTION_TABLE_COLUMNS, + TableTypes::Linking => self::LINKING_TABLE_COLUMNS, + TableTypes::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS + }, fn(string $v) => !in_array($v, $columns_to_skip) ) ) ); } - #[\Override] public function getIdColumn( TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' + TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( $table_name_builder, @@ -128,34 +122,8 @@ public function getIdColumn( }; } - #[\Override] - public function getForeignKeyColumn( - TableNameBuilder $table_name_builder, - TableTypesInterface $table_type, - string $sub_table_identifier = '' - ): Column { - $table = $this->persistence_factory->table( - $table_name_builder, - $table_type - ); - - return match($table_type) { - TableTypes::Linking => $this->persistence_factory->column( - $table, - self::LINKING_TABLE_FOREIGN_KEY_COLUMN - ), - TableTypes::MigrationsTable => $this->persistence_factory->column( - $table, - self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN - ), - default => null - }; - } - - #[\Override] - public function completeQuery( - Query $query, - ?Column $base_table_id_column + public function completeLoadQuestionQuery( + Query $query ): Query { $table_name_builder = $query->getTableNameBuilder(null); $questions_id_column = $this->getIdColumn( diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 9464b891d25b..e406d59f83c5 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -625,7 +625,6 @@ private function buildUpdateLinkingStatement( $question_tables_definitions->getColumns( $table_name_builder, $table_type, - '', [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] ), [ @@ -640,8 +639,9 @@ private function buildUpdateLinkingStatement( ], [ $persistence_factory->where( - $table_type->getIdColumn( - $persistence_factory + $question_tables_definitions->getIdColumn( + $table_name_builder, + $table_type ), $persistence_factory->value( FieldDefinition::T_TEXT, @@ -662,7 +662,6 @@ private function buildUpdateQuestionStatement( $question_tables_definitions->getColumns( $table_name_builder, $table_type, - '', [ 'id', 'page_id', @@ -724,7 +723,6 @@ private function buildDeleteQuestionStatement( [ $persistence_factory->where( $question_tables_definitions->getIdColumn( - $persistence_factory, $table_name_builder, $table_type ), diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index 8f0ac87daa7f..2479343b9bb4 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -21,7 +21,7 @@ namespace ILIAS\Questions\Setup; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; -use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions as ClozeTableDefinitions; use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -81,7 +81,7 @@ public function getUpdateObjective( 'cloze' ) ), - new TableDefinitions( + new ClozeTableDefinitions( $this->persistence_factory, new TableSubNameSpace( 'ILIAS', From 8f4fae349e54834e67c9ce6ff7692b3ab1a88aba Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 8 May 2026 16:19:44 +0200 Subject: [PATCH 095/108] Questions: Introduce ForImmediateStorage --- .../Questions/src/AnswerForm/Views/Edit.php | 3 ++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 7 ++++--- .../Definitions/ForImmediateStorage.php | 18 ++++++++++-------- .../Questions/src/Presentation/Views/Edit.php | 9 +++++++++ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php index c34071e081eb..b54a01818687 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; @@ -35,7 +36,7 @@ public function create( public function edit( Environment $environment - ): EditOverview|EditForm|Async|Properties; + ): EditOverview|EditForm|Async|ForImmediateStorage|Properties; public function other( Environment $environment diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 8e57ac3faac9..f17edeab7b53 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -26,6 +26,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\EditOverview; @@ -69,7 +70,7 @@ public function create( #[\Override] public function edit( Environment $environment - ): EditOverview|EditForm|Async|Properties { + ): EditOverview|EditForm|Async|ForImmediateStorage|Properties { $sub_action = $environment->getSubAction(); $combinations = $environment->getAnswerFormProperties()->getCombinations(); @@ -208,7 +209,7 @@ private function addLegacyTextToBasicProperties( private function processBasicEditingForm( Environment $environment - ): EditForm|Async|Properties { + ): EditForm|Async|ForImmediateStorage|Properties { $inputs_builder = $this->buildInputsBuilderForBasicInputs( $environment, false, @@ -243,7 +244,7 @@ private function processBasicEditingForm( } if ($new_gaps->getAddedGaps($old_gaps) === []) { - return $data; + return new ForImmediateStorage($data); } return $this->edit_gaps->do( diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/ForImmediateStorage.php b/components/ILIAS/Questions/src/Presentation/Definitions/ForImmediateStorage.php index b77e2ee1b97e..a0c7dfc5172a 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/ForImmediateStorage.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/ForImmediateStorage.php @@ -20,15 +20,17 @@ namespace ILIAS\Questions\Presentation\Definitions; -use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Persistence\Storable; -interface Actor +class ForImmediateStorage { - public function can( - string $action - ): bool; + public function __construct( + private readonly Storable $storable + ) { + } - public function do( - string $action - ): Async; + public function unpack(): Storable + { + return $this->storable; + } } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 959b214ec893..1ca4fb6633ca 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -33,6 +33,7 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; +use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; use ILIAS\Questions\Presentation\Layout\QuestionsTable; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; use ILIAS\Questions\Question\Persistence\Repository; @@ -294,6 +295,14 @@ public function getEditAnswerForm( return $from_edit_view->getUI(); } + if ($from_edit_view instanceof ForImmediateStorage) { + $this->updateAnswerFormAndRedirect( + $environment, + $question, + $from_edit_view->unpack() + ); + } + $return_form_step_actions = $this->capabilities_edit_view ->doFirstFormStepAction( $environment->withAnswerFormProperties($from_edit_view), From 9f1bd75a1cb543dec0c046671e58008c2e9681ed Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 27 Apr 2026 13:23:08 +0200 Subject: [PATCH 096/108] Question: Implementing Attempt --- .../class.ilObjQuestionPreviewGUI.php | 424 ++++++++++++++++++ .../class.ilObjQuestionsGUI.php | 91 +++- .../ILIAS/Questions/Legacy/LocalDIC.php | 103 +++-- .../Legacy/PageEditor/QstsQuestionPage.php | 26 ++ .../PageEditor/class.QstsQuestionPageGUI.php | 33 +- .../PageEditor/class.ilPCAnswerForm.php | 10 +- .../Legacy/Temp/QstsTempAttemptRepository.php | 65 +++ components/ILIAS/Questions/Questions.php | 12 +- .../AnswerForm/Capabilities/ActionWithTab.php | 2 +- .../Capabilities/Async/Async.php} | 10 +- .../Capabilities/Async/Capability.php | 85 ++++ .../AnswerForm/Capabilities/Capability.php | 7 + .../src/AnswerForm/Capabilities/Edit.php | 16 +- .../Capabilities/Feedback/Capability.php | 15 +- .../Feedback/TableDefinitions.php | 20 +- .../Capabilities/Marking/Capability.php | 16 +- .../SuggestedLearningContent.php | 15 +- .../TableDefinitions.php | 12 +- .../Questions/src/AnswerForm/Definition.php | 12 + .../AnswerForm/Migration/MigrationInsert.php | 6 +- .../AnswerFormGenericTableDefinitions.php | 22 +- .../Persistence/TableDefinitions.php | 8 + .../Response.php} | 9 +- .../src/AnswerForm/TypeGenericProperties.php | 22 +- .../src/AnswerForm/Views/Participant.php | 9 +- .../Cloze/Capabilities/Async.php | 55 +++ .../src/AnswerFormTypes/Cloze/Definition.php | 24 +- .../Cloze/Layout/OverviewTable.php | 2 +- .../Migration/BasicMigrationFunctions.php | 12 +- .../Cloze/Migration/MigrationCloze.php | 8 +- .../Properties/Combinations/Combination.php | 28 +- .../Properties/Combinations/Combinations.php | 14 +- .../{EditCombinations.php => Edit.php} | 9 +- .../Properties/Combinations/MatchingValue.php | 4 +- .../Cloze/Properties/Factory.php | 2 + .../Gaps/AnswerOptions/AnswerOption.php | 6 +- .../Gaps/AnswerOptions/AnswerOptions.php | 28 +- .../Gaps/AnswerOptions/Upload.php} | 4 +- .../EditGaps.php => Properties/Gaps/Edit.php} | 7 +- .../Cloze/Properties/Gaps/Gap.php | 15 +- .../Cloze/Properties/Gaps/Gaps.php | 77 +++- .../Cloze/Properties/Gaps/LongMenu.php | 8 +- .../Cloze/Properties/Gaps/Numeric.php | 4 +- .../Cloze/Properties/Gaps/Select.php | 35 +- .../Cloze/Properties/Gaps/Text.php | 4 +- .../Cloze/Properties/Gaps/Type.php | 4 +- .../Cloze/Properties/Properties.php | 95 ++-- .../Cloze/Response/AnswerForm.php | 110 +++++ .../Cloze/Response/AnswerInput.php | 89 ++++ .../Cloze/Response/Factory.php | 64 +++ .../Cloze/TableDefinitions.php | 95 +++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 19 +- .../Cloze/Views/Participant.php | 19 +- .../ILIAS/Questions/src/Attempt/Attempt.php | 180 ++++++++ .../Questions/src/Attempt/Repository.php | 288 ++++++++++++ .../ILIAS/Questions/src/Attempt/Response.php | 162 +++++++ .../src/Attempt/TableDefinitions.php | 198 ++++++++ .../Repository.php => Attempt/TableTypes.php} | 21 +- components/ILIAS/Questions/src/Collector.php | 63 --- .../Questions/src/DefaultPublicInterface.php | 108 +++++ .../Questions/src/Persistence/Factory.php | 4 +- .../ILIAS/Questions/src/Persistence/Join.php | 15 - .../ILIAS/Questions/src/Persistence/Table.php | 4 +- .../Definitions/DefaultEnvironment.php | 8 +- .../Definitions/OverviewTableColumns.php | 6 +- .../Presentation/Layout/QuestionsTable.php | 19 +- .../Questions/src/Presentation/Views/Edit.php | 85 ++-- .../src/Presentation/Views/Participant.php | 137 ++++++ .../ILIAS/Questions/src/PublicInterface.php | 24 +- .../src/Question/Persistence/Repository.php | 3 +- .../Question/Persistence/TableDefinitions.php | 16 +- .../ILIAS/Questions/src/Question/Question.php | 211 ++++++--- .../Questions/src/Question/Views/Edit.php | 19 +- .../src/Question/Views/Participant.php | 91 +--- .../ILIAS/Questions/src/Response/Response.php | 38 -- .../ILIAS/Questions/src/Setup/Agent.php | 11 +- .../src/Setup/ClozeQuestionTables.php | 26 +- .../src/Setup/OverarchingQuestionTables.php | 104 ++++- .../src/Setup/QuestionsMigration.php | 16 +- .../ILIAS/Questions/src/Setup/TempTables.php | 60 +++ .../tpl.qsts_question_presentation.html | 9 + 81 files changed, 3123 insertions(+), 694 deletions(-) create mode 100755 components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php create mode 100644 components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php rename components/ILIAS/Questions/src/{Question/Response.php => AnswerForm/Capabilities/Async/Async.php} (68%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Capability.php rename components/ILIAS/Questions/src/{Question/PublicQuestionInterface.php => AnswerForm/Response.php} (75%) create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/{EditCombinations.php => Edit.php} (91%) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/{Views/UploadAnswerOptions.php => Properties/Gaps/AnswerOptions/Upload.php} (97%) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/{Views/EditGaps.php => Properties/Gaps/Edit.php} (98%) create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php create mode 100644 components/ILIAS/Questions/src/Attempt/Attempt.php create mode 100644 components/ILIAS/Questions/src/Attempt/Repository.php create mode 100644 components/ILIAS/Questions/src/Attempt/Response.php create mode 100644 components/ILIAS/Questions/src/Attempt/TableDefinitions.php rename components/ILIAS/Questions/src/{Response/Repository.php => Attempt/TableTypes.php} (65%) delete mode 100644 components/ILIAS/Questions/src/Collector.php create mode 100644 components/ILIAS/Questions/src/DefaultPublicInterface.php create mode 100644 components/ILIAS/Questions/src/Presentation/Views/Participant.php delete mode 100644 components/ILIAS/Questions/src/Response/Response.php create mode 100644 components/ILIAS/Questions/src/Setup/TempTables.php create mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php new file mode 100755 index 000000000000..6bb2f61c08cc --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php @@ -0,0 +1,424 @@ +repository = $local_dic[Repository::class]; + $this->answer_form_factory = $local_dic[AnswerFormFactory::class]; + $this->participant_view = $local_dic[PublicInterface::class] + ->getParticipantView($this->object_id); + + /** @var ILIAS\DI\Container $DIC */ + global $DIC; + $this->ctrl = $DIC['ilCtrl']; + $this->lng = $DIC['lng']; + $this->tpl = $DIC['tpl']; + $this->current_user = $DIC['user']->getLoggedInUser(); + $this->tabs_gui = $DIC['ilTabs']; + $this->http = $DIC['http']; + $this->ui_factory = $DIC['ui.factory']; + $this->ui_renderer = $DIC['ui.renderer']; + $this->ui_service = $DIC->uiService(); + $this->refinery = $DIC['refinery']; + $this->data_factory = new DataFactory(); + $this->uuid_factory = new UuidFactory(); + + $this->temp_attempt_repository = new QstsTempAttemptRepository( + $DIC['ilDB'], + $this->uuid_factory + ); + + [ + $this->url_builder, + $this->action_token, + $this->row_id_token + ] = $this->getUrlBuilder()->acquireParameters( + self::PARAMETER_NAMENSPACE, + self::ACTION_TOKEN_STRING, + self::ROW_ID_TOKEN_STRING + ); + } + + public function executeCommand(): void + { + $cmd = $this->ctrl->getCmd(); + + $table_action = $this->http->wrapper()->query()->retrieve( + $this->action_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always(null) + ]) + ); + + if ($table_action !== null) { + $cmd = $table_action; + } + + if ($cmd === null || $cmd === '') { + $cmd = self::CMD_DEFAULT; + } + $cmd .= 'Cmd'; + $this->$cmd(); + } + + private function showCmd(): void + { + $filter = $this->buildFilter( + $this->ctrl->getLinkTargetByClass( + $this->getClassPath() + ) + ); + + $this->tpl->setContent( + $this->ui_renderer->render([ + $filter, + $this->ui_factory->table()->data( + $this, + $this->lng->txt('questions'), + [ + OverviewTableColumns::Title->value + => $this->ui_factory->table()->column()->text( + $this->lng->txt('title') + ), + OverviewTableColumns::AnswerFormTypes->value + => $this->ui_factory->table()->column()->text( + $this->lng->txt('contained_types') + )->withIsOptional(true, true) + ->withIsSortable(false), + ], + )->withActions( + $this->getActions() + )->withRange(new Range(0, 20)) + ->withFilter( + $this->ui_service->filter()->getData($filter) + )->withRequest($this->http->request()) + ]) + ); + } + + private function showQuestionCmd(): void + { + $question_id = $this->retrieveQuestionIdFromQuery(); + $attempt_id = $this->temp_attempt_repository->get( + $this->current_user->getId() + ); + + if ($question_id === null) { + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_FAILURE, + $this->lng->txt('invalid') + ); + $this->showCmd(); + } + + $this->tabs_gui->clearTargets(); + $this->tabs_gui->setBackTarget( + $this->lng->txt('back'), + $this->ctrl->getLinkTargetByClass( + $this->getClassPath() + ) + ); + + $view = $this->participant_view + ->withRequiredCapabilities([ + Feedback::class, + SuggestedLearningContent::class, + Marking::class + ])->getQuestionView( + $question_id, + $attempt_id + ); + + if ($attempt_id === null) { + $this->temp_attempt_repository->store( + $this->current_user->getId(), + $view->getAttemptId() + ); + } + + $this->tpl->setContent( + $this->ui_renderer->render( + $this->ui_factory->panel()->standard( + $this->lng->txt('question'), + $this->ui_factory->legacy()->content( + $this->buildQuestionForm($view) + ) + ) + ) + ); + } + + private function showQuestionAsyncCmd(): void + { + $this->tpl->setContent('Async'); + } + + private function respondCmd(): void + { + $question_id = $this->retrieveQuestionIdFromQuery(); + $attempt_id = $this->temp_attempt_repository->get( + $this->current_user->getId() + ); + + if ($question_id === null || $attempt_id === null) { + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_FAILURE, + $this->lng->txt('invalid') + ); + $this->showCmd(); + } + + $this->tabs_gui->clearTargets(); + $this->tabs_gui->setBackTarget( + $this->lng->txt('back'), + $this->ctrl->getLinkTargetByClass( + $this->getClassPath() + ) + ); + + $response_id = $this->participant_view + ->withRequiredCapabilities([ + Feedback::class, + SuggestedLearningContent::class, + Marking::class + ])->persistResponse( + $question_id, + $response_id + ); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + foreach ($this->repository->getQuestionDataOnlyForAllQuestions( + $range, + $order, + $filter_data + ) as $question) { + yield $row_builder->buildDataRow( + $question->getid()->toString(), + [ + OverviewTableColumns::Title->value => $question->getTitle(), + OverviewTableColumns::AnswerFormTypes->value => implode( + '
', + $question->getListOfContainedAnswerFormTypeLabels($this->lng) + ) + ] + ); + } + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return $this->repository->getQuestionsCount(); + } + + private function buildFilter( + string $action + ): Filter { + $filter_inputs = OverviewTableColumns::getFilterInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect( + $this->lng + ) + ); + + $active = array_fill(0, count($filter_inputs), true); + + $filter = $this->ui_service->filter()->standard( + 'question_table_filter_id', + $action, + $filter_inputs, + $active, + true, + true + ); + + $request = $this->http->request(); + return $request->getMethod() === 'POST' + ? $filter->withRequest($request) + : $filter; + } + + private function getActions(): array + { + return [ + 'show' => $this->ui_factory->table()->action()->single( + $this->lng->txt('show'), + $this->url_builder->withParameter($this->action_token, self::CMD_SHOW_QUESTION), + $this->row_id_token + ), + 'show_async' => $this->ui_factory->table()->action()->single( + $this->lng->txt('show_async'), + $this->url_builder->withParameter($this->action_token, self::CMD_SHOW_QUESTION_ASYNC), + $this->row_id_token + ), + ]; + } + + private function retrieveQuestionIdFromQuery(): ?Uuid + { + return $this->http->wrapper()->query()->retrieve( + $this->row_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + function (array $v): Uuid { + try { + return $this->uuid_factory->fromString($v[0]); + } catch (Throwable $e) { + throw new ConstraintViolationException( + sprintf('The value could not be transformed into a Uuid'), + 'not_valid' + ); + } + } + ), + $this->refinery->always(null) + ]) + ); + } + + private function getUrlBuilder(): URLBuilder + { + return new URLBuilder( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( + $this->getClassPath() + ) + ) + ); + } + + private function buildQuestionForm( + QuestionParticipantView $view + ): string { + $form = new ilPropertyFormGUI(); + $form->setCloseTag(false); + $form->setFormAction( + $this->ctrl->getFormActionByClass( + $this->getClassPath() + ) + ); + $form->addCommandButton( + self::CMD_RESPOND, + $this->lng->txt('send') + ); + + $form_opening = $form->getHTML(); + + $form->setOpenTag(false); + $form->setCloseTag(true); + + return $form_opening + . $this->ui_renderer->render( + $view->getUI() + ) . $form->getHTML(); + } + + private function getClassPath(): array + { + return [ilObjQuestionsGUI::class, ilObjQuestionPreviewGUI::class]; + } +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 260afefa98f8..589035656ab9 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -23,9 +23,9 @@ use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; -use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\PublicInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\URI; @@ -42,11 +42,15 @@ class ilObjQuestionsGUI extends ilObjectGUI private const string TAB_IDENTIFIER_UNITS = 'units'; private const string TAB_IDENTIFIER_PERMISSIONS = 'perm_settings'; + private const string SUB_TAB_IDENTIFIER_EDIT_QUESTIONS = 'edit'; + private const string SUB_TAB_IDENTIFIER_PREVIEW_QUESTIONS = 'preview'; + private readonly UnitsRepository $units_repository; private readonly Edit $edit_view; private readonly ConfigurationRepository $configuration_repository; - private DataFactory $data_factory; + private readonly ilHelpGUI $help; + private readonly DataFactory $data_factory; public function __construct( $a_data, @@ -55,11 +59,16 @@ public function __construct( bool $a_prepare_output = true ) { global $DIC; + $this->help = $DIC['ilHelp']; $this->data_factory = new DataFactory(); + $this->type = 'qsts'; + + parent::__construct($a_data, $a_id, $a_call_by_reference, false); + $local_dic = LocalDIC::dic(); - $this->units_repository = $local_dic[UnitsRepository::class]; - $this->edit_view = $local_dic[Edit::class] + $this->edit_view = $local_dic[PublicInterface::class] + ->getEditView($this->object->getId()) ->withRequiredCapabilities([ Feedback::class, SuggestedLearningContent::class, @@ -67,10 +76,6 @@ public function __construct( ]); $this->configuration_repository = $local_dic[ConfigurationRepository::class]; - $this->type = 'qsts'; - - parent::__construct($a_data, $a_id, $a_call_by_reference, false); - $this->lng->loadLanguageModule('assessment'); $this->lng->loadLanguageModule('qsts'); @@ -86,10 +91,6 @@ public function executeCommand(): void $this->prepareOutput(); switch ($next_class) { - case strtolower(UploadAnswerOptionsGUI::class): - $this->ctrl->forwardCommand(new UploadAnswerOptionsGUI()); - break; - case strtolower(ilPermissionGUI::class): $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_PERMISSIONS); $this->ctrl->forwardCommand(new \ilPermissionGUI($this)); @@ -97,9 +98,7 @@ public function executeCommand(): void case strtolower(QstsQuestionPageGUI::class): $this->edit_view->forwardPageCmds( - $this->tpl, $this->buildEditQuestionsBaseUri(), - $this->obj_id, $this->ref_id ); break; @@ -120,19 +119,48 @@ public function executeCommand(): void ); break; + case strtolower(GlobalConfigurationGUI::class): + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_UNITS); + $this->ctrl->forwardCommand( + new GlobalConfigurationGUI( + $this->units_repository, + $this->lng, + $this->ctrl, + $this->rbac_system, + $this->tpl, + $this->toolbar, + $this->tabs_gui, + $this->help + ) + ); + break; + + case strtolower(ilObjQuestionPreviewGUI::class): + $this->addSubTabs(); + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_QUESTIONS); + $this->tabs_gui->activateSubTab(self::SUB_TAB_IDENTIFIER_PREVIEW_QUESTIONS); + $this->ctrl->forwardCommand( + new ilObjQuestionPreviewGUI( + $this->object->getId() + ) + ); + + break; default: + $this->addSubTabs(); if ($cmd === null || $cmd === '' || $cmd === 'view') { - $cmd = 'viewQuestions'; + $cmd = 'editQuestions'; } - $cmd .= 'Object'; + $cmd .= 'Cmd'; $this->$cmd(); break; } } - public function viewQuestionsObject(): void + private function editQuestionsCmd(): void { - $this->tabs_gui->activateTab('questions'); + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_QUESTIONS); + $this->tabs_gui->activateSubTab(self::SUB_TAB_IDENTIFIER_EDIT_QUESTIONS); $this->tpl->setContent( $this->ui_renderer->render( @@ -146,16 +174,16 @@ public function viewQuestionsObject(): void public function getAdminTabs(): void { - $this->getTabs(); + $this->addTabs(); } - protected function getTabs(): void + protected function addTabs(): void { if ($this->rbac_system->checkAccess('read', $this->object->getRefId())) { $this->tabs_gui->addTab( self::TAB_IDENTIFIER_QUESTIONS, $this->lng->txt(self::TAB_IDENTIFIER_QUESTIONS), - $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + $this->ctrl->getLinkTargetByClass(self::class, 'editQuestions') ); $this->tabs_gui->addTab( @@ -174,10 +202,29 @@ protected function getTabs(): void } } + private function addSubTabs(): void + { + if (!$this->rbac_system->checkAccess('read', $this->object->getRefId())) { + return; + } + + $this->tabs_gui->addSubTab( + self::SUB_TAB_IDENTIFIER_EDIT_QUESTIONS, + $this->lng->txt('edit'), + $this->ctrl->getLinkTargetByClass(self::class, 'editQuestions') + ); + + $this->tabs_gui->addSubTab( + self::SUB_TAB_IDENTIFIER_PREVIEW_QUESTIONS, + $this->lng->txt('preview'), + $this->ctrl->getLinkTargetByClass([self::class, ilObjQuestionPreviewGUI::class]) + ); + } + private function buildEditQuestionsBaseUri(): URI { return $this->data_factory->uri( - ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'viewQuestions') + ILIAS_HTTP_PATH . '/' . $this->ctrl->getFormActionByClass(self::class, 'editQuestions') ); } } diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index bff8a122c20b..e3a310ef0e21 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -25,12 +25,16 @@ use ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze; +use ILIAS\Questions\Attempt\Repository as AttemptRepository; +use ILIAS\Questions\Attempt\TableDefinitions as AttemptTableDefinitions; +use ILIAS\Questions\DefaultPublicInterface; +use ILIAS\Questions\Question\Persistence\DatabaseStatementBuilder; use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Question\Persistence\Repository as QuestionsRepository; -use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; +use ILIAS\Questions\PublicInterface; use ILIAS\Questions\UserSettings\CreateMode; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -63,6 +67,30 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->fileServiceSettings() ); + $dic[PublicInterface::class] = static fn($c): PublicInterface + => new DefaultPublicInterface( + $DIC['lng'], + $DIC['user']->getLoggedInUser(), + $DIC['refinery'], + $DIC['ui.factory'], + $DIC['ui.renderer'], + $DIC['global_screen'], + $DIC['tpl'], + $DIC->contentStyle(), + $DIC['ilCtrl'], + $DIC['http'], + $DIC['ilTabs'], + $DIC->uiService(), + $c[UuidFactory::class], + $c[ConfigurationRepository::class], + $c[AnswerFormFactory::class], + $c[QuestionsRepository::class], + $c[AttemptRepository::class], + $c[LayoutFactory::class], + $c[Capabilities\Factory::class], + $c[Capabilities\Edit::class] + ); + $dic[ConfigurationRepository::class] = static fn($c): ConfigurationRepository => new ConfigurationRepository( $DIC['ilSetting'], @@ -81,6 +109,24 @@ protected static function buildDIC(ILIASContainer $DIC): self => new AnswerFormGenericTableDefinitions( $c[PersistenceFactory::class] ); + $dic[DatabaseStatementBuilder::class] = static fn($c): DatabaseStatementBuilder + => new DatabaseStatementBuilder( + $c[PersistenceFactory::class], + $c[QuestionTableDefinitions::class], + $c[AnswerFormGenericTableDefinitions::class] + ); + $dic[AttemptTableDefinitions::class] = static fn($c): AttemptTableDefinitions + => new AttemptTableDefinitions( + $c[PersistenceFactory::class] + ); + $dic[AttemptRepository::class] = static fn($c): AttemptRepository + => new AttemptRepository( + $DIC['ilDB'], + $DIC['refinery'], + $c[UuidFactory::class], + $c[PersistenceFactory::class], + $c[AttemptTableDefinitions::class] + ); $dic[Capabilities\Factory::class] = static fn($c): Capabilities\Factory => new Capabilities\Factory([ Capabilities\Feedback\Feedback::class => new Capabilities\Feedback\Capability( @@ -113,7 +159,8 @@ protected static function buildDIC(ILIASContainer $DIC): self ) ) ), - Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability() + Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability(), + Capabilities\Async\Async::class => new Capabilities\Async\Capability() ]); $dic[Capabilities\Edit::class] = fn($c): Capabilities\Edit => new Capabilities\Edit( @@ -131,6 +178,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilDB'], $DIC['refinery'], $c[UuidFactory::class], + $c[DatabaseStatementBuilder::class], $c[PersistenceFactory::class], $c[QuestionTableDefinitions::class], $c[AnswerFormGenericTableDefinitions::class], @@ -146,27 +194,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['upload'], $c[\ilFileServicesFilenameSanitizer::class] ); - $dic[Edit::class] = static fn($c): Edit => new Edit( - $DIC['lng'], - $c[ConfigurationRepository::class], - $DIC['user']->getLoggedInUser(), - $DIC['refinery'], - $DIC['ui.factory'], - $DIC['ui.renderer'], - $DIC['global_screen'], - $DIC['tpl'], - $DIC->contentStyle(), - $DIC['ilCtrl'], - $DIC['http'], - $DIC['ilTabs'], - $DIC->uiService(), - $c[UuidFactory::class], - $c[Capabilities\Edit::class], - $c[AnswerFormFactory::class], - $c[QuestionsRepository::class], - $c[LayoutFactory::class] - ); - $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory => new Cloze\Properties\ClozeText\Factory( $DIC['refinery'], @@ -207,13 +234,6 @@ protected static function buildDIC(ILIASContainer $DIC): self ) ] ); - $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory - => new Cloze\Properties\Factory( - $c[PersistenceFactory::class], - $c[Cloze\Properties\ClozeText\Factory::class], - $c[Cloze\Properties\Gaps\Factory::class], - $c[Cloze\Properties\Combinations\Factory::class] - ); $dic[Cloze\TableDefinitions::class] = static fn($c): Cloze\TableDefinitions => new Cloze\TableDefinitions( $c[PersistenceFactory::class], @@ -222,8 +242,19 @@ protected static function buildDIC(ILIASContainer $DIC): self 'cloze' ) ); - $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps - => new Cloze\Views\EditGaps( + $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory + => new Cloze\Properties\Factory( + $c[PersistenceFactory::class], + $c[Cloze\Properties\ClozeText\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class], + $c[Cloze\Properties\Combinations\Factory::class] + ); + $dic[Cloze\Response\Factory::class] = static fn($c): Cloze\Response\Factory + => new Cloze\Response\Factory( + $c[PersistenceFactory::class] + ); + $dic[Cloze\Properties\Gaps\Edit::class] = static fn($c): Cloze\Properties\Gaps\Edit + => new Cloze\Properties\Gaps\Edit( $DIC['upload'], $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\Gaps\Factory::class] @@ -232,7 +263,7 @@ protected static function buildDIC(ILIASContainer $DIC): self => new Cloze\Views\Edit( $c[Cloze\Properties\Factory::class], $c[Cloze\Properties\ClozeText\Factory::class], - $c[Cloze\Views\EditGaps::class] + $c[Cloze\Properties\Gaps\Edit::class] ); $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant => new Cloze\Views\Participant( @@ -240,7 +271,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[MustacheEngine::class] ); $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( - $c[Cloze\Properties\Factory::class], $c[Cloze\TableDefinitions::class], [ Capabilities\Feedback\Feedback::class => new Cloze\Capabilities\Feedback( @@ -249,8 +279,11 @@ protected static function buildDIC(ILIASContainer $DIC): self ), Capabilities\Marking\Marking::class => new Cloze\Capabilities\Marking( $c[Cloze\Properties\Factory::class] - ) + ), + Capabilities\Async\Async::class => new Cloze\Capabilities\Async() ], + $c[Cloze\Properties\Factory::class], + $c[Cloze\Response\Factory::class], $c[Cloze\Views\Edit::class], $c[Cloze\Views\Participant::class] ); diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 8d70bf7e1339..cf84ca72ecfa 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Question; @@ -25,6 +27,8 @@ class QstsQuestionPage extends ilPageObject { private readonly Edit $edit_view; private readonly Question $question; + private ?Attempt $attempt_data = null; + private ?Viewable $participant_view = null; #[\Override] public function getParentType(): string @@ -54,6 +58,28 @@ public function setQuestion( $this->question = $question; } + public function getParticipantView(): Viewable + { + return $this->participant_view; + } + + public function setParticipantView( + Viewable $participant_view + ): void { + $this->participant_view = $participant_view; + } + + public function getAttemptData(): ?Attempt + { + return $this->attempt_data; + } + + public function setAttemptData( + ?Attempt $attempt_data + ): void { + $this->attempt_data = $attempt_data; + } + public function addQuestionText( string $text ): void { diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index 541b0e08e158..7898ac07cd32 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Question; use ILIAS\Data\URI; @@ -34,8 +36,7 @@ class QstsQuestionPageGUI extends ilPageObjectGUI public function __construct( Question $question, - int $obj_id, - ?Edit $edit_view = null + int $obj_id ) { parent::__construct('qsts', $question->getPageId()); @@ -43,10 +44,6 @@ public function __construct( $this->obj->setQuestion($question); $this->obj->setParentId($obj_id); - - if ($edit_view !== null) { - $this->obj->setEditView($edit_view); - } } #[\Override] @@ -55,6 +52,30 @@ public function finishEditing(): void $this->ctrl->redirectToURL($this->return_uri->__toString()); } + public function withEditView( + Edit $edit_view + ): self { + $clone = clone $this; + $clone->obj->setEditView($edit_view); + return $clone; + } + + public function withParticipantView( + Viewable $participant_view + ): self { + $clone = clone $this; + $clone->obj->setParticipantView($participant_view); + return $clone; + } + + public function withAttemptData( + ?Attempt $attempt + ): self { + $clone = clone $this; + $clone->obj->setAttemptData($attempt); + return $clone; + } + public function withReturnUri( URI $return_uri ): self { diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index d370ed5a96f5..5bacb3c2fb85 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Question\Persistence\Repository; @@ -57,7 +58,6 @@ public function modifyPageContentPostXsl( $ui_factory = $DIC['ui.factory']; $ui_renderer = $DIC['ui.renderer']; $lng = $DIC['lng']; - $question = $this->pg_obj->getQuestion(); return mb_ereg_replace_callback( self::ANSWER_FORM_PLACEHOLDER, @@ -65,7 +65,8 @@ public function modifyPageContentPostXsl( $ui_factory, $ui_renderer, $lng, - $question->getAnswerFormPropertiesByIdString($matches[1]) + $this->pg_obj->getQuestion()->getAnswerFormPropertiesByIdString($matches[1]), + $this->pg_obj->getAttemptData() ), $output ); @@ -178,6 +179,7 @@ private function renderAnswerForm( UIRenderer $ui_renderer, Language $lng, ?AnswerFormProperties $answer_form_properties, + ?Attempt $attempt_data ): string { if ($answer_form_properties === null) { return $lng->txt('broken_answer_form'); @@ -186,9 +188,9 @@ private function renderAnswerForm( return $ui_renderer->render( $ui_factory->legacy()->latexContent( $answer_form_properties->getDefinition()->getParticipantView() - ->get( + ->show( $answer_form_properties, - null + $attempt_data ) ) ); diff --git a/components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php b/components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php new file mode 100644 index 000000000000..3a0c146d6c18 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php @@ -0,0 +1,65 @@ +db->fetchObject( + $this->db->queryF( + 'SELECT attempt_id from ' . self::TABLE_NAME . ' WHERE user_id = %s', + [FieldDefinition::T_INTEGER], + [$user_id] + ) + ); + + if ($value === null) { + return null; + } + + return $this->uuid_factory->fromString($value->attempt_id); + } + + public function store( + int $user_id, + Uuid $attempt_id + ): void { + $this->db->insert( + self::TABLE_NAME, + [ + 'user_id' => [FieldDefinition::T_INTEGER, $user_id], + 'attempt_id' => [FieldDefinition::T_TEXT, $attempt_id->toString()] + ] + ); + } +} diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index bb99c6fe2e72..412d9e21373f 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -50,9 +50,11 @@ public function init( array | \ArrayAccess &$pull, array | \ArrayAccess &$internal, ): void { - $define[] = AnswerFormDefinition::class; - $define[] = AnswerFormMigration::class; - $define[] = CapabilityMigration::class; + $define[] = Questions\PublicInterface::class; + + $internal[PersistenceFactory::class] = static fn() + => new PersistenceFactory(); + $internal[\EvalMath::class] = static fn() => new \EvalMath(); $contribute[AgentInterface::class] = static fn() => new Agent( @@ -127,9 +129,5 @@ public function init( ); $contribute[User\Settings\UserSettings::class] = fn() => new Questions\UserSettings\Settings(); - - $internal[PersistenceFactory::class] = static fn() - => new PersistenceFactory(); - $internal[\EvalMath::class] = static fn() => new \EvalMath(); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php index 8cc9584c9cd3..58d35299d702 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/ActionWithTab.php @@ -71,7 +71,7 @@ public function activateTab( \ilTabsGUI $tabs_gui ): void { $tabs_gui->activateTab( - $this->buildIdentifier() + $this->getIdentifier() ); } } diff --git a/components/ILIAS/Questions/src/Question/Response.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Async.php similarity index 68% rename from components/ILIAS/Questions/src/Question/Response.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Async.php index 3bba6d1e8643..d5ca76605ed1 100644 --- a/components/ILIAS/Questions/src/Question/Response.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Async.php @@ -18,8 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question; +namespace ILIAS\Questions\AnswerForm\Capabilities\Async; -class Response +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Presentation\Layout\Viewable; + +interface Async { + public function getViewable( + Properties $answer_form_properties + ): Viewable; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Capability.php new file mode 100644 index 000000000000..c02a4f33abca --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Async/Capability.php @@ -0,0 +1,85 @@ +getDefinition() + ->hasCapability( + Async::class + ); + } + + #[\Override] + public function providesAnswerFormEditAdditionalTab(): bool + { + return false; + } + + #[\Override] + public function getAnswerFormEditAdditionalTab(): null + { + return null; + } + + #[\Override] + public function providesAnswerFormEditAdditionalStep(): bool + { + return false; + } + + #[\Override] + public function getAnswerFormEditAdditionalStep(): null + { + return null; + } + + #[\Override] + public function providesRenderer(): bool + { + return true; + } + + #[\Override] + public function getRenderer( + Properties $answer_form_properties + ): Viewable { + return $answer_form_properties + ->getDefinition() + ->getCapability(Async::class) + ->getViewable($answer_form_properties); + } + + #[\Override] + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): void { + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index 42b7b8881217..1fb93b446896 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Presentation\Layout\Viewable; interface Capability { @@ -36,6 +37,12 @@ public function providesAnswerFormEditAdditionalStep(): bool; public function getAnswerFormEditAdditionalStep(): ?AdditionalFormStepAction; + public function providesRenderer(): bool; + + public function getRenderer( + Properties $answer_form_properties + ): ?Viewable; + public function onAnswerFormUpdate( Properties $answer_form_properties ): void; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php index 11e51aedaac6..40ec3b7bf1e9 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php @@ -20,12 +20,14 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Capabilities\Capability; +use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; +use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\Viewable; class Edit { @@ -70,12 +72,16 @@ public function edit( DefaultEnvironment $environment, AnswerFormEditView $edit_view, string $action_from_get - ): EditForm|AnswerFormProperties|null { + ): Async|Viewable|AnswerFormProperties|null { $environment->setEditAnswerFormTabs( $this->required_actions_with_tab ); $action = $this->required_actions_with_tab[$action_from_get] ?? null; + if ($action === null) { + return null; + } + if ($action !== null) { $action->activateTab($tabs_gui); } else { @@ -86,10 +92,6 @@ public function edit( ); } - if ($action === null) { - return null; - } - return $action->do( $environment->withActionParameter($action_from_get) ); @@ -167,7 +169,7 @@ function (array $c, string $v): array { "The capability {$v} does not exist." ); } - $c['required_capabilities'][] = $capability; + $c['required_capabilities'][$v] = $capability; $action_with_tab = $capability->getAnswerFormEditAdditionalTab(); if ($action_with_tab !== null) { diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php index 49f5b836384c..b9c54288436b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; +use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -97,6 +97,19 @@ public function onAnswerFormUpdate( ); } + #[\Override] + public function providesRenderer(): bool + { + return false; + } + + #[\Override] + public function getRenderer( + Properties $answer_form_properties + ): null { + return null; + } + private function buildDoEditActionClosure(): \Closure { return function ( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php index 9471dde44e43..0abe788dd282 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php @@ -62,11 +62,11 @@ public function getTableSubNameSpace(): ?TableSubNameSpace } public function getColumns( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type, ): array { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); @@ -83,11 +83,11 @@ public function getColumns( } public function getIdColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); @@ -104,11 +104,11 @@ public function getIdColumn( } public function getForeignKeyColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); @@ -128,19 +128,19 @@ public function completeQuery( Query $query, ?Column $base_table_id_column ): Query { - $table_name_builder = $query->getTableNameBuilder(null); + $table_names_builder = $query->getTableNameBuilder(null); return $query->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, TableTypes::FeedbackGeneric ) ) )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, TableTypes::FeedbackSpecific ) ) @@ -148,7 +148,7 @@ public function completeQuery( $this->persistence_factory->join( $base_table_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, TableTypes::FeedbackSpecific ), JoinType::Left diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php index d4e45cd07295..7b721df05ed5 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php @@ -20,12 +20,11 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; -use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; +use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; -use ILIAS\Questions\Response\Response; class Capability implements CapabilityInterface { @@ -76,6 +75,19 @@ public function getAnswerFormEditAdditionalStep(): AdditionalFormStepAction ); } + #[\Override] + public function providesRenderer(): bool + { + return false; + } + + #[\Override] + public function getRenderer( + Properties $answer_form_properties + ): null { + return null; + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php index c7d7b4fc6f0e..7e11f6920df8 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php @@ -20,8 +20,8 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent; -use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; +use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -75,6 +75,19 @@ public function getAnswerFormEditAdditionalStep(): null return null; } + #[\Override] + public function providesRenderer(): bool + { + return false; + } + + #[\Override] + public function getRenderer( + Properties $answer_form_properties + ): null { + return null; + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php index b66db76b69bf..001213a858bd 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/TableDefinitions.php @@ -47,11 +47,11 @@ public function getTableSubNameSpace(): ?TableSubNameSpace } public function getColumns( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type ): array { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); return array_map( @@ -64,11 +64,11 @@ public function getColumns( } public function getIdColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); @@ -82,12 +82,12 @@ public function completeQuery( Query $query, ?Column $base_table_id_column ): Query { - $table_name_builder = $query->getTableNameBuilder(null); + $table_names_builder = $query->getTableNameBuilder(null); return $query->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, TableTypes::SuggestedLearningContent ) ) diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 061968baa540..7f4ddc0155aa 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -20,8 +20,10 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions; use ILIAS\Language\Language; @@ -34,6 +36,11 @@ public function buildProperties( TypeGenericProperties $type_generic_properties, ?Query $query ): Properties; + + public function buildResponse( + ?Query $query + ): Response; + public function getTableDefinitions(): TableDefinitions; public function hasCapability( @@ -44,6 +51,11 @@ public function getCapability( string $capability_class_name ): mixed; + public function initializeAttemptData( + Attempt $attempt, + AnswerFormProperties $answer_form_properties + ): Attempt; + public function getEditView(): Edit; public function getParticipantView(): Participant; diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index e5b312acc396..eee64a939653 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -44,7 +44,7 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly PersistenceFactory $persistence_factory, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - private readonly TableNameBuilder $table_name_builder, + private readonly TableNameBuilder $table_names_builder, private array $inserts, private readonly int $old_question_id, private readonly Uuid $new_question_id, @@ -76,7 +76,7 @@ public function getPersistenceFactory(): PersistenceFactory public function getTableNameBuilder(): TableNameBuilder { - return $this->table_name_builder; + return $this->table_names_builder; } public function getOldQuestionId(): int @@ -171,7 +171,7 @@ private function buildCoreAnswerFormInsertStatement(): Insert { return $this->persistence_factory->insert( $this->answer_form_generic_table_definitions->getColumns( - $this->table_name_builder, + $this->table_names_builder, AnswerFormGenericTableTypes::AnswerForms ), [ diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php index 61059ef661f4..94e0fa025042 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/AnswerFormGenericTableDefinitions.php @@ -23,13 +23,12 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; -use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions as TableDefinitionsInterface; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableTypes; -class AnswerFormGenericTableDefinitions implements TableDefinitionsInterface +class AnswerFormGenericTableDefinitions { public const string ANSWER_FORM_TABLE_ID_COLUMN = 'id'; private const string ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN = 'question_id'; @@ -55,14 +54,14 @@ public function getTableSubNameSpace(): ?TableSubNameSpace } public function getColumns( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type, array $columns_to_skip = [] ): array { return array_map( fn(string $v): Column => $this->persistence_factory->column( $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), $v @@ -77,12 +76,12 @@ public function getColumns( } public function getIdColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type ): Column { return $this->persistence_factory->column( $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), self::ANSWER_FORM_TABLE_ID_COLUMN @@ -90,29 +89,28 @@ public function getIdColumn( } public function getForeignKeyColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type ): Column { return $this->persistence_factory->column( $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN ); } - #[\Override] public function completeQuestionQuery( Query $query, ?Column $base_table_id_column ): Query { - $table_name_builder = $query->getTableNameBuilder(null); + $table_names_builder = $query->getTableNameBuilder(null); return $query->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormGenericTableTypes::AnswerForms ) ) @@ -120,7 +118,7 @@ public function completeQuestionQuery( $this->persistence_factory->join( $base_table_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormGenericTableTypes::AnswerForms ), JoinType::Left diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php index 94260ca7713b..b4617848609b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence/TableDefinitions.php @@ -22,11 +22,19 @@ use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Query; +use ILIAS\Questions\Persistence\TableSubNameSpace; interface TableDefinitions { + public function getTableSubNameSpace(): TableSubNameSpace; + public function completeQuestionQuery( Query $query, ?Column $base_table_id_column ): Query; + + public function completeResponseQuery( + Query $query, + ?Column $base_table_id_column + ): Query; } diff --git a/components/ILIAS/Questions/src/Question/PublicQuestionInterface.php b/components/ILIAS/Questions/src/AnswerForm/Response.php similarity index 75% rename from components/ILIAS/Questions/src/Question/PublicQuestionInterface.php rename to components/ILIAS/Questions/src/AnswerForm/Response.php index b6338f329375..222da52ab7f4 100644 --- a/components/ILIAS/Questions/src/Question/PublicQuestionInterface.php +++ b/components/ILIAS/Questions/src/AnswerForm/Response.php @@ -18,11 +18,12 @@ declare(strict_types=1); -namespace ILIAS\Questions\Question; +namespace ILIAS\Questions\AnswerForm; -use ILIAS\Questions\Question\Views\Participant; +use ILIAS\Questions\Persistence\Storable; +use ILIAS\Data\UUID\Uuid; -interface PublicQuestionInterface +interface Response extends Storable { - public function getParticipantView(): Participant; + public function getAnswerFormId(): Uuid; } diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 110d7227f80e..7c64920b5efe 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -96,18 +96,18 @@ public function toStorage( ); } - $table_name_builder = $manipulate->getTableNameBuilder(null); + $table_names_builder = $manipulate->getTableNameBuilder(null); return $manipulate->withAdditionalStatement( $manipulate->getManipulationType() === ManipulationType::Create ? $this->buildInsertStatement( $persistence_factory, $answer_form_generic_definitions, - $table_name_builder + $table_names_builder ) : $this->buildUpdateStatement( $persistence_factory, $answer_form_generic_definitions, - $table_name_builder + $table_names_builder ) ); } @@ -117,7 +117,7 @@ public function toDelete( AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, Manipulate $manipulate ): Manipulate { - $table_name_builder = $manipulate->getTableNameBuilder( + $table_names_builder = $manipulate->getTableNameBuilder( $answer_form_generic_table_definitions->getTableSubNameSpace() ); $table_type = AnswerFormGenericTableTypes::AnswerForms; @@ -125,13 +125,13 @@ public function toDelete( return $manipulate->withAdditionalStatement( $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $answer_form_generic_table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -147,11 +147,11 @@ public function toDelete( private function buildInsertStatement( PersistenceFactory $persistence_factory, AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Insert { return $persistence_factory->insert( $answer_form_generic_table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormGenericTableTypes::AnswerForms ), [ @@ -194,13 +194,13 @@ private function buildInsertStatement( private function buildUpdateStatement( PersistenceFactory $persistence_factory, AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Update { $table_type = AnswerFormGenericTableTypes::AnswerForms; return $persistence_factory->update( $answer_form_generic_table_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type, [ 'id', @@ -231,7 +231,7 @@ private function buildUpdateStatement( [ $persistence_factory->where( $answer_form_generic_table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php index 4f9811b388b2..50a512ed65d4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php @@ -21,14 +21,15 @@ namespace ILIAS\Questions\AnswerForm\Views; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Response\Response; interface Participant { - public function isAsyncPresentationAvailable(): bool; - - public function get( + public function show( Properties $properties, - ?Response $response + ?Attempt $attempt_data ): string; + + public function retrieveResponse(): Response; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php new file mode 100644 index 000000000000..85a096facff1 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php @@ -0,0 +1,55 @@ +answer_form_properties = $answer_form_properties; + return $clone; + } + + #[\Override] + public function getUI(): array|Component + { + if ($this->answer_form_properties === null) { + throw new UnexpectedValueException( + 'This is an uninitalized Async and cannot be viewed.' + ); + } + return $this->answer_form_properties + ->getDefinition() + ->getCapability( + Async::class + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index 8914f7ebc041..a6b7ec0077b2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -21,11 +21,15 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerForm as Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions; use ILIAS\Language\Language; @@ -36,9 +40,10 @@ class Definition implements DefinitionInterface * @param array $available_capabilities */ public function __construct( - private readonly PropertiesFactory $properties_factory, private readonly TableDefinitions $table_definitions, private readonly array $available_capabilities, + private readonly PropertiesFactory $properties_factory, + private readonly ResponseFactory $response_factory, private readonly Edit $edit_view, private readonly Participant $participant_view ) { @@ -62,6 +67,15 @@ public function buildProperties( ); } + #[\Override] + public function buildResponse( + ?Query $query + ): Response { + return $this->response_factory->fromData( + $query + ); + } + #[\Override] public function getTableDefinitions(): TableDefinitions { @@ -88,6 +102,14 @@ public function getEditView(): Edit return $this->edit_view; } + #[\Override] + public function initializeAttemptData( + Attempt $attempt, + AnswerFormProperties $answer_form_properties + ): Attempt { + return $answer_form_properties->getGaps()->initializeAttemptData($attempt); + } + #[\Override] public function getParticipantView(): Participant { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index e595a2da1287..aa294087b57a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Layout; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\EditGaps; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Edit as EditGaps; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\Range; use ILIAS\Data\Order; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index b73a0e3f1185..b028a6cd3b13 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -52,7 +52,7 @@ public function getNewAnswerInputIdForOld( private function buildGapInsertStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, ?Insert $gaps_insert, Uuid $answer_input_id, Uuid $answer_form_id, @@ -67,7 +67,7 @@ private function buildGapInsertStatement( if ($gaps_insert === null) { return $persistence_factory->insert( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerInputs ), $this->buildGapValuesForInsert( @@ -129,7 +129,7 @@ private function buildGapValuesForInsert( private function buildAnswerOptionInsertStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, ?Insert $options_insert, Uuid $answer_option_id, Uuid $answer_input_id, @@ -144,7 +144,7 @@ private function buildAnswerOptionInsertStatement( if ($options_insert === null) { return $persistence_factory->insert( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerOptions ), $this->buildOptionValuesForInsert( @@ -203,14 +203,14 @@ private function buildOptionValuesForInsert( private function buildAnswerFormInsertStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, Uuid $answer_form_id, ScoringIdentical $scoring_identical, int $combinations_enabled ): Insert { return $persistence_factory->insert( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), [ diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 103e73310a17..9b22bf1dd09b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -291,7 +291,7 @@ private function addCombinationInsertStatements( private function buildCombinationsInsert( PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, ?Insert $combinations_insert, Uuid $combination_id, Uuid $answer_form_id, @@ -306,7 +306,7 @@ private function buildCombinationsInsert( if ($combinations_insert === null) { return $persistence_factory->insert( $this->table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationsTableIdentifier() ), @@ -319,7 +319,7 @@ private function buildCombinationsInsert( private function buildCombinationsToAnswerOptionsInsert( PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, ?Insert $combinations_to_answer_options_insert, Uuid $combination_id, Uuid $gap_id, @@ -336,7 +336,7 @@ private function buildCombinationsToAnswerOptionsInsert( if ($combinations_to_answer_options_insert === null) { return $persistence_factory->insert( $this->table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index afe01f2abfd9..834cecef96f3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -97,7 +97,7 @@ public function toStorage( Uuid $answer_form_id, PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, Manipulate $manipulate ): Manipulate { if ($this->matching_values === []) { @@ -112,14 +112,14 @@ public function toStorage( $v->toStorage( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ), $manipulate->withAdditionalStatement( $this->buildReplace( $table_definitions, $persistence_factory, - $table_name_builder, + $table_names_builder, $answer_form_id ) ) @@ -129,20 +129,20 @@ public function toStorage( public function toDelete( PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, Manipulate $manipulate ): Manipulate { return $manipulate->withAdditionalStatement( $this->buildDelete( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) )->withAdditionalStatement( $this->buildDeleteForLinkedValues( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ); } @@ -150,12 +150,12 @@ public function toDelete( private function buildReplace( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, Uuid $answer_form_id ): Replace { return $persistence_factory->replace( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, $table_definitions->getCombinationsTableIdentifier() ), @@ -170,19 +170,19 @@ private function buildReplace( private function buildDelete( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_definition = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_definition, $table_definitions->getCombinationsTableIdentifier() ), [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_definition, $table_definitions->getCombinationsTableIdentifier() ), @@ -198,19 +198,19 @@ private function buildDelete( private function buildDeleteForLinkedValues( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_definition = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_definition, $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_definition, $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index c9bb588d5a29..d5d03e943e6a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -98,9 +98,9 @@ public function hasMatchingCombinationForAnswerOptionIds( return false; } - public function getEditView(): EditCombinations + public function getEditView(): Edit { - return new EditCombinations( + return new Edit( $this->combinations_factory ); } @@ -108,7 +108,7 @@ public function getEditView(): EditCombinations public function toStorage( Manipulate $manipulate, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Manipulate { return array_reduce( $this->combinations, @@ -116,7 +116,7 @@ public function toStorage( $this->answer_form_id, $this->persistence_factory, $table_definitions, - $table_name_builder, + $table_names_builder, $c ), array_reduce( @@ -124,7 +124,7 @@ public function toStorage( fn(Manipulate $c, Combination $v): Manipulate => $v->toDelete( $this->persistence_factory, $table_definitions, - $table_name_builder, + $table_names_builder, $manipulate ), $manipulate @@ -135,7 +135,7 @@ public function toStorage( public function toDelete( Manipulate $manipulate, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Manipulate { return array_reduce( $this->combinations, @@ -143,7 +143,7 @@ public function toDelete( $v->toDelete( $this->persistence_factory, $table_definitions, - $table_name_builder, + $table_names_builder, $manipulate ) ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php similarity index 91% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php index c584a585bb2c..2d1302a2c459 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php @@ -20,12 +20,13 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; +use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; -class EditCombinations +class Edit { private const string SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW = 'eco'; @@ -47,7 +48,11 @@ public function addCombinationsSubTab( public function show( Environment $environment - ): Async|Viewable|Properties { + ): Async|Viewable|Properties|null { + if (!$environment->isCapabilityRequired(Marking::class)) { + return null; + } + $environment->addEditAnswerFormSubTab( self::SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW, self::LANG_VAR_EDIT_COMBINATIONS diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index 24a59608e5a8..644e6f102ac8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -76,7 +76,7 @@ public function buildPresentationString( public function toStorage( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Replace { if ($this->answer_option === null) { throw new \UnexpectedValueException( @@ -87,7 +87,7 @@ public function toStorage( $table_type = AnswerFormSpecificTableTypes::Additional; return $persistence_factory->replace( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type, $table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php index 4d0573df90bb..0d22027a12f0 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -49,6 +49,7 @@ public function fromData( $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), $type_generic_properties->getDefinition(), + $type_generic_properties->getAvailablePoints(), $this->cloze_text_factory->buildFromTextString( $type_generic_properties->getAdditionalText() ), @@ -105,6 +106,7 @@ function (array $vs) use ($type_generic_properties): array { $type_generic_properties->getAnswerFormId(), $type_generic_properties->getQuestionId(), $type_generic_properties->getDefinition(), + $type_generic_properties->getAvailablePoints(), $this->cloze_text_factory->buildFromTextString( $type_generic_properties->getAdditionalText() ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index 958a777c27c4..d3f6d1c820ad 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -41,7 +41,7 @@ public function __construct( private string $text_value = '', private ?float $lower_limit = null, private ?float $upper_limit = null, - private float $available_points = 0.0 + private ?float $available_points = null ) { } @@ -102,13 +102,13 @@ public function withUpperLimit( return $clone; } - public function getAvailablePoints(): float + public function getAvailablePoints(): ?float { return $this->available_points; } public function withAvailablePoints( - float $available_points + ?float $available_points ): self { $clone = clone $this; $clone->available_points = $available_points; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index e27f32873aca..83d33d8f4a77 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -50,6 +50,24 @@ public function __construct( $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); } + public function getMaxAvailablePoints(): float + { + return array_reduce( + $this->answer_options_awarding_points, + function (?float $c, AnswerOption $v): ?float { + if ($v->getAvailablePoints() === null) { + return $c; + } + + if ($c === null) { + return $v->getAvailablePoints(); + } + + return max($c, $v->getAvailablePoints()); + } + ); + } + public function isIncomplete(): bool { return $this->is_incomplete @@ -250,7 +268,7 @@ public function buildReplace( ?Replace $replace, TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Replace { return array_reduce( $this->answer_options, @@ -258,7 +276,7 @@ public function buildReplace( $persistence_factory, $c, $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerOptions ) ), @@ -269,20 +287,20 @@ public function buildReplace( public function buildDelete( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = AnswerFormSpecificTableTypes::AnswerOptions; return $persistence_factory->delete( $table_type->getTable( $persistence_factory, - $table_name_builder + $table_names_builder ), [ $persistence_factory->where( $table_definitions->getForeignKeyColumn( $persistence_factory, - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Upload.php similarity index 97% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Upload.php index 08d3cd9d60cd..4f36758c8459 100755 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/UploadAnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Upload.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; use ILIAS\Questions\Presentation\Definitions\Actor; use ILIAS\Questions\Presentation\Definitions\Environment; @@ -31,7 +31,7 @@ use ILIAS\FileUpload\FileUpload; use ILIAS\UI\Component\Input\Field\UploadHandler; -class UploadAnswerOptions implements UploadHandler, Actor +class Upload implements UploadHandler, Actor { private const string SUB_ACTION_UPLOAD = 'uhu'; private const string SUB_ACTION_REMOVE = 'uhr'; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Edit.php similarity index 98% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Edit.php index 38f8aff882d2..97a6243b7859 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Edit.php @@ -18,10 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Upload; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Factory as GapFactory; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -32,7 +33,7 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\URLBuilder; -class EditGaps +class Edit { private const string SUB_ACTION_BACK_TO_EDIT_BASIC_PROPERTIES = 'bebp'; private const string SUB_ACTION_SET_GAP_TYPES = 'sgt'; @@ -55,7 +56,7 @@ public function do( Environment $environment, string $sub_action = self::SUB_ACTION_SET_GAP_TYPES ): EditForm|Async|Properties|string { - $upload_handler = new UploadAnswerOptions( + $upload_handler = new Upload( $this->file_upload, $environment ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 741a1bb77857..76a32030165d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; @@ -262,7 +263,7 @@ public function buildReplace( ?Replace $replace, TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Replace { if ($this->type === null) { throw new \UnexpectedValueException( @@ -275,7 +276,7 @@ public function buildReplace( if ($replace === null) { return $persistence_factory->replace( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type ), $this->buildValuesForGapReplace($persistence_factory) @@ -307,9 +308,13 @@ public function buildGapPlaceholderNameWithId(): string return self::GAP_PLACEHOLDER_NAME . '_' . $this->getAnswerInputId()->toString(); } - public function buildParticipantViewLegacyInput(): string - { - return $this->type->getParticipantViewLegacyInput($this); + public function buildParticipantViewLegacyInput( + ?Attempt $attempt_data + ): string { + return $this->type->getParticipantViewLegacyInput( + $this, + $attempt_data + ); } public function getEditAnswerOptionsSection( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index bca52aabb28b..5d0a97fe4f25 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Junctor; @@ -35,6 +36,7 @@ use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Random\Seed\RandomSeed; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\MultiSelect; use ILIAS\UI\Component\Input\Field\Section; @@ -64,6 +66,26 @@ function (array $c, Gap $v): array { ); } + public function getTotalAvailablePoints(): ?float + { + return array_reduce( + $this->gaps, + function (?float $c, Gap $v): ?float { + $points_from_options = $v->getAnswerOptions()->getMaxAvailablePoints(); + + if ($points_from_options === null) { + return $c; + } + + if ($c === null) { + return $points_from_options; + } + + return $c + $points_from_options; + } + ); + } + public function getGapById( Uuid $gap_id ): ?Gap { @@ -163,12 +185,15 @@ public function getAddedGaps( return array_diff_key($this->gaps, $old_gaps->gaps); } - public function getPlaceholderArrayForParticipantView(): array - { + public function getPlaceholderArrayForParticipantView( + ?Attempt $attempt_data + ): array { return array_reduce( $this->gaps, - function (array $c, Gap $v): array { - $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildParticipantViewLegacyInput(); + function (array $c, Gap $v) use ($attempt_data): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildParticipantViewLegacyInput( + $attempt_data + ); return $c; }, [] @@ -356,6 +381,22 @@ public function withValuesFromCarry( return $clone; } + public function initializeAttemptData( + Attempt $attempt + ): Attempt { + return array_reduce( + $this->gaps, + fn(Attempt $c, Gap $v): Attempt => $v->getShuffleAnswerOptions() + ? $c->withAdditionalData( + $v->getAnswerInputId(), + $this->refinery->kindlyTo()->string()->transform( + (new RandomSeed())->createSeed() + ) + ) : $c, + $attempt + ); + } + public function toTableRows( DataRowBuilder $row_builder, Language $lng @@ -372,7 +413,7 @@ public function toStorage( Manipulate $manipulate, PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Manipulate { [ 'gaps' => $replace_for_gaps, @@ -384,13 +425,13 @@ public function toStorage( $c['gaps'], $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ), 'answer_options' => $v->getAnswerOptions()->buildReplace( $c['answer_options'], $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ], [ @@ -403,7 +444,7 @@ public function toStorage( $this->buildDeleteForRemovedGaps( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) )->withAdditionalStatement($replace_for_gaps) ->withAdditionalStatement($replace_for_answer_options); @@ -413,7 +454,7 @@ public function toDelete( Manipulate $manipulate, PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Manipulate { return array_reduce( $this->gaps, @@ -421,14 +462,14 @@ public function toDelete( $v->getAnswerOptions()->buildDelete( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ), $manipulate->withAdditionalStatement( $this->buildDeleteForDeletionOfAnswerForm( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ) ); @@ -437,18 +478,18 @@ public function toDelete( private function buildDeleteForRemovedGaps( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = AnswerFormSpecificTableTypes::AnswerInputs; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $table_definitions->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -458,7 +499,7 @@ private function buildDeleteForRemovedGaps( ), $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -479,19 +520,19 @@ private function buildDeleteForRemovedGaps( private function buildDeleteForDeletionOfAnswerForm( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = AnswerFormSpecificTableTypes::AnswerInputs; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $table_definitions->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index b9e7e59f5563..e71d254cb5c9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -22,7 +22,8 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\UploadAnswerOptions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Upload; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\FileUpload\MimeType; use ILIAS\FileUpload\FileUpload; @@ -55,7 +56,8 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( - Gap $gap + Gap $gap, + ?Attempt $attempt_data ): string { $answer_input_id = $gap->getAnswerInputId()->toString(); $gaptemplate = new \ilTemplate( @@ -96,7 +98,7 @@ public function getEditAnswerOptionsInputs( [] )->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'upload_answer_options' => $ff->file( - new UploadAnswerOptions( + new Upload( $file_upload, $environment ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index a16e242a15d2..b8f89a049d60 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -53,7 +54,8 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( - Gap $gap + Gap $gap, + ?Attempt $attempt_data ): string { $gaptemplate = new \ilTemplate( 'tpl.il_as_qpl_cloze_question_gap_numeric.html', diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 3f616bdccb3b..09bf36e4140e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -22,12 +22,14 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Random\Seed\GivenSeed; +use ILIAS\Refinery\Random\Seed\RandomSeed; use ILIAS\Refinery\Transformation; use ILIAS\UI\Factory as UIFactory; @@ -51,7 +53,8 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( - Gap $gap + Gap $gap, + ?Attempt $attempt_data ): string { $gaptemplate = new \ilTemplate( 'tpl.il_as_qpl_cloze_question_gap_select.html', @@ -60,11 +63,12 @@ public function getParticipantViewLegacyInput( 'components/ILIAS/TestQuestionPool' ); - $shuffler = $gap->getShuffleAnswerOptions() - ? $this->refinery->random()->shuffleArray(new GivenSeed(4)) - : $this->refinery->random()->dontShuffle(); - - foreach ($gap->getAnswerOptions()->buildArrayForSelectInput($shuffler) as $key => $answer_option) { + foreach ($gap->getAnswerOptions()->buildArrayForSelectInput( + $this->buildShuffler( + $gap, + $attempt_data + ) + ) as $key => $answer_option) { $gaptemplate->setCurrentBlock('select_gap_option'); $gaptemplate->setVariable( 'SELECT_GAP_VALUE', @@ -153,4 +157,23 @@ public function getBuildGapTransformation( )->withShuffleAnswerOptions($vs['shuffle_answer_options']) ); } + + private function buildShuffler( + Gap $gap, + Attempt $attempt_data + ): Transformation { + if (!$gap->getShuffleAnswerOptions()) { + return $this->refinery->random()->dontShuffle(); + } + + return $this->refinery->random()->shuffleArray( + $attempt_data === null + ? new RandomSeed() + : new GivenSeed( + $this->refinery->kindlyTo()->int()->transform( + $attempt_data->getAdditionalDataFor($gap->getAnswerInputId()) + ) + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 3a55ba341ca4..3317f8ae7833 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\FileUpload\FileUpload; @@ -51,7 +52,8 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( - Gap $gap + Gap $gap, + ?Attempt $attempt_data ): string { $gaptemplate = new \ilTemplate( 'tpl.il_as_qpl_cloze_question_gap_text.html', diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index 36fe05d28a1d..9bdabef77863 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types as FeedbackTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Definitions\Range; use ILIAS\Data\UUID\Factory as UuidFactory; @@ -43,7 +44,8 @@ public function __construct( abstract public function getIdentifier(): string; abstract public function getParticipantViewLegacyInput( - Gap $gap + Gap $gap, + ?Attempt $attempt ): string; abstract public function getEditAnswerOptionsInputs( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index ea51a86cba05..890eae1a00f4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; +use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Properties as PropertiesInterface; use ILIAS\Questions\AnswerForm\TypeGenericProperties; @@ -42,8 +43,6 @@ use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Table\Data as DataTable; @@ -54,6 +53,7 @@ class Properties implements PropertiesInterface private const string KEY_ENABLE_COMBINATIONS = 'enable_combinations'; private const string KEY_GAPS = 'gaps'; + private bool $updated_gaps = false; private bool $updated_combinations = false; /** @@ -63,6 +63,7 @@ public function __construct( private readonly Uuid $answer_form_id, private readonly Uuid $question_id, private readonly Definition $definition, + private readonly ?float $available_points, private Text $cloze_text, private readonly string $legacy_cloze_text, private ScoringIdentical $scoring_identical, @@ -96,7 +97,7 @@ public function getTypeGenericProperties(): TypeGenericProperties $this->answer_form_id, $this->question_id, $this->definition, - null, + $this->buildAvailablePointsForGenericProperties(), null, null, $this->cloze_text->getRawRepresentation(), @@ -166,6 +167,7 @@ public function withGaps( ): self { $clone = clone $this; $clone->gaps = $gaps; + $clone->updated_gaps = true; return $clone; } @@ -197,13 +199,15 @@ public function getOverviewTable( } public function buildBasicEditingInputs( - Language $lng, - FieldFactory $ff, - Refinery $refinery, + Environment $environment, Factory $properties_factory, ClozeTextFactory $cloze_text_factory, bool $add_legacy_cloze_text_to_input ): Section { + $lng = $environment->getLanguage(); + $ff = $environment->getUIFactory()->input()->field(); + $refinery = $environment->getRefinery(); + $cloze_text_input = $this->cloze_text->getInput( $lng, $ff, @@ -219,18 +223,24 @@ public function buildBasicEditingInputs( ); } + $inputs = [ + self::KEY_CLOZE_TEXT => $cloze_text_input, + self::KEY_SCORING_IDENTICAL => ScoringIdentical::buildInput( + $lng, + $ff, + $refinery, + $this->scoring_identical + )->withValue($this->getScoringOfIdenticalResponses()->value) + ]; + + if ($environment->isCapabilityRequired(Marking::class)) { + $inputs[self::KEY_ENABLE_COMBINATIONS] = $ff->checkbox( + $lng->txt('cloze_enable_combinations') + )->withValue($this->combinations->areCombinationsEnabled()); + } + return $ff->section( - [ - self::KEY_CLOZE_TEXT => $cloze_text_input, - self::KEY_SCORING_IDENTICAL => ScoringIdentical::buildInput( - $lng, - $ff, - $refinery, - $this->scoring_identical - )->withValue($this->getScoringOfIdenticalResponses()->value), - self::KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) - ->withValue($this->combinations->areCombinationsEnabled()) - ], + $inputs, $lng->txt('set_basic_properties') )->withAdditionalTransformation( $refinery->custom()->transformation( @@ -238,7 +248,7 @@ public function buildBasicEditingInputs( $this, $vs[self::KEY_CLOZE_TEXT], $vs[self::KEY_SCORING_IDENTICAL], - $vs[self::KEY_ENABLE_COMBINATIONS] + $vs[self::KEY_ENABLE_COMBINATIONS] ?? false ) ) ); @@ -283,7 +293,7 @@ public function toStorage( ): Manipulate { $table_definitions = $this->definition->getTableDefinitions(); - $table_name_builder = $manipulate->getTableNameBuilder( + $table_names_builder = $manipulate->getTableNameBuilder( $table_definitions->getTableSubNameSpace() ); @@ -291,24 +301,24 @@ public function toStorage( ? $this->buildInsertAnswerFormStatement( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) : $this->buildUpdateAnswerFormStatement( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ); return $this->gaps->toStorage( $this->addReplaceCombinationsStatements( $manipulate, $table_definitions, - $table_name_builder + $table_names_builder )->withAdditionalStatement( $answer_form_statement ), $persistence_factory, $table_definitions, - $table_name_builder + $table_names_builder ); } @@ -318,7 +328,7 @@ public function toDelete( Manipulate $manipulate ): Manipulate { $table_definitions = $this->definition->getTableDefinitions(); - $table_name_builder = $manipulate->getTableNameBuilder( + $table_names_builder = $manipulate->getTableNameBuilder( $table_definitions->getTableSubNameSpace() ); @@ -327,23 +337,23 @@ public function toDelete( $this->buildDeleteAnswerFormStatement( $table_definitions, $persistence_factory, - $table_name_builder + $table_names_builder ) ), $persistence_factory, $table_definitions, - $table_name_builder + $table_names_builder ); } private function buildInsertAnswerFormStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Insert { return $persistence_factory->insert( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), [ @@ -366,12 +376,12 @@ private function buildInsertAnswerFormStatement( private function buildUpdateAnswerFormStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Update { $table_type = AnswerFormSpecificTableTypes::TypeSpecificAnswerForms; return $persistence_factory->update( $table_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type, '', ['answer_form_id'] @@ -389,7 +399,7 @@ private function buildUpdateAnswerFormStatement( [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -404,7 +414,7 @@ private function buildUpdateAnswerFormStatement( private function addReplaceCombinationsStatements( Manipulate $manipulate, TableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Manipulate { if (!$this->combinations->areCombinationsEnabled() || !$this->updated_combinations) { @@ -414,26 +424,26 @@ private function addReplaceCombinationsStatements( return $this->combinations->toStorage( $manipulate, $table_definitions, - $table_name_builder + $table_names_builder ); } private function buildDeleteAnswerFormStatement( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = AnswerFormSpecificTableTypes::TypeSpecificAnswerForms; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $table_definitions->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -444,4 +454,17 @@ private function buildDeleteAnswerFormStatement( ] ); } + + private function buildAvailablePointsForGenericProperties(): ?float + { + if (!$this->updated_gaps && !$this->updated_combinations) { + return $this->available_points; + } + + if (!$this->combinations->areCombinationsEnabled()) { + return $this->gaps->getTotalAvailablePoints(); + } + + return 1.1; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php new file mode 100644 index 000000000000..96f84348cf72 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php @@ -0,0 +1,110 @@ + $answer_input_responses + */ + private readonly array $answer_input_responses; + + /** + * @param array $answer_input_responses + */ + public function __construct( + private readonly TableDefinitions $table_definitions, + private readonly Uuid $response_id, + private readonly Uuid $answer_form_id, + array $answer_input_responses + ) { + $this->answer_input_responses = array_reduce( + $answer_input_responses, + function (array $c, AnswerInput $v): array { + $c[$v->getAnswerInputId()->toString()] = $v; + return $c; + }, + [] + ); + } + + #[\Override] + public function getAnswerFormId(): Uuid + { + return $this->answer_form_id; + } + + public function toStorage( + PersistenceFactory $persistence_factory, + Manipulate $manipulate + ): Manipulate { + return $manipulate->withAdditionalStatement( + array_reduce( + $this->answer_input_responses, + fn(?Replace $c, AnswerInput $v): Insert => $v->toStorage( + $this->table_definitions, + $manipulate->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), + $persistence_factory, + $c, + $this->response_id + ) + ) + ); + } + + public function toDelete( + PersistenceFactory $persistence_factory, + Manipulate $manipulate + ): Manipulate { + return $manipulate->withAdditionalStatement( + $persistence_factory->delete( + $persistence_factory->table( + $manipulate->getTableNameBuilder( + AnswerFormSpecificTableTypes::Responses + ), + AnswerFormSpecificTableTypes::Responses + ), + [ + $persistence_factory->where( + $this->table_definitions->getIdColumn( + $persistence_factory, + AnswerFormSpecificTableTypes::Responses + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->response_id->toString() + ) + ) + ] + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php new file mode 100644 index 000000000000..5ee53fabace5 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php @@ -0,0 +1,89 @@ +answer_input_id; + } + + public function toStorage( + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder, + PersistenceFactory $persistence_factory, + ?Replace $insert, + Uuid $id + ): Insert { + if ($insert === null) { + return $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + AnswerFormSpecificTableTypes::Responses + ), + $this->buildValuesArrayForStorage($persistence_factory, $id) + ); + } + + return $insert->withAdditionalValues( + $this->buildValuesArrayForStorage($persistence_factory, $id) + ); + } + + private function buildValuesArrayForStorage( + PersistenceFactory $persistence_factory, + Uuid $id + ): array { + return [ + $persistence_factory->value( + FieldDefinition::T_TEXT, + $id->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->answer_input_id->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->selected_answer_option?->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->text + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php new file mode 100644 index 000000000000..6f04a89ac9f5 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php @@ -0,0 +1,64 @@ +retrieveCurrentRecord( + $this->persistence_factory->table( + $query->getTableNameBuilder( + $this->definition + ->getTableDefinitions() + ->getTableSubNameSpace() + ), + AnswerFormSpecificTableTypes::Responses + ), + $query->getRefinery()->custom()->transformation( + fn(array $vs): AnswerForm => new AnswerForm( + $this->definition->getTableDefinitions(), + ) + ) + ); + } + +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php index 3796515bc0ab..1beb39643c14 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php @@ -56,6 +56,7 @@ class TableDefinitions implements TableDefinitionsInterface ]; private const string ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN = 'answer_input_id'; + private const string ANSWER_OPTIONS_TABLE_ADDITIONAL_ORDER_COLUMN = 'position'; private const array ANSWER_OPTIONS_TABLE_COLUMNS = [ 'id', 'answer_input_id', @@ -84,6 +85,15 @@ class TableDefinitions implements TableDefinitionsInterface 'in_range' ]; + private const string RESPONSE_TABLE_ID_COLUMN = 'response_id'; + private const string RESPONSE_TABLE_FOREIGN_KEY_COLUMN = 'response_id'; + private const array RESPONSE_TABLE_COLUMNS = [ + 'response_id', + 'answer_input_id', + 'selected_answer_option', + 'text' + ]; + public function __construct( private readonly PersistenceFactory $persistence_factory, private readonly TableSubNameSpace $table_name_specifiers @@ -96,13 +106,13 @@ public function getTableSubNameSpace(): TableSubNameSpace } public function getColumns( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type, string $sub_table_identifier = '', array $columns_to_skip = [] ): array { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type, $sub_table_identifier ); @@ -110,11 +120,13 @@ public function getColumns( AnswerFormSpecificTableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, AnswerFormSpecificTableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, AnswerFormSpecificTableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, + AnswerFormSpecificTableTypes::Responses => self::RESPONSE_TABLE_COLUMNS, AnswerFormSpecificTableTypes::Additional => match($sub_table_identifier) { self::COMBINATION_TABLE_IDENTIFIER => self::COMBINATION_TABLE_COLUMNS, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER => self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS } }; + return array_map( fn(string $v): Column => $this->persistence_factory->column( $table, @@ -130,12 +142,12 @@ public function getColumns( } public function getIdColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type, string $sub_table_identifier = '' ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type, $sub_table_identifier ); @@ -147,6 +159,13 @@ public function getIdColumn( ); } + if ($table_type === AnswerFormSpecificTableTypes::Responses) { + return $this->persistence_factory->column( + $table, + self::RESPONSE_TABLE_ID_COLUMN + ); + } + if ($sub_table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { return $this->persistence_factory->column( $table, @@ -161,12 +180,12 @@ public function getIdColumn( } public function getForeignKeyColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table_type, string $sub_table_identifier = '' ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type, $sub_table_identifier ); @@ -184,6 +203,10 @@ public function getForeignKeyColumn( $table, self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN ), + AnswerFormSpecificTableTypes::Responses => $this->persistence_factory->column( + $table, + self::RESPONSE_TABLE_FOREIGN_KEY_COLUMN + ), AnswerFormSpecificTableTypes::Additional => $this->persistence_factory->column( $table, $sub_table_identifier === self::COMBINATION_TABLE_IDENTIFIER @@ -198,12 +221,12 @@ public function completeQuestionQuery( Query $query, ?Column $answer_form_id_column ): Query { - $table_name_builder = $query->getTableNameBuilder( + $table_names_builder = $query->getTableNameBuilder( $this->getTableSubNameSpace() ); $combinations_id_column = $this->getIdColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ); @@ -212,7 +235,7 @@ public function completeQuestionQuery( $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ), JoinType::Left @@ -220,7 +243,7 @@ public function completeQuestionQuery( )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ) ) @@ -228,7 +251,7 @@ public function completeQuestionQuery( $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerInputs ), JoinType::Left @@ -236,18 +259,18 @@ public function completeQuestionQuery( )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerInputs ) ) )->withAdditionalJoin( $this->persistence_factory->join( $this->getIdColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerInputs ), $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerOptions ), JoinType::Left @@ -255,14 +278,14 @@ public function completeQuestionQuery( )->withAdditionalOrder( $this->persistence_factory->order( $this->getIdColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerInputs ) ) )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerOptions ) ) @@ -270,17 +293,17 @@ public function completeQuestionQuery( $this->persistence_factory->order( $this->persistence_factory->column( $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::AnswerOptions ), - 'position' + self::ANSWER_OPTIONS_TABLE_ADDITIONAL_ORDER_COLUMN ) ) )->withAdditionalJoin( $this->persistence_factory->join( $answer_form_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ), @@ -289,7 +312,7 @@ public function completeQuestionQuery( )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TABLE_IDENTIFIER ) @@ -298,7 +321,7 @@ public function completeQuestionQuery( $this->persistence_factory->join( $combinations_id_column, $this->getForeignKeyColumn( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ), @@ -307,7 +330,7 @@ public function completeQuestionQuery( )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, AnswerFormSpecificTableTypes::Additional, self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER ) @@ -319,6 +342,34 @@ public function completeQuestionQuery( ); } + #[\Override] + public function completeResponseQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_names_builder = $query->getTableNameBuilder( + $this->getTableSubNameSpace() + ); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_names_builder, + AnswerFormSpecificTableTypes::Responses + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $base_table_id_column, + $this->getForeignKeyColumn( + $table_names_builder, + AnswerFormSpecificTableTypes::Responses + ), + JoinType::Left + ) + ); + } + public function getCombinationsTableIdentifier(): string { return self::COMBINATION_TABLE_IDENTIFIER; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index f17edeab7b53..67f98f9a7f6b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -20,10 +20,12 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerForm\Views\Edit as EditViewInterface; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Edit as EditGaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; @@ -74,7 +76,8 @@ public function edit( $sub_action = $environment->getSubAction(); $combinations = $environment->getAnswerFormProperties()->getCombinations(); - if ($combinations->areCombinationsEnabled()) { + if ($environment->isCapabilityRequired(Marking::class) + && $combinations->areCombinationsEnabled()) { $combinations->getEditView()->addCombinationsSubTab($environment); } @@ -108,9 +111,17 @@ public function edit( public function other( Environment $environment ): Async|Viewable|Properties { - return $environment + $from_other = $environment ->getAnswerFormProperties() ->getCombinations()->getEditView()->show($environment); + + if ($from_other === null) { + return $this->edit( + $environment->withDefaultSubAction() + ); + } + + return $from_other; } #[\Override] @@ -282,9 +293,7 @@ private function buildInputsBuilderForBasicInputs( $environment->getAnswerFormProperties(), $carry )->buildBasicEditingInputs( - $environment->getLanguage(), - $environment->getUIFactory()->input()->field(), - $environment->getRefinery(), + $environment, $this->properties_factory, $this->cloze_text_factory, $add_legacy_cloze_text_to_input diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php index 77579cd67a73..200f6fcb3d3a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\AnswerForm\Views\Participant as ParticipantViewInterface; use ILIAS\Questions\Response\Response; @@ -35,20 +36,20 @@ public function __construct( } #[\Override] - public function isAsyncPresentationAvailable(): bool - { - return true; - } - - #[\Override] - public function get( + public function show( Properties $properties, - ?Response $response + ?Attempt $attempt_data ): string { $this->global_tpl->addJavaScript('assets/js/ParticipantViewLongMenu.js'); return $this->mustache_engine->render( $properties->getClozeTextForPresentation(), - $properties->getGaps()->getPlaceholderArrayForParticipantView() + $properties->getGaps()->getPlaceholderArrayForParticipantView($attempt_data) ); } + + #[\Override] + public function retrieveResponse(): Response + { + + } } diff --git a/components/ILIAS/Questions/src/Attempt/Attempt.php b/components/ILIAS/Questions/src/Attempt/Attempt.php new file mode 100644 index 000000000000..3e47dd0e4a7a --- /dev/null +++ b/components/ILIAS/Questions/src/Attempt/Attempt.php @@ -0,0 +1,180 @@ + + */ + private array $responses = []; + + public function __construct( + private readonly Uuid $identifier, + private readonly int $shuffle_questions_seed, + private array $additional_data = [] + ) { + } + + public function getId(): Uuid + { + return $this->identifier; + } + + public function getShuffleQuestionsSeed(): GivenSeed + { + return new GivenSeed($this->shuffle_questions_seed); + } + + public function getAdditionalDataFor( + Uuid $parent_id + ): ?string { + if (!isset($this->additional_data[$parent_id->toString()])) { + return null; + } + return $this->additional_data[$parent_id->toString()]; + } + + public function withAdditionalData( + Uuid $parent_id, + string $data + ): self { + if (isset($this->additional_data[$parent_id->toString()])) { + throw new InvalidArgumentException( + 'This is a storage for data that stays constant accross the test run.' + . 'Data cannot be changed one it is set.' + ); + } + + $clone = clone $this; + $clone->additional_data[$parent_id->toString()] = $data; + return $clone; + } + + public function getResponseFor( + Uuid $question_id + ): ?Response { + return $this->responses[$question_id->toString()] ?? null; + } + + public function withAdditionalResponse( + Response $response + ): self { + $clone = clone $this; + $clone->responses[$response->getQuestionId()->toString()] = $response; + return $clone; + } + + public function basicDataToStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder + ): Insert { + return $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + TableTypes::AttemptData + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->identifier->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->shuffle_questions_seed + ) + ] + ); + } + + public function additionalDataToStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder + ): ?Insert { + return array_reduce( + array_keys($this->additional_data), + fn(?Insert $c, string $v): Insert + => $this->buildAdditionalDataInsert( + $persistence_factory, + $table_definitions, + $table_names_builder, + $c, + $v + ) + ); + } + + private function buildAdditionalDataInsert( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder, + ?Insert $insert, + string $parent_id + ): Insert { + if ($insert === null) { + return $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + TableTypes::AdditionalAttemptData + ), + $this->buildAdditionalDataValuesArray( + $persistence_factory, + $parent_id + ) + ); + } + + return $insert->withAdditionalValues( + $this->buildAdditionalDataValuesArray( + $persistence_factory, + $parent_id + ) + ); + } + + private function buildAdditionalDataValuesArray( + PersistenceFactory $persistence_factory, + string $parent_id + ): array { + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->identifier->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $parent_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_data[$parent_id] + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/Attempt/Repository.php b/components/ILIAS/Questions/src/Attempt/Repository.php new file mode 100644 index 000000000000..ec0e37e93f69 --- /dev/null +++ b/components/ILIAS/Questions/src/Attempt/Repository.php @@ -0,0 +1,288 @@ +table_names_builder = new TableNameBuilder( + QuestionRepository::COMPONENT_NAMESPACE, + null + ); + } + + /** + * @param array $questions + * @throws InvalidArgumentException + */ + public function getAttemptFor( + Uuid $attempt_id, + array $questions + ): Attempt { + if ($attempt_id === null) { + $attempt = $this->getNewAttempt($questions); + $this->storeAttempt($attempt); + return $attempt; + } + + $base_table_id_column = $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::AttemptData + ); + + $database_values = array_reduce( + $questions, + fn(Query $c, Question $v): Query => $v->completeResponseQuery( + $c, + $base_table_id_column + ), + $this->buildQuery($attempt_id) + )->loadNextRecord() + ->current(); + + if ($database_values === null) { + throw new \InvalidArgumentException('No Attempt With Given Identifier'); + } + + return array_reduce( + $questions, + fn(Attempt $c, Question $v): Attempt => $c->withAdditionalResponse( + $this->retriveResponseFromQuery( + $v, + $c->getId(), + $database_values + ) + ), + $this->retrieveAttemptFromQuery( + $attempt_id, + $database_values, + $this->retriveAdditionalDataFromQuery( + $database_values + ) + ) + ); + } + + public function getNewResponseFor( + Uuid $question_id, + Uuid $attempt_id + ): Response { + return new Response( + $this->uuid_factory->uuid4(), + $question_id, + $attempt_id, + new \DateTimeImmutable('@' . time()) + ); + } + + private function storeAttempt( + Attempt $attempt + ): void { + $manipulate = (new Manipulate( + $this->db, + ManipulationType::Create, + QuestionRepository::COMPONENT_NAMESPACE + ))->withAdditionalStatement( + $attempt->basicDataToStorage( + $this->persistence_factory, + $this->table_definitions, + $this->table_names_builder + ) + ); + + $additional_data_statement = $attempt->additionalDataToStorage( + $this->persistence_factory, + $this->table_definitions, + $this->table_names_builder + ); + + if ($additional_data_statement !== null) { + $manipulate = $manipulate->withAdditionalStatement( + $additional_data_statement + ); + } + + $manipulate->run(); + } + + /** + * @param array $questions + */ + private function getNewAttempt( + array $questions + ): Attempt { + $attempt = new Attempt( + $this->uuid_factory->uuid4(), + (new RandomSeed())->createSeed() + ); + + return array_reduce( + $questions, + fn(Attempt $c, Question $v) => $v->initializeAttemptData($c), + $attempt + ); + } + + private function buildQuery( + Uuid $attempt_id + ): Query { + $attempt_data_id_column = $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::AttemptData + ); + return $this->table_definitions->completeLoadAttemptQuery( + new Query( + $this->db, + $this->refinery, + QuestionRepository::COMPONENT_NAMESPACE, + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::AttemptData + ) + ), + $attempt_data_id_column + )->withAdditionalWhere( + $this->persistence_factory->where( + $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::AttemptData + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $attempt_id->toString() + ) + ) + ); + } + + private function retriveAdditionalDataFromQuery( + Query $query + ): array { + return $query->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::AdditionalAttemptData + ), + $this->refinery->custom()->transformation( + function (array $vs): array { + return array_reduce( + $vs, + function (array $c, array $v): array { + $c[$v['parent_id']] = $v['data']; + return $c; + }, + [] + ); + } + ) + ); + } + + private function retrieveAttemptFromQuery( + Uuid $attempt_id, + Query $query, + array $additional_seeds + ): Attempt { + return $query->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::AttemptData + ), + $this->refinery->custom()->transformation( + fn(array $vs): Attempt => new Attempt( + $attempt_id, + $vs[0]['shuffler_seed'], + $additional_seeds + ) + ) + ); + } + + private function retriveResponseFromQuery( + Question $question, + Uuid $attempt_id, + Query $query + ): Attempt { + $query->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::Responses + ), + $this->refinery->custom()->transformation( + function ( + array $vs + ) use ( + $question, + $attempt_id, + $query + ): Response { + if ($vs === []) { + return $this->getNewResponseFor( + $question->getId(), + $attempt_id + ); + } + + $last_record = array_last($vs); + $response_id = $this->uuid_factory + ->fromString($last_record['id']); + + $answer_form_responses = $question + ->retrieveAnswerFormResponsesFromQuery( + $response_id, + $query + ); + + return new Response( + $response_id, + $question->getId(), + $attempt_id, + new \DateTimeImmutable("@{$last_record['create_timestamp']}"), + $last_record['reached_points'], + $answer_form_responses + ); + } + ) + ); + + } +} diff --git a/components/ILIAS/Questions/src/Attempt/Response.php b/components/ILIAS/Questions/src/Attempt/Response.php new file mode 100644 index 000000000000..25de6b2669a9 --- /dev/null +++ b/components/ILIAS/Questions/src/Attempt/Response.php @@ -0,0 +1,162 @@ + $answer_form_responses + */ + private readonly array $answer_form_responses; + + /** + * @param array $answer_form_responses + */ + public function __construct( + private readonly Uuid $id, + private readonly Uuid $question_id, + private readonly Uuid $attempt_id, + private readonly \DateTimeImmutable $create_date, + private ?float $reached_points, + array $answer_form_responses = [] + ) { + $this->answer_form_responses = array_reduce( + $answer_form_responses, + function (array $c, AnswerFormResponse $v): array { + $c[$v->getAnswerFormId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function getReachedPoints(): float + { + return $this->reached_points; + } + + public function getCreateDate(): \DateTimeImmutable + { + return $this->create_date; + } + + public function withAnswerFormResponse( + AnswerFormResponse $response + ): self { + $clone = clone $this; + $clone->answer_form_responses[$response->getAnswerFormId()->toString()] = $response; + return $clone; + } + + public function toStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder, + Manipulate $manipulate + ): Manipulate { + return array_reduce( + $this->answer_form_responses, + fn(Manipulate $c, AnswerFormResponse $v): Manipulate + => $v->toStorage( + $persistence_factory, + $c + ), + $manipulate->withAdditionalStatement( + $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + TableTypes::Responses + ), + $this->buildValuesArrayForStorage($persistence_factory) + ) + ) + ); + } + + public function toDelete( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder, + Manipulate $manipulate + ): Manipulate { + return array_reduce( + $this->answer_form_responses, + fn(Manipulate $c, AnswerFormResponse $v): Manipulate + => $v->toDelete( + $persistence_factory, + $c + ), + $manipulate->withAdditionalStatement( + $persistence_factory->delete( + $persistence_factory->table( + $table_names_builder, + TableTypes::Responses + ), + [ + $persistence_factory->where( + $table_definitions->getIdColumn( + $table_names_builder, + TableTypes::Responses + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->id->toString() + ) + ) + ] + ) + ) + ); + } + + private function buildValuesArrayForStorage( + PersistenceFactory $persistence_factory + ): array { + return [ + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->attempt_id->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->question_id->toString() + ), + $persistence_factory->value( + FieldDefinition::T_FLOAT, + $this->reached_points + ), + $persistence_factory->value( + FieldDefinition::T_INTEGER, + $this->create_date->getTimestamp() + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/Attempt/TableDefinitions.php b/components/ILIAS/Questions/src/Attempt/TableDefinitions.php new file mode 100644 index 000000000000..3e8084119d02 --- /dev/null +++ b/components/ILIAS/Questions/src/Attempt/TableDefinitions.php @@ -0,0 +1,198 @@ +persistence_factory->table( + $table_names_builder, + $table_type + ); + + return array_map( + fn(string $v): Column => $this->persistence_factory->column( + $table, + $v + ), + match($table_type) { + TableTypes::AttemptData => self::ATTEMPT_DATA_TABLE_COLUMNS, + TableTypes::AdditionalAttemptData => self::ADDITIONAL_ATTEMPT_DATA_TABLE_COLUMNS, + TableTypes::Responses => self::RESPONSES_TABLE_COLUMNS + } + ); + } + + public function getIdColumn( + TableNameBuilder $table_names_builder, + TableTypesInterface $table_type + ): ?Column { + return match($table_type) { + TableTypes::AttemptData, + TableTypes::Responses => $this->persistence_factory->column( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + self::ID_COLUMN + ), + default => null + }; + } + + public function getForeignKeyColumn( + TableNameBuilder $table_names_builder, + TableTypesInterface $table_type + ): ?Column { + $table = $this->persistence_factory->table( + $table_names_builder, + $table_type + ); + + return match($table_type) { + TableTypes::AdditionalAttemptData => $this->persistence_factory->column( + $table, + self::ADDITIONAL_ATTEMPT_DATA_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::Responses => $this->persistence_factory->column( + $table, + self::RESPONSES_TABLE_PRIMARY_FOREIGN_KEY_COLUMN + ), + default => null + }; + } + + public function completeLoadAttemptQuery( + Query $query, + ?Column $base_table_id_column + ): Query { + $table_names_builder = $query->getTableNameBuilder(null); + + return $query->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_names_builder, + TableTypes::AttemptData + ) + ) + )->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_names_builder, + TableTypes::AdditionalAttemptData + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $base_table_id_column, + $this->getForeignKeyColumn( + $table_names_builder, + TableTypes::AdditionalAttemptData + ), + JoinType::Left + ) + )->withAdditionalSelect( + $this->persistence_factory->select( + $this->getColumns( + $table_names_builder, + TableTypes::Responses + ) + ) + )->withAdditionalJoin( + $this->persistence_factory->join( + $base_table_id_column, + $this->getForeignKeyColumn( + $table_names_builder, + TableTypes::Responses + ), + JoinType::Left + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $base_table_id_column + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_names_builder, + TableTypes::Responses + ), + self::RESPONSES_TABLE_SECONDARY_FOREIGN_KEY_COLUMN + ) + ) + )->withAdditionalOrder( + $this->persistence_factory->order( + $this->persistence_factory->table( + $table_names_builder, + TableTypes::Responses + ), + self::RESPONSES_TABLE_ADDITIONAL_ORDERING_COLUMN + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/Response/Repository.php b/components/ILIAS/Questions/src/Attempt/TableTypes.php similarity index 65% rename from components/ILIAS/Questions/src/Response/Repository.php rename to components/ILIAS/Questions/src/Attempt/TableTypes.php index 01e2ff350f1b..968ad7c793ef 100644 --- a/components/ILIAS/Questions/src/Response/Repository.php +++ b/components/ILIAS/Questions/src/Attempt/TableTypes.php @@ -18,20 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Questions\Response; +namespace ILIAS\Questions\Attempt; -interface Repository -{ - public function getForUser( - int $question_id, - int $user_id - ): Result; - - public function getAllForQuestion( - int $question_id - ): Result; +use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; - public function storeResult( - Result $result - ): void; +enum TableTypes: string implements TableTypesInterface +{ + case AttemptData = 'attempt'; + case AdditionalAttemptData = 'attempt_additional_data'; + case Responses = 'responses'; } diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php deleted file mode 100644 index d5143cc9d536..000000000000 --- a/components/ILIAS/Questions/src/Collector.php +++ /dev/null @@ -1,63 +0,0 @@ -checkCapabilities($capability_class_names); - $clone = clone $this; - $clone->required_capabilities = $capability_class_names; - return $clone; - } - - public function getQuestionsForId( - Uuid $id - ): ?PublicQuestionInterface { - return $this->repository->getForQuestionId($id); - } - - /** - * Use with Care: This is going to be freakishly expensive, if you ask - * for a lot of questions as the query will contain a huge amount of joins! - * - * @param list<\ILIAS\Data\Uuid> $ids - * @return \Generator - */ - public function getQuestionsForIds( - array $ids - ): \Generator { - yield from $this->repository->getForQuestionIds($ids); - } -} diff --git a/components/ILIAS/Questions/src/DefaultPublicInterface.php b/components/ILIAS/Questions/src/DefaultPublicInterface.php new file mode 100644 index 000000000000..5cf56482f9eb --- /dev/null +++ b/components/ILIAS/Questions/src/DefaultPublicInterface.php @@ -0,0 +1,108 @@ +ui_factory, + $this->capabilities_factory, + $this->questions_repository, + $this->attempt_repository, + $owner_obj_id + ); + } + + #[\Override] + public function getEditView( + int $owner_obj_id + ): Edit { + return new Edit( + $this->lng, + $this->current_user, + $this->refinery, + $this->ui_factory, + $this->ui_renderer, + $this->global_screen, + $this->global_tpl, + $this->content_style, + $this->ctrl, + $this->http, + $this->tabs_gui, + $this->ui_services, + $this->uuid_factory, + $this->configuration_repository, + $this->answer_form_factory, + $this->questions_repository, + $this->layout_factory, + $this->capabilities_edit_view, + $owner_obj_id + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Factory.php b/components/ILIAS/Questions/src/Persistence/Factory.php index 6a9b3f3e0b79..f2cf8d805a4b 100644 --- a/components/ILIAS/Questions/src/Persistence/Factory.php +++ b/components/ILIAS/Questions/src/Persistence/Factory.php @@ -68,11 +68,11 @@ public function delete( } public function table( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypes $table, string $sub_table_identifier = '' ): Table { - return new Table($table_name_builder, $table, $sub_table_identifier); + return new Table($table_names_builder, $table, $sub_table_identifier); } public function column( diff --git a/components/ILIAS/Questions/src/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php index 6ef4e4d941fd..de17734f1e58 100644 --- a/components/ILIAS/Questions/src/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -34,19 +34,4 @@ public function toSql(): string return "{$this->type->value} JOIN {$this->right->getTableName()} " . "ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; } - - public function getLeft(): Column - { - return $this->left; - } - - public function getRight(): Column - { - return $this->right; - } - - public function getType(): JoinType - { - return $this->type; - } } diff --git a/components/ILIAS/Questions/src/Persistence/Table.php b/components/ILIAS/Questions/src/Persistence/Table.php index f1c26a97c039..9b56135bcad9 100644 --- a/components/ILIAS/Questions/src/Persistence/Table.php +++ b/components/ILIAS/Questions/src/Persistence/Table.php @@ -23,7 +23,7 @@ class Table { public function __construct( - private readonly TableNameBuilder $table_name_builder, + private readonly TableNameBuilder $table_names_builder, private readonly TableTypes $table, private readonly string $sub_table_identifier ) { @@ -31,7 +31,7 @@ public function __construct( public function getName(): string { - return $this->table_name_builder->getTableNameFor( + return $this->table_names_builder->getTableNameFor( $this->table, $this->sub_table_identifier ); diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index 36845b35222a..c5d226624c94 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -81,8 +81,8 @@ public function __construct( private readonly Factory $presentation_factory, private readonly Editability $editability, private readonly array $required_capabilities, - URI $base_uri, - private readonly int $obj_id + private readonly int $owner_obj_id, + URI $base_uri ) { $this->acquireURLBuilderAndParameters($base_uri); } @@ -342,9 +342,9 @@ public function withIsInCreationContext( return $clone; } - public function getObjId(): int + public function getOwnerObjId(): int { - return $this->obj_id; + return $this->owner_obj_id; } public function getAction(): string diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php index 875ff8edd89d..34e257e25050 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php @@ -50,7 +50,7 @@ public static function getTableColums( ]; } - public static function getFilderInputs( + public static function getFilterInputs( Language $lng, FieldFactory $field_factory, array $answer_form_types_array_for_select @@ -68,11 +68,11 @@ public static function getFilderInputs( public function getDatabaseColumn( PersistenceFactory $persistence_factory, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): ?Column { return $persistence_factory->column( $persistence_factory->table( - $table_name_builder, + $table_names_builder, match($this) { self::Title => TableTypes::Questions, self::AnswerFormTypes => AnswerFormGenericTableTypes::AnswerForms diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 71f8737614b6..6fefffcf203c 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -27,11 +27,10 @@ use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\Range; use ILIAS\Data\Order; -use ILIAS\UI\Component\Table\DataRetrieval; -use ILIAS\UI\Component\Table\DataRowBuilder; -use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Button\Primary as PrimaryButton; use ILIAS\UI\Component\Input\Container\Filter\Standard as Filter; +use ILIAS\UI\Component\Table\DataRetrieval; +use ILIAS\UI\Component\Table\DataRowBuilder; class QuestionsTable implements Viewable, DataRetrieval { @@ -41,7 +40,8 @@ public function __construct( private readonly \ilUIService $ui_service, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, - private readonly DefaultEnvironment $environment + private readonly DefaultEnvironment $environment, + private readonly array $required_capabilities ) { $environment->getLanguage()->loadLanguageModule('qpl'); } @@ -95,10 +95,15 @@ public function getRows( $order, $filter_data ) as $question) { - yield $question->toTableRow( + $table_row = $question->toTableRow( $row_builder, - $environment_with_action + $environment_with_action, + $this->required_capabilities ); + + if ($table_row !== null) { + yield $table_row; + } } } @@ -138,7 +143,7 @@ private function buildTable(): array private function buildFilter( string $action ): Filter { - $filter_inputs = OverviewTableColumns::getFilderInputs( + $filter_inputs = OverviewTableColumns::getFilterInputs( $this->environment->getLanguage(), $this->environment->getUIFactory()->input()->field(), $this->answer_form_factory->getAnswerFormTypesArrayForSelect( diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 1ca4fb6633ca..03549db0af91 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -42,10 +42,10 @@ use ILIAS\Data\URI; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; -use ILIAS\UICore\GlobalTemplate; +use ILIAS\HTTP\Services as HTTP; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\HTTP\Services as HTTP; +use ILIAS\UICore\GlobalTemplate; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Component; @@ -67,7 +67,6 @@ class Edit public function __construct( private readonly Language $lng, - private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, private readonly Refinery $refinery, private readonly UIFactory $ui_factory, @@ -80,10 +79,12 @@ public function __construct( private readonly \ilTabsGUI $tabs_gui, private readonly \ilUIService $ui_services, private readonly UuidFactory $uuid_factory, - private CapabilitiesEditView $capabilities_edit_view, + private readonly ConfigurationRepository $configuration_repository, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, - private readonly LayoutFactory $definitions_factory + private readonly LayoutFactory $layout_factory, + private CapabilitiesEditView $capabilities_edit_view, + private readonly int $owner_object_id ) { } @@ -114,7 +115,6 @@ public function withOrderingEnabled( public function getUI( URI $base_uri, - int $obj_id, int $ref_id ): array|Component { $this->content_style->gui()->addCss( @@ -122,10 +122,7 @@ public function getUI( $ref_id ); - $environment = $this->buildEnvironment( - $base_uri, - $obj_id - ); + $environment = $this->buildEnvironment($base_uri); $view = match($environment->getAction()) { self::ACTION_CREATE_QUESTION => $this->createQuestion( @@ -144,15 +141,10 @@ public function getUI( } public function forwardPageCmds( - \ilGlobalTemplateInterface $tpl, URI $base_uri, - int $obj_id, int $ref_id ): void { - $environment = $this->buildEnvironment( - $base_uri, - $obj_id - ); + $environment = $this->buildEnvironment($base_uri); if ($this->ctrl->getCmd() === 'insert' && $environment->getAction() === self::ACTION_DELETE_QUESTIONS) { @@ -164,17 +156,18 @@ public function forwardPageCmds( $environment->preserveParametersForPageEditorCmds(); $this->content_style->gui()->addCss( - $tpl, + $this->global_tpl, $ref_id ); - $tpl->setContent( + $this->global_tpl->setContent( $this->ctrl->forwardCommand( new \QstsQuestionPageGUI( $this->questions_repository->getForQuestionId( $environment->getQuestionId() ), - $obj_id, + $this->owner_object_id + )->withEditView( $this )->withReturnURI( $environment @@ -189,7 +182,6 @@ public function forwardPageCmds( public function getCreateAnswerForm( URI $base_uri, - int $obj_id, Question $question, \ilPCAnswerForm $content_object ): array|Component { @@ -223,7 +215,7 @@ public function getCreateAnswerForm( $question, $content_object, $type_definition->getEditView() - ); + )->getUI(); } return match($environment->getAction()) { @@ -231,14 +223,13 @@ public function getCreateAnswerForm( $environment, $question, $content_object - ), - default => $this->buildCreateAnswerForm($environment) + )->getUI(), + default => $this->buildCreateAnswerForm($environment)->getUI() }; } public function getEditAnswerForm( URI $base_uri, - int $obj_id, Question $question, AnswerFormProperties $answer_form_properties, Definition $type_definition @@ -278,7 +269,7 @@ public function getEditAnswerForm( $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM), $question, $edit_view - ); + )->getUI(); } $from_edit_view = $edit_view->edit($environment); @@ -336,11 +327,13 @@ private function createQuestion( ); $create = $this->questions_repository->getNew( - $environment->getObjId() + $environment->getOwnerObjId() )->getEditView( - $this->configuration_repository, $this->current_user, - $this->ctrl + $this->ctrl, + $this->ui_renderer, + $this->configuration_repository, + $this->capabilities_edit_view->getRequiredCapabilities() )->create( $environment->withActionParameter(self::ACTION_CREATE_QUESTION) ); @@ -375,14 +368,14 @@ private function editQuestion( ->withQuestionIdParameter($question_id); $edit = $question->getEditView( - $this->configuration_repository, $this->current_user, $this->ctrl, - $this->ui_renderer + $this->ui_renderer, + $this->configuration_repository, + $this->capabilities_edit_view->getRequiredCapabilities() )->edit( $environment_with_question_parameter - ->withActionParameter(self::ACTION_EDIT_QUESTION), - $question->getParticipantView() + ->withActionParameter(self::ACTION_EDIT_QUESTION) ); if ($edit instanceof EditForm) { @@ -440,7 +433,8 @@ private function showTable( $this->ui_services, $this->answer_form_factory, $this->questions_repository, - $environment + $environment, + $this->capabilities_edit_view->getRequiredCapabilities() )->withCreateQuestionButton( $this->ui_factory->button()->primary( $this->lng->txt('create'), @@ -559,13 +553,17 @@ private function processOtherAnswerFormAction( Environment $environment, Question $question, AnswerFormEditView $edit_view - ) { + ): Viewable { $from_edit_view = $edit_view->other($environment); - if (!($from_edit_view instanceof AnswerFormProperties)) { + if ($from_edit_view instanceof Viewable) { return $from_edit_view; } + if ($from_edit_view instanceof Async) { + $from_edit_view->render($this->ui_renderer); + } + $this->updateAnswerFormAndRedirect( $environment, $question, @@ -599,7 +597,7 @@ private function createAnswerFormAndRedirect( ); $content_object->create( - $properties->getAnswerFormId() + $environment->getAnswerFormProperties()->getAnswerFormId() ); $content_object->getPage()->update(); @@ -683,13 +681,13 @@ private function buildEditStartView( Question $question ): EditForm { return $question->getEditView( - $this->configuration_repository, $this->current_user, $this->ctrl, - $this->ui_renderer + $this->ui_renderer, + $this->configuration_repository, + $this->capabilities_edit_view->getRequiredCapabilities() )->edit( - $environment, - $question->getParticipantView() + $environment ); } @@ -784,7 +782,6 @@ private function addSaveAndNewToAnswerFormCreateIfNeeded( private function buildEnvironment( URI $base_uri, - int $obj_id ): DefaultEnvironment { return new DefaultEnvironment( $this->ctrl, @@ -793,11 +790,11 @@ private function buildEnvironment( $this->lng, $this->tabs_gui, $this->uuid_factory, - $this->definitions_factory, + $this->layout_factory, $this->editability, $this->capabilities_edit_view->getRequiredCapabilities(), - $base_uri, - $obj_id + $this->owner_object_id, + $base_uri ); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Participant.php b/components/ILIAS/Questions/src/Presentation/Views/Participant.php new file mode 100644 index 000000000000..e94b179685eb --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Views/Participant.php @@ -0,0 +1,137 @@ +presentation_identifier; + } + + public function withRequiredCapabilities( + array $capability_class_names + ): self { + $clone = clone $this; + $clone->required_capabilities = array_reduce( + $capability_class_names, + function (array $c, string $v): array { + $c[$v] = $this->capabilities_factory->get($v); + + if ($c[$v] === null) { + throw new \InvalidArgumentException( + "The capability {$v} does not exist." + ); + } + + return $c; + }, + [] + ); + return $clone; + } + + public function withShuffleQuestionOrder( + bool $shuffle_question_order + ): self { + $clone = clone $this; + $clone->shuffle_question_order = $shuffle_question_order; + return $clone; + } + + public function getQuestionView( + Uuid $question_id, + ?Uuid $attempt_id = null, + bool $interactive = true, + bool $show_marks = false, + bool $show_correct_solution = false + ): QuestionParticipantView { + $question = $this->question_repository->getForQuestionId( + $question_id + ); + + return $question->getParticipantView( + $this->ui_factory, + $this->required_capabilities, + $this->attempt_repository->getAttemptFor( + $attempt_id, + [$question] + ), + $interactive, + $show_marks && in_array(Marking::class, $this->required_capabilities), + $show_correct_solution && in_array(Marking::class, $this->required_capabilities) + ); + } + + public function persistResponse( + Uuid $question_id, + Uuid $attempt_id + ): Uuid { + $question = $this->question_repository->getForQuestionId( + $question_id + ); + + $attempt_data = $this->attempt_repository->getAttemptFor( + $attempt_id, + [$question] + ); + + if ($attempt_data === null) { + throw new \UnexpectedValueException( + 'The provided attempt identifier is invalid. Response cannot be persisted.' + ); + } + + $current_reponse = $attempt_data->getResponseFor($question_id); + if ($current_reponse === null) { + $current_response = $this->attempt_repository->getNewResponseFor( + $question_id, + $attempt_id + ); + } + + foreach ($this->question->getAnswerFormProperties() as $property) { + $property->getDefinition()->getParticipantView()->retrieveResponse(); + } + } +} diff --git a/components/ILIAS/Questions/src/PublicInterface.php b/components/ILIAS/Questions/src/PublicInterface.php index 85370e839b29..0da4f5fdc98a 100644 --- a/components/ILIAS/Questions/src/PublicInterface.php +++ b/components/ILIAS/Questions/src/PublicInterface.php @@ -21,23 +21,15 @@ namespace ILIAS\Questions; use ILIAS\Questions\Presentation\Views\Edit; +use ILIAS\Questions\Presentation\Views\Participant; -class PublicInterface +interface PublicInterface { - public function __construct( - private readonly Collector $collector, - private readonly Edit $edit_presentation - ) { + public function getParticipantView( + int $owner_obj_id + ): Participant; - } - - public function getCollector(): Collector - { - return $this->collector; - } - - public function getEditPresentation(): Edit - { - return $this->edit_presentation; - } + public function getEditView( + int $owner_obj_id + ): Edit; } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 08b5e4af692c..17ff78228c9a 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -239,7 +239,8 @@ public function delete( => $v->toDelete( $c, $this->persistence_factory, - $this->question_table_definitions + $this->question_table_definitions, + $this->answer_form_generic_table_definitions ), $this->buildManipulate( ManipulationType::Delete diff --git a/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php index 7e19071a152f..41e4c1e1a7cc 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php +++ b/components/ILIAS/Questions/src/Question/Persistence/TableDefinitions.php @@ -98,11 +98,11 @@ public function getColumns( } public function getIdColumn( - TableNameBuilder $table_name_builder, + TableNameBuilder $table_names_builder, TableTypesInterface $table_type ): Column { $table = $this->persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ); @@ -125,30 +125,30 @@ public function getIdColumn( public function completeLoadQuestionQuery( Query $query ): Query { - $table_name_builder = $query->getTableNameBuilder(null); + $table_names_builder = $query->getTableNameBuilder(null); $questions_id_column = $this->getIdColumn( - $table_name_builder, + $table_names_builder, TableTypes::Questions ); return $query->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, TableTypes::Linking ) ) )->withAdditionalSelect( $this->persistence_factory->select( $this->getColumns( - $table_name_builder, + $table_names_builder, TableTypes::Questions ) ) )->withAdditionalJoin( $this->persistence_factory->join( $this->getIdColumn( - $table_name_builder, + $table_names_builder, TableTypes::Linking ), $questions_id_column, @@ -161,7 +161,7 @@ public function completeLoadQuestionQuery( )->withAdditionalOrder( $this->persistence_factory->order( $this->getIdColumn( - $table_name_builder, + $table_names_builder, TableTypes::Linking ) ) diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index e406d59f83c5..82c162626f38 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -21,16 +21,20 @@ namespace ILIAS\Questions\Question; use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; +use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; +use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; @@ -38,6 +42,7 @@ use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; +use ILIAS\Language\Language; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -45,7 +50,7 @@ use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; -class Question implements PublicQuestionInterface +class Question { private ?CreateModes $create_mode = null; private bool $linking_information_updated = false; @@ -205,11 +210,6 @@ public function getCreated(): ?\DateTimeImmutable return $this->created; } - public function getAnswerFormProperties(): array - { - return $this->answer_forms; - } - public function getAnswerFormPropertiesByIdString( string $form_id ): ?AnswerFormProperties { @@ -260,27 +260,71 @@ public function withCreateMode( return $clone; } + public function getListOfContainedAnswerFormTypeLabels( + Language $lng + ): array { + return array_map( + fn(AnswerFormDefinition $v): string => $v->getLabel($lng), + $this->getListOfContainedAnswerFormTypes() + ); + } + public function getEditView( - ConfigurationRepository $configuration_repository, \ilObjUser $current_user, \ilCtrl $ctrl, - UIRenderer $ui_renderer + UIRenderer $ui_renderer, + ConfigurationRepository $configuration_repository, + array $required_capabilities ): Views\Edit { + if (!$this->supportsRequiredCapabilities($required_capabilities)) { + throw new \UnexpectedValueException( + "The Question does not support all required Capabilities." + ); + } + return new Views\Edit( - $configuration_repository, $current_user, $ctrl, $ui_renderer, + $configuration_repository, + $required_capabilities, $this ); } - #[\Override] public function getParticipantView( - UIFactory $ui_factory + UIFactory $ui_factory, + array $required_capabilities, + ?Attempt $attempt_data, + bool $interactive = true, + bool $show_marks = false, + bool $show_correct_solution = false ): Views\Participant { + if (!$this->supportsRequiredCapabilities($required_capabilities)) { + throw new \UnexpectedValueException( + "The Question does not support all required Capabilities." + ); + } + return new Views\Participant( - $this + $ui_factory, + $required_capabilities, + $this, + $attempt_data, + $interactive, + $show_marks, + $show_correct_solution + ); + } + + public function initializeAttemptData( + Attempt $attempt + ): Attempt { + return array_reduce( + $this->answer_forms, + fn(Attempt $c, AnswerFormProperties $v): Attempt + => $v->getDefinition()->initializeAttemptData($c, $v), + $attempt ); } @@ -299,8 +343,13 @@ public function toEditLink( public function toTableRow( DataRowBuilder $row_builder, - DefaultEnvironment $environment - ): DataRow { + DefaultEnvironment $environment, + array $required_capabilities + ): ?DataRow { + if (!$this->supportsRequiredCapabilities($required_capabilities)) { + return null; + } + return $row_builder->buildDataRow( $this->id->toString(), [ @@ -315,21 +364,8 @@ public function toTableRow( ), OverviewTableColumns::AnswerFormTypes->value => implode( '
', - array_reduce( - $this->answer_forms, - function ( - array $c, - AnswerFormProperties $v - ) use ($environment): array { - $type_label = $v->getDefinition()->getLabel( - $environment->getLanguage() - ); - if (!in_array($type_label, $c)) { - $c[] = $type_label; - } - return $c; - }, - [] + $this->getListOfContainedAnswerFormTypeLabels( + $environment->getLanguage() ) ) ] @@ -359,35 +395,53 @@ public function toStorage( public function toDelete( Manipulate $manipulate, PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions + QuestionTableDefinitions $question_tables_definitions, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions ): Manipulate { - $table_name_builder = $manipulate->getTableNameBuilder(null); + $table_names_builder = $manipulate->getTableNameBuilder(null); return $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate->withAdditionalStatement( $this->buildDeleteQuestionStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) )->withAdditionalStatement( $this->buildDeleteLinkingStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) )->withAdditionalStatement( $this->buildDeleteMigrationStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) ), $persistence_factory, + $answer_form_generic_table_definitions, $this->answer_forms ); } + public function completeResponseQuery( + Query $query, + Column $base_table_id_column + ): Query { + return array_reduce( + $this->getListOfContainedAnswerFormTypes(), + fn(Query $c, AnswerFormDefinition $v): Query => $v + ->getTableDefinitions() + ->completeResponseQuery( + $c, + $base_table_id_column + ), + $query + ); + } + private function addInsertStatementsToManipulation( Manipulate $manipulate, PersistenceFactory $persistence_factory, @@ -438,7 +492,7 @@ private function addUpdateStatementsToManipulation( QuestionTableDefinitions $question_tables_definitions, AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions ): Manipulate { - $table_name_builder = $manipulate->getTableNameBuilder(null); + $table_names_builder = $manipulate->getTableNameBuilder(null); if ($this->linking_information_updated) { $manipulate = $manipulate @@ -446,7 +500,7 @@ private function addUpdateStatementsToManipulation( $this->buildUpdateLinkingStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) ); } @@ -456,7 +510,7 @@ private function addUpdateStatementsToManipulation( $this->buildUpdateQuestionStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) ); } @@ -466,7 +520,7 @@ private function addUpdateStatementsToManipulation( $this->buildUpdatePageIdStatement( $persistence_factory, $question_tables_definitions, - $table_name_builder + $table_names_builder ) ); } @@ -475,6 +529,7 @@ private function addUpdateStatementsToManipulation( $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( $manipulate, $persistence_factory, + $answer_form_generic_table_definitions, $this->deleted_answer_forms ); } @@ -521,6 +576,7 @@ function ( private function addDeleteAnswerFormsStatementsToManipulate( Manipulate $manipulate, PersistenceFactory $persistence_factory, + AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_forms_to_delete ): Manipulate { return array_reduce( @@ -528,6 +584,7 @@ private function addDeleteAnswerFormsStatementsToManipulate( fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( $v->getTypeGenericProperties()->toDelete( $persistence_factory, + $answer_form_generic_table_definitions, $c, ) ), @@ -540,11 +597,11 @@ private function addDeleteAnswerFormsStatementsToManipulate( private function buildInsertLinkingStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Insert { return $persistence_factory->insert( $question_tables_definitions->getColumns( - $table_name_builder, + $table_names_builder, QuestionTableTypes::Linking ), [ @@ -567,11 +624,11 @@ private function buildInsertLinkingStatement( private function buildInsertQuestionStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Insert { return $persistence_factory->insert( $question_tables_definitions->getColumns( - $table_name_builder, + $table_names_builder, QuestionTableTypes::Questions ), [ @@ -618,12 +675,12 @@ private function buildInsertQuestionStatement( private function buildUpdateLinkingStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Update { $table_type = QuestionTableTypes::Linking; return $persistence_factory->update( $question_tables_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type, [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] ), @@ -640,7 +697,7 @@ private function buildUpdateLinkingStatement( [ $persistence_factory->where( $question_tables_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -655,12 +712,12 @@ private function buildUpdateLinkingStatement( private function buildUpdateQuestionStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Update { $table_type = QuestionTableTypes::Questions; return $persistence_factory->update( $question_tables_definitions->getColumns( - $table_name_builder, + $table_names_builder, $table_type, [ 'id', @@ -697,7 +754,7 @@ private function buildUpdateQuestionStatement( [ $persistence_factory->where( $question_tables_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -712,18 +769,18 @@ private function buildUpdateQuestionStatement( private function buildDeleteQuestionStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = QuestionTableTypes::Questions; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $question_tables_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -738,18 +795,18 @@ private function buildDeleteQuestionStatement( private function buildDeleteLinkingStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = QuestionTableTypes::Linking; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -768,18 +825,18 @@ private function buildDeleteLinkingStatement( private function buildDeleteMigrationStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Delete { $table_type = QuestionTableTypes::MigrationsTable; return $persistence_factory->delete( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -798,21 +855,21 @@ private function buildDeleteMigrationStatement( private function buildUpdatePageIdStatement( PersistenceFactory $persistence_factory, QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_name_builder + TableNameBuilder $table_names_builder ): Update { $table_type = QuestionTableTypes::Questions; return $persistence_factory->update( [ $persistence_factory->column( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), 'page_id' ), $persistence_factory->column( $persistence_factory->table( - $table_name_builder, + $table_names_builder, $table_type ), 'last_update' @@ -831,7 +888,7 @@ private function buildUpdatePageIdStatement( [ $persistence_factory->where( $table_definitions->getIdColumn( - $table_name_builder, + $table_names_builder, $table_type ), $persistence_factory->value( @@ -842,4 +899,36 @@ private function buildUpdatePageIdStatement( ] ); } + + private function getListOfContainedAnswerFormTypes(): array + { + return array_reduce( + $this->answer_forms, + function ( + array $c, + AnswerFormProperties $v + ): array { + $definition = $v->getDefinition(); + if (!array_key_exists($definition::class, $c)) { + $c[$definition::class] = $definition; + } + return $c; + }, + [] + ); + } + + private function supportsRequiredCapabilities( + array $required_capabilities + ): bool { + foreach ($this->answer_forms as $property) { + foreach ($required_capabilities as $capability) { + if (!$capability->isAvailableFor($property)) { + return false; + } + } + } + + return true; + } } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index cb026eca4f99..31e08cba4d6a 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -29,6 +29,7 @@ use ILIAS\Language\Language; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; +use ILIAS\UI\Renderer as UIRenderer; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; @@ -37,9 +38,11 @@ class Edit private const string CMD_SAVE_QUESTION = 'sq'; public function __construct( - private readonly ConfigurationRepository $configuration_repository, private readonly \ilObjUser $current_user, private readonly \ilCtrl $ctrl, + private readonly UIRenderer $ui_renderer, + private readonly ConfigurationRepository $configuration_repository, + private readonly array $required_capabilities, private readonly Question $question ) { @@ -58,7 +61,6 @@ public function create( public function edit( DefaultEnvironment $environment, - Participant $participant_view ): EditForm|Question { return match ($environment->getSubAction()) { self::CMD_SAVE_QUESTION => $this->processBasicPropertiesEditingForm( @@ -66,7 +68,7 @@ public function edit( ), default => $this->buildBasicPropertiesEditingForm($environment) ->withContentAfterForm( - $this->buildPreviewPanel($environment, $participant_view) + $this->buildPreviewPanel($environment) ) }; } @@ -222,14 +224,19 @@ function (array $vs): Question { } private function buildPreviewPanel( - DefaultEnvironment $environment, - Participant $participant_view + DefaultEnvironment $environment ): StandardPanel { $environment->preserveParametersForPageEditorCmds(); return $environment->getUIFactory()->panel()->standard( $environment->getLanguage()->txt('preview'), $environment->getUIFactory()->legacy()->content( - $participant_view->get($environment->getObjId()) + $this->ui_renderer->render( + $this->question->getParticipantView( + $environment->getUIFactory(), + $this->required_capabilities, + null + )->getUI() + ) ) )->withActions( $environment->getUIFactory()->dropdown()->standard([ diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index a6eeec176824..7400e9116f4b 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -20,94 +20,51 @@ namespace ILIAS\Questions\Question\Views; +use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Question\Question; +use ILIAS\Data\UUID\Uuid; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Legacy\Content as LegacyContent; -class Participant +class Participant implements Viewable { - private bool $async = false; - private bool $interactive = true; - private bool $show_marks = false; - private bool $show_correct_solution = false; - public function __construct( - private readonly Question $question + private readonly UIFactory $ui_factory, + private readonly array $required_capabilities, + private readonly Question $question, + private readonly ?Attempt $attempt_data, + private readonly bool $interactive, + private readonly bool $show_marks, + private readonly bool $show_correct_solution ) { } - public function withIsAsync( - bool $async - ): self { - foreach ($this->question->getAnswerFormProperties() as $form) { - if (!$form->getType()->isAsyncPresentationAvailable()) { - throw \Exception('This QuestionType has no async presentation.'); - } - } - $clone = clone $this; - $clone->async = $async; - return $clone; - } - - public function withIsInteractive( - bool $interactive - ): self { - $clone = clone $this; - $clone->interactive = $interactive; - return $clone; + public function getAttemptId(): ?Uuid + { + return $this->attempt_data?->getIdentifier(); } - public function withShowMarks( - bool $show_marks - ): self { - foreach ($this->question->getAnswerFormProperties() as $form) { - if (!$form->getType()->isMarkable()) { - throw \Exception('This QuestionType cannot be marked.'); - } - } - - $clone = clone $this; - $clone->show_marks = $show_marks; - return $clone; - } - - public function withShowCorrectSolution( - bool $show_correct_solution - ): self { - foreach ($this->question->getAnswerFormProperties() as $form) { - if (!$form->getType()->isMarkable()) { - throw \Exception('This QuestionType cannot be marked.'); - } - } - - $clone = clone $this; - $clone->show_correct_solution = $show_correct_solution; - return $clone; - } - - public function get( - int $obj_id - ): string { + #[\Override] + public function getUI(): LegacyContent + { $tpl = new \ilTemplate( - 'tpl.qpl_question_preview.html', + 'tpl.qsts_question_presentation.html', true, true, - 'components/ILIAS/TestQuestionPool' - ); - - $tpl->setVariable( - 'PREVIEW_FORMACTION', - '' + 'components/ILIAS/Questions' ); $question_page = new \QstsQuestionPageGUI( $this->question, - $obj_id - ); + $this->question->getParentObjId() + )->withAttemptData($this->attempt_data); $question_page->setPresentationTitle($this->question->getTitle()); $tpl->setVariable( 'QUESTION_OUTPUT', $question_page->presentation() ); - return $tpl->get(); + return $this->ui_factory->legacy()->content($tpl->get()); } } diff --git a/components/ILIAS/Questions/src/Response/Response.php b/components/ILIAS/Questions/src/Response/Response.php deleted file mode 100644 index b2cffed5cb8c..000000000000 --- a/components/ILIAS/Questions/src/Response/Response.php +++ /dev/null @@ -1,38 +0,0 @@ -question_table_name_builder + $this->question_table_names_builder ) ), new \ilDatabaseUpdateStepsExecutedObjective( @@ -90,6 +90,9 @@ public function getUpdateObjective( ) ) ), + new \ilDatabaseUpdateStepsExecutedObjective( + new TempTables() + ), new \ilTreeAdminNodeAddedObjective( 'qsts', 'Questions' @@ -107,7 +110,7 @@ public function getStatusObjective( new \ilDatabaseUpdateStepsMetricsCollectedObjective( $storage, new OverarchingQuestionTables( - $this->question_table_name_builder + $this->question_table_names_builder ) ), new \ilDatabaseUpdateStepsMetricsCollectedObjective( @@ -127,7 +130,7 @@ public function getMigrations(): array return [ new QuestionsMigration( $this->persistence_factory, - $this->question_table_name_builder, + $this->question_table_names_builder, new QuestionTableDefinitions( $this->persistence_factory ), diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index bf8de6be0a6e..171308453395 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -29,7 +29,7 @@ class ClozeQuestionTables implements \ilDatabaseUpdateSteps protected \ilDBInterface $db; public function __construct( - private readonly SetupTableNameBuilder $table_name_builder, + private readonly SetupTableNameBuilder $table_names_builder, private readonly TableDefinitions $table_definitions ) { } @@ -43,7 +43,7 @@ public function prepare( public function step_1(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::TypeSpecificAnswerForms ); if (!$this->db->tableExists($table_name)) { @@ -73,7 +73,7 @@ public function step_1(): void public function step_2(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::AnswerInputs ); if (!$this->db->tableExists($table_name)) { @@ -136,7 +136,7 @@ public function step_2(): void public function step_3(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::AnswerOptions ); if (!$this->db->tableExists($table_name)) { @@ -171,7 +171,7 @@ public function step_3(): void ], 'points' => [ 'type' => FieldDefinition::T_FLOAT, - 'notnull' => true + 'notnull' => false ] ]); } @@ -187,7 +187,7 @@ public function step_3(): void public function step_4(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationsTableIdentifier() ); @@ -221,7 +221,7 @@ public function step_4(): void public function step_5(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ); @@ -261,12 +261,12 @@ public function step_5(): void public function step_6(): void { - $table_name = $this->table_name_builder->getTableNameFor( + $table_name = $this->table_names_builder->getTableNameFor( AnswerFormSpecificTableTypes::Responses ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ - 'id' => [ + 'response_id' => [ 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true @@ -289,12 +289,8 @@ public function step_6(): void ]); } - if (!$this->db->primaryExistsByFields($table_name, ['id'])) { - $this->db->addPrimaryKey($table_name, ['id']); - } - - if (!$this->db->indexExistsByFields($table_name, ['answer_input_id'])) { - $this->db->addIndex($table_name, ['answer_input_id'], 'ai'); + if (!$this->db->primaryExistsByFields($table_name, ['response_id'])) { + $this->db->addPrimaryKey($table_name, ['response_id']); } } } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index c8b33900fb1a..f321f885239e 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\TableTypes as SuggestedContentTableTypes; use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableTypes as FeedbackTableTypes; +use ILIAS\Questions\Attempt\TableTypes as AttemptTableTypes; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Database\FieldDefinition; @@ -32,7 +33,7 @@ class OverarchingQuestionTables implements \ilDatabaseUpdateSteps protected \ilDBInterface $db; public function __construct( - private readonly TableNameBuilder $basic_table_name_builder + private readonly TableNameBuilder $basic_table_names_builder ) { } @@ -45,7 +46,7 @@ public function prepare( public function step_1(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( QuestionTableTypes::Questions ); if (!$this->db->tableExists($table_name)) { @@ -105,7 +106,7 @@ public function step_1(): void public function step_2(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( AnswerFormGenericTableTypes::AnswerForms ); if (!$this->db->tableExists($table_name)) { @@ -161,7 +162,7 @@ public function step_2(): void public function step_3(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( QuestionTableTypes::Responses ); if (!$this->db->tableExists($table_name)) { @@ -171,11 +172,21 @@ public function step_3(): void 'length' => 64, 'notnull' => true ], + 'attempt_id' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 64, + 'notnull' => true + ], 'question_id' => [ 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true ], + 'create_timestamp' => [ + 'type' => FieldDefinition::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], 'reached_points' => [ 'type' => FieldDefinition::T_FLOAT, 'notnull' => false @@ -194,7 +205,7 @@ public function step_3(): void public function step_4(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( QuestionTableTypes::Linking ); if (!$this->db->tableExists($table_name)) { @@ -228,7 +239,7 @@ public function step_4(): void public function step_5(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( QuestionTableTypes::MigrationsTable ); if (!$this->db->tableExists($table_name)) { @@ -264,7 +275,7 @@ public function step_5(): void public function step_6(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( FeedbackTableTypes::FeedbackGeneric ); if (!$this->db->tableExists($table_name)) { @@ -306,7 +317,7 @@ public function step_6(): void public function step_7(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( FeedbackTableTypes::FeedbackSpecific ); if (!$this->db->tableExists($table_name)) { @@ -352,23 +363,15 @@ public function step_7(): void ); } - if (!$this->db->indexExistsByFields( - $table_name, - [ - 'answer_form_id', - 'parent_id', - 'condition' - ] - )) { - $this->db->manipulate( - "CREATE UNIQUE INDEX apc_idx ON {$table_name} (`answer_form_id`, `parent_id`, `condition`)" - ); - } + $this->db->manipulate( + "CREATE UNIQUE INDEX IF NOT EXISTS apc_idx ON {$table_name}" . PHP_EOL + . '(`answer_form_id`, `parent_id`, `condition`)' + ); } public function step_8(): void { - $table_name = $this->basic_table_name_builder->getTableNameFor( + $table_name = $this->basic_table_names_builder->getTableNameFor( SuggestedContentTableTypes::SuggestedLearningContent ); if (!$this->db->tableExists($table_name)) { @@ -400,4 +403,63 @@ public function step_8(): void ); } } + + public function step_9(): void + { + $table_name = $this->basic_table_names_builder->getTableNameFor( + AttemptTableTypes::AttemptData + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'shuffler_seed' => [ + 'type' => FieldDefinition::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + } + + public function step_10(): void + { + $table_name = $this->basic_table_names_builder->getTableNameFor( + AttemptTableTypes::AdditionalAttemptData + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'attempt_id' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'parent_id' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'data' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 4000, + 'notnull' => true + ], + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['attempt_id', 'parent_id'])) { + $this->db->addPrimaryKey($table_name, ['attempt_id', 'parent_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['attempt_id'])) { + $this->db->addIndex($table_name, ['attempt_id'], 'aid'); + } + } } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index ecb28a23c26e..56ca766f733a 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -59,7 +59,7 @@ class QuestionsMigration implements Migration */ public function __construct( private readonly PersistenceFactory $persistence_factory, - private readonly TableNameBuilder $question_table_name_builder, + private readonly TableNameBuilder $question_table_names_builder, private readonly QuestionTableDefinitions $question_table_definitions, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, array $answer_form_migrations, @@ -185,7 +185,7 @@ public function step( #[\Override] public function getRemainingAmountOfSteps(): int { - $migration_table_name = $this->question_table_name_builder + $migration_table_name = $this->question_table_names_builder ->getTableNameFor(QuestionTableTypes::MigrationsTable); $query = $this->db->query( @@ -211,7 +211,7 @@ public function getRemainingAmountOfSteps(): int private function fetchValidRecord(): ?\stdClass { - $migration_table_name = $this->question_table_name_builder + $migration_table_name = $this->question_table_names_builder ->getTableNameFor(QuestionTableTypes::MigrationsTable); $query = $this->db->query( @@ -275,9 +275,9 @@ private function cleanupAndMigrateOriginalId( private function loadAlreadyMigratedQuestions(): void { - $migration_table_name = $this->question_table_name_builder + $migration_table_name = $this->question_table_names_builder ->getTableNameFor(QuestionTableTypes::MigrationsTable); - $linking_table_name = $this->question_table_name_builder + $linking_table_name = $this->question_table_names_builder ->getTableNameFor(QuestionTableTypes::Linking); $query = $this->db->query( @@ -331,7 +331,7 @@ private function buildInsertLinkingStatement( ): Insert { return $this->persistence_factory->insert( $this->question_table_definitions->getColumns( - $this->question_table_name_builder, + $this->question_table_names_builder, QuestionTableTypes::Linking ), [ @@ -362,7 +362,7 @@ private function buildInsertQuestionStatement( ): Insert { return $this->persistence_factory->insert( $this->question_table_definitions->getColumns( - $this->question_table_name_builder, + $this->question_table_names_builder, QuestionTableTypes::Questions ), [ @@ -412,7 +412,7 @@ private function buildInsertMigrationStatement( ): Insert { return $this->persistence_factory->insert( $this->persistence_factory->getColumns( - $this->question_table_name_builder, + $this->question_table_names_builder, QuestionTableTypes::MigrationsTable ), [ diff --git a/components/ILIAS/Questions/src/Setup/TempTables.php b/components/ILIAS/Questions/src/Setup/TempTables.php new file mode 100644 index 000000000000..ab2f045c3ad9 --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/TempTables.php @@ -0,0 +1,60 @@ +db = $db; + } + + public function step_1(): void + { + $table_name = QstsTempAttemptRepository::TABLE_NAME; + + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'user_id' => [ + 'type' => FieldDefinition::T_INTEGER, + 'length' => 4, + 'notnull' => true + ], + 'attempt_id' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 64, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['user_id'])) { + $this->db->addPrimaryKey($table_name, ['user_id']); + } + } +} diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html b/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html new file mode 100644 index 000000000000..92f87c343d29 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html @@ -0,0 +1,9 @@ +
+
+ {QUESTION_OUTPUT} + +

{RECEIVED_POINTS_INFORMATION}

+ +
+
From 32eace103368684d7dabb4e25ef968bde6843fa0 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 14 May 2026 11:13:34 +0200 Subject: [PATCH 097/108] Questions: Clarify Parameters in Join --- components/ILIAS/Questions/src/Persistence/Join.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Questions/src/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php index de17734f1e58..63e13042ff96 100644 --- a/components/ILIAS/Questions/src/Persistence/Join.php +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -23,15 +23,15 @@ class Join { public function __construct( - private readonly Column $left, - private readonly Column $right, + private readonly Column $existing, + private readonly Column $new, private readonly JoinType $type ) { } public function toSql(): string { - return "{$this->type->value} JOIN {$this->right->getTableName()} " - . "ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; + return "{$this->type->value} JOIN {$this->new->getTableName()} " + . "ON {$this->existing->getColumnString()} = {$this->new->getColumnString()}"; } } From 5c193f3fd1a533869d456c52a2c69741ede4ecc8 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 14 May 2026 14:51:49 +0200 Subject: [PATCH 098/108] Question: Extract Database-Functions From Question --- .../src/Attempt/AdditionalAttemptData.php | 180 ++++++ .../src/Presentation/Definitions/ViewMode.php | 28 + .../Persistence/DatabaseStatementBuilder.php | 518 +++++++++++++++++ .../src/Question/Persistence/Repository.php | 13 +- .../ILIAS/Questions/src/Question/Question.php | 544 ++---------------- .../default/tpl.cloze_gap_longmenu.html | 4 + .../default/tpl.cloze_gap_numeric.html | 1 + .../default/tpl.cloze_gap_select.html | 6 + .../default/tpl.cloze_gap_static.html | 1 + .../templates/default/tpl.cloze_gap_text.html | 1 + ...pl.qsts_preview_question_presentation.html | 9 + 11 files changed, 802 insertions(+), 503 deletions(-) create mode 100644 components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php create mode 100644 components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php create mode 100755 components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html create mode 100755 components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html create mode 100755 components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html create mode 100755 components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html create mode 100755 components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html create mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html diff --git a/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php b/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php new file mode 100644 index 000000000000..3e47dd0e4a7a --- /dev/null +++ b/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php @@ -0,0 +1,180 @@ + + */ + private array $responses = []; + + public function __construct( + private readonly Uuid $identifier, + private readonly int $shuffle_questions_seed, + private array $additional_data = [] + ) { + } + + public function getId(): Uuid + { + return $this->identifier; + } + + public function getShuffleQuestionsSeed(): GivenSeed + { + return new GivenSeed($this->shuffle_questions_seed); + } + + public function getAdditionalDataFor( + Uuid $parent_id + ): ?string { + if (!isset($this->additional_data[$parent_id->toString()])) { + return null; + } + return $this->additional_data[$parent_id->toString()]; + } + + public function withAdditionalData( + Uuid $parent_id, + string $data + ): self { + if (isset($this->additional_data[$parent_id->toString()])) { + throw new InvalidArgumentException( + 'This is a storage for data that stays constant accross the test run.' + . 'Data cannot be changed one it is set.' + ); + } + + $clone = clone $this; + $clone->additional_data[$parent_id->toString()] = $data; + return $clone; + } + + public function getResponseFor( + Uuid $question_id + ): ?Response { + return $this->responses[$question_id->toString()] ?? null; + } + + public function withAdditionalResponse( + Response $response + ): self { + $clone = clone $this; + $clone->responses[$response->getQuestionId()->toString()] = $response; + return $clone; + } + + public function basicDataToStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder + ): Insert { + return $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + TableTypes::AttemptData + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->identifier->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->shuffle_questions_seed + ) + ] + ); + } + + public function additionalDataToStorage( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder + ): ?Insert { + return array_reduce( + array_keys($this->additional_data), + fn(?Insert $c, string $v): Insert + => $this->buildAdditionalDataInsert( + $persistence_factory, + $table_definitions, + $table_names_builder, + $c, + $v + ) + ); + } + + private function buildAdditionalDataInsert( + PersistenceFactory $persistence_factory, + TableDefinitions $table_definitions, + TableNameBuilder $table_names_builder, + ?Insert $insert, + string $parent_id + ): Insert { + if ($insert === null) { + return $persistence_factory->insert( + $table_definitions->getColumns( + $table_names_builder, + TableTypes::AdditionalAttemptData + ), + $this->buildAdditionalDataValuesArray( + $persistence_factory, + $parent_id + ) + ); + } + + return $insert->withAdditionalValues( + $this->buildAdditionalDataValuesArray( + $persistence_factory, + $parent_id + ) + ); + } + + private function buildAdditionalDataValuesArray( + PersistenceFactory $persistence_factory, + string $parent_id + ): array { + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->identifier->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $parent_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_data[$parent_id] + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php b/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php new file mode 100644 index 000000000000..f7ca340fb0d8 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php @@ -0,0 +1,28 @@ +created === null) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildInsertLinkingStatement( + $manipulate->getTableNameBuilder(null), + $question_id, + $parent_obj_id, + $position + ) + )->withAdditionalStatement( + $this->buildInsertQuestionStatement( + $manipulate->getTableNameBuilder(null), + $question_id, + $page_id, + $title, + $author, + $lifecycle, + $remarks, + $original_id + ) + ); + } + + if ($this->updated_answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + if ($this->answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->answer_forms + ); + } + + return $manipulate; + } + + public function addUpdateStatementsToManipulation( + Manipulate $manipulate, + bool $self_updated, + bool $linking_information_updated, + bool $page_id_updated, + Uuid $question_id, + ?int $page_id, + string $title, + string $author, + Lifecycle $lifecycle, + string $remarks, + ?Uuid $original_id, + int $parent_obj_id, + ?int $position, + array $updated_answer_forms, + array $deleted_answer_forms + ): Manipulate { + $table_names_builder = $manipulate->getTableNameBuilder(null); + + if ($linking_information_updated) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildUpdateLinkingStatement( + $table_names_builder, + $question_id, + $parent_obj_id, + $position + ) + ); + } + + if ($self_updated) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdateQuestionStatement( + $table_names_builder, + $question_id, + $title, + $author, + $lifecycle, + $remarks, + $original_id + ) + ); + } + + if ($page_id_updated) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdatePageIdStatement(), + $question_id, + $page_id + ); + } + + if ($deleted_answer_forms !== []) { + $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate, + $deleted_answer_forms + ); + } + + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $updated_answer_forms + ); + } + + public function addDeleteAnswerFormsStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms_to_delete + ): Manipulate { + return array_reduce( + $answer_forms_to_delete, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( + $this->persistence_factory, + $v->getTypeGenericProperties()->toDelete( + $this->persistence_factory, + $this->answer_form_generic_table_definitions, + $c, + ) + ), + $manipulate + ); + } + + public function buildDeleteQuestionStatement( + TableNamesBuilder $table_names_builder, + Uuid $question_id + ): Delete { + $table_type = TableTypes::Questions; + return $this->persistence_factory->delete( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } + + public function buildDeleteLinkingStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id + ): Delete { + $table_type = TableTypes::Linking; + return $this->persistence_factory->delete( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after + * this MUST go! + */ + public function buildDeleteMigrationStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id + ): Delete { + $table_type = TableTypes::MigrationsTable; + return $this->persistence_factory->delete( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } + + private function addAnswerFormStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms + ): Manipulate { + return array_reduce( + $answer_forms, + function ( + Manipulate $c, + AnswerFormProperties $v + ): Manipulate { + $manipulate_with_generic_properties = $v->getTypeGenericProperties() + ->toStorage( + $this->persistence_factory, + $this->answer_form_generic_table_definitions, + $c + ); + + return $v->toStorage( + $this->persistence_factory, + $manipulate_with_generic_properties + ); + }, + $manipulate + ); + } + + private function buildInsertLinkingStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id, + int $parent_obj_id, + ?int $position + ): Insert { + return $this->persistence_factory->insert( + $this->question_tables_definitions->getColumns( + $table_names_builder, + TableTypes::Linking + ), + [ + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + $parent_obj_id + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + $position + ) + ] + ); + } + + private function buildInsertQuestionStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id, + ?int $page_id, + string $title, + string $author, + Lifecycle $lifecycle, + string $remarks, + ?Uuid $original_id + ): Insert { + return $this->persistence_factory->insert( + $this->question_tables_definitions->getColumns( + $table_names_builder, + TableTypes::Questions + ), + [ + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + $page_id + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $title + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $author + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $lifecycle->value + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $remarks + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $original_id?->toString() + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + time() + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + time() + ) + ] + ); + } + + private function buildUpdateLinkingStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id, + int $parent_obj_id, + ?int $position + ): Update { + $table_type = TableTypes::Linking; + return $this->persistence_factory->update( + $this->question_tables_definitions->getColumns( + $table_names_builder, + $table_type, + [TableTypes::LINKING_TABLE_ID_COLUMN] + ), + [ + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + $parent_obj_id + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + $position + ) + ], + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } + + private function buildUpdateQuestionStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id, + string $title, + string $author, + Lifecycle $lifecycle, + string $remarks, + ?Uuid $original_id + ): Update { + $table_type = TableTypes::Questions; + return $this->persistence_factory->update( + $this->question_tables_definitions->getColumns( + $table_names_builder, + $table_type, + [ + 'id', + 'page_id', + 'created' + ] + ), + [ + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $title + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $author + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $lifecycle->value + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $remarks + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $original_id?->toString() + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + time() + ) + ], + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after + * this a question MUST never change the page assigned to it after its creation! + */ + private function buildUpdatePageIdStatement( + TableNameBuilder $table_names_builder, + Uuid $question_id, + ?int $page_id + ): Update { + $table_type = TableTypes::Questions; + return $this->persistence_factory->update( + [ + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + 'page_id' + ), + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_names_builder, + $table_type + ), + 'last_update' + ) + ], + [ + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $page_id + ), + $this->persistence_factory->value( + FieldDefinition::T_INTEGER, + time() + ) + ], + [ + $this->persistence_factory->where( + $this->question_tables_definitions->getIdColumn( + $table_names_builder, + $table_type + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $question_id->toString() + ) + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 17ff78228c9a..ed7f9ce1655f 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -53,6 +53,7 @@ public function __construct( private readonly \ilDBInterface $db, private readonly Refinery $refinery, private readonly UuidFactory $uuid_factory, + private readonly DatabaseStatementBuilder $database_statement_builder, private readonly PersistenceFactory $persistence_factory, private readonly TableDefinitions $question_table_definitions, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, @@ -237,10 +238,8 @@ public function delete( $questions, fn(Manipulate $c, Question $v): Manipulate => $v->toDelete( - $c, - $this->persistence_factory, - $this->question_table_definitions, - $this->answer_form_generic_table_definitions + $this->database_statement_builder, + $c ), $this->buildManipulate( ManipulationType::Delete @@ -403,10 +402,8 @@ private function store( array_reduce( $questions, fn(Manipulate $c, Question $v): Manipulate => $v->toStorage( - $c, - $this->persistence_factory, - $this->question_table_definitions, - $this->answer_form_generic_table_definitions + $this->database_statement_builder, + $c ), $manipulate )->run(); diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 82c162626f38..82006d0193c4 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -22,26 +22,18 @@ use ILIAS\Questions\Administration\ConfigurationRepository; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; -use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Attempt\Attempt; -use ILIAS\Questions\Question\Persistence\TableDefinitions as QuestionTableDefinitions; -use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\Column; -use ILIAS\Questions\Persistence\Delete; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\Insert; -use ILIAS\Questions\Persistence\Update; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\Query; -use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\Question\Persistence\DatabaseStatementBuilder; use ILIAS\Questions\UserSettings\CreateModes; use ILIAS\Data\UUID\Uuid; -use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; @@ -373,55 +365,63 @@ public function toTableRow( } public function toStorage( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + DatabaseStatementBuilder $database_statement_builder, + Manipulate $manipulate ): Manipulate { return $manipulate->getManipulationType() === ManipulationType::Create - ? $this->addInsertStatementsToManipulation( + ? $database_statement_builder->addInsertStatementsToManipulation( $manipulate, - $persistence_factory, - $question_tables_definitions, - $answer_form_generic_table_definitions - ) : $this->addUpdateStatementsToManipulation( + $this->id, + $this->page_id, + $this->title, + $this->author, + $this->lifecycle, + $this->remarks, + $this->original_id, + $this->parent_obj_id, + $this->position + ) : $database_statement_builder->addUpdateStatementsToManipulation( $manipulate, - $persistence_factory, - $question_tables_definitions, - $answer_form_generic_table_definitions + $this->self_updated, + $this->linking_information_updated, + $this->page_id_updated, + $this->id, + $this->page_id, + $this->title, + $this->author, + $this->lifecycle, + $this->remarks, + $this->original_id, + $this->parent_obj_id, + $this->position, + $this->updated_answer_forms, + $this->deleted_answer_forms ); } public function toDelete( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions + DatabaseStatementBuilder $database_statement_builder, + Manipulate $manipulate ): Manipulate { $table_names_builder = $manipulate->getTableNameBuilder(null); - return $this->addDeleteAnswerFormsStatementsToManipulate( + return $database_statement_builder->addDeleteAnswerFormsStatementsToManipulate( $manipulate->withAdditionalStatement( - $this->buildDeleteQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder + $database_statement_builder->buildDeleteQuestionStatement( + $table_names_builder, + $this->id ) )->withAdditionalStatement( - $this->buildDeleteLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder + $database_statement_builder->buildDeleteLinkingStatement( + $table_names_builder, + $this->id ) )->withAdditionalStatement( - $this->buildDeleteMigrationStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder + $database_statement_builder->buildDeleteMigrationStatement( + $table_names_builder, + $this->id ) ), - $persistence_factory, - $answer_form_generic_table_definitions, $this->answer_forms ); } @@ -442,461 +442,15 @@ public function completeResponseQuery( ); } - private function addInsertStatementsToManipulation( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions - ): Manipulate { - if ($this->created === null) { - $manipulate = $manipulate - ->withAdditionalStatement( - $this->buildInsertLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $manipulate->getTableNameBuilder(null) - ) - )->withAdditionalStatement( - $this->buildInsertQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $manipulate->getTableNameBuilder(null) - ) - ); - } - - if ($this->updated_answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->updated_answer_forms - ); - } - - if ($this->answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->answer_forms - ); - } - - return $manipulate; - } - - private function addUpdateStatementsToManipulation( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions - ): Manipulate { - $table_names_builder = $manipulate->getTableNameBuilder(null); - - if ($this->linking_information_updated) { - $manipulate = $manipulate - ->withAdditionalStatement( - $this->buildUpdateLinkingStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder - ) - ); - } - - if ($this->self_updated) { - $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdateQuestionStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder - ) - ); - } - - if ($this->page_id) { - $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdatePageIdStatement( - $persistence_factory, - $question_tables_definitions, - $table_names_builder - ) - ); - } - - if ($this->deleted_answer_forms !== []) { - $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->deleted_answer_forms - ); - } - - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $persistence_factory, - $answer_form_generic_table_definitions, - $this->updated_answer_forms - ); - } - - private function addAnswerFormStatementsToManipulate( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - array $answer_forms - ): Manipulate { - return array_reduce( - $answer_forms, - function ( - Manipulate $c, - AnswerFormProperties $v - ) use ( - $persistence_factory, - $answer_form_generic_table_definitions - ): Manipulate { - $manipulate_with_generic_properties = $v->getTypeGenericProperties() - ->toStorage( - $persistence_factory, - $answer_form_generic_table_definitions, - $c - ); - - return $v->toStorage( - $persistence_factory, - $manipulate_with_generic_properties - ); - }, - $manipulate - ); - } - - private function addDeleteAnswerFormsStatementsToManipulate( - Manipulate $manipulate, - PersistenceFactory $persistence_factory, - AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - array $answer_forms_to_delete - ): Manipulate { - return array_reduce( - $answer_forms_to_delete, - fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( - $v->getTypeGenericProperties()->toDelete( - $persistence_factory, - $answer_form_generic_table_definitions, - $c, - ) - ), - $manipulate - ); - } - - - - private function buildInsertLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_names_builder - ): Insert { - return $persistence_factory->insert( - $question_tables_definitions->getColumns( - $table_names_builder, - QuestionTableTypes::Linking - ), - [ - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - $this->parent_obj_id - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - $this->position - ) - ] - ); - } - - private function buildInsertQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_names_builder - ): Insert { - return $persistence_factory->insert( - $question_tables_definitions->getColumns( - $table_names_builder, - QuestionTableTypes::Questions - ), - [ - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - $this->page_id - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->title - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->author - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->lifecycle->value - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->remarks - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->original_id?->toString() - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - time() - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - time() - ) - ] - ); - } - - private function buildUpdateLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_names_builder - ): Update { - $table_type = QuestionTableTypes::Linking; - return $persistence_factory->update( - $question_tables_definitions->getColumns( - $table_names_builder, - $table_type, - [QuestionTableTypes::LINKING_TABLE_ID_COLUMN] - ), - [ - $persistence_factory->value( - FieldDefinition::T_INTEGER, - $this->parent_obj_id - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - $this->position - ) - ], - [ - $persistence_factory->where( - $question_tables_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildUpdateQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_names_builder - ): Update { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->update( - $question_tables_definitions->getColumns( - $table_names_builder, - $table_type, - [ - 'id', - 'page_id', - 'created' - ] - ), - [ - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->title - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->author - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->lifecycle->value - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->remarks - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->original_id?->toString() - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - time() - ) - ], - [ - $persistence_factory->where( - $question_tables_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildDeleteQuestionStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $question_tables_definitions, - TableNameBuilder $table_names_builder - ): Delete { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->delete( - $persistence_factory->table( - $table_names_builder, - $table_type - ), - [ - $persistence_factory->where( - $question_tables_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - private function buildDeleteLinkingStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_names_builder - ): Delete { - $table_type = QuestionTableTypes::Linking; - return $persistence_factory->delete( - $persistence_factory->table( - $table_names_builder, - $table_type - ), - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - /** - * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after - * this MUST go! - */ - private function buildDeleteMigrationStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_names_builder - ): Delete { - $table_type = QuestionTableTypes::MigrationsTable; - return $persistence_factory->delete( - $persistence_factory->table( - $table_names_builder, - $table_type - ), - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] - ); - } - - /** - * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after - * this a question MUST never change the page assigned to it after its creation! - */ - private function buildUpdatePageIdStatement( - PersistenceFactory $persistence_factory, - QuestionTableDefinitions $table_definitions, - TableNameBuilder $table_names_builder - ): Update { - $table_type = QuestionTableTypes::Questions; - return $persistence_factory->update( - [ - $persistence_factory->column( - $persistence_factory->table( - $table_names_builder, - $table_type - ), - 'page_id' - ), - $persistence_factory->column( - $persistence_factory->table( - $table_names_builder, - $table_type - ), - 'last_update' - ) - ], - [ - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->page_id - ), - $persistence_factory->value( - FieldDefinition::T_INTEGER, - time() - ) - ], - [ - $persistence_factory->where( - $table_definitions->getIdColumn( - $table_names_builder, - $table_type - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->id->toString() - ) - ) - ] + public function retrieveAnswerFormResponsesFromQuery( + Uuid $response_id, + Query $query + ): array { + return array_map( + fn(AnswerFormProperties $v): Response => $v + ->getDefinition() + ->buildResponse($query), + $this->answer_forms ); } diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html new file mode 100755 index 000000000000..f4367a1b8a44 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html @@ -0,0 +1,4 @@ + + + {ICON_OK} + \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html new file mode 100755 index 000000000000..bb71cd14a1f8 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html @@ -0,0 +1 @@ + size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}" /> diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html new file mode 100755 index 000000000000..e6c97f9bf635 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html new file mode 100755 index 000000000000..f71601d93384 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html @@ -0,0 +1 @@ + size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}"/> \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html new file mode 100755 index 000000000000..f71601d93384 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html @@ -0,0 +1 @@ + size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}"/> \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html b/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html new file mode 100644 index 000000000000..92f87c343d29 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html @@ -0,0 +1,9 @@ +
+
+ {QUESTION_OUTPUT} + +

{RECEIVED_POINTS_INFORMATION}

+ +
+
From 172a686411c14ea14597dab59db2b55df36dbad5 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 18 May 2026 10:24:44 +0200 Subject: [PATCH 099/108] Questions: Implement Response --- .../Administration/ViewConfiguration.php | 209 ++++++++++++ .../class.ilObjQuestionPreviewGUI.php | 297 ++++++++++++------ .../class.ilObjQuestionsGUI.php | 22 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 82 +++-- .../Legacy/PageEditor/QstsQuestionPage.php | 49 +++ .../PageEditor/class.QstsQuestionPageGUI.php | 11 +- .../PageEditor/class.ilPCAnswerForm.php | 185 ++++++++++- .../ILIAS/Questions/Legacy/Temp/Attempt.php | 74 +++++ ...ptRepository.php => AttemptRepository.php} | 33 +- components/ILIAS/Questions/Questions.php | 11 +- .../Capabilities/AsyncView/AsyncView.php | 35 +++ .../{Async => AsyncView}/Capability.php | 53 +--- .../AnswerForm/Capabilities/Capability.php | 17 +- .../Capabilities/DefaultView/Capability.php | 62 ++++ .../Capabilities/DefaultView/DefaultView.php | 35 +++ .../{ => Definitions}/ActionWithTab.php | 3 +- .../AdditionalFormStepAction.php | 3 +- .../Definitions/AdditionalStepProvider.php | 26 ++ .../Definitions/AdditionalTabProvider.php | 26 ++ .../Capabilities/Definitions/Feedback.php | 41 +++ .../FeedbackProvider.php} | 9 +- .../Capabilities/Definitions/FeedbackView.php | 38 +++ .../Capabilities/Definitions/Marking.php | 36 +++ .../Definitions/MarkingProvider.php | 31 ++ .../{ => Definitions}/Migration.php | 2 +- .../Definitions/ParticipantViewProvider.php | 31 ++ .../src/AnswerForm/Capabilities/Factory.php | 86 ++++- .../Capability.php | 51 ++- .../MarkingAllowingPartialPoints.php} | 19 +- .../{Edit.php => RequiredCapabilities.php} | 165 +++++----- ...stedLearningContent.php => Capability.php} | 79 +++-- .../SuggestedLearningContent/Content.php | 11 +- .../SuggestedLearningContent/Migration.php | 2 +- .../NodeRetrieval.php | 8 +- .../SuggestedLearningContent/Overview.php | 11 +- .../SuggestedLearningContent/Types.php | 80 ++++- .../{Feedback => TextFeedback}/Capability.php | 63 ++-- .../{Feedback => TextFeedback}/Migration.php | 4 +- .../{Feedback => TextFeedback}/Overview.php | 86 +++-- .../OverviewTable.php | 8 +- .../{Feedback => TextFeedback}/Repository.php | 10 +- .../SpecificTextFeedback.php} | 6 +- .../TableDefinitions.php | 2 +- .../{Feedback => TextFeedback}/TableTypes.php | 6 +- .../TextFeedback.php} | 171 ++++++++-- .../{Feedback => TextFeedback}/Types.php | 10 +- .../Capabilities/TypeSpecification.php | 26 ++ .../Questions/src/AnswerForm/Definition.php | 16 +- .../Migration/MigrationPurifier.php | 8 +- .../Questions/src/AnswerForm/Properties.php | 3 +- .../Questions/src/AnswerForm/Response.php | 4 + .../src/AnswerForm/Views/Participant.php | 19 +- .../Cloze/Capabilities/Async.php | 55 ---- .../Cloze/Capabilities/AsyncView.php | 38 +++ .../Cloze/Capabilities/DefaultView.php | 38 +++ .../Cloze/Capabilities/Feedback.php | 131 -------- ...g.php => MarkingAllowingPartialPoints.php} | 27 +- .../Cloze/Capabilities/TextFeedback.php | 228 ++++++++++++++ ... => TextFeedbackOverviewDataRetrieval.php} | 16 +- ...able.php => TextFeedbackOverviewTable.php} | 64 ++-- .../src/AnswerFormTypes/Cloze/Definition.php | 58 +++- .../Cloze/Layout/OverviewTable.php | 34 +- .../Cloze/Migration/MigrationCloze.php | 4 +- .../Cloze/Properties/ClozeText/Text.php | 2 +- .../Properties/Combinations/Combination.php | 16 +- .../Cloze/Properties/Combinations/Edit.php | 3 +- .../Properties/Combinations/Overview.php | 8 +- .../Gaps/AnswerOptions/AnswerOptions.php | 27 +- .../Cloze/Properties/Gaps/Gap.php | 44 ++- .../Cloze/Properties/Gaps/Gaps.php | 174 +++++++++- .../Cloze/Properties/Gaps/LongMenu.php | 131 ++++++-- .../Cloze/Properties/Gaps/Numeric.php | 150 +++++++-- .../Cloze/Properties/Gaps/Select.php | 130 ++++++-- .../Cloze/Properties/Gaps/Text.php | 157 +++++++-- .../Cloze/Properties/Gaps/Type.php | 204 +++++++++++- .../Cloze/Properties/Properties.php | 28 +- .../Cloze/Response/AnswerForm.php | 74 ++++- .../Cloze/Response/AnswerInput.php | 42 ++- .../Cloze/Response/Factory.php | 112 ++++++- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 7 +- .../Cloze/Views/Participant.php | 37 ++- .../src/Attempt/AdditionalAttemptData.php | 154 +-------- .../ILIAS/Questions/src/Attempt/Attempt.php | 22 +- .../Questions/src/Attempt/Repository.php | 190 +++++++++-- .../ILIAS/Questions/src/Attempt/Response.php | 49 ++- .../src/Attempt/TableDefinitions.php | 17 +- .../Questions/src/DefaultPublicInterface.php | 15 +- .../Questions/src/Persistence/Delete.php | 8 +- .../Questions/src/Persistence/Update.php | 8 +- .../Definitions/DefaultEnvironment.php | 34 +- .../Presentation/Definitions/Environment.php | 5 +- .../Definitions/OverviewTableColumns.php | 4 +- .../src/Presentation/Definitions/ViewMode.php | 8 +- .../src/Presentation/Layout/EditOverview.php | 2 +- .../Presentation/Layout/QuestionsTable.php | 3 +- .../Questions/src/Presentation/Views/Edit.php | 85 +++-- .../src/Presentation/Views/Participant.php | 126 +++++--- .../ILIAS/Questions/src/PublicInterface.php | 11 + .../ILIAS/Questions/src/Question/Question.php | 127 ++++++-- .../Questions/src/Question/Views/Edit.php | 203 +++++++++++- .../src/Question/Views/Participant.php | 48 ++- .../src/Setup/ClozeQuestionTables.php | 8 +- .../src/Setup/OverarchingQuestionTables.php | 8 +- .../ILIAS/Questions/src/Setup/TempTables.php | 10 +- .../Questions/src/UserSettings/CreateMode.php | 4 +- .../default/tpl.cloze_gap_longmenu.html | 2 +- .../default/tpl.cloze_gap_numeric.html | 3 +- .../default/tpl.cloze_gap_select.html | 2 +- .../default/tpl.cloze_gap_static.html | 2 +- .../templates/default/tpl.cloze_gap_text.html | 2 +- ...qsts_preview_presentation_interactive.html | 8 + ...pl.qsts_preview_question_presentation.html | 9 - .../tpl.qsts_question_presentation.html | 10 +- .../classes/class.ilUnitConfigurationGUI.php | 1 + .../legacy/Modules/_component_test.scss | 6 + templates/default/delos.css | 4 + 116 files changed, 4199 insertions(+), 1413 deletions(-) create mode 100755 components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php create mode 100644 components/ILIAS/Questions/Legacy/Temp/Attempt.php rename components/ILIAS/Questions/Legacy/Temp/{QstsTempAttemptRepository.php => AttemptRepository.php} (61%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Async => AsyncView}/Capability.php (58%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{ => Definitions}/ActionWithTab.php (94%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{ => Definitions}/AdditionalFormStepAction.php (98%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalStepProvider.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalTabProvider.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Feedback.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Async/Async.php => Definitions/FeedbackProvider.php} (78%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/FeedbackView.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Marking.php create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/MarkingProvider.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{ => Definitions}/Migration.php (94%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/ParticipantViewProvider.php rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Marking => MarkingAllowingPartialPoints}/Capability.php (68%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Marking/Marking.php => MarkingAllowingPartialPoints/MarkingAllowingPartialPoints.php} (60%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Edit.php => RequiredCapabilities.php} (52%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/{SuggestedLearningContent.php => Capability.php} (63%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/Capability.php (79%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/Migration.php (98%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/Overview.php (85%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/OverviewTable.php (86%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/Repository.php (95%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback/SpecificFeedback.php => TextFeedback/SpecificTextFeedback.php} (93%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/TableDefinitions.php (98%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/TableTypes.php (80%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback/Feedback.php => TextFeedback/TextFeedback.php} (67%) rename components/ILIAS/Questions/src/AnswerForm/Capabilities/{Feedback => TextFeedback}/Types.php (76%) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/TypeSpecification.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/{Marking.php => MarkingAllowingPartialPoints.php} (70%) create mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/{FeedbackOverviewDataRetrieval.php => TextFeedbackOverviewDataRetrieval.php} (89%) rename components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/{FeedbackOverviewTable.php => TextFeedbackOverviewTable.php} (90%) create mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_preview_presentation_interactive.html delete mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html diff --git a/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php b/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php new file mode 100755 index 000000000000..194a8d3be806 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php @@ -0,0 +1,209 @@ +url_builder, + $this->configuration_token + ] = $url_builder->acquireParameter( + self::PARAMETER_NAMESPACE, + self::CONFIGURATION_TOKEN_STRING + ); + + $this->current_configuration = $query_wrapper->retrieve( + $this->configuration_token->getName(), + $this->refinery->custom()->transformation( + function (mixed $values): array { + $current_configuration = [ + DefaultView::getIdentifier() + ]; + if (!is_array($values)) { + return $current_configuration; + } + + $available_capabilities = [ + AsyncView::getIdentifier(), + DefaultView::getIdentifier(), + TextFeedback::getIdentifier(), + SuggestedLearningContent::getIdentifier(), + MarkingAllowingPartialPoints::getIdentifier() + ]; + + return array_reduce( + $values, + function (array $c, string $v) use ($available_capabilities): array { + if ($v === AsyncView::getIdentifier() + || $v === DefaultView::getIdentifier()) { + $c = array_filter( + $c, + fn(string $v): bool => $v !== DefaultView::getIdentifier() + && $v !== AsyncView::getIdentifier() + ); + } + + if (in_array($v, $available_capabilities) + && !in_array($v, $c)) { + $c[] = $v; + } + + return $c; + }, + $current_configuration + ); + } + ) + ); + } + + public function getCurrentConfiguration(): array + { + return $this->current_configuration; + } + + public function getURLBuilderWithPreservedConfigurationParameter( + ?URLBuilder $url_builder = null + ): URLBuilder { + if ($url_builder !== null) { + [ + $url_builder, + $configuration_token + ] = $url_builder->acquireParameter( + self::PARAMETER_NAMESPACE, + self::CONFIGURATION_TOKEN_STRING + ); + } else { + $url_builder = $this->url_builder; + $configuration_token = $this->configuration_token; + } + + return $url_builder->withParameter( + $configuration_token, + $this->current_configuration + ); + } + + public function initializeToolbar(): void + { + $configuration_without_view = array_filter( + $this->current_configuration, + fn(string $v): bool => $v !== DefaultView::getIdentifier() && $v !== AsyncView::getIdentifier() + ); + $this->toolbar->addComponent( + $this->ui_factory->viewControl()->mode( + [ + $this->lng->txt('default_view') => $this->url_builder->withParameter( + $this->configuration_token, + [...$configuration_without_view, DefaultView::getIdentifier()] + )->buildURI()->__toString(), + $this->lng->txt('async_view') => $this->url_builder->withParameter( + $this->configuration_token, + [...$configuration_without_view, AsyncView::getIdentifier()] + )->buildURI()->__toString() + ], + $this->lng->txt('select_view') + )->withActive( + in_array(DefaultView::getIdentifier(), $this->current_configuration) + ? $this->lng->txt('default_view') + : $this->lng->txt('async_view') + ), + ); + + $this->toolbar->addComponent( + $this->buildToolbarButton( + MarkingAllowingPartialPoints::getIdentifier() + ) + ); + + $this->toolbar->addComponent( + $this->buildToolbarButton( + TextFeedback::getIdentifier() + ) + ); + + $this->toolbar->addComponent( + $this->buildToolbarButton( + SuggestedLearningContent::getIdentifier() + ) + ); + } + + private function buildToolbarButton( + string $identifier + ): StandardButton { + $capability_activated = in_array($identifier, $this->current_configuration); + $filtered_configuration = array_filter( + $this->current_configuration, + fn(string $v): bool => $v !== $identifier + ); + + return $this->ui_factory->button()->standard( + $this->buildButtonLabel($identifier, $capability_activated), + $this->url_builder->withParameter( + $this->configuration_token, + $capability_activated + ? $filtered_configuration + : [...$filtered_configuration, $identifier] + )->buildURI()->__toString() + ); + } + + private function buildButtonLabel( + string $identifier, + bool $enabled + ): string { + $identifier_lng_string = strtolower($identifier); + + return $enabled + ? $this->lng->txt("disable_{$identifier_lng_string}") + : $this->lng->txt("enable_{$identifier_lng_string}"); + } +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php index 6bb2f61c08cc..dbe9911e8c00 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php @@ -18,16 +18,15 @@ declare(strict_types=1); -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; +use ILIAS\Questions\Legacy\Administration\ViewConfiguration; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Questions\Question\Views\Participant as QuestionParticipantView; use ILIAS\Questions\PublicInterface; use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; use ILIAS\Questions\Presentation\Views\Participant; +use ILIAS\Questions\Temp\AttemptRepository; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\Order; use ILIAS\Data\Range; @@ -57,19 +56,22 @@ class ilObjQuestionPreviewGUI implements DataRetrieval private const string CMD_DEFAULT = 'show'; private const string CMD_SHOW_QUESTION = 'showQuestion'; - private const string CMD_SHOW_QUESTION_ASYNC = 'showQuestionAsync'; private const string CMD_RESPOND = 'respond'; + private const string CMD_DELETE_RESPONSES = 'deleteResponses'; private readonly Repository $repository; private readonly AnswerFormFactory $answer_form_factory; private readonly Participant $participant_view; + private readonly ViewConfiguration $view_configuration; + private readonly ilCtrl $ctrl; private readonly Language $lng; private readonly HTTPServices $http; private readonly ilGlobalTemplateInterface $tpl; private readonly ilObjUser $current_user; private readonly ilTabsGUI $tabs_gui; + private readonly ilToolbarGUI $toolbar; private readonly UIFactory $ui_factory; private readonly UIRenderer $ui_renderer; private readonly \ilUIService $ui_service; @@ -81,22 +83,11 @@ class ilObjQuestionPreviewGUI implements DataRetrieval private readonly URLBuilderToken $action_token; private readonly URLBuilderToken $row_id_token; - private readonly QstsTempAttemptRepository $temp_attempt_repository; + private readonly AttemptRepository $temp_attempt_repository; public function __construct( private readonly int $object_id ) { - /** - * sk, 2026.05.06: This is done this way as this is a completely - * temporary class. It should be made as simple as possible to get rid - * of it. - */ - $local_dic = LocalDIC::dic(); - $this->repository = $local_dic[Repository::class]; - $this->answer_form_factory = $local_dic[AnswerFormFactory::class]; - $this->participant_view = $local_dic[PublicInterface::class] - ->getParticipantView($this->object_id); - /** @var ILIAS\DI\Container $DIC */ global $DIC; $this->ctrl = $DIC['ilCtrl']; @@ -104,6 +95,7 @@ public function __construct( $this->tpl = $DIC['tpl']; $this->current_user = $DIC['user']->getLoggedInUser(); $this->tabs_gui = $DIC['ilTabs']; + $this->toolbar = $DIC['ilToolbar']; $this->http = $DIC['http']; $this->ui_factory = $DIC['ui.factory']; $this->ui_renderer = $DIC['ui.renderer']; @@ -112,13 +104,13 @@ public function __construct( $this->data_factory = new DataFactory(); $this->uuid_factory = new UuidFactory(); - $this->temp_attempt_repository = new QstsTempAttemptRepository( + $this->temp_attempt_repository = new AttemptRepository( $DIC['ilDB'], $this->uuid_factory ); [ - $this->url_builder, + $url_builder, $this->action_token, $this->row_id_token ] = $this->getUrlBuilder()->acquireParameters( @@ -126,6 +118,32 @@ public function __construct( self::ACTION_TOKEN_STRING, self::ROW_ID_TOKEN_STRING ); + + $this->view_configuration = new ViewConfiguration( + $this->lng, + $this->refinery, + $this->ui_factory, + $this->http->wrapper()->query(), + $url_builder, + $DIC['ilToolbar'] + ); + + $this->url_builder = $this->view_configuration + ->getURLBuilderWithPreservedConfigurationParameter(); + + /** + * sk, 2026.05.06: This is done this way as this is a completely + * temporary class. It should be made as simple as possible to get rid + * of it. + */ + $local_dic = LocalDIC::dic(); + $this->repository = $local_dic[Repository::class]; + $this->answer_form_factory = $local_dic[AnswerFormFactory::class]; + $this->participant_view = $local_dic[PublicInterface::class] + ->getParticipantView( + $this->view_configuration->getCurrentConfiguration(), + $this->object_id + ); } public function executeCommand(): void @@ -153,6 +171,8 @@ public function executeCommand(): void private function showCmd(): void { + $this->view_configuration->initializeToolbar(); + $filter = $this->buildFilter( $this->ctrl->getLinkTargetByClass( $this->getClassPath() @@ -167,17 +187,15 @@ private function showCmd(): void $this->lng->txt('questions'), [ OverviewTableColumns::Title->value - => $this->ui_factory->table()->column()->text( + => $this->ui_factory->table()->column()->link( $this->lng->txt('title') ), OverviewTableColumns::AnswerFormTypes->value => $this->ui_factory->table()->column()->text( - $this->lng->txt('contained_types') + $this->lng->txt('contained_answer_form_types') )->withIsOptional(true, true) ->withIsSortable(false), ], - )->withActions( - $this->getActions() )->withRange(new Range(0, 20)) ->withFilter( $this->ui_service->filter()->getData($filter) @@ -189,7 +207,7 @@ private function showCmd(): void private function showQuestionCmd(): void { $question_id = $this->retrieveQuestionIdFromQuery(); - $attempt_id = $this->temp_attempt_repository->get( + $attempt = $this->temp_attempt_repository->get( $this->current_user->getId() ); @@ -204,53 +222,111 @@ private function showQuestionCmd(): void $this->tabs_gui->clearTargets(); $this->tabs_gui->setBackTarget( $this->lng->txt('back'), - $this->ctrl->getLinkTargetByClass( - $this->getClassPath() + $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( + $this->getUrlBuilder() + )->buildURI()->__toString() + ); + + $this->toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('delete_responses'), + $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( + $this->getUrlBuilderWithPreservedQuestionParameter( + $question_id, + self::CMD_DELETE_RESPONSES + ) + )->buildURI()->__toString() ) ); - $view = $this->participant_view - ->withRequiredCapabilities([ - Feedback::class, - SuggestedLearningContent::class, - Marking::class - ])->getQuestionView( - $question_id, - $attempt_id - ); + $view = $this->participant_view->getQuestionView( + $question_id, + $attempt?->getAttemptId(), + true, + false, + false, + false + ); - if ($attempt_id === null) { - $this->temp_attempt_repository->store( + if ($attempt === null) { + $this->temp_attempt_repository->storeNew( $this->current_user->getId(), $view->getAttemptId() ); } + $content = [ + $this->ui_factory->panel()->standard( + $this->lng->txt('question'), + $this->ui_factory->legacy()->content( + $this->buildQuestionForm($view, $question_id) + ) + ) + ]; + + if ($attempt?->isQuestionSolved($question_id) ?? false) { + $content[] = $this->ui_factory->panel()->standard( + $this->lng->txt('feedback'), + $this->participant_view->getQuestionView( + $question_id, + $attempt->getAttemptId(), + false, + true, + true, + true + )->getUI() + ); + } + $this->tpl->setContent( $this->ui_renderer->render( - $this->ui_factory->panel()->standard( - $this->lng->txt('question'), - $this->ui_factory->legacy()->content( - $this->buildQuestionForm($view) - ) - ) + $content ) ); } - private function showQuestionAsyncCmd(): void + private function respondCmd(): void { - $this->tpl->setContent('Async'); + $question_id = $this->retrieveQuestionIdFromQuery(); + $attempt = $this->temp_attempt_repository->get( + $this->current_user->getId() + ); + + if ($question_id === null || $attempt === null) { + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_FAILURE, + $this->lng->txt('invalid') + ); + $this->showCmd(); + } + + $this->participant_view->persistResponse( + $question_id, + $attempt->getAttemptId() + ); + + $this->temp_attempt_repository->storeSolved( + $attempt->withAdditionalSolvedQuestion($question_id) + ); + + $this->ctrl->redirectToURL( + $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( + $this->getUrlBuilderWithPreservedQuestionParameter( + $question_id, + self::CMD_SHOW_QUESTION + ) + )->buildURI()->__toString() + ); } - private function respondCmd(): void + private function deleteResponsesCmd(): void { $question_id = $this->retrieveQuestionIdFromQuery(); - $attempt_id = $this->temp_attempt_repository->get( + $attempt = $this->temp_attempt_repository->get( $this->current_user->getId() ); - if ($question_id === null || $attempt_id === null) { + if ($question_id === null || $attempt === null) { $this->tpl->setOnScreenMessage( GlobalTemplate::MESSAGE_TYPE_FAILURE, $this->lng->txt('invalid') @@ -258,23 +334,23 @@ private function respondCmd(): void $this->showCmd(); } - $this->tabs_gui->clearTargets(); - $this->tabs_gui->setBackTarget( - $this->lng->txt('back'), - $this->ctrl->getLinkTargetByClass( - $this->getClassPath() - ) + $this->participant_view->deleteResponsesFor( + $attempt->getAttemptId(), + $question_id ); - $response_id = $this->participant_view - ->withRequiredCapabilities([ - Feedback::class, - SuggestedLearningContent::class, - Marking::class - ])->persistResponse( - $question_id, - $response_id - ); + $this->temp_attempt_repository->storeSolved( + $attempt->withQuestionRemovedFromSolved($question_id) + ); + + $this->ctrl->redirectToURL( + $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( + $this->getUrlBuilderWithPreservedQuestionParameter( + $question_id, + self::CMD_SHOW_QUESTION + ) + )->buildURI()->__toString() + ); } #[\Override] @@ -295,7 +371,15 @@ public function getRows( yield $row_builder->buildDataRow( $question->getid()->toString(), [ - OverviewTableColumns::Title->value => $question->getTitle(), + OverviewTableColumns::Title->value => $this->ui_factory->link()->standard( + $question->getTitle(), + $this->view_configuration + ->getURLBuilderWithPreservedConfigurationParameter() + ->withParameter($this->action_token, self::CMD_SHOW_QUESTION) + ->withParameter($this->row_id_token, $question->getid()->toString()) + ->buildURI() + ->__toString() + ), OverviewTableColumns::AnswerFormTypes->value => implode( '
', $question->getListOfContainedAnswerFormTypeLabels($this->lng) @@ -342,31 +426,15 @@ private function buildFilter( : $filter; } - private function getActions(): array - { - return [ - 'show' => $this->ui_factory->table()->action()->single( - $this->lng->txt('show'), - $this->url_builder->withParameter($this->action_token, self::CMD_SHOW_QUESTION), - $this->row_id_token - ), - 'show_async' => $this->ui_factory->table()->action()->single( - $this->lng->txt('show_async'), - $this->url_builder->withParameter($this->action_token, self::CMD_SHOW_QUESTION_ASYNC), - $this->row_id_token - ), - ]; - } - private function retrieveQuestionIdFromQuery(): ?Uuid { return $this->http->wrapper()->query()->retrieve( $this->row_id_token->getName(), $this->refinery->byTrying([ $this->refinery->custom()->transformation( - function (array $v): Uuid { + function (string $v): Uuid { try { - return $this->uuid_factory->fromString($v[0]); + return $this->uuid_factory->fromString($v); } catch (Throwable $e) { throw new ConstraintViolationException( sprintf('The value could not be transformed into a Uuid'), @@ -380,41 +448,68 @@ function (array $v): Uuid { ); } - private function getUrlBuilder(): URLBuilder - { + private function getUrlBuilder( + ?string $cmd = null + ): URLBuilder { return new URLBuilder( $this->data_factory->uri( ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( - $this->getClassPath() + $this->getClassPath(), + $cmd ) ) ); } + private function getUrlBuilderWithPreservedQuestionParameter( + Uuid $question_id, + string $cmd + ): URLBuilder { + [$url_builder, $row_id_parameter] = $this->getUrlBuilder($cmd) + ->acquireParameter( + self::PARAMETER_NAMENSPACE, + self::ROW_ID_TOKEN_STRING + ); + + return $url_builder->withParameter( + $row_id_parameter, + $question_id->toString() + ); + } + private function buildQuestionForm( - QuestionParticipantView $view + QuestionParticipantView $view, + Uuid $question_id ): string { - $form = new ilPropertyFormGUI(); - $form->setCloseTag(false); - $form->setFormAction( - $this->ctrl->getFormActionByClass( - $this->getClassPath() - ) + $tpl = new \ilTemplate( + 'tpl.qsts_preview_presentation_interactive.html', + true, + true, + 'components/ILIAS/Questions' ); - $form->addCommandButton( - self::CMD_RESPOND, - $this->lng->txt('send') + + $tpl->setVariable( + 'FORM_ACTION', + $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( + $this->getUrlBuilderWithPreservedQuestionParameter( + $question_id, + self::CMD_RESPOND + ) + )->buildURI() + ->__toString() ); - $form_opening = $form->getHTML(); + $tpl->setVariable( + 'QUESTION_OUTPUT', + $this->ui_renderer->render($view->getUI()) + ); - $form->setOpenTag(false); - $form->setCloseTag(true); + $tpl->setVariable( + 'SUBMIT_BUTTON_LABEL', + $this->lng->txt('send') + ); - return $form_opening - . $this->ui_renderer->render( - $view->getUI() - ) . $form->getHTML(); + return $tpl->get(); } private function getClassPath(): array diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 589035656ab9..4a55bb6a7b5d 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -20,9 +20,10 @@ use ILIAS\Questions\Administration\ConfigurationGUI; use ILIAS\Questions\Administration\ConfigurationRepository; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\SuggestedLearningContent; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\DefaultView\Capability as DefaultView; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Capability as TextFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\Capability as SuggestedLearningContent; +use ILIAS\Questions\AnswerForm\Capabilities\MarkingAllowingPartialPoints\Capability as MarkingAllowingPartialPoints; use ILIAS\Questions\Legacy\LocalDIC; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\PublicInterface; @@ -68,12 +69,15 @@ public function __construct( $local_dic = LocalDIC::dic(); $this->edit_view = $local_dic[PublicInterface::class] - ->getEditView($this->object->getId()) - ->withRequiredCapabilities([ - Feedback::class, - SuggestedLearningContent::class, - Marking::class - ]); + ->getEditView( + [ + DefaultView::getIdentifier(), + TextFeedback::getIdentifier(), + SuggestedLearningContent::getIdentifier(), + MarkingAllowingPartialPoints::getIdentifier() + ], + $this->object->getId() + ); $this->configuration_repository = $local_dic[ConfigurationRepository::class]; $this->lng->loadLanguageModule('assessment'); diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index e3a310ef0e21..3d158c45f5b0 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -87,8 +87,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[QuestionsRepository::class], $c[AttemptRepository::class], $c[LayoutFactory::class], - $c[Capabilities\Factory::class], - $c[Capabilities\Edit::class] + $c[Capabilities\Factory::class] ); $dic[ConfigurationRepository::class] = static fn($c): ConfigurationRepository @@ -129,43 +128,39 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Capabilities\Factory::class] = static fn($c): Capabilities\Factory => new Capabilities\Factory([ - Capabilities\Feedback\Feedback::class => new Capabilities\Feedback\Capability( + new Capabilities\TextFeedback\Capability( $c[DataFactory::class]->text(), - new Capabilities\Feedback\Repository( + new Capabilities\TextFeedback\Repository( $DIC['ilDB'], $DIC['refinery'], $c[UuidFactory::class], $c[DataFactory::class]->text(), $c[PersistenceFactory::class], - new Capabilities\Feedback\TableDefinitions( + new Capabilities\TextFeedback\TableDefinitions( $c[PersistenceFactory::class] ) ) ), - Capabilities\SuggestedLearningContent\SuggestedLearningContent::class - => new Capabilities\SuggestedLearningContent\SuggestedLearningContent( - $DIC['ilCtrl'], - $DIC['rbacsystem'], - $DIC['tree'], - $DIC['static_url'], - $DIC['user']->getLoggedInUser(), - new Capabilities\SuggestedLearningContent\Repository( - $DIC['ilDB'], - $DIC['refinery'], - $DIC['resource_storage'], - $c[PersistenceFactory::class], - new Capabilities\SuggestedLearningContent\TableDefinitions( - $c[PersistenceFactory::class] - ) + new Capabilities\SuggestedLearningContent\Capability( + $DIC['ilCtrl'], + $DIC['rbacsystem'], + $DIC['tree'], + $DIC['static_url'], + $DIC['user']->getLoggedInUser(), + new Capabilities\SuggestedLearningContent\Repository( + $DIC['ilDB'], + $DIC['refinery'], + $DIC['resource_storage'], + $c[PersistenceFactory::class], + new Capabilities\SuggestedLearningContent\TableDefinitions( + $c[PersistenceFactory::class] ) - ), - Capabilities\Marking\Marking::class => new Capabilities\Marking\Capability(), - Capabilities\Async\Async::class => new Capabilities\Async\Capability() + ) + ), + new Capabilities\MarkingAllowingPartialPoints\Capability(), + new Capabilities\DefaultView\Capability(), + new Capabilities\AsyncView\Capability() ]); - $dic[Capabilities\Edit::class] = fn($c): Capabilities\Edit - => new Capabilities\Edit( - $c[Capabilities\Factory::class] - ); $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory => new AnswerFormFactory( $c[UuidFactory::class], @@ -212,24 +207,20 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Properties\Gaps\AnswerOptions\Factory::class], [ new Cloze\Properties\Gaps\Text( - $DIC['refinery'], $DIC['lng'], - $DIC['ui.factory'] + $DIC['refinery'] ), new Cloze\Properties\Gaps\Numeric( - $DIC['refinery'], $DIC['lng'], - $DIC['ui.factory'] + $DIC['refinery'] ), new Cloze\Properties\Gaps\Select( - $DIC['refinery'], $DIC['lng'], - $DIC['ui.factory'] + $DIC['refinery'] ), new Cloze\Properties\Gaps\LongMenu( - $DIC['refinery'], $DIC['lng'], - $DIC['ui.factory'], + $DIC['refinery'], $DIC['tpl'] ) ] @@ -251,6 +242,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic[Cloze\Response\Factory::class] = static fn($c): Cloze\Response\Factory => new Cloze\Response\Factory( + $c[UuidFactory::class], $c[PersistenceFactory::class] ); $dic[Cloze\Properties\Gaps\Edit::class] = static fn($c): Cloze\Properties\Gaps\Edit @@ -268,24 +260,30 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant => new Cloze\Views\Participant( $DIC['tpl'], - $c[MustacheEngine::class] + $c[MustacheEngine::class], + $c[Cloze\Response\Factory::class] ); $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( $c[Cloze\TableDefinitions::class], [ - Capabilities\Feedback\Feedback::class => new Cloze\Capabilities\Feedback( + new Cloze\Capabilities\TextFeedback( $c[UuidFactory::class], $c[DataFactory::class]->text() ), - Capabilities\Marking\Marking::class => new Cloze\Capabilities\Marking( - $c[Cloze\Properties\Factory::class] + new Cloze\Capabilities\MarkingAllowingPartialPoints( + $c[Cloze\Properties\Factory::class], + $c[Cloze\Response\Factory::class] ), - Capabilities\Async\Async::class => new Cloze\Capabilities\Async() + new Cloze\Capabilities\DefaultView( + $c[Cloze\Views\Participant::class] + ), + new Cloze\Capabilities\AsyncView( + $c[Cloze\Views\Participant::class] + ) ], $c[Cloze\Properties\Factory::class], $c[Cloze\Response\Factory::class], - $c[Cloze\Views\Edit::class], - $c[Cloze\Views\Participant::class] + $c[Cloze\Views\Edit::class] ); $dic[Cloze\Properties\Combinations\Factory::class] = static fn($c): Cloze\Properties\Combinations\Factory => new Cloze\Properties\Combinations\Factory( diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index cf84ca72ecfa..3d4088177fcc 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; @@ -29,6 +30,10 @@ class QstsQuestionPage extends ilPageObject private readonly Question $question; private ?Attempt $attempt_data = null; private ?Viewable $participant_view = null; + private ?RequiredCapabilities $required_capabilites = null; + private bool $interactive = true; + private bool $show_best_response = true; + private bool $show_feedback = false; #[\Override] public function getParentType(): string @@ -80,6 +85,50 @@ public function setAttemptData( $this->attempt_data = $attempt_data; } + public function getRequiredCapabilities(): ?RequiredCapabilities + { + return $this->required_capabilites; + } + + public function setRequiredCapabilities( + RequiredCapabilities $required_capabilities + ): void { + $this->required_capabilites = $required_capabilities; + } + + public function getInteractive(): bool + { + return $this->interactive; + } + + public function setInteractive( + bool $interactive + ): void { + $this->interactive = $interactive; + } + + public function getShowBestResponse(): bool + { + return $this->show_best_response; + } + + public function setShowBestResponse( + bool $show_best_response + ): void { + $this->show_best_response = $show_best_response; + } + + public function getShowFeedback(): bool + { + return $this->show_feedback; + } + + public function setShowFeedback( + bool $show_feedback + ): void { + $this->show_feedback = $show_feedback; + } + public function addQuestionText( string $text ): void { diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index 7898ac07cd32..80d2992a3d75 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; @@ -36,7 +37,11 @@ class QstsQuestionPageGUI extends ilPageObjectGUI public function __construct( Question $question, - int $obj_id + int $obj_id, + RequiredCapabilities $required_capabilities, + bool $interactive, + bool $show_best_response, + bool $show_feedback ) { parent::__construct('qsts', $question->getPageId()); @@ -44,6 +49,10 @@ public function __construct( $this->obj->setQuestion($question); $this->obj->setParentId($obj_id); + $this->obj->setRequiredCapabilities($required_capabilities); + $this->obj->setInteractive($interactive); + $this->obj->setShowBestResponse($show_best_response); + $this->obj->setShowFeedback($show_feedback); } #[\Override] diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 5bacb3c2fb85..05d6dc31fa73 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -18,12 +18,18 @@ declare(strict_types=1); -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; +use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Legacy\LocalDIC; +use ILIAS\Questions\Presentation\Definitions\ViewMode; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Component\Legacy\Content as LegacyContent; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -33,6 +39,9 @@ class ilPCAnswerForm extends ilPageContent private const string ANSWER_FORM_ID_ATTRIBUTE = 'Uuid'; private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_([0-9a-f\-]+)\]\]\]'; + private const string TEMPLATE_VARIABLE_MAIN = 'OUTPUT'; + + public function init(): void { $this->setType('answf'); @@ -55,16 +64,18 @@ public function modifyPageContentPostXsl( } global $DIC; + $lng = $DIC['lng']; $ui_factory = $DIC['ui.factory']; $ui_renderer = $DIC['ui.renderer']; - $lng = $DIC['lng']; + $refinery = $DIC['refinery']; return mb_ereg_replace_callback( self::ANSWER_FORM_PLACEHOLDER, fn(array $matches): string => $this->renderAnswerForm( + $lng, $ui_factory, $ui_renderer, - $lng, + $refinery, $this->pg_obj->getQuestion()->getAnswerFormPropertiesByIdString($matches[1]), $this->pg_obj->getAttemptData() ), @@ -175,9 +186,10 @@ public function getAnswerFormIdStringFromAttribute(): string } private function renderAnswerForm( + Language $lng, UIFactory $ui_factory, UIRenderer $ui_renderer, - Language $lng, + Refinery $refinery, ?AnswerFormProperties $answer_form_properties, ?Attempt $attempt_data ): string { @@ -185,14 +197,165 @@ private function renderAnswerForm( return $lng->txt('broken_answer_form'); } - return $ui_renderer->render( - $ui_factory->legacy()->latexContent( - $answer_form_properties->getDefinition()->getParticipantView() - ->show( - $answer_form_properties, - $attempt_data - ) + $template = new \ilTemplate( + 'tpl.qsts_question_presentation.html', + true, + true, + 'components/ILIAS/Questions' + ); + + $question_response = $attempt_data?->getResponseForQuestion( + $answer_form_properties->getQuestionId() + ); + + $answer_form_response = $question_response?->getAnswerFormResponse( + $answer_form_properties->getAnswerFormId() + ); + + $main_content = $this->buildMainContent( + $lng, + $ui_factory, + $answer_form_properties, + $attempt_data, + $answer_form_response + ); + + if (!$this->pg_obj->getShowFeedback()) { + return $this->renderTemplate( + $template, + $ui_renderer->render($main_content) + ); + } + + return $this->renderTemplate( + $template, + $ui_renderer->render( + $this->addFeedbackSubPanelsToContent( + $lng, + $refinery, + $ui_factory, + $answer_form_properties, + $answer_form_response, + [ + $ui_factory->panel()->standard( + $this->buildMainContentLabel($lng), + $main_content + ) + ] + ) + ) + ); + + + } + + private function renderTemplate( + \ilTemplate $template, + string $content + ): string { + $template->setVariable( + self::TEMPLATE_VARIABLE_MAIN, + $content + ); + + return $template->get(); + } + + private function buildMainContentLabel( + Language $lng + ): string { + if ($this->pg_obj->getShowBestResponse()) { + return $lng->txt('best_response'); + } + + return $lng->txt('question'); + } + + private function buildMainContent( + Language $lng, + UIFactory $ui_factory, + ?AnswerFormProperties $answer_form_properties, + ?Attempt $attempt_data, + ?AnswerFormResponse $answer_form_response + ): LegacyContent { + /** @var RequiredCapabilities $required_capabilities */ + $required_capabilities = $this->pg_obj->getRequiredCapabilities(); + $participant_view = $required_capabilities + ->getParticipantViewProvider() + ->getParticipantView($answer_form_properties); + + if ($this->pg_obj->getShowBestResponse()) { + $best_response = $this->pg_obj->getRequiredCapabilities()->getMarking( + $answer_form_properties + )?->getBestResponse( + $answer_form_properties + ); + + return $ui_factory->legacy()->content( + $participant_view->show( + $lng, + $answer_form_properties, + $attempt_data, + $best_response, + ViewMode::ViewBestResponse + ) + ); + } + + return $ui_factory->legacy()->content( + $participant_view->show( + $lng, + $answer_form_properties, + $attempt_data, + $answer_form_response, + $this->pg_obj->getInteractive() + ? ViewMode::Respond + : ViewMode::ViewResponse ) ); } + + private function addFeedbackSubPanelsToContent( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + AnswerFormProperties $answer_form_properties, + AnswerFormResponse $answer_form_response, + array $content + ): array { + $required_capabilities = $this->pg_obj->getRequiredCapabilities(); + return array_reduce( + $required_capabilities->getRequiredFeedbackProviders(), + function ( + array $c, + FeedbackProvider $v + ) use ( + $lng, + $refinery, + $ui_factory, + $answer_form_properties, + $answer_form_response, + $required_capabilities + ): array { + $output = $v->getFeedback( + $answer_form_properties + )->getParticipantOutput( + $lng, + $refinery, + $ui_factory, + $answer_form_properties, + $answer_form_response, + $required_capabilities + ); + + if ($output === null) { + return $c; + } + + $c[] = $output->getUI(); + return $c; + }, + $content + ); + } } diff --git a/components/ILIAS/Questions/Legacy/Temp/Attempt.php b/components/ILIAS/Questions/Legacy/Temp/Attempt.php new file mode 100644 index 000000000000..0c705fd68274 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Temp/Attempt.php @@ -0,0 +1,74 @@ +attempt_id; + } + + public function isQuestionSolved( + Uuid $question_id + ): bool { + return in_array($question_id->toString(), $this->solved_questions); + } + + public function getSolvedQuestionsForStorage(): string + { + return implode(',', $this->solved_questions); + } + + public function withAdditionalSolvedQuestion( + Uuid $question_id + ): self { + if ($this->isQuestionSolved($question_id)) { + return $this; + } + + $clone = clone $this; + $clone->solved_questions[] = $question_id->toString(); + return $clone; + } + + public function withQuestionRemovedFromSolved( + Uuid $question_id + ): self { + if (!$this->isQuestionSolved($question_id)) { + return $this; + } + + $clone = clone $this; + unset( + $clone->solved_questions[array_search($question_id->toString(), $clone->solved_questions)] + ); + return $clone; + } +} diff --git a/components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php b/components/ILIAS/Questions/Legacy/Temp/AttemptRepository.php similarity index 61% rename from components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php rename to components/ILIAS/Questions/Legacy/Temp/AttemptRepository.php index 3a0c146d6c18..31a59b4fe920 100644 --- a/components/ILIAS/Questions/Legacy/Temp/QstsTempAttemptRepository.php +++ b/components/ILIAS/Questions/Legacy/Temp/AttemptRepository.php @@ -18,26 +18,28 @@ declare(strict_types=1); +namespace ILIAS\Questions\Temp; + use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; -class QstsTempAttemptRepository +class AttemptRepository { public const string TABLE_NAME = 'qsts_temp_attempt'; public function __construct( - private readonly ilDBInterface $db, + private readonly \ilDBInterface $db, private readonly UuidFactory $uuid_factory ) { } public function get( int $user_id, - ): ?Uuid { + ): ?Attempt { $value = $this->db->fetchObject( $this->db->queryF( - 'SELECT attempt_id from ' . self::TABLE_NAME . ' WHERE user_id = %s', + 'SELECT attempt_id, solved_questions from ' . self::TABLE_NAME . ' WHERE user_id = %s', [FieldDefinition::T_INTEGER], [$user_id] ) @@ -47,10 +49,15 @@ public function get( return null; } - return $this->uuid_factory->fromString($value->attempt_id); + return new Attempt( + $this->uuid_factory->fromString($value->attempt_id), + $value->solved_questions === '' + ? [] + : explode(',', $value->solved_questions) + ); } - public function store( + public function storeNew( int $user_id, Uuid $attempt_id ): void { @@ -62,4 +69,18 @@ public function store( ] ); } + + public function storeSolved( + Attempt $attempt + ): void { + $this->db->update( + self::TABLE_NAME, + [ + 'solved_questions' => [FieldDefinition::T_TEXT, $attempt->getSolvedQuestionsForStorage()] + ], + [ + 'attempt_id' => [FieldDefinition::T_TEXT, $attempt->getAttemptId()->toString()] + ] + ); + } } diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 412d9e21373f..083e344234cf 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -20,12 +20,11 @@ namespace ILIAS; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Migration as FeedbackMigration; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableDefinitions as FeedbackTableDefinitions; -use ILIAS\Questions\AnswerForm\Capabilities\Migration as CapabilityMigration; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Migration as TextFeedbackMigration; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\TableDefinitions as TextFeedbackTableDefinitions; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Migration as CapabilityMigration; use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\Migration as SuggestedLearningContentMigration; use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\TableDefinitions as SuggestedLearningContentTableDefinitions; -use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationCloze; use ILIAS\Questions\AnswerFormTypes\Cloze\Migration\MigrationLongMenu; @@ -110,8 +109,8 @@ public function init( ); $contribute[CapabilityMigration::class] = static fn() - => new FeedbackMigration( - new FeedbackTableDefinitions( + => new TextFeedbackMigration( + new TextFeedbackTableDefinitions( $internal[PersistenceFactory::class] ) ); diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php new file mode 100644 index 000000000000..09cf3937467f --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php @@ -0,0 +1,35 @@ +getDefinition() ->hasCapability( - Async::class + self::getIdentifier() ); } #[\Override] - public function providesAnswerFormEditAdditionalTab(): bool - { - return false; - } - - #[\Override] - public function getAnswerFormEditAdditionalTab(): null - { - return null; - } - - #[\Override] - public function providesAnswerFormEditAdditionalStep(): bool - { - return false; - } - - #[\Override] - public function getAnswerFormEditAdditionalStep(): null - { - return null; - } - - #[\Override] - public function providesRenderer(): bool - { - return true; - } - - #[\Override] - public function getRenderer( + public function getParticipantView( Properties $answer_form_properties - ): Viewable { + ): Participant { return $answer_form_properties ->getDefinition() - ->getCapability(Async::class) - ->getViewable($answer_form_properties); + ->getCapability(self::getIdentifier()) + ->getParticipantView(); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index 1fb93b446896..caaea9974839 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -21,28 +21,15 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\Presentation\Layout\Viewable; interface Capability { + public static function getIdentifier(): string; + public function isAvailableFor( Properties $answer_form_properties ): bool; - public function providesAnswerFormEditAdditionalTab(): bool; - - public function getAnswerFormEditAdditionalTab(): ?ActionWithTab; - - public function providesAnswerFormEditAdditionalStep(): bool; - - public function getAnswerFormEditAdditionalStep(): ?AdditionalFormStepAction; - - public function providesRenderer(): bool; - - public function getRenderer( - Properties $answer_form_properties - ): ?Viewable; - public function onAnswerFormUpdate( Properties $answer_form_properties ): void; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php new file mode 100644 index 000000000000..f09e3e51cee8 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php @@ -0,0 +1,62 @@ +getDefinition() + ->hasCapability( + self::getIdentifier() + ); + } + + #[\Override] + public function getParticipantView( + Properties $answer_form_properties + ): Participant { + return $answer_form_properties + ->getDefinition() + ->getCapability(self::getIdentifier()) + ->getParticipantView(); + } + + #[\Override] + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): void { + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php new file mode 100644 index 000000000000..28fd75c0ba66 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php @@ -0,0 +1,35 @@ +feedback; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Marking.php new file mode 100644 index 000000000000..192187709fed --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Marking.php @@ -0,0 +1,36 @@ + $available_capabilities + * @param array $available_capabilities + */ + private readonly array $available_capabilities; + + /** + * @param list $available_capabilities */ public function __construct( - private readonly array $available_capabilities + array $available_capabilities ) { + $this->available_capabilities = array_reduce( + $available_capabilities, + function (array $c, Capability $v): array { + $c[$v->getIdentifier()] = $v; + return $c; + }, + [] + ); } + /** + * + * @param list> $capability_identifiers + */ public function get( - string $class - ): ?Capability { - return $this->available_capabilities[$class] ?? null; + array $capability_identifiers + ): RequiredCapabilities { + $required_capabilities = []; + $participant_view_providers = []; + $marking_providers = []; + $required_feedback_providers = []; + $required_actions_with_tab = []; + $required_form_step_actions = []; + + foreach ($capability_identifiers as $capability_identifier) { + if (!isset($this->available_capabilities[$capability_identifier])) { + throw new \InvalidArgumentException( + "The capability {$capability_identifier} does not exist." + ); + } + + $capability = $this->available_capabilities[$capability_identifier]; + $required_capabilities[$capability_identifier] = $capability; + + if ($capability instanceof ParticipantViewProvider) { + $participant_view_providers[] = $capability; + } + + if ($capability instanceof MarkingProvider) { + $marking_providers[] = $capability; + } + + if ($capability instanceof FeedbackProvider) { + $required_feedback_providers[] = $capability; + } + + if ($capability instanceof AdditionalTabProvider) { + $action_with_tab = $capability->getAnswerFormEditAdditionalTab(); + $required_actions_with_tab[$action_with_tab->getIdentifier()] = $action_with_tab; + } + + if ($capability instanceof AdditionalStepProvider) { + $form_step_action = $capability->getAnswerFormEditAdditionalStep(); + $required_form_step_actions[$form_step_action->getIdentifier()] = $form_step_action; + } + } + + if (count($participant_view_providers) !== 1 || count($marking_providers) > 1) { + throw new \InvalidArgumentException( + 'You have to provide exactly one Capability providing a Participant View' + . 'and one Capability providing Marking at most.' + ); + } + + return new RequiredCapabilities( + $required_capabilities, + $participant_view_providers[0], + $required_feedback_providers, + $required_actions_with_tab, + $required_form_step_actions, + $marking_providers[0] ?? null + ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php similarity index 68% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php index 7b721df05ed5..a4772b8c9501 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php @@ -18,16 +18,25 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; +namespace ILIAS\Questions\AnswerForm\Capabilities\MarkingAllowingPartialPoints; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; -use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalFormStepAction; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalStepProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\MarkingProvider; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; -class Capability implements CapabilityInterface +class Capability implements CapabilityInterface, AdditionalStepProvider, MarkingProvider { + #[\Override] + public static function getIdentifier(): string + { + return 'marking_allowing_partial_points'; + } + #[\Override] public function isAvailableFor( Properties $answer_form_properties @@ -36,31 +45,13 @@ public function isAvailableFor( ->getTypeGenericProperties() ->getDefinition() ->hasCapability( - Marking::class + self::getIdentifier() ) && $answer_form_properties ->getTypeGenericProperties() ->getAvailablePoints() !== null; } - #[\Override] - public function providesAnswerFormEditAdditionalTab(): bool - { - return false; - } - - #[\Override] - public function getAnswerFormEditAdditionalTab(): null - { - return null; - } - - #[\Override] - public function providesAnswerFormEditAdditionalStep(): bool - { - return true; - } - #[\Override] public function getAnswerFormEditAdditionalStep(): AdditionalFormStepAction { @@ -70,22 +61,18 @@ public function getAnswerFormEditAdditionalStep(): AdditionalFormStepAction fn(Environment $v): InputsBuilderSession => $v->getAnswerFormProperties() ->getDefinition() - ->getCapability(Marking::class) + ->getCapability(self::getIdentifier()) ->getEditFormInputsBuilder($v) ); } #[\Override] - public function providesRenderer(): bool - { - return false; - } - - #[\Override] - public function getRenderer( + public function getMarking( Properties $answer_form_properties - ): null { - return null; + ): Marking { + return $answer_form_properties + ->getDefinition() + ->getCapability(self::getIdentifier()); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/MarkingAllowingPartialPoints.php similarity index 60% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/MarkingAllowingPartialPoints.php index 4b30fded4799..5b470a3116ee 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Marking/Marking.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/MarkingAllowingPartialPoints.php @@ -18,21 +18,22 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Marking; +namespace ILIAS\Questions\AnswerForm\Capabilities\MarkingAllowingPartialPoints; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Response\Response; -interface Marking +abstract class MarkingAllowingPartialPoints implements Marking, TypeSpecification { - public function getEditFormInputsBuilder( + abstract public function getEditFormInputsBuilder( Environment $environment, ): InputsBuilderSession; - public function addAchievedPointsToResponse( - Response $response - ): Response; - - public function getBestResponse(): Response; + #[\Override] + final public static function getCapabilityIdentifier(): string + { + return Capability::getIdentifier(); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php similarity index 52% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php index 40ec3b7bf1e9..932f294fae7d 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Edit.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php @@ -21,7 +21,11 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\Capabilities\AdditionalFormStepAction; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\ActionWithTab; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalFormStepAction; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\MarkingProvider; +use ILIAS\Questions\AnswerForm\Capabilities\ParticipantViewProvider; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; @@ -29,40 +33,84 @@ use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Viewable; -class Edit +class RequiredCapabilities { - private array $required_capabilites = []; - private array $required_actions_with_tab = []; - private array $required_form_step_actions = []; - + /** + * @param array $capability_class_names + * @param array $required_feedback_views + * @param array $required_actions_with_tab + * @param array $required_step_actions + */ public function __construct( - private readonly Factory $factory + private readonly array $capabilities, + private ParticipantViewProvider $participant_view_provider, + private array $required_feedback_providers, + private array $required_actions_with_tab, + private array $required_form_step_actions, + private ?MarkingProvider $marking_provider ) { + + } + + /** + * @param list $answer_form_properties + */ + public function areAllCapabilitiesSupportedByAnswerForms( + array $answer_form_properties + ): bool { + foreach ($answer_form_properties as $property) { + foreach ($this->capabilities as $capability) { + if (!$capability->isAvailableFor($property)) { + return false; + } + } + } + + return true; } - public function getRequiredCapabilities(): array + public function getParticipantViewProvider(): ParticipantViewProvider { - return $this->required_capabilites; + return $this->participant_view_provider; } - public function withRequiredCapabilities( - array $capability_class_names - ): self { - $clone = clone $this; - [ - 'required_capabilities' => $clone->required_capabilites, - 'required_actions_with_tab' => $clone->required_actions_with_tab, - 'required_form_step_actions' => $clone->required_form_step_actions - ] = $clone->buildRequiredCapabilitiesAndActions( - $capability_class_names - ); - return $clone; + /** + * @return array + */ + public function getRequiredFeedbackProviders(): array + { + return $this->required_feedback_providers; + } + + /** + * @return array + */ + public function getRequiredFormStepActions(): array + { + return $this->required_form_step_actions; + } + + public function isMarkingRequired(): bool + { + return $this->marking_provider !== null; + } + + public function getMarking( + AnswerFormProperties $answer_form_properties + ): ?Marking { + return $this->marking_provider?->getMarking($answer_form_properties); + } + + public function isCapabilityRequired( + string $identifier + ): bool { + return array_key_exists($identifier, $this->capabilities); } public function onAnswerFormUpdate( AnswerFormProperties $properties ): void { - foreach ($this->required_capabilites as $capability) { + foreach ($this->capabilities as $capability) { $capability->onAnswerFormUpdate($properties); } } @@ -77,32 +125,33 @@ public function edit( $this->required_actions_with_tab ); - $action = $this->required_actions_with_tab[$action_from_get] ?? null; - if ($action === null) { - return null; - } + $tab_action = $this->required_actions_with_tab[$action_from_get] ?? null; + if ($tab_action !== null) { + $tab_action->activateTab($tabs_gui); - if ($action !== null) { - $action->activateTab($tabs_gui); - } else { - $environment->setEditAnswerFormBackTarget(); - $action = $this->retrieveNextFormStepFromActionIdentifier( - $edit_view, - $action_from_get + return $tab_action->do( + $environment->withActionParameter($action_from_get) ); } - return $action->do( + $step_action = $this->retrieveNextFormStepFromActionIdentifier( + $edit_view, + $action_from_get + ); + + if ($step_action === null) { + return null; + } + + $environment->setEditAnswerFormBackTarget(); + return $step_action->do( $environment->withActionParameter($action_from_get) ); } - public function providesAnswerFormEditAdditionalSteps(): bool + public function additionalAnswerFormStepsRequired(): bool { - return array_filter( - $this->required_capabilites, - fn(Capability $v): bool => $v->providesAnswerFormEditAdditionalStep() - ) !== []; + return $this->required_form_step_actions !== []; } public function doFirstFormStepAction( @@ -152,42 +201,4 @@ private function retrieveNextFormStepFromActionIdentifier( : $edit_view ); } - - /** - * @param list $capabilities - * @return list<\ILIAS\Questions\AnswerForm\Capabilities\Capability> - */ - private function buildRequiredCapabilitiesAndActions( - array $capabilities - ): array { - return array_reduce( - $capabilities, - function (array $c, string $v): array { - $capability = $this->factory->get($v); - if ($capability === null) { - throw new \InvalidArgumentException( - "The capability {$v} does not exist." - ); - } - $c['required_capabilities'][$v] = $capability; - - $action_with_tab = $capability->getAnswerFormEditAdditionalTab(); - if ($action_with_tab !== null) { - $c['required_actions_with_tab'][$action_with_tab->getIdentifier()] = $action_with_tab; - } - - $form_step_action = $capability->getAnswerFormEditAdditionalStep(); - if ($form_step_action !== null) { - $c['required_form_step_actions'][$form_step_action->getIdentifier()] = $form_step_action; - } - - return $c; - }, - [ - 'required_capabilities' => [], - 'required_actions_with_tab' => [], - 'required_form_step_actions' => [] - ] - ); - } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php similarity index 63% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php index 7e11f6920df8..13467e8a749d 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/SuggestedLearningContent.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php @@ -21,14 +21,23 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; -use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\ActionWithTab; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalTabProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackView; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\StaticURL\Services as StaticURLServices; +use ILIAS\UI\Factory as UIFactory; -class SuggestedLearningContent implements CapabilityInterface +class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider, Feedback { public function __construct( private readonly \ilCtrl $ctrl, @@ -41,15 +50,16 @@ public function __construct( } #[\Override] - public function isAvailableFor( - Properties $answer_form_properties - ): bool { - return true; + public static function getIdentifier(): string + { + return 'suggested_learning_content'; } + #[\Override] - public function providesAnswerFormEditAdditionalTab(): bool - { + public function isAvailableFor( + Properties $answer_form_properties + ): bool { return true; } @@ -64,28 +74,10 @@ public function getAnswerFormEditAdditionalTab(): ActionWithTab } #[\Override] - public function providesAnswerFormEditAdditionalStep(): bool - { - return false; - } - - #[\Override] - public function getAnswerFormEditAdditionalStep(): null - { - return null; - } - - #[\Override] - public function providesRenderer(): bool - { - return false; - } - - #[\Override] - public function getRenderer( + public function getFeedback( Properties $answer_form_properties - ): null { - return null; + ): ?Feedback { + return $this; } #[\Override] @@ -94,6 +86,35 @@ public function onAnswerFormUpdate( ): void { } + #[\Override] + public function getParticipantOutput( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + Properties $properties, + ?Response $response, + RequiredCapabilities $required_capabilities + ): ?FeedbackView { + $content = $this->repository->getFor( + $properties->getAnswerFormId() + )->getContentForPresentation( + $lng, + $this->ctrl, + $this->static_url, + $ui_factory + ); + if ($content === null) { + return null; + } + + return new FeedbackView( + $ui_factory->panel()->standard( + $lng->txt('suggested_learning_content'), + $content + ) + ); + } + private function buildDoEditActionClosure(): \Closure { return function ( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php index 1fe94cce976c..60b40e44bdd0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php @@ -23,10 +23,12 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\StaticURL\Services as StaticURLServices; use ILIAS\UI\Component\Link\Standard as StandardLink; +use ILIAS\UI\Factory as UIFactory; class Content { @@ -105,15 +107,17 @@ public function withFileInfo( } public function getContentForPresentation( + Language $lng, \ilCtrl $ctrl, StaticURLServices $static_url, - Environment $environment + UIFactory $ui_factory ): ?StandardLink { return $this->type->present( + $lng, $ctrl, $static_url, $this->irss, - $environment, + $ui_factory, $this->file_title, $this->rid, $this->target_ref_id, @@ -132,9 +136,10 @@ public function getListing( $lng->txt('type') => $this->type->getTranslatedOptionName($lng), $lng->txt('content') => $this ->getContentForPresentation( + $environment->getLanguage(), $ctrl, $static_url, - $environment + $environment->getUIFactory() ) ?? '' ]; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php index 89c0147f5361..86036dc8c9e4 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent; -use ILIAS\Questions\AnswerForm\Capabilities\Migration as MigrationInterface; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Migration as MigrationInterface; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerForm\Migration\SanitizeLegacyText; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php index fa3068ef0410..2311c9f4eed6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/NodeRetrieval.php @@ -68,10 +68,10 @@ public function __construct( self::PARAMETER_ID_STRING_NODE ); - $this->requested_types = array_merge( - self::CONTAINER_CONTENT_TYPES, - [$requested_type] - ); + $this->requested_types = [ + ...self::CONTAINER_CONTENT_TYPES, + $requested_type + ]; } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php index f98a653a7fac..75206e73a8c0 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Overview.php @@ -275,13 +275,8 @@ private function buildTypeSelect(): Select { return $this->environment->getUIFactory()->input()->field()->select( $this->environment->getLanguage()->txt('type'), - array_reduce( - Types::cases(), - function (array $c, Types $v): array { - $c[$v->value] = $v->getTranslatedOptionName($this->environment->getLanguage()); - return $c; - }, - [] + Types::buildOptionsList( + $this->environment->getLanguage() ) )->withAdditionalTransformation( $this->environment->getRefinery()->custom()->transformation( @@ -334,7 +329,7 @@ private function buildPromptShowAsync( $this->environment->getUIFactory()->prompt()->state()->show( $form )->withTitle( - $lng->txt('edit_suggested_solution') + $lng->txt('edit_suggested_learning_content') ) ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Types.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Types.php index b49f9403cf7e..eedd2d1e54ae 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Types.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Types.php @@ -28,10 +28,11 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; +use ILIAS\UI\Factory as UIFactory; enum Types: string { - case None = 'no_content'; + case None = 'none'; case LearningModule = 'lm'; case LearningModuleChapter = 'st'; case LearningModulePage = 'pg'; @@ -47,20 +48,35 @@ public function getTranslatedOptionName( }; } + public static function buildOptionsList( + Language $lng + ): array { + return array_reduce( + self::cases(), + function (array $c, Types $v) use ($lng): array { + if ($v === Types::None) { + return $c; + } + $c[$v->value] = $v->getTranslatedOptionName($lng); + return $c; + }, + [] + ); + } + public function present( + Language $lng, \ilCtrl $ctrl, StaticURLServices $static_url, IRSS $irss, - Environment $environment, + UIFactory $ui_factory, string $file_title, ?ResourceIdentification $rid, ?int $target_ref_id, ?int $sub_object_id ): ?StandardLink { - $lf = $environment->getUIFactory()->link(); - $lng = $environment->getLanguage(); return match($this) { - self::File => $lf->standard( + self::File => $ui_factory->link()->standard( $file_title === '' ? $irss->manage()->getResource($rid)->getCurrentRevision()->getTitle() : $file_title, @@ -69,18 +85,17 @@ public function present( self::LearningModule => $this->buildLinkToLearningModule( $ctrl, $lng, - $lf, + $ui_factory->link(), $target_ref_id ), self::LearningModulePage, self::LearningModuleChapter, - self::GlossaryTerm => $lf->standard( - $lng->txt('show'), - $static_url->builder()->build( - $this->value, - null, - [$sub_object_id] - )->__toString() + self::GlossaryTerm => $this->buildLinkToSubObject( + $lng, + $ui_factory->link(), + $static_url, + $target_ref_id, + $sub_object_id ), self::None => null }; @@ -173,8 +188,9 @@ private function buildLinkToLearningModule( 'ref_id', $target_ref_id ); + $link = $link_factory->standard( - $lng->txt('show'), + "{$this->getTranslatedOptionName($lng)}: {$this->lookupObjectTitle($target_ref_id)}", $ctrl->getLinkTargetByClass( [ \ilLMPresentationGUI::class @@ -188,6 +204,40 @@ private function buildLinkToLearningModule( return $link; } + private function buildLinkToSubObject( + Language $lng, + LinkFactory $link_factory, + StaticURLServices $static_url, + int $target_ref_id, + int $sub_object_id + ): StandardLink { + $sub_object_title = match($this) { + self::GlossaryTerm => ( + new \ilGlossaryTerm($sub_object_id) + )->getTerm(), + default => \ilLMObject::_lookupTitle( + $sub_object_id + ) + }; + + return $link_factory->standard( + "{$this->getTranslatedOptionName($lng)}: {$this->lookupObjectTitle($target_ref_id)} - {$sub_object_title}", + $static_url->builder()->build( + $this->value, + null, + [$sub_object_id] + )->__toString() + ); + } + + private function lookupObjectTitle( + int $target_ref_id + ): string { + return \ilObject::_lookupTitle( + \ilObject::_lookupObjId($target_ref_id) + ); + } + private function buildUploadFileInput( Repository $repository, Environment $environment, @@ -247,7 +297,7 @@ private function buildSelectObjectInput( $node_retrieval->buildValidNodeConstraint() ) ], - $environment->getLanguage()->txt('select_object') + $this->getTranslatedOptionName($lng) )->withAdditionalTransformation( $environment->getRefinery()->custom()->transformation( fn(array $vs): Content => $repository->getNew( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php similarity index 79% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php index b9c54288436b..a992fc6b53e1 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php @@ -18,17 +18,20 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; -use ILIAS\Questions\AnswerForm\Capabilities\ActionWithTab; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\ActionWithTab; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalTabProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Text\Factory as TextFactory; -class Capability implements CapabilityInterface +class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider { private const string SUB_ACTION_SAVE = 's'; private const string SUB_ACTION_INSERT_LEGACY_TEXTS = 'ilt'; @@ -39,6 +42,13 @@ public function __construct( ) { } + #[\Override] + public static function getIdentifier(): string + { + return 'text_feedback'; + } + + #[\Override] public function isAvailableFor( Properties $answer_form_properties @@ -47,16 +57,10 @@ public function isAvailableFor( ->getTypeGenericProperties() ->getDefinition() ->hasCapability( - Feedback::class + self::getIdentifier() ); } - #[\Override] - public function providesAnswerFormEditAdditionalTab(): bool - { - return true; - } - #[\Override] public function getAnswerFormEditAdditionalTab(): ActionWithTab { @@ -68,15 +72,15 @@ public function getAnswerFormEditAdditionalTab(): ActionWithTab } #[\Override] - public function providesAnswerFormEditAdditionalStep(): bool - { - return false; - } - - #[\Override] - public function getAnswerFormEditAdditionalStep(): null - { - return null; + public function getFeedback( + Properties $answer_form_properties + ): ?Feedback { + return $answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability( + self::getIdentifier() + ); } #[\Override] @@ -90,26 +94,13 @@ public function onAnswerFormUpdate( $answer_form_properties ->getTypeGenericProperties() ->getDefinition() - ->getCapability(Feedback::class) + ->getCapability(self::getIdentifier()) )->onAnswerFormUpdate( $answer_form_properties ) ); } - #[\Override] - public function providesRenderer(): bool - { - return false; - } - - #[\Override] - public function getRenderer( - Properties $answer_form_properties - ): null { - return null; - } - private function buildDoEditActionClosure(): \Closure { return function ( @@ -118,10 +109,10 @@ private function buildDoEditActionClosure(): \Closure $sub_action = $environment->getSubAction(); return match ($sub_action) { '' => $this->buildOverview($environment), - self::STEP_INSERT_LEGACY_TEXTS => $this->buildOverviewWithLegacyTexts( + self::SUB_ACTION_INSERT_LEGACY_TEXTS => $this->buildOverviewWithLegacyTexts( $environment ), - self::STEP_SAVE => $this->save($environment), + self::SUB_ACTION_SAVE => $this->save($environment), default => $this->buildOverview($environment)->doAction( $this->repository, $sub_action @@ -142,7 +133,7 @@ private function buildOverview( ->getAnswerFormProperties() ->getTypeGenericProperties() ->getDefinition() - ->getCapability(Feedback::class) + ->getCapability(self::getIdentifier()) ), $environment->withSubActionParameter( self::SUB_ACTION_SAVE diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php similarity index 98% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php index 011b9b9b6e38..68731851e89f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php @@ -18,9 +18,9 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; -use ILIAS\Questions\AnswerForm\Capabilities\Migration as MigrationInterface; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Migration as MigrationInterface; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerForm\Migration\SanitizeLegacyText; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php similarity index 85% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php index 5822908e0d99..be0a04297744 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -40,7 +40,7 @@ class Overview implements Viewable public function __construct( private readonly Environment $environment, private readonly TextFactory $text_factory, - private Feedback $feedback, + private TextFeedback $feedback, private readonly URLBuilder $save_uri, private readonly URLBuilder $insert_legacy_texts_uri ) { @@ -68,7 +68,10 @@ public function getUI(): array ]); } - $content[] = $this->form ?? $this->buildForm(); + $form = $this->form ?? $this->buildForm(); + if ($form !== null) { + $content[] = $form; + } if ($this->specific_feedback_table !== null) { $modal = $this->specific_feedback_table->getCreateModal( @@ -101,12 +104,12 @@ public function withLegacyTextsAsValues( return $clone; } - public function processForm(): Feedback|StandardForm + public function processForm(): TextFeedback|StandardForm { - $this->form = $this->buildForm()->withRequest( + $this->form = $this->buildForm()?->withRequest( $this->environment->getHttpServices()->request() ); - $data = $this->form->getData(); + $data = $this->form?->getData(); return $data === null ? $this @@ -141,42 +144,19 @@ public function doAction( return $clone; } - private function buildForm(): StandardForm + private function buildForm(): ?StandardForm { $if = $this->environment->getUIFactory()->input(); $lng = $this->environment->getLanguage(); - $inputs = [ - 'generic_feedback' => $if->field()->section( - [ - 'max_points' => $if->field()->markdown( - new \ilUIMarkdownPreviewGUI(), - $lng->txt('edit_feedback_max_points') - )->withValue( - $this->set_legacy_texts_as_values - ? $this->feedback->getFeedbackBestResponseLegacy() - : $this->feedback->getFeedbackBestResponse() - ?->getRawRepresentation() ?? '' - ), - 'not_max_points' => $if->field()->markdown( - new \ilUIMarkdownPreviewGUI(), - $lng->txt('edit_feedback_not_max_points') - )->withValue( - $this->set_legacy_texts_as_values - ? $this->feedback->getFeedbackOtherResponseLegacy() - : $this->feedback->getFeedbackOtherResponse() - ?->getRawRepresentation() ?? '' - ), - ], - $lng->txt('edit_generic_feedback') - ) - ]; + $inputs = $this->buildGenericFormInputs(); $additional_inputs = $this->feedback->getAdditionalInputs( $lng, $this->environment->getUIFactory(), $this->set_legacy_texts_as_values ); + if ($additional_inputs !== null) { $inputs['specific_feedback'] = $if->field()->section( $additional_inputs, @@ -184,6 +164,10 @@ private function buildForm(): StandardForm ); } + if ($inputs === []) { + return null; + } + return $if->container()->form()->standard( $this->save_uri->buildURI()->__toString(), [ @@ -191,7 +175,7 @@ private function buildForm(): StandardForm $inputs )->withAdditionalTransformation( $this->environment->getRefinery()->custom()->transformation( - fn(array $vs): Feedback => ($vs['specific_feedback'] ?? $this->feedback) + fn(array $vs): TextFeedback => ($vs['specific_feedback'] ?? $this->feedback) ->withFeedbackBestResponse( $this->text_factory->markdown($vs['generic_feedback']['max_points']) )->withFeedbackOtherResponse( @@ -202,4 +186,40 @@ private function buildForm(): StandardForm ] ); } + + private function buildGenericFormInputs(): array + { + if (!$this->environment->isMarkingRequired()) { + return []; + } + + $if = $this->environment->getUIFactory()->input(); + $lng = $this->environment->getLanguage(); + + return [ + 'generic_feedback' => $if->field()->section( + [ + 'max_points' => $if->field()->markdown( + new \ilUIMarkdownPreviewGUI(), + Types::BestResponse->getTranslatedOptionName($lng) + )->withValue( + $this->set_legacy_texts_as_values + ? $this->feedback->getFeedbackBestResponseLegacy() + : $this->feedback->getFeedbackBestResponse() + ?->getRawRepresentation() ?? '' + ), + 'not_max_points' => $if->field()->markdown( + new \ilUIMarkdownPreviewGUI(), + Types::OtherResponse->getTranslatedOptionName($lng) + )->withValue( + $this->set_legacy_texts_as_values + ? $this->feedback->getFeedbackOtherResponseLegacy() + : $this->feedback->getFeedbackOtherResponse() + ?->getRawRepresentation() ?? '' + ), + ], + $lng->txt('edit_generic_feedback') + ) + ]; + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/OverviewTable.php similarity index 86% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/OverviewTable.php index 75c90a131f35..e0fb1a05607a 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/OverviewTable.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -33,12 +33,12 @@ public function getCreateModal( public function getTable( Environment $environment, - Feedback $feedback + TextFeedback $feedback ): DataTable; public function doAction( Environment $environment, - Feedback $feedback, + TextFeedback $feedback, string $action - ): Async|RoundTripModal|Feedback; + ): Async|RoundTripModal|TextFeedback; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php similarity index 95% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php index ac8e95d2c8da..80432745ed70 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; @@ -54,8 +54,8 @@ public function __construct( public function getFor( Uuid $answer_form_id, - Feedback $feedback - ): Feedback { + TextFeedback $feedback + ): TextFeedback { $database_values = $this ->buildQuery() ->withAdditionalWhere( @@ -86,7 +86,7 @@ public function getFor( TableTypes::FeedbackGeneric ), $this->refinery->custom()->transformation( - fn(array $vs): Feedback => $feedback->withGenericFeedbackFromDatabase( + fn(array $vs): TextFeedback => $feedback->withGenericFeedbackFromDatabase( $this->text_factory, $vs ) @@ -108,7 +108,7 @@ public function getFor( public function store( Uuid $answer_form_id, - Feedback $feedback + TextFeedback $feedback ): void { $feedback->toStorage( $this->persistence_factory, diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php similarity index 93% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php index d7a84116ca3c..54597df7cb68 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/SpecificFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php @@ -18,14 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; -class SpecificFeedback +class SpecificTextFeedback { public function __construct( private readonly Uuid $id, @@ -70,7 +70,7 @@ public function toStorage( ): array { if ($this->feedback_text === null) { throw new \UnexpectedValueException( - 'You cannot save a SpecificFeedback without a feedback text!' + 'You cannot save a SpecificTextFeedback without a feedback text!' ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableDefinitions.php similarity index 98% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableDefinitions.php index 0abe788dd282..10453c206f7c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableDefinitions.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableTypes.php similarity index 80% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableTypes.php index 56af7fc6fc4a..2d9b54ebf5da 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/TableTypes.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TableTypes.php @@ -18,12 +18,12 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; use ILIAS\Questions\Persistence\TableTypes as TableTypesInterface; enum TableTypes: string implements TableTypesInterface { - case FeedbackGeneric = 'feedback_generic'; - case FeedbackSpecific = 'feedback_specific'; + case FeedbackGeneric = 'text_feedback_generic'; + case FeedbackSpecific = 'text_feedback_specific'; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php similarity index 67% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php index f01ba83be273..c24bfaf3c9bc 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php @@ -18,9 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Feedback; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackView; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; +use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; @@ -28,22 +33,26 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\Value; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Question\Response; use ILIAS\Data\Text\Factory as TextFactory; use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Component\Component; use ILIAS\UI\Factory as UIFactory; -abstract class Feedback +abstract class TextFeedback implements TypeSpecification, Feedback { private ?Markdown $feedback_best_response = null; private string $feedback_best_response_legacy = ''; private ?Markdown $feedback_other_response = null; private string $feedback_other_response_legacy = ''; + /** + * @var array + */ private array $specific_feedbacks = []; abstract public function getAdditionalInputs( @@ -56,17 +65,28 @@ abstract public function getSpecificFeedbackTable( Environment $environment ): ?OverviewTable; - abstract public function getSpecificFeedbackParticipantOutput( - Response $response, - string $answer_id - ): array; - abstract public function specificFeedbackInputsHaveLegacyTexts(): bool; abstract public function onAnswerFormUpdate( Properties $answer_form_properties ): static; + /** + * @return list + */ + abstract protected function getSpecificFeedbackParticipantOutput( + UIFactory $ui_factory, + Refinery $refinery, + Properties $properties, + Response $response + ): array; + + #[\Override] + final public static function getCapabilityIdentifier(): string + { + return Capability::getIdentifier(); + } + public function withGenericFeedbackFromDatabase( TextFactory $text_factory, ?array $database_data @@ -75,13 +95,18 @@ public function withGenericFeedbackFromDatabase( return $this; } $clone = clone $this; - $clone->feedback_best_response = $text_factory->markdown( - $database_data[0]['feedback_best_response'] - ); + if ($database_data[0]['feedback_best_response'] !== '') { + $clone->feedback_best_response = $text_factory->markdown( + $database_data[0]['feedback_best_response'] + ); + } $clone->feedback_best_response_legacy = $database_data[0]['feedback_best_response_legacy']; - $clone->feedback_other_response = $text_factory->markdown( - $database_data[0]['feedback_other_response'] - ); + + if ($database_data[0]['feedback_other_response'] !== '') { + $clone->feedback_other_response = $text_factory->markdown( + $database_data[0]['feedback_other_response'] + ); + } $clone->feedback_other_response_legacy = $database_data[0]['feedback_other_response_legacy']; return $clone; } @@ -98,7 +123,7 @@ public function withSpecificFeedbackFromDatabase( $clone = clone $this; foreach ($database_data as $feedback_data) { - $feedback = new SpecificFeedback( + $feedback = new SpecificTextFeedback( $uuid_factory->fromString($feedback_data['id']), $uuid_factory->fromString($feedback_data['answer_form_id']), $uuid_factory->fromString($feedback_data['parent_id']), @@ -158,8 +183,8 @@ public function hasLegacyTexts(): bool public function getSpecificFeedbackForId( Uuid $id - ): ?SpecificFeedback { - return $this->specific_feedbacks[$id->toString()]; + ): ?SpecificTextFeedback { + return $this->specific_feedbacks[$id->toString()] ?? null; } public function getSpecificFeedbackForConditionOrNew( @@ -167,16 +192,16 @@ public function getSpecificFeedbackForConditionOrNew( Uuid $answer_form_id, Uuid $parent_id, string $condition - ): SpecificFeedback { + ): SpecificTextFeedback { $feedback = array_filter( $this->specific_feedbacks, - fn(SpecificFeedback $v): bool => $v->getParentId()->toString() === $parent_id->toString() + fn(SpecificTextFeedback $v): bool => $v->getParentId()->toString() === $parent_id->toString() && $v->getCondition() === $condition ); return $feedback !== [] ? current($feedback) - : new SpecificFeedback( + : new SpecificTextFeedback( $uuid_factory->uuid4(), $answer_form_id, $parent_id, @@ -184,13 +209,16 @@ public function getSpecificFeedbackForConditionOrNew( ); } + /** + * @var array + */ public function getSpecificFeedbacks(): array { return $this->specific_feedbacks; } public function withSpecificFeedback( - SpecificFeedback $specific_feedback + SpecificTextFeedback $specific_feedback ): static { $clone = clone $this; $clone->specific_feedbacks[$specific_feedback->getId()->toString()] = $specific_feedback; @@ -198,17 +226,62 @@ public function withSpecificFeedback( } public function withoutSpecificFeedback( - SpecificFeedback $specific_feedback + SpecificTextFeedback $specific_feedback ): static { $clone = clone $this; unset($clone->specific_feedbacks[$specific_feedback->getId()->toString()]); return $clone; } - public function getGenericFeedbackParticipantOutput( - Response $response - ): array { + /** + * @return list + */ + public function getParticipantOutput( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + Properties $properties, + ?Response $response, + RequiredCapabilities $required_capabilities + ): ?FeedbackView { + if ($response === null) { + return []; + } + $generic_feedback = []; + if ($required_capabilities->isMarkingRequired()) { + $generic_feedback[] = $response->isBest() + ? $this->getRenderedFeedbackBestResponse( + $lng, + $refinery, + $ui_factory + ) : $this->getRenderedFeedbackOtherResponse( + $lng, + $refinery, + $ui_factory + ); + } + + $feedback = [ + ...$generic_feedback, + ...$this->getSpecificFeedbackParticipantOutput( + $ui_factory, + $refinery, + $properties, + $response + ) + ]; + + if ($feedback === []) { + return null; + } + + return new FeedbackView( + $ui_factory->panel()->standard( + $lng->txt('feedback'), + $feedback + ) + ); } public function toStorage( @@ -319,7 +392,7 @@ private function buildReplaceForSpecificFeedback( ): ?Replace { return array_reduce( $this->specific_feedbacks, - fn(?Replace $c, SpecificFeedback $v) => $c === null + fn(?Replace $c, SpecificTextFeedback $v) => $c === null ? $persistence_factory->replace( $feedback_table_definitions->getColumns( $feedback_table_names_builder, @@ -331,4 +404,50 @@ private function buildReplaceForSpecificFeedback( ) ); } + + private function getRenderedFeedbackBestResponse( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory + ): Component { + if ($this->feedback_best_response === null) { + return $ui_factory->messageBox()->success( + $this->feedback_other_response_legacy === '' + ? $lng->txt('best_response_given') + : $this->feedback_best_response_legacy + ); + } + + $rendered_markdown = $refinery->string()->markdown()->toHTML() + ->transform($this->feedback_best_response->getRawRepresentation()); + + return $ui_factory->messageBox()->success( + $rendered_markdown === '' + ? $lng->txt('best_response_given') + : $rendered_markdown + ); + } + + private function getRenderedFeedbackOtherResponse( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory + ): Component { + if ($this->feedback_other_response === null) { + return $ui_factory->messageBox()->info( + $this->feedback_other_response_legacy === '' + ? $lng->txt('other_response_given') + : $this->feedback_other_response_legacy + ); + } + + $rendered_markdown = $refinery->string()->markdown()->toHTML() + ->transform($this->feedback_other_response->getRawRepresentation()); + + return $ui_factory->messageBox()->info( + $rendered_markdown === '' + ? $lng->txt('orther_response_given') + : $rendered_markdown + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Types.php similarity index 76% rename from components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php rename to components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Types.php index 20f8670e1d6d..c50fcd2e072b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Feedback/Types.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Types.php @@ -18,13 +18,15 @@ declare(strict_types=1); -namespace ILIAS\Questions\AnswerForm\Capabilities\Feedback; +namespace ILIAS\Questions\AnswerForm\Capabilities\TextFeedback; + +use ILIAS\Language\Language; enum Types: string { - case MaxPoints = 'max_points'; - case NotMaxPoints = 'not_max_points'; - case NothingSelected = 'nothing_selected'; + case BestResponse = 'best_response'; + case OtherResponse = 'other_response'; + case NoResponse = 'no_response'; public function getTranslatedOptionName( Language $lng diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TypeSpecification.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TypeSpecification.php new file mode 100644 index 000000000000..f6da8d139020 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TypeSpecification.php @@ -0,0 +1,26 @@ +autoFinalize = false; $config->set( 'URI.AllowedSchemes', - array_merge( - $config->get('URI.AllowedSchemes'), - ['data' => true] - ) + [ + ...$config->get('URI.AllowedSchemes'), + 'data' => true + ] ); $config->autoFinalize = true; if (($def = $config->maybeGetRawHTMLDefinition()) !== null) { diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index cf058f4c21df..8075edaf1fc6 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -23,7 +23,6 @@ use ILIAS\Questions\Persistence\Storable; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; -use ILIAS\Language\Language; use ILIAS\UI\Component\Table\Data as DataTable; use ILIAS\UI\Component\Table\Ordering as OrderingTable; @@ -38,7 +37,7 @@ public function getDefinition(): ?Definition; public function getTypeGenericProperties(): TypeGenericProperties; public function getBasicPropertiesForListing( - Language $lng + Environment $lng ): array; public function getOverviewTable( diff --git a/components/ILIAS/Questions/src/AnswerForm/Response.php b/components/ILIAS/Questions/src/AnswerForm/Response.php index 222da52ab7f4..7139066f1deb 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Response.php +++ b/components/ILIAS/Questions/src/AnswerForm/Response.php @@ -26,4 +26,8 @@ interface Response extends Storable { public function getAnswerFormId(): Uuid; + + public function isBest(): bool; + + public function toPreviewStorage(): array; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php index 50a512ed65d4..4e597183ebc7 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php @@ -21,15 +21,26 @@ namespace ILIAS\Questions\AnswerForm\Views; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\Attempt\Attempt; -use ILIAS\Questions\Response\Response; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewMode; +use ILIAS\HTTP\Wrapper\RequestWrapper; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; interface Participant { public function show( + Language $lng, Properties $properties, - ?Attempt $attempt_data + ?AdditionalAttemptData $attempt_data, + ?Response $response_data, + ViewMode $view_mode ): string; - public function retrieveResponse(): Response; + public function retrieveResponse( + Uuid $response_id, + Properties $properties, + RequestWrapper $post_wrapper + ): Response; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php deleted file mode 100644 index 85a096facff1..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Async.php +++ /dev/null @@ -1,55 +0,0 @@ -answer_form_properties = $answer_form_properties; - return $clone; - } - - #[\Override] - public function getUI(): array|Component - { - if ($this->answer_form_properties === null) { - throw new UnexpectedValueException( - 'This is an uninitalized Async and cannot be viewed.' - ); - } - return $this->answer_form_properties - ->getDefinition() - ->getCapability( - Async::class - ); - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php new file mode 100644 index 000000000000..0a710cc5a334 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php @@ -0,0 +1,38 @@ +participant_view; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php new file mode 100644 index 000000000000..ec6dac55e68e --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php @@ -0,0 +1,38 @@ +participant_view; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php deleted file mode 100644 index 19d1bfb58499..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Feedback.php +++ /dev/null @@ -1,131 +0,0 @@ -uuid_factory, - $this->text_factory, - new FeedbackOverviewDataRetrieval( - $this->uuid_factory, - $environment->getRefinery(), - $environment->getAnswerFormProperties(), - $this - ) - ); - } - - #[\Override] - public function getSpecificFeedbackParticipantOutput( - Response $response, - string $answer_id - ): array { - - } - - #[\Override] - public function specificFeedbackInputsHaveLegacyTexts(): bool - { - return false; - } - - #[\Override] - public function onAnswerFormUpdate( - Properties $answer_form_properties - ): static { - $gaps = $answer_form_properties->getGaps(); - - return array_reduce( - $this->getSpecificFeedbacks(), - function ( - Feedback $c, - SpecificFeedback $v - ) use ($gaps): self { - $gap = $gaps->getGapById($v->getParentId()); - if ($gap === null - || !$gap->getType()->isValidFeedbackCondition( - $this->uuid_factory, - $gap, - $v->getCondition() - ) - ) { - return $c->withoutSpecificFeedback($v); - } - - return $c; - }, - clone $this - ); - } - - public function buildSpecificFeedbackOverviewTableArray(): array - { - return array_reduce( - $this->getSpecificFeedbacks(), - function (array $c, SpecificFeedback $v): array { - $gap_id = $v->getParentId()->toString(); - $key = md5($v->getFeedbackText()->getRawRepresentation()); - if (!array_key_exists($gap_id, $c)) { - $c[$gap_id] = []; - } - if (!array_key_exists($key, $c[$gap_id])) { - $c[$gap_id][$key] = []; - } - - $c[$gap_id][$key][] = $v; - return $c; - }, - [] - ); - } -} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php similarity index 70% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php index 56b6ad938080..6f84e1d2fd8c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/Marking.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php @@ -20,17 +20,20 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking as MarkingInterface; +use ILIAS\Questions\AnswerForm\Capabilities\MarkingAllowingPartialPoints\MarkingAllowingPartialPoints as MarkingAllowingPartialPointsInterface; +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; -use ILIAS\Questions\Response\Response; use ILIAS\UI\Component\Input\Field\Section; -class Marking implements MarkingInterface +class MarkingAllowingPartialPoints extends MarkingAllowingPartialPointsInterface { public function __construct( - private readonly PropertiesFactory $properties_factory + private readonly PropertiesFactory $properties_factory, + private readonly ResponseFactory $response_factory ) { } @@ -49,7 +52,7 @@ function (?string $carry) use ($environment): Section { return $properties_from_carry->getGaps() ->buildPointInputs( $environment->getLanguage(), - $environment->getUIFactory()->input()->field(), + $environment->getUIFactory(), $properties_from_carry, $environment->isInCreationContext(), $environment->getTableRowIds() @@ -60,15 +63,17 @@ function (?string $carry) use ($environment): Section { } #[\Override] - public function addAchievedPointsToResponse( + public function calculateAwardedPoints( + Properties $properties, Response $response - ): Response { - + ): float { + return $response->calculateAwardedPoints($properties); } #[\Override] - public function getBestResponse(): Response - { - + public function getBestResponse( + Properties $properties + ): Response { + return $this->response_factory->getBestResponse($properties); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php new file mode 100644 index 000000000000..a6f7711085ec --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php @@ -0,0 +1,228 @@ +uuid_factory, + $this->text_factory, + new TextFeedbackOverviewDataRetrieval( + $environment->getLanguage(), + $environment->getRefinery(), + $this->uuid_factory, + $environment->getAnswerFormProperties(), + $this + ) + ); + } + + #[\Override] + public function getSpecificFeedbackParticipantOutput( + UIFactory $ui_factory, + Refinery $refinery, + Properties $properties, + ?Response $response + ): array { + $specific_feedbacks = $this->getSpecificFeedbacks(); + if ($response === null || $specific_feedbacks === []) { + return []; + } + + /** @var Gaps $gaps */ + $gaps = $properties->getGaps(); + + return array_reduce( + $this->orderFeedbacksByAnswerInputId($specific_feedbacks), + function ( + array $c, + array $v + ) use ( + $ui_factory, + $refinery, + $gaps, + $response + ): array { + $gap_id = $v[0]->getAnswerInputId(); + + /** @var ?AnswerInputResponse $answer_for_gap */ + $answer_for_gap = $response?->getResponseForInput($gap_id); + + if ($answer_for_gap === null) { + return $this->addFeedbackForNoSelectionToParticipantOutput( + $ui_factory, + $refinery, + $v, + $c + ); + } + + $gap = $gaps->getGapById($gap_id); + $output = $gap->getType()->getSpecificFeedbackParticipantOutput( + $ui_factory, + $gap, + $v, + $answer_for_gap + ); + + if ($output !== null) { + $c[] = $output; + } + + return $c; + }, + [] + ); + } + + #[\Override] + public function specificFeedbackInputsHaveLegacyTexts(): bool + { + return false; + } + + #[\Override] + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): static { + $gaps = $answer_form_properties->getGaps(); + + return array_reduce( + $this->getSpecificFeedbacks(), + function ( + TextFeedback $c, + SpecificTextFeedback $v + ) use ($gaps): self { + $gap = $gaps->getGapById($v->getParentId()); + if ($gap === null + || !$gap->getType()->isValidFeedbackCondition( + $this->uuid_factory, + $gap, + $v->getCondition() + ) + ) { + return $c->withoutSpecificFeedback($v); + } + + return $c; + }, + clone $this + ); + } + + public function buildSpecificFeedbackOverviewTableArray(): array + { + return array_reduce( + $this->getSpecificFeedbacks(), + function (array $c, SpecificTextFeedback $v): array { + $gap_id = $v->getParentId()->toString(); + $key = md5($v->getFeedbackText()->getRawRepresentation()); + if (!array_key_exists($gap_id, $c)) { + $c[$gap_id] = []; + } + if (!array_key_exists($key, $c[$gap_id])) { + $c[$gap_id][$key] = []; + } + + $c[$gap_id][$key][] = $v; + return $c; + }, + [] + ); + } + + private function orderFeedbacksByAnswerInputId( + array $specific_feedbacks + ): array { + return array_reduce( + $specific_feedbacks, + function (array $c, SpecificTextFeedback $v): array { + $parent_id = $v->getParentId()->toString(); + if (!array_key_exists($parent_id, $c)) { + $c[$parent_id] = []; + } + + $c[$parent_id][] = $v; + return $c; + }, + [] + ); + } + + private function addFeedbackForNoSelectionToParticipantOutput( + UIFactory $ui_factory, + Refinery $refinery, + array $specific_feedbacks, + array $participant_output + ): array { + $feedback_nothing_selected = array_filter( + $specific_feedbacks, + fn(SpecificFeedback $v): bool => TextFeedbackTypes::tryFrom($v->getCondition()) === TextFeedbackTypes::NoResponse + ); + + if ($feedback_nothing_selected !== []) { + $participant_output[] = $ui_factory->legacy()->content( + $refinery->string()->markdown()->toHTML() + ->transform( + $feedback_nothing_selected[0]->getFeedbackText() + ->getRawRepresentation() + ) + ); + } + + return $participant_output; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php similarity index 89% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php index 0b57c2679a79..4608f3c82f52 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewDataRetrieval.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php @@ -20,30 +20,32 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\SpecificFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\SpecificTextFeedback; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Data\Order; use ILIAS\Data\Range; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Language\Language; use ILIAS\UI\Component\Table\DataRetrieval; use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; -class FeedbackOverviewDataRetrieval implements DataRetrieval +class TextFeedbackOverviewDataRetrieval implements DataRetrieval { /** * @var array{ - * string: array<\ILIAS\Questions\AnswerForm\Capabilities\Feedback\SpecificFeedback> + * string: array * } */ private readonly array $feedback; public function __construct( - private readonly UuidFactory $uuid_factory, + private readonly Language $lng, private readonly Refinery $refinery, + private readonly UuidFactory $uuid_factory, private readonly Properties $answer_form_properties, - Feedback $feedback + TextFeedback $feedback ) { $this->feedback = $feedback->buildSpecificFeedbackOverviewTableArray(); } @@ -86,7 +88,7 @@ public function getSpecificFeedbacksForRowId( ); return array_filter( $feedbacks_for_gap, - fn(SpecificFeedback $v): bool => in_array($v->getId(), $ids) + fn(SpecificTextFeedback $v): bool => in_array($v->getId(), $ids) ); } @@ -124,7 +126,7 @@ private function buildTableRow( implode('_', $keys), [ 'gap' => $gap->buildShortenedGapRepresentation(), - 'answer_option' => implode('
', $answer_options), + 'answer_options' => implode('
', $answer_options), 'feedback' => $this->refinery->string()->markdown()->toHTML()->transform( $feedbacks[0]->getFeedbackText() ->getRawRepresentation() diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewTable.php similarity index 90% rename from components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php rename to components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewTable.php index 2f5fb13cf000..da5478a2b0da 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/FeedbackOverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewTable.php @@ -20,10 +20,9 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Feedback; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\OverviewTable; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\SpecificFeedback; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\TextFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\OverviewTable; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\SpecificTextFeedback; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -36,13 +35,12 @@ use ILIAS\Refinery\Custom\Transformation as CustomTransformation; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Modal\InterruptiveItem\InterruptiveItem; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; use ILIAS\UI\Component\Table\Column\Factory as ColumnFactory; use ILIAS\UI\Component\Table\Data as DataTable; use ILIAS\UI\Factory as UIFactory; -class FeedbackOverviewTable implements OverviewTable +class TextFeedbackOverviewTable implements OverviewTable { private const string SUB_ACTION_ENTER_FEEDBACK = 'ef'; private const string SUB_ACTION_EDIT_FEEDBACK = 'edf'; @@ -53,7 +51,7 @@ class FeedbackOverviewTable implements OverviewTable public function __construct( private readonly UuidFactory $uuid_factory, private readonly TextFactory $text_factory, - private readonly FeedbackOverviewDataRetrieval $data_retrieval + private readonly TextFeedbackOverviewDataRetrieval $data_retrieval ) { } @@ -88,7 +86,7 @@ public function getCreateModal( #[\Override] public function getTable( Environment $environment, - Feedback $feedback + TextFeedback $feedback ): DataTable { return $environment->getUIFactory()->table()->data( $this->data_retrieval, @@ -107,9 +105,9 @@ public function getTable( #[\Override] public function doAction( Environment $environment, - Feedback $feedback, + TextFeedback $feedback, string $action - ): Async|RoundTripModal|Feedback { + ): Async|RoundTripModal|TextFeedback { return match($action) { self::SUB_ACTION_ENTER_FEEDBACK => $this->processSelectGapModal( $environment, @@ -124,8 +122,7 @@ public function doAction( $feedback ), self::SUB_ACTION_CONFIRM_DELETE_FEEDBACK => $this->confirmDeleteFeedback( - $environment, - $feedback + $environment ), self::SUB_ACTION_DELETE_FEEDBACK => $this->deleteFeedback( $environment, @@ -136,7 +133,7 @@ public function doAction( private function processSelectGapModal( Environment $environment, - Feedback $feedback + TextFeedback $feedback ): RoundTripModal { $create_modal = $this->getCreateModal($environment) ->withRequest($environment->getHttpServices()->request()); @@ -169,7 +166,7 @@ private function buildEnterFeedbackModal( InputsBuilderSession $inputs_builder ): RoundTripModal { return $environment->getUIFactory()->modal()->roundtrip( - $environment->getLanguage()->txt('create_feedback'), + $environment->getLanguage()->txt('edit_feedback'), null, [ 'feedback' => $inputs_builder->getInputs() @@ -182,8 +179,8 @@ private function buildEnterFeedbackModal( private function processEnterFeedbackModal( Environment $environment, - Feedback $feedback - ): RoundTripModal|Feedback { + TextFeedback $feedback + ): RoundTripModal|TextFeedback { $specific_feedbacks = $this->getSpecifcFeedbacksFromQuery( $environment ); @@ -199,7 +196,7 @@ private function processEnterFeedbackModal( $feedback, $gap_id, array_map( - fn(SpecificFeedback $v): string => $v->getCondition(), + fn(SpecificTextFeedback $v): string => $v->getCondition(), $specific_feedbacks ) ); @@ -220,8 +217,8 @@ private function processEnterFeedbackModal( private function editFeedback( Environment $environment, - Feedback $feedback - ): Async|Feedback { + TextFeedback $feedback + ): Async|TextFeedback { $specific_feedbacks = $this->getSpecifcFeedbacksFromQuery( $environment ); @@ -234,7 +231,7 @@ private function editFeedback( $feedback, $specific_feedbacks[0]->getParentId(), array_map( - fn(SpecificFeedback $v): string => $v->getCondition(), + fn(SpecificTextFeedback $v): string => $v->getCondition(), $specific_feedbacks ), $specific_feedbacks[0]->getFeedbackText()->getRawRepresentation() @@ -244,8 +241,7 @@ private function editFeedback( } private function confirmDeleteFeedback( - Environment $environment, - Feedback $feedback + Environment $environment ): Async { $pf = $environment->getPresentationFactory(); $uf = $environment->getUIFactory(); @@ -283,8 +279,8 @@ private function confirmDeleteFeedback( private function deleteFeedback( Environment $environment, - Feedback $feedback - ): RoundTripModal|Feedback { + TextFeedback $feedback + ): RoundTripModal|TextFeedback { $uf = $environment->getUIFactory(); $lng = $environment->getLanguage(); $selected_feedbacks = $environment->getHttpServices()->wrapper()->post() @@ -307,14 +303,14 @@ private function deleteFeedback( return array_reduce( $selected_feedbacks, - fn(Feedback $c, SpecificFeedback $v): Feedback => $c->withoutSpecificFeedback($v), + fn(TextFeedback $c, SpecificTextFeedback $v): TextFeedback => $c->withoutSpecificFeedback($v), $feedback ); } private function buildEnterFeedbackInputBuilder( Environment $environment, - Feedback $feedback, + TextFeedback $feedback, ?Uuid $selected_gap = null, array $selected_conditions = [], string $feedback_text = '' @@ -340,7 +336,7 @@ function ( $environment->getAnswerFormProperties() ->getGaps() ->getGapById($gap_id), - $environment->isCapabilityRequired(Marking::class), + $environment->isMarkingRequired(), $feedback->getSpecificFeedbacks(), $selected_conditions, $feedback_text @@ -397,13 +393,13 @@ private function buildSpecificFeedbackSection( )->withRequired(true) ->withValue($feedback_text) ], - $lng->txt('enter_feedback') + $lng->txt('feedback') ); } private function buildSpecificFeedbackTransformation( Refinery $refinery, - Feedback $feedback, + TextFeedback $feedback, Uuid $answer_form_id, Uuid $gap_id, array $selected_conditions @@ -416,10 +412,10 @@ function ( $answer_form_id, $gap_id, $selected_conditions - ): Feedback { + ): TextFeedback { $feedback_with_removed_specific_feedbacks = array_reduce( array_diff($selected_conditions, $vs['answer_options']), - fn(Feedback $c, string $v): Feedback => $c->withoutSpecificFeedback( + fn(TextFeedback $c, string $v): TextFeedback => $c->withoutSpecificFeedback( $c->getSpecificFeedbackForConditionOrNew( $this->uuid_factory, $answer_form_id, @@ -432,7 +428,7 @@ function ( return array_reduce( $vs['answer_options'], - fn(Feedback $c, string $v): Feedback => $c->withSpecificFeedback( + fn(TextFeedback $c, string $v): TextFeedback => $c->withSpecificFeedback( $c->getSpecificFeedbackForConditionOrNew( $this->uuid_factory, $answer_form_id, @@ -468,8 +464,8 @@ private function getColumns( 'gap' => $column_factory->text( $lng->txt('gap') )->withIsSortable(false), - 'answer_option' => $column_factory->text( - $lng->txt('answer_option') + 'answer_options' => $column_factory->text( + $lng->txt('answer_options') )->withIsSortable(false), 'feedback' => $column_factory->text( $lng->txt('feedback') diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index a6b7ec0077b2..018e1729da13 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze; +use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; use ILIAS\Questions\AnswerForm\Definition as DefinitionInterface; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\TypeGenericProperties; @@ -28,32 +29,44 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerForm as Response; use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Edit; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\AnswerForm\Persistence\TableDefinitions; use ILIAS\Language\Language; +use ILIAS\Data\UUID\Uuid; class Definition implements DefinitionInterface { /** - * @param array $available_capabilities + * @param array $available_capabilities + */ + private readonly array $available_capabilities; + + /** + * @param list $available_capabilities */ public function __construct( private readonly TableDefinitions $table_definitions, - private readonly array $available_capabilities, + array $available_capabilities, private readonly PropertiesFactory $properties_factory, private readonly ResponseFactory $response_factory, - private readonly Edit $edit_view, - private readonly Participant $participant_view + private readonly Edit $edit_view ) { + $this->available_capabilities = array_reduce( + $available_capabilities, + function (array $c, TypeSpecification $v): array { + $c[$v::getCapabilityIdentifier()] = $v; + return $c; + }, + [] + ); } #[\Override] public function getLabel( Language $lng ): string { - return $lng->txt('assClozeTest'); + return $lng->txt('cloze'); } #[\Override] @@ -69,13 +82,30 @@ public function buildProperties( #[\Override] public function buildResponse( + Uuid $response_id, + AnswerFormProperties $answer_form_properties, ?Query $query ): Response { - return $this->response_factory->fromData( + return $this->response_factory->fromQuery( + $response_id, + $answer_form_properties, $query ); } + #[\Override] + public function buildResponseFromPreviewData( + Uuid $response_id, + AnswerFormProperties $answer_form_properties, + array $preview_data + ): Response { + return $this->response_factory->fromPreviewData( + $response_id, + $answer_form_properties, + $preview_data + ); + } + #[\Override] public function getTableDefinitions(): TableDefinitions { @@ -84,16 +114,16 @@ public function getTableDefinitions(): TableDefinitions #[\Override] public function hasCapability( - string $capability_class_name + string $capability_identifier ): bool { - return array_key_exists($capability_class_name, $this->available_capabilities); + return array_key_exists($capability_identifier, $this->available_capabilities); } #[\Override] public function getCapability( - string $capability_class_name + string $capability_identifier ): mixed { - return $this->available_capabilities[$capability_class_name]; + return $this->available_capabilities[$capability_identifier] ?? null; } #[\Override] @@ -109,10 +139,4 @@ public function initializeAttemptData( ): Attempt { return $answer_form_properties->getGaps()->initializeAttemptData($attempt); } - - #[\Override] - public function getParticipantView(): Participant - { - return $this->participant_view; - } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php index aa294087b57a..731f85f2b8ea 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -59,7 +59,11 @@ public function getRows( mixed $additional_parameters ): \Generator { yield from $this->environment->getAnswerFormProperties()->getGaps() - ->toTableRows($row_builder, $this->environment->getLanguage()); + ->toTableRows( + $row_builder, + $this->environment->getLanguage(), + $this->environment->isMarkingRequired() + ); } #[\Override] @@ -76,21 +80,29 @@ private function getColums(): array { $f = $this->environment->getUIFactory()->table()->column(); - return [ + $columns = [ 'gap' => $f->text( $this->environment->getLanguage()->txt('title') )->withIsSortable(false), 'type' => $f->text( - $this->environment->getLanguage()->txt('cloze_type') - )->withIsSortable(false), - 'answers_options_awarding_points' => $f->text( - $this->environment->getLanguage()->txt('answer_options_awarding_points') - )->withIsSortable(false), - 'available_points' => $f->number( - $this->environment->getLanguage()->txt('available_points') - )->withDecimals(4) - ->withIsSortable(false) + $this->environment->getLanguage()->txt('gap_type') + )->withIsSortable(false) ]; + + if ($this->environment->isMarkingRequired()) { + $columns = [ + ...$columns, + 'answers_options_awarding_points' => $f->text( + $this->environment->getLanguage()->txt('answer_options_awarding_points') + )->withIsSortable(false), + 'available_points' => $f->number( + $this->environment->getLanguage()->txt('available_points') + )->withDecimals(4) + ->withIsSortable(false) + ]; + } + + return $columns; } private function getActions(): array diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 9b22bf1dd09b..89046844bc48 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Migration; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Types; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Migration\Migration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; @@ -185,7 +185,7 @@ public function getConditionsForFeedbackFromOldValues( } if ($answer === -1) { - return [Types::NothingSelected->value]; + return [Types::NoResponse->value]; } if ($gap['is_numeric']) { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index 54868bb3e3f7..efbf18568f6a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -53,7 +53,7 @@ public function getInput( new \ilUIMarkdownPreviewGUI(), $lng->txt('cloze_text') )->withMustacheVariables([ - Gap::GAP_PLACEHOLDER_NAME => $lng->txt('insert_a_gap') + Gap::GAP_PLACEHOLDER_NAME => $lng->txt('insert_gap') ])->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(string $v): self => $cloze_text_factory->buildFromTextString($v) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 834cecef96f3..c01acc6e87cc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\MatchingValue; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Delete; @@ -42,7 +43,7 @@ class Combination { /** * @param Uuid $id - * @param array $matching_values + * @param list $matching_values */ public function __construct( private readonly Uuid $id, @@ -258,6 +259,7 @@ public function buildPointsInputs( return $field_factory->section( [ 'values' => $this->buildValuesInputs( + $lng, $field_factory, $properties ), @@ -272,7 +274,7 @@ public function buildPointsInputs( $refinery->custom()->constraint( fn(array $vs): bool => !$properties->getCombinations() ->hasMatchingCombinationForAnswerOptionIds($vs['values']), - $lng->txt('combination_already_exists') + $lng->txt('gap_combination_already_exists') ) )->withAdditionalTransformation( $refinery->custom()->transformation( @@ -304,13 +306,21 @@ public function buildCarryString(): string } private function buildValuesInputs( + Language $lng, FieldFactory $field_factory, Properties $properties ): Group { return $field_factory->group( array_reduce( $this->matching_values, - function (array $c, MatchingValue $v) use ($field_factory, $properties): array { + function ( + array $c, + MatchingValue $v + ) use ( + $field_factory, + $lng, + $properties + ): array { $gap_id = $v->getGap()->getAnswerInputId(); $gap = $properties->getGaps()->getGapById($gap_id); $c[$gap_id->toString()] = $field_factory->select( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php index 2d1302a2c459..39df9a022512 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; @@ -49,7 +48,7 @@ public function addCombinationsSubTab( public function show( Environment $environment ): Async|Viewable|Properties|null { - if (!$environment->isCapabilityRequired(Marking::class)) { + if (!$environment->isMarkingRequired()) { return null; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index e128c0f36b3e..0766f0775148 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -59,7 +59,7 @@ public function getUI(): array $modal = $this->buildSetCombinationGapsModal(); $content = [ $this->environment->getUIFactory()->button()->standard( - $this->environment->getLanguage()->txt('add_combination'), + $this->environment->getLanguage()->txt('add_gap_combination'), $modal->getShowSignal() ), $modal, @@ -112,7 +112,7 @@ private function buildTable(): DataTable { return $this->environment->getUIFactory()->table()->data( $this, - $this->environment->getLanguage()->txt('combinations'), + $this->environment->getLanguage()->txt('gap_combinations'), $this->getColumns() )->withActions($this->getActions()) ->withRequest($this->environment->getHttpServices()->request()); @@ -186,7 +186,7 @@ private function buildSetCombinationGapsModal(): RoundTripModal $properties = $this->environment->getAnswerFormProperties(); $gaps = $properties->getGaps(); return $this->environment->getUIFactory()->modal()->roundtrip( - $this->environment->getLanguage()->txt('add_combination'), + $this->environment->getLanguage()->txt('add_gap_combination'), $properties->getClozeText()->buildPanelForEditing( $this->environment->getUIFactory(), $this->environment->getLanguage(), @@ -195,7 +195,7 @@ private function buildSetCombinationGapsModal(): RoundTripModal ), [ 'combination' => $gaps->buildGapsMultiSelect( - $this->environment->getLanguage()->txt('select_gaps_for_combinations'), + $this->environment->getLanguage()->txt('select_gaps_for_combination'), $this->environment->getUIFactory()->input()->field() )->withRequired(true) ->withAdditionalTransformation( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 83d33d8f4a77..90d61cde1fe7 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -92,6 +92,15 @@ public function getAnswerOptionById( ); } + public function getAnswerOptionByTextValue( + string $text_value + ): ?AnswerOption { + return array_find( + $this->answer_options, + fn(AnswerOption $v): bool => $v->getTextValue() === $text_value + ); + } + public function getAnswerOptionForPositionOrNew( int $position ): AnswerOption { @@ -102,6 +111,17 @@ public function getAnswerOptionForPositionOrNew( ); } + public function getBestAnswerOption(): ?AnswerOption + { + return array_reduce( + $this->answer_options_awarding_points, + fn(?AnswerOption $c, AnswerOption $v): ?AnswerOption + => $v->getAvailablePoints() > $c?->getAvailablePoints() + ? $v + : $c + ); + } + public function getTagsArrayFromAnswerOptions(): array { return array_reduce( @@ -292,14 +312,13 @@ public function buildDelete( $table_type = AnswerFormSpecificTableTypes::AnswerOptions; return $persistence_factory->delete( - $table_type->getTable( - $persistence_factory, - $table_names_builder + $persistence_factory->table( + $table_names_builder, + $table_type ), [ $persistence_factory->where( $table_definitions->getForeignKeyColumn( - $persistence_factory, $table_names_builder, $table_type ), diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 76a32030165d..20d8cdbde5b8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -21,9 +21,11 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; @@ -34,7 +36,7 @@ use ILIAS\FileUpload\FileUpload; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; +use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -53,7 +55,7 @@ class Gap private const string KEY_ANSWER_OPTIONS = 'answer_options'; /** - * @param array $answer_options + * @param list $answer_options */ public function __construct( private readonly Uuid $answer_input_id, @@ -225,7 +227,7 @@ public function withValuesFromCarry( $clone = clone $this; $clone->type = $carry[self::KEY_TYPE] === '' - ? $this->getType() + ? $this->type : $gaps_factory->getGapTypeByIdentifier($carry[self::KEY_TYPE]); $clone->position = $carry[self::KEY_POSITION]; @@ -309,11 +311,15 @@ public function buildGapPlaceholderNameWithId(): string } public function buildParticipantViewLegacyInput( - ?Attempt $attempt_data + Language $lng, + Refinery $refinery, + ?AdditionalAttemptData $attempt_data, + ?Response $response_data ): string { return $this->type->getParticipantViewLegacyInput( $this, - $attempt_data + $attempt_data, + $response_data ); } @@ -322,43 +328,47 @@ public function getEditAnswerOptionsSection( Environment $environment ): Section { $section = $environment->getUIFactory()->input()->field()->section( - $this->getType()->getEditAnswerOptionsInputs( + $this->type->getEditAnswerOptionsInputs( $file_upload, $environment, $this ), - "{$this->buildShortenedGapName()} ({$environment->getLanguage()->txt("{$this->getType()->getIdentifier()}_gap")})" + "{$this->buildShortenedGapName()} ({$environment->getLanguage()->txt("{$this->type->getIdentifier()}_gap")})" ); - $edit_section_constraint = $this->getType()->getEditAnswerOptionsSectionConstraint(); + $edit_section_constraint = $this->type->getEditAnswerOptionsSectionConstraint(); if ($edit_section_constraint !== null) { $section = $section->withAdditionalTransformation($edit_section_constraint); } return $section->withAdditionalTransformation( - $this->getType()->getBuildGapTransformation($this) + $this->type->getBuildGapTransformation( + $this + ) ); } public function getEditPointsSection( Language $lng, - FieldFactory $ff + UIFactory $ui_factory ): Section { - $type = $this->getType(); - $section = $ff->section( - $type->getEditPointsInputs($this->getAnswerOptions()), - "{$this->buildShortenedGapName()} ({$lng->txt("{$type->getIdentifier()}_gap")})" + $section = $ui_factory->input()->field()->section( + $this->type->getEditPointsInputs( + $ui_factory, + $this->getAnswerOptions() + ), + "{$this->buildShortenedGapName()} ({$lng->txt("{$this->type->getIdentifier()}_gap")})" ); - $edit_section_constraint = $type->getEditPointsSectionConstraint(); + $edit_section_constraint = $this->type->getEditPointsSectionConstraint(); if ($edit_section_constraint !== null) { $section = $section->withAdditionalTransformation($edit_section_constraint); } return $section->withAdditionalTransformation( - $type->getAddPointsTransformation($this) + $this->type->getAddPointsTransformation($this) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 5d0a97fe4f25..69370b7ea8ff 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -22,7 +22,10 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerForm as Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; @@ -31,9 +34,12 @@ use ILIAS\Questions\Persistence\Operator; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Definitions\ViewMode; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\FileUpload\FileUpload; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Random\Seed\RandomSeed; @@ -42,6 +48,7 @@ use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Input\Field\Select; use ILIAS\UI\Component\Table\DataRowBuilder; +use ILIAS\UI\Factory as UIFactory; class Gaps { @@ -185,15 +192,35 @@ public function getAddedGaps( return array_diff_key($this->gaps, $old_gaps->gaps); } - public function getPlaceholderArrayForParticipantView( - ?Attempt $attempt_data + public function getPlaceholderArray( + Language $lng, + ViewMode $view_mode, + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): array { return array_reduce( $this->gaps, - function (array $c, Gap $v) use ($attempt_data): array { - $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildParticipantViewLegacyInput( - $attempt_data - ); + function ( + array $c, + Gap $v + ) use ( + $lng, + $view_mode, + $additional_attempt_data, + $response_data + ): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $view_mode === ViewMode::Respond + ? $v->buildParticipantViewLegacyInput( + $lng, + $this->refinery, + $additional_attempt_data, + $response_data + ) : $this->buildStaticGapReplacement( + $lng, + $view_mode, + $response_data, + $v + ); return $c; }, [] @@ -275,7 +302,7 @@ function (array $c, Gap $v) use ($environment, $file_upload): array { }, [] ), - $environment->getLanguage()->txt('add_answer_options') + $environment->getLanguage()->txt('edit_answer_options') )->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(array $vs): Properties => $properties->withGaps( @@ -291,27 +318,27 @@ function (array $c, Gap $v) use ($environment, $file_upload): array { public function buildPointInputs( Language $lng, - FieldFactory $ff, + UIFactory $ui_factory, Properties $properties, bool $is_in_creation_context, array $selected_gaps ): Section { - return $ff->section( + return $ui_factory->input()->field()->section( array_reduce( $this->retrieveGapsForInputs( $is_in_creation_context, $selected_gaps ), - function (array $c, Gap $v) use ($lng, $ff): array { + function (array $c, Gap $v) use ($lng, $ui_factory): array { $c[$v->getAnswerInputId()->toString()] = $v->getEditPointsSection( $lng, - $ff + $ui_factory ); return $c; }, [] ), - $lng->txt('add_points') + $lng->txt('edit_points') )->withAdditionalTransformation( $this->refinery->custom()->transformation( fn(array $vs): Properties => $properties->withGaps( @@ -381,6 +408,22 @@ public function withValuesFromCarry( return $clone; } + /** + * + * @return list + */ + public function getBestResponses(): array + { + return array_filter( + array_map( + fn(Gap $v): AnswerInputResponse => $v->getType()->getBestResponse( + $v + ), + $this->gaps + ) + ); + } + public function initializeAttemptData( Attempt $attempt ): Attempt { @@ -397,6 +440,58 @@ public function initializeAttemptData( ); } + /** + * + * @return list + */ + public function retrieveResponsesFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory + ): array { + return array_map( + fn(Gap $v): AnswerInputResponse => $v->getType() + ->retrieveResponseFromPost( + $post_wrapper, + $uuid_factory, + $v + ), + $this->gaps + ); + } + + /** + * + * @return list + */ + public function retrieveResponsesFromPreviewData( + UuidFactory $uuid_factory, + array $preview_data + ): array { + return array_reduce( + $this->gaps, + function ( + array $c, + Gap $v + ) use ( + $uuid_factory, + $preview_data + ): array { + $response_object = $v->getType()->retrieveResponseFromPreviewData( + $uuid_factory, + $v, + $preview_data[$v->getAnswerInputId()->toString()] ?? [] + ); + + if ($response_object !== null) { + $c[$v->getAnswerInputId()->toString()] = $response_object; + } + + return $c; + }, + [] + ); + } + public function toTableRows( DataRowBuilder $row_builder, Language $lng @@ -573,6 +668,61 @@ function (array $c, Gap $v): array { ); } + private function buildStaticGapReplacement( + Language $lng, + ViewMode $view_mode, + ?Response $response_data, + Gap $gap + ): string { + $static_gap_template = new \ilTemplate( + 'tpl.cloze_gap_static.html', + true, + true, + 'components/ILIAS/Questions' + ); + $static_gap_template->setVariable( + 'SOLUTION_VALUE', + $this->retrieveStaticGapReplacementValue( + $lng, + $view_mode, + $response_data, + $gap + ) + ); + return $static_gap_template->get(); + } + + private function retrieveStaticGapReplacementValue( + Language $lng, + ViewMode $view_mode, + ?Response $response_data, + Gap $gap + ): string { + $empty_gap_text = $lng->txt( + $view_mode === ViewMode::ViewBestResponse + ? 'no_best_response_available' + : 'no_response_given' + ); + + if ($response_data === null) { + return $empty_gap_text; + } + + $response = $response_data->getResponseForInput( + $gap->getAnswerInputId() + ); + + if ($response === null) { + return $empty_gap_text; + } + + if ($response instanceof Uuid) { + return $gap->getAnswerOptions()->getAnswerOptionById($response)->getTextValue(); + } + + return $response; + } + private function retrieveGapsForInputs( bool $is_in_creation_context, array $selected_gaps diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index e71d254cb5c9..f1e8759f11ae 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -20,13 +20,18 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\Upload; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\MimeType; use ILIAS\FileUpload\FileUpload; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; @@ -40,12 +45,11 @@ class LongMenu extends Type private const array ACCEPTED_MIME_TYPES = [MimeType::TEXT__PLAIN]; public function __construct( - Refinery $refinery, Language $lng, - private readonly UIFactory $ui_factory, + Refinery $refinery, private readonly GlobalTemplate $global_tpl ) { - parent::__construct($refinery, $lng); + parent::__construct($lng, $refinery); } #[\Override] @@ -57,23 +61,39 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( Gap $gap, - ?Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): string { - $answer_input_id = $gap->getAnswerInputId()->toString(); + $gap_name = $this->buildGapName($gap); + $gaptemplate = new \ilTemplate( - 'tpl.il_as_qpl_longmenu_question_text_gap.html', + 'tpl.cloze_gap_longmenu.html', true, true, - 'components/ILIAS/TestQuestionPool' + 'components/ILIAS/Questions' ); $gaptemplate->setVariable( - 'KEY', - $answer_input_id + 'GAP_NAME', + $gap_name ); + $response = $response_data?->getResponseForInput($gap->getAnswerInputId()); + if ($response !== null) { + $gaptemplate->setVariable( + 'VALUE', + htmlentities( + $response instanceof Uuid + ? $gap->getAnswerOptions() + ->getAnswerOptionById($response) + ->getTextValue() + : $response + ) + ); + } + $this->global_tpl->addOnLoadCode('il.test.player.longmenu.init(' - . "document.querySelector('input[name=\"answer[{$answer_input_id}]\"]'), " + . "document.querySelector('input[name=\"{$gap_name}\"]'), " . "{$gap->getMinAutocomplete()}, " . json_encode( array_values( @@ -91,10 +111,10 @@ public function getEditAnswerOptionsInputs( Environment $environment, Gap $gap ): array { - $ff = $this->ui_factory->input()->field(); + $ff = $environment->getUIFactory()->input()->field(); return [ 'answer_options' => $ff->tag( - $this->lng->txt('answer_options'), + $environment->getLanguage()->txt('answer_options'), [] )->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'upload_answer_options' => $ff->file( @@ -102,15 +122,15 @@ public function getEditAnswerOptionsInputs( $file_upload, $environment ), - $this->lng->txt('upload_answer_options'), - $this->lng->txt('upload_answer_options_info') + $environment->getLanguage()->txt('upload_answer_options'), + $environment->getLanguage()->txt('upload_answer_options_info') )->withAcceptedMimeTypes(self::ACCEPTED_MIME_TYPES), 'min_autocomplete' => $ff->numeric( - $this->lng->txt('min_autocomplete') + $environment->getLanguage()->txt('min_auto_complete') )->withRequired(true) ->withValue($gap->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), 'options_awarding_points' => $ff->tag( - $this->lng->txt('answer_options'), + $environment->getLanguage()->txt('answer_options'), $gap->getAnswerOptions()->getTagsArrayFromAnswerOptions() ) ->withRequired(true) @@ -129,10 +149,12 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( function (array $vs): bool { - $values = array_merge( - $vs['answer_options'], - $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) - ); + $values = [ + ...$vs['answer_options'], + ...$this->retrieveAnswerOptionsArrayFromUpload( + $vs['upload_answer_options'] + ) + ]; return $values !== [] && array_filter( $vs['options_awarding_points'], @@ -144,10 +166,11 @@ function (array $vs): bool { } public function getEditPointsInputs( + UIFactory $ui_factory, AnswerOptions $answer_options ): array { return $answer_options->getEditPointsInputs( - $this->ui_factory->input()->field(), + $ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue(), $answer_options->getAnswerOptionsAwardingPoints() ); @@ -178,15 +201,42 @@ public function getBuildGapTransformation( ->withMinAutocomplete($vs['min_autocomplete']) ->withAnswerOptions( $gap->getAnswerOptions()->withAnswerOptionsFromTags( - array_merge( - $vs['answer_options'], - $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) - ) + [ + ...$vs['answer_options'], + ...$this->retrieveAnswerOptionsArrayFromUpload( + $vs['upload_answer_options'] + ) + ] )->withAnswerOptionsAwardingPoints($vs['options_awarding_points']) ) ); } + #[\Override] + public function retrieveResponseFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): AnswerInputResponse { + $response_value = $this->retrieveResponseValueFromPost( + $post_wrapper, + $uuid_factory, + $gap + ); + + $response_is_uuid = $response_value instanceof Uuid; + + return new AnswerInputResponse( + $gap, + $response_is_uuid + ? $response_value + : null, + $response_is_uuid + ? '' + : $response_value + ); + } + private function retrieveAnswerOptionsArrayFromUpload( ?array $upload_value ): array { @@ -199,4 +249,33 @@ private function retrieveAnswerOptionsArrayFromUpload( mb_split('\R', $decoded_value) ); } + + private function retrieveResponseValueFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): Uuid|string { + return $post_wrapper->retrieve( + $this->buildGapName($gap), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + function (?string $v) use ($gap): Uuid|string { + if ($v === null) { + return ''; + } + + $answer_option = $gap->getAnswerOptions() + ->getAnswerOptionByTextValue($v); + + if ($answer_option === null) { + return $v; + } + + return $answer_option?->getAnswerOptionId(); + } + ), + $this->refinery->always('') + ]) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index b8f89a049d60..69b75cb37a97 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -20,15 +20,17 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; use ILIAS\UI\Factory as UIFactory; @@ -38,14 +40,6 @@ class Numeric extends Type { private const float DEFAULT_SUB_ACTION_SIZE = 0.0001; - public function __construct( - Refinery $refinery, - Language $lng, - private readonly UIFactory $ui_factory - ) { - parent::__construct($refinery, $lng); - } - #[\Override] public function getIdentifier(): string { @@ -55,20 +49,31 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( Gap $gap, - ?Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): string { $gaptemplate = new \ilTemplate( - 'tpl.il_as_qpl_cloze_question_gap_numeric.html', + 'tpl.cloze_gap_numeric.html', true, true, - 'components/ILIAS/TestQuestionPool' + 'components/ILIAS/Questions' ); $gaptemplate->setVariable( - 'GAP_COUNTER', - $gap->getAnswerInputId()->toString() + 'GAP_NAME', + $this->buildGapName($gap) ); + $response = $response_data?->getResponseForInput($gap->getAnswerInputId()); + if ($response !== null) { + $gaptemplate->setVariable( + 'VALUE_GAP', + ' value="' . \ilLegacyFormElementsUtil::prepareFormOutput( + $response + ) . '"' + ); + } + return $gaptemplate->get(); } @@ -80,16 +85,16 @@ public function getEditAnswerOptionsInputs( ): array { $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); - $ff = $this->ui_factory->input()->field(); + $ff = $environment->getUIFactory()->input()->field(); return [ - 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) + 'lower_limit' => $ff->numeric($environment->getLanguage()->txt('range_lower_limit')) ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withRequired(true) ->withValue($answer_option->getLowerLimit()), - 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) + 'upper_limit' => $ff->numeric($environment->getLanguage()->txt('range_upper_limit')) ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withValue($answer_option->getUpperLimit()), - 'step_size' => $ff->numeric($this->lng->txt('step_size')) + 'step_size' => $ff->numeric($environment->getLanguage()->txt('step_size')) ->withStepSize(0.000001) ->withRequired(true) ->withValue($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) @@ -109,10 +114,11 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint #[\Override] public function getEditPointsInputs( + UIFactory $ui_factory, AnswerOptions $answer_options ): array { $inputs = $answer_options->getEditPointsInputs( - $this->ui_factory->input()->field(), + $ui_factory->input()->field(), function (AnswerOption $v): string { if ($v->getUpperLimit() === null) { return sprintf( @@ -190,4 +196,106 @@ public function getLabelForValue( ): string { return Range::tryFrom($value)?->getLabel($this->lng) ?? ''; } + + #[\Override] + public function retrieveResponseFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): AnswerInputResponse { + return new AnswerInputResponse( + $gap, + null, + $post_wrapper->retrieve( + $this->buildGapName($gap), + $this->refinery->byTrying([ + $this->refinery->in()->series([ + $this->refinery->kindlyTo()->float(), + $this->refinery->kindlyTo()->string() + ]), + $this->refinery->always('') + ]) + ) + ); + } + + #[\Override] + public function isBestResponse( + Gap $gap, + AnswerInputResponse $response + ): bool { + /** @var ?AnswerOption $answer_option */ + $answer_options_awarding_points = $gap->getAnswerOptions() + ->getAnswerOptionsAwardingPoints(); + + $answer_option = $answer_options_awarding_points === null + ? null + : array_shift($answer_options_awarding_points); + + if ($answer_option === null) { + return false; + } + + $response_as_float = $this->refinery->kindlyTo()->float()->transform( + $response->getResponse() + ); + + $upper_limit = $answer_option->getUpperLimit(); + $lower_limit = $answer_option->getLowerLimit(); + if ($upper_limit === null + && $response_as_float === $lower_limit + || $response_as_float >= $lower_limit + && $response_as_float <= $upper_limit) { + return true; + } + + return false; + } + + #[\Override] + public function calculateAwardedPointsForResponse( + Gap $gap, + Uuid|string|null $response + ): float { + /** @var ?AnswerOption $answer_option */ + $answer_options_awarding_points = $gap->getAnswerOptions() + ->getAnswerOptionsAwardingPoints(); + + $answer_option = $answer_options_awarding_points === null + ? null + : array_shift($answer_options_awarding_points); + + if ($answer_option === null) { + return 0.0; + } + + $response_as_float = $this->refinery->kindlyTo()->float()->transform( + $response + ); + + $upper_limit = $answer_option->getUpperLimit(); + $lower_limit = $answer_option->getLowerLimit(); + if ($upper_limit === null + && $response_as_float === $lower_limit + || $response_as_float >= $lower_limit + && $response_as_float <= $upper_limit) { + return $answer_option->getAvailablePoints(); + } + + return 0.0; + } + + #[\Override] + protected function retrieveResponseTextFromAnswerOption( + AnswerOption $answer_option + ): string { + $trafo = $this->refinery->kindlyTo()->string(); + $lower_limit_string = $trafo->transform( + $answer_option->getLowerLimit() + ); + $upper_limit = $answer_option->getUpperLimit(); + return $upper_limit === null + ? $lower_limit_string + : "{$lower_limit_string} - {$trafo->transform($upper_limit)}"; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 09bf36e4140e..02d2ba1203f8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -20,13 +20,16 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Random\Seed\GivenSeed; use ILIAS\Refinery\Random\Seed\RandomSeed; @@ -37,14 +40,6 @@ class Select extends Type { private const bool DEFAULT_SHUFFLE_ANSWER_OPTIONS = false; - public function __construct( - Refinery $refinery, - Language $lng, - private readonly UIFactory $ui_factory - ) { - parent::__construct($refinery, $lng); - } - #[\Override] public function getIdentifier(): string { @@ -54,30 +49,43 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( Gap $gap, - ?Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): string { $gaptemplate = new \ilTemplate( - 'tpl.il_as_qpl_cloze_question_gap_select.html', + 'tpl.cloze_gap_select.html', true, true, - 'components/ILIAS/TestQuestionPool' + 'components/ILIAS/Questions' ); + $selected_answer_option = $response_data?->getResponseForInput( + $gap->getAnswerInputId() + )?->toString(); + foreach ($gap->getAnswerOptions()->buildArrayForSelectInput( $this->buildShuffler( $gap, - $attempt_data + $additional_attempt_data ) - ) as $key => $answer_option) { + ) as $answer_option_id => $answer_option) { $gaptemplate->setCurrentBlock('select_gap_option'); $gaptemplate->setVariable( 'SELECT_GAP_VALUE', - $key + $answer_option_id ); $gaptemplate->setVariable( 'SELECT_GAP_TEXT', \ilLegacyFormElementsUtil::prepareFormOutput($answer_option) ); + + if ($answer_option_id === $selected_answer_option) { + $gaptemplate->setVariable( + 'SELECT_GAP_SELECTED', + ' selected="selected"' + ); + } + $gaptemplate->parseCurrentBlock(); } @@ -87,8 +95,8 @@ public function getParticipantViewLegacyInput( ); $gaptemplate->setVariable( - 'GAP_COUNTER', - $gap->getAnswerInputId()->toString() + 'GAP_NAME', + $this->buildGapName($gap) ); return $gaptemplate->get(); @@ -100,15 +108,15 @@ public function getEditAnswerOptionsInputs( Environment $environment, Gap $gap ): array { - $ff = $this->ui_factory->input()->field(); + $ff = $environment->getUIFactory()->input()->field(); return [ 'answer_options' => $ff->tag( - $this->lng->txt('answer_options'), + $environment->getLanguage()->txt('answer_options'), [] )->withRequired(true) ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'shuffle_answer_options' => $ff->checkbox( - $this->lng->txt('shuffle_answers') + $environment->getLanguage()->txt('shuffle_answers') )->withValue($gap?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) ]; } @@ -121,10 +129,11 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint #[\Override] public function getEditPointsInputs( + UIFactory $ui_factory, AnswerOptions $answer_options ): array { return $answer_options->getEditPointsInputs( - $this->ui_factory->input()->field(), + $ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue() ); } @@ -158,22 +167,81 @@ public function getBuildGapTransformation( ); } + #[\Override] + public function retrieveResponseFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): AnswerInputResponse { + return new AnswerInputResponse( + $gap, + $post_wrapper->retrieve( + $this->buildGapName($gap), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + function (?string $v) use ($uuid_factory, $gap): ?Uuid { + if ($v === null) { + return null; + } + + try { + $answer_option_id = $uuid_factory->fromString($v); + } catch (\Exception $e) { + return null; + } + + if ($gap->getAnswerOptions()->getAnswerOptionById( + $answer_option_id + ) === null) { + return null; + } + + return $answer_option_id; + } + ), + $this->refinery->always(null) + ]) + ), + '' + ); + } + private function buildShuffler( Gap $gap, - Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data ): Transformation { if (!$gap->getShuffleAnswerOptions()) { return $this->refinery->random()->dontShuffle(); } return $this->refinery->random()->shuffleArray( - $attempt_data === null - ? new RandomSeed() - : new GivenSeed( - $this->refinery->kindlyTo()->int()->transform( - $attempt_data->getAdditionalDataFor($gap->getAnswerInputId()) - ) - ) + $this->buildSeed( + $gap, + $additional_attempt_data + ) + ); + } + + private function buildSeed( + Gap $gap, + ?AdditionalAttemptData $additional_attempt_data + ): GivenSeed { + if ($additional_attempt_data === null) { + return new RandomSeed(); + } + + $given_seed = $additional_attempt_data->getAdditionalDataFor( + $gap->getAnswerInputId() ); + + if (is_numeric($given_seed)) { + return new GivenSeed( + $this->refinery->kindlyTo()->int()->transform( + $given_seed + ) + ); + } + + return new RandomSeed(); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 3317f8ae7833..e6782979f607 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -20,30 +20,28 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\SpecificTextFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Types as TextFeedbackTypes; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Component\Component; use ILIAS\UI\Factory as UIFactory; class Text extends Type { private const TextMatchingOptions DEFAULT_TECT_MATCHING_METHOD = TextMatchingOptions::CaseInsensitive; - public function __construct( - Refinery $refinery, - Language $lng, - private readonly UIFactory $ui_factory - ) { - parent::__construct($refinery, $lng); - } - #[\Override] public function getIdentifier(): string { @@ -53,13 +51,14 @@ public function getIdentifier(): string #[\Override] public function getParticipantViewLegacyInput( Gap $gap, - ?Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): string { $gaptemplate = new \ilTemplate( - 'tpl.il_as_qpl_cloze_question_gap_text.html', + 'tpl.cloze_gap_text.html', true, true, - 'components/ILIAS/TestQuestionPool' + 'components/ILIAS/Questions' ); $gap_size = $gap->getMaxChars(); @@ -69,10 +68,20 @@ public function getParticipantViewLegacyInput( $gaptemplate->parseCurrentBlock(); } $gaptemplate->setVariable( - 'GAP_COUNTER', - $gap->getAnswerInputId()->toString() + 'GAP_NAME', + $this->buildGapName($gap) ); + $response = $response_data?->getResponseForInput($gap->getAnswerInputId()); + if ($response !== null) { + $gaptemplate->setVariable( + 'VALUE_GAP', + ' value="' . \ilLegacyFormElementsUtil::prepareFormOutput( + $response + ) . '"' + ); + } + return $gaptemplate->get(); } @@ -82,20 +91,20 @@ public function getEditAnswerOptionsInputs( Environment $environment, Gap $gap ): array { - $ff = $this->ui_factory->input()->field(); + $ff = $environment->getUIFactory()->input()->field(); return [ 'answer_options' => $ff->tag( - $this->lng->txt('answer_options'), + $environment->getLanguage()->txt('answer_options'), [] )->withRequired(true) ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), 'matching_method' => $ff->select( - $this->lng->txt('matching_method'), - TextMatchingOptions::buildOptionsList($this->lng) + $environment->getLanguage()->txt('text_matching_method'), + TextMatchingOptions::buildOptionsList($environment->getLanguage()) )->withRequired(true) ->withValue($gap->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), 'max_chars' => $ff->numeric( - $this->lng->txt('max_chars'), + $environment->getLanguage()->txt('max_chars'), )->withValue($gap->getMaxChars()) ]; } @@ -114,10 +123,11 @@ public function getEditAnswerOptionsSectionConstraint(): ?Constraint #[\Override] public function getEditPointsInputs( + UIFactory $ui_factory, AnswerOptions $answer_options ): array { return $answer_options->getEditPointsInputs( - $this->ui_factory->input()->field(), + $ui_factory->input()->field(), fn(AnswerOption $v): string => $v->getTextValue() ); } @@ -154,4 +164,107 @@ public function getBuildGapTransformation( ) ); } + + #[\Override] + public function retrieveResponseFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): AnswerInputResponse { + return new AnswerInputResponse( + $gap, + null, + $post_wrapper->retrieve( + $this->buildGapName($gap), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]) + ) + ); + } + + #[\Override] + public function isBestResponse( + Gap $gap, + AnswerInputResponse $response + ): bool { + return $this->getBestResponse( + $gap + )?->getResponse() === $response->getResponse(); + } + + #[\Override] + public function calculateAwardedPointsForResponse( + Gap $gap, + Uuid|string|null $response + ): float { + + $answer_option = array_filter( + $gap->getAnswerOptions()->getAnswerOptionsAwardingPoints(), + fn(AnswerOption $v): bool => $response === $v->getTextValue() + ); + + if ($answer_option === []) { + return 0.0; + } + + return array_shift($answer_option)->getAvailablePoints(); + } + + #[\Override] + public function getSpecificFeedbackParticipantOutput( + UIFactory $ui_factory, + Gap $gap, + array $specific_feedbacks, + Uuid|string $answer_input_response + ): ?Component { + $specific_feedbacks_by_condition = array_reduce( + $specific_feedbacks, + function (array $c, SpecificFeedback $v): array { + if (!array_key_exists($v->getCondition(), $c)) { + $c[$v->getCondition()] = []; + } + + $c[$v->getCondition()] = $v->getFeedbackText(); + + return $c; + }, + [] + ); + + if ($answer_input_response instanceof Uuid) { + return $this->getSpecificFeedbackParticipantOutputForAnswerOption( + $ui_factory, + $specific_feedbacks_by_condition[$answer_input_response->toString()] ?? null + ); + } + + if ($this->getBestResponse($gap) === null) { + return null; + } + + if ($this->getBestResponse($gap)->getResponse() === $answer_input_response) { + return isset($specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]) + ? $ui_factory->legacy()->content( + $this->refinery->string()->markdown()->toHTML()->transform( + $specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]->getRawRepresentation() + ) + ) : null; + } + + return isset($specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse->value]) + ? $ui_factory->legacy()->content( + $this->refinery->string()->markdown()->toHTML()->transform( + $specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse->value]->getRawRepresentation() + ) + ) : null; + } + + #[\Override] + protected function retrieveResponseTextFromAnswerOption( + AnswerOption $answer_option + ): string { + return $answer_option->getTextValue(); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index 9bdabef77863..c38994d17aa1 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -20,32 +20,42 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\Types as FeedbackTypes; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\SpecificTextFeedback; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Types as TextFeedbackTypes; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; -use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; +use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Definitions\Range; use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Component\Component; +use ILIAS\UI\Factory as UIFactory; use Ramsey\Uuid\Exception\InvalidUuidStringException; abstract class Type { public function __construct( - protected readonly Refinery $refinery, - protected readonly Language $lng + protected readonly Language $lng, + protected readonly Refinery $refinery ) { + } abstract public function getIdentifier(): string; abstract public function getParticipantViewLegacyInput( Gap $gap, - ?Attempt $attempt + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data ): string; abstract public function getEditAnswerOptionsInputs( @@ -57,15 +67,23 @@ abstract public function getEditAnswerOptionsInputs( abstract public function getEditAnswerOptionsSectionConstraint(): ?Constraint; abstract public function getEditPointsInputs( + UIFactory $ui_factory, AnswerOptions $answer_options ): array; - abstract public function getEditPointsSectionConstraint(): ?Constraint; + abstract public function getEditPointsSectionConstraint( + ): ?Constraint; abstract public function getBuildGapTransformation( Gap $gap ): Transformation; + abstract public function retrieveResponseFromPost( + RequestWrapper $post_wrapper, + UuidFactory $uuid_factory, + Gap $gap + ): AnswerInputResponse; + public function getCombinationsSelectValues( Gap $gap ): array { @@ -78,21 +96,23 @@ public function getFeedbackSelectValues( Gap $gap, bool $is_marking_required ): array { - $basic_select_values = $this->getCombinationsSelectValues($gap); + $basic_select_values = $this->getCombinationsSelectValues( + $gap + ); if (!$is_marking_required) { return $basic_select_values; } - return array_merge( - [ - FeedbackTypes::MaxPoints => FeedbackTypes::MaxPoints + return [ + ...[ + TextFeedbackTypes::BestResponse->value => TextFeedbackTypes::BestResponse ->getTranslatedOptionName($this->lng), - FeedbackTypes::NotMaxPoints => FeedbackTypes::NotMaxPoints + TextFeedbackTypes::OtherResponse->value => TextFeedbackTypes::OtherResponse ->getTranslatedOptionName($this->lng), - FeedbackTypes::NothingSelected => FeedbackTypes::NothingSelected + TextFeedbackTypes::NoResponse->value => TextFeedbackTypes::NoResponse ->getTranslatedOptionName($this->lng) ], - $basic_select_values - ); + ...$basic_select_values + ]; } public function isValidFeedbackCondition( @@ -100,7 +120,7 @@ public function isValidFeedbackCondition( Gap $gap, string $condition ): bool { - if (FeedbackTypes::tryFrom($condition) !== null + if (TextFeedbackTypes::tryFrom($condition) !== null || Range::tryFrom($condition) !== null) { return true; } @@ -119,6 +139,11 @@ public function getLabelForValue( Gap $gap, string $value ): string { + $from_feedback = TextFeedbackTypes::tryFrom($value); + if ($from_feedback !== null) { + return $from_feedback->getTranslatedOptionName($this->lng); + } + return $gap->getAnswerOptions()->getAnswerOptionById( $uuid_factory->fromString($value) )->getTextValue(); @@ -134,4 +159,153 @@ public function getAddPointsTransformation( ) ); } + + public function getBestResponse( + Gap $gap + ): ?AnswerInputResponse { + $best_answer_option = $gap->getAnswerOptions()->getBestAnswerOption(); + if ($best_answer_option === null) { + return null; + } + + $text = $this->retrieveResponseTextFromAnswerOption( + $best_answer_option + ); + if ($text !== '') { + $best_answer_option = null; + } + + return new AnswerInputResponse( + $gap, + $best_answer_option?->getAnswerOptionId(), + $text + ); + } + + public function isBestResponse( + Gap $gap, + AnswerInputResponse $response + ): bool { + $response_value = $response->getResponse(); + + if (!($response_value instanceof Uuid)) { + return false; + } + + return $this->getBestResponse( + $gap + )->getResponse()?->compareTo( + $response_value + ) === 0; + } + + public function calculateAwardedPointsForResponse( + Gap $gap, + Uuid|string|null $response + ): float { + if (!($response instanceof Uuid)) { + return 0.0; + } + + $answer_option = $gap->getAnswerOptions()->getAnswerOptionById($response); + + if ($answer_option === null) { + return 0.0; + } + + return $answer_option->getAvailablePoints() ?? 0.0; + } + + public function getSpecificFeedbackParticipantOutput( + UIFactory $ui_factory, + Gap $gap, + array $specific_feedbacks, + Uuid|string $answer_input_response + ): ?Component { + $specific_feedbacks_by_condition = array_reduce( + $specific_feedbacks, + function (array $c, SpecificTextFeedback $v): array { + if (!array_key_exists($v->getCondition(), $c)) { + $c[$v->getCondition()] = []; + } + + $c[$v->getCondition()] = $v->getFeedbackText(); + + return $c; + }, + [] + ); + + if ($answer_input_response instanceof Uuid) { + return $this->getSpecificFeedbackParticipantOutputForAnswerOption( + $ui_factory, + $specific_feedbacks_by_condition[$answer_input_response->toString()] ?? null + ); + } + + if ($this->getBestResponse($gap) === null) { + return null; + } + + if ($this->getBestResponse($gap)->getResponse() === $answer_input_response) { + return isset($specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]) + ? $ui_factory->legacy()->content( + $this->refinery->string()->markdown()->toHTML()->transform( + $specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]->getRawRepresentation() + ) + ) : null; + } + + return isset($specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse->value]) + ? $ui_factory->legacy()->content( + $this->refinery->string()->markdown()->toHTML()->transform( + $specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse]->getRawRepresentation() + ) + ) : null; + } + + public function retrieveResponseFromPreviewData( + UuidFactory $uuid_factory, + Gap $gap, + array $preview_data + ): ?AnswerInputResponse { + if ($preview_data === []) { + return null; + } + + return new AnswerInputResponse( + $gap, + isset($preview_data[AnswerInputResponse::KEY_SELECTED_ANSWER_OPTION]) + ? $uuid_factory->fromString($preview_data[AnswerInputResponse::KEY_SELECTED_ANSWER_OPTION]) + : null, + $preview_data[AnswerInputResponse::KEY_TEXT] ?? '' + ); + } + + protected function retrieveResponseTextFromAnswerOption( + AnswerOption $answer_option + ): string { + return ''; + } + + final protected function buildGapName( + Gap $gap + ): string { + return "gap_{$gap->getAnswerInputId()->toString()}"; + } + + private function getSpecificFeedbackParticipantOutputForAnswerOption( + UIFactory $ui_factory, + ?Markdown $specific_feedback + ): ?Component { + if ($specific_feedback === null) { + return null; + } + + return $ui_factory->legacy()->content( + $this->refinery->string()->markdown()->toHTML()->transform( + $specific_feedback + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 890eae1a00f4..455a03a62fce 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Properties as PropertiesInterface; use ILIAS\Questions\AnswerForm\TypeGenericProperties; @@ -42,7 +41,6 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; -use ILIAS\Language\Language; use ILIAS\UI\Component\Input\Field\Section; use ILIAS\UI\Component\Table\Data as DataTable; @@ -173,16 +171,26 @@ public function withGaps( #[\Override] public function getBasicPropertiesForListing( - Language $lng + Environment $environment ): array { - return [ + $lng = $environment->getLanguage(); + + $listing = [ $lng->txt('cloze_text') => $this->cloze_text ->getRenderedMarkdownForEditingPresentation( $this->gaps, $this->getLegacyClozeText() - ), - $lng->txt('score_identical') => $this->scoring_identical - ->getTranslatedOptionName($lng), + ) + ]; + + if ($environment->isMarkingRequired()) { + $listing[$lng->txt('scoring_of_identical_responses')] = $this + ->scoring_identical + ->getTranslatedOptionName($lng); + } + + return [ + ...$listing, $lng->txt('gap_combinations') => $this->combinations->areCombinationsEnabled() ? $lng->txt('enabled') : $lng->txt('disabled') @@ -233,15 +241,15 @@ public function buildBasicEditingInputs( )->withValue($this->getScoringOfIdenticalResponses()->value) ]; - if ($environment->isCapabilityRequired(Marking::class)) { + if ($environment->isMarkingRequired()) { $inputs[self::KEY_ENABLE_COMBINATIONS] = $ff->checkbox( - $lng->txt('cloze_enable_combinations') + $lng->txt('cloze_enable_gap_combinations') )->withValue($this->combinations->areCombinationsEnabled()); } return $ff->section( $inputs, - $lng->txt('set_basic_properties') + $lng->txt('edit_basic_answer_form_properties') )->withAdditionalTransformation( $refinery->custom()->transformation( fn(array $vs): self => $properties_factory->fromBasicEditingForm( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php index 96f84348cf72..7ad4bb5cafe1 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php @@ -22,6 +22,8 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Manipulate; @@ -60,6 +62,29 @@ public function getAnswerFormId(): Uuid return $this->answer_form_id; } + #[\Override] + public function isBest(): bool + { + foreach ($this->answer_input_responses as $response) { + if (!$response->isBest()) { + return false; + } + } + + + return true; + } + + #[\Override] + public function toPreviewStorage(): array + { + return array_map( + fn(AnswerInput $v): array => $v->toPreviewStorage(), + $this->answer_input_responses + ); + } + + #[\Override] public function toStorage( PersistenceFactory $persistence_factory, Manipulate $manipulate @@ -67,7 +92,7 @@ public function toStorage( return $manipulate->withAdditionalStatement( array_reduce( $this->answer_input_responses, - fn(?Replace $c, AnswerInput $v): Insert => $v->toStorage( + fn(?Insert $c, AnswerInput $v): Insert => $v->toStorage( $this->table_definitions, $manipulate->getTableNameBuilder( $this->table_definitions->getTableSubNameSpace() @@ -80,6 +105,7 @@ public function toStorage( ); } + #[\Override] public function toDelete( PersistenceFactory $persistence_factory, Manipulate $manipulate @@ -88,14 +114,16 @@ public function toDelete( $persistence_factory->delete( $persistence_factory->table( $manipulate->getTableNameBuilder( - AnswerFormSpecificTableTypes::Responses + $this->table_definitions->getTableSubNameSpace() ), AnswerFormSpecificTableTypes::Responses ), [ $persistence_factory->where( $this->table_definitions->getIdColumn( - $persistence_factory, + $manipulate->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), AnswerFormSpecificTableTypes::Responses ), $persistence_factory->value( @@ -107,4 +135,44 @@ public function toDelete( ) ); } + + public function getResponseForInput( + Uuid $answer_input_id + ): Uuid|string|null { + if (isset($this->answer_input_responses[$answer_input_id->toString()])) { + return $this->answer_input_responses[$answer_input_id->toString()]->getResponse(); + } + + return null; + } + + public function calculateAwardedPoints( + Properties $answer_form_properties + ): float { + return array_reduce( + $this->answer_input_responses, + function (?float $c, AnswerInput $v) use ($answer_form_properties): ?float { + $gap = $answer_form_properties + ->getGaps() + ->getGapById( + $v->getAnswerInputId() + ); + + if ($gap === null) { + return 0.0; + } + + $awarded_points = $gap->getType()->calculateAwardedPointsForResponse( + $gap, + $v->getResponse() + ); + + if ($c === null) { + return $awarded_points; + } + + return $c + $awarded_points; + } + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php index 5ee53fabace5..6c686e3a62e2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php @@ -21,16 +21,22 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Response; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\Replace; +use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; class AnswerInput { + public const string KEY_ANSWER_INPUT_ID = 'answer_input_id'; + public const string KEY_SELECTED_ANSWER_OPTION = 'selected_answer_option'; + public const string KEY_TEXT = 'text'; + public function __construct( - private readonly Uuid $answer_input_id, + private readonly Gap $gap, private readonly ?Uuid $selected_answer_option, private readonly string $text ) { @@ -38,14 +44,40 @@ public function __construct( public function getAnswerInputId(): Uuid { - return $this->answer_input_id; + return $this->gap->getAnswerInputId(); + } + + public function getResponse(): Uuid|string|null + { + if ($this->selected_answer_option !== null) { + return $this->selected_answer_option; + } + + if ($this->text !== '') { + return $this->text; + } + + return null; + } + + public function isBest(): bool + { + return $this->gap->getType()->isBestResponse($this->gap, $this); + } + + public function toPreviewStorage(): array + { + return [ + self::KEY_SELECTED_ANSWER_OPTION => $this->selected_answer_option?->toString(), + self::KEY_TEXT => $this->text + ]; } public function toStorage( TableDefinitions $table_definitions, TableNameBuilder $table_names_builder, PersistenceFactory $persistence_factory, - ?Replace $insert, + ?Insert $insert, Uuid $id ): Insert { if ($insert === null) { @@ -74,7 +106,7 @@ private function buildValuesArrayForStorage( ), $persistence_factory->value( FieldDefinition::T_TEXT, - $this->answer_input_id->toString() + $this->gap->getAnswerInputId()->toString() ), $persistence_factory->value( FieldDefinition::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php index 6f04a89ac9f5..c5d46e33f2b5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/Factory.php @@ -21,44 +21,138 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Response; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; -use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\Data\UUID\Uuid; +use ILIAS\HTTP\Wrapper\RequestWrapper; class Factory { public function __construct( + private readonly UuidFactory $uuid_factory, private readonly PersistenceFactory $persistence_factory ) { } - public function fromData( + public function fromQuery( Uuid $response_id, - Uuid $answer_form_id, + Properties $answer_form_properties, ?Query $query ): AnswerForm { + $table_definitions = $answer_form_properties + ->getDefinition() + ->getTableDefinitions(); + if ($query === null) { return new AnswerForm( + $table_definitions, $response_id, - $answer_form_id + $answer_form_properties->getAnswerFormId() ); } return $query->retrieveCurrentRecord( $this->persistence_factory->table( $query->getTableNameBuilder( - $this->definition - ->getTableDefinitions() - ->getTableSubNameSpace() + $table_definitions->getTableSubNameSpace() ), AnswerFormSpecificTableTypes::Responses ), $query->getRefinery()->custom()->transformation( - fn(array $vs): AnswerForm => new AnswerForm( - $this->definition->getTableDefinitions(), + fn (array $vs): AnswerForm => new AnswerForm( + $table_definitions, + $response_id, + $answer_form_properties->getAnswerFormId(), + $this->retrieveAnswerInputResponsesFromValues( + $answer_form_properties, + $vs + ) ) ) ); } + public function fromPost( + Uuid $response_id, + Properties $answer_form_properties, + RequestWrapper $post_wrapper + ): AnswerForm { + return new AnswerForm( + $answer_form_properties + ->getDefinition() + ->getTableDefinitions(), + $response_id, + $answer_form_properties->getAnswerFormId(), + $answer_form_properties->getGaps()->retrieveResponsesFromPost( + $post_wrapper, + $this->uuid_factory + ), + ); + } + + public function fromPreviewData( + Uuid $response_id, + Properties $answer_form_properties, + array $preview_data + ): AnswerForm { + return new AnswerForm( + $answer_form_properties + ->getDefinition() + ->getTableDefinitions(), + $response_id, + $answer_form_properties->getAnswerFormId(), + $answer_form_properties->getGaps()->retrieveResponsesFromPreviewData( + $this->uuid_factory, + $preview_data + ), + ); + } + + public function getBestResponse( + Properties $answer_form_properties, + ): AnswerForm { + return new AnswerForm( + $answer_form_properties + ->getDefinition() + ->getTableDefinitions(), + $this->uuid_factory->uuid4(), + $answer_form_properties->getAnswerFormId(), + $answer_form_properties->getGaps()->getBestResponses(), + ); + } + + private function retrieveAnswerInputResponsesFromValues( + Properties $answer_form_properties, + array $values + ): array { + return array_reduce( + $values, + function (array $c, array $vs) use ($answer_form_properties): array { + $answer_input_id = $this->uuid_factory + ->fromString($vs['answer_input_id']); + + $gap = $answer_form_properties->getGaps()->getGapById( + $answer_input_id + ); + + if ($gap === null) { + return $c; + } + + $c[] = new AnswerInput( + $gap, + $vs['selected_answer_option'] === null + ? null + : $this->uuid_factory + ->fromString($vs['selected_answer_option']), + $vs['text'] + ); + + return $c; + }, + [] + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index 67f98f9a7f6b..ab3f5203d226 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; use ILIAS\Questions\AnswerForm\Views\Edit as EditViewInterface; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Factory as PropertiesFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; @@ -76,7 +75,7 @@ public function edit( $sub_action = $environment->getSubAction(); $combinations = $environment->getAnswerFormProperties()->getCombinations(); - if ($environment->isCapabilityRequired(Marking::class) + if ($environment->isMarkingRequired() && $combinations->areCombinationsEnabled()) { $combinations->getEditView()->addCombinationsSubTab($environment); } @@ -85,6 +84,7 @@ public function edit( return $environment->getPresentationFactory()->getEditOverview( $environment, $environment->withSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) + ->withFormStartSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) ->getUrlBuilder() ); } @@ -263,7 +263,7 @@ private function processBasicEditingForm( $data->withGaps( $data->getGaps()->withMarkedIncompleteGaps() ) - ) + )->withPreservedFormStartSubActionParameter() ); } @@ -275,6 +275,7 @@ private function buildEditFormForBasicInputs( $inputs_builder, $environment ->withSubActionParameter(self::SUB_ACTION_PROCESS_BASIC_PROPERTIES) + ->withPreservedFormStartSubActionParameter() ->getUrlBuilder(), null ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php index 200f6fcb3d3a..aa21db9459be 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php @@ -20,36 +20,57 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Views; -use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerForm\Views\Participant as ParticipantViewInterface; -use ILIAS\Questions\Response\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewMode; use ILIAS\UICore\GlobalTemplate; +use ILIAS\HTTP\Wrapper\RequestWrapper; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; use Mustache\Engine as MustacheEngine; class Participant implements ParticipantViewInterface { public function __construct( private readonly GlobalTemplate $global_tpl, - private readonly MustacheEngine $mustache_engine + private readonly MustacheEngine $mustache_engine, + private readonly ResponseFactory $response_factory ) { } #[\Override] public function show( + Language $lng, Properties $properties, - ?Attempt $attempt_data + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data, + ViewMode $view_mode ): string { $this->global_tpl->addJavaScript('assets/js/ParticipantViewLongMenu.js'); return $this->mustache_engine->render( $properties->getClozeTextForPresentation(), - $properties->getGaps()->getPlaceholderArrayForParticipantView($attempt_data) + $properties->getGaps()->getPlaceholderArray( + $lng, + $view_mode, + $additional_attempt_data, + $response_data + ) ); } #[\Override] - public function retrieveResponse(): Response - { - + public function retrieveResponse( + Uuid $response_id, + Properties $properties, + RequestWrapper $post_wrapper + ): Response { + return $this->response_factory->fromPost( + $response_id, + $properties, + $post_wrapper + ); } } diff --git a/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php b/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php index 3e47dd0e4a7a..ff7fbbea58a9 100644 --- a/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php +++ b/components/ILIAS/Questions/src/Attempt/AdditionalAttemptData.php @@ -20,161 +20,11 @@ namespace ILIAS\Questions\Attempt; -use ILIAS\Questions\Persistence\Factory as PersistenceFactory; -use ILIAS\Questions\Persistence\Insert; -use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; -use ILIAS\Refinery\Random\Seed\GivenSeed; -class Attempt +interface AdditionalAttemptData { - /** - * @var array - */ - private array $responses = []; - - public function __construct( - private readonly Uuid $identifier, - private readonly int $shuffle_questions_seed, - private array $additional_data = [] - ) { - } - - public function getId(): Uuid - { - return $this->identifier; - } - - public function getShuffleQuestionsSeed(): GivenSeed - { - return new GivenSeed($this->shuffle_questions_seed); - } - public function getAdditionalDataFor( Uuid $parent_id - ): ?string { - if (!isset($this->additional_data[$parent_id->toString()])) { - return null; - } - return $this->additional_data[$parent_id->toString()]; - } - - public function withAdditionalData( - Uuid $parent_id, - string $data - ): self { - if (isset($this->additional_data[$parent_id->toString()])) { - throw new InvalidArgumentException( - 'This is a storage for data that stays constant accross the test run.' - . 'Data cannot be changed one it is set.' - ); - } - - $clone = clone $this; - $clone->additional_data[$parent_id->toString()] = $data; - return $clone; - } - - public function getResponseFor( - Uuid $question_id - ): ?Response { - return $this->responses[$question_id->toString()] ?? null; - } - - public function withAdditionalResponse( - Response $response - ): self { - $clone = clone $this; - $clone->responses[$response->getQuestionId()->toString()] = $response; - return $clone; - } - - public function basicDataToStorage( - PersistenceFactory $persistence_factory, - TableDefinitions $table_definitions, - TableNameBuilder $table_names_builder - ): Insert { - return $persistence_factory->insert( - $table_definitions->getColumns( - $table_names_builder, - TableTypes::AttemptData - ), - [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->identifier->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_INTEGER, - $this->shuffle_questions_seed - ) - ] - ); - } - - public function additionalDataToStorage( - PersistenceFactory $persistence_factory, - TableDefinitions $table_definitions, - TableNameBuilder $table_names_builder - ): ?Insert { - return array_reduce( - array_keys($this->additional_data), - fn(?Insert $c, string $v): Insert - => $this->buildAdditionalDataInsert( - $persistence_factory, - $table_definitions, - $table_names_builder, - $c, - $v - ) - ); - } - - private function buildAdditionalDataInsert( - PersistenceFactory $persistence_factory, - TableDefinitions $table_definitions, - TableNameBuilder $table_names_builder, - ?Insert $insert, - string $parent_id - ): Insert { - if ($insert === null) { - return $persistence_factory->insert( - $table_definitions->getColumns( - $table_names_builder, - TableTypes::AdditionalAttemptData - ), - $this->buildAdditionalDataValuesArray( - $persistence_factory, - $parent_id - ) - ); - } - - return $insert->withAdditionalValues( - $this->buildAdditionalDataValuesArray( - $persistence_factory, - $parent_id - ) - ); - } - - private function buildAdditionalDataValuesArray( - PersistenceFactory $persistence_factory, - string $parent_id - ): array { - return [ - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->identifier->toString() - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $parent_id - ), - $persistence_factory->value( - \ilDBConstants::T_TEXT, - $this->additional_data[$parent_id] - ) - ]; - } + ): ?string; } diff --git a/components/ILIAS/Questions/src/Attempt/Attempt.php b/components/ILIAS/Questions/src/Attempt/Attempt.php index 3e47dd0e4a7a..5892d6e6dec8 100644 --- a/components/ILIAS/Questions/src/Attempt/Attempt.php +++ b/components/ILIAS/Questions/src/Attempt/Attempt.php @@ -26,8 +26,11 @@ use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Random\Seed\GivenSeed; -class Attempt +class Attempt implements AdditionalAttemptData { + public const string KEY_ADDITONAL_DATA = 'additional_data'; + public const string KEY_RESPONSES = 'responses'; + /** * @var array */ @@ -75,13 +78,13 @@ public function withAdditionalData( return $clone; } - public function getResponseFor( + public function getResponseForQuestion( Uuid $question_id ): ?Response { return $this->responses[$question_id->toString()] ?? null; } - public function withAdditionalResponse( + public function withResponse( Response $response ): self { $clone = clone $this; @@ -130,6 +133,19 @@ public function additionalDataToStorage( ); } + public function toPreviewStorage(): string + { + return json_encode( + [ + self::KEY_ADDITONAL_DATA => $this->additional_data, + self::KEY_RESPONSES => array_map( + fn(Response $v): array => $v->toPreviewStorage(), + $this->responses + ) + ] + ); + } + private function buildAdditionalDataInsert( PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, diff --git a/components/ILIAS/Questions/src/Attempt/Repository.php b/components/ILIAS/Questions/src/Attempt/Repository.php index ec0e37e93f69..8438b2386fbc 100644 --- a/components/ILIAS/Questions/src/Attempt/Repository.php +++ b/components/ILIAS/Questions/src/Attempt/Repository.php @@ -55,7 +55,7 @@ public function __construct( * @throws InvalidArgumentException */ public function getAttemptFor( - Uuid $attempt_id, + ?Uuid $attempt_id, array $questions ): Attempt { if ($attempt_id === null) { @@ -66,7 +66,7 @@ public function getAttemptFor( $base_table_id_column = $this->table_definitions->getIdColumn( $this->table_names_builder, - TableTypes::AttemptData + TableTypes::Responses ); $database_values = array_reduce( @@ -85,8 +85,8 @@ public function getAttemptFor( return array_reduce( $questions, - fn(Attempt $c, Question $v): Attempt => $c->withAdditionalResponse( - $this->retriveResponseFromQuery( + fn(Attempt $c, Question $v): Attempt => $c->withResponse( + $this->retrieveCurrentResponseFromQuery( $v, $c->getId(), $database_values @@ -114,6 +114,80 @@ public function getNewResponseFor( ); } + public function storeResponse( + Response $response + ): void { + $response->toStorage( + $this->persistence_factory, + $this->table_definitions, + $this->table_names_builder, + new Manipulate( + $this->db, + ManipulationType::Create, + QuestionRepository::COMPONENT_NAMESPACE + ) + )->run(); + } + + public function deleteResponsesFor( + Uuid $attempt_id, + Question $question + ): void { + $manipulate = new Manipulate( + $this->db, + ManipulationType::Delete, + QuestionRepository::COMPONENT_NAMESPACE + ); + foreach ($this->getAllResponsesFor($attempt_id, $question) as $response) { + $manipulate = $response->toDelete( + $this->persistence_factory, + $this->table_definitions, + $this->table_names_builder, + $manipulate + ); + } + + $manipulate->run(); + } + + public function getAttemptFromPreviewData( + Question $question, + string $preview_data + ): ?Attempt { + $data_array = json_decode($preview_data, true); + + if (!is_array($data_array) || $data_array === []) { + return $this->getNewAttempt([$question]); + } + + $response_id = $this->uuid_factory->uuid4(); + + return array_reduce( + $data_array[Attempt::KEY_RESPONSES] ?? [], + fn(Attempt $c, array $v): Attempt => $c->withResponse( + new Response( + $response_id, + $question->getId(), + $this->uuid_factory->uuid4(), + new \DateTimeImmutable( + 'now', + new \DateTimeZone('UTC') + ), + $v[Response::KEY_POINTS] ?? 0.0, + $question->retrieveAnswerFormResponsesFromPreviewData( + $response_id, + $v[Response::KEY_RESPONSES] ?? [] + ) + ) + ), + new Attempt( + $this->uuid_factory->uuid4(), + 0, + $data_array[Attempt::KEY_ADDITONAL_DATA] ?? [] + ) + ); + } + private function storeAttempt( Attempt $attempt ): void { @@ -162,6 +236,32 @@ private function getNewAttempt( ); } + private function getAllResponsesFor( + Uuid $attempt_id, + Question $question + ): \Generator { + $base_table_id_column = $this->table_definitions->getIdColumn( + $this->table_names_builder, + TableTypes::Responses + ); + + $database_values = $question->completeResponseQuery( + $this->buildQuery($attempt_id), + $base_table_id_column + )->loadNextRecord() + ->current(); + + if ($database_values === null) { + throw new \InvalidArgumentException('No Attempt With Given Identifier'); + } + + return $this->retrieveAllResponsesFromQuery( + $question, + $attempt_id, + $database_values + ); + } + private function buildQuery( Uuid $attempt_id ): Query { @@ -169,7 +269,7 @@ private function buildQuery( $this->table_names_builder, TableTypes::AttemptData ); - return $this->table_definitions->completeLoadAttemptQuery( + return $this->table_definitions->completeAttemptQuery( new Query( $this->db, $this->refinery, @@ -182,15 +282,14 @@ private function buildQuery( $attempt_data_id_column )->withAdditionalWhere( $this->persistence_factory->where( - $this->table_definitions->getIdColumn( - $this->table_names_builder, - TableTypes::AttemptData - ), + $attempt_data_id_column, $this->persistence_factory->value( FieldDefinition::T_TEXT, $attempt_id->toString() ) ) + )->withGroupBy( + $attempt_data_id_column ); } @@ -237,12 +336,12 @@ private function retrieveAttemptFromQuery( ); } - private function retriveResponseFromQuery( + private function retrieveCurrentResponseFromQuery( Question $question, Uuid $attempt_id, Query $query - ): Attempt { - $query->retrieveCurrentRecord( + ): Response { + return $query->retrieveCurrentRecord( $this->persistence_factory->table( $this->table_names_builder, TableTypes::Responses @@ -255,34 +354,81 @@ function ( $attempt_id, $query ): Response { - if ($vs === []) { + $question_id_string = $question->getId()->toString(); + + $last_record = array_reduce( + $vs, + fn(array $c, array $v): array + => $v['question_id'] === $question_id_string ? $v : $c, + [] + ); + + if ($last_record === []) { return $this->getNewResponseFor( $question->getId(), $attempt_id ); } - $last_record = array_last($vs); $response_id = $this->uuid_factory ->fromString($last_record['id']); - $answer_form_responses = $question - ->retrieveAnswerFormResponsesFromQuery( - $response_id, - $query - ); - return new Response( $response_id, $question->getId(), $attempt_id, new \DateTimeImmutable("@{$last_record['create_timestamp']}"), - $last_record['reached_points'], - $answer_form_responses + $last_record['awarded_points'], + $question->retrieveAnswerFormResponsesFromQuery( + $response_id, + $query + ) ); } ) ); + } + private function retrieveAllResponsesFromQuery( + Question $question, + Uuid $attempt_id, + Query $query + ): \Generator { + return $query->retrieveCurrentRecord( + $this->persistence_factory->table( + $this->table_names_builder, + TableTypes::Responses + ), + $this->refinery->custom()->transformation( + function ( + array $vs + ) use ( + $question, + $attempt_id, + $query + ): \Generator { + if ($vs === []) { + return; + } + + foreach ($vs as $v) { + $response_id = $this->uuid_factory + ->fromString($v['id']); + + yield new Response( + $response_id, + $question->getId(), + $attempt_id, + new \DateTimeImmutable("@{$v['create_timestamp']}"), + $v['awarded_points'], + $question->retrieveAnswerFormResponsesFromQuery( + $response_id, + $query + ) + ); + } + } + ) + ); } } diff --git a/components/ILIAS/Questions/src/Attempt/Response.php b/components/ILIAS/Questions/src/Attempt/Response.php index 25de6b2669a9..e759bd0542d6 100644 --- a/components/ILIAS/Questions/src/Attempt/Response.php +++ b/components/ILIAS/Questions/src/Attempt/Response.php @@ -23,15 +23,19 @@ use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; class Response { + public const string KEY_POINTS = 'points'; + public const string KEY_RESPONSES = 'responses'; + /** * @var array $answer_form_responses */ - private readonly array $answer_form_responses; + private array $answer_form_responses; /** * @param array $answer_form_responses @@ -41,7 +45,7 @@ public function __construct( private readonly Uuid $question_id, private readonly Uuid $attempt_id, private readonly \DateTimeImmutable $create_date, - private ?float $reached_points, + private ?float $awarded_points = null, array $answer_form_responses = [] ) { $this->answer_form_responses = array_reduce( @@ -54,9 +58,27 @@ function (array $c, AnswerFormResponse $v): array { ); } - public function getReachedPoints(): float + public function getId(): Uuid + { + return $this->id; + } + + public function getQuestionId(): Uuid + { + return $this->question_id; + } + + public function getAwardedPoints(): ?float { - return $this->reached_points; + return $this->awarded_points; + } + + public function withAwardedPoints( + float $awarded_points + ): self { + $clone = clone $this; + $clone->awarded_points = $awarded_points; + return $clone; } public function getCreateDate(): \DateTimeImmutable @@ -64,6 +86,12 @@ public function getCreateDate(): \DateTimeImmutable return $this->create_date; } + public function getAnswerFormResponse( + Uuid $answer_form_id + ): ?AnswerFormResponse { + return $this->answer_form_responses[$answer_form_id->toString()] ?? null; + } + public function withAnswerFormResponse( AnswerFormResponse $response ): self { @@ -72,6 +100,17 @@ public function withAnswerFormResponse( return $clone; } + public function toPreviewStorage(): array + { + return [ + self::KEY_POINTS => $this->awarded_points, + self::KEY_RESPONSES => array_map( + fn(AnswerFormResponse $v): array => $v->toPreviewStorage(), + $this->answer_form_responses + ) + ]; + } + public function toStorage( PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, @@ -151,7 +190,7 @@ private function buildValuesArrayForStorage( ), $persistence_factory->value( FieldDefinition::T_FLOAT, - $this->reached_points + $this->awarded_points ), $persistence_factory->value( FieldDefinition::T_INTEGER, diff --git a/components/ILIAS/Questions/src/Attempt/TableDefinitions.php b/components/ILIAS/Questions/src/Attempt/TableDefinitions.php index 3e8084119d02..f60fa886fa7e 100644 --- a/components/ILIAS/Questions/src/Attempt/TableDefinitions.php +++ b/components/ILIAS/Questions/src/Attempt/TableDefinitions.php @@ -21,8 +21,10 @@ namespace ILIAS\Questions\Attempt; use ILIAS\Questions\Persistence\Column; +use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\JoinType; +use ILIAS\Questions\Persistence\OrderDirection; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -51,7 +53,7 @@ class TableDefinitions 'id', 'attempt_id', 'question_id', - 'reached_points', + 'awarded_points', 'create_timestamp' ]; @@ -126,7 +128,7 @@ public function getForeignKeyColumn( }; } - public function completeLoadAttemptQuery( + public function completeAttemptQuery( Query $query, ?Column $base_table_id_column ): Query { @@ -187,11 +189,14 @@ public function completeLoadAttemptQuery( ) )->withAdditionalOrder( $this->persistence_factory->order( - $this->persistence_factory->table( - $table_names_builder, - TableTypes::Responses + $this->persistence_factory->column( + $this->persistence_factory->table( + $table_names_builder, + TableTypes::Responses + ), + self::RESPONSES_TABLE_ADDITIONAL_ORDERING_COLUMN ), - self::RESPONSES_TABLE_ADDITIONAL_ORDERING_COLUMN + OrderDirection::Asc ) ); } diff --git a/components/ILIAS/Questions/src/DefaultPublicInterface.php b/components/ILIAS/Questions/src/DefaultPublicInterface.php index 5cf56482f9eb..3332591b11a3 100644 --- a/components/ILIAS/Questions/src/DefaultPublicInterface.php +++ b/components/ILIAS/Questions/src/DefaultPublicInterface.php @@ -61,26 +61,31 @@ public function __construct( private readonly Repository $questions_repository, private readonly AttemptRepository $attempt_repository, private readonly LayoutFactory $layout_factory, - private readonly CapabilitiesFactory $capabilities_factory, - private CapabilitiesEditView $capabilities_edit_view + private readonly CapabilitiesFactory $capabilities_factory ) { } #[\Override] public function getParticipantView( + array $required_capabilities_class_names, int $owner_obj_id ): Participant { return new Participant( + $this->lng, + $this->refinery, $this->ui_factory, - $this->capabilities_factory, + $this->http, $this->questions_repository, $this->attempt_repository, + $this->capabilities_factory, + $required_capabilities_class_names, $owner_obj_id ); } #[\Override] public function getEditView( + array $required_capabilities_class_names, int $owner_obj_id ): Edit { return new Edit( @@ -100,8 +105,10 @@ public function getEditView( $this->configuration_repository, $this->answer_form_factory, $this->questions_repository, + $this->attempt_repository, $this->layout_factory, - $this->capabilities_edit_view, + $this->capabilities_factory, + $required_capabilities_class_names, $owner_obj_id ); } diff --git a/components/ILIAS/Questions/src/Persistence/Delete.php b/components/ILIAS/Questions/src/Persistence/Delete.php index 7abec2bc259e..5bf813a4ccae 100644 --- a/components/ILIAS/Questions/src/Persistence/Delete.php +++ b/components/ILIAS/Questions/src/Persistence/Delete.php @@ -52,10 +52,10 @@ private function buildWhereString( function (?string $c, Where $v) use ($db, &$values): string { $quoted_value = $v->getRight()->getQuotedValue($db); if (is_array($quoted_value)) { - $values = array_merge( - $values, - array_values($quoted_value) - ); + $values = [ + ...$values, + ...array_values($quoted_value) + ]; } else { $values[] = $quoted_value; } diff --git a/components/ILIAS/Questions/src/Persistence/Update.php b/components/ILIAS/Questions/src/Persistence/Update.php index 0631f6099b59..19b20ec78299 100644 --- a/components/ILIAS/Questions/src/Persistence/Update.php +++ b/components/ILIAS/Questions/src/Persistence/Update.php @@ -85,10 +85,10 @@ private function buildWhereString( function (?string $c, Where $v) use ($db, &$values): string { $quoted_value = $v->getRight()->getQuotedValue($db); if (is_array($quoted_value)) { - $values = array_merge( - $values, - array_values($quoted_value) - ); + $values = [ + ...$values, + ...array_values($quoted_value) + ]; } else { $values[] = $quoted_value; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index c5d226624c94..4d7423cfd80c 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -20,7 +20,8 @@ namespace ILIAS\Questions\Presentation\Definitions; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalFormStepAction; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; use ILIAS\Questions\Presentation\Views\Edit; @@ -31,6 +32,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Component\Table\Action\Action as TableAction; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -80,7 +82,7 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly Factory $presentation_factory, private readonly Editability $editability, - private readonly array $required_capabilities, + private readonly RequiredCapabilities $required_capabilities, private readonly int $owner_obj_id, URI $base_uri ) { @@ -211,31 +213,19 @@ public function getEditability(): Editability } #[\Override] - public function isCapabilityRequired( - string $capability - ): bool { - return array_key_exists( - $capability, - $this->required_capabilities - ); + public function isMarkingRequired(): bool + { + return $this->required_capabilities->isMarkingRequired(); } #[\Override] public function getAnswerFormTableActionsForRequiredCapabilities(): array { - return array_reduce( - $this->required_capabilities, - function (array $c, Capability $v): array { - $action = $v->getAnswerFormEditAdditionalStep($this); - if ($action !== null) { - $c[] = $action->getAsTableAction( - $this->withActionParameter($action->getIdentifier()) - ); - } - - return $c; - }, - [] + return array_map( + fn(AdditionalFormStepAction $v): TableAction => $v->getAsTableAction( + $this->withActionParameter($v->getIdentifier()) + ), + $this->required_capabilities->getRequiredFormStepActions() ); } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php index 0714f978b9bc..5ca27a0dd621 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Environment.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Presentation\Definitions; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Layout\Factory; use ILIAS\Data\UUID\Uuid; @@ -65,9 +66,7 @@ public function withDefaultSubAction(): self; public function getEditability(): Editability; - public function isCapabilityRequired( - string $capability - ): bool; + public function isMarkingRequired(): bool; public function getAnswerFormTableActionsForRequiredCapabilities(): array; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php index 34e257e25050..fc116167f482 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/OverviewTableColumns.php @@ -44,7 +44,7 @@ public static function getTableColums( => $column_factory->link($lng->txt('title')), OverviewTableColumns::AnswerFormTypes->value => $column_factory->text( - $lng->txt('contained_types') + $lng->txt('contained_answer_form_types') )->withIsOptional(true, true) ->withIsSortable(false), ]; @@ -60,7 +60,7 @@ public static function getFilterInputs( $lng->txt('title') ), self::AnswerFormTypes->value => $field_factory->multiSelect( - $lng->txt('contains_type'), + $lng->txt('contains_answer_form_types'), $answer_form_types_array_for_select )->withRequired(true), ]; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php b/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php index f7ca340fb0d8..ab49fdbe8576 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php @@ -20,9 +20,9 @@ namespace ILIAS\Questions\Presentation\Definitions; -enum Editability +enum ViewMode { - case ReadOnly; - case Limited; - case Full; + case Respond; + case ViewResponse; + case ViewBestResponse; } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index 70913bd458ed..bfd7f853f73f 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -50,7 +50,7 @@ private function buildBasicAnswerFormPanel(): StandardPanel $this->environment->getUIFactory()->listing()->descriptive( $this->environment->getAnswerFormProperties() ->getBasicPropertiesForListing( - $this->environment->getLanguage() + $this->environment ) ) ]; diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 6fefffcf203c..799c17a44ddd 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\Presentation\Layout; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; @@ -41,7 +42,7 @@ public function __construct( private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, private readonly DefaultEnvironment $environment, - private readonly array $required_capabilities + private readonly RequiredCapabilities $required_capabilities ) { $environment->getLanguage()->loadLanguageModule('qpl'); } diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 03549db0af91..33991505991b 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -20,22 +20,24 @@ namespace ILIAS\Questions\Presentation\Views; -use ILIAS\Questions\AnswerForm\Capabilities\Edit as CapabilitiesEditView; +use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\AnswerForm\Capabilities\Factory as CapabilitiesFactory; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Definition; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; -use ILIAS\Questions\Administration\ConfigurationRepository; -use ILIAS\Questions\Presentation\Layout\Async; -use ILIAS\Questions\Presentation\Layout\EditForm; -use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; -use ILIAS\Questions\Presentation\Layout\Viewable; +use ILIAS\Questions\Attempt\Repository as AttemptRepository; +use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; -use ILIAS\Questions\Presentation\Layout\QuestionsTable; +use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Questions\Presentation\Layout\EditForm; +use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; use ILIAS\Questions\Presentation\Layout\GlobalScreen\LayoutProvider; +use ILIAS\Questions\Presentation\Layout\QuestionsTable; +use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Questions\Question\Question; use ILIAS\Questions\UserSettings\CreateModes; @@ -65,6 +67,11 @@ class Edit private Editability $editability = Editability::Full; private bool $ordering_enabled = false; + private readonly RequiredCapabilities $required_capabilities; + + /** + * @param list> $capability_identifiers + */ public function __construct( private readonly Language $lng, private readonly \ilObjUser $current_user, @@ -82,19 +89,15 @@ public function __construct( private readonly ConfigurationRepository $configuration_repository, private readonly AnswerFormFactory $answer_form_factory, private readonly Repository $questions_repository, + private readonly AttemptRepository $attempt_repository, private readonly LayoutFactory $layout_factory, - private CapabilitiesEditView $capabilities_edit_view, + private readonly CapabilitiesFactory $capabilities_factory, + array $capability_identifiers, private readonly int $owner_object_id ) { - } - - public function withRequiredCapabilities( - array $capability_class_names - ): self { - $clone = clone $this; - $clone->capabilities_edit_view = $this->capabilities_edit_view - ->withRequiredCapabilities($capability_class_names); - return $clone; + $this->required_capabilities = $this->capabilities_factory->get( + $capability_identifiers + ); } public function withEditable( @@ -166,7 +169,12 @@ public function forwardPageCmds( $this->questions_repository->getForQuestionId( $environment->getQuestionId() ), - $this->owner_object_id + $this->owner_object_id, + $this->required_capabilities, + true, + false, + false, + false )->withEditView( $this )->withReturnURI( @@ -241,7 +249,7 @@ public function getEditAnswerForm( $action = $environment->getAction(); $edit_view = $type_definition->getEditView(); - $from_capabilites = $this->capabilities_edit_view->edit( + $from_capabilites = $this->required_capabilities->edit( $this->tabs_gui, $environment, $edit_view, @@ -274,7 +282,7 @@ public function getEditAnswerForm( $from_edit_view = $edit_view->edit($environment); if ($from_edit_view instanceof EditForm - && $this->capabilities_edit_view->providesAnswerFormEditAdditionalSteps()) { + && $this->required_capabilities->additionalAnswerFormStepsRequired()) { return $from_edit_view->withIsFinalStep(false)->getUI(); } @@ -294,7 +302,7 @@ public function getEditAnswerForm( ); } - $return_form_step_actions = $this->capabilities_edit_view + $return_form_step_actions = $this->required_capabilities ->doFirstFormStepAction( $environment->withAnswerFormProperties($from_edit_view), $edit_view @@ -331,9 +339,12 @@ private function createQuestion( )->getEditView( $this->current_user, $this->ctrl, + $this->http->wrapper()->post(), $this->ui_renderer, + $this->uuid_factory, $this->configuration_repository, - $this->capabilities_edit_view->getRequiredCapabilities() + $this->attempt_repository, + $this->required_capabilities )->create( $environment->withActionParameter(self::ACTION_CREATE_QUESTION) ); @@ -370,9 +381,12 @@ private function editQuestion( $edit = $question->getEditView( $this->current_user, $this->ctrl, + $this->http->wrapper()->post(), $this->ui_renderer, + $this->uuid_factory, $this->configuration_repository, - $this->capabilities_edit_view->getRequiredCapabilities() + $this->attempt_repository, + $this->required_capabilities )->edit( $environment_with_question_parameter ->withActionParameter(self::ACTION_EDIT_QUESTION) @@ -414,7 +428,7 @@ private function deleteQuestions( return $environment->getPresentationFactory()->getAsync( $this->ui_factory->modal()->interruptive( $this->lng->txt('confirm'), - $this->lng->txt('qpl_confirm_delete_questions'), + $this->lng->txt('confirm_delete_questions'), $environment->withActionParameter( self::ACTION_DELETE_QUESTIONS )->withSubActionParameter( @@ -434,7 +448,7 @@ private function showTable( $this->answer_form_factory, $this->questions_repository, $environment, - $this->capabilities_edit_view->getRequiredCapabilities() + $this->required_capabilities )->withCreateQuestionButton( $this->ui_factory->button()->primary( $this->lng->txt('create'), @@ -492,7 +506,7 @@ private function forwardCreateAnswerFormCmd( ): ?EditForm { $action = $environment->getAction(); - $from_capabilites = $this->capabilities_edit_view->edit( + $from_capabilites = $this->required_capabilities->edit( $this->tabs_gui, $environment, $answer_form_edit_view, @@ -523,13 +537,13 @@ private function forwardCreateAnswerFormCmd( if ($from_edit_view instanceof EditForm) { return $this->addSaveAndNewToAnswerFormCreateIfNeeded( $environment, - $this->capabilities_edit_view->providesAnswerFormEditAdditionalSteps() + $this->required_capabilities->additionalAnswerFormStepsRequired() ? $from_edit_view->withIsFinalStep(false) : $from_edit_view ); } - $from_capabilities_first_step = $this->capabilities_edit_view + $from_capabilities_first_step = $this->required_capabilities ->doFirstFormStepAction( $environment->withAnswerFormProperties($from_edit_view), $answer_form_edit_view @@ -580,7 +594,7 @@ private function updateAnswerFormAndRedirect( [$question->withAnswerFormProperties($properties)] ); - $this->capabilities_edit_view->onAnswerFormUpdate($properties); + $this->required_capabilities->onAnswerFormUpdate($properties); $this->ctrl->redirectToURL( $environment->getUrlBuilder()->buildURI()->__toString() @@ -637,12 +651,12 @@ private function buildQuestionListSlate( DefaultEnvironment $environment ): LegacySlate { return $this->ui_factory->mainControls()->slate()->legacy( - $this->lng->txt('mainbar_button_label_questionlist'), + $this->lng->txt('questionlist'), $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), $this->ui_factory->legacy()->content( $this->ui_renderer->render( $this->ui_factory->panel()->secondary()->listing( - $this->lng->txt('mainbar_button_label_questionlist'), + $this->lng->txt('questionlist'), [ $this->buildItemGroupForQuestionListSlate($environment) ] @@ -683,9 +697,12 @@ private function buildEditStartView( return $question->getEditView( $this->current_user, $this->ctrl, + $this->http->wrapper()->post(), $this->ui_renderer, + $this->uuid_factory, $this->configuration_repository, - $this->capabilities_edit_view->getRequiredCapabilities() + $this->attempt_repository, + $this->required_capabilities )->edit( $environment ); @@ -792,7 +809,7 @@ private function buildEnvironment( $this->uuid_factory, $this->layout_factory, $this->editability, - $this->capabilities_edit_view->getRequiredCapabilities(), + $this->required_capabilities, $this->owner_object_id, $base_uri ); diff --git a/components/ILIAS/Questions/src/Presentation/Views/Participant.php b/components/ILIAS/Questions/src/Presentation/Views/Participant.php index e94b179685eb..5e4c8eeda37f 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Participant.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Participant.php @@ -21,27 +21,44 @@ namespace ILIAS\Questions\Presentation\Views; use ILIAS\Questions\AnswerForm\Capabilities\Factory as CapabilitiesFactory; -use ILIAS\Questions\AnswerForm\Capabilities\Marking\Marking; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\Capability as TextFeedback; +use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Attempt\Repository as AttemptRepository; +use ILIAS\Questions\Attempt\Response; use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; use ILIAS\Questions\Question\Views\Participant as QuestionParticipantView; use ILIAS\Data\UUID\Uuid; +use ILIAS\HTTP\Services as HTTP; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; class Participant { - private array $required_capabilities = []; + private readonly RequiredCapabilities $required_capabilities; private bool $shuffle_question_order = false; + private array $attempt_cache = []; + + /** + * @param list> $capability_identifiers + */ public function __construct( + private readonly Language $lng, + private readonly Refinery $refinery, private readonly UIFactory $ui_factory, - private readonly CapabilitiesFactory $capabilities_factory, + private readonly HTTP $http, private readonly QuestionRepository $question_repository, private readonly AttemptRepository $attempt_repository, + private readonly CapabilitiesFactory $capabilities_factory, + array $capability_identifiers, private readonly int $owner_object_id ) { - + $this->required_capabilities = $this->capabilities_factory->get( + $capability_identifiers + ); } public function getPresentationIdentifier(): Uuid @@ -49,28 +66,6 @@ public function getPresentationIdentifier(): Uuid return $this->presentation_identifier; } - public function withRequiredCapabilities( - array $capability_class_names - ): self { - $clone = clone $this; - $clone->required_capabilities = array_reduce( - $capability_class_names, - function (array $c, string $v): array { - $c[$v] = $this->capabilities_factory->get($v); - - if ($c[$v] === null) { - throw new \InvalidArgumentException( - "The capability {$v} does not exist." - ); - } - - return $c; - }, - [] - ); - return $clone; - } - public function withShuffleQuestionOrder( bool $shuffle_question_order ): self { @@ -81,32 +76,47 @@ public function withShuffleQuestionOrder( public function getQuestionView( Uuid $question_id, - ?Uuid $attempt_id = null, - bool $interactive = true, - bool $show_marks = false, - bool $show_correct_solution = false + ?Uuid $attempt_id, + bool $interactive, + bool $show_marks, + bool $show_best_response, + bool $show_feedback ): QuestionParticipantView { $question = $this->question_repository->getForQuestionId( $question_id ); + $marking_required = $this->required_capabilities->isMarkingRequired(); + + if ($attempt_id === null + || !isset($this->attempt_cache[$attempt_id->toString()][$question_id->toString()])) { + $attempt = $this->attempt_repository->getAttemptFor( + $attempt_id, + [$question] + ); + $attempt_id = $attempt->getId(); + $this->attempt_cache[$attempt_id->toString()][$question_id->toString()] = $attempt; + } + return $question->getParticipantView( + $this->lng, + $this->refinery, $this->ui_factory, $this->required_capabilities, - $this->attempt_repository->getAttemptFor( - $attempt_id, - [$question] - ), + $this->attempt_cache[$attempt_id->toString()][$question_id->toString()], $interactive, - $show_marks && in_array(Marking::class, $this->required_capabilities), - $show_correct_solution && in_array(Marking::class, $this->required_capabilities) + $show_marks && $marking_required, + $show_best_response && $marking_required, + $show_feedback && $this->required_capabilities->isCapabilityRequired( + TextFeedback::getIdentifier() + ) ); } public function persistResponse( Uuid $question_id, Uuid $attempt_id - ): Uuid { + ): void { $question = $this->question_repository->getForQuestionId( $question_id ); @@ -122,16 +132,42 @@ public function persistResponse( ); } - $current_reponse = $attempt_data->getResponseFor($question_id); - if ($current_reponse === null) { - $current_response = $this->attempt_repository->getNewResponseFor( - $question_id, - $attempt_id + $response = $this->attempt_repository->getNewResponseFor( + $question_id, + $attempt_id + ); + + $response_with_values_from_post = array_reduce( + $question->retrieveAnswerFormResponsesFromPost( + $this->required_capabilities, + $this->http->wrapper()->post(), + $response->getId() + ), + fn(Response $c, AnswerFormResponse $v): Response + => $c->withAnswerFormResponse($v), + $response + ); + + if ($this->required_capabilities->isMarkingRequired()) { + $response_with_values_from_post = $question->addAwardedPointsToResponse( + $response_with_values_from_post ); } - foreach ($this->question->getAnswerFormProperties() as $property) { - $property->getDefinition()->getParticipantView()->retrieveResponse(); - } + $this->attempt_repository->storeResponse( + $response_with_values_from_post + ); + } + + public function deleteResponsesFor( + Uuid $attempt_id, + Uuid $question_id + ): void { + $this->attempt_repository->deleteResponsesFor( + $attempt_id, + $this->question_repository->getForQuestionId( + $question_id + ) + ); } } diff --git a/components/ILIAS/Questions/src/PublicInterface.php b/components/ILIAS/Questions/src/PublicInterface.php index 0da4f5fdc98a..78902c6bc939 100644 --- a/components/ILIAS/Questions/src/PublicInterface.php +++ b/components/ILIAS/Questions/src/PublicInterface.php @@ -20,16 +20,27 @@ namespace ILIAS\Questions; +use ILIAS\Questions\AnswerForm\Capabilities\Capability; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Presentation\Views\Participant; interface PublicInterface { + /** + * + * @param list> $required_capabilities_class_names + */ public function getParticipantView( + array $required_capabilities_class_names, int $owner_obj_id ): Participant; + /** + * + * @param list> $required_capabilities_class_names + */ public function getEditView( + array $required_capabilities_class_names, int $owner_obj_id ): Edit; } diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 82006d0193c4..b8384e0a817d 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -21,9 +21,14 @@ namespace ILIAS\Questions\Question; use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\AnswerForm\Capabilities\MarkingAllowingPartialPoints\Capability as MarkingAllowingPartialPoints; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Definition as AnswerFormDefinition; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Attempt\Repository as AttemptRepository; +use ILIAS\Questions\Attempt\Response; use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; @@ -33,8 +38,11 @@ use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\Persistence\DatabaseStatementBuilder; use ILIAS\Questions\UserSettings\CreateModes; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Link\Factory as LinkFactory; use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Component\Table\DataRowBuilder; @@ -264,11 +272,14 @@ public function getListOfContainedAnswerFormTypeLabels( public function getEditView( \ilObjUser $current_user, \ilCtrl $ctrl, + RequestWrapper $post_wrapper, UIRenderer $ui_renderer, + UuidFactory $uuid_factory, ConfigurationRepository $configuration_repository, - array $required_capabilities + AttemptRepository $attempt_repository, + RequiredCapabilities $required_capabilities ): Views\Edit { - if (!$this->supportsRequiredCapabilities($required_capabilities)) { + if (!$required_capabilities->areAllCapabilitiesSupportedByAnswerForms($this->answer_forms)) { throw new \UnexpectedValueException( "The Question does not support all required Capabilities." ); @@ -277,35 +288,44 @@ public function getEditView( return new Views\Edit( $current_user, $ctrl, + $post_wrapper, $ui_renderer, + $uuid_factory, $configuration_repository, + $attempt_repository, $required_capabilities, $this ); } public function getParticipantView( + Language $lng, + Refinery $refinery, UIFactory $ui_factory, - array $required_capabilities, + RequiredCapabilities $required_capabilities, ?Attempt $attempt_data, bool $interactive = true, bool $show_marks = false, - bool $show_correct_solution = false + bool $show_best_response = false, + bool $show_feedback = false ): Views\Participant { - if (!$this->supportsRequiredCapabilities($required_capabilities)) { + if (!$required_capabilities->areAllCapabilitiesSupportedByAnswerForms($this->answer_forms)) { throw new \UnexpectedValueException( "The Question does not support all required Capabilities." ); } return new Views\Participant( + $lng, + $refinery, $ui_factory, $required_capabilities, $this, $attempt_data, $interactive, $show_marks, - $show_correct_solution + $show_best_response, + $show_feedback ); } @@ -336,9 +356,9 @@ public function toEditLink( public function toTableRow( DataRowBuilder $row_builder, DefaultEnvironment $environment, - array $required_capabilities + RequiredCapabilities $required_capabilities ): ?DataRow { - if (!$this->supportsRequiredCapabilities($required_capabilities)) { + if (!$required_capabilities->areAllCapabilitiesSupportedByAnswerForms($this->answer_forms)) { return null; } @@ -447,13 +467,86 @@ public function retrieveAnswerFormResponsesFromQuery( Query $query ): array { return array_map( - fn(AnswerFormProperties $v): Response => $v + fn(AnswerFormProperties $v): AnswerFormResponse => $v ->getDefinition() - ->buildResponse($query), + ->buildResponse( + $response_id, + $v, + $query + ), + $this->answer_forms + ); + } + + public function retrieveAnswerFormResponsesFromPost( + RequiredCapabilities $required_capabilities, + RequestWrapper $post_wrapper, + Uuid $response_id + ): array { + return array_map( + fn(AnswerFormProperties $v): AnswerFormResponse => $required_capabilities + ->getParticipantViewProvider() + ->getParticipantView($v) + ->retrieveResponse( + $response_id, + $v, + $post_wrapper + ), $this->answer_forms ); } + public function retrieveAnswerFormResponsesFromPreviewData( + Uuid $response_id, + array $preview_data + ): array { + return array_map( + fn(AnswerFormProperties $v): AnswerFormResponse => $v + ->getDefinition() + ->buildResponseFromPreviewData( + $response_id, + $v, + $preview_data[$v->getAnswerFormId()->toString()] ?? [] + ), + $this->answer_forms + ); + } + + public function addAwardedPointsToResponse( + Response $response + ): Response { + return $response->withAwardedPoints( + array_reduce( + $this->answer_forms, + function (?float $c, AnswerFormProperties $v) use ($response): ?float { + /** @var Marking $marking */ + $marking = $v->getDefinition()->getCapability( + MarkingAllowingPartialPoints::getIdentifier() + ); + + if ($marking === null) { + return $c; + } + + $points_from_form = $marking->calculateAwardedPoints( + $v, + $response->getAnswerFormResponse($v->getAnswerFormId()) + ); + + if ($points_from_form === null) { + return $c; + } + + if ($c === null) { + return $points_from_form; + } + + return $c + $points_from_form; + } + ) + ); + } + private function getListOfContainedAnswerFormTypes(): array { return array_reduce( @@ -471,18 +564,4 @@ function ( [] ); } - - private function supportsRequiredCapabilities( - array $required_capabilities - ): bool { - foreach ($this->answer_forms as $property) { - foreach ($required_capabilities as $capability) { - if (!$capability->isAvailableFor($property)) { - return false; - } - } - } - - return true; - } } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 31e08cba4d6a..b64d5ae79fae 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -20,12 +20,19 @@ namespace ILIAS\Questions\Question\Views; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; +use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Administration\ConfigurationRepository; +use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Attempt\Repository as AttemptRepository; +use ILIAS\Questions\Attempt\Response; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Question\Question; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\UserSettings\CreateModes; +use ILIAS\Data\UUID\Factory as UuidFactory; +use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Language\Language; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; use ILIAS\UI\Component\Panel\Standard as StandardPanel; @@ -36,13 +43,20 @@ class Edit { private const string CMD_SAVE_QUESTION = 'sq'; + private const string CMD_RESET_PREVIEW = 'rp'; + private const string CMD_SEND_RESPONSE = 'sr'; + + private const string SESSION_VAR_RESPONSE_DATA = 'response_data'; public function __construct( private readonly \ilObjUser $current_user, private readonly \ilCtrl $ctrl, + private readonly RequestWrapper $post_wrapper, private readonly UIRenderer $ui_renderer, + private readonly UuidFactory $uuid_factory, private readonly ConfigurationRepository $configuration_repository, - private readonly array $required_capabilities, + private readonly AttemptRepository $attempt_repository, + private readonly RequiredCapabilities $required_capabilities, private readonly Question $question ) { @@ -66,6 +80,12 @@ public function edit( self::CMD_SAVE_QUESTION => $this->processBasicPropertiesEditingForm( $environment ), + self::CMD_SEND_RESPONSE => $this->sendResponse( + $environment + ), + self::CMD_RESET_PREVIEW => $this->resetPreview( + $environment + ), default => $this->buildBasicPropertiesEditingForm($environment) ->withContentAfterForm( $this->buildPreviewPanel($environment) @@ -227,24 +247,179 @@ private function buildPreviewPanel( DefaultEnvironment $environment ): StandardPanel { $environment->preserveParametersForPageEditorCmds(); + + $session_key = $this->buildPreviewSessionKey(); + + $attempt_data = $this->attempt_repository->getAttemptFromPreviewData( + $this->question, + \ilSession::get($session_key) ?? '' + ); + + if (!\ilSession::has($session_key)) { + \ilSession::set( + $session_key, + $attempt_data->toPreviewStorage() + ); + } + return $environment->getUIFactory()->panel()->standard( $environment->getLanguage()->txt('preview'), - $environment->getUIFactory()->legacy()->content( - $this->ui_renderer->render( - $this->question->getParticipantView( - $environment->getUIFactory(), - $this->required_capabilities, - null - )->getUI() - ) - ) - )->withActions( - $environment->getUIFactory()->dropdown()->standard([ - $environment->getUIFactory()->link()->standard( + [ + $environment->getUIFactory()->button()->primary( $environment->getLanguage()->txt('edit'), $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') + ), + $environment->getUIFactory()->button()->standard( + $environment->getLanguage()->txt('reset_preview'), + $environment + ->withSubActionParameter(self::CMD_RESET_PREVIEW) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ), + ...$this->buildPreviewPanelQuestionContent( + $environment, + $attempt_data ) - ]) + ] ); } + + private function buildPreviewPanelQuestionContent( + DefaultEnvironment $environment, + Attempt $attempt_data + ): array { + $content = [ + $environment->getUIFactory()->legacy()->content( + $this->buildQuestionForm( + $environment, + $attempt_data + ) + ) + ]; + + if ($attempt_data->getResponseForQuestion($this->question->getId()) !== null) { + $content[] = $environment->getUIFactory()->divider()->horizontal(); + $content[] = $environment->getUIFactory()->panel()->standard( + $environment->getLanguage()->txt('feedback'), + $this->question->getParticipantView( + $environment->getLanguage(), + $environment->getRefinery(), + $environment->getUIFactory(), + $this->required_capabilities, + $attempt_data, + false, + true, + true, + true + )->getUI() + ); + } + + return $content; + } + + private function buildQuestionForm( + DefaultEnvironment $environment, + Attempt $attempt_data + ): string { + $tpl = new \ilTemplate( + 'tpl.qsts_preview_presentation_interactive.html', + true, + true, + 'components/ILIAS/Questions' + ); + + $tpl->setVariable( + 'FORM_ACTION', + $environment + ->withSubActionParameter(self::CMD_SEND_RESPONSE) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + + $tpl->setVariable( + 'QUESTION_OUTPUT', + $this->ui_renderer->render( + $this->question->getParticipantView( + $environment->getLanguage(), + $environment->getRefinery(), + $environment->getUIFactory(), + $this->required_capabilities, + $attempt_data + )->getUI() + ) + ); + + $tpl->setVariable( + 'SUBMIT_BUTTON_LABEL', + $environment->getLanguage()->txt('send') + ); + + return $tpl->get(); + } + + private function sendResponse( + DefaultEnvironment $environment + ) { + $session_key = $this->buildPreviewSessionKey(); + $attempt = $this->attempt_repository->getAttemptFromPreviewData( + $this->question, + \ilSession::get($session_key) ?? '' + ); + + \ilSession::set( + $session_key, + $attempt->withResponse( + $this->retrieveResponse() + )->toPreviewStorage() + ); + + return $this->buildBasicPropertiesEditingForm($environment) + ->withContentAfterForm( + $this->buildPreviewPanel($environment) + ); + } + + private function resetPreview( + DefaultEnvironment $environment + ): EditForm { + \ilSession::clear($this->buildPreviewSessionKey()); + return $this->buildBasicPropertiesEditingForm($environment) + ->withContentAfterForm( + $this->buildPreviewPanel($environment) + ); + } + + private function retrieveResponse(): Response + { + $response = $this->attempt_repository->getNewResponseFor( + $this->question->getId(), + $this->uuid_factory->uuid4() + ); + + $response_with_data = array_reduce( + $this->question->retrieveAnswerFormResponsesFromPost( + $this->required_capabilities, + $this->post_wrapper, + $response->getId() + ), + fn(Response $c, AnswerFormResponse $v): Response + => $c->withAnswerFormResponse($v), + $response + ); + + if ($this->required_capabilities->isMarkingRequired()) { + return $this->question->addAwardedPointsToResponse($response_with_data); + } + + return $response_with_data; + } + + private function buildPreviewSessionKey(): string + { + return self::SESSION_VAR_RESPONSE_DATA + . "_{$this->question->getId()->toString()}"; + } } diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index 7400e9116f4b..3533c364d733 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -20,51 +20,67 @@ namespace ILIAS\Questions\Question\Views; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Question\Question; use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Legacy\Content as LegacyContent; class Participant implements Viewable { public function __construct( + private readonly Language $lng, + private readonly Refinery $refinery, private readonly UIFactory $ui_factory, - private readonly array $required_capabilities, + private readonly RequiredCapabilities $required_capabilities, private readonly Question $question, private readonly ?Attempt $attempt_data, private readonly bool $interactive, private readonly bool $show_marks, - private readonly bool $show_correct_solution + private readonly bool $show_best_response, + private readonly bool $show_feedback ) { } public function getAttemptId(): ?Uuid { - return $this->attempt_data?->getIdentifier(); + return $this->attempt_data?->getId(); } #[\Override] - public function getUI(): LegacyContent + public function getUI(): array { - $tpl = new \ilTemplate( - 'tpl.qsts_question_presentation.html', - true, - true, - 'components/ILIAS/Questions' - ); - $question_page = new \QstsQuestionPageGUI( $this->question, - $this->question->getParentObjId() + $this->question->getParentObjId(), + $this->required_capabilities, + $this->interactive, + $this->show_best_response, + $this->show_feedback )->withAttemptData($this->attempt_data); $question_page->setPresentationTitle($this->question->getTitle()); - $tpl->setVariable( - 'QUESTION_OUTPUT', + + if ($this->show_marks) { + $content[] = $this->ui_factory->listing()->characteristicValue()->text( + [ + $this->lng->txt('awarded_points') => $this->refinery->kindlyTo()->string()->transform( + $this->attempt_data?->getResponseForQuestion( + $this->question->getId() + )?->getAwardedPoints() ?? 0 + ) + ] + ); + $content[] = $this->ui_factory->divider()->horizontal(); + } + + $content[] = $this->ui_factory->legacy()->content( $question_page->presentation() ); - return $this->ui_factory->legacy()->content($tpl->get()); + + return $content; } } diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index 171308453395..ccbb9ae6dcb4 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -289,8 +289,12 @@ public function step_6(): void ]); } - if (!$this->db->primaryExistsByFields($table_name, ['response_id'])) { - $this->db->addPrimaryKey($table_name, ['response_id']); + if (!$this->db->primaryExistsByFields($table_name, ['response_id', 'answer_input_id'])) { + $this->db->addPrimaryKey($table_name, ['response_id', 'answer_input_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['response_id'])) { + $this->db->addIndex($table_name, ['response_id'], 'r'); } } } diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php index f321f885239e..82ce74a091e2 100644 --- a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -22,7 +22,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableTypes; use ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent\TableTypes as SuggestedContentTableTypes; -use ILIAS\Questions\AnswerForm\Capabilities\Feedback\TableTypes as FeedbackTableTypes; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\TableTypes as TextFeedbackTableTypes; use ILIAS\Questions\Attempt\TableTypes as AttemptTableTypes; use ILIAS\Questions\Question\Persistence\TableTypes as QuestionTableTypes; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -187,7 +187,7 @@ public function step_3(): void 'length' => 8, 'notnull' => true ], - 'reached_points' => [ + 'awarded_points' => [ 'type' => FieldDefinition::T_FLOAT, 'notnull' => false ] @@ -276,7 +276,7 @@ public function step_5(): void public function step_6(): void { $table_name = $this->basic_table_names_builder->getTableNameFor( - FeedbackTableTypes::FeedbackGeneric + TextFeedbackTableTypes::FeedbackGeneric ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ @@ -318,7 +318,7 @@ public function step_6(): void public function step_7(): void { $table_name = $this->basic_table_names_builder->getTableNameFor( - FeedbackTableTypes::FeedbackSpecific + TextFeedbackTableTypes::FeedbackSpecific ); if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ diff --git a/components/ILIAS/Questions/src/Setup/TempTables.php b/components/ILIAS/Questions/src/Setup/TempTables.php index ab2f045c3ad9..61662c0125af 100644 --- a/components/ILIAS/Questions/src/Setup/TempTables.php +++ b/components/ILIAS/Questions/src/Setup/TempTables.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Setup; -use QstsTempAttemptRepository; +use ILIAS\Questions\Temp\AttemptRepository; use ILIAS\Database\FieldDefinition; class TempTables implements \ilDatabaseUpdateSteps @@ -36,7 +36,7 @@ public function prepare( public function step_1(): void { - $table_name = QstsTempAttemptRepository::TABLE_NAME; + $table_name = AttemptRepository::TABLE_NAME; if (!$this->db->tableExists($table_name)) { $this->db->createTable($table_name, [ @@ -49,6 +49,12 @@ public function step_1(): void 'type' => FieldDefinition::T_TEXT, 'length' => 64, 'notnull' => true + ], + 'solved_questions' => [ + 'type' => FieldDefinition::T_TEXT, + 'length' => 4000, + 'notnull' => true, + 'default' => '' ] ]); } diff --git a/components/ILIAS/Questions/src/UserSettings/CreateMode.php b/components/ILIAS/Questions/src/UserSettings/CreateMode.php index 437d76125b97..e44c5266b3b6 100644 --- a/components/ILIAS/Questions/src/UserSettings/CreateMode.php +++ b/components/ILIAS/Questions/src/UserSettings/CreateMode.php @@ -83,7 +83,7 @@ public function getInput( $v->getBylineForInput($lng) ), $field_factory->radio( - $lng->txt('create_mode') + $lng->txt('question_create_mode') ) )->withValue( $user !== null @@ -117,7 +117,7 @@ function ( ); return $c; }, - new \ilRadioGroupInputGUI($lng->txt('create_mode')) + new \ilRadioGroupInputGUI($lng->txt('question_create_mode')) ); $input->setValue( diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html index f4367a1b8a44..102448295200 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_longmenu.html @@ -1,4 +1,4 @@ - + {ICON_OK} \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html index bb71cd14a1f8..26110f460c00 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_numeric.html @@ -1 +1,2 @@ - size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}" /> + size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}" /> + diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html index e6c97f9bf635..fd86a5c57dfb 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_select.html @@ -1,4 +1,4 @@ - diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html index f71601d93384..b428c058b060 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html @@ -1 +1 @@ - size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}"/> \ No newline at end of file +{SOLUTION_VALUE} \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html index f71601d93384..bf213ad6472d 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_text.html @@ -1 +1 @@ - size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}"/> \ No newline at end of file + size="{TEXT_GAP_SIZE}" maxlength="{TEXT_GAP_SIZE}"/> \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_preview_presentation_interactive.html b/components/ILIAS/Questions/templates/default/tpl.qsts_preview_presentation_interactive.html new file mode 100644 index 000000000000..96a915b096ec --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.qsts_preview_presentation_interactive.html @@ -0,0 +1,8 @@ +
+
+
+ {QUESTION_OUTPUT} +
+ +
+
\ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html b/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html deleted file mode 100644 index 92f87c343d29..000000000000 --- a/components/ILIAS/Questions/templates/default/tpl.qsts_preview_question_presentation.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
- {QUESTION_OUTPUT} - -

{RECEIVED_POINTS_INFORMATION}

- -
-
diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html b/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html index 92f87c343d29..70665a7d4e63 100644 --- a/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html +++ b/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html @@ -1,9 +1,5 @@
-
- {QUESTION_OUTPUT} - -

{RECEIVED_POINTS_INFORMATION}

- +
+ {OUTPUT}
-
+
\ No newline at end of file diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php index fac027de50c8..913bf774ffb3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php @@ -47,6 +47,7 @@ public function __construct( $this->request = $local_dic['request_data_collector']; $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); } abstract protected function getDefaultCommand(): string; diff --git a/templates/default/070-components/legacy/Modules/_component_test.scss b/templates/default/070-components/legacy/Modules/_component_test.scss index 3d082718e745..c1ab0123e04d 100755 --- a/templates/default/070-components/legacy/Modules/_component_test.scss +++ b/templates/default/070-components/legacy/Modules/_component_test.scss @@ -16,6 +16,8 @@ $il-test-margin-large-horizontal: $il-padding-large-horizontal; $il-test-working-time-font-size: $il-font-size-large; $il-test-working-time-font-weight: $il-font-weight-bold; +$il-test-solution-value-background: $il-highlight-bg; + // general layout #tst_output { display: flex; @@ -397,3 +399,7 @@ $cons-scoring-bottom-fade-height: $il-padding-xxxlarge-vertical * 2; } } } + +.c-test__solution-value { + background: $il-test-solution-value-background +} diff --git a/templates/default/delos.css b/templates/default/delos.css index c23d5b29f046..df11de7dde69 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -17076,6 +17076,10 @@ div.ilc_Page.readonly textarea[disabled] { height: 50vh; } +.c-test__solution-value { + background: rgb(226.2857142857, 231.6428571429, 238.7142857143); +} + /* Modules/Wiki */ a.ilWikiPageMissing:link, a.ilWikiPageMissing:visited { color: #d00; From 8dcaa80cc49578509a30da1262f52a043bfd9347 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 4 Jun 2026 11:13:59 +0200 Subject: [PATCH 100/108] Questions: Add Language Variables --- .../COPage/classes/class.ilPageObjectGUI.php | 1 + .../classes/class.ilQuestionExporter.php | 1 + ...rseObjectiveQuestionAssignmentTableGUI.php | 2 +- .../Questions/src/Definitions/Clonable.php | 34 +++++ .../src/Presentation/Layout/EditOverview.php | 2 +- .../Questions/src/Question/Views/Edit.php | 4 +- .../class.ilObjSurveyQuestionPoolGUI.php | 2 +- ...lTestManScoringParticipantNotification.php | 1 + .../class.ilTestPlayerLayoutProvider.php | 2 +- .../Screen/ilTestPlayerToolProvider.php | 2 +- .../ILIAS/Test/classes/class.ilObjTest.php | 3 +- .../Test/classes/class.ilObjTestAccess.php | 1 + .../Test/classes/class.ilObjTestFolderGUI.php | 1 + .../ILIAS/Test/classes/class.ilObjTestGUI.php | 4 +- .../classes/class.ilTestPlayerAbstractGUI.php | 2 +- .../AdditionalInformationGenerator.php | 1 + .../classes/class.assKprimChoice.php | 1 + .../classes/class.assMatchingQuestion.php | 1 + .../classes/class.assQuestionGUI.php | 2 +- .../classes/class.ilAssQuestionPreviewGUI.php | 2 +- .../classes/class.ilObjQuestionPoolGUI.php | 9 +- .../classes/class.ilQuestionEditGUI.php | 1 + ...class.ilAssLongmenuCorrectionsInputGUI.php | 4 +- lang/ilias_ar.lang | 60 ++++----- lang/ilias_bg.lang | 60 ++++----- lang/ilias_cs.lang | 60 ++++----- lang/ilias_da.lang | 60 ++++----- lang/ilias_de.lang | 117 ++++++++++++++---- lang/ilias_el.lang | 60 ++++----- lang/ilias_en.lang | 115 +++++++++++++---- lang/ilias_es.lang | 60 ++++----- lang/ilias_et.lang | 60 ++++----- lang/ilias_fa.lang | 60 ++++----- lang/ilias_fr.lang | 60 ++++----- lang/ilias_hr.lang | 60 ++++----- lang/ilias_hu.lang | 60 ++++----- lang/ilias_it.lang | 60 ++++----- lang/ilias_ja.lang | 60 ++++----- lang/ilias_ka.lang | 60 ++++----- lang/ilias_lt.lang | 60 ++++----- lang/ilias_nl.lang | 60 ++++----- lang/ilias_pl.lang | 60 ++++----- lang/ilias_pt.lang | 60 ++++----- lang/ilias_ro.lang | 60 ++++----- lang/ilias_ru.lang | 60 ++++----- lang/ilias_sk.lang | 60 ++++----- lang/ilias_sl.lang | 60 ++++----- lang/ilias_sq.lang | 60 ++++----- lang/ilias_sr.lang | 60 ++++----- lang/ilias_sv.lang | 58 ++++----- lang/ilias_tr.lang | 60 ++++----- lang/ilias_uk.lang | 60 ++++----- lang/ilias_vi.lang | 60 ++++----- lang/ilias_zh.lang | 60 ++++----- 54 files changed, 1116 insertions(+), 937 deletions(-) create mode 100644 components/ILIAS/Questions/src/Definitions/Clonable.php diff --git a/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php b/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php index e933fc001306..61f69d26a913 100755 --- a/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php +++ b/components/ILIAS/COPage/classes/class.ilPageObjectGUI.php @@ -930,6 +930,7 @@ public function executeCommand(): string // load required lang mods $this->lng->loadLanguageModule("assessment"); + $this->lng->loadLanguageModule('qsts'); // set context tabs $questionGUI = assQuestionGUI::_getQuestionGUI( diff --git a/components/ILIAS/COPage/classes/class.ilQuestionExporter.php b/components/ILIAS/COPage/classes/class.ilQuestionExporter.php index ec9c2baf51b0..62c5505f4f2a 100755 --- a/components/ILIAS/COPage/classes/class.ilQuestionExporter.php +++ b/components/ILIAS/COPage/classes/class.ilQuestionExporter.php @@ -62,6 +62,7 @@ public function __construct(bool $a_preview_mode = false) $this->lng = $lng; $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $this->inst_id = IL_INST_ID; diff --git a/components/ILIAS/Course/classes/Objectives/class.ilCourseObjectiveQuestionAssignmentTableGUI.php b/components/ILIAS/Course/classes/Objectives/class.ilCourseObjectiveQuestionAssignmentTableGUI.php index 2a1900f9bd6a..a4e433952c4b 100755 --- a/components/ILIAS/Course/classes/Objectives/class.ilCourseObjectiveQuestionAssignmentTableGUI.php +++ b/components/ILIAS/Course/classes/Objectives/class.ilCourseObjectiveQuestionAssignmentTableGUI.php @@ -92,7 +92,7 @@ protected function fillRow(array $a_set): void if ($sub_data['qst_txt']) { $txt = $sub_data['qst_txt']; if ($sub_data['qst_points']) { - $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $txt .= (' (' . $sub_data['qst_points'] . ' ' . $this->lng->txt('points') . ')'); } diff --git a/components/ILIAS/Questions/src/Definitions/Clonable.php b/components/ILIAS/Questions/src/Definitions/Clonable.php new file mode 100644 index 000000000000..b77e2ee1b97e --- /dev/null +++ b/components/ILIAS/Questions/src/Definitions/Clonable.php @@ -0,0 +1,34 @@ +environment->getUIFactory()->panel()->standard( - $this->environment->getLanguage()->txt('basic_answer_form_properites'), + $this->environment->getLanguage()->txt('basic_answer_form_properties'), $content ); } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index b64d5ae79fae..7c54405b51e6 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -129,7 +129,7 @@ private function buildBasicPropertiesCreateForm( return $environment->getPresentationFactory()->getEditForm( $ff->section( $inputs, - $environment->getLanguage()->txt('edit_basic_form_properties') + $environment->getLanguage()->txt('edit_basic_question_properties') ), $environment ->withSubActionParameter(self::CMD_SAVE_QUESTION) @@ -160,7 +160,7 @@ private function buildBasicPropertiesEditingForm( $environment->getUIFactory()->input()->field(), $environment->getLanguage() ), - $environment->getLanguage()->txt('edit_basic_form_properties') + $environment->getLanguage()->txt('edit_basic_question_properties') )->withAdditionalTransformation( $this->buildAddBasicPropertiesToQuestionTrafo( $environment->getRefinery() diff --git a/components/ILIAS/SurveyQuestionPool/classes/class.ilObjSurveyQuestionPoolGUI.php b/components/ILIAS/SurveyQuestionPool/classes/class.ilObjSurveyQuestionPoolGUI.php index 44c96721e67e..ba5836229de9 100755 --- a/components/ILIAS/SurveyQuestionPool/classes/class.ilObjSurveyQuestionPoolGUI.php +++ b/components/ILIAS/SurveyQuestionPool/classes/class.ilObjSurveyQuestionPoolGUI.php @@ -268,7 +268,7 @@ public function deleteQuestionsObject(): void } $cgui = new ilConfirmationGUI(); - $cgui->setHeaderText($this->lng->txt("qpl_confirm_delete_questions")); + $cgui->setHeaderText($this->lng->txt("confirm_delete_questions")); $cgui->setFormAction($this->ctrl->getFormAction($this)); $cgui->setCancel($this->lng->txt("cancel"), "cancelDeleteQuestions"); diff --git a/components/ILIAS/Test/classes/Notifications/class.ilTestManScoringParticipantNotification.php b/components/ILIAS/Test/classes/Notifications/class.ilTestManScoringParticipantNotification.php index 3e332f894529..78cce6992b8a 100755 --- a/components/ILIAS/Test/classes/Notifications/class.ilTestManScoringParticipantNotification.php +++ b/components/ILIAS/Test/classes/Notifications/class.ilTestManScoringParticipantNotification.php @@ -29,6 +29,7 @@ public function __construct($userId, $testRefId) $this->initLanguage($this->getRecipient()); $this->getLanguage()->loadLanguageModule('assessment'); + $this->getLanguage()->loadLanguageModule('qsts'); $this->initMail(); } diff --git a/components/ILIAS/Test/classes/Screen/class.ilTestPlayerLayoutProvider.php b/components/ILIAS/Test/classes/Screen/class.ilTestPlayerLayoutProvider.php index 55cc8b484af7..0b41e55cb29e 100755 --- a/components/ILIAS/Test/classes/Screen/class.ilTestPlayerLayoutProvider.php +++ b/components/ILIAS/Test/classes/Screen/class.ilTestPlayerLayoutProvider.php @@ -88,7 +88,7 @@ public function getMainBarModification(CalledContexts $called_contexts): ?MainBa $question_listing = $f->legacy()->content($r->render($question_listing)); - $label = $lng->txt('mainbar_button_label_questionlist'); + $label = $lng->txt('questionlist'); $entry = $f->maincontrols()->slate()->legacy( $label, $f->symbol()->icon()->standard('tst', $label), diff --git a/components/ILIAS/Test/classes/Screen/ilTestPlayerToolProvider.php b/components/ILIAS/Test/classes/Screen/ilTestPlayerToolProvider.php index a0cb5bf56989..f739551acbb6 100644 --- a/components/ILIAS/Test/classes/Screen/ilTestPlayerToolProvider.php +++ b/components/ILIAS/Test/classes/Screen/ilTestPlayerToolProvider.php @@ -48,7 +48,7 @@ public function getToolsForContextStack(CalledContexts $called_contexts): array $this->factory->tool( $this->identification_provider->contextAwareIdentifier('tst_qst_list') )->withSymbol($ui->factory()->symbol()->icon()->standard('tst', $lng->txt('more'))) - ->withTitle($lng->txt('mainbar_button_label_questionlist')) + ->withTitle($lng->txt('questionlist')) ->withContent( $ui->factory()->legacy()->content( $ui->renderer()->render( diff --git a/components/ILIAS/Test/classes/class.ilObjTest.php b/components/ILIAS/Test/classes/class.ilObjTest.php index e324175b584e..b075d81b97b6 100755 --- a/components/ILIAS/Test/classes/class.ilObjTest.php +++ b/components/ILIAS/Test/classes/class.ilObjTest.php @@ -193,7 +193,8 @@ public function __construct(int $id = 0, bool $a_call_by_reference = true) parent::__construct($id, $a_call_by_reference); - $this->lng->loadLanguageModule("assessment"); + $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $this->score_settings = null; $this->question_set_config_factory = new ilTestQuestionSetConfigFactory( diff --git a/components/ILIAS/Test/classes/class.ilObjTestAccess.php b/components/ILIAS/Test/classes/class.ilObjTestAccess.php index 434fced9b3b5..682c856ff660 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestAccess.php +++ b/components/ILIAS/Test/classes/class.ilObjTestAccess.php @@ -186,6 +186,7 @@ public static function _getCommands(): array { global $DIC; $DIC->language()->loadLanguageModule('assessment'); + $DIC->language()->loadLanguageModule('qsts'); return [ [ diff --git a/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php b/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php index ccdb44413a7b..31874d33fca9 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php @@ -62,6 +62,7 @@ public function __construct( } $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); } private function getTestFolder(): ilObjTestFolder diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index ceebd6d9f0d7..dadb8082d5fa 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -1737,7 +1737,7 @@ private function buildQuestionCreationForm(): Form $inputs['pool_selection'] = $this->buildInputPoolSelection(); $section = [ - $this->ui_factory->input()->field()->section($inputs, $this->lng->txt('ass_create_question')) + $this->ui_factory->input()->field()->section($inputs, $this->lng->txt('create_question')) ]; $form = $this->ui_factory->input()->container()->form()->standard( @@ -1928,7 +1928,7 @@ private function setupToolBarAndMessage(bool $has_started_test_runs): void return; } - $this->toolbar->addButton($this->lng->txt('ass_create_question'), $this->ctrl->getLinkTarget($this, 'createQuestionForm')); + $this->toolbar->addButton($this->lng->txt('create_question'), $this->ctrl->getLinkTarget($this, 'createQuestionForm')); $this->toolbar->addSeparator(); $this->populateQuestionBrowserToolbarButtons($this->toolbar); } diff --git a/components/ILIAS/Test/classes/class.ilTestPlayerAbstractGUI.php b/components/ILIAS/Test/classes/class.ilTestPlayerAbstractGUI.php index 194abaa4eb24..c5389da89071 100755 --- a/components/ILIAS/Test/classes/class.ilTestPlayerAbstractGUI.php +++ b/components/ILIAS/Test/classes/class.ilTestPlayerAbstractGUI.php @@ -1853,7 +1853,7 @@ protected function showSideList($current_sequence_element): void } $question_listing = $this->ui_factory->listing()->workflow()->linear( - $this->lng->txt('mainbar_button_label_questionlist'), + $this->lng->txt('questionlist'), $questions )->withActive($active); diff --git a/components/ILIAS/Test/src/Logging/AdditionalInformationGenerator.php b/components/ILIAS/Test/src/Logging/AdditionalInformationGenerator.php index e466d9f7cbc9..a31d8210dfbf 100644 --- a/components/ILIAS/Test/src/Logging/AdditionalInformationGenerator.php +++ b/components/ILIAS/Test/src/Logging/AdditionalInformationGenerator.php @@ -263,6 +263,7 @@ public function __construct( private readonly GeneralQuestionPropertiesRepository $questions_repo ) { $lng->loadLanguageModule('assessment'); + $lng->loadLanguageModule('qsts'); $lng->loadLanguageModule('crs'); $this->tags = $this->buildTags(); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index c8356c52e5b6..f881f4064b8f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -710,6 +710,7 @@ protected function lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigra public function toJSON(): string { $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $result = []; $result['id'] = $this->getId(); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index fa5c34243f11..85e14711684a 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -1176,6 +1176,7 @@ public function toJSON(): string $result['mobs'] = $mobs; $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $result['reset_button_label'] = $this->lng->txt("reset_terms"); return json_encode($result); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php index 8b484bfac5f4..dbdf43b26ee0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php @@ -1259,7 +1259,7 @@ public function suggestedsolution(bool $save = false): void $formchange = new ilPropertyFormGUI(); $formchange->setFormAction($this->ctrl->getFormAction($this)); - $title = $solution ? $this->lng->txt('changeSuggestedSolution') : $this->lng->txt('addSuggestedSolution'); + $title = $solution ? $this->lng->txt('changeSuggestedSolution') : $this->lng->txt('suggested_learning_content'); $formchange->setTitle($title); $formchange->setMultipart(false); $formchange->setTableWidth('100%'); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPreviewGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPreviewGUI.php index 1b74c1325ff5..c08225138906 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPreviewGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPreviewGUI.php @@ -373,7 +373,7 @@ private function populateToolbar(): void $this->toolbar->addComponent( $this->ui_factory->button()->standard( - $this->lng->txt('qpl_reset_preview'), + $this->lng->txt('reset_preview'), $this->ctrl->getLinkTargetByClass(ilAssQuestionPreviewGUI::class, self::CMD_RESET) ) ); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index c1e066ff7ed8..53f618b9e152 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -136,6 +136,7 @@ public function __construct() $this->ctrl->saveParameterByClass('ilobjquestionpoolgui', 'consumer_context'); $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $here_uri = $this->data_factory->uri($this->request->getUri()->__toString()); $url_builder = new URLBuilder($here_uri); @@ -973,7 +974,7 @@ public function confirmDeleteQuestions(array $ids): void $this->ctrl->redirect($this, self::DEFAULT_CMD); } - $this->tpl->setOnScreenMessage('question', $this->lng->txt('qpl_confirm_delete_questions')); + $this->tpl->setOnScreenMessage('question', $this->lng->txt('confirm_delete_questions')); $deleteable_questions = $this->object->getDeleteableQuestionDetails($questionIdsToDelete); $table_gui = new ilQuestionBrowserTableGUI($this, self::DEFAULT_CMD, (($rbacsystem->checkAccess('write', $this->request_data_collector->getRefId()) ? true : false)), true); $table_gui->setShowRowsSelector(false); @@ -1001,7 +1002,7 @@ public function deleteQuestionsObject(): void $this->ctrl->redirect($this, self::DEFAULT_CMD); } - $this->tpl->setOnScreenMessage('question', $this->lng->txt('qpl_confirm_delete_questions')); + $this->tpl->setOnScreenMessage('question', $this->lng->txt('confirm_delete_questions')); $deleteable_questions = &$this->object->getDeleteableQuestionDetails($questionIdsToDelete); $table_gui = new ilQuestionBrowserTableGUI( $this, @@ -1100,7 +1101,7 @@ public function questionsObject(?RoundTripModal $import_questions_modal = null): $out = []; if ($this->rbac_system->checkAccess('write', $this->request_data_collector->getRefId())) { $btn = $this->ui_factory->button()->primary( - $this->lng->txt('ass_create_question'), + $this->lng->txt('create_question'), $this->ctrl->getLinkTargetByClass([ilRepositoryGUI::class, self::class], 'createQuestionForm') ); $this->toolbar->addComponent($btn); @@ -1163,7 +1164,7 @@ private function buildQuestionCreationForm(): Form $inputs['editing_type'] = $this->buildInputEditingType(); $section = [ - $this->ui_factory->input()->field()->section($inputs, $this->lng->txt('ass_create_question')) + $this->ui_factory->input()->field()->section($inputs, $this->lng->txt('create_question')) ]; $form = $this->ui_factory->input()->container()->form()->standard( diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilQuestionEditGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilQuestionEditGUI.php index d6c7af521379..37ef4c2aa168 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilQuestionEditGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilQuestionEditGUI.php @@ -85,6 +85,7 @@ public function __construct() $this->setQuestionId($this->request->getQuestionId()); $this->setQuestionType($this->request->raw('q_type')); $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $this->ctrl->saveParameter($this, ['qpool_ref_id', 'qpool_obj_id', 'q_id', 'q_type']); diff --git a/components/ILIAS/TestQuestionPool/classes/forms/class.ilAssLongmenuCorrectionsInputGUI.php b/components/ILIAS/TestQuestionPool/classes/forms/class.ilAssLongmenuCorrectionsInputGUI.php index 5dd73db6c22d..c841e2df7e3c 100755 --- a/components/ILIAS/TestQuestionPool/classes/forms/class.ilAssLongmenuCorrectionsInputGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/forms/class.ilAssLongmenuCorrectionsInputGUI.php @@ -55,7 +55,7 @@ public function insert(ilTemplate $a_tpl): void $this->answer_options_modal = $this->ui->factory()->modal()->lightbox( $this->ui->factory()->modal()->lightboxTextPage( $inp->render(), - $this->lng->txt('answer_options') + "{$this->lng->txt('answer_options')}:" ) ); @@ -72,7 +72,7 @@ public function insert(ilTemplate $a_tpl): void ) ) ); - $tpl->setVariable('TXT_ANSWERS', $this->lng->txt('answer_options')); + $tpl->setVariable('TXT_ANSWERS', "{$this->lng->txt('answer_options')}:"); $tpl->setVariable('TXT_CORRECT_ANSWERS', $this->lng->txt('correct_answers') . ':'); $tpl->setVariable('POSTVAR', $this->getPostVar()); diff --git a/lang/ilias_ar.lang b/lang/ilias_ar.lang index e1c0c51f7ca6..6eb2c4442280 100644 --- a/lang/ilias_ar.lang +++ b/lang/ilias_ar.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#تفعيل تسجيل الاختبار والتقييم assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#اضافة حل مقترح assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area assessment#:#add_gap#:#Add Gap Text @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Your solution is correct assessment#:#answer_is_wrong#:#Your solution is wrong assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Answer Text assessment#:#answer_types#:#Answer Types @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions. assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Cloze Text -assessment#:#cloze_textgap_case_insensitive#:#Case Insensitive -assessment#:#cloze_textgap_case_sensitive#:#Case Sensitive -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein Distance of %s assessment#:#code#:#Code assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Concatenation @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Gap assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Glossary Term assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Insert After assessment#:#insert_before#:#Insert Before -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Internal Links assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Maintenance assessment#:#manscoring#:#Manual Scoring assessment#:#manscoring_done#:#Scored Participants @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#You have reached the maximum number o assessment#:#maximum_points#:#Maxium Available Points assessment#:#maxsize#:#Maximum file upload size assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#You must define a minimum percentage of 0 percent! The mark schema wasn't saved. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions. assessment#:#msg_poly_added#:#Polygon added assessment#:#msg_questions_moved#:#Question(s) moved @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence. assessment#:#ordertext#:#Ordering Text assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output assessment#:#output_mode#:#Output Mode assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#هل انت متأكد من أنك تريد حذف السؤال \الاسئلة التالي\ة؟ assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard assessment#:#qpl_delete_rbac_error#:#You have no rights to delete this question! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#The question you are about to edit exists in %s test(s). If you change this question, you will NOT change the question(s) in the test(s), because the system creates a copy of a question when it is inserted in a test! assessment#:#qpl_questions_deleted#:#Question(s) deleted. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Question Title assessment#:#question_type#:#Question Type assessment#:#questionpool_not_entered#:#Please enter a name for a question pool! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#questions from assessment#:#questions_per_page_view#:#Page View assessment#:#random_accept_sample#:#Accept Sample assessment#:#random_another_sample#:#Get another Sample assessment#:#random_selection#:#Random Selection assessment#:#range#:#Range -assessment#:#range_lower_limit#:#Lower Bound assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper Bound assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Search Roles assessment#:#search_term#:#مصطلح البحث assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Select Gap assessment#:#select_max_one_item#:#الرجاء اختيار عنصر واحد فقط assessment#:#select_one_user#:#Please select at least one user. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Answers -assessment#:#shuffle_answers#:#Shuffle Answers assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Cloze Text +qsts#:#cloze_textgapcase_insensitive#:#Case Insensitive +qsts#:#cloze_textgapcase_sensitive#:#Case Sensitive +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein Distance of %s +qsts#:#confirm_delete_questions#:#هل انت متأكد من أنك تريد حذف السؤال \الاسئلة التالي\ة؟ +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Gap +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower Bound +qsts#:#range_upper_limit#:#Upper Bound +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Select Gap +qsts#:#shuffle_answers#:#Shuffle Answers +qsts#:#suggested_learning_content#:#اضافة حل مقترح rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Question Block survey#:#questionblock_inserted#:#Question Block inserted survey#:#questionblocks#:#Question Blocks survey#:#questionblocks_inserted#:#Question Blocks inserted -survey#:#questions#:#Questions survey#:#questions_inserted#:#Question(s) inserted! survey#:#questions_removed#:#Question(s) and/or question block(s) removed! survey#:#questiontype#:#Question Type @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Questions survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_bg.lang b/lang/ilias_bg.lang index ec20f122d166..ec545e65cf90 100644 --- a/lang/ilias_bg.lang +++ b/lang/ilias_bg.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###26 08 2024 new v assessment#:#activate_logging#:#Активиране записването на теста и оценяването assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Добавяне на текст в празнина @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Вашето решение е правилно assessment#:#answer_is_wrong#:#Вашето решение е грешно assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Текст за отговора assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Затваряне на текста -assessment#:#cloze_textgap_case_insensitive#:#Case insensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_case_sensitive#:#Case sensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable assessment#:#code#:#Код assessment#:#codebase#:#Codebase###25 02 2007 new variable assessment#:#concatenation#:#Concatenation###06 09 2006 new variable @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Празнина assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Термин от речника assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Вмъкване след assessment#:#insert_before#:#Вмъкване преди -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Вътрешни линкове assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Поддръжка assessment#:#manscoring#:#Manual Scoring###25 02 2007 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Достигнали сте макс assessment#:#maximum_points#:#Maxium Available Points###25 02 2007 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Трябва да дефинирате минимален резултат от 0 процента! Схемата с оценките не е съхранена. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 02 2007 new variable assessment#:#output_mode#:#Output Mode###25 02 2007 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Сигурни ли сте, че желаете да изтриете следният въпрос(и)? Ако изтриете заключен въпрос, резултатите от всички тестове, съдържащи заключения въпрос, също ще бъдат изтрити. assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard###03 Nov 2005 new variable assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard###03 Nov 2005 new variable assessment#:#qpl_delete_rbac_error#:#Нямате права, за да изтриете този въпрос! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Въпросът, който желаете да редактирате, съществува в %s тест(а). Ако промените този въпрос, НЯМА да се получи промяна във въпроса(ите) в теста(овете), защото системата създава копие на въпроса, когато той бъде вмъкнат в тест! assessment#:#qpl_questions_deleted#:#Въпросът(ите) изтрит(и). -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Заглавие на въпроса assessment#:#question_type#:#Тип на въпроса assessment#:#questionpool_not_entered#:#Моля, въведете име за набора от въпроси! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#въпроси от assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Приемане на примера assessment#:#random_another_sample#:#Извличане на друг пример assessment#:#random_selection#:#Случаен подбор assessment#:#range#:#Range###25 02 2007 new variable -assessment#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Search Roles###06 09 2006 new variable assessment#:#search_term#:#Search Term###06 09 2006 new variable assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Изберете празнина assessment#:#select_max_one_item#:#Моля, изберете само едно assessment#:#select_one_user#:#Please select at least one user###23 Dec 2005 new variable assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Разбъркване на отговорите assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Затваряне на текста +qsts#:#cloze_textgapcase_insensitive#:#Case insensitive###03 Nov 2005 new variable +qsts#:#cloze_textgapcase_sensitive#:#Case sensitive###03 Nov 2005 new variable +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable +qsts#:#confirm_delete_questions#:#Сигурни ли сте, че желаете да изтриете следният въпрос(и)? Ако изтриете заключен въпрос, резултатите от всички тестове, съдържащи заключения въпрос, също ще бъдат изтрити. +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Празнина +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable +qsts#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Изберете празнина +qsts#:#shuffle_answers#:#Разбъркване на отговорите +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Блок въпроси survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Блокове въпроси survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Въпроси survey#:#questions_inserted#:#Въпросът(ите) вмъкнат(и)! survey#:#questions_removed#:#Въпросът(ите) и/или блокът(овете) въпроси премахнат(и)! survey#:#questiontype#:#Тип въпрос @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Въпроси survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_cs.lang b/lang/ilias_cs.lang index 0cd6fb6a5092..468be202fd77 100644 --- a/lang/ilias_cs.lang +++ b/lang/ilias_cs.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Zapnout TinyMCE pro editaci WYSIWYG assessment#:#activate_logging#:#Aktivovat záznam Test a Hodnocení assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Přidat doporučené řešení assessment#:#add_answers#:#Přidat odpovědi assessment#:#add_circle#:#Přidat kruhovou plochu assessment#:#add_gap#:#Přidat textovou mezeru @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Obdržel/a jste body za Vaše assessment#:#answer_is_right#:#Vaše řešení je správné assessment#:#answer_is_wrong#:#Vaše řešení je chybné assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Možnosti odpovědi: assessment#:#answer_question#:#Odpovědět na otázku assessment#:#answer_text#:#Text odpovědi assessment#:#answer_types#:#Typy odpovědi @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Splnit odevzdáním assessment#:#ass_completion_by_submission_info#:#Je-li zapnuto, odevzdání alespoň jednoho souboru způsobí splnění dané otázky s přidělením maximálního počtu bodů této otázky. Tento počet bodů může být později změněn manuálně. Změna tohoto nastavení nemá vliv na již odevzdaná řešení. assessment#:#ass_create_export_file_with_results#:#Vytvořit exportní soubor testu (vč. výsledků účastníků) assessment#:#ass_create_export_test_archive#:#Vytvořit archivní soubor testu -assessment#:#ass_create_question#:#Vytvořit otázku assessment#:#ass_imap_hint#:#Nápověda zobrazena jako nápovědná bublina assessment#:#ass_imap_map_file_not_readable#:#Nahranou obrazovou mapu nelze načíst. assessment#:#ass_imap_no_map_found#:#V nahrané obrazové mapě nelze najít žádný formulář. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Délka textového pole assessment#:#cloze_fixed_textlength_description#:#Pokud vložíte hodnotu větší než 0, všechna textová pole textových i numerických mezer budou vytvářeny s pevnou délkou rovnou této hodnotě. assessment#:#cloze_gap_size_info#:#Pokud zadáte hodnotu větší než 0, bude toto textové pole mezery vytvořeno s pevnou délkou této hodnoty. Pokud nezadáte hodnotu, bude textové pole mezery vytvořeno s hodnotou globální pevné délky. -assessment#:#cloze_text#:#Doplňovaný text -assessment#:#cloze_textgap_case_insensitive#:#Nerozlišuje velká/malá písmena -assessment#:#cloze_textgap_case_sensitive#:#Rozlišuje velká/malá písmena -assessment#:#cloze_textgap_levenshtein_of#:#Vzdálenost Levenshtein %s assessment#:#code#:#Kód assessment#:#codebase#:#Databáze kódu assessment#:#concatenation#:#Sřetězení @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Můžete zadat předem definované proměnné ($v assessment#:#fq_no_restriction_info#:#Desetinná místa i zlomky jsou přijaty jako vstup. assessment#:#fq_precision_info#:#Zadejte počet požadovaných desetinných míst. assessment#:#fq_question_desc#:#Můžete určit proměnné vložením $v1, $v2 ... $vn, výsledky vložením $r1, $r2 .... $rn na požadované místo v textu otázky. Klepněte na tlačítko "Rozbor otázky" k vytvoření ediovaního formuláře pro proměnné a výsledky. -assessment#:#gap#:#Mezera assessment#:#gap_combination#:#Kombinace mezer -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Pojem glosáře assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Vaše výsledná známka je: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Tato otázka již obsahuje obrázky. Nem assessment#:#info_text_upload#:#Vyberte soubor odpovědí, který chcete nahrát assessment#:#insert_after#:#Vložit za assessment#:#insert_before#:#Vložit před -assessment#:#insert_gap#:#Vložit rezervované místo assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Interní odkazy assessment#:#intprecision#:#Dělitelné @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Účastník zadal nesprávné h assessment#:#longmenu#:#Dlouhé menu assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Text dlouhého menu -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Údržba assessment#:#manscoring#:#Manuální hodnocení assessment#:#manscoring_done#:#Vyhodnocení účastníci @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Dosáhl/a jste maximální počet pok assessment#:#maximum_points#:#Maxium dostupných bodů assessment#:#maxsize#:#Maximální velkost ukládaného souboru assessment#:#maxsize_info#:#Vložit maximální velikost v bytech, která je povolena pro ukládané soubory. Pokud necháte toto pole prázdné, bude namísto něj nastavena maximální velikost této instalace. -assessment#:#min_auto_complete#:#Automatické dokončování assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Musíte definovat minimální procento od specifikace 0 procent! Schéma známkování nebylo uloženo. assessment#:#misc#:#Různé možnosti @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Kruh přidán -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Počet výrazů musí být větší, nebo roven počtu definicí. assessment#:#msg_poly_added#:#Polygon přidán assessment#:#msg_questions_moved#:#Otázky přesunuty @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Sekvence odpovědi Vámi zde definovaná bude považována za sekvenci správného řešení. assessment#:#ordertext#:#Řazení textu assessment#:#ordertext_info#:#Vložte prosím text, který má být řazen horizontálně. Řazený text bude oddělen pomocí značek mezer v textu. Pokud potřebujete jiné oddělení, můžete použít separátor %s k oddělení Vašich textových jednotek. -assessment#:#out_of_range#:#Mimo rozsah assessment#:#output#:#Výstup assessment#:#output_mode#:#Výstupní mód assessment#:#parseQuestion#:#Rozbor otázky @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Zrušit -assessment#:#qpl_confirm_delete_questions#:#Jste si jist/a, že chcete smazat následující otázky? assessment#:#qpl_copy_insert_clipboard#:#Vybrané otázky jsou zkopírovány do schránky assessment#:#qpl_copy_select_none#:#Označte alespoň jednu otázku ke zkopírování do schránky assessment#:#qpl_delete_rbac_error#:#Nemáte oprávnění smazat tuto otázku! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Způsobilost assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Celkový součet bodů způsobilosti podle způsobilosti assessment#:#qpl_question_is_in_use#:#Otázka, kterou upravujete, existuje v %s testech. Pokud tuto otázku změníte, NEZMĚNÍTE otázku(y) v testu(ech), protože systém vytváří kopii otázky, která je vložena do testu! assessment#:#qpl_questions_deleted#:#Otázky smazány. -assessment#:#qpl_reset_preview#:#Obnovit náhled assessment#:#qpl_save_skill_assigns_update#:#Uložit přiřazení způsobilosti assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Pokud je zapnuto, pro filtrování jsou zobrazeny možné vytvořené taxonomie. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Počet pokusů @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Název otázky assessment#:#question_type#:#Typ otázky assessment#:#questionpool_not_entered#:#Vložte prosím název zásobníku otázek! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#otázky z assessment#:#questions_per_page_view#:#Zobrazení stránky assessment#:#random_accept_sample#:#Akceptovat příklad assessment#:#random_another_sample#:#Vybrat jiný příklad assessment#:#random_selection#:#Náhodný výběr assessment#:#range#:#Rozmezí -assessment#:#range_lower_limit#:#Spodní hranice assessment#:#range_max#:#Rozmezí (Maximum) assessment#:#range_min#:#Rozmezí (Minimum) -assessment#:#range_upper_limit#:#Horní hranice assessment#:#rated_sign#:#Podpis assessment#:#rated_unit#:#Jednotka assessment#:#rated_value#:#Hodnota @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Vyhledat role assessment#:#search_term#:#Vyhledat termín assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Vybrat mezeru assessment#:#select_max_one_item#:#Vyberte prosím pouze jednu položku assessment#:#select_one_user#:#Vyberte prosím alespoň jednoho uživatele assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Zobrazit přehled označených průchodů assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Zobrazit označené odpovědi uživatelů -assessment#:#shuffle_answers#:#Zamíchat odpovědi assessment#:#skip_question#:#Neodpovídat a Další assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Možnosti odpovědi +qsts#:#cloze_text#:#Doplňovaný text +qsts#:#cloze_textgapcase_insensitive#:#Nerozlišuje velká/malá písmena +qsts#:#cloze_textgapcase_sensitive#:#Rozlišuje velká/malá písmena +qsts#:#cloze_textgaplevenshtein_of#:#Vzdálenost Levenshtein %s +qsts#:#confirm_delete_questions#:#Jste si jist/a, že chcete smazat následující otázky? +qsts#:#create_question#:#Vytvořit otázku +qsts#:#gap#:#Mezera +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Vložit rezervované místo +qsts#:#min_auto_complete#:#Automatické dokončování +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Mimo rozsah +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Spodní hranice +qsts#:#range_upper_limit#:#Horní hranice +qsts#:#reset_preview#:#Obnovit náhled +qsts#:#select_gap#:#Vybrat mezeru +qsts#:#shuffle_answers#:#Zamíchat odpovědi +qsts#:#suggested_learning_content#:#Přidat doporučené řešení rating#:#rat_not_rated_yet#:#Dosud nehodnoceno rating#:#rat_nr_ratings#:#%s Hodnocení rating#:#rat_one_rating#:#Jedno hodnocení @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok otázek survey#:#questionblock_inserted#:#Blok otázek vložen survey#:#questionblocks#:#Bloky otázek survey#:#questionblocks_inserted#:#Bloky otázek vloženy -survey#:#questions#:#Otázky survey#:#questions_inserted#:#Otázky vloženy survey#:#questions_removed#:#Otázky a/nebo bloky otázek odstraněny! survey#:#questiontype#:#Typ otázky @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Skrýt štítky survey#:#svy_print_show_labels#:#Zobrazit štítky survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Otázky survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Šablona pošty diff --git a/lang/ilias_da.lang b/lang/ilias_da.lang index 4a6807934292..bdff6e7db8e7 100644 --- a/lang/ilias_da.lang +++ b/lang/ilias_da.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Aktiver logging for Test & Assessment assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Opret løsningsforslag assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Lav cirkelområde assessment#:#add_gap#:#Tilføj hul tekst @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Du har fået point for din lø assessment#:#answer_is_right#:#Din løsning er rigtig assessment#:#answer_is_wrong#:#Din løsning er forkert assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Besvar tekst assessment#:#answer_types#:#Svartype @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###16 09 2013 new variable -assessment#:#ass_create_question#:#Create Question###16 09 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Længde på tekstfelt assessment#:#cloze_fixed_textlength_description#:#Hvis du indtaster en værdi der er større end 0 bliver alle numeriske og tekst huller den størrelse. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Luk tekst -assessment#:#cloze_textgap_case_insensitive#:#Ingen forskel på store og små bogstaver -assessment#:#cloze_textgap_case_sensitive#:#Forskel på store og små bogstaver -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein afstand på %s assessment#:#code#:#Kode assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Konkatnering @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Hul assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Ordbogsforklaring assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Indsæt efter assessment#:#insert_before#:#Indsæt før -assessment#:#insert_gap#:#Insert Gap###11 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Interne links assessment#:#intprecision#:#Divisible By###16 09 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Vedligeholdelse assessment#:#manscoring#:#Manuelle karakterer assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Du har nået maximum antallet af fors assessment#:#maximum_points#:#Maksimalt opnåelige point assessment#:#maxsize#:#Maksimal størrelse på uploadede filer assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Du skal angive et minimum procentdel af 0 procent! Karakterskemaet er ikke gemt! assessment#:#misc#:#Misc Options###16 09 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Cirkel tilføjet -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon tilføjet assessment#:#msg_questions_moved#:#Spørgsmål flyttet @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Rækkefølgen ovenfor skal være den korrekte besvarelse. assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 10 2006 new variable assessment#:#output_mode#:#Output mode assessment#:#parseQuestion#:#Parse Question###16 09 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Er du sikker på du vil slette følgende spørgsmål? Hvis du sletter låste spørgsmål vil resultater af alle test der bruger et låst spørgsmål også blive slettet. assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard assessment#:#qpl_delete_rbac_error#:#Du har ikke rettigheder til at slette dette spørgsmål! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Spørgsmålet du er ved at ændre findes i %s test. Hvis du ændrer spørgsmålet vil det ikke ændres i disse tests, fordi der laves en kopi af spørgsmålet når det tilføjes. assessment#:#qpl_questions_deleted#:#Spørgsmål slettet. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###16 09 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Antal forsøg @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Titel assessment#:#question_type#:#Spørgsmåls type assessment#:#questionpool_not_entered#:#Indtast venligst et navn for spørgsmålsgruppe! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#spørgsmål fra### assessment#:#questions_per_page_view#:#Sidevisning assessment#:#random_accept_sample#:#Accepter prøve assessment#:#random_another_sample#:#Få en anden prøve assessment#:#random_selection#:#Tilfældigt valg assessment#:#range#:#Range -assessment#:#range_lower_limit#:#Lower limit assessment#:#range_max#:#Range (Maximum)###16 09 2013 new variable assessment#:#range_min#:#Range (Minimum)###16 09 2013 new variable -assessment#:#range_upper_limit#:#Upper limit assessment#:#rated_sign#:#Sign###16 09 2013 new variable assessment#:#rated_unit#:#Unit###16 09 2013 new variable assessment#:#rated_value#:#Value###16 09 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Søg roller assessment#:#search_term#:#Søge kriterier assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Vælg hul assessment#:#select_max_one_item#:#Vælg venligst kun en ting assessment#:#select_one_user#:#Vælg mindst en bruger assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Vis overblik for de markerede besvarelser assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Vis de markerede besvarelser -assessment#:#shuffle_answers#:#Bland svar assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Tekst @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Luk tekst +qsts#:#cloze_textgapcase_insensitive#:#Ingen forskel på store og små bogstaver +qsts#:#cloze_textgapcase_sensitive#:#Forskel på store og små bogstaver +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein afstand på %s +qsts#:#confirm_delete_questions#:#Er du sikker på du vil slette følgende spørgsmål? Hvis du sletter låste spørgsmål vil resultater af alle test der bruger et låst spørgsmål også blive slettet. +qsts#:#create_question#:#Create Question###16 09 2013 new variable +qsts#:#gap#:#Hul +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###11 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower limit +qsts#:#range_upper_limit#:#Upper limit +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Vælg hul +qsts#:#shuffle_answers#:#Bland svar +qsts#:#suggested_learning_content#:#Opret løsningsforslag rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Spørgsmåls blok survey#:#questionblock_inserted#:#Blok indsat survey#:#questionblocks#:#Spørgsmåls blok survey#:#questionblocks_inserted#:#Spørgsmålsblokke indsat -survey#:#questions#:#Spørgsmål survey#:#questions_inserted#:#Spørgsmål indsat! survey#:#questions_removed#:#Spørgsmål og/eller spørgsmålsblok fjernet! survey#:#questiontype#:#Spørgsmåls type @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Spørgsmål survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 300fceedea6e..459e1c929b60 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#TinyMCE-Editor für WYSIWYG-Bearbeitung aktivieren assessment#:#activate_logging#:#Protokollieren des Test und Assessments aktivieren assessment#:#activate_manual_scoring#:#Bewertung aktivieren assessment#:#activate_manual_scoring_desc#:#Aktiviert die Bewertung für alle Fragentypen -assessment#:#addSuggestedSolution#:#Inhalte zur Wiederholung assessment#:#add_answers#:#Antworten hinzufügen assessment#:#add_circle#:#Kreis hinzufügen assessment#:#add_gap#:#Lückentext hinzufügen @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Sie haben Punkte für Ihre Lö assessment#:#answer_is_right#:#Ihre Lösung ist korrekt. assessment#:#answer_is_wrong#:#Ihre Lösung ist falsch. assessment#:#answer_of#:#Antwort von -assessment#:#answer_options#:#Antwort-Optionen: assessment#:#answer_question#:#Frage beantworten assessment#:#answer_text#:#Antwort-Text assessment#:#answer_types#:#Antwort-Editor @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Bestehen durch Abgabe assessment#:#ass_completion_by_submission_info#:#Falls aktiviert, führt die Abgabe einer Lösungsdatei zur Vergabe der Maximalpunktzahl für diese Frage. Die Bewertung kann jederzeit manuell angepasst werden. Das Ändern dieser Einstellung hat keine nachträglichen Auswirkungen auf bereits eingereichte Lösungen. assessment#:#ass_create_export_file_with_results#:#inkl. Teilnehmerergebnisse assessment#:#ass_create_export_test_archive#:#als Archivdatei -assessment#:#ass_create_question#:#Frage erstellen assessment#:#ass_imap_hint#:#Hinweis (angezeigt als Tooltipp) assessment#:#ass_imap_map_file_not_readable#:#Die hochgeladene Imagemap kann nicht gelesen werden. assessment#:#ass_imap_no_map_found#:#In der hochgeladenen Imagemap konnte keine unterstützte Form gefunden werden. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Vorangestellte oder nachfolgende Leerzeich assessment#:#cloze_fixed_textlength#:#Länge des Textfeldes assessment#:#cloze_fixed_textlength_description#:#Wenn Sie hier einen Wert eintragen, werden Textlücken, welche keinen eigenen Wert für eine maximale Länge definieren, sowie numerische Lücken mit dieser Länge erzeugt, so dass es nicht möglich ist eine größere Anzahl an Zeichen einzugeben. Für numerische Lücken ist darüber hinaus zu beachten, dass das Dezimaltrennzeichen dabei mit gezählt wird. assessment#:#cloze_gap_size_info#:#Ist ein Wert größer 0 eingetragen, wird diese Lücke mit der hier angegebenen Länge erzeugt. Ist kein Wert angegeben, wird diese Lücke mit der global angegebenen Textfeldlänge erzeugt. -assessment#:#cloze_text#:#Lückentextfrage -assessment#:#cloze_textgap_case_insensitive#:#Zwischen Groß- und Kleinschreibung wird nicht unterschieden -assessment#:#cloze_textgap_case_sensitive#:#Groß- und Kleinschreibung beachten -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein-Abstand von %s assessment#:#code#:#Code assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Verknüpfung @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Erlaubt ist die Verwendung von bereits definierte assessment#:#fq_no_restriction_info#:#Sowohl Dezimalzahlen als auch Brüche werden als Eingabe akzeptiert. assessment#:#fq_precision_info#:#Geben Sie hier die Anzahl der gewünschten Nachkommastellen an. assessment#:#fq_question_desc#:#Sie definieren Variablen durch die Angabe von $v1, $v2 ... $vn, Ergebnisfelder mit $r1, $r2 .... $rn an den gewünschten Positionen im Text. Klicken Sie dann auf die Schaltfläche „Frage analysieren“, um Bearbeitungsformulare für alle Variablen und Ergebnisse zu erzeugen. -assessment#:#gap#:#Lücke assessment#:#gap_combination#:#Lückentext-Kombination -assessment#:#gaps#:#Lücken assessment#:#glossary_term#:#Glossarbegriff assessment#:#goto_first_question#:#Zur ersten Frage assessment#:#grading_mark_msg#:#Sie haben die Note "[mark]" erzielt. @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Die Frage beinhaltet bereits Bilder. Der assessment#:#info_text_upload#:#Wählen Sie eine Textdatei (UTF-8) mit Antworten zum Hochladen aus. assessment#:#insert_after#:#Einfügen hinter assessment#:#insert_before#:#Einfügen vor -assessment#:#insert_gap#:#Lücke einfügen assessment#:#interaction_type#:#Interaktion assessment#:#internal_links#:#Interne Verweise assessment#:#intprecision#:#Teilbar durch @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Teilnehmer hat falsches Testpas assessment#:#longmenu#:#Longmenu assessment#:#longmenu_answeroptions_differ#:#Diese Frage funktioniert nicht richtig, denn sie hat nicht die gleiche Anzahl Lücken im Text wie in den Antwortoptionen. assessment#:#longmenu_text#:#„Long Menu“-Text -assessment#:#mainbar_button_label_questionlist#:#Fragenliste assessment#:#maintenance#:#Wartung assessment#:#manscoring#:#Bewertung assessment#:#manscoring_done#:#Bereits bewertete Teilnehmer @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Sie haben die maximale Anzahl von Tes assessment#:#maximum_points#:#Maximal erreichbare Punktezahl assessment#:#maxsize#:#Maximale Dateigröße assessment#:#maxsize_info#:#Geben Sie die maximale Größe in Bytes an, die eine hochgeladene Datei haben darf. Wenn Sie das Feld leer lassen, wird die Einstellung des zugrunde liegenden Systems verwendet. -assessment#:#min_auto_complete#:#Autovervollständigung assessment#:#min_ip_label#:#Kleinste IP mit Zugriff assessment#:#min_percentage_ne_0#:#Das Notenschema muss mindestens eine Stufe mit einem minimalen Prozentsatz von 0 Prozent enthalten. assessment#:#misc#:#Verschiedene Optionen @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#Nacheinander assessment#:#mode_question#:#Nach Frage assessment#:#mode_user#:#Nach Teilnehmer assessment#:#msg_circle_added#:#Kreis hinzugefügt -assessment#:#msg_no_questions_selected#:#Keine Fragen ausgewählt. assessment#:#msg_number_of_terms_too_low#:#Die Anzahl der Terme muss größer oder gleich der Anzahl der Definitionen sein. assessment#:#msg_poly_added#:#Polygon hinzugefügt assessment#:#msg_questions_moved#:#Fragen verschoben @@ -984,7 +971,6 @@ assessment#:#order#:#Sortierung assessment#:#ordering_answer_sequence_info#:#Die hier definierte Reihenfolge der Antworten wird als korrekte Lösungsreihenfolge verwendet. assessment#:#ordertext#:#Anzuordnender Text assessment#:#ordertext_info#:#Bitte geben Sie den Text in der Reihenfolge ein, in der er horizontal angeordnet werden soll. Die einzelnen Bestandteile werden durch Leerzeichen getrennt. Wenn Sie eine abweichende Trennung benötigen, verwenden Sie bitte den Trenner %s anstelle der Leerzeichen. -assessment#:#out_of_range#:#Ausserhalb des Bereichs assessment#:#output#:#Ausgabe assessment#:#output_mode#:#Ausgabemodus assessment#:#parseQuestion#:#Frage analysieren @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Hinzufügen assessment#:#qpl_bulk_save_overwrite#:#Überschreiben assessment#:#qpl_bulkedit_success#:#Änderungen gespeichert. assessment#:#qpl_cancel_skill_assigns_update#:#Abbrechen -assessment#:#qpl_confirm_delete_questions#:#Sind Sie sicher, dass Sie die folgenden Fragen entfernen wollen? assessment#:#qpl_copy_insert_clipboard#:#Die ausgewählten Fragen wurden in die Zwischenablage kopiert assessment#:#qpl_copy_select_none#:#Bitte wählen Sie mindestens eine Frage aus, um diese in die Zwischenablage zu kopieren! assessment#:#qpl_delete_rbac_error#:#Sie haben keine Berechtigung, diese Frage zu entfernen! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetenz assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Summe aller Kompetenzpunkte je Kompetenz assessment#:#qpl_question_is_in_use#:#Die Frage, die Sie jetzt bearbeiten möchten, existiert bereits in %s Test(s). Wenn Sie diese Frage jetzt verändern, so hat das KEINE Auswirkungen auf bereits in Tests eingebundene Fragen, da das System automatisch eine Kopie der Frage anlegt, wenn diese in einen Test eingebunden wird! assessment#:#qpl_questions_deleted#:#Fragen gelöscht -assessment#:#qpl_reset_preview#:#Vorschau zurücksetzen assessment#:#qpl_save_skill_assigns_update#:#Kompetenzzuweisung speichern assessment#:#qpl_settings_availability#:#Verfügbarkeit assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Vorhandene Taxonomien können zum Filtern der Fragen genutzt werden. @@ -1160,17 +1144,14 @@ assessment#:#question_type#:#Fragetyp assessment#:#questionlist_cannot_be_altered#:#Die Fragenliste kann nicht verändert werden da der Test Teilnehmerdatensätze enthält. assessment#:#questionpool_not_entered#:#Bitte geben Sie einen Namen für den Fragenpool an! assessment#:#questionpool_not_selected#:#Bitte wählen Sie einen Fragenpool aus! -assessment#:#questions#:#Fragen assessment#:#questions_from#:#Fragen aus assessment#:#questions_per_page_view#:#Seitenansicht assessment#:#random_accept_sample#:#Zusammenstellung akzeptieren assessment#:#random_another_sample#:#Neue Zusammenstellung assessment#:#random_selection#:#Zufällige Auswahl assessment#:#range#:#Bereich -assessment#:#range_lower_limit#:#Untere Schranke assessment#:#range_max#:#Bereich (Maximum) assessment#:#range_min#:#Bereich (Minimum) -assessment#:#range_upper_limit#:#Obere Schranke assessment#:#rated_sign#:#Vorzeichen assessment#:#rated_unit#:#Einheit assessment#:#rated_value#:#Wert @@ -1246,7 +1227,6 @@ assessment#:#search_roles#:#nach Rollen assessment#:#search_term#:#Suchbegriff assessment#:#select_at_least_one_feedback_type_and_trigger#:#Bitte mindestens eine Art der Rückmeldung und einen Auslöser auswählen. assessment#:#select_at_least_one_lock_answer_type#:#Bitte mindestens eine Art Antworten festzuschreiben auswählen. -assessment#:#select_gap#:#Auswahl-Lücke assessment#:#select_max_one_item#:#Bitte wählen Sie nur ein Objekt aus! assessment#:#select_one_user#:#Bitte wählen Sie mindestens einen Benutzer aus! assessment#:#select_question#:#Frage auswählen @@ -1273,7 +1253,6 @@ assessment#:#show_old_introduction#:#Alten Einleitungstext anzeigen assessment#:#show_pass_overview#:#Ergebnisübersicht (bewerteter Testdurchlauf) assessment#:#show_results#:#Testergebnisse anzeigen assessment#:#show_user_answers#:#Antworten (bewerteter Testdurchlauf) -assessment#:#shuffle_answers#:#Antworten mischen assessment#:#skip_question#:#Nicht antworten und weiter assessment#:#solution#:#Lösungen assessment#:#solutionText#:#Text @@ -14126,12 +14105,82 @@ qpl#:#qpl_page_type_qfbg#:#Generelles Feedback qpl#:#qpl_page_type_qfbs#:#Spezielles Feedback qpl#:#qpl_page_type_qht#:#Hinweis qpl#:#qpl_page_type_qpl#:#Fragenseite -qsts#:#answer_form#:#Antwort Formular -qsts#:#cloze_enable_combinations#:#Enable Gap Combination +qsts#:#above_range#:#Oberhalb des Bereichs +qsts#:#add_gap_combination#:#Lückentext-Kombination hinzufügen +qsts#:#answer_form#:#Antwortformular +qsts#:#answer_options#:#Antwort-Optionen +qsts#:#answer_options_awarding_points#:#Punktevergebende Antwort-Optionen +qsts#:#answer_options_must_be_unique#:#Antwort-Optionen müssen einzigartig sein. +qsts#:#async_view#:#Asynchrone Ansicht +qsts#:#at_least_one_gap_positiv_points#:#Mindestens eine Lücke muss Punkte geben. +qsts#:#available_points#:#Maximal erreichbare Punkte +qsts#:#awarded_points#:#Erhaltene Punkte +qsts#:#basic_answer_form_properties#:#Grundlegende Eigenschaften des Antwortformulars +qsts#:#below_range#:#Unterhalb des Bereichs +qsts#:#best_response#:#Beste Antwort +qsts#:#best_response_given#:#Dies ist die bestmögliche Antwort. +qsts#:#between#:#Zwischen %s und %s +qsts#:#broken_answer_form#:#Ungültiges Antwortformular +qsts#:#byline_create_mode_full#:#Der Seiteneditor wird verwendet. Der Fragentext kann Formatierungen und mehrere Antwortformulare enthalten. +qsts#:#byline_create_mode_simple#:#Es wird nur ein Textfeld zur Eingabe des Fragentexts angezeigt. Der Fragetext kann keine Formatierungen enthalten und nur ein Antwortformular kann erstellt werden. Beim Abschluss des Erstellprozesses wird ein Knopf angeboten, der es erlaubt beim Speichern gleich die nächste Frage zu erstellen. +qsts#:#cloze#:#Lückentext +qsts#:#cloze_enable_gap_combinations#:#Enable Gap Combination +qsts#:#cloze_text#:#Lückentextfrage +qsts#:#cloze_textgap_case_insensitive#:#Zwischen Groß- und Kleinschreibung wird nicht unterschieden +qsts#:#cloze_textgap_case_sensitive#:#Groß- und Kleinschreibung beachten +qsts#:#cloze_textgap_levenshtein_of#:#Levenshtein-Abstand von %s +qsts#:#combination_needs_more_than_one#:#Eine Kombination von Lücken muss mehr als eine Lücke enthalten. +qsts#:#confirm_delete_feedback#:#Soll diese Rückmeldung wirklich gelöscht werden? +qsts#:#confirm_delete_questions#:#Sind Sie sicher, dass Sie die folgenden Fragen entfernen wollen? +qsts#:#confirm_remove_gaps#:#Es wurden Lücken aus dem Lückentext entfernt. Sollen diese nun definitiv entfernt und alle dazugehörigen Informationen gelöscht werden. qsts#:#cont_ed_insert_answf#:#Antwortformular einfügen +qsts#:#contained_answer_form_types#:#Enthaltene Antwortformulartypen +qsts#:#contains_answer_form_types#:#Enthält Antwortformulartypen qsts#:#create_answer_form#:#Antwortformular erstellen -qsts#:#edit_basic_form_properties#:#Grundeinstellungen der Frage bearbeiten +qsts#:#create_feedback#:#Rückmeldung erstellen +qsts#:#create_mode_full#:#Vollständig +qsts#:#create_mode_simple#:#Einfach +qsts#:#create_question#:#Frage erstellen +qsts#:#default_user_settings#:#Standard Einstellungen +qsts#:#default_view#:#Standard Ansicht +qsts#:#delete_combination#:#Delete Combination of Gaps +qsts#:#delete_responses#:#Antworten löschen +qsts#:#disable_marking_allowing_partial_points#:#Bewertung deaktivieren +qsts#:#disable_suggested_learning_content#:#Inhalte zur Wiederholung deaktivieren +qsts#:#disable_text_feedback#:#Rückmeldungen deaktivieren +qsts#:#edit_answer_options#:#Antwort-Optionen bearbeiten +qsts#:#edit_available_points#:#Verfügbare Punkte bearbeiten +qsts#:#edit_basic_answer_form_properties#:#Grundlegende Eigenschaften des Antwortformulars bearbeiten +qsts#:#edit_basic_question_properties#:#Grundeinstellungen der Frage bearbeiten +qsts#:#edit_feedback#:#eedback bearbeiten +qsts#:#edit_gaps#:#Lücken bearbeiten +qsts#:#edit_generic_feedback#:#Rückmeldung basierend auf den erreichten Punkten +qsts#:#edit_points#:#Punkte bearbeiten +qsts#:#edit_suggested_learning_content#:#Inhalt zur Wiederholung bearbeiten +qsts#:#enable_marking_allowing_partial_points#:#Bewertung aktivieren +qsts#:#enable_suggested_learning_content#:#Inhalte zur Wiederholung +qsts#:#enable_text_feedback#:#Rückmeldungen aktivieren +qsts#:#equal#:#Gleich %s +qsts#:#gap#:#Lücke +qsts#:#gap_combination_already_exists#:#Diese Kombination von Lücken existiert bereits. +qsts#:#gap_combinations#:#Lückentext-Kombinationen +qsts#:#gap_type#:#Art der Lücke +qsts#:#gaps#:#Lücken +qsts#:#insert_gap#:#Lücke einfügen +qsts#:#insert_legacy_texts#:#Texte aus Altdaten importieren +qsts#:#insert_legacy_texts_info#:#Es gibt Texte, die aus einer älteren Version importiert wurden. Diese Texte können nicht direkt bearbeitet werden, sie können jedoch in das Formulare importiert werden. Dabei gehen alle Formatierungen verloren. qsts#:#legacy_text_cannot_be_edited#:#Der Text stammt aus einer überführten Frage und kann nicht geändert werden. +qsts#:#long_menu_gap#:#Longmenu +qsts#:#max_characters#:#Maximale Zeichenanzahl +qsts#:#min_auto_complete#:#Autovervollständigung +qsts#:#msg_no_questions_selected#:#Keine Fragen ausgewählt. +qsts#:#no_best_response_available#:#Keine beste Antwort definiert +qsts#:#no_gaps#:#Der eingegebene Text enthält keine Lücken. +qsts#:#no_response#:#Keine Antwort +qsts#:#no_response_given#:#Keine Antwort +qsts#:#other_response#:#Nicht die Beste Antwort +qsts#:#other_response_given#:#Dies ist nicht die bestmögliche Antwort. +qsts#:#out_of_range#:#Ausserhalb des Bereichs qsts#:#qst_lifecycle#:#Lebenszyklus qsts#:#qst_lifecycle_draft#:#Entwurf qsts#:#qst_lifecycle_filter_all#:#Alle Lebenszyklen @@ -14141,10 +14190,28 @@ qsts#:#qst_lifecycle_rejected#:#Abgelehnt qsts#:#qst_lifecycle_review#:#Überarbeitung notwendig qsts#:#qst_lifecycle_sharable#:#Verteilbar qsts#:#qst_remarks#:#Bemerkungen +qsts#:#question_create_mode#:#Art der Fragenerstellung +qsts#:#question_text#:#Fragentext +qsts#:#questionlist#:#Fragenliste +qsts#:#questions#:#Fragen +qsts#:#range_lower_limit#:#Untere Schranke +qsts#:#range_upper_limit#:#Obere Schranke +qsts#:#reset_preview#:#Vorschau zurücksetzen +qsts#:#save_and_new#:#Speichern und weitere Frage erstellen qsts#:#score_all#:#Alle Antworten bewerten qsts#:#score_distinct#:#Gleiche Antworten nur einmal bewerten qsts#:#scoring_of_identical_responses#:#Bewertung gleicher Antworten qsts#:#select_answer_form_type#:#Art des Antwortformulars auswählen +qsts#:#select_gap#:#Auswahl-Lücke +qsts#:#select_gaps_for_combination#:#Lücken zur Kombination auswählen +qsts#:#shuffle_answers#:#Antworten mischen +qsts#:#specific_feedback#:#Rückmeldung basierend auf der gewählten Antwortoption +qsts#:#step_size#:#Präzision +qsts#:#suggested_learning_content#:#Inhalte zur Wiederholung +qsts#:#text_matching_method#:#Textvergleichsmethode +qsts#:#upload_answer_options#:#Antwort-Optionen hochladen +qsts#:#upload_answer_options_info#:#Es kann eine Text-Datei hochgeladen werden, die eine Liste von Antwort-Optionen enthält. Die Datei muss eine Liste von Antwort-Optionen enthalten, wobei jede Antwort-Option auf einer neuen Zeile stehen muss. Die Antwort-Optionen werden zur bereits bestehenden Liste hinzugefügt. +qsts#:#upper_limit_bigger_than_lower#:#Die obere Schranke muss sowohl grösser sein als die untere als auch als die Präzision. rating#:#rat_not_rated_yet#:#Noch nicht bewertet rating#:#rat_nr_ratings#:#%s Bewertungen rating#:#rat_one_rating#:#Eine Bewertung @@ -16796,7 +16863,6 @@ survey#:#questionblock#:#Fragenblock survey#:#questionblock_inserted#:#Fragenblock eingefügt survey#:#questionblocks#:#Fragenblöcke survey#:#questionblocks_inserted#:#Fragenblöcke eingefügt -survey#:#questions#:#Fragen survey#:#questions_inserted#:#Frage(n) hinzugefügt. survey#:#questions_removed#:#Fragen und/oder Fragenblock entfernt! survey#:#questiontype#:#Fragetyp @@ -17097,6 +17163,7 @@ survey#:#svy_please_select_unused_codes#:#Bitte wählen Sie zumindest einen unge survey#:#svy_print_hide_labels#:#Labels ausblenden survey#:#svy_print_show_labels#:#Labels anzeigen survey#:#svy_privacy_info#:#Persönliche Daten +survey#:#svy_questions#:#Fragen survey#:#svy_rater#:#Feedback-Geber survey#:#svy_rater_see_app_info#:#Den Feedback-Gebern werden die Namen der Feedback-Nehmern angezeigt, damit die Fragen personenbezogen beantwortet werden können. survey#:#svy_reminder_mail_template#:#Mail-Vorlage diff --git a/lang/ilias_el.lang b/lang/ilias_el.lang index 0983be59ab38..e114c3aaab9d 100644 --- a/lang/ilias_el.lang +++ b/lang/ilias_el.lang @@ -423,7 +423,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Ενεργοποίηση καταγραφής διαγωνίσματος & αξιολόγησης assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###30 04 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Προσθήκη κειμένου κενών @@ -448,7 +447,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Παίρνετε βαθμού assessment#:#answer_is_right#:#Η λύση σας είναι σωστή assessment#:#answer_is_wrong#:#Η λύση σας είναι λάθος assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Κείμενο απάντησης assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -502,7 +500,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -572,10 +569,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length assessment#:#cloze_fixed_textlength_description#:#Εαν εισάγετε τιμή μεγαλύτερη του 0, όλα τα κείμενα και οι αριθμητικές τιμές θα δημιουργηθούν μέ αυτόν ώς μέγιστο αριθμό επιτρεπόμενο χαρακτήρων assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Κείμενο με κενά -assessment#:#cloze_textgap_case_insensitive#:#Χωρίς ταίριασμα πεζών-κεφαλαίων -assessment#:#cloze_textgap_case_sensitive#:#Με ταίριασμα πεζών-κεφαλαίων -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein απόσταση του %s assessment#:#code#:#Κώδικας assessment#:#codebase#:#Κωδικοσελίδα assessment#:#concatenation#:#Συγχώνευση @@ -758,9 +751,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Κενό assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Όρος γλωσσαρίου assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -778,7 +769,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Εισαγωγή μετά assessment#:#insert_before#:#Εισαγωγή πριν -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Εσωτερικοί σύνδεσμοι assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -890,7 +880,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Συντήρηση assessment#:#manscoring#:#Χειροκίνητη Βαθμολόγηση assessment#:#manscoring_done#:#Scored Participants###30 04 2009 new variable @@ -917,7 +906,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Φθάσατε το μέγιστο α assessment#:#maximum_points#:#Μέγιστη δυνατή βαθμολογία assessment#:#maxsize#:#Maximum file upload size ###30 04 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###30 04 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Πρέπει να ορίσετε ένα ελάχιστο ποσοστό 0%! Το βαθμολογικό σχήμα δεν αποθηκεύθηκε. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -926,7 +914,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -985,7 +972,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###30 04 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###30 04 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output assessment#:#output_mode#:#Κατάσταση εξόδου assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1054,7 +1040,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Είστε σίγουροι ότι θέλετε να διαγράψετε τις ακόλουθες ερωτήσεις; Αν διαγράψετε κλειδωμένες ερωτήσεις τα αποτελέσματα των τεστ που περιέχουν μια κλειδωμένη ερώτηση θα διαγραφούν. assessment#:#qpl_copy_insert_clipboard#:#Οι επιλεγμένες ερωτήσεις αντιγράφηκαν στο πρόχειρο assessment#:#qpl_copy_select_none#:#Παρακαλώ επιλέξτε τουλάχιστο μια ερώτηση για αντιγραφή στο πρόχειρο assessment#:#qpl_delete_rbac_error#:#Δεν έχετε δικαίωμα να διαγράψετε την ερώτηση! @@ -1107,7 +1092,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Η ερώτηση που πρόκειται να επεξεργαστείτε βρίσκεται στα τεστ %s. Αν αλλάξετε την ερώτηση, δεν θ' αλλάξει μέσα στα τεστ, επειδή το σύστημα δημιουργεί αντίγραφα της ερώτησης όταν την εισάγετε σ' ένα τεστ! assessment#:#qpl_questions_deleted#:#Οι ερωτήσεις διαγράφηκαν. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1137,14 +1121,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1168,17 +1144,14 @@ assessment#:#question_title#:#Τίτλος ερώτησης assessment#:#question_type#:#Τύπος ερώτησης assessment#:#questionpool_not_entered#:#Παρακαλώ εισάγετε ένα όνομα για την πηγή ερωτήσεων! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#ερωτήσεις από assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Αποδοχή δείγματος assessment#:#random_another_sample#:#Λήψη άλλου δείγματος assessment#:#random_selection#:#Τυχαία επιλογή assessment#:#range#:#Εύρος -assessment#:#range_lower_limit#:#Κάτω όριο assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Άνω όριο assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1254,7 +1227,6 @@ assessment#:#search_roles#:#Αναζήτηση Ρόλων assessment#:#search_term#:#Αναζήτηση Όρου assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Επιλογή κενού assessment#:#select_max_one_item#:#Παρακαλώ επιλέξτε μόνο ένα αντικείμενο assessment#:#select_one_user#:#Παρακαλώ επιλέξτε τουλάχιστο ένα χρήστη assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1281,7 +1253,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Εμφάνιση γενικής εικόνας αποτελεσμάτων του Χρήστη assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Εμφάνιση απαντήσεων του Χρήστη -assessment#:#shuffle_answers#:#Ανακάτεμα απαντήσεων assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###30 04 2009 new variable @@ -13972,6 +13943,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Κείμενο με κενά +qsts#:#cloze_textgapcase_insensitive#:#Χωρίς ταίριασμα πεζών-κεφαλαίων +qsts#:#cloze_textgapcase_sensitive#:#Με ταίριασμα πεζών-κεφαλαίων +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein απόσταση του %s +qsts#:#confirm_delete_questions#:#Είστε σίγουροι ότι θέλετε να διαγράψετε τις ακόλουθες ερωτήσεις; Αν διαγράψετε κλειδωμένες ερωτήσεις τα αποτελέσματα των τεστ που περιέχουν μια κλειδωμένη ερώτηση θα διαγραφούν. +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Κενό +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Κάτω όριο +qsts#:#range_upper_limit#:#Άνω όριο +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Επιλογή κενού +qsts#:#shuffle_answers#:#Ανακάτεμα απαντήσεων +qsts#:#suggested_learning_content#:#Add suggested solution###30 04 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16621,7 +16621,6 @@ survey#:#questionblock#:#Μπλοκ ερώτησης survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Μπλοκ ερωτήσεων survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Ερωτήσεις survey#:#questions_inserted#:#Εισαχθείσες ερωτήσεις! survey#:#questions_removed#:#Οι ερωτήσεις και/η τα μπλοκ ερωτήσεων διαγράφηκαν! survey#:#questiontype#:#Τύπος ερώτησης @@ -16922,6 +16921,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Ερωτήσεις survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 7a1e3493a148..f683044b08a7 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE Editor for WYSIWYG Editing assessment#:#activate_logging#:#Activate Test and Assessment Logging assessment#:#activate_manual_scoring#:#Enable Scoring assessment#:#activate_manual_scoring_desc#:#Enables Scoring for all question types. -assessment#:#addSuggestedSolution#:#Add Content for Recapitulation assessment#:#add_answers#:#Add answers assessment#:#add_circle#:#Add circle area assessment#:#add_gap#:#Add Gap Text @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Your solution is correct assessment#:#answer_is_wrong#:#Your solution is wrong assessment#:#answer_of#:#Answer of -assessment#:#answer_options#:#Answer Options: assessment#:#answer_question#:#Answer Question assessment#:#answer_text#:#Answer Text assessment#:#answer_types#:#Editor for Answers @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions. assessment#:#ass_create_export_file_with_results#:#incl. Participant Results assessment#:#ass_create_export_test_archive#:#as Archive File -assessment#:#ass_create_question#:#Create Question assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read. assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length assessment#:#cloze_fixed_textlength_description#:#If you enter a value, all text gap fields, not providing an own maximum character limitation, as well as all numeric gap fields will be created with a fixed length of this value, so entering more than the allowed characters is not possible. Note, that for numeric gaps the decimal separator is counted as a regular character. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value, the gap text field will be created with the value of the global fixed length. -assessment#:#cloze_text#:#Cloze Text -assessment#:#cloze_textgap_case_insensitive#:#Case Insensitive -assessment#:#cloze_textgap_case_sensitive#:#Case Sensitive -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein Distance of %s assessment#:#code#:#Code assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Concatenation @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input. assessment#:#fq_precision_info#:#Enter the number of desired decimal places. assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button ‘Parse Question’ to create editing forms for variables and results. -assessment#:#gap#:#Gap assessment#:#gap_combination#:#Gap Combination -assessment#:#gaps#:#Gaps assessment#:#glossary_term#:#Glossary Term assessment#:#goto_first_question#:#Show First Question assessment#:#grading_mark_msg#:#Your grade is: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer text (UTF-8) file to upload. assessment#:#insert_after#:#Insert After assessment#:#insert_before#:#Insert Before -assessment#:#insert_gap#:#Insert Gap assessment#:#interaction_type#:#Interaction Type assessment#:#internal_links#:#Internal Links assessment#:#intprecision#:#Divisible By @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options. assessment#:#longmenu_text#:#Long Menu Text -assessment#:#mainbar_button_label_questionlist#:#Question List assessment#:#maintenance#:#Maintenance assessment#:#manscoring#:#Scoring assessment#:#manscoring_done#:#Scored Participants @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#You have already taken this test the assessment#:#maximum_points#:#Maximum Available Points assessment#:#maxsize#:#Maximum file upload size assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead. -assessment#:#min_auto_complete#:#Autocomplete assessment#:#min_ip_label#:#Lowest IP With Access assessment#:#min_percentage_ne_0#:#One of your grade categories needs to start at the ‘Minimum Score Required (in %)’ level of 0% Your grading system hasn’t been saved. assessment#:#misc#:#Misc Options @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One assessment#:#mode_question#:#Question oriented assessment#:#mode_user#:#Participant oriented assessment#:#msg_circle_added#:#Circle added -assessment#:#msg_no_questions_selected#:#No questions were selected. assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions. assessment#:#msg_poly_added#:#Polygon added assessment#:#msg_questions_moved#:#Question(s) moved @@ -984,7 +971,6 @@ assessment#:#order#:#Order assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence. assessment#:#ordertext#:#Ordering Text assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units. -assessment#:#out_of_range#:#Out of range assessment#:#output#:#Output assessment#:#output_mode#:#Output Mode assessment#:#parseQuestion#:#Parse Question @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add assessment#:#qpl_bulk_save_overwrite#:#Overwrite assessment#:#qpl_bulkedit_success#:#Modifications saved. assessment#:#qpl_cancel_skill_assigns_update#:#Cancel -assessment#:#qpl_confirm_delete_questions#:#Are you sure you want to remove the following questions? assessment#:#qpl_copy_insert_clipboard#:#Selected question(s) successfully copied to clipboard. assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard assessment#:#qpl_delete_rbac_error#:#You have no rights to remove this question! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence assessment#:#qpl_question_is_in_use#:#The question you are about to edit exists in %s test(s). If you change this question, you will NOT change the question(s) in the test(s), because the system creates a copy of a question when it is inserted in a test! assessment#:#qpl_questions_deleted#:#Question(s) removed. -assessment#:#qpl_reset_preview#:#Reset Preview assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments assessment#:#qpl_settings_availability#:#Availability assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Existing taxonomies in this pool are offered for question filtering. @@ -1160,17 +1144,14 @@ assessment#:#question_type#:#Question Type assessment#:#questionlist_cannot_be_altered#:#The questionlist cannot be altered as the test already contains participant data sets. assessment#:#questionpool_not_entered#:#Please enter a name for a question pool! assessment#:#questionpool_not_selected#:#Please select a question pool! -assessment#:#questions#:#Questions assessment#:#questions_from#:#questions from assessment#:#questions_per_page_view#:#Page View assessment#:#random_accept_sample#:#Accept Sample assessment#:#random_another_sample#:#Get another Sample assessment#:#random_selection#:#Random Selection assessment#:#range#:#Range -assessment#:#range_lower_limit#:#Lower Bound assessment#:#range_max#:#Range (Maximum) assessment#:#range_min#:#Range (Minimum) -assessment#:#range_upper_limit#:#Upper Bound assessment#:#rated_sign#:#Sign assessment#:#rated_unit#:#Unit assessment#:#rated_value#:#Value @@ -1246,7 +1227,6 @@ assessment#:#search_roles#:#Search Roles assessment#:#search_term#:#Search Term assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger. assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock. -assessment#:#select_gap#:#Select Gap assessment#:#select_max_one_item#:#Please select one item only assessment#:#select_one_user#:#Please select at least one user. assessment#:#select_question#:#Select a Question @@ -1273,7 +1253,6 @@ assessment#:#show_old_introduction#:#Show old introduction assessment#:#show_pass_overview#:#Show Marked Pass Overview assessment#:#show_results#:#Show Results assessment#:#show_user_answers#:#Show User’s Marked Answers -assessment#:#shuffle_answers#:#Shuffle Answers assessment#:#skip_question#:#Do not Answer and Next assessment#:#solution#:#Solution assessment#:#solutionText#:#Text @@ -14097,12 +14076,82 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback qpl#:#qpl_page_type_qfbs#:#Special Feedback qpl#:#qpl_page_type_qht#:#Hint qpl#:#qpl_page_type_qpl#:#Question Page +qsts#:#above_range#:#Above Range +qsts#:#add_gap_combination#:#Add Combination of Gaps qsts#:#answer_form#:#Answer Form -qsts#:#cloze_enable_combinations#:#Lückenkombinationen aktivieren +qsts#:#answer_options#:#Answer Options +qsts#:#answer_options_awarding_points#:#Answer Options Awarding Points +qsts#:#answer_options_must_be_unique#:#All answer options must be unique. +qsts#:#async_view#:#Asynchronous View +qsts#:#at_least_one_gap_positiv_points#:#At least one gap needs to award points. +qsts#:#available_points#:#Available Points +qsts#:#awarded_points#:#Awarded Points +qsts#:#basic_answer_form_properties#:#Basic Answer Form Properties +qsts#:#below_range#:#Below Range +qsts#:#best_response#:#Best Response +qsts#:#best_response_given#:#You have provided the best possible response. +qsts#:#between#:#Between %s and %s +qsts#:#broken_answer_form#:#Broken Answer Form +qsts#:#byline_create_mode_full#:#The page editor is used. The question text can contain rich content and multiple answer forms. +qsts#:#byline_create_mode_simple#:#Only a simple text input is shown to enter the question text. The question text cannot be formatted and only one answer form can be created. At the end of the creation process a button is shown to directly create an additional question after saving. +qsts#:#cloze#:#Cloze +qsts#:#cloze_enable_gap_combinations#:#Lückenkombinationen aktivieren +qsts#:#cloze_text#:#Cloze Text +qsts#:#cloze_textgap_case_insensitive#:#Case Insensitive +qsts#:#cloze_textgap_case_sensitive#:#Case Sensitive +qsts#:#cloze_textgap_levenshtein_of#:#Levenshtein Distance of %s +qsts#:#combination_needs_more_than_one#:#A combination of gaps needs to contain more than one gap. +qsts#:#confirm_delete_feedback#:#Do you really want to delete this feedback. +qsts#:#confirm_delete_questions#:#Are you sure you want to remove the following questions? +qsts#:#confirm_remove_gaps#:#You have removed gaps from the cloze text. Do you really want to delete them and all associated information? qsts#:#cont_ed_insert_answf#:#Insert Answer Form +qsts#:#contained_answer_form_types#:#Contained Answer Form Types +qsts#:#contains_answer_form_types#:#Contains Answer Form Types qsts#:#create_answer_form#:#Create Answer Form -qsts#:#edit_basic_form_properties#:#Edit Basic Form Properties +qsts#:#create_feedback#:#Create Feedback +qsts#:#create_mode_full#:#Full +qsts#:#create_mode_simple#:#Simple +qsts#:#create_question#:#Create Question +qsts#:#default_user_settings#:#Default Settings +qsts#:#default_view#:#Default View +qsts#:#delete_combination#:#Delete Combination of Gaps +qsts#:#delete_responses#:#Delete Responses +qsts#:#disable_marking_allowing_partial_points#:#Disable Marking +qsts#:#disable_suggested_learning_content#:#Disable Suggested Learning Content +qsts#:#disable_text_feedback#:#Disable Feedback +qsts#:#edit_answer_options#:#Edit Answer Options +qsts#:#edit_available_points#:#Edit Available Points +qsts#:#edit_basic_answer_form_properties#:#Edit Basic Answer Form Properties +qsts#:#edit_basic_question_properties#:#Edit Basic Question Properties +qsts#:#edit_feedback#:#Edit Feedback +qsts#:#edit_gaps#:#Edit Gaps +qsts#:#edit_generic_feedback#:#Feedback Based On Reached Points +qsts#:#edit_points#:#Edit Points +qsts#:#edit_suggested_learning_content#:#Edit Suggested Learning Content +qsts#:#enable_marking_allowing_partial_points#:#Enable Marking +qsts#:#enable_suggested_learning_content#:#Enable Suggested Learning Content +qsts#:#enable_text_feedback#:#Enable Feedback +qsts#:#equal#:#Equal to %s +qsts#:#gap#:#Gap +qsts#:#gap_combination_already_exists#:#This combination of gaps already exists. +qsts#:#gap_combinations#:#Combinations of Gaps +qsts#:#gap_type#:#Type of Gap +qsts#:#gaps#:#Gaps +qsts#:#insert_gap#:#Insert Gap +qsts#:#insert_legacy_texts#:#Import Legacy Texts +qsts#:#insert_legacy_texts_info#:#Some texts have been migrated from a previous version. They cannot be edited directed, but you can import them into the form. All formatting will be lost. qsts#:#legacy_text_cannot_be_edited#:#This text was migrated from the previous question implementation and cannot be edited. +qsts#:#long_menu_gap#:#Longmenu +qsts#:#max_characters#:#Character Limit +qsts#:#min_auto_complete#:#Autocomplete +qsts#:#msg_no_questions_selected#:#No questions were selected. +qsts#:#no_best_response_available#:#The Best Response is not Defined +qsts#:#no_gaps#:#The provided text does not contain any gaps. +qsts#:#no_response#:#No Response +qsts#:#no_response_given#:#No Response Given +qsts#:#other_response#:#Not the Best Response +qsts#:#other_response_given#:#There are better responses. +qsts#:#out_of_range#:#Out of range qsts#:#qst_lifecycle#:#Lifecycle qsts#:#qst_lifecycle_draft#:#Draft qsts#:#qst_lifecycle_filter_all#:#All Lifecycles @@ -14112,10 +14161,28 @@ qsts#:#qst_lifecycle_rejected#:#Rejected qsts#:#qst_lifecycle_review#:#To be Reviewed qsts#:#qst_lifecycle_sharable#:#Shareable qsts#:#qst_remarks#:#Remarks +qsts#:#question_create_mode#:#Question Create Mode +qsts#:#question_text#:#Question Text +qsts#:#questionlist#:#Question List +qsts#:#questions#:#Questions +qsts#:#range_lower_limit#:#Lower Bound +qsts#:#range_upper_limit#:#Upper Bound +qsts#:#reset_preview#:#Reset Preview +qsts#:#save_and_new#:#Save and New qsts#:#score_all#:#All Responses are Scored qsts#:#score_distinct#:#Identical Responses are Only Scored Once qsts#:#scoring_of_identical_responses#:#Scoring of Identical Responses qsts#:#select_answer_form_type#:#Select Type of Answer Form +qsts#:#select_gap#:#Select Gap +qsts#:#select_gaps_for_combination#:#Select Gap for Combination +qsts#:#shuffle_answers#:#Shuffle Answers +qsts#:#specific_feedback#:#Feedback Based On Answer Options +qsts#:#step_size#:#Step Size +qsts#:#suggested_learning_content#:#Suggested Content +qsts#:#text_matching_method#:#Text Matching Method +qsts#:#upload_answer_options#:#Upload Answer Options +qsts#:#upload_answer_options_info#:#You can upload a file containing a list of answer options that will be added to the list. Each answer option needs to be on a new line. +qsts#:#upper_limit_bigger_than_lower#:#The upper bound must be bigger than the lower bound and the step size. rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16765,7 +16832,6 @@ survey#:#questionblock#:#Question Block survey#:#questionblock_inserted#:#Question block inserted. survey#:#questionblocks#:#Question Blocks survey#:#questionblocks_inserted#:#Question blocks inserted. -survey#:#questions#:#Questions survey#:#questions_inserted#:#Question(s) added. survey#:#questions_removed#:#Question and/or question block removal successful. survey#:#questiontype#:#Question Type @@ -17048,6 +17114,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels survey#:#svy_print_show_labels#:#Show labels survey#:#svy_privacy_info#:#Privacy +survey#:#svy_questions#:#Questions survey#:#svy_rater#:#Rater survey#:#svy_rater_see_app_info#:#Raters are shown the names of their appraisees so that they can meaningfully respond to the questions concerning those individuals. survey#:#svy_reminder_mail_template#:#Mail Template diff --git a/lang/ilias_es.lang b/lang/ilias_es.lang index 52c4dd49cd9a..cb63a766aa2f 100644 --- a/lang/ilias_es.lang +++ b/lang/ilias_es.lang @@ -425,7 +425,6 @@ adve#:#adve_use_tiny_mce#:#Activar el editor TinyMCE para edición WYSIWYG assessment#:#activate_logging#:#Activar el registro de pruebas y evaluaciones assessment#:#activate_manual_scoring#:#Activar puntuación manual assessment#:#activate_manual_scoring_desc#:#Habilita la puntuación manual para todos los tipos de preguntas. -assessment#:#addSuggestedSolution#:#Agregar contenido para recapitulación assessment#:#add_answers#:#Agregar respuestas assessment#:#add_circle#:#Agregar área circular assessment#:#add_gap#:#Agregar texto de hueco @@ -450,7 +449,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Has obtenido puntos por tu sol assessment#:#answer_is_right#:#Tu solución es correcta assessment#:#answer_is_wrong#:#Tu solución es incorrecta assessment#:#answer_of#:#Respuesta de -assessment#:#answer_options#:#Opciones de respuesta: assessment#:#answer_question#:#Responder pregunta assessment#:#answer_text#:#Texto de la respuesta assessment#:#answer_types#:#Editor de respuestas @@ -504,7 +502,6 @@ assessment#:#ass_completion_by_submission#:#Completado por envío assessment#:#ass_completion_by_submission_info#:#Si está habilitado, la entrega de al menos un archivo provoca la finalización de esta pregunta otorgando la puntuación máxima para esta pregunta. La puntuación puede modificarse manualmente más tarde. Cambiar esta configuración no afecta a las soluciones ya enviadas. assessment#:#ass_create_export_file_with_results#:#incl. Resultados de participantes assessment#:#ass_create_export_test_archive#:#como archivo comprimido -assessment#:#ass_create_question#:#Crear pregunta assessment#:#ass_imap_hint#:#Sugerencia que se mostrará como información sobre herramientas assessment#:#ass_imap_map_file_not_readable#:#No se pudo leer el mapa de imagen subido. assessment#:#ass_imap_no_map_found#:#No se pudo encontrar ningún formulario en el mapa de imagen subido. @@ -574,10 +571,6 @@ assessment#:#cloze_answer_text_info#:#Los espacios que precedan o sigan al texto assessment#:#cloze_fixed_textlength#:#Longitud del campo de texto assessment#:#cloze_fixed_textlength_description#:#Si introduce un valor, todos los campos de hueco de texto que no proporcionen una limitación propia de caracteres, así como todos los campos de hueco numéricos, se crearán con una longitud fija de este valor, por lo que no será posible introducir más caracteres de los permitidos. Tenga en cuenta que para los huecos numéricos el separador decimal se cuenta como un carácter normal. assessment#:#cloze_gap_size_info#:#Si introduce un valor mayor que 0, este campo de texto del hueco se creará con la longitud fija de ese valor. Si no introduce un valor, el campo de texto del hueco se creará con el valor de la longitud fija global. -assessment#:#cloze_text#:#Texto Cloze -assessment#:#cloze_textgap_case_insensitive#:#Sin distinción de mayúsculas/minúsculas -assessment#:#cloze_textgap_case_sensitive#:#Con distinción de mayúsculas/minúsculas -assessment#:#cloze_textgap_levenshtein_of#:#Distancia de Levenshtein de %s assessment#:#code#:#Código assessment#:#codebase#:#Base de código assessment#:#concatenation#:#Concatenación @@ -761,9 +754,7 @@ assessment#:#fq_formula_desc#:#Puede introducir variables predefinidas ($v1 a $v assessment#:#fq_no_restriction_info#:#Se aceptan tanto decimales como fracciones. assessment#:#fq_precision_info#:#Introduzca el número de decimales deseados. assessment#:#fq_question_desc#:#Puede definir variables insertando $v1, $v2 ... $vn, resultados insertando $r1, $r2 ... $rn en la posición deseada del texto de la pregunta. Haga clic en el botón ‘Analizar pregunta’ para crear formularios de edición para variables y resultados. -assessment#:#gap#:#Hueco assessment#:#gap_combination#:#Combinación de huecos -assessment#:#gaps#:#Huecos assessment#:#glossary_term#:#Término del glosario assessment#:#goto_first_question#:#Mostrar la primera pregunta assessment#:#grading_mark_msg#:#Su calificación es: "[mark]" @@ -781,7 +772,6 @@ assessment#:#info_answer_type_change#:#La pregunta ya contiene imágenes. No pue assessment#:#info_text_upload#:#Seleccione un archivo de texto de respuesta (UTF-8) para subir. assessment#:#insert_after#:#Insertar después assessment#:#insert_before#:#Insertar antes -assessment#:#insert_gap#:#Insertar hueco assessment#:#interaction_type#:#Tipo de interacción assessment#:#internal_links#:#Enlaces internos assessment#:#intprecision#:#Divisible por @@ -893,7 +883,6 @@ assessment#:#logs_wrong_test_password_provided#:#El participante introdujo una c assessment#:#longmenu#:#Menú largo assessment#:#longmenu_answeroptions_differ#:#Esta pregunta no funciona correctamente, ya que no hay la misma cantidad de huecos en el texto que en las opciones de corrección. assessment#:#longmenu_text#:#Texto del menú largo -assessment#:#mainbar_button_label_questionlist#:#Lista de preguntas assessment#:#maintenance#:#Mantenimiento assessment#:#manscoring#:#Calificación manual assessment#:#manscoring_done#:#Participantes calificados @@ -920,7 +909,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Ha alcanzado el número máximo de in assessment#:#maximum_points#:#Puntos máximos disponibles assessment#:#maxsize#:#Tamaño máximo de archivo subido assessment#:#maxsize_info#:#Introduzca el tamaño máximo en bytes que se permitirá para las cargas de archivos. Si deja este campo vacío, se elegirá en su lugar el tamaño máximo de esta instalación. -assessment#:#min_auto_complete#:#Autocompletar assessment#:#min_ip_label#:#IP más baja con acceso assessment#:#min_percentage_ne_0#:#Una de sus categorías de calificación debe comenzar en el nivel ‘Puntuación mínima requerida (en %)’ de 0%. Su sistema de calificaciones no se ha guardado. assessment#:#misc#:#Opciones varias @@ -929,7 +917,6 @@ assessment#:#mode_onebyone#:#One by One assessment#:#mode_question#:#Question oriented assessment#:#mode_user#:#Participant oriented assessment#:#msg_circle_added#:#Círculo añadido -assessment#:#msg_no_questions_selected#:#No se seleccionaron preguntas. assessment#:#msg_number_of_terms_too_low#:#El número de términos debe ser mayor o igual que el número de definiciones. assessment#:#msg_poly_added#:#Polígono añadido assessment#:#msg_questions_moved#:#Pregunta(s) movida(s) @@ -988,7 +975,6 @@ assessment#:#order#:#Orden assessment#:#ordering_answer_sequence_info#:#La secuencia de respuestas que defina aquí se considerará la secuencia de solución correcta. assessment#:#ordertext#:#Texto para ordenar assessment#:#ordertext_info#:#Por favor, introduzca el texto que debe ordenarse horizontalmente. El texto para ordenar será separado por los espacios en blanco del texto. Si necesita una separación diferente, puede usar el separador %s para separar sus unidades de texto. -assessment#:#out_of_range#:#Fuera de rango assessment#:#output#:#Salida assessment#:#output_mode#:#Modo de salida assessment#:#parseQuestion#:#Analizar pregunta @@ -1057,7 +1043,6 @@ assessment#:#qpl_bulk_save_add#:#Agregar assessment#:#qpl_bulk_save_overwrite#:#Sobrescribir assessment#:#qpl_bulkedit_success#:#Modificaciones guardadas. assessment#:#qpl_cancel_skill_assigns_update#:#Cancelar -assessment#:#qpl_confirm_delete_questions#:#¿Está seguro de que desea eliminar las siguientes preguntas? assessment#:#qpl_copy_insert_clipboard#:#La(s) pregunta(s) seleccionada(s) se han copiado al portapapeles assessment#:#qpl_copy_select_none#:#Por favor seleccione al menos una pregunta para copiarla al portapapeles assessment#:#qpl_delete_rbac_error#:#¡No tiene permisos para eliminar esta pregunta! @@ -1110,7 +1095,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competencia assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Suma total de puntos de competencia por competencia assessment#:#qpl_question_is_in_use#:#La pregunta que está a punto de editar existe en %s prueba(s). Si cambia esta pregunta, NO cambiará la(s) pregunta(s) en la(s) prueba(s), ¡porque el sistema crea una copia de una pregunta cuando se inserta en una prueba! assessment#:#qpl_questions_deleted#:#Pregunta(s) eliminada(s). -assessment#:#qpl_reset_preview#:#Restablecer vista previa assessment#:#qpl_save_skill_assigns_update#:#Guardar asignaciones de competencias assessment#:#qpl_settings_availability#:#Disponibilidad assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Las taxonomías existentes en este banco de preguntas se ofrecen para filtrar preguntas. @@ -1140,14 +1124,6 @@ assessment#:#qst_essay_chars_remaining#:#Caracteres restantes: assessment#:#qst_essay_wordcounter_enabled#:#Contar palabras assessment#:#qst_essay_wordcounter_enabled_info#:#Se cuentan las palabras introducidas. El número de palabras escritas se muestra a los participantes debajo del campo de entrada de texto. assessment#:#qst_essay_written_words#:#Número de palabras introducidas: -assessment#:#qst_lifecycle#:#Ciclo de vida -assessment#:#qst_lifecycle_draft#:#Borrador -assessment#:#qst_lifecycle_filter_all#:#Todos los ciclos de vida -assessment#:#qst_lifecycle_final#:#Final -assessment#:#qst_lifecycle_outdated#:#Obsoleto -assessment#:#qst_lifecycle_rejected#:#Rechazado -assessment#:#qst_lifecycle_review#:#Por revisar -assessment#:#qst_lifecycle_sharable#:#Compartible assessment#:#qst_nested_nested_answers_off#:#Sin sangrías, solo orden assessment#:#qst_nested_nested_answers_on#:#Usar sangrías en las respuestas assessment#:#qst_nr_of_tries#:#Número de intentos @@ -1172,17 +1148,14 @@ assessment#:#question_type#:#Tipo de pregunta assessment#:#questionlist_cannot_be_altered#:#La lista de preguntas no puede modificarse porque la prueba ya contiene conjuntos de datos de participantes. assessment#:#questionpool_not_entered#:#¡Por favor ingrese un nombre para el banco de preguntas! assessment#:#questionpool_not_selected#:#¡Por favor seleccione un banco de preguntas! -assessment#:#questions#:#Preguntas assessment#:#questions_from#:#preguntas de assessment#:#questions_per_page_view#:#Vista de página assessment#:#random_accept_sample#:#Aceptar muestra assessment#:#random_another_sample#:#Obtener otra muestra assessment#:#random_selection#:#Selección aleatoria assessment#:#range#:#Rango -assessment#:#range_lower_limit#:#Límite inferior assessment#:#range_max#:#Rango (máximo) assessment#:#range_min#:#Rango (mínimo) -assessment#:#range_upper_limit#:#Límite superior assessment#:#rated_sign#:#Signo assessment#:#rated_unit#:#Unidad assessment#:#rated_value#:#Valor @@ -1258,7 +1231,6 @@ assessment#:#search_roles#:#Buscar Roles assessment#:#search_term#:#Término de búsqueda assessment#:#select_at_least_one_feedback_type_and_trigger#:#Por favor, seleccione al menos un tipo de feedback y un desencadenador. assessment#:#select_at_least_one_lock_answer_type#:#Por favor, seleccione al menos un tipo de bloqueo de respuesta. -assessment#:#select_gap#:#Seleccionar hueco assessment#:#select_max_one_item#:#Por favor, seleccione solo un elemento assessment#:#select_one_user#:#Por favor, seleccione al menos un usuario. assessment#:#select_question#:#Seleccionar una pregunta @@ -1285,7 +1257,6 @@ assessment#:#show_old_introduction#:#Mostrar introducción antigua assessment#:#show_pass_overview#:#Mostrar resumen de pases marcados assessment#:#show_results#:#Mostrar resultados assessment#:#show_user_answers#:#Mostrar respuestas marcadas del usuario -assessment#:#shuffle_answers#:#Barajar respuestas assessment#:#skip_question#:#No responder y pasar a la siguiente assessment#:#solution#:#Solución assessment#:#solutionText#:#Texto @@ -14036,6 +14007,35 @@ qpl#:#qpl_page_type_qfbg#:#Feedback general qpl#:#qpl_page_type_qfbs#:#Feedback especial qpl#:#qpl_page_type_qht#:#Sugerencia qpl#:#qpl_page_type_qpl#:#Página de pregunta +qsts#:#answer_options#:#Opciones de respuesta +qsts#:#cloze_text#:#Texto Cloze +qsts#:#cloze_textgapcase_insensitive#:#Sin distinción de mayúsculas/minúsculas +qsts#:#cloze_textgapcase_sensitive#:#Con distinción de mayúsculas/minúsculas +qsts#:#cloze_textgaplevenshtein_of#:#Distancia de Levenshtein de %s +qsts#:#confirm_delete_questions#:#¿Está seguro de que desea eliminar las siguientes preguntas? +qsts#:#create_question#:#Crear pregunta +qsts#:#gap#:#Hueco +qsts#:#gaps#:#Huecos +qsts#:#insert_gap#:#Insertar hueco +qsts#:#min_auto_complete#:#Autocompletar +qsts#:#msg_no_questions_selected#:#No se seleccionaron preguntas. +qsts#:#out_of_range#:#Fuera de rango +qsts#:#qst_lifecycle#:#Ciclo de vida +qsts#:#qst_lifecycle_draft#:#Borrador +qsts#:#qst_lifecycle_filter_all#:#Todos los ciclos de vida +qsts#:#qst_lifecycle_final#:#Final +qsts#:#qst_lifecycle_outdated#:#Obsoleto +qsts#:#qst_lifecycle_rejected#:#Rechazado +qsts#:#qst_lifecycle_review#:#Por revisar +qsts#:#qst_lifecycle_sharable#:#Compartible +qsts#:#questionlist#:#Lista de preguntas +qsts#:#questions#:#Preguntas +qsts#:#range_lower_limit#:#Límite inferior +qsts#:#range_upper_limit#:#Límite superior +qsts#:#reset_preview#:#Restablecer vista previa +qsts#:#select_gap#:#Seleccionar hueco +qsts#:#shuffle_answers#:#Barajar respuestas +qsts#:#suggested_learning_content#:#Agregar contenido para recapitulación rating#:#rat_not_rated_yet#:#Aún sin calificar rating#:#rat_nr_ratings#:#%s Calificaciones rating#:#rat_one_rating#:#Una calificación @@ -16691,7 +16691,6 @@ survey#:#questionblock#:#Bloque de preguntas survey#:#questionblock_inserted#:#Bloque de preguntas insertado. survey#:#questionblocks#:#Bloques de preguntas survey#:#questionblocks_inserted#:#Bloques de preguntas insertados. -survey#:#questions#:#Preguntas survey#:#questions_inserted#:#Pregunta(s) añadida(s). survey#:#questions_removed#:#La eliminación de preguntas y/o bloques de preguntas se ha realizado con éxito. survey#:#questiontype#:#Tipo de pregunta @@ -16992,6 +16991,7 @@ survey#:#svy_please_select_unused_codes#:#Por favor, seleccione al menos un cód survey#:#svy_print_hide_labels#:#Ocultar etiquetas survey#:#svy_print_show_labels#:#Mostrar etiquetas survey#:#svy_privacy_info#:#Privacidad +survey#:#svy_questions#:#Preguntas survey#:#svy_rater#:#Evaluador survey#:#svy_rater_see_app_info#:#A los evaluadores se les muestran los nombres de sus evaluados para que puedan responder de forma significativa a las preguntas relativas a esas personas. survey#:#svy_reminder_mail_template#:#Plantilla de correo diff --git a/lang/ilias_et.lang b/lang/ilias_et.lang index c7ab92d89998..9a2ed9537ce7 100644 --- a/lang/ilias_et.lang +++ b/lang/ilias_et.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Võimalda TinyMCE WYSIWYG toimetamiseks assessment#:#activate_logging#:#aktiveeri testi ja hindamise logimine assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Lisa soovituslik lahendus assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Lisa ringiala assessment#:#add_gap#:#Lisa lünka tekst @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Said vastuse eest punkti(d), k assessment#:#answer_is_right#:#Lahendus on õige assessment#:#answer_is_wrong#:#Lahendus on vale assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Vastustekst assessment#:#answer_types#:#Vastusetüübid @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Tööd saadetud assessment#:#ass_completion_by_submission_info#:#Võimaldamise korral loetakse antud küsimus vastatuks vähemalt ühe faili esitamisel ja selle eest saab maksimaalse punktisumma. Punktisummat saab hiljem käsitsi muuta. Antud seade vahetamine ei muuda olemasolevaid ehk juba ära saadetud vastuseid. assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File -assessment#:#ass_create_question#:#Create Question assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Tekstivälja pikkus assessment#:#cloze_fixed_textlength_description#:#Kui sisestad nullist suurema väärtuse, luuakse kõik teksti- ja numbriväljad selles väärtuses fikseeritud pikkuses. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length. -assessment#:#cloze_text#:#Lünktekst -assessment#:#cloze_textgap_case_insensitive#:#Mittetundlik variant -assessment#:#cloze_textgap_case_sensitive#:#Tundlik variant -assessment#:#cloze_textgap_levenshtein_of#:#Levenšteini kaugus of %s assessment#:#code#:#Kood assessment#:#codebase#:#Koodibaas assessment#:#concatenation#:#Jätkamine @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input. assessment#:#fq_precision_info#:#Enter the number of desired decimal places. assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results. -assessment#:#gap#:#Lünk assessment#:#gap_combination#:#Gap Combination -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Sõnastiku mõiste assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Küsimus juba sisaldab pilte. Te ei saa m assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Sisesta pärast assessment#:#insert_before#:#Sisesta enne -assessment#:#insert_gap#:#Insert Gap assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Sisemised lingid assessment#:#intprecision#:#Divisible By @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Säilitamine assessment#:#manscoring#:#Käsitsi punktihaldus assessment#:#manscoring_done#:#Hinnatud osalejad @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Lubatud soorituste arv on täis. Test assessment#:#maximum_points#:#Maksimaalsed võimalikud punktid assessment#:#maxsize#:#Üleslaetava faili maksimaalne suurus assessment#:#maxsize_info#:#Sisesta üleslaetavate failide lubatud maksimumsuurus baitides. Kui jätad selle välja tühjaks, valib see installatsioon lubatud maksimumsuruse ise. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Sa pead määrama miinimumtasemeks 0 protsenti! Hindeskeemi ei salvestatud. assessment#:#misc#:#Misc Options @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Ringiala lisatud -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Mõistete arv peab olema võrdne (või suurem) definitsioonide arvuga. assessment#:#msg_poly_added#:#Hulknurk lisatud assessment#:#msg_questions_moved#:#Küsimus(ed) on ümber tõstetud @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Vastuste järjestust siin arvestatakse kui õige vastuse järjestust. assessment#:#ordertext#:#Teksti järjestamine assessment#:#ordertext_info#:#Palun sisesta tekst, mis peaks olema järjestatud horisontaalselt. Järjestatav tekst eraldatakse tekstis olevate valgete tühikutega. Kui vajad muud eraldajat, võid kasutada eraldajat %s oma tekstiosade eraldamiseks. -assessment#:#out_of_range#:#Out of range assessment#:#output#:#Väljund assessment#:#output_mode#:#Väljundi olek assessment#:#parseQuestion#:#Parse Question @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Kas oled kindel, et soovid järgmise(d) küsimuse(d) kustutada? Kui sa kustutad lukustatud küsimused, kustutatakse ka kõiki lukustatud küsimusi sisaldavad testitulemused. assessment#:#qpl_copy_insert_clipboard#:#Valitud küsimus(ed) on puhvrisse salvestatud. assessment#:#qpl_copy_select_none#:#Palun märgista puhvrisse kopeerimiseks vähemalt üks küsimus assessment#:#qpl_delete_rbac_error#:#Sul ei ole piisavalt õigusi selle küsimuse kustutamiseks! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Küsimus, mida muuta soovid, on käigus juba %s testis. Kui muudad seda küsimust, ei muuda sa seda küsimust testi(de)s, sest süsteem loob testidesse sisestamiseks küsimusest koopia! assessment#:#qpl_questions_deleted#:#Küsimus(ed) on kustutatud. -assessment#:#qpl_reset_preview#:#Reset Preview assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Katsetuste arv @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Küsimuse pealkiri assessment#:#question_type#:#Küsimuse liik assessment#:#questionpool_not_entered#:#Palun sisesta küsimustevaramu nimi! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#küsimust assessment#:#questions_per_page_view#:#Lehe vaade assessment#:#random_accept_sample#:#Kinnita küsimused assessment#:#random_another_sample#:#Küsi uusi küsimusi assessment#:#random_selection#:#Juhuvalik assessment#:#range#:#Vahemik -assessment#:#range_lower_limit#:#Alumine piir assessment#:#range_max#:#Range (Maximum) assessment#:#range_min#:#Range (Minimum) -assessment#:#range_upper_limit#:#Ülemine piir assessment#:#rated_sign#:#Sign assessment#:#rated_unit#:#Unit assessment#:#rated_value#:#Value @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Otsi rolle assessment#:#search_term#:#Otsingusõna assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Vali lünk assessment#:#select_max_one_item#:#Palun vali ainult üks element. assessment#:#select_one_user#:#Palun vali vähemalt üks kasutaja. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Näita märgistatud soorituse ülevaadet assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Näita kasutaja märgitud vastuseid -assessment#:#shuffle_answers#:#Sega vastuseid assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Tekst @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Lünktekst +qsts#:#cloze_textgapcase_insensitive#:#Mittetundlik variant +qsts#:#cloze_textgapcase_sensitive#:#Tundlik variant +qsts#:#cloze_textgaplevenshtein_of#:#Levenšteini kaugus of %s +qsts#:#confirm_delete_questions#:#Kas oled kindel, et soovid järgmise(d) küsimuse(d) kustutada? Kui sa kustutad lukustatud küsimused, kustutatakse ka kõiki lukustatud küsimusi sisaldavad testitulemused. +qsts#:#create_question#:#Create Question +qsts#:#gap#:#Lünk +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Alumine piir +qsts#:#range_upper_limit#:#Ülemine piir +qsts#:#reset_preview#:#Reset Preview +qsts#:#select_gap#:#Vali lünk +qsts#:#shuffle_answers#:#Sega vastuseid +qsts#:#suggested_learning_content#:#Lisa soovituslik lahendus rating#:#rat_not_rated_yet#:#Pole veel hääletatud rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#Üks hääletus @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Küsimusteplokk survey#:#questionblock_inserted#:#Küsimusteplokk on sisestatud survey#:#questionblocks#:#Küsimusteplokid survey#:#questionblocks_inserted#:#Küsimusteplokid on sisestatud -survey#:#questions#:#Küsimused survey#:#questions_inserted#:#Küsimus(ed) on sisestatud! survey#:#questions_removed#:#Küsimus(ed) ja/või küsimusteplok(id) on eemaldatud! survey#:#questiontype#:#Küsimuse tüüp @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Küsimused survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_fa.lang b/lang/ilias_fa.lang index 6dd4ffa6a623..fedabde89a79 100644 --- a/lang/ilias_fa.lang +++ b/lang/ilias_fa.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#فعال کردن ثبت وقایع تست و ارزیابی assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#اضافه کردن راه حل پیشنهادی assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area assessment#:#add_gap#:#Add Gap Text @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Your solution is correct assessment#:#answer_is_wrong#:#Your solution is wrong assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#متن جواب assessment#:#answer_types#:#انواع جواب @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#کامل شده با ارسال assessment#:#ass_completion_by_submission_info#:#اگر فعال شود، ارسال حداقل یک فایل باعث کامل شدن سوال شده و نمره دهی هم انجام می شود. assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#طول فیلد متن assessment#:#cloze_fixed_textlength_description#:#اگر مقداری بیشتر از 0 وارد کنید، همه فیلدهای متن با این اندازه ساخته خواهند شد. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#متن -assessment#:#cloze_textgap_case_insensitive#:#غیر حساس به کوچک و بزرگی حروف -assessment#:#cloze_textgap_case_sensitive#:#حساس به کوچک و بزرگی حروف -assessment#:#cloze_textgap_levenshtein_of#:#فاصله Levenshtein %s assessment#:#code#:#کد assessment#:#codebase#:#پایگاه کد assessment#:#concatenation#:#Concatenation @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Gap assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#اصطلاح لغتنامه assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Insert After assessment#:#insert_before#:#Insert Before -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Internal Links assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#نگهداری assessment#:#manscoring#:#امتیازدهی دستی assessment#:#manscoring_done#:#شرکت کنندگان دارای نمره @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#You have reached the maximum number o assessment#:#maximum_points#:#حداکثر امتیازات موجود assessment#:#maxsize#:#حداکثر اندازه فایل برای آپلود assessment#:#maxsize_info#:#مقدار به بایت -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#You must define a minimum percentage of 0 percent! The mark schema wasn't saved. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions. assessment#:#msg_poly_added#:#Polygon added assessment#:#msg_questions_moved#:#Question(s) moved @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#دنباله جوابی که اینجا مشخص می کنید به عنوان دنباله جواب در نظر گرفته خواهد شد. assessment#:#ordertext#:#متن مرتب سازی assessment#:#ordertext_info#:#لطفا متنی را که باید بصورت افقی مرتب شود را وارد کنید. متنها با فاصله از هم جدا می شوند. می توانید از %s هم برای جدا کردن واحدهای متن استفاده کنید. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output assessment#:#output_mode#:#حالت خروجی assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Are you sure you want to delete the following question(s)? assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard assessment#:#qpl_delete_rbac_error#:#You have no rights to delete this question! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#The question you are about to edit exists in %s test(s). If you change this question, you will NOT change the question(s) in the test(s), because the system creates a copy of a question when it is inserted in a test! assessment#:#qpl_questions_deleted#:#Question(s) deleted. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#عنوان سوال assessment#:#question_type#:#نوع سوال assessment#:#questionpool_not_entered#:#Please enter a name for a question pool! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#questions from assessment#:#questions_per_page_view#:#نمای صفحه assessment#:#random_accept_sample#:#Accept Sample assessment#:#random_another_sample#:#Get another Sample assessment#:#random_selection#:#Random Selection assessment#:#range#:#محدوده -assessment#:#range_lower_limit#:#حد پایین assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#حد بالا assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Search Roles assessment#:#search_term#:#Search Term assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Select Gap assessment#:#select_max_one_item#:#Please select one item only assessment#:#select_one_user#:#لطفا حداقل یک کاربر انتخاب کنید. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#نمایش مرور کلی دوره انتخاب شده assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#نمایش پاسخهای کاربر انتخاب شده -assessment#:#shuffle_answers#:#قاطی کردن جوابها assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#متن @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#متن +qsts#:#cloze_textgapcase_insensitive#:#غیر حساس به کوچک و بزرگی حروف +qsts#:#cloze_textgapcase_sensitive#:#حساس به کوچک و بزرگی حروف +qsts#:#cloze_textgaplevenshtein_of#:#فاصله Levenshtein %s +qsts#:#confirm_delete_questions#:#Are you sure you want to delete the following question(s)? +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Gap +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#حد پایین +qsts#:#range_upper_limit#:#حد بالا +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Select Gap +qsts#:#shuffle_answers#:#قاطی کردن جوابها +qsts#:#suggested_learning_content#:#اضافه کردن راه حل پیشنهادی rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16621,7 +16621,6 @@ survey#:#questionblock#:#Question Block survey#:#questionblock_inserted#:#Question Block inserted survey#:#questionblocks#:#بلوکهای سوال survey#:#questionblocks_inserted#:#Question Blocks inserted -survey#:#questions#:#سوالها survey#:#questions_inserted#:#Question(s) inserted! survey#:#questions_removed#:#Question(s) and/or question block(s) removed! survey#:#questiontype#:#Question Type @@ -16922,6 +16921,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#سوالها survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_fr.lang b/lang/ilias_fr.lang index 2ebad3a3957f..b4fe4570f655 100644 --- a/lang/ilias_fr.lang +++ b/lang/ilias_fr.lang @@ -407,7 +407,6 @@ adve#:#adve_use_tiny_mce#:#Activez TinyMCE pour une Edition WYSIWYG assessment#:#activate_logging#:#Activer le Suivi des Tests et Evaluations assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Ajouter solution suggérée assessment#:#add_answers#:#Add Antworten assessment#:#add_circle#:#Ajouter Zone Circulaire assessment#:#add_gap#:#Ajouter une nouvelle valeur @@ -432,7 +431,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Vous obtenez des points pour v assessment#:#answer_is_right#:#Votre réponse est correcte assessment#:#answer_is_wrong#:#Votre réponse est incorrecte assessment#:#answer_of#:#Answer of -assessment#:#answer_options#:#Options de réponse: assessment#:#answer_question#:#Répondez à la question assessment#:#answer_text#:#Texte de Réponse assessment#:#answer_types#:#Types de réponse @@ -486,7 +484,6 @@ assessment#:#ass_completion_by_submission#:#Validation Automatique assessment#:#ass_completion_by_submission_info#:#Le dépôt d'un fichier permet de valider cette question en attribuant le score maximum. Le score peut toutefois être modifié ultérieurement. Modifier cette option n'affecte pas les réponses déjà fournies. assessment#:#ass_create_export_file_with_results#:#Créer un fichier d’exportation de tests (y compris les résultats des participants) assessment#:#ass_create_export_test_archive#:#Créer Archive du Test -assessment#:#ass_create_question#:#Créer Question assessment#:#ass_imap_hint#:#Indice à montrer comme info-bulle assessment#:#ass_imap_map_file_not_readable#:#L’image de carte chargée n’a pas pu être lue. assessment#:#ass_imap_no_map_found#:#N’a pas pu trouver de forme dans la carte d’image chargée. @@ -556,10 +553,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Longueur du Champ Texte assessment#:#cloze_fixed_textlength_description#:#Si une valeur supérieure à zéro est saisie , tous les champs de texte seront créés avec cette valeur comme longueur de champ. assessment#:#cloze_gap_size_info#:#Si vous entrez une valeur supérieure à 0, ce champ de texte d'écart sera créé avec la longueur fixe de cette valeur. Si vous n'entrez pas de valeur, le champ de texte d'écart sera créé avec la valeur de la longueur fixe globale. -assessment#:#cloze_text#:#Texte à trous -assessment#:#cloze_textgap_case_insensitive#:#Ne pas distinguer majuscules et minuscules -assessment#:#cloze_textgap_case_sensitive#:#Distinguer majuscules et minuscules -assessment#:#cloze_textgap_levenshtein_of#:#Distance Levenshtein de %s assessment#:#code#:#Code assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Concaténation @@ -742,9 +735,7 @@ assessment#:#fq_formula_desc#:#Vous pouvez entrer des variables prédéfinies ($ assessment#:#fq_no_restriction_info#:#Les décimales et les fractions sont des entrées acceptées. assessment#:#fq_precision_info#:#Entrez le nombre de décimales souhaitées. assessment#:#fq_question_desc#:#Vous pouvez définir des variables en insérant $v1, $v2 ... $vn, des résultats en insérant $r1, $r2 .... $rn aux positions souhaitées dans le texte de la question. Cliquer sur le bouton "Traiter la Question" pour créer le formulaire d'édition des variables et réponses. -assessment#:#gap#:#Trou assessment#:#gap_combination#:#Association de Gap -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Terme de glossaire assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Votre résultat est: "[mark]" @@ -762,7 +753,6 @@ assessment#:#info_answer_type_change#:#La question contient déjà des images. assessment#:#info_text_upload#:#Choisissez un fichier de réponses à télécharger assessment#:#insert_after#:#Ajouter après assessment#:#insert_before#:#Ajouter avant -assessment#:#insert_gap#:#Insérer Ecart assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Liens internes assessment#:#intprecision#:#Divisible Par @@ -874,7 +864,6 @@ assessment#:#logs_wrong_test_password_provided#:#Le Participant a entré un mot assessment#:#longmenu#:#Longmenu assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options. assessment#:#longmenu_text#:#Texte de menu long -assessment#:#mainbar_button_label_questionlist#:#Questionlist###31 10 2023 new variable assessment#:#maintenance#:#Maintenance assessment#:#manscoring#:#Notation Manuelle assessment#:#manscoring_done#:#Participants notés @@ -901,7 +890,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Vous avez atteint le nombre de tentat assessment#:#maximum_points#:#Nombre Maximum de Points assessment#:#maxsize#:#Taille maximale du fichier à télécharger assessment#:#maxsize_info#:#Saisir la taille maximale en bytes autorisée pour le téléversement. Si cette zone de saisie reste vide, le maximum par défaut sera proposé. -assessment#:#min_auto_complete#:#Saisie automatique assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Vous devez définir un pourcentage minimal de 0 %. L'échelle d'évaluation n'a pas été enregistrée. assessment#:#misc#:#Options Mixtes @@ -910,7 +898,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Cercle ajouté -assessment#:#msg_no_questions_selected#:#No questions were selected.###31 10 2023 new variable assessment#:#msg_number_of_terms_too_low#:#Le nombre de termes doit être supérieur ou équal au nombre de définitions. assessment#:#msg_poly_added#:#Polygone ajouté assessment#:#msg_questions_moved#:#Question(s) déplacée(s) @@ -969,7 +956,6 @@ assessment#:#order#:#Order assessment#:#ordering_answer_sequence_info#:#La séquence de réponse définie, ici, sera prise comme séquence correcte de la solution. assessment#:#ordertext#:#Texte à ordonner assessment#:#ordertext_info#:#Veuillez saisir le texte devant être ordonné horizontalement. Le texte à ordonner devra être séparé par des caractères blancs dans le texte. Vous pouvez aussi utiliser le séparateur %s pour séparer vos groupes de texte. -assessment#:#out_of_range#:#Hors de portée assessment#:#output#:#Restitution assessment#:#output_mode#:#Mode de Restitution assessment#:#parseQuestion#:#Traiter la Question @@ -1038,7 +1024,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Annuler -assessment#:#qpl_confirm_delete_questions#:#Etes-vous sûr(e) de vouloir supprimer la(les) question(s) suivante(s) ? Si vous détruisez des questions verrouillées, les résultats de tous les tests contenant une question verrouillée seront détruits également. assessment#:#qpl_copy_insert_clipboard#:#La ou les questions sélectionnés sont copiées dans le presse-papier assessment#:#qpl_copy_select_none#:#Cocher au moins une question à copier dans le presse-papier assessment#:#qpl_delete_rbac_error#:#Vous n'avez pas le droit de supprimer cette question! @@ -1091,7 +1076,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Compétence assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Somme totale de points de compétence par compétence assessment#:#qpl_question_is_in_use#:#La question que vous êtes sur le point de modifier existe dans %s test(s). Si vous changez cette question, vous ne la changerez pas dans ce(s) test(s), car le système crée une copie de la question quand elle est ajoutée dans un test. assessment#:#qpl_questions_deleted#:#Question(s) détruite(s). -assessment#:#qpl_reset_preview#:#Réinitialiser l'aperàçu assessment#:#qpl_save_skill_assigns_update#:#Sauvegarder les tâches de compétence assessment#:#qpl_settings_availability#:#Availability###31 10 2023 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Lorsque activé, les taxinomies créées sont utilisées pour le filtrage. @@ -1121,14 +1105,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters: assessment#:#qst_essay_wordcounter_enabled#:#Count Words assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field. assessment#:#qst_essay_written_words#:#Number of entered words: -assessment#:#qst_lifecycle#:#Lifecycle -assessment#:#qst_lifecycle_draft#:#Draft -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles -assessment#:#qst_lifecycle_final#:#Final -assessment#:#qst_lifecycle_outdated#:#Outdated -assessment#:#qst_lifecycle_rejected#:#Rejected -assessment#:#qst_lifecycle_review#:#To be Reviewed -assessment#:#qst_lifecycle_sharable#:#Sharable assessment#:#qst_nested_nested_answers_off#:#No indents, just order assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers assessment#:#qst_nr_of_tries#:#Nombre de tentatives @@ -1152,17 +1128,14 @@ assessment#:#question_title#:#Titre de la Question assessment#:#question_type#:#Type de Question assessment#:#questionpool_not_entered#:#Veuillez saisir un nom pour un groupe de questions. assessment#:#questionpool_not_selected#:#Please select a question pool! -assessment#:#questions#:#Questions###31 10 2023 new variable assessment#:#questions_from#:#questions de assessment#:#questions_per_page_view#:#Vue par Page assessment#:#random_accept_sample#:#Accepter échantillon assessment#:#random_another_sample#:#Obtenir un autre échantillon assessment#:#random_selection#:#Tirage Aléatoire assessment#:#range#:#Etendue -assessment#:#range_lower_limit#:#Limite Inférieure assessment#:#range_max#:#Intervalle (Maximum) assessment#:#range_min#:#Intervalle (Minimum) -assessment#:#range_upper_limit#:#Limite Supérieure assessment#:#rated_sign#:#Signe assessment#:#rated_unit#:#Unité assessment#:#rated_value#:#Valeur @@ -1238,7 +1211,6 @@ assessment#:#search_roles#:#Recherche des rôles assessment#:#search_term#:#Rechercher des termes assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Selectionner trou assessment#:#select_max_one_item#:#Veuillez ne saisir qu'un objet assessment#:#select_one_user#:#Veuillez sélectionner au moins un utilisateur assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1265,7 +1237,6 @@ assessment#:#show_old_introduction#:#Afficher l'ancienne introduction assessment#:#show_pass_overview#:#Afficher les Résultats assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Afficher les Réponses de l'Utilisateur -assessment#:#shuffle_answers#:#Mélanger les Réponses assessment#:#skip_question#:#Ne pas répondre et passer assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Texte @@ -13956,6 +13927,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback qpl#:#qpl_page_type_qfbs#:#Special Feedback qpl#:#qpl_page_type_qht#:#Hint qpl#:#qpl_page_type_qpl#:#Question Page +qsts#:#answer_options#:#Options de réponse +qsts#:#cloze_text#:#Texte à trous +qsts#:#cloze_textgapcase_insensitive#:#Ne pas distinguer majuscules et minuscules +qsts#:#cloze_textgapcase_sensitive#:#Distinguer majuscules et minuscules +qsts#:#cloze_textgaplevenshtein_of#:#Distance Levenshtein de %s +qsts#:#confirm_delete_questions#:#Etes-vous sûr(e) de vouloir supprimer la(les) question(s) suivante(s) ? Si vous détruisez des questions verrouillées, les résultats de tous les tests contenant une question verrouillée seront détruits également. +qsts#:#create_question#:#Créer Question +qsts#:#gap#:#Trou +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insérer Ecart +qsts#:#min_auto_complete#:#Saisie automatique +qsts#:#msg_no_questions_selected#:#No questions were selected.###31 10 2023 new variable +qsts#:#out_of_range#:#Hors de portée +qsts#:#qst_lifecycle#:#Lifecycle +qsts#:#qst_lifecycle_draft#:#Draft +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles +qsts#:#qst_lifecycle_final#:#Final +qsts#:#qst_lifecycle_outdated#:#Outdated +qsts#:#qst_lifecycle_rejected#:#Rejected +qsts#:#qst_lifecycle_review#:#To be Reviewed +qsts#:#qst_lifecycle_sharable#:#Sharable +qsts#:#questionlist#:#Questionlist###31 10 2023 new variable +qsts#:#questions#:#Questions###31 10 2023 new variable +qsts#:#range_lower_limit#:#Limite Inférieure +qsts#:#range_upper_limit#:#Limite Supérieure +qsts#:#reset_preview#:#Réinitialiser l'aperàçu +qsts#:#select_gap#:#Selectionner trou +qsts#:#shuffle_answers#:#Mélanger les Réponses +qsts#:#suggested_learning_content#:#Ajouter solution suggérée rating#:#rat_not_rated_yet#:#Pas Encore Noté rating#:#rat_nr_ratings#:#%s Notations rating#:#rat_one_rating#:#Une Notation @@ -16605,7 +16605,6 @@ survey#:#questionblock#:#Bloc de Questions survey#:#questionblock_inserted#:#Bloc de question ajouté survey#:#questionblocks#:#Blocs de Questions survey#:#questionblocks_inserted#:#Bloc de questions ajouté -survey#:#questions#:#Questions survey#:#questions_inserted#:#Question(s) ajoutée(s) survey#:#questions_removed#:#Question(s) et/ou bloc(s) de questions supprimé(es) ! survey#:#questiontype#:#Type de Question @@ -16906,6 +16905,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Masquer les vignettes survey#:#svy_print_show_labels#:#Montrer les vignettes survey#:#svy_privacy_info#:#Privacy###31 10 2023 new variable +survey#:#svy_questions#:#Questions survey#:#svy_rater#:#Rater###31 10 2023 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###31 10 2023 new variable survey#:#svy_reminder_mail_template#:#Modèle d’e-mail diff --git a/lang/ilias_hr.lang b/lang/ilias_hr.lang index 77f56a71ffb7..29bd1184a5b7 100644 --- a/lang/ilias_hr.lang +++ b/lang/ilias_hr.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#Aktivirajte protokoliranje testa i procjene assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Sadržaji za ponavljanje assessment#:#add_answers#:#Dodaj odgovore assessment#:#add_circle#:#Dodaj krug assessment#:#add_gap#:#Dodaj tekst za praznine @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Dobili ste bodove za svoje rje assessment#:#answer_is_right#:#Vaše rješenje je ispravno. assessment#:#answer_is_wrong#:#Vaše rješenje je pogrešno. assessment#:#answer_of#:#Answer of###04 06 2021 new variable -assessment#:#answer_options#:#Opcija odgovora: assessment#:#answer_question#:#Odgovori na pitanje assessment#:#answer_text#:#Tekst odgovora assessment#:#answer_types#:#Urednik odgovora @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Polaganje dostavom assessment#:#ass_completion_by_submission_info#:#Ako je to aktivirano, dostava datoteke s rješenjem dovodi do dodjele maksimalnog broja bodova za ovo pitanje. Procjena se u svakom trenutku može prilagoditi ručno. Promjena ove postavke nema nikakvih naknadnih učinaka na već dostavljena rješenja. assessment#:#ass_create_export_file_with_results#:#Izradi datoteku za izvoz (uklj. rezultate sudionika) assessment#:#ass_create_export_test_archive#:#Izradi datoteku arhive za test -assessment#:#ass_create_question#:#Izradi pitanje assessment#:#ass_imap_hint#:#Napomena (prikazuje kao tooltip) assessment#:#ass_imap_map_file_not_readable#:#Učitani se Imagemap ne može pročitati. assessment#:#ass_imap_no_map_found#:#U učitanom Imapemap-u nije pronađen nijedan oblik koji se podržava. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Duljina tekstualnog polja assessment#:#cloze_fixed_textlength_description#:#Ako ovdje unesete vrijednost, generiraju se praznine u tekstu koje ne definiraju vlastitu vrijednost za maksimalnu duljinu kao i numeričke praznine te duljine, tako da nije moguće upisati veći broj znakova od dopuštenog. Kod numeričkih praznina također treba voditi računa da se decimalni separator također računa kao znak. assessment#:#cloze_gap_size_info#:#Ako je upisana vrijednost veća od 0, to se tekstualno polje s prazninom generira s ovdje navedenom duljinom. Ako nije upisana vrijednost, to se tekstualno polje s prazninom generira s globalno određenom duljinom polja za tekst. -assessment#:#cloze_text#:#Pitanje s prazninom u tekstu -assessment#:#cloze_textgap_case_insensitive#:#Neosjetljivo na velika i mala slova -assessment#:#cloze_textgap_case_sensitive#:#Osjetljivo na velika i mala slova -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein razmak od%s assessment#:#code#:#Kod assessment#:#codebase#:#Kodna osnova assessment#:#concatenation#:#Povezivanje @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Možete unijeti unaprijed definirane varijable ($ assessment#:#fq_no_restriction_info#:#Kao unos prihvaćeni su kako decimale tako i razlomci. assessment#:#fq_precision_info#:#Unesite broj željenih decimalnih mjesta. assessment#:#fq_question_desc#:#Varijable možete definirati tako da umetnete $v1, $v2 ... $ VN, a rezultate da umetnete $r1, $r2 .... $rn na željeno mjesto u tekstu pitanja. Kliknite na uklopnu površinu „Analiziraj Pitanje” kako bi se generirali obrasci za obradu varijabli i rezultata. -assessment#:#gap#:#Praznina assessment#:#gap_combination#:#Kombinacija praznina -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Pojam iz pojmovnika assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Vaša ocjena je. "[ocjena]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Pitanje već sadrži slike. Ne možete pr assessment#:#info_text_upload#:#Odaberite datoteku s tekstom odgovora (UTF-8) kako biste je učitali. assessment#:#insert_after#:#Umetni iza assessment#:#insert_before#:#Umetni ispred -assessment#:#insert_gap#:#Umetni prazninu assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Unutarnje poveznice assessment#:#intprecision#:#Djeljivo s @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Sudionik je unio pogrešnu lozi assessment#:#longmenu#:#Dugi izbornik assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Tekst dugog izbornika -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Održavanje assessment#:#manscoring#:#Ručno bodovanje assessment#:#manscoring_done#:#Sudionici koji su već bodovani @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Dosegli ste maksimalan broj prolaza z assessment#:#maximum_points#:#Maksimalno raspoloživi broj bodova assessment#:#maxsize#:#Maksimalna veličina datoteke za učitavanje assessment#:#maxsize_info#:#Navedite u bajtovima maksimalnu veličinu koju bi smjela imati datoteka namijenjena za učitavanje. Ako ovo polje ostavite prazno, koristit će se umjesto toga maksimalna veličina osnovnog sustava. -assessment#:#min_auto_complete#:#Automatsko dopunjavanje assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Morate odrediti minimalni postotak od 0 posto! Shema ocjena nije spremljena. assessment#:#misc#:#Razne opcije @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Dodan je krug -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Broj pojmova mora biti veći ili jednak broju definicija. assessment#:#msg_poly_added#:#Dodan je poligon assessment#:#msg_questions_moved#:#Pitanje(a) je(su) pomaknuto(a) @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Slijed odgovora koji ovdje definirate uzet će se kao ispravan redoslijed rješenja. assessment#:#ordertext#:#Tekst za raspoređivanje assessment#:#ordertext_info#:#Unesite tekst koji treba rasporediti vodoravno. Tekst koji treba rasporediti bit će odvojen razmaknicama u tekstu. Ako trebate drugačiji odvajanje, za razdvajanje tekstualnih jedinica umjesto razmaknica možete koristiti separator%s. -assessment#:#out_of_range#:#Izvan raspona assessment#:#output#:#Izdavanje assessment#:#output_mode#:#Način izdavanja assessment#:#parseQuestion#:#Analiziraj pitanje @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Otkaži -assessment#:#qpl_confirm_delete_questions#:#Jeste li sigurni da želite ukloniti sljedeća pitanja? assessment#:#qpl_copy_insert_clipboard#:#Odabrano(a) pitanje(a) se kopiraju u međuspremnik assessment#:#qpl_copy_select_none#:#Molimo odaberite najmanje jedno pitanje za kopiranje u međuspremnik! assessment#:#qpl_delete_rbac_error#:#Nemate prava za uklanjanje ovog pitanja! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetencija assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Ukupan broj kompetencijskih bodova po kompetenciji assessment#:#qpl_question_is_in_use#:#Pitanje koje želite urediti postoji već u u %s testova. Ako promijenite ovo pitanje, to NEĆE imati nikakav utjecaj na pitanja koja su već sadržana u testovima, jer sustav automatski izrađuje kopiju pitanja kada se umetne u test! assessment#:#qpl_questions_deleted#:#Pitanje(a) izbrisano(a). -assessment#:#qpl_reset_preview#:#Resetiraj pregled assessment#:#qpl_save_skill_assigns_update#:#Spremi dodjelu kompetencija assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Postojeće taksonomije u ovom bazenu mogu se koristiti za filtriranje pitanja. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###04 06 2021 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###04 06 2021 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###04 06 2021 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###04 06 2021 new variable -assessment#:#qst_lifecycle#:#Lifecycle###04 06 2021 new variable -assessment#:#qst_lifecycle_draft#:#Draft###04 06 2021 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###04 06 2021 new variable -assessment#:#qst_lifecycle_final#:#Final###04 06 2021 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###04 06 2021 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###04 06 2021 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###04 06 2021 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###04 06 2021 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Broj pokušaja @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Naziv pitanja assessment#:#question_type#:#Tip pitanja assessment#:#questionpool_not_entered#:#Unesite naziv za bazen pitanja! assessment#:#questionpool_not_selected#:#Please select a question pool!###04 06 2021 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#Pitanja iz assessment#:#questions_per_page_view#:#Prikaz stranice assessment#:#random_accept_sample#:#Prihvati uzorak assessment#:#random_another_sample#:#Uzmite drugi uzorak assessment#:#random_selection#:#Slučajni odabir assessment#:#range#:#Raspon -assessment#:#range_lower_limit#:#Donja granica assessment#:#range_max#:#Raspon (maksimalni) assessment#:#range_min#:#Raspon (minimalni) -assessment#:#range_upper_limit#:#Gornja granica assessment#:#rated_sign#:#Predznak assessment#:#rated_unit#:#Jedinica assessment#:#rated_value#:#Vrijednost @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Pretraži po ulogama assessment#:#search_term#:#Pojam za pretraživanje assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Odaberi prazninu assessment#:#select_max_one_item#:#Molimo odaberite samo jedan objekt assessment#:#select_one_user#:#Molimo odaberite najmanje jednog korisnika. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Prikaži pregled rezultata ocijenjenog prolaza testa assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Pokaži ocijenjene odgovore korisnika -assessment#:#shuffle_answers#:#Pomiješaj odgovore assessment#:#skip_question#:#Nemoj odgovoriti i dalje assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Tekst @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###04 06 2021 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###04 06 2021 new variable qpl#:#qpl_page_type_qht#:#Hint###04 06 2021 new variable qpl#:#qpl_page_type_qpl#:#Question Page###04 06 2021 new variable +qsts#:#answer_options#:#Opcija odgovora +qsts#:#cloze_text#:#Pitanje s prazninom u tekstu +qsts#:#cloze_textgapcase_insensitive#:#Neosjetljivo na velika i mala slova +qsts#:#cloze_textgapcase_sensitive#:#Osjetljivo na velika i mala slova +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein razmak od%s +qsts#:#confirm_delete_questions#:#Jeste li sigurni da želite ukloniti sljedeća pitanja? +qsts#:#create_question#:#Izradi pitanje +qsts#:#gap#:#Praznina +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Umetni prazninu +qsts#:#min_auto_complete#:#Automatsko dopunjavanje +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Izvan raspona +qsts#:#qst_lifecycle#:#Lifecycle###04 06 2021 new variable +qsts#:#qst_lifecycle_draft#:#Draft###04 06 2021 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###04 06 2021 new variable +qsts#:#qst_lifecycle_final#:#Final###04 06 2021 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###04 06 2021 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###04 06 2021 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###04 06 2021 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###04 06 2021 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Donja granica +qsts#:#range_upper_limit#:#Gornja granica +qsts#:#reset_preview#:#Resetiraj pregled +qsts#:#select_gap#:#Odaberi prazninu +qsts#:#shuffle_answers#:#Pomiješaj odgovore +qsts#:#suggested_learning_content#:#Sadržaji za ponavljanje rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok pitanja survey#:#questionblock_inserted#:#Blok pitanja je umetnut survey#:#questionblocks#:#Blokovi pitanja survey#:#questionblocks_inserted#:#Blokovi pitanja su umetnuti -survey#:#questions#:#Pitanja survey#:#questions_inserted#:#Pitanje(a) je(su) umetnuto(a)! survey#:#questions_removed#:#Pitanje(a) i/ili blok(ovi) pitanja je(su) uklonjeno(i)! survey#:#questiontype#:#Tip pitanja @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Sakrij oznake survey#:#svy_print_show_labels#:#Pokaži oznake survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Pitanja survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Predložak pošte diff --git a/lang/ilias_hu.lang b/lang/ilias_hu.lang index f1271ccc3d4a..b6c765634413 100644 --- a/lang/ilias_hu.lang +++ b/lang/ilias_hu.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#TinyMCE engedélyezése WYSIWYG szerkesztéshez assessment#:#activate_logging#:#Teszt és értékelés naplózásának bekapcsolása assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Ismétlő összefoglaláshoz tartalom hozzáadása assessment#:#add_answers#:#Válasz hozzáadása assessment#:#add_circle#:#Kör alakú terület hozzáadása assessment#:#add_gap#:#Kiegészítendő szöveg hozzáadása @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Szerzett pontot a megoldásár assessment#:#answer_is_right#:#A megoldása helyes assessment#:#answer_is_wrong#:#A megoldása rossz assessment#:#answer_of#:#Válasz -assessment#:#answer_options#:#Válaszlehetőségek: assessment#:#answer_question#:#Kérdés megválaszolása assessment#:#answer_text#:#Válaszszöveg assessment#:#answer_types#:#Válaszokhoz szerkesztő @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Befejezve elküldéssel assessment#:#ass_completion_by_submission_info#:#Ha be van kapcsolva, legalább egy fájl elküldése ennek a kérdésnek a teljesítését jelenti a maximális pontszám megszerzésével. A pont a későbbiekben manuálisan módosítható. Ennek a beállításnak a bekapcsolása nincs hatással a már beküldött megoldásokra. assessment#:#ass_create_export_file_with_results#:#Export fájl létrehozása (résztvevők eredményeivel) assessment#:#ass_create_export_test_archive#:#Tesztarchívum fájl létrehozása -assessment#:#ass_create_question#:#Kérdés létrehozása assessment#:#ass_imap_hint#:#Tippek buboréksúgóként jelenjenek meg assessment#:#ass_imap_map_file_not_readable#:#A feltöltött képtérkép nem olvasható. assessment#:#ass_imap_no_map_found#:#Egy űrlap sem található a feltöltött képtérképben. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Szövegmező hossza assessment#:#cloze_fixed_textlength_description#:#Ha megad egy értéket, akkor az összes szöveg- és szám mező evvel a rögzített hosszal kerül létrehozásra, így nincs lehetőség a több karakter bevitelére. Szövegmezőknek lehet saját korlátjuk a karakterszámra. Számmezők esetén a tizedesvessző is egy karakternek számít. assessment#:#cloze_gap_size_info#:#Ha 0-nál nagyobb értéked ad meg, ilyen hosszúságú lesz a szövegmező. Amennyiben nem ad meg értéket, a globális hossz lesz ez az érték. -assessment#:#cloze_text#:#Kiegészítendő szöveg -assessment#:#cloze_textgap_case_insensitive#:#Kis-/nagybetűre nem érzékeny -assessment#:#cloze_textgap_case_sensitive#:#Kis-/nagybetűre érzékeny -assessment#:#cloze_textgap_levenshtein_of#:#'%s' Levenshtein-távolsága assessment#:#code#:#Kód assessment#:#codebase#:#Kódbázis assessment#:#concatenation#:#Szavak összekapcsolása @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Beírhat megadott változókat ($v1, $v2, ..., $v assessment#:#fq_no_restriction_info#:#Tizedestört és tört egyaránt megadható. assessment#:#fq_precision_info#:#Adja meg a tizedeshelyek számát. assessment#:#fq_question_desc#:#Definiálhat változókat $v1, $v2 ... $vn beírásával, végeredményeket $r1, $r2 .... $rn beírásával a kérdés szövegének tetszőleges helyén. Kattints a 'Kérdés elemzése' gombra, hogy a változók és a végeredmények szerkeszthető űrlapja megjelenjen. -assessment#:#gap#:#Kitöltendő hely assessment#:#gap_combination#:#Kitöltéskombináció -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Fogalomtár fogalma assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Érdemjegye: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#A kérdés képet tartalmaz. Nem változt assessment#:#info_text_upload#:#Válasszon egy UTF-8 kódolású válaszfájlt a feltöltéshez assessment#:#insert_after#:#Beszúrás utána assessment#:#insert_before#:#Beszúrás elé -assessment#:#insert_gap#:#Kitöltendő hely beszúrása assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Belső linkek assessment#:#intprecision#:#Osztható a következővel @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#A résztvevő rossz jelszót ad assessment#:#longmenu#:#Étlap assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#'Étlap'-szöveg -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Karbantartás assessment#:#manscoring#:#Manuális pontozás assessment#:#manscoring_done#:#Pontozott résztvevők @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Elfogyott az összes próbálkozási assessment#:#maximum_points#:#Maximálisan elérhető pont assessment#:#maxsize#:#Feltölthető maximális fájlméret assessment#:#maxsize_info#:#Adja meg bájtban a feltölthető fájlméretet. Ha üresen hagyja ezt a mezőt, a rendszerben beállított maximális méret kerül használatra. -assessment#:#min_auto_complete#:#Automatikus kiegészítés assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Meg kell határoznia a 0 százalék minimális pontértékét. Az érdemjegyképzést nem mentettük. assessment#:#misc#:#Egyéb beállítások @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Kör hozzáadva -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#A kifejezések száma nagyobb vagy egyenlő legyen, mint a definíciók száma. assessment#:#msg_poly_added#:#Sokszög hozzáadva assessment#:#msg_questions_moved#:#Kérdés(ek) áthelyezve. @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Az itt megadott válaszsorozat a helyes megoldások sorozataként kerül felhasználásra. assessment#:#ordertext#:#Sorrendbe rakó szöveg assessment#:#ordertext_info#:#Adja meg a vízszintesen sorba rakandó szöveget. A rendezendő szövegegységek nem látható (fehér) karakterekkel lesznek elválasztva. Ha más elválasztójelre van szüksége, használhatja a %s elválasztójelet szövegegységeinek elkülönítésére. -assessment#:#out_of_range#:#Tartományon kívül esik assessment#:#output#:#Kimenet (output) assessment#:#output_mode#:#Kimeneti mód assessment#:#parseQuestion#:#Kérdés elemzése @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Mégsem -assessment#:#qpl_confirm_delete_questions#:#Biztos, hogy eltávolítja az alábbi kérdés(eke)t? assessment#:#qpl_copy_insert_clipboard#:#A kijelölt kérdés(ek)et a vágólapra helyezte. assessment#:#qpl_copy_select_none#:#Jelöljön ki legalább egy kérdést a másoláshoz! assessment#:#qpl_delete_rbac_error#:#Nincs joga a kérdés(ek) eltávolításához. @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetencia assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Kompetencia-pontszámok összege kérdésenként assessment#:#qpl_question_is_in_use#:#A módosítandó kérdés %s tesztben szerepel. Ha megváltoztatja a kérdést, az a teszt(ek)ben nem fog módosulni, mert a rendszer másolatot hoz létre a kérdésekről, mielőtt beilleszti azokat a teszt(ek)be. assessment#:#qpl_questions_deleted#:#Kérdés(eke)et sikeresen eltávolított. -assessment#:#qpl_reset_preview#:#Előnézet alaphelyzetbe állítása assessment#:#qpl_save_skill_assigns_update#:#Kompetencia-összerendelése mentése assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#A már létező taxonómiák a kérdések szűrésére használhatóak ebben a gyűjteményben. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Fennmaradó karakterek száma: assessment#:#qst_essay_wordcounter_enabled#:#Szavak számolása assessment#:#qst_essay_wordcounter_enabled_info#:#A beírt szavak számát számoljuk és megjelenítjük a felhasználóknak a bevitelő mező alatt . assessment#:#qst_essay_written_words#:#Szavak megadott száma: -assessment#:#qst_lifecycle#:#Életciklus -assessment#:#qst_lifecycle_draft#:#Piszkozat -assessment#:#qst_lifecycle_filter_all#:#Összes életciklus -assessment#:#qst_lifecycle_final#:#Végső -assessment#:#qst_lifecycle_outdated#:#Elavult -assessment#:#qst_lifecycle_rejected#:#Elutasított -assessment#:#qst_lifecycle_review#:#Átnézendő -assessment#:#qst_lifecycle_sharable#:#Megosztható assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Próbálkozások száma @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Kérdéscím assessment#:#question_type#:#Kérdéstípus assessment#:#questionpool_not_entered#:#Adjon nevet a kérdésgyűjteménynek. assessment#:#questionpool_not_selected#:#Kérem, válasszon ki egy kérdésgyűjteményt. -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#kérdések innen: assessment#:#questions_per_page_view#:#Lapnézet assessment#:#random_accept_sample#:#Minta elfogadása assessment#:#random_another_sample#:#Másik minta kérése assessment#:#random_selection#:#Véletlen-kiválasztás assessment#:#range#:#Terület -assessment#:#range_lower_limit#:#Alsó határ assessment#:#range_max#:#Tartomány (Maximum) assessment#:#range_min#:#Tartomány (Minimum) -assessment#:#range_upper_limit#:#Felső határ assessment#:#rated_sign#:#Jel assessment#:#rated_unit#:#Mértékegység assessment#:#rated_value#:#Érték @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Talált szerepek assessment#:#search_term#:#Fogalom keresése assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Szöveghely választása assessment#:#select_max_one_item#:#Csak egy elemet válasszon! assessment#:#select_one_user#:#Válasszon ki legalább egy felhasználót! assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Figyelembe vett tesztkitöltés áttekintése assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Figyelembe vett tesztkitöltés válaszainak megjelenítése -assessment#:#shuffle_answers#:#Kevert válaszok assessment#:#skip_question#:#Válaszadás kihagyása és következő assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Szöveg @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#Általános visszajelzés qpl#:#qpl_page_type_qfbs#:#Speciális visszajelzés qpl#:#qpl_page_type_qht#:#Tipp qpl#:#qpl_page_type_qpl#:#Kérdésoldala +qsts#:#answer_options#:#Válaszlehetőségek +qsts#:#cloze_text#:#Kiegészítendő szöveg +qsts#:#cloze_textgapcase_insensitive#:#Kis-/nagybetűre nem érzékeny +qsts#:#cloze_textgapcase_sensitive#:#Kis-/nagybetűre érzékeny +qsts#:#cloze_textgaplevenshtein_of#:#'%s' Levenshtein-távolsága +qsts#:#confirm_delete_questions#:#Biztos, hogy eltávolítja az alábbi kérdés(eke)t? +qsts#:#create_question#:#Kérdés létrehozása +qsts#:#gap#:#Kitöltendő hely +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Kitöltendő hely beszúrása +qsts#:#min_auto_complete#:#Automatikus kiegészítés +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Tartományon kívül esik +qsts#:#qst_lifecycle#:#Életciklus +qsts#:#qst_lifecycle_draft#:#Piszkozat +qsts#:#qst_lifecycle_filter_all#:#Összes életciklus +qsts#:#qst_lifecycle_final#:#Végső +qsts#:#qst_lifecycle_outdated#:#Elavult +qsts#:#qst_lifecycle_rejected#:#Elutasított +qsts#:#qst_lifecycle_review#:#Átnézendő +qsts#:#qst_lifecycle_sharable#:#Megosztható +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Alsó határ +qsts#:#range_upper_limit#:#Felső határ +qsts#:#reset_preview#:#Előnézet alaphelyzetbe állítása +qsts#:#select_gap#:#Szöveghely választása +qsts#:#shuffle_answers#:#Kevert válaszok +qsts#:#suggested_learning_content#:#Ismétlő összefoglaláshoz tartalom hozzáadása rating#:#rat_not_rated_yet#:#Még nincs értékelve rating#:#rat_nr_ratings#:#%s értékelés rating#:#rat_one_rating#:#Egy értékelés @@ -16621,7 +16621,6 @@ survey#:#questionblock#:#Kérdésblokk survey#:#questionblock_inserted#:#Kérdésblokk beszúrva survey#:#questionblocks#:#Kérdésblokkok survey#:#questionblocks_inserted#:#Kérdésblokkok beszúrva -survey#:#questions#:#Kérdések survey#:#questions_inserted#:#Kérdés(ek) beszúrva. survey#:#questions_removed#:#Kérdés(ek)/kérdésblokk(ok) eltávolítva. survey#:#questiontype#:#Kérdéstípus @@ -16922,6 +16921,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Címkék elrejtése survey#:#svy_print_show_labels#:#Címkék megjelenítése survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Kérdések survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Levélsablon diff --git a/lang/ilias_it.lang b/lang/ilias_it.lang index ee903ef4af9d..3f5a718d345c 100644 --- a/lang/ilias_it.lang +++ b/lang/ilias_it.lang @@ -421,7 +421,6 @@ adve#:#adve_use_tiny_mce#:#Abilita TinyMCE come Editor WYSIWYG (What You See Is assessment#:#activate_logging#:#Activate Test and Assessment Logging###28 11 2025 new variable assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 11 2025 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 11 2025 new variable -assessment#:#addSuggestedSolution#:#Contenuti per il ripasso assessment#:#add_answers#:#Aggiungi risposte assessment#:#add_circle#:#Aggiungi cerchio assessment#:#add_gap#:#Aggiungi lacuna di testo @@ -446,7 +445,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Hai ricevuto dei punti per la assessment#:#answer_is_right#:#La soluzione è corretta. assessment#:#answer_is_wrong#:#La soluzione è sbagliata. assessment#:#answer_of#:#Risposta di -assessment#:#answer_options#:#Opzioni di risposte: assessment#:#answer_question#:#Risposta domanda assessment#:#answer_text#:#Testo della risposta assessment#:#answer_types#:#Tipi di risposta @@ -500,7 +498,6 @@ assessment#:#ass_completion_by_submission#:#Completato da presentazione assessment#:#ass_completion_by_submission_info#:#Se abilitato, la presentazione di almeno un file causa il completamento di questa domanda garantendo il punteggio massimo per questa domanda. Il punteggio può essere modificato manualmente in seguito. In cambiamento di questa impostazione non effetto sulle domande già presentate. assessment#:#ass_create_export_file_with_results#:#Crea file di esportazione dei test (inclusi i risultati dei partecipanti) assessment#:#ass_create_export_test_archive#:#Crea file archivio dei test -assessment#:#ass_create_question#:#Crea domanda assessment#:#ass_imap_hint#:#Il suggerimento deve essere mostrato come Descrizione comando assessment#:#ass_imap_map_file_not_readable#:#L’immagine caricata della mappa non può essere letta. assessment#:#ass_imap_no_map_found#:#Non è possibile trovare alcun modello nell’immagine della mappa caricata. @@ -570,10 +567,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Lunghezza del campo di testo assessment#:#cloze_fixed_textlength_description#:#Se si inserisce un valore maggiore di 0 tutte le mancanze di testo e di numero avranno quella lunghezza. assessment#:#cloze_gap_size_info#:#Se si immette un valore maggiore di 0, questo campo di testo spazio verrà creato con la lunghezza fissa di questo valore. Se non si immette un valore, il campo di testo del gap verrà creato con il valore della lunghezza fissa globale. -assessment#:#cloze_text#:#Testo corrispondente -assessment#:#cloze_textgap_case_insensitive#:#No Maiuscole/minuscole -assessment#:#cloze_textgap_case_sensitive#:#Maiuscole/minuscole -assessment#:#cloze_textgap_levenshtein_of#:#Distanza di Levenshtein del %s assessment#:#code#:#Codice assessment#:#codebase#:#Base dei codici assessment#:#concatenation#:#Concatenazione @@ -756,9 +749,7 @@ assessment#:#fq_formula_desc#:#È possibile inserire variabili predefinite (da $ assessment#:#fq_no_restriction_info#:#Entrambi i decimali e le frazioni sono accettati come input. assessment#:#fq_precision_info#:#Digita il numero delle posizioni decimali desiderate. assessment#:#fq_question_desc#:#È possibile definire le variabili inserendo $ v1, $ v2 ... $ vn, i risultati inserendo $ r1, $ r2 .... $ rn nella posizione desiderata nel testo della domanda. Fare clic sul pulsante "Analizza domanda" per creare moduli di modifica per variabili e risultati. -assessment#:#gap#:#Spazio vuoto assessment#:#gap_combination#:#Combinazione di Gap -assessment#:#gaps#:#Gaps###28 11 2025 new variable assessment#:#glossary_term#:#Voce del glossario assessment#:#goto_first_question#:#Show first question###28 11 2025 new variable assessment#:#grading_mark_msg#:#Il tuo punteggio risultante è: "[mark]" @@ -776,7 +767,6 @@ assessment#:#info_answer_type_change#:#La domanda contiene già delle immagini. assessment#:#info_text_upload#:#Scegli un file di risposte da caricare assessment#:#insert_after#:#Inserisci dopo assessment#:#insert_before#:#Inserisci prima -assessment#:#insert_gap#:#Inserisci Gap assessment#:#interaction_type#:#Interaction Type###28 11 2025 new variable assessment#:#internal_links#:#Link interni assessment#:#intprecision#:#Divisibile per @@ -888,7 +878,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Menù esteso assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###28 07 2023 new variable assessment#:#longmenu_text#:#Testo menu esteso -assessment#:#mainbar_button_label_questionlist#:#Questionlist###30 04 2024 new variable assessment#:#maintenance#:#Manutenzione assessment#:#manscoring#:#Punteggio manuale assessment#:#manscoring_done#:#Partecipanti Valutati @@ -915,7 +904,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Hai raggiunto il numero massimo di pa assessment#:#maximum_points#:#Punteggio massimo disponibile assessment#:#maxsize#:#massima dimensione file di upload assessment#:#maxsize_info#:#Inserire la massima dimensione in, bytes, dei files da caricare. Se il campo è lasciato in bianco, la dimensione sarà definita dal sistema. -assessment#:#min_auto_complete#:#Completamento automatico assessment#:#min_ip_label#:#Lowest IP With Access###28 11 2025 new variable assessment#:#min_percentage_ne_0#:#Devi raggiungere la percentuale minima dello 0 percento! Lo schema voti non e' stato salvato. assessment#:#misc#:#Opzioni varie @@ -924,7 +912,6 @@ assessment#:#mode_onebyone#:#One by One###28 11 2025 new variable assessment#:#mode_question#:#Question oriented###28 11 2025 new variable assessment#:#mode_user#:#Participant oriented###28 11 2025 new variable assessment#:#msg_circle_added#:#Circonferenza aggiunta -assessment#:#msg_no_questions_selected#:#No questions were selected.###30 04 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Il numero dei termini deve essere maggiore o uguale al numero delle definizioni assessment#:#msg_poly_added#:#Aggiunto poligono assessment#:#msg_questions_moved#:#Domanda(e) spostata(e) @@ -983,7 +970,6 @@ assessment#:#order#:#Order###09 11 2022 new variable assessment#:#ordering_answer_sequence_info#:#La sequenza di risposte che definisci qui sara' presa come sequenza di risposte corrette. assessment#:#ordertext#:#Ordinando il testo assessment#:#ordertext_info#:#Per favore, inserisci il testo che dovrebbe essere ordinato orizzontalmente. Il testo ordinato sarà separato da spazi vuoti. Se c'e' bisogno di una differente spaziatura, si può usare il separatore %s per separare il testo.
-assessment#:#out_of_range#:#Fuori portata assessment#:#output#:#Uscita assessment#:#output_mode#:#Modalità di output assessment#:#parseQuestion#:#Analizza domanda @@ -1052,7 +1038,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 11 2025 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 11 2025 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 11 2025 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Annulla -assessment#:#qpl_confirm_delete_questions#:#Sei sicuro di voler eliminare le seguenti domande? Se cancelli le domande bloccate, anche i risultati dei test che le contengono verranno cancellati. assessment#:#qpl_copy_insert_clipboard#:#Le domande selezionate sono state copiate negli appunti assessment#:#qpl_copy_select_none#:#Seleziona almeno una domanda da copiare negli appunti assessment#:#qpl_delete_rbac_error#:#Non hai l'autorizzazione per eliminare questa raccolta di domande! @@ -1105,7 +1090,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#competenze assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Somma totale di punti di competenze per competenze assessment#:#qpl_question_is_in_use#:#La domanda che stai per modificare è inserita in un %s dei test. Se modifichi la domanda, NON verranno modificate le domande nei test, perchè il sistema crea una copia delle domande quando queste vengono inserite in un test! assessment#:#qpl_questions_deleted#:#Domande eliminate -assessment#:#qpl_reset_preview#:#Ripristina anteprima assessment#:#qpl_save_skill_assigns_update#:#Salva assegnazioni della competenze assessment#:#qpl_settings_availability#:#Availability###30 04 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Se abilitato, le tassonomie eventualmente create vengono mostrate per il filtro. @@ -1135,14 +1119,6 @@ assessment#:#qst_essay_chars_remaining#:#Caratteri rimanenti: assessment#:#qst_essay_wordcounter_enabled#:#Conta parole assessment#:#qst_essay_wordcounter_enabled_info#:#Le parole inserite vengono contate. Il numero di parole scritte viene mostrato ai partecipanti sotto il campo di immissione del testo. assessment#:#qst_essay_written_words#:#Numero di parole inserite: -assessment#:#qst_lifecycle#:#Ciclo vitale -assessment#:#qst_lifecycle_draft#:#Bozza -assessment#:#qst_lifecycle_filter_all#:#Tutti i cicli di vita -assessment#:#qst_lifecycle_final#:#Finale -assessment#:#qst_lifecycle_outdated#:#Obsoleto -assessment#:#qst_lifecycle_rejected#:#Respinto -assessment#:#qst_lifecycle_review#:#Da rivedere -assessment#:#qst_lifecycle_sharable#:#Condivisibile assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Numero di tentativi @@ -1166,17 +1142,14 @@ assessment#:#question_title#:#Titolo della domanda assessment#:#question_type#:#Tipo di domanda assessment#:#questionpool_not_entered#:#Per favore inserisci il nome della raccolta di domande! assessment#:#questionpool_not_selected#:#Please select a question pool! -assessment#:#questions#:#Questions###30 04 2024 new variable assessment#:#questions_from#:#domande da assessment#:#questions_per_page_view#:#Visualizza pagina assessment#:#random_accept_sample#:#Accetta esempio assessment#:#random_another_sample#:#Prendi un altro esempio assessment#:#random_selection#:#Scegli casualmente assessment#:#range#:#Intervallo -assessment#:#range_lower_limit#:#Limite inferiore assessment#:#range_max#:#Portata (Massima) assessment#:#range_min#:#Portata (Minima) -assessment#:#range_upper_limit#:#Limite superiore assessment#:#rated_sign#:#Segno assessment#:#rated_unit#:#Unità assessment#:#rated_value#:#Valore @@ -1252,7 +1225,6 @@ assessment#:#search_roles#:#Ricerca ruoli assessment#:#search_term#:#Ricerca termini assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###30 04 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###28 11 2025 new variable -assessment#:#select_gap#:#Seleziona spazio assessment#:#select_max_one_item#:#Per favore scegli un solo elemento assessment#:#select_one_user#:#Seleziona almeno un utente assessment#:#select_question#:#Select a Question###28 11 2025 new variable @@ -1279,7 +1251,6 @@ assessment#:#show_old_introduction#:#Show old introduction###30 04 2024 new vari assessment#:#show_pass_overview#:#Mostra il sommario dei voti assessment#:#show_results#:#Show Results###28 11 2025 new variable assessment#:#show_user_answers#:#Mostra le risposte valutate degli utenti -assessment#:#shuffle_answers#:#Mescola le risposte assessment#:#skip_question#:#Non rispondere e Avanti assessment#:#solution#:#Solution###28 11 2025 new variable assessment#:#solutionText#:#testo @@ -13977,6 +13948,35 @@ qpl#:#qpl_page_type_qfbg#:#Feedback generale qpl#:#qpl_page_type_qfbs#:#Feedback speciali qpl#:#qpl_page_type_qht#:#Suggerimento qpl#:#qpl_page_type_qpl#:#Pagina delle domande +qsts#:#answer_options#:#Opzioni di risposte +qsts#:#cloze_text#:#Testo corrispondente +qsts#:#cloze_textgapcase_insensitive#:#No Maiuscole/minuscole +qsts#:#cloze_textgapcase_sensitive#:#Maiuscole/minuscole +qsts#:#cloze_textgaplevenshtein_of#:#Distanza di Levenshtein del %s +qsts#:#confirm_delete_questions#:#Sei sicuro di voler eliminare le seguenti domande? Se cancelli le domande bloccate, anche i risultati dei test che le contengono verranno cancellati. +qsts#:#create_question#:#Crea domanda +qsts#:#gap#:#Spazio vuoto +qsts#:#gaps#:#Gaps###28 11 2025 new variable +qsts#:#insert_gap#:#Inserisci Gap +qsts#:#min_auto_complete#:#Completamento automatico +qsts#:#msg_no_questions_selected#:#No questions were selected.###30 04 2024 new variable +qsts#:#out_of_range#:#Fuori portata +qsts#:#qst_lifecycle#:#Ciclo vitale +qsts#:#qst_lifecycle_draft#:#Bozza +qsts#:#qst_lifecycle_filter_all#:#Tutti i cicli di vita +qsts#:#qst_lifecycle_final#:#Finale +qsts#:#qst_lifecycle_outdated#:#Obsoleto +qsts#:#qst_lifecycle_rejected#:#Respinto +qsts#:#qst_lifecycle_review#:#Da rivedere +qsts#:#qst_lifecycle_sharable#:#Condivisibile +qsts#:#questionlist#:#Questionlist###30 04 2024 new variable +qsts#:#questions#:#Questions###30 04 2024 new variable +qsts#:#range_lower_limit#:#Limite inferiore +qsts#:#range_upper_limit#:#Limite superiore +qsts#:#reset_preview#:#Ripristina anteprima +qsts#:#select_gap#:#Seleziona spazio +qsts#:#shuffle_answers#:#Mescola le risposte +qsts#:#suggested_learning_content#:#Contenuti per il ripasso rating#:#rat_not_rated_yet#:#Non valutato rating#:#rat_nr_ratings#:#%s Valutazioni rating#:#rat_one_rating#:#Una valutazione @@ -16627,7 +16627,6 @@ survey#:#questionblock#:#Blocco di domande survey#:#questionblock_inserted#:#Blocco di domande inserito survey#:#questionblocks#:#Blocchi di domande survey#:#questionblocks_inserted#:#Blocchi di domande inseriti -survey#:#questions#:#Domande survey#:#questions_inserted#:#Domande inserite! survey#:#questions_removed#:#Domande e/o blocco di domande cancellate! survey#:#questiontype#:#Tipo di domanda @@ -16928,6 +16927,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Nascondi etichette survey#:#svy_print_show_labels#:#Mostra etichette survey#:#svy_privacy_info#:#Privacy###31 03 2023 new variable +survey#:#svy_questions#:#Domande survey#:#svy_rater#:#Rater###31 03 2023 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###31 03 2023 new variable survey#:#svy_reminder_mail_template#:#Modello di mail diff --git a/lang/ilias_ja.lang b/lang/ilias_ja.lang index cac05820e23c..9bae515b8ec3 100644 --- a/lang/ilias_ja.lang +++ b/lang/ilias_ja.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#WYSIWYG編集用にTinyMCEを有効にする assessment#:#activate_logging#:#テストとログをアクティブにする assessment#:#activate_manual_scoring#:#手動並び替えを有効にする assessment#:#activate_manual_scoring_desc#:#全ての問題タイプの手動並び替えを有効にする -assessment#:#addSuggestedSolution#:#要約用のコンテンツを追加 assessment#:#add_answers#:#解答を追加 assessment#:#add_circle#:#円形エリアを追加 assessment#:#add_gap#:#穴埋めテキストを追加 @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#ベストな正解ではあり assessment#:#answer_is_right#:#解答は正解 assessment#:#answer_is_wrong#:#解答は誤り assessment#:#answer_of#:#回答者 -assessment#:#answer_options#:#解答オプション: assessment#:#answer_question#:#問題回答 assessment#:#answer_text#:#答案テキスト assessment#:#answer_types#:#答案タイプ @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#提出すると完了 assessment#:#ass_completion_by_submission_info#:#有効にすると提出ファイルの一つはこの問題の最高点で承認して完了した事になります。点数は後で手動にて修正可能です。この設定の変更は既に提出済みの解答には影響しません。 assessment#:#ass_create_export_file_with_results#:#テストエクスポートファイル(受験者結果を含む)を作成 assessment#:#ass_create_export_test_archive#:#テストアーカイブファイルを作成 -assessment#:#ass_create_question#:#問題を作成 assessment#:#ass_imap_hint#:#ツールチップとしてヒントを表示 assessment#:#ass_imap_map_file_not_readable#:#アップロードされたイメージマップを読み込む事ができませんでした。 assessment#:#ass_imap_no_map_found#:#アップロードされたイメージマップ内に書式が見つかりませんでした。 @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#フォームが保存されると回答テ assessment#:#cloze_fixed_textlength#:#テキスト欄の長さ assessment#:#cloze_fixed_textlength_description#:#0以上の値を入力すると全てのテキストと数値穴埋めテキストフィールドがこの値の固定長で作成されます。 assessment#:#cloze_gap_size_info#:#0以上を入力するとこのギャップテキストフィールドはこの値の固定長で作成されます。値を入力しない場合はギャップテキストフィールドはグローバル固定長で作成されます。 -assessment#:#cloze_text#:#穴埋め問題のテキスト -assessment#:#cloze_textgap_case_insensitive#:#大文字・小文字を非区分 -assessment#:#cloze_textgap_case_sensitive#:#大文字・小文字を区分 -assessment#:#cloze_textgap_levenshtein_of#:#%sのレーベンシュタイン距離 assessment#:#code#:#コード assessment#:#codebase#:#コードベース assessment#:#concatenation#:#結び付け @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#事前定義変数($v1を$vnへ)を(例 $r1)の assessment#:#fq_no_restriction_info#:#10進と分母両方共に入力として承認されます。 assessment#:#fq_precision_info#:#希望する10進位置を入力してください。 assessment#:#fq_question_desc#:#問題文章中の必要な位置で$r1, $r2 .... $rnを挿入する事により$v1, $v2 ... $vnの挿入による変数を定義できます。変数と結果のフォーム編集を作成するには"問題解析"ボタンをクリックします。 -assessment#:#gap#:#穴埋め assessment#:#gap_combination#:#ギャップ連結 -assessment#:#gaps#:#Gaps assessment#:#glossary_term#:#用語集 assessment#:#goto_first_question#:#第1問を表示 assessment#:#grading_mark_msg#:#結果: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#問題にはイメージが既に含ま assessment#:#info_text_upload#:#アップロードする回答テキスト(UTF-8)を選択 assessment#:#insert_after#:#後に挿入 assessment#:#insert_before#:#前に挿入 -assessment#:#insert_gap#:#ギャップを挿入 assessment#:#interaction_type#:#インタラクションタイプ assessment#:#internal_links#:#内部リンク assessment#:#intprecision#:#非表示者 @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#受験者が間違ったテス assessment#:#longmenu#:#ロングメニュー assessment#:#longmenu_answeroptions_differ#:#訂正オプションのテキスト内ギャップ数が同じではないので問題は正しく動作しません。 assessment#:#longmenu_text#:#長いメニューテキスト -assessment#:#mainbar_button_label_questionlist#:#問題リスト assessment#:#maintenance#:#メンテナンス assessment#:#manscoring#:#手動採点 assessment#:#manscoring_done#:#採点済み受講者 @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#このテストの最大受験回数 assessment#:#maximum_points#:#最高得点 assessment#:#maxsize#:#最大アップロードファイルサイズ assessment#:#maxsize_info#:#ファイル アップロードを許可するバイト単位の最大サイズを入力します。このフィールドを空白のままにする場合は、このインストールの最大サイズが代わりに選択されます。 -assessment#:#min_auto_complete#:#オートコンプリート assessment#:#min_ip_label#:#アクセス可能な最低IP assessment#:#min_percentage_ne_0#:#最小0のパーセントを定義する必要があります! 評価尺度は保存されませんでした。 assessment#:#misc#:#その他オプション @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#一つずつ assessment#:#mode_question#:#問題指向 assessment#:#mode_user#:#受験者志向 assessment#:#msg_circle_added#:#円の追加 -assessment#:#msg_no_questions_selected#:#問題が選択されませんでした。 assessment#:#msg_number_of_terms_too_low#:#用語数は定義数と等しいかそれ以上である必要があります。 assessment#:#msg_poly_added#:#多角形の追加 assessment#:#msg_questions_moved#:#問題の移動 @@ -984,7 +971,6 @@ assessment#:#order#:#順番 assessment#:#ordering_answer_sequence_info#:#ここで定義する解答順序は正解として使用されます。 assessment#:#ordertext#:#整列するテキスト assessment#:#ordertext_info#:#水平方向にそろえるテキストを入力してください。整列するテキストはテキストの空白記号で区切られます。別の区切りが必要な場合は、%s の区切り記号を使用してテキストの単位を区切る事ができます。 -assessment#:#out_of_range#:#範囲外 assessment#:#output#:#出力 assessment#:#output_mode#:#出力モード assessment#:#parseQuestion#:#問題解析 @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#追加 assessment#:#qpl_bulk_save_overwrite#:#上書き assessment#:#qpl_bulkedit_success#:#変更を保存しました。 assessment#:#qpl_cancel_skill_assigns_update#:#キャンセル -assessment#:#qpl_confirm_delete_questions#:#本当に次の問題を削除しますか? assessment#:#qpl_copy_insert_clipboard#:#選択した問題をクリップボードへコピーします assessment#:#qpl_copy_select_none#:#クリップボードへコピーする問題を最低1個チェックしてください assessment#:#qpl_delete_rbac_error#:#この問題の削除権がありません! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#能力 assessment#:#qpl_qst_skl_usg_sklpnt_col#:#能力当たりの能力-ポイントの総合計 assessment#:#qpl_question_is_in_use#:#テスト%s 内にある修正しようとしている問題です。 この問題を修正しても、テストへ挿入の時点でシステムが問題のコピーを作成するためテストの問題は修正されません! assessment#:#qpl_questions_deleted#:#問題は削除されました。 -assessment#:#qpl_reset_preview#:#プレビューをリセット assessment#:#qpl_save_skill_assigns_update#:#能力割り当てを保存 assessment#:#qpl_settings_availability#:#Availability(空き状況) assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#有効にすると作成可能性のある分類基準がフィルタ用として表示されます。 @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#残り文字数: assessment#:#qst_essay_wordcounter_enabled#:#カウント文字数 assessment#:#qst_essay_wordcounter_enabled_info#:#入力の文字数をカウントします。受験者は以下のテキスト入力欄へ書きこんだ文字数が表示されます。 assessment#:#qst_essay_written_words#:#入力した文字数: -assessment#:#qst_lifecycle#:#ライフサイクル -assessment#:#qst_lifecycle_draft#:#ドラフト -assessment#:#qst_lifecycle_filter_all#:#全てのライフサイクル -assessment#:#qst_lifecycle_final#:#最終 -assessment#:#qst_lifecycle_outdated#:#旧 -assessment#:#qst_lifecycle_rejected#:#拒否されました -assessment#:#qst_lifecycle_review#:#要レビュー -assessment#:#qst_lifecycle_sharable#:#共有化 assessment#:#qst_nested_nested_answers_off#:#インデントなし、順番のみ assessment#:#qst_nested_nested_answers_on#:#回答にインデントを使用 assessment#:#qst_nr_of_tries#:#実行回数 @@ -1168,17 +1144,14 @@ assessment#:#question_type#:#問題のタイプ assessment#:#questionlist_cannot_be_altered#:#テストにはすでに参加者のデータセットが含まれているため、質問リストを変更することはできません。 assessment#:#questionpool_not_entered#:#問題集の名前を入力してください! assessment#:#questionpool_not_selected#:#問題集を選択して下さい! -assessment#:#questions#:#問題 assessment#:#questions_from#:#質問者 assessment#:#questions_per_page_view#:#ページ表示 assessment#:#random_accept_sample#:#サンプルを承諾 assessment#:#random_another_sample#:#他のサンプルを入手 assessment#:#random_selection#:#ランダム選択 assessment#:#range#:#範囲 -assessment#:#range_lower_limit#:#下限 assessment#:#range_max#:#範囲(最大) assessment#:#range_min#:#範囲(最小) -assessment#:#range_upper_limit#:#上限 assessment#:#rated_sign#:#サイン assessment#:#rated_unit#:#ユニット assessment#:#rated_value#:#値 @@ -1254,7 +1227,6 @@ assessment#:#search_roles#:#役割を検索 assessment#:#search_term#:#用語を検索 assessment#:#select_at_least_one_feedback_type_and_trigger#:#ファイードバックとトリガーの一つは選択してください。 assessment#:#select_at_least_one_lock_answer_type#:#ロックされた回答の一つは選択してください。 -assessment#:#select_gap#:#穴埋めを選択 assessment#:#select_max_one_item#:#一アイテムのみ選択してください assessment#:#select_one_user#:#最低ユーザ1名を選択してください。 assessment#:#select_question#:#問題を選択 @@ -1281,7 +1253,6 @@ assessment#:#show_old_introduction#:#過去の序論を表示 assessment#:#show_pass_overview#:#マークした試験状況を表示 assessment#:#show_results#:#結果を表示 assessment#:#show_user_answers#:#マークしたユーザの解答を表示 -assessment#:#shuffle_answers#:#解答をシャッフル assessment#:#skip_question#:#回答しないで次へ assessment#:#solution#:#解答 assessment#:#solutionText#:#テキスト @@ -14049,6 +14020,35 @@ qpl#:#qpl_page_type_qfbg#:#全般フィードバック qpl#:#qpl_page_type_qfbs#:#特別フィードバック qpl#:#qpl_page_type_qht#:#ヒント qpl#:#qpl_page_type_qpl#:#質問ページ +qsts#:#answer_options#:#解答オプション +qsts#:#cloze_text#:#穴埋め問題のテキスト +qsts#:#cloze_textgapcase_insensitive#:#大文字・小文字を非区分 +qsts#:#cloze_textgapcase_sensitive#:#大文字・小文字を区分 +qsts#:#cloze_textgaplevenshtein_of#:#%sのレーベンシュタイン距離 +qsts#:#confirm_delete_questions#:#本当に次の問題を削除しますか? +qsts#:#create_question#:#問題を作成 +qsts#:#gap#:#穴埋め +qsts#:#gaps#:#Gaps +qsts#:#insert_gap#:#ギャップを挿入 +qsts#:#min_auto_complete#:#オートコンプリート +qsts#:#msg_no_questions_selected#:#問題が選択されませんでした。 +qsts#:#out_of_range#:#範囲外 +qsts#:#qst_lifecycle#:#ライフサイクル +qsts#:#qst_lifecycle_draft#:#ドラフト +qsts#:#qst_lifecycle_filter_all#:#全てのライフサイクル +qsts#:#qst_lifecycle_final#:#最終 +qsts#:#qst_lifecycle_outdated#:#旧 +qsts#:#qst_lifecycle_rejected#:#拒否されました +qsts#:#qst_lifecycle_review#:#要レビュー +qsts#:#qst_lifecycle_sharable#:#共有化 +qsts#:#questionlist#:#問題リスト +qsts#:#questions#:#問題 +qsts#:#range_lower_limit#:#下限 +qsts#:#range_upper_limit#:#上限 +qsts#:#reset_preview#:#プレビューをリセット +qsts#:#select_gap#:#穴埋めを選択 +qsts#:#shuffle_answers#:#解答をシャッフル +qsts#:#suggested_learning_content#:#要約用のコンテンツを追加 rating#:#rat_not_rated_yet#:#星評価なし rating#:#rat_nr_ratings#:#%s星評価 rating#:#rat_one_rating#:#1つ星評価 @@ -16698,7 +16698,6 @@ survey#:#questionblock#:#質問ブロック survey#:#questionblock_inserted#:#質問ブロックの挿入 survey#:#questionblocks#:#質問ブロック survey#:#questionblocks_inserted#:#質問ブロックの挿入 -survey#:#questions#:#質問 survey#:#questions_inserted#:#質問を挿入しました! survey#:#questions_removed#:#質問や質問ブロックを削除しました! survey#:#questiontype#:#質問タイプ @@ -16981,6 +16980,7 @@ survey#:#svy_please_select_unused_codes#:#未使用のコードを一つは選 survey#:#svy_print_hide_labels#:#ラベルを非表示 survey#:#svy_print_show_labels#:#ラベルを表示 survey#:#svy_privacy_info#:#プライバシー +survey#:#svy_questions#:#質問 survey#:#svy_rater#:#評価者 survey#:#svy_rater_see_app_info#:#評価対象者名は問題評価を可能にする評価者へ表示されます。 survey#:#svy_reminder_mail_template#:#メールテンプレート diff --git a/lang/ilias_ka.lang b/lang/ilias_ka.lang index c1ebbd302731..9610bb082db7 100644 --- a/lang/ilias_ka.lang +++ b/lang/ilias_ka.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#ტესტისა და შეფასების ჩატვირთვის გააქტიურება assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#დაამატეთ შემოთავაზებული ამოხსნა assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#დაამატეთ წრის სივრცე assessment#:#add_gap#:#დაამტეთ ნაპრალის ტექსტი @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#თქვენ მიიღ assessment#:#answer_is_right#:#თქვენი პასუხი სწორია assessment#:#answer_is_wrong#:#თქვენი პასუხი არასწორია assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#პასუხის ტექსტი assessment#:#answer_types#:#პასუხის სახეობები @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#დასრულებულია assessment#:#ass_completion_by_submission_info#:#თუ გააქტიურებულია, ერთი ფაილის ჩაბარებაც იწვევს დავალების დასრულებას. ნიშნის ხელით შეცვლა შესაძლებელია მოგვიანებით. Aმ პარამეტრების ჩართვა არ იწვევს ცვლიბებს უკვე ჩაბარებულ პასუხებში assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#ტექსტის ველის სიგრძე assessment#:#cloze_fixed_textlength_description#:#თუ თქვენ შეიყვანთ 0-ზე მეტ ღირებულებას მთლიანი ტექსტი და რიცხვითი "ნაპრალები" ამ ღირებულებით განისაზღვრება assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#ტექსტის დახურვა -assessment#:#cloze_textgap_case_insensitive#:#შემთხვევა აუთვისებადია -assessment#:#cloze_textgap_case_sensitive#:#შემთხვევა ათვისებულია -assessment#:#cloze_textgap_levenshtein_of#:# assessment#:#code#:#კოდი assessment#:#codebase#:#კოდების ბაზა assessment#:#concatenation#:#ურთიერთაკვშირი, დამთხვევა @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#ნაპრალი assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#ლექსიკონის პერიოდი assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#კითხვა უკვე შეი assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#ჩავსვათ შემდეგ assessment#:#insert_before#:#წინასწარ ჩავსვათ -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#შიდა ლინკები assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#შენარჩუნება assessment#:#manscoring#:#ხელით შეფასება assessment#:#manscoring_done#:#შეფასებული მონაწილეები @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#თქვენ მიაღწეთ assessment#:#maximum_points#:#ქულების მაქსიმალური რაოდენობა assessment#:#maxsize#:#ატვირთული ფაილის მაქსიმალური ზომა assessment#:#maxsize_info#:#შეიყვანეთ მაქსიმალური ზომა ბაიტებში რის ატვირთვაც იქნება შესაძლებელი. თუ თქვენ ამ ველს ცარიელს დატოვებთ, ინსტალაციისთვის მაქსიმალური ზომა იქნება დაშვებული. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#თქვენ უნდა განსაზღვროთ 0ის მინიმალური %s. Qქულის სქემა არ არის შენახული assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#წრე დაემატა -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#დასახელებების რიცხვი მეტი ან ტოლი უნდა იყოს განსაზღვრებების რიცხვისა assessment#:#msg_poly_added#:#პოლიგონი დაემატა assessment#:#msg_questions_moved#:#კითხვა გადაადგილდა @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#პასუხების თანმიმდევრობა რომელიც თქვენ აქ განსაზღვრეთ გამოიყენება როგორც სწორი პასუხების თანმიმდევრობა assessment#:#ordertext#:#თანმიმდევრობითი ტექსტი assessment#:#ordertext_info#:#გთხოვთ შეიყვანოთ ტექსტი რომელიც ჰორიზონტალურად უნდა დალაგდეს. თანმიმდევრობითი ტექსტი გამოიყოფა თეთრი თავისუფალი სივრცის ნიშნებით. თუ გსურთ სხვა სახის გამოყოფა, თქვენ შეგიძლიათ გამოიყენოთ სხვა გამომყოფი %s. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#შედეგი assessment#:#output_mode#:#შედეგის სახეობა assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#დარწმუნებული ხართ რომ ნამდვილად გსურთ შემდეგი კითხვების წაშლა? assessment#:#qpl_copy_insert_clipboard#:#მონიშნული კითხვების ასლი გადავიდა დაფაზე assessment#:#qpl_copy_select_none#:#გთხოვთ შეამოწმოთ სულ მცირე ერთი კითხვა რომ გადაიტანოთ ის დაფაზე assessment#:#qpl_delete_rbac_error#:#თქვენ არ გაქვთ უფლება წაშალოთ ეს კითხვა! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#კითხვები რომლის რედაქტირებასაც თქვენ ცდილობთ არსებობს %s ტესტში. თუ თქვენ შეცვლით ამ ტესტს, თქვენ ვეღარ შეცვლით კითხვებს ტესტებში, რადგან სისტემა ქმნის კითხვების ასლს როცა მას ტესტში სვავთ assessment#:#qpl_questions_deleted#:#კითხვები წაიშალა -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#მცდელობების რაოდენობა @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#კითხვის დასახელებ assessment#:#question_type#:#კითხვის სახეობა assessment#:#questionpool_not_entered#:#გთხოვთ შეიყვანოთ სახელი კითხვების ნაკადისთვის assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#კითხვების ფორმა assessment#:#questions_per_page_view#:#გვერდის ხედვა assessment#:#random_accept_sample#:#ნიმუშის მიღება assessment#:#random_another_sample#:#სხვა ნიმუშის მიღება assessment#:#random_selection#:#შემთხვევითი მონიშვნა assessment#:#range#:#რიგი -assessment#:#range_lower_limit#:#ქვედა ზღვარი assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#ზედა ზღვარი assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#პასუხისმგებლობეის assessment#:#search_term#:#დასახელების ძიება assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#აირჩიეთ ნაპრალი assessment#:#select_max_one_item#:#გთხოვთ აირჩიოთ მხოლოდ ერთი პუნქტი assessment#:#select_one_user#:#გთხოვთ აირჩიოთ სულ მცირე ერთი მომხმარებელი assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#აჩვენე შეფასებული ჩაბარების მიმოხილვა assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#აჩვენე მომხმარებელთა შეფასებული პასუხები -assessment#:#shuffle_answers#:#პასუხების არევა assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#ტექსტი @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#ტექსტის დახურვა +qsts#:#cloze_textgapcase_insensitive#:#შემთხვევა აუთვისებადია +qsts#:#cloze_textgapcase_sensitive#:#შემთხვევა ათვისებულია +qsts#:#cloze_textgaplevenshtein_of#:# +qsts#:#confirm_delete_questions#:#დარწმუნებული ხართ რომ ნამდვილად გსურთ შემდეგი კითხვების წაშლა? +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#ნაპრალი +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#ქვედა ზღვარი +qsts#:#range_upper_limit#:#ზედა ზღვარი +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#აირჩიეთ ნაპრალი +qsts#:#shuffle_answers#:#პასუხების არევა +qsts#:#suggested_learning_content#:#დაამატეთ შემოთავაზებული ამოხსნა rating#:#rat_not_rated_yet#:#არ არის შეფასებული ჯერ rating#:#rat_nr_ratings#:#%s რეიტინგები rating#:#rat_one_rating#:#ერთი რეიტინგი @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#კითხვის ბლოკი survey#:#questionblock_inserted#:#კითხვის ბლოკი ჩასმულია survey#:#questionblocks#:#კითხვის ბლოკები survey#:#questionblocks_inserted#:#კითხვების ბლოკები ჩასმულია -survey#:#questions#:#კითხვები survey#:#questions_inserted#:#კითხვები ჩასმულია survey#:#questions_removed#:#კითხვები და/ან კითხვების ბლოკები ამოშლილია! survey#:#questiontype#:#კითხვის სახეობა @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#კითხვები survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_lt.lang b/lang/ilias_lt.lang index 23cdb510600d..babdd31c07de 100644 --- a/lang/ilias_lt.lang +++ b/lang/ilias_lt.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Aktyvuoti Testavimo ir Vertinimo registravimą assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Pridėti Tarpo (Gap) Tekstą @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Jūs gavote taškų už savo s assessment#:#answer_is_right#:#Jūsų sprendimas yra teisingas assessment#:#answer_is_wrong#:#Jūsų sprendimas yra klaidingas assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Atsakymo tekstas assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Sąlygos tekstas -assessment#:#cloze_textgap_case_insensitive#:#ABC/abc nesvarbu -assessment#:#cloze_textgap_case_sensitive#:#ABC/abc svarbu -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein atstumas iš %s assessment#:#code#:#Kodas assessment#:#codebase#:#Kodo bazė assessment#:#concatenation#:#Tarpusavio ryšis @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Tarpas (Gap) assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Glosarijaus terminas assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Įterpti po assessment#:#insert_before#:#Įterpti prieš -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Vidinės Nuorodos assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Priežiūra assessment#:#manscoring#:#Individualus Vertinimas assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Jūs pasiekėte maksimalų testo band assessment#:#maximum_points#:#Maksimalus galimas balų skaičius assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Jūs privalote nustatyti minimalų procentų skaičių lygų 0 procentų! Vertinimo sistema neišsaugota. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Išvestis assessment#:#output_mode#:#Išvesties būdas assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Ar jūs tikrai norite ištrinti šį(-iuos) klausimą(-us)? Jei ištrinsite užrakintus klausimus, visų testų, kuriuose yra užrakintas klausimas rezultatai bus ištrinti taip pat. assessment#:#qpl_copy_insert_clipboard#:#Pasirinktas(-i) klausimas(-ai) nukopijuoti į atminties krepšelį assessment#:#qpl_copy_select_none#:#Norėdami perkelti į atminties krepšelį pasirinkite bent vieną klausimą assessment#:#qpl_delete_rbac_error#:#Jūs neturite teisių ištrinti šį klausimą! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Klausimas, kurį ruošiatės redaguoti įtrauktas į %s testą(-us). Jei pakeisite šį klausimą, klausimas(-ai) teste(-uose) NEpasikeis, kadangi sistema sukuria įterpiamo į testą klausimo kopiją! assessment#:#qpl_questions_deleted#:#Klausimas(-ai) ištrintas(-i). -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Klausimo pavadinimas assessment#:#question_type#:#Klausimo Tipas assessment#:#questionpool_not_entered#:#Prašome įrašyti apklausos pavadinimą! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#klausimai iš assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Priimti pavyzdį assessment#:#random_another_sample#:#Pasirinkite kitą pavyzdį assessment#:#random_selection#:#Atsitiktinis pasirinkimas assessment#:#range#:#Ribos -assessment#:#range_lower_limit#:#Žemutinė riba assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Viršutinė riba assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Ieškoti Rolių assessment#:#search_term#:#Ieškoti Žodžio assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Pasirinkite tarpą assessment#:#select_max_one_item#:#Pasirinkite tik vieną elementą assessment#:#select_one_user#:#Pasirinkite bent vieną vartotoją assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Išmaišyti atsakymus assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Sąlygos tekstas +qsts#:#cloze_textgapcase_insensitive#:#ABC/abc nesvarbu +qsts#:#cloze_textgapcase_sensitive#:#ABC/abc svarbu +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein atstumas iš %s +qsts#:#confirm_delete_questions#:#Ar jūs tikrai norite ištrinti šį(-iuos) klausimą(-us)? Jei ištrinsite užrakintus klausimus, visų testų, kuriuose yra užrakintas klausimas rezultatai bus ištrinti taip pat. +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Tarpas (Gap) +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Žemutinė riba +qsts#:#range_upper_limit#:#Viršutinė riba +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Pasirinkite tarpą +qsts#:#shuffle_answers#:#Išmaišyti atsakymus +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Klausimo blokas survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Klausimo blokai survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Klausimai survey#:#questions_inserted#:#Klausimas(-ai) įterpti! survey#:#questions_removed#:#Klausimas(-ai) ir/ar klausimo blokas(-ai) pašalinti! survey#:#questiontype#:#Klausimo tipas @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Klausimai survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_nl.lang b/lang/ilias_nl.lang index 10383e62b332..d59bfefa87f8 100644 --- a/lang/ilias_nl.lang +++ b/lang/ilias_nl.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Inschakelen TinyMCE voor WYSIWYG Editen assessment#:#activate_logging#:#Inschakelen Toets&Beoordeling logging assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Voeg voorgestelde oplossing toe assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Voeg cirkel gebied toe assessment#:#add_gap#:#Voeg spatie toe @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Je hebt punten gekregen voor d assessment#:#answer_is_right#:#Je oplossing is goed assessment#:#answer_is_wrong#:#Je oplossing is fout assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Tekst assessment#:#answer_types#:#Antwoord type @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions. assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Aanmaken van een Test Archive bestand -assessment#:#ass_create_question#:#Vraag aanmaken assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Tekst veldlengte assessment#:#cloze_fixed_textlength_description#:#Als je een waarde hoger dan 0 ingeeft, dan zullen alle tekst en numerieke velden een lengte krijgen van deze waarde. assessment#:#cloze_gap_size_info#:#Als je een waarde groter dan 0 ingeeft, zal dit gap tekstveld worden aangemaakt met deze lengte. Als je geen waarde ingeeft, zal het gap tekstveld worden aangemaakt met de waarde van het globale vaste lengte. -assessment#:#cloze_text#:#Verhaspelde zin -assessment#:#cloze_textgap_case_insensitive#:#Hoofdletter ongevoelig -assessment#:#cloze_textgap_case_sensitive#:#Hoofdletter gevoelig -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein afstand van %s assessment#:#code#:#Code assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Aaneenschakeling @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Zowel decimalen als breuken worden geaccepteerd als input assessment#:#fq_precision_info#:#Gewenste aantal decimale plaatsen assessment#:#fq_question_desc#:#Je kunt variabelen definieren met $v1, $v2 ... $vn, resultaten door het gebruik van $r1, $r2 .... $rn op de gewenste positie in de vraagtekst. Klik op de knop "Ontleed vraag" om editforms voor variabelen en resultaten te openen. -assessment#:#gap#:#Gat assessment#:#gap_combination#:#Gap Combinatie -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Verklarende term assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Je resultaat is: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#De vraag bevat reeds plaatjes.U kunt het assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Voeg in na assessment#:#insert_before#:#Voeg in voor -assessment#:#insert_gap#:#Insert Gap assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Interne links assessment#:#intprecision#:#Deelbaar door @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Onderhoud assessment#:#manscoring#:#Handmatige score assessment#:#manscoring_done#:#Scorende Deelnemers @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Je hebt het maximum aantal pogingen v assessment#:#maximum_points#:#Maximum behaalbare punten assessment#:#maxsize#:#Maximum file upload size assessment#:#maxsize_info#:#Vul in de maximale hoeveelheid bytes die wordt toegestaan voor bestand-uploads. Als je dit vak blanco laat, wordt de maximale grootte van deze installatie gekozen. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Je moet een minimum percentage opgeven van ministens 0 procent! Het punten schema is niet opgeslaan assessment#:#misc#:#Misc Options @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Cirkel toegevoegd -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Het aantal termen moet groter of gelijk zijn aan het aantal definities. assessment#:#msg_poly_added#:#Polygon toegevoegd assessment#:#msg_questions_moved#:#Vragen verplaatst @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#De volgorde van de antwoorden die je hebt bepaald, wordt beschouwd als de juiste volgorde. assessment#:#ordertext#:#Tekstvolgorde assessment#:#ordertext_info#:#Voer de tekst in die horizontaal komt te staan. De tekst wordt gescheiden door spaties. Als je daarvoor iets anders wenst gebruik dan %s om je tekstonderdelen te scheiden. -assessment#:#out_of_range#:#Out of range###06 02 2015 new variable assessment#:#output#:#Output assessment#:#output_mode#:#Output modus assessment#:#parseQuestion#:#Vraag ontleden @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Weet je zeker dat je de volgende vraag(vragen) wilt verwijderen Als je een geblokkeerde vraag verwijderd, dan worden de resultaten van alle toetsen die deze geblokkeerde vraag bevatten, ook verwijderd. assessment#:#qpl_copy_insert_clipboard#:#De geselecteerde vragen zijn gekopieerd naar het klembord assessment#:#qpl_copy_select_none#:#Selecteer minstens één vraag om te kopiëren naar het klembord assessment#:#qpl_delete_rbac_error#:#Je hebt geen toegang om deze vraag te verwijderen! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#De vraag, die je nu gaat wijzigen, bestaat al in %s toets(en). Als je deze vraag nu wijzigt, wijzig je niet de vraag/vragen in de toets(en), omdat het systeem een kopie van de vraag maakt als de vraag in een toets wordt opgenomen! assessment#:#qpl_questions_deleted#:#Vraag/vragen verwijderd. -assessment#:#qpl_reset_preview#:#Herstel beeld assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Aantal pogingen @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Vraag titel assessment#:#question_type#:#Vraag type assessment#:#questionpool_not_entered#:#Geef een naam voor de vragenpool! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#vragen van assessment#:#questions_per_page_view#:#Paginabeeld assessment#:#random_accept_sample#:#Accepteer voorbeeld assessment#:#random_another_sample#:#Kies een ander voorbeeld assessment#:#random_selection#:#Willekeurige selectie assessment#:#range#:#Limiet -assessment#:#range_lower_limit#:#Verlaag limit assessment#:#range_max#:#Bereik (Maximum) assessment#:#range_min#:#Bereik (Minimum) -assessment#:#range_upper_limit#:#Verhoog limit assessment#:#rated_sign#:#Teken assessment#:#rated_unit#:#Eenheid assessment#:#rated_value#:#Beoordeling @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Zoek rollen assessment#:#search_term#:#Zoek begrippen assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Selecteer gaten assessment#:#select_max_one_item#:#Selecteer minstens 1 item assessment#:#select_one_user#:#Selecteer minstens 1 gebruiker assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Toon gemarkeerde afgelopen toetsen assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Toon gebruikers gemarkeerde antwoorden. -assessment#:#shuffle_answers#:#Antwoorden mixen assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Tekst @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Verhaspelde zin +qsts#:#cloze_textgapcase_insensitive#:#Hoofdletter ongevoelig +qsts#:#cloze_textgapcase_sensitive#:#Hoofdletter gevoelig +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein afstand van %s +qsts#:#confirm_delete_questions#:#Weet je zeker dat je de volgende vraag(vragen) wilt verwijderen Als je een geblokkeerde vraag verwijderd, dan worden de resultaten van alle toetsen die deze geblokkeerde vraag bevatten, ook verwijderd. +qsts#:#create_question#:#Vraag aanmaken +qsts#:#gap#:#Gat +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###06 02 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Verlaag limit +qsts#:#range_upper_limit#:#Verhoog limit +qsts#:#reset_preview#:#Herstel beeld +qsts#:#select_gap#:#Selecteer gaten +qsts#:#shuffle_answers#:#Antwoorden mixen +qsts#:#suggested_learning_content#:#Voeg voorgestelde oplossing toe rating#:#rat_not_rated_yet#:#Nog geen beoordelingen rating#:#rat_nr_ratings#:#%s Beoordelingen rating#:#rat_one_rating#:#Eén beoordeling @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Vragenblok survey#:#questionblock_inserted#:#Vraagblok ingevoegd survey#:#questionblocks#:#Vraagblokken survey#:#questionblocks_inserted#:#Vraakblokken ingevoegd -survey#:#questions#:#Vragen survey#:#questions_inserted#:#Vraag(vragen) ingevoegd! survey#:#questions_removed#:#Vraag(vragen) en/of vraag-blok(ken)verwijderd! survey#:#questiontype#:#Vraagtype @@ -16922,6 +16921,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Vragen survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_pl.lang b/lang/ilias_pl.lang index 31cfeefd861d..66d094c45806 100644 --- a/lang/ilias_pl.lang +++ b/lang/ilias_pl.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Aktywuj TinyMCE do edytowania WYSIWYG assessment#:#activate_logging#:#Aktywuj śledzenie testów i oceniania assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Pokaż sugerowane rozwiązanie assessment#:#add_answers#:#Dodaj odpowiedzi assessment#:#add_circle#:#Dodaj krąg assessment#:#add_gap#:#Dodaj tekst odstępu @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Otrzymałeś punkty za swoją assessment#:#answer_is_right#:#Twoja odpowiedź jest prawidłowa. assessment#:#answer_is_wrong#:#Twoja odpowiedź jest błędna. assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Opcje z odpowiedziami: assessment#:#answer_question#:#Odpowiedz na pytanie assessment#:#answer_text#:#Tekst odpowiedzi assessment#:#answer_types#:#Edytor odpowiedzi @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Zaliczenie przez oddanie assessment#:#ass_completion_by_submission_info#:#Jeśli ta opcja jest aktywna, oddanie pliku z odpowiedziami skutkuje naliczeniem maksymalnej liczny punktów dla danego pytania. Ocenę można w każdej chwili dopasować ręcznie. Zmiana tego ustawienia nie ma żadnego wpływu na już oddane rozwiązania. assessment#:#ass_create_export_file_with_results#:#Utwórz plik eksportowy (łącznie z wynikami uczestników) assessment#:#ass_create_export_test_archive#:#Utwórz plik archiwalny dla testu -assessment#:#ass_create_question#:#Utwórz pytanie assessment#:#ass_imap_hint#:#Wskazówka (wyświetlona jako porada) assessment#:#ass_imap_map_file_not_readable#:#Nie udało się wczytać wysłanej mapy bitowej. assessment#:#ass_imap_no_map_found#:#W wysłanej mapie bitowej nie udało się znaleźć obsługiwanego formatu. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Długość pola tekstowego assessment#:#cloze_fixed_textlength_description#:#Jeśli wpiszesz tutaj wartość, zostaną utworzone luki w tekście, które nie mają określonej maksymalnej długości, oraz luki numeryczne o tej długości, dzięki czemu nie będzie możliwości wprowadzić większej liczby znaków. W przypadku luk numerycznych należy również pamiętać, że przy liczeniu uwzględniany jest separator dziesiętny. assessment#:#cloze_gap_size_info#:#Jeśli wpisana wartość jest większa niż 0, ta przerwa generowane jest o tutaj wpisanej długości. Jeśli nie podano żadnej wartości, ta przerwa zostanie wygenerowana o długości pola tekstowego podanej globalnie. -assessment#:#cloze_text#:#Uzupełnij tekst -assessment#:#cloze_textgap_case_insensitive#:#Nie uwzględniaj wielkości liter -assessment#:#cloze_textgap_case_sensitive#:#Uwzględniaj wielkość liter -assessment#:#cloze_textgap_levenshtein_of#:#Odległość Levenshtein %s assessment#:#code#:#Kod assessment#:#codebase#:#Baza kodu assessment#:#concatenation#:#Złączenie @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Dozwolone jest użycie już zdefiniowanych zmienn assessment#:#fq_no_restriction_info#:#Dozwolone są zarówno liczby dziesiętne jak i ułamki. assessment#:#fq_precision_info#:#Wpisz tutaj wymaganą liczbę miejsc po przecinku. assessment#:#fq_question_desc#:#Zmienne definiujesz przez podanie w wybranych pozycjach w tekście $v1, $v2 ... $vn, pól z wynikami z $r1, $r2 .... $rn. Następnie kliknij na przycisk "Analizuj pytanie", aby wygenerować formularze edycji dla wszystkich zmiennych i wyników. -assessment#:#gap#:#Luka assessment#:#gap_combination#:#Kombinacji tekstu do uzupełnienia -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Hasło słownika assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Otrzymałeś/otrzymałaś ocenę Note "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Pytanie zawiera już obrazy. Dlatego nie assessment#:#info_text_upload#:#Wybierz plik tekstowy (UTF-8) z odpowiedziami do załadowania. assessment#:#insert_after#:#Wstaw po assessment#:#insert_before#:#Wstaw przed -assessment#:#insert_gap#:#Wypełnij lukę assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Linki wewnętrzne assessment#:#intprecision#:#Dzielone przez @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Uczestnik wprowadził nieprawid assessment#:#longmenu#:#Longmenu assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Tekst ‘Long Menu’ -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Konserwacja assessment#:#manscoring#:#Ręczna punktacja assessment#:#manscoring_done#:#Już ocenieni uczestnicy @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Przekroczyłeś maksymalną ilość p assessment#:#maximum_points#:#Maksymalna liczba dostępnych punktów assessment#:#maxsize#:#Maksymalny rozmiar pliku assessment#:#maxsize_info#:#Podaj maksymalny rozmiar pliku w bytach, którego ładowany plik nie może przekroczyć. Jeśli to pole będzie puste, zastosowane zostanie ustawienie podstawowego systemu. -assessment#:#min_auto_complete#:#Automatyczne uzupełnianie assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Musisz określić minimum na 0 procent! Schemat zaznaczeń nie został zapisany. Schemat zaznaczeń nie został zapisany. assessment#:#misc#:#Różne opcje @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Dodano krąg. -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Liczba pojęć musi być większa lub równa liczbie definicji. assessment#:#msg_poly_added#:#Dodano wielobok. assessment#:#msg_questions_moved#:#Przesunięto pytanie(a). @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Ustalona tutaj kolejność odpowiedzi używana jest jako prawidłowa kolejność rozwiązań. assessment#:#ordertext#:#Tekst, który trzeba umieścić assessment#:#ordertext_info#:#Proszę podaj tekst w takiej kolejności, w jakiej ma zostać umieszczony w układzie poziomym. Poszczególne elementy składowe oddzielone są znakiem pustym. Jeśli konieczny jest inny sposób oddzielenia, należy zamiast znaku pustego użyć separatora %s. -assessment#:#out_of_range#:#Poza obszarem assessment#:#output#:#Wyjście assessment#:#output_mode#:#Tryb wyjścia assessment#:#parseQuestion#:#Analizuj pytanie @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Przerwij -assessment#:#qpl_confirm_delete_questions#:#Czy jesteś pewien, że chcesz usunąć następujące pytania? Jeśli usuniesz zaznaczone pytania, to rezultaty wszystkich testów zawierających zaznaczone pytania będą również usunięte. assessment#:#qpl_copy_insert_clipboard#:#Wybrane pytania zostały skopiowane do schowka assessment#:#qpl_copy_select_none#:#Wybierz co najmniej jedno pytanie do skopiowania do schowka assessment#:#qpl_delete_rbac_error#:#Nie masz uprawnień do usunięcia tego pytania! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetencje assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Suma wszystkich punktów kompetencyjnych dla każdej kompetencji assessment#:#qpl_question_is_in_use#:#Pytanie, które modyfikujesz, jest w %s testach. Jeśli zmieniasz to pytanie, to NIE będziesz mógł zmienić pytań w testach, ponieważ system tworzy kopie pytań po wstawieniu ich do testu! assessment#:#qpl_questions_deleted#:#Usunięte pytania. -assessment#:#qpl_reset_preview#:#Przywróć podgląd assessment#:#qpl_save_skill_assigns_update#:#Zapisz przyporządkowane kompetencje assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Istniejące klasyfikacje można użyć do filtrowania pytań. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Ilość prób @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Tytuł pytania assessment#:#question_type#:#Typ pytania assessment#:#questionpool_not_entered#:#Podaj nazwę puli pytań! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#pytania z assessment#:#questions_per_page_view#:#Podgląd strony assessment#:#random_accept_sample#:#Akceptuj próbkę assessment#:#random_another_sample#:#Pobierz inną próbkę assessment#:#random_selection#:#Wybór przypadkowy assessment#:#range#:#Zakres -assessment#:#range_lower_limit#:#Limit dolny assessment#:#range_max#:#Zakres (maksimum) assessment#:#range_min#:#Zakres (minimum) -assessment#:#range_upper_limit#:#Limit górny assessment#:#rated_sign#:#Znak liczby assessment#:#rated_unit#:#Jednostka assessment#:#rated_value#:#Wartość @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Szukaj ról assessment#:#search_term#:#Szukaj frazy assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Wybierz odstęp assessment#:#select_max_one_item#:#Wybierz tylko jedną pozycję. assessment#:#select_one_user#:#Wybierz co najmniej jednego użytkownika. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Pokaż podgląd zaznaczonych wykonań assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Pokaż zaznaczone przez użytkownika odpowiedzi -assessment#:#shuffle_answers#:#Mieszaj odpowiedzi assessment#:#skip_question#:#Nie udzielaj odpowiedzi i przejdź do następnego pytania assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Tekst @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Opcje z odpowiedziami +qsts#:#cloze_text#:#Uzupełnij tekst +qsts#:#cloze_textgapcase_insensitive#:#Nie uwzględniaj wielkości liter +qsts#:#cloze_textgapcase_sensitive#:#Uwzględniaj wielkość liter +qsts#:#cloze_textgaplevenshtein_of#:#Odległość Levenshtein %s +qsts#:#confirm_delete_questions#:#Czy jesteś pewien, że chcesz usunąć następujące pytania? Jeśli usuniesz zaznaczone pytania, to rezultaty wszystkich testów zawierających zaznaczone pytania będą również usunięte. +qsts#:#create_question#:#Utwórz pytanie +qsts#:#gap#:#Luka +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Wypełnij lukę +qsts#:#min_auto_complete#:#Automatyczne uzupełnianie +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Poza obszarem +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Limit dolny +qsts#:#range_upper_limit#:#Limit górny +qsts#:#reset_preview#:#Przywróć podgląd +qsts#:#select_gap#:#Wybierz odstęp +qsts#:#shuffle_answers#:#Mieszaj odpowiedzi +qsts#:#suggested_learning_content#:#Pokaż sugerowane rozwiązanie rating#:#rat_not_rated_yet#:#Jeszcze nie oceniono rating#:#rat_nr_ratings#:#%s oceny rating#:#rat_one_rating#:#Ocena @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok pytań survey#:#questionblock_inserted#:#Wstawiono blok pytań survey#:#questionblocks#:#Bloki pytań survey#:#questionblocks_inserted#:#Wstawiono bloki pytań -survey#:#questions#:#Pytania survey#:#questions_inserted#:#Pytania wstawione! survey#:#questions_removed#:#Pytania i/lub bloki pytań usunięte! survey#:#questiontype#:#Typ pytania @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Ukryj etykiety survey#:#svy_print_show_labels#:#Pokaż etykiety survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Pytania survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Szablon wiadomości mailowej diff --git a/lang/ilias_pt.lang b/lang/ilias_pt.lang index 275e0811abf8..4d30fa0888d9 100644 --- a/lang/ilias_pt.lang +++ b/lang/ilias_pt.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Ativar TinyMCE para a edição WYSIWYG assessment#:#activate_logging#:#Novas notificações de correio assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Adicionar conteúdo para recapitulação assessment#:#add_answers#:#Adicionar respostas assessment#:#add_circle#:#Adicionar área de círculo assessment#:#add_gap#:#Adicionar texto do intervalo @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Tem pontos para a sua soluçã assessment#:#answer_is_right#:#A sua solução é correta assessment#:#answer_is_wrong#:#A sua solução está errada assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Opções de resposta: assessment#:#answer_question#:#Responder a pergunta assessment#:#answer_text#:#Texto de resposta assessment#:#answer_types#:#Editor para respostas @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completado por submissão assessment#:#ass_completion_by_submission_info#:#Se estiver ativado, a submissão de pelo menos um ficheiro leva à conclusão desta pergunta, concedendo a pontuação máxima para esta pergunta. A pontuação pode ser alterada mais tarde manualmente. Mudar esta definição não afeta as soluções já submetidas. assessment#:#ass_create_export_file_with_results#:#Criar ficheiro de exportação de texto (incl. resultados do participante) assessment#:#ass_create_export_test_archive#:#Criar ficheiro de arquivo de teste -assessment#:#ass_create_question#:#Criar pergunta assessment#:#ass_imap_hint#:#Sugestão para ser apresentado como dica de contexto. assessment#:#ass_imap_map_file_not_readable#:#Não foi possível ler o mapa de imagem carregada. assessment#:#ass_imap_no_map_found#:#Não foi possível encontrar qualquer forma na mapa de imagem carregada. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Comprimento do campo de texto assessment#:#cloze_fixed_textlength_description#:#Se introduzir um novo valor, todos os campos de espaços do texto que não impõem uma limitação máxima própria de caracteres, assim como todos os campos de espaços numéricos, serão criados com um comprimento fixo deste valor, não sendo assim possível introduzir mais do que os caracteres permitidos. Repare que para os espaços numéricos, o separador decimal conta como um caracter regular. assessment#:#cloze_gap_size_info#:#Se introduzir um valor superior a 0, este campo de texto de espaços será criado com o comprimento fixo deste valor. Se não introduzir um valor, o campo de texto de espaços será criado com o valor do comprimento global fixo. -assessment#:#cloze_text#:#Fechar texto -assessment#:#cloze_textgap_case_insensitive#:#Caso insensível -assessment#:#cloze_textgap_case_sensitive#:#Caso sensível -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distância de %s assessment#:#code#:#Código assessment#:#codebase#:#Base do código assessment#:#concatenation#:#Concatenação @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Pode introduzir variáveis predefinidas ($v1 até assessment#:#fq_no_restriction_info#:#São aceites entradas decimais e também frações. assessment#:#fq_precision_info#:#Introduza o número de casas decimais pretendido. assessment#:#fq_question_desc#:#Pode definir variáveis ao inserir $v1, $v2 ... $vn, resultados ao inserir $r1, $r2 .... $rn na posição pretendida no texto de perguntas. Clique no botão ‘Analisar pergunta’ para criar formas de edição para variáveis e resultados. -assessment#:#gap#:#Intervalo assessment#:#gap_combination#:#Combinação de intervalo -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Termo do glossário assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#A sua marca é: "[mark]" @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#A pergunta já contém imagens. Não pode assessment#:#info_text_upload#:#Escolha um ficheiro de texto de resposta (UTF-8) para carregar. assessment#:#insert_after#:#Inserir depois assessment#:#insert_before#:#Inserir antes -assessment#:#insert_gap#:#Inserir intervalo assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Links internos assessment#:#intprecision#:#Divisível por @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#O participante introduziu uma p assessment#:#longmenu#:#Menu comprido assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Texto de menu comprido -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Manutenção assessment#:#manscoring#:#Pontuação manual assessment#:#manscoring_done#:#Participantes pontuados @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Chegou ao número máximo de tentativ assessment#:#maximum_points#:#Pontos máximos disponíveis assessment#:#maxsize#:#Tamanho máximo para carregar ficheiro: assessment#:#maxsize_info#:#Introduzir o tamanho máximo em bytes que deve ser permitido para carregar ficheiros. Se deixar este campo vazio, será em vez disso escolhido o tamanho máximo desta instalação. -assessment#:#min_auto_complete#:#Autocompletado assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Tem e definir uma percentagem mínima de 0 por cento! O esquema de anotações não foi guardado. assessment#:#misc#:#Opções combinadas @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Círculo adicionado -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#O número de termos tem de ser superior ou igual ao número de definições. assessment#:#msg_poly_added#:#Polígono adicionado assessment#:#msg_questions_moved#:#Pergunta(s) movida(s) @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#A sequência de respostas que define aqui será assumida como a sequência de solução correta. assessment#:#ordertext#:#Ordenação texto assessment#:#ordertext_info#:#Introduza o texto que deve ser ordenado na horizontal. O texto de ordenação será separado pelos sinais de espaço em branco no texto. Se precisar de uma separação diferente, pode usar o separador %s para separar as suas unidades de texto. -assessment#:#out_of_range#:#Fora de alcance assessment#:#output#:#Saída assessment#:#output_mode#:#Modo saída assessment#:#parseQuestion#:#Analisar pergunta @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancelar -assessment#:#qpl_confirm_delete_questions#:#Tem a certeza que quer remover as seguintes perguntas? assessment#:#qpl_copy_insert_clipboard#:#A(s) pergunta(s) selecionada(s) são copiada(s) para a área de transferência assessment#:#qpl_copy_select_none#:#Selecione pelo menos uma pergunta para copiar para a área de transferência! assessment#:#qpl_delete_rbac_error#:#Não tem permissão para remover esta pergunta! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competência assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total dos pontos de competência por competência assessment#:#qpl_question_is_in_use#:#A pergunta que está prestes a editar existe em %s teste(s). Se mudar esta pergunta, NÃO muda a(s) pergunta(s) no teste(s), porque o sistema cria uma cópia de uma pergunta quando é inserida num teste! assessment#:#qpl_questions_deleted#:#Pergunta(s) removida(s). -assessment#:#qpl_reset_preview#:#Repor pré-visualização assessment#:#qpl_save_skill_assigns_update#:#Guardar atribuições de competência assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#São propostas taxonomias existentes neste banco para filtrar as perguntas. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Número de tentativas @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Título da pergunta assessment#:#question_type#:#Tipo de pergunta assessment#:#questionpool_not_entered#:#Introduza um nome para um banco de perguntas! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#perguntas de assessment#:#questions_per_page_view#:#Vista de página assessment#:#random_accept_sample#:#Aceitar amostra assessment#:#random_another_sample#:#Obter outra amostra assessment#:#random_selection#:#Seleção aleatória assessment#:#range#:#Faixa -assessment#:#range_lower_limit#:#Limite inferior assessment#:#range_max#:#Faixa (máxima) assessment#:#range_min#:#Faixa (mínima) -assessment#:#range_upper_limit#:#Limite superior assessment#:#rated_sign#:#Sinal assessment#:#rated_unit#:#Unidade assessment#:#rated_value#:#Valor @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Pesquisar funções assessment#:#search_term#:#Pesquisar termo assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Selecionar espaço assessment#:#select_max_one_item#:#Selecione apenas um item assessment#:#select_one_user#:#Selecione pelo menos um utilizador. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Mostrar vista geral de tentativas marcadas assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Mostrar respostas marcadas do utilizador -assessment#:#shuffle_answers#:#Misturar respostas assessment#:#skip_question#:#Não responder e próximo assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Texto @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Opções de resposta +qsts#:#cloze_text#:#Fechar texto +qsts#:#cloze_textgapcase_insensitive#:#Caso insensível +qsts#:#cloze_textgapcase_sensitive#:#Caso sensível +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distância de %s +qsts#:#confirm_delete_questions#:#Tem a certeza que quer remover as seguintes perguntas? +qsts#:#create_question#:#Criar pergunta +qsts#:#gap#:#Intervalo +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Inserir intervalo +qsts#:#min_auto_complete#:#Autocompletado +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Fora de alcance +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Limite inferior +qsts#:#range_upper_limit#:#Limite superior +qsts#:#reset_preview#:#Repor pré-visualização +qsts#:#select_gap#:#Selecionar espaço +qsts#:#shuffle_answers#:#Misturar respostas +qsts#:#suggested_learning_content#:#Adicionar conteúdo para recapitulação rating#:#rat_not_rated_yet#:#Não classificado ainda rating#:#rat_nr_ratings#:#%s Classificações rating#:#rat_one_rating#:#Uma classificação @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Bloco de perguntas survey#:#questionblock_inserted#:#Bloco de perguntas inserido survey#:#questionblocks#:#Questionblocks survey#:#questionblocks_inserted#:#Blocos de perguntas inseridos -survey#:#questions#:#Perguntas survey#:#questions_inserted#:#Pergunta(s) inserida(s)! survey#:#questions_removed#:#Pergunta(s) e/ou bloco(s) de perguntas removidos! survey#:#questiontype#:#Tipo de pergunta @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Ocultar etiquetas survey#:#svy_print_show_labels#:#Mostrar etiquetas survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Perguntas survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Modelo mail diff --git a/lang/ilias_ro.lang b/lang/ilias_ro.lang index 02aca9fa8d8b..5d9da532a00c 100644 --- a/lang/ilias_ro.lang +++ b/lang/ilias_ro.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Activati log-ul de Testare si Evaluare assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Adaugati text cu spatii goale @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Solutia dumneavoastra este corecta###28 Jul 2006 content changed assessment#:#answer_is_wrong#:# Solutia dumneavoastra este gresita assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Raspuns la text assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Text cu spatii se completat -assessment#:#cloze_textgap_case_insensitive#:#Case insensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_case_sensitive#:#Case sensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable assessment#:#code#:#Cod assessment#:#codebase#:#Codebase###10 Jul 2006 new variable assessment#:#concatenation#:#Concatenare @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Spatiu assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Termen de glosar assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Inserati dupa assessment#:#insert_before#:#Inserati inainte -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Legaturi Interne assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Intretinere assessment#:#manscoring#:#Manual Scoring###12 11 2006 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Ati ajuns la numarul maxim de incerca assessment#:#maximum_points#:#Maxium available points###10 Jul 2006 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Trebuie sa definiti un procentaj minim de 0 la suta! Schema de notare nu a fost salvata.###10 Jul 2006 content changed assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 10 2006 new variable assessment#:#output_mode#:#Output mode###10 Jul 2006 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Sunteti sigur/a ca doriti sa stergeti urmatoarele intrebari)? Daca stergeti intrebarile blocate rezultatele tuturor testelor continand intrebari blocate vor fi sterse de asemenea.###10 Jul 2006 content changed assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard###03 Nov 2005 new variable assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard###03 Nov 2005 new variable assessment#:#qpl_delete_rbac_error#:#Nu aveti dreptul sa stergeti aceasta intrebare! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Intrebarea pe care doriti sa o editati axista in %s test(e). Daca schimbati aceasta intrebare, NU veti putea schimba intrebarea din alte teste pentru ca sistemul creaza o copie a intrebarii atunci cand este introdusa in test! assessment#:#qpl_questions_deleted#:#Intrebare stearsa. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Titlul intrebarii assessment#:#question_type#:#Tipul intrebarii assessment#:#questionpool_not_entered#:#Va rugam introduceti un nume pentru baza de intrebari! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#Forma intrebarii assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Acceptati mostra assessment#:#random_another_sample#:#Alta mostra assessment#:#random_selection#:#Selectie la intamplare assessment#:#range#:#Range###22 Feb 2006 new variable -assessment#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Cauta roluri assessment#:#search_term#:#Cauta termeni assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Selectati un spatiu assessment#:#select_max_one_item#:#Selectati un singur obiect assessment#:#select_one_user#:#Please select at least one user###23 Dec 2005 new variable assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Amestecati raspunsurile assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Text cu spatii se completat +qsts#:#cloze_textgapcase_insensitive#:#Case insensitive###03 Nov 2005 new variable +qsts#:#cloze_textgapcase_sensitive#:#Case sensitive###03 Nov 2005 new variable +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable +qsts#:#confirm_delete_questions#:#Sunteti sigur/a ca doriti sa stergeti urmatoarele intrebari)? Daca stergeti intrebarile blocate rezultatele tuturor testelor continand intrebari blocate vor fi sterse de asemenea.###10 Jul 2006 content changed +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Spatiu +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable +qsts#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Selectati un spatiu +qsts#:#shuffle_answers#:#Amestecati raspunsurile +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Baraj de intrebari survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Baraje de intrebari survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Intrebari survey#:#questions_inserted#:#Intrebari inserate! survey#:#questions_removed#:#Intrebarile sau barajele de intrebari eliminate! survey#:#questiontype#:#Tipul intrebarii @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Intrebari survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_ru.lang b/lang/ilias_ru.lang index 8156d786d99e..f1b0db7970d2 100644 --- a/lang/ilias_ru.lang +++ b/lang/ilias_ru.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#Активировать тестирование assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Добавить предполагаемое решение assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Добавить зону в виде круга assessment#:#add_gap#:#Добавить пропущенный текст @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Вы получили балл assessment#:#answer_is_right#:#Ваше решение верно assessment#:#answer_is_wrong#:#Ваше решение ошибочно assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Текст ответа assessment#:#answer_types#:#Типы ответа @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions. assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Создать файл архива теста -assessment#:#ass_create_question#:#Создать вопрос assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Длинна текстового поля assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Текст с пропусками -assessment#:#cloze_textgap_case_insensitive#:#Нечувствительный к регистру -assessment#:#cloze_textgap_case_sensitive#:#Чувствительный к регистру -assessment#:#cloze_textgap_levenshtein_of#:#Расстояние Левинштейна %s assessment#:#code#:#Код assessment#:#codebase#:#Кодовая база assessment#:#concatenation#:#Конкатенация @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input. assessment#:#fq_precision_info#:#Enter the number of desired decimal places. assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results. -assessment#:#gap#:#Пропуск assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Термин глоссария assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Вставить после assessment#:#insert_before#:#Вставить перед -assessment#:#insert_gap#:#Вставить пропуск assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Внутренние связи assessment#:#intprecision#:#Divisible By @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Обслуживание assessment#:#manscoring#:#Ручная проверка assessment#:#manscoring_done#:#Оцененые участники @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Вы достигли максима assessment#:#maximum_points#:#Максимальное число баллов assessment#:#maxsize#:#Максимальный размер загружаемого файла assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Вы должны определить минимальное значение как 0 процентов. Схема оценки не сохранена. assessment#:#misc#:#Прочие опции @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Круг добавлен -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Число терминов должно быть больше или равно числу определений. assessment#:#msg_poly_added#:#Многогранник добавлен assessment#:#msg_questions_moved#:#Вопрос(ы) перемещены @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence. assessment#:#ordertext#:#Упорядоченный текст assessment#:#ordertext_info#:#Пожалуйста введите текст который был бы упорядочен горизонтально. Упорядоченный текст должен быть разделен пробелами. Если вам нужен другой разделитель, вы можете использовать %s для отделение ваших блоков текста. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Вывод assessment#:#output_mode#:#Режим вывода assessment#:#parseQuestion#:#Разобрать вопрос @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Вы действительно желаете удалить следующие вопрос(ы)? assessment#:#qpl_copy_insert_clipboard#:#Выбранные вопросы скопированы в буфер обмена assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard assessment#:#qpl_delete_rbac_error#:#У вас нет прав удаления этого вопроса! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Вопрос, который вы выбрали на редактирование существует в %s тесте(ах). Если вы измените этот вопрос, то вы не сможете изменить вопрос(ы) в тесте(ах), потому, что система создает копию этого вопроса, когда добавляет его в тест! assessment#:#qpl_questions_deleted#:#Вопрос(ы) удален(ы). -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Число попыток @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Название вопроса assessment#:#question_type#:#Тип вопроса assessment#:#questionpool_not_entered#:#Пожалуйста, задайте имя для набора вопросов! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#форма вопросов assessment#:#questions_per_page_view#:#Page View assessment#:#random_accept_sample#:#Взять пример assessment#:#random_another_sample#:#Взять другой пример assessment#:#random_selection#:#Случайный выбор assessment#:#range#:#Диапазон -assessment#:#range_lower_limit#:#Нижняя граница assessment#:#range_max#:#Range (Maximum) assessment#:#range_min#:#Range (Minimum) -assessment#:#range_upper_limit#:#Верхняя граница assessment#:#rated_sign#:#Sign assessment#:#rated_unit#:#Unit assessment#:#rated_value#:#Value @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Поиск ролей assessment#:#search_term#:#Поиск терминов assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Выберите пропуск assessment#:#select_max_one_item#:#Пожалуйста, выберите только один пункт assessment#:#select_one_user#:#Please select at least one user. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Показать обзор отмеченных верных ответов assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Показать отмеченные пользователем ответы -assessment#:#shuffle_answers#:#Перемешать ответы assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Текст @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Текст с пропусками +qsts#:#cloze_textgapcase_insensitive#:#Нечувствительный к регистру +qsts#:#cloze_textgapcase_sensitive#:#Чувствительный к регистру +qsts#:#cloze_textgaplevenshtein_of#:#Расстояние Левинштейна %s +qsts#:#confirm_delete_questions#:#Вы действительно желаете удалить следующие вопрос(ы)? +qsts#:#create_question#:#Создать вопрос +qsts#:#gap#:#Пропуск +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Вставить пропуск +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Нижняя граница +qsts#:#range_upper_limit#:#Верхняя граница +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Выберите пропуск +qsts#:#shuffle_answers#:#Перемешать ответы +qsts#:#suggested_learning_content#:#Добавить предполагаемое решение rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Блок вопроса survey#:#questionblock_inserted#:#Question Block inserted survey#:#questionblocks#:#Блоки вопроса survey#:#questionblocks_inserted#:#Question Blocks inserted -survey#:#questions#:#Вопросы survey#:#questions_inserted#:#Вопрос(ы) вставлен(ы)! survey#:#questions_removed#:#Вопрос(ы) и/или удаленный блок(и) вопроса! survey#:#questiontype#:#Тип вопроса @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Вопросы survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_sk.lang b/lang/ilias_sk.lang index 675ccc20e0fe..396d593f4dbb 100644 --- a/lang/ilias_sk.lang +++ b/lang/ilias_sk.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Aktivovat záznam Test a Hodnocení assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Přidat doporučené řešení assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Přidat kruhovou plochu assessment#:#add_gap#:#Přidat textovou mezeru @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Obdržel(a) jste body za Vaše assessment#:#answer_is_right#:#Vaše řešení je správné assessment#:#answer_is_wrong#:#Vaše řešení je chybné assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Text odpovědi assessment#:#answer_types#:#Typy odpovědi @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Délka textového pole assessment#:#cloze_fixed_textlength_description#:#Pokud vložíte hodnotu větší než 0, všechna textová pole textových i numerických mezer budou vytvářeny s pevnou délkou rovnou této hodnotě. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Doplňovaný text -assessment#:#cloze_textgap_case_insensitive#:#Nerozlišuje velká/malá písmena -assessment#:#cloze_textgap_case_sensitive#:#Rozlišuje velká/malá písmena -assessment#:#cloze_textgap_levenshtein_of#:#Vzdálenost Levenshtein %s assessment#:#code#:#Kód assessment#:#codebase#:#Databáze kódu assessment#:#concatenation#:#Sřetězení @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Mezera assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Pojem glosáře assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Tato otázka již obsahuje obrázky. Nem assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Vložit za assessment#:#insert_before#:#Vložit před -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Interní odkazy assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Údržba assessment#:#manscoring#:#Manuální hodnocení assessment#:#manscoring_done#:#Vyhodnocení účastníci @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Dosáhl(a) jste maximální počet po assessment#:#maximum_points#:#Maxium dostupných bodů assessment#:#maxsize#:#Maximální velkost ukládaného souboru assessment#:#maxsize_info#:#Vložit maximální velikost v bytech, která je povolena pro ukládané soubory. Pokud necháte toto pole prázdné, bude namísto něj nastavena maximální velikost této instalace. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Musíte definovat minimální procento od specifikace 0 procent! Schéma známkování nebylo uloženo. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Kruh přidán -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Počet výrazů musí být větší, nebo roven počtu definicí. assessment#:#msg_poly_added#:#Polygon přidán assessment#:#msg_questions_moved#:#Otázky přesunuty @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###28 10 2010 new variable assessment#:#ordertext#:#Řazení textu assessment#:#ordertext_info#:#Vložte prosím text, který má být řazen horizontálně. Řazený text bude oddělen pomocí značek mezer v textu. Pokud potřebujete jiné oddělení, můžete použít separátor %s k oddělení Vašich textových jednotek. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Výstup assessment#:#output_mode#:#Výstupní mód assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Jste si jist(a), že chcete smazat následující otázky? assessment#:#qpl_copy_insert_clipboard#:#Vybrané otázky jsou zkopírovány do schránky assessment#:#qpl_copy_select_none#:#Označte alespoň jednu otázku ke zkopírování do schránky assessment#:#qpl_delete_rbac_error#:#Nemáte oprávnění smazat tuto otázku! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Otázka, kterou upravujete, existuje v %s testech. Pokud tuto otázku změníte, NEZMĚNÍTE otázku(y) v testu(ech), protože systém vytváří kopii otázky, která je vložena do testu! assessment#:#qpl_questions_deleted#:#Otázky smazány. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Počet pokusů @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Název otázky assessment#:#question_type#:#Typ otázky assessment#:#questionpool_not_entered#:#Vložte prosím název zásobníku otázek! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#otázky z assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Akceptovat příklad assessment#:#random_another_sample#:#Vybrat jiný příklad assessment#:#random_selection#:#Náhodný výběr assessment#:#range#:#Rozmezí -assessment#:#range_lower_limit#:#Spodní hranice assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Horní hranice assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Vyhledat role assessment#:#search_term#:#Vyhledat termín assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Vybírací mezera assessment#:#select_max_one_item#:#Vyberte prosím pouze jednu položku assessment#:#select_one_user#:#Vyberte prosím alespoň jednoho uživatele assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Zobrazit přehled označených průchodů assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Zobrazit označené odpovědi uživatelů -assessment#:#shuffle_answers#:#Zamíchat odpovědi assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Doplňovaný text +qsts#:#cloze_textgapcase_insensitive#:#Nerozlišuje velká/malá písmena +qsts#:#cloze_textgapcase_sensitive#:#Rozlišuje velká/malá písmena +qsts#:#cloze_textgaplevenshtein_of#:#Vzdálenost Levenshtein %s +qsts#:#confirm_delete_questions#:#Jste si jist(a), že chcete smazat následující otázky? +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Mezera +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Spodní hranice +qsts#:#range_upper_limit#:#Horní hranice +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Vybírací mezera +qsts#:#shuffle_answers#:#Zamíchat odpovědi +qsts#:#suggested_learning_content#:#Přidat doporučené řešení rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok otázek survey#:#questionblock_inserted#:#Blok otázek vložen survey#:#questionblocks#:#Bloky otázek survey#:#questionblocks_inserted#:#Bloky otázek vloženy -survey#:#questions#:#Otázky survey#:#questions_inserted#:#Otázky vloženy survey#:#questions_removed#:#Otázky a/nebo bloky otázek odstraněny! survey#:#questiontype#:#Typ otázky @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Otázky survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_sl.lang b/lang/ilias_sl.lang index f28ff5c9c8e3..4031825ec3e8 100644 --- a/lang/ilias_sl.lang +++ b/lang/ilias_sl.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing assessment#:#activate_logging#:#Aktiviraj protokol testov in ocen assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Vsebine za ponavljanje assessment#:#add_answers#:#Dodaj odgovore assessment#:#add_circle#:#Dodaj krog assessment#:#add_gap#:#Dodaj besedilo s prazninami @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Dobili ste točke za svojo re assessment#:#answer_is_right#:#Vaša rešitev je pravilna. assessment#:#answer_is_wrong#:#Vaša rešitev je napačna. assessment#:#answer_of#:#Answer of -assessment#:#answer_options#:#Možnosti odgovora: assessment#:#answer_question#:#Odgovori na vprašanje assessment#:#answer_text#:#Besedilo odgovora assessment#:#answer_types#:#Urednik odgovorov @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Obstoj z oddajo assessment#:#ass_completion_by_submission_info#:#Če je to aktivirano, oddaja datoteke z rešitvijo povzroči največjo oceno za to vprašanje. Oceno lahko kadar koli ročno prilagodite. Sprememba te nastavitve naknadno ne vpliva na že oddane rešitve. assessment#:#ass_create_export_file_with_results#:#Ustvari datoteko za izvoz (vključno z rezultati udeležencev) assessment#:#ass_create_export_test_archive#:#Ustvari arhivsko datoteko za test -assessment#:#ass_create_question#:#Ustvari vprašanje assessment#:#ass_imap_hint#:#Opomba (prikaže se kot opis orodja) assessment#:#ass_imap_map_file_not_readable#:#Naloženega slikovnega zemljevida ni mogoče prebrati. assessment#:#ass_imap_no_map_found#:#V naloženem Imapemap ni bila najdena nobena podprta oblika. @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Dolžina besedilnega polja assessment#:#cloze_fixed_textlength_description#:#Če tukaj vnesete vrednost, se v besedilu ustvarijo praznine, ki ne določajo lastne vrednosti za maksimalno dolžino, kot tudi praznine za številčni odgovor s to dolžino, zato ni mogoče vnesti več znakov, kot je dovoljeno. Pri prazninah za številčni odgovor je poleg tega treba upoštevati, da se pri štetju upoštevajo tudi decimalna ločila. assessment#:#cloze_gap_size_info#:#Če je vnesena vrednost večja od 0, se ta praznina ustvari z dolžino, ki je tukaj navedena. Če ni vnesena nobena vrednost, se ta praznina ustvari z globalno določeno dolžino besedilnega polja. -assessment#:#cloze_text#:#Vprašanje s praznino v besedilu -assessment#:#cloze_textgap_case_insensitive#:#Ni razlikovanja med velikimi in malimi črkami -assessment#:#cloze_textgap_case_sensitive#:#Upoštevaj razlikovanje velikih in malih črk -assessment#:#cloze_textgap_levenshtein_of#:#Levenshteinov razmik v %s assessment#:#code#:#Koda assessment#:#codebase#:#Osnova kode assessment#:#concatenation#:#Povezava @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#Dovoljena je uporaba že definiranih spremenljivk assessment#:#fq_no_restriction_info#:#Kot vnos se sprejmejo tako decimalne številke kot ulomki. assessment#:#fq_precision_info#:#Tukaj vnesite želeno število decimalnih mest. assessment#:#fq_question_desc#:#Spremenljivke lahko definirate tako, da vstavite $ v1, $ v2 ... $vn, rezultate pa tako, da vstavite $ r1, $ r2 .... $ rn na želeno mesto v besedilu. Nato kliknite na gumb "Analiziraj vprašanje", da ustvarite obrazce za obdelavo spremenljivk in rezultatov. -assessment#:#gap#:#Praznina assessment#:#gap_combination#:#Kombinacija besedila s prazninami -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Pojem iz glosarja assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Vaša ocena je "[mark]""; @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Vprašanje že vsebuje slike. Tipa odgovo assessment#:#info_text_upload#:#Izberite besedilno datoteko (UTF-8) z odgovori, ki jih želite naložiti. assessment#:#insert_after#:#Vstavi za assessment#:#insert_before#:#Vstavi pred -assessment#:#insert_gap#:#Vstavi praznino assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Notranje opombe assessment#:#intprecision#:#Deljivo z @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Udeleženec je vnesel napačno assessment#:#longmenu#:#Longmenu assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Besedilo ‘Long Menu’ -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Vzdrževanje assessment#:#manscoring#:#Ročno ocenjevanje assessment#:#manscoring_done#:#Udeleženci, ki so že bili ocenjeni @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Dosegli ste maksimalno dovoljeno šte assessment#:#maximum_points#:#Maksimalno možno število točk assessment#:#maxsize#:#Maksimalna velikost datoteke assessment#:#maxsize_info#:#Navedite maksimalno velikost v bitih, ki jo lahko ima datoteka, ki se nalaga. Če pustite to polje prazno, bo uporabljena nastavitev osnovnega sistema. -assessment#:#min_auto_complete#:#Samodejno dopolnjevanje assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Določiti morate minimalni odstotek v vrednosti 0, da se lahko upoštevajo vse dosežene točke. Shema ocenjevanja ni shranjena! assessment#:#misc#:#Različne možnosti @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Dodan je krog -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Število izrazov mora biti večje ali enako številu definicij. assessment#:#msg_poly_added#:#Dodan je poligon assessment#:#msg_questions_moved#:#Vprašanje/-a je/so premaknjeno/-a. @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Na tem mestu definirano zaporedje odgovorov se uporabi kot pravilen vrstni red rešitev. assessment#:#ordertext#:#Besedilo za razporeditev assessment#:#ordertext_info#:#Vnesite besedilo v takšnem zaporedju, kot naj bo razvrščeno horizontalno. Posamezne sestavine so ločene s presledki. Če potrebujete drugačno ločitev, namesto presledkov uporabite ločilo %s. -assessment#:#out_of_range#:#Izven področja assessment#:#output#:#Izdaja assessment#:#output_mode#:#Način izdaje assessment#:#parseQuestion#:#Analiziraj vprašanje @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Prekini -assessment#:#qpl_confirm_delete_questions#:#Ali ste prepričani, da želite odstraniti naslednja vprašanja? assessment#:#qpl_copy_insert_clipboard#:#Izbrano/-a vprašanje/-a se kopira/-jo v odložišče assessment#:#qpl_copy_select_none#:#Izberite vsaj eno vprašanje za kopiranje v odložišče assessment#:#qpl_delete_rbac_error#:#Nimate pravice za odstranitev tega vprašanja! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetenca assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Skupno število kompetenčnih točk na kompetenco assessment#:#qpl_question_is_in_use#:#Vprašanje, ki ga zdaj želite obdelati, že obstaja v %st testu/-ih. Če zdaj spremenite to vprašanje, to NE bo vplivalo na vprašanja, ki so že vključena v testih, ker sistem samodejno naredi kopijo vprašanja, ko je vključeno v test! assessment#:#qpl_questions_deleted#:#Vprašanje/-a izbrisano/-a -assessment#:#qpl_reset_preview#:#Ponastavite predogled assessment#:#qpl_save_skill_assigns_update#:#Shranite dodelitev kompetenc assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Obstoječe taksonomije se lahko uporabijo za filtriranje vprašanj. @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters assessment#:#qst_essay_wordcounter_enabled#:#Count Words assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field. assessment#:#qst_essay_written_words#:#Number of entered words -assessment#:#qst_lifecycle#:#Lifecycle -assessment#:#qst_lifecycle_draft#:#Draft -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles -assessment#:#qst_lifecycle_final#:#Final -assessment#:#qst_lifecycle_outdated#:#Outdated -assessment#:#qst_lifecycle_rejected#:#Rejected -assessment#:#qst_lifecycle_review#:#To be Reviewed -assessment#:#qst_lifecycle_sharable#:#Sharable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###19 08 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###19 08 2022 new variable assessment#:#qst_nr_of_tries#:#Število poskusov @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Naslov vprašanja assessment#:#question_type#:#Tip vprašanja assessment#:#questionpool_not_entered#:#Vnesite ime za skupino vprašanj! assessment#:#questionpool_not_selected#:#Please select a question pool! -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#Vprašanja iz assessment#:#questions_per_page_view#:#Prikaz strani assessment#:#random_accept_sample#:#Sprejmite seznam assessment#:#random_another_sample#:#Nov seznam assessment#:#random_selection#:#Naključna izbira assessment#:#range#:#Področje -assessment#:#range_lower_limit#:#Spodnja meja assessment#:#range_max#:#Področje (maksimalno) assessment#:#range_min#:#Področje (minimalno) -assessment#:#range_upper_limit#:#Zgornja meja assessment#:#rated_sign#:#Predznak assessment#:#rated_unit#:#Enota assessment#:#rated_value#:#Vrednost @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#po vlogah assessment#:#search_term#:#Iskani pojem assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Izbrana praznina assessment#:#select_max_one_item#:#Izberite samo en objekt assessment#:#select_one_user#:#Izberite vsaj enega uporabnika. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Pregled rezultatov (ocenjeno opravljanje testa) assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Odgovori (ocenjeno opravljanje testa) -assessment#:#shuffle_answers#:#Pomešaj odgovore assessment#:#skip_question#:#Ne odgovarjaj in naprej assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Besedilo @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback qpl#:#qpl_page_type_qfbs#:#Special Feedback qpl#:#qpl_page_type_qht#:#Hint qpl#:#qpl_page_type_qpl#:#Question Page +qsts#:#answer_options#:#Možnosti odgovora +qsts#:#cloze_text#:#Vprašanje s praznino v besedilu +qsts#:#cloze_textgapcase_insensitive#:#Ni razlikovanja med velikimi in malimi črkami +qsts#:#cloze_textgapcase_sensitive#:#Upoštevaj razlikovanje velikih in malih črk +qsts#:#cloze_textgaplevenshtein_of#:#Levenshteinov razmik v %s +qsts#:#confirm_delete_questions#:#Ali ste prepričani, da želite odstraniti naslednja vprašanja? +qsts#:#create_question#:#Ustvari vprašanje +qsts#:#gap#:#Praznina +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Vstavi praznino +qsts#:#min_auto_complete#:#Samodejno dopolnjevanje +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Izven področja +qsts#:#qst_lifecycle#:#Lifecycle +qsts#:#qst_lifecycle_draft#:#Draft +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles +qsts#:#qst_lifecycle_final#:#Final +qsts#:#qst_lifecycle_outdated#:#Outdated +qsts#:#qst_lifecycle_rejected#:#Rejected +qsts#:#qst_lifecycle_review#:#To be Reviewed +qsts#:#qst_lifecycle_sharable#:#Sharable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Spodnja meja +qsts#:#range_upper_limit#:#Zgornja meja +qsts#:#reset_preview#:#Ponastavite predogled +qsts#:#select_gap#:#Izbrana praznina +qsts#:#shuffle_answers#:#Pomešaj odgovore +qsts#:#suggested_learning_content#:#Vsebine za ponavljanje rating#:#rat_not_rated_yet#:#Not Rated Yet rating#:#rat_nr_ratings#:#%s Ratings rating#:#rat_one_rating#:#One Rating @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok vprašanj survey#:#questionblock_inserted#:#Blok vprašanj je vstavljen survey#:#questionblocks#:#Bloki vprašanj survey#:#questionblocks_inserted#:#Bloki vprašanj so vstavljeni -survey#:#questions#:#Vprašanja survey#:#questions_inserted#:#Vstavljeno/-a vprašanje/-a! survey#:#questions_removed#:#Vprašanje/-a in/ali bloki vprašanj je bilo/so bili odstranjeno/-i! survey#:#questiontype#:#Tip vprašanja @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Skrij oznake survey#:#svy_print_show_labels#:#Prikaži oznake survey#:#svy_privacy_info#:#Privacy###19 08 2022 new variable +survey#:#svy_questions#:#Vprašanja survey#:#svy_rater#:#Rater###19 08 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###19 08 2022 new variable survey#:#svy_reminder_mail_template#:#Predloga za e-pošto diff --git a/lang/ilias_sq.lang b/lang/ilias_sq.lang index 4ac3b6b07264..4227dbadd080 100644 --- a/lang/ilias_sq.lang +++ b/lang/ilias_sq.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Aktivizo testin&Ruaj aktivitetin e vlerësimit assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Shto zbrazëtirë për tekst @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Zgjidhja juaj është e drejtë###28 Jul 2006 content changed assessment#:#answer_is_wrong#:#Zgjidhja juaj është e padrejtë assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Përgjigju tekstit assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Mbyll tekstin###20 Jun 2005 content changed -assessment#:#cloze_textgap_case_insensitive#:#Case insensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_case_sensitive#:#Case sensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable assessment#:#code#:#Kod assessment#:#codebase#:#Codebase###10 Jul 2006 new variable assessment#:#concatenation#:#Concatenation ### 2006-8-11 --- Add new translation here. @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Zbrazëtirë assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Term nga fjalorthi assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Vendos pas assessment#:#insert_before#:#Vendos para -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Vendos linqe assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Mirëmbajtje assessment#:#manscoring#:#Manual Scoring###12 11 2006 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Keni arritur numrin maksimal të tent assessment#:#maximum_points#:#Maxium available points###10 Jul 2006 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Duhet të definoni minimumin e përqindjes të 0 përqind! Skema e markuar nuk është ruajtur.###10 Jul 2006 content changed assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 10 2006 new variable assessment#:#output_mode#:#Output mode###10 Jul 2006 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#A jeni të sigurt për të fshirë pyetjen/pyetjet ? Nëse i fshini pyetjet e mbyllura po ashtu do të fshihen edhe rezultatet e testeve që i përmbajn ato pyetje.###10 Jul 2006 content changed assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard###03 Nov 2005 new variable assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard###03 Nov 2005 new variable assessment#:#qpl_delete_rbac_error#:#Nuk keni të drejta për të fshirë këtë pyetje! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Pyetjen që jeni duke e edituar ekziston në %s test(e). Nëse e ndërroni këtë pyetje, nuk do të ndërroni pyetjen/pyetjet në testin/testet, për shkak se sistemi krijon një kopje të pyetjes kur ajo futet në test! assessment#:#qpl_questions_deleted#:#Pyetja/pyetjet janë fshirë -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Titulli i pyetjes assessment#:#question_type#:#Lloji i pyetjes assessment#:#questionpool_not_entered#:#Ju lutem të emërtoni fondin e pyetjeve! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#formë e pyetjeve assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Prano shembullin assessment#:#random_another_sample#:#Merr një shembull tjetër assessment#:#random_selection#:#Përzgjedhje sipas rastit assessment#:#range#:#Range###22 Feb 2006 new variable -assessment#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Search Roles ### 2006-8-11 --- Add new translation h assessment#:#search_term#:#Search Term ### 2006-8-11 --- Add new translation here. assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Zgjedh zbrazëtirë assessment#:#select_max_one_item#:#Ju lutem të zgjedhni vetëm një artikull assessment#:#select_one_user#:#Please select at least one user###23 Dec 2005 new variable assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Përziej përgjigjet assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Mbyll tekstin###20 Jun 2005 content changed +qsts#:#cloze_textgapcase_insensitive#:#Case insensitive###03 Nov 2005 new variable +qsts#:#cloze_textgapcase_sensitive#:#Case sensitive###03 Nov 2005 new variable +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable +qsts#:#confirm_delete_questions#:#A jeni të sigurt për të fshirë pyetjen/pyetjet ? Nëse i fshini pyetjet e mbyllura po ashtu do të fshihen edhe rezultatet e testeve që i përmbajn ato pyetje.###10 Jul 2006 content changed +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Zbrazëtirë +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable +qsts#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Zgjedh zbrazëtirë +qsts#:#shuffle_answers#:#Përziej përgjigjet +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Grupi i pyetjeve survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Grupet e pyetjeve survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Pyetjet survey#:#questions_inserted#:#Pyetjet janë vendosur! survey#:#questions_removed#:#Pyetjet dhe/ose grupet e pyetjeve janë fshirë! survey#:#questiontype#:#Tipi i pyetjes @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Pyetjet survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_sr.lang b/lang/ilias_sr.lang index 07aba9eaa1af..90c1ffe1245d 100644 --- a/lang/ilias_sr.lang +++ b/lang/ilias_sr.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Aktiviraj test i proveru prijave assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#dodajte razmak u tekstu @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Vaše rešenje je ispravno###28 Jul 2006 content changed assessment#:#answer_is_wrong#:#Vaše rešenje je pogrešno assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Tekst odgovora assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Zatvori tekst###20 Jun 2005 content changed -assessment#:#cloze_textgap_case_insensitive#:#Case insensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_case_sensitive#:#Case sensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable assessment#:#code#:#Kod assessment#:#codebase#:#Codebase###10 Jul 2006 new variable assessment#:#concatenation#:#Concatenation ### 2006-8-11 --- Add new translation here. @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Razmak assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Recnik termina assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Ubacite posle assessment#:#insert_before#:#Ubacite pre -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Unutrašnje veze assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Održavanje assessment#:#manscoring#:#Manual Scoring###12 11 2006 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Dostigli ste maksimalan broj pokušaj assessment#:#maximum_points#:#Maxium available points###10 Jul 2006 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Morate definisati minimalan procenat od 0 procenata! Šema ocena nije sacuvana.###10 Jul 2006 content changed assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 10 2006 new variable assessment#:#output_mode#:#Output mode###10 Jul 2006 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Da li ste sigurni da želite da obrišete sledece(a) pitanje(a)? Ukoliko obrišete zakljucana pitanja, rezultati svih testova koji sadrže zakljucana pitanja bice takode obrisani.###10 Jul 2006 content changed assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard###03 Nov 2005 new variable assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard###03 Nov 2005 new variable assessment#:#qpl_delete_rbac_error#:#Nemate prava da obrišete ovo pitanje! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Pitanje koje nameravate da izmenite nalazi se u %s testova. Ukoliko promenite ovo pitanje, NECETE promeniti pitanje(a) u testu/testovima, zato što sistem pravi kopije pitanja kada su una ubacena u test! assessment#:#qpl_questions_deleted#:#Pitanje(a) obrisano(a). -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Naziv pitanja assessment#:#question_type#:#Vrsta pitanja assessment#:#questionpool_not_entered#:#Molimo unesite naziv bazu pitanja! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#pitanje iz assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Prihvati primer assessment#:#random_another_sample#:#Uzmi drugi primer assessment#:#random_selection#:#Slucajni izbor assessment#:#range#:#Range###22 Feb 2006 new variable -assessment#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Search Roles ### 2006-8-11 --- Add new translation h assessment#:#search_term#:#Search Term ### 2006-8-11 --- Add new translation here. assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Odaberite razmak assessment#:#select_max_one_item#:#Molimo odaberite samo jedno polje assessment#:#select_one_user#:#Please select at least one user###23 Dec 2005 new variable assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Izbegni odgovore assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Zatvori tekst###20 Jun 2005 content changed +qsts#:#cloze_textgapcase_insensitive#:#Case insensitive###03 Nov 2005 new variable +qsts#:#cloze_textgapcase_sensitive#:#Case sensitive###03 Nov 2005 new variable +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable +qsts#:#confirm_delete_questions#:#Da li ste sigurni da želite da obrišete sledece(a) pitanje(a)? Ukoliko obrišete zakljucana pitanja, rezultati svih testova koji sadrže zakljucana pitanja bice takode obrisani.###10 Jul 2006 content changed +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Razmak +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower limit###22 Feb 2006 new variable +qsts#:#range_upper_limit#:#Upper limit###22 Feb 2006 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Odaberite razmak +qsts#:#shuffle_answers#:#Izbegni odgovore +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Blok pitanja survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Blokovi pitanja survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Pitanja survey#:#questions_inserted#:#Pitanja ubacena! survey#:#questions_removed#:#Pitanja i/ili blok pitanja je uklonjen! survey#:#questiontype#:#Vrsta pitanja @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Pitanja survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_sv.lang b/lang/ilias_sv.lang index 552360765072..173e0180d12f 100644 --- a/lang/ilias_sv.lang +++ b/lang/ilias_sv.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Aktivera TinyMCE för WYSIWYG-redigering assessment#:#activate_logging#:#Aktivera loggning av test och bedömning assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Innehåll för repetition assessment#:#add_answers#:#Lägg till svar assessment#:#add_circle#:#Lägg till cirkel assessment#:#add_gap#:#Lägg till cloze @@ -501,7 +500,6 @@ assessment#:#ass_completion_by_submission#:#Passera genom att lämna in assessment#:#ass_completion_by_submission_info#:#Om den är aktiverad leder inlämnandet av en lösningsfil till att maximalt antal poäng tilldelas för denna fråga. Poängen kan justeras manuellt när som helst. Ändring av denna inställning har ingen efterföljande effekt på redan inlämnade lösningar. assessment#:#ass_create_export_file_with_results#:#Skapa exportfil (inkl. deltagarresultat) assessment#:#ass_create_export_test_archive#:#Skapa arkivfil för test -assessment#:#ass_create_question#:#Skapa fråga assessment#:#ass_imap_hint#:#Note (visas som verktygstips) assessment#:#ass_imap_map_file_not_readable#:#Den uppladdade bildkartan kan inte läsas. assessment#:#ass_imap_no_map_found#:#Ingen stödd form kunde hittas i den uppladdade bildkartan. @@ -571,10 +569,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Längd på textfältet assessment#:#cloze_fixed_textlength_description#:#Om du anger ett värde här skapas textgap som inte definierar ett eget värde för en maximal längd, samt numeriska gap med denna längd, så att det inte är möjligt att ange ett större antal tecken. För numeriska luckor, observera att decimalavgränsaren ocks assessment#:#cloze_gap_size_info#:#Om ett värde större än 0 anges skapas detta mellanrum med den längd som anges här. Om inget värde anges skapas detta mellanrum med den globalt angivna textfältslängden. -assessment#:#cloze_text#:#Cloze fråga -assessment#:#cloze_textgap_case_insensitive#:#Ingen skillnad görs mellan stora och små bokstäver -assessment#:#cloze_textgap_case_sensitive#:#Känsliga gemener -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein avstånd från %s assessment#:#code#:#Kod assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Länk @@ -757,9 +751,7 @@ assessment#:#fq_formula_desc#:#Tillåtet är användning av redan definierade va assessment#:#fq_no_restriction_info#:#Både decimaltal och bråktal accepteras som indata. assessment#:#fq_precision_info#:#Ange det antal decimaler du vill ha här. assessment#:#fq_question_desc#:#Du definierar variabler genom att ange $v1, $v2 ... $vn, resultatfält med $r1, $r2 .... $rn på önskade positioner i texten. Klicka sedan på knappen "Analysera fråga" för att generera redigeringsformulär för alla variabler och resultat. -assessment#:#gap#:#Gap assessment#:#gap_combination#:#Cloze kombination -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Glossarisk term assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Du har uppnått märket "[märke]". @@ -777,7 +769,6 @@ assessment#:#info_answer_type_change#:#Frågan innehåller redan bilder. Fråget assessment#:#info_text_upload#:#Välj en textfil (UTF-8) med svar som ska laddas upp. assessment#:#insert_after#:#Insätt bakom assessment#:#insert_before#:#Infoga före -assessment#:#insert_gap#:#Insert gap assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Interna referenser assessment#:#intprecision#:#Kan delas av @@ -889,7 +880,6 @@ assessment#:#logs_wrong_test_password_provided#:#Deltagaren har angett fel testl assessment#:#longmenu#:#Lång meny assessment#:#longmenu_answeroptions_differ#:#Den här frågan fungerar inte korrekt eftersom den inte har samma antal luckor i texten som i svarsalternativen. assessment#:#longmenu_text#:#"Lång meny" text -assessment#:#mainbar_button_label_questionlist#:#Questionlist###21 11 2023 new variable assessment#:#maintenance#:#Underhåll assessment#:#manscoring#:#Manuell bedömning assessment#:#manscoring_done#:#Redan betygsatta deltagare @@ -916,7 +906,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Du har förbrukat det maximala antale assessment#:#maximum_points#:#Maximalt uppnåelig poäng assessment#:#maxsize#:#Maximal filstorlek assessment#:#maxsize_info#:#Anger den maximala storleken i bytes som en uppladdad fil får ha. Om du lämnar fältet tomt används inställningen för det underliggande systemet. -assessment#:#min_auto_complete#:#Autokomplett assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Du måste ange en lägsta procentsats på 0 procent. Betygsschemat har inte sparats. assessment#:#misc#:#Olika alternativ @@ -925,7 +914,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle tillagd -assessment#:#msg_no_questions_selected#:#No questions were selected.###21 11 2023 new variable assessment#:#msg_number_of_terms_too_low#:#Antalet termer måste vara större än eller lika med antalet definitioner. assessment#:#msg_poly_added#:#Polygon tillagd assessment#:#msg_questions_moved#:#Frågeställningar uppskjutna @@ -984,7 +972,6 @@ assessment#:#order#:#Sortering assessment#:#ordering_answer_sequence_info#:#Den svarsordning som anges här används som korrekt lösningsordning. assessment#:#ordertext#:#Texten ska ordnas assessment#:#ordertext_info#:#Var vänlig ange texten i den ordning som den ska placeras horisontellt. De enskilda komponenterna separeras med mellanslag. Om du vill ha en annan avgränsning kan du använda avgränsaren %s istället för mellanslag. -assessment#:#out_of_range#:#Utanför intervall assessment#:#output#:#Utmatning assessment#:#output_mode#:#Utmatningsläge assessment#:#parseQuestion#:#Analysera frågan @@ -1053,7 +1040,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Avbryt -assessment#:#qpl_confirm_delete_questions#:#Är du säker på att du vill ta bort följande frågor? assessment#:#qpl_copy_insert_clipboard#:#De valda frågorna har kopierats till Urklipp assessment#:#qpl_copy_select_none#:#Välj minst en fråga för att kopiera den till urklipp! assessment#:#qpl_delete_rbac_error#:#Du har inte tillstånd att ta bort denna fråga! @@ -1106,7 +1092,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Kompetens assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Summa av alla kompetenspoäng per kompetens assessment#:#qpl_question_is_in_use#:#Frågan du vill redigera nu finns redan i %s test(s). Om du ändrar den här frågan nu kommer det INTE att påverka frågor som redan ingår i test, eftersom systemet automatiskt skapar en kopia av frågan när den ingår i ett test! assessment#:#qpl_questions_deleted#:#Frågor borttagna -assessment#:#qpl_reset_preview#:#Återställ förhandsgranskning assessment#:#qpl_save_skill_assigns_update#:#Spara kompetensuppdrag assessment#:#qpl_settings_availability#:#Availability###21 11 2023 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#Existerande taxonomier kan användas för att filtrera frågorna. @@ -1136,14 +1121,6 @@ assessment#:#qst_essay_chars_remaining#:#Andra tecken assessment#:#qst_essay_wordcounter_enabled#:#Räkna ord assessment#:#qst_essay_wordcounter_enabled_info#:#De inmatade orden räknas. Antalet ord visas för deltagarna under textinmatningsfältet. assessment#:#qst_essay_written_words#:#Antal inmatade ord -assessment#:#qst_lifecycle#:#Livscykel -assessment#:#qst_lifecycle_draft#:#Draft -assessment#:#qst_lifecycle_filter_all#:#Alla livscykler -assessment#:#qst_lifecycle_final#:#Final -assessment#:#qst_lifecycle_outdated#:#Veraltet -assessment#:#qst_lifecycle_rejected#:#Avvisad -assessment#:#qst_lifecycle_review#:#Revision nödvändig -assessment#:#qst_lifecycle_sharable#:#Distribuerbar assessment#:#qst_nested_nested_answers_off#:# Utan indragning assessment#:#qst_nested_nested_answers_on#:#Med indrag assessment#:#qst_nr_of_tries#:#Antal försök @@ -1167,17 +1144,14 @@ assessment#:#question_title#:#Frågans titel assessment#:#question_type#:#Typ av fråga assessment#:#questionpool_not_entered#:#Var vänlig ange ett namn för frågepoolen! assessment#:#questionpool_not_selected#:#Välkomna att välja en frågepool! -assessment#:#questions#:#Questions###21 11 2023 new variable assessment#:#questions_from#:#Frågor från assessment#:#questions_per_page_view#:#Sidvisning assessment#:#random_accept_sample#:#Acceptera sammanställning assessment#:#random_another_sample#:#Ny sammanställning assessment#:#random_selection#:#Slumpmässigt urval assessment#:#range#:#Område -assessment#:#range_lower_limit#:#Lägre barriär assessment#:#range_max#:#Range (maximalt) assessment#:#range_min#:#Range (Minimum) -assessment#:#range_upper_limit#:#Övre barriär assessment#:#rated_sign#:#Omens assessment#:#rated_unit#:#Enhet assessment#:#rated_value#:#Värde @@ -1253,7 +1227,6 @@ assessment#:#search_roles#:#Efter rullar assessment#:#search_term#:#Sökord assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Selektionsgap assessment#:#select_max_one_item#:#Välj endast ett objekt! assessment#:#select_one_user#:#Välj minst en användare! assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1253,6 @@ assessment#:#show_old_introduction#:#Show old introduction###21 11 2023 new vari assessment#:#show_pass_overview#:#Resultatöversikt (poängsatt provkörning) assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Answers (poängsatt testkörning) -assessment#:#shuffle_answers#:#Svar från Shuffle assessment#:#skip_question#:#Svara inte och fortsätt assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text @@ -13971,6 +13943,34 @@ qpl#:#qpl_page_type_qfbg#:#Allmän feedback qpl#:#qpl_page_type_qfbs#:#Särskild feedback qpl#:#qpl_page_type_qht#:#Note qpl#:#qpl_page_type_qpl#:#Frågesida +qsts#:#cloze_text#:#Cloze fråga +qsts#:#cloze_textgapcase_insensitive#:#Ingen skillnad görs mellan stora och små bokstäver +qsts#:#cloze_textgapcase_sensitive#:#Känsliga gemener +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein avstånd från %s +qsts#:#confirm_delete_questions#:#Är du säker på att du vill ta bort följande frågor? +qsts#:#create_question#:#Skapa fråga +qsts#:#gap#:#Gap +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert gap +qsts#:#min_auto_complete#:#Autokomplett +qsts#:#msg_no_questions_selected#:#No questions were selected.###21 11 2023 new variable +qsts#:#out_of_range#:#Utanför intervall +qsts#:#qst_lifecycle#:#Livscykel +qsts#:#qst_lifecycle_draft#:#Draft +qsts#:#qst_lifecycle_filter_all#:#Alla livscykler +qsts#:#qst_lifecycle_final#:#Final +qsts#:#qst_lifecycle_outdated#:#Veraltet +qsts#:#qst_lifecycle_rejected#:#Avvisad +qsts#:#qst_lifecycle_review#:#Revision nödvändig +qsts#:#qst_lifecycle_sharable#:#Distribuerbar +qsts#:#questionlist#:#Questionlist###21 11 2023 new variable +qsts#:#questions#:#Questions###21 11 2023 new variable +qsts#:#range_lower_limit#:#Lägre barriär +qsts#:#range_upper_limit#:#Övre barriär +qsts#:#reset_preview#:#Återställ förhandsgranskning +qsts#:#select_gap#:#Selektionsgap +qsts#:#shuffle_answers#:#Svar från Shuffle +qsts#:#suggested_learning_content#:#Innehåll för repetition rating#:#rat_not_rated_yet#:#Not yet rated rating#:#rat_nr_ratings#:#%s betyg rating#:#rat_one_rating#:#En bedömning @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Frågeställning block survey#:#questionblock_inserted#:#Frågeblock infört survey#:#questionblocks#:#Frågeblock survey#:#questionblocks_inserted#:#Frågeblock infogade -survey#:#questions#:#Frågor och svar survey#:#questions_inserted#:#Frågor tillagda survey#:#questions_removed#:#Frågor och/eller frågeblock borttagna! survey#:#questiontype#:#Typ av fråga @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Välj minst en oanvänd åtkomstnyckel survey#:#svy_print_hide_labels#:#Dölj Etiketter survey#:#svy_print_show_labels#:#Visa Märkningar survey#:#svy_privacy_info#:#Personuppgifter +survey#:#svy_questions#:#Frågor och svar survey#:#svy_rater#:#Feedback givare survey#:#svy_rater_see_app_info#:#feedbackgivare, namnen på de som tar emot feedback visas så att de kan svara på frågorna i förhållande till den som tar emot feedback. survey#:#svy_reminder_mail_template#:#Mall för e-post diff --git a/lang/ilias_tr.lang b/lang/ilias_tr.lang index c630f5961f1a..6419a607890c 100644 --- a/lang/ilias_tr.lang +++ b/lang/ilias_tr.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#WYSIWYG düzenleme için TinyMCE kullan assessment#:#activate_logging#:#Test ve Değerlendirme Günlüğünü Aktif Yap assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Önerilen çözüm ekle assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Daire alanı ekle assessment#:#add_gap#:#Boşluk Metni Ekle @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#Çözümünüz için puan ald assessment#:#answer_is_right#:#Çözümünüz doğru assessment#:#answer_is_wrong#:#Çözümünüz yanlış assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Yanıt Metin assessment#:#answer_types#:#Yanıt Türleri @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Başvuru ile Tamamlandı assessment#:#ass_completion_by_submission_info#:#Etkinse, bu soru için en az bir dosya gönderme maksimum puan alma için yeterlidir. Puan daha sonra elle değiştirilebilir. Bu ayarı değiştirme önceden gönderilen sorulara etki etmez assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Metin Alanı Uzunluğu assessment#:#cloze_fixed_textlength_description#:#Eğer 0'dan büyük bir değer girerseniz , tüm metin ve sayısal alan boşluklarının karakter uzunluğu bu değer olacaktır. assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Boşluklu Metin -assessment#:#cloze_textgap_case_insensitive#:#Büyük/Küçük Harf Duyarsız -assessment#:#cloze_textgap_case_sensitive#:#Büyük/Küçük Harf Duyarlı -assessment#:#cloze_textgap_levenshtein_of#:#%S in Levenshtein Uzaklığı assessment#:#code#:#Kod assessment#:#codebase#:#Codebase assessment#:#concatenation#:#Birleştirme @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Boşluk assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Sözlük Terim assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#Soru zaten resimler içeriyor. Cevap tür assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Sonrasına Ekle assessment#:#insert_before#:#Öncesine Ekle -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Dahili Linkler assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Bakım assessment#:#manscoring#:#Manuel Puanlama assessment#:#manscoring_done#:#Puanlanan Katılımcılar @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Bu testi tamamladınız. assessment#:#maximum_points#:#Maksimum Kullanılabilir Puanlar assessment#:#maxsize#:#Maksimum dosya yükleme boyutu assessment#:#maxsize_info#:#Dosya yüklemeleri için izin verilen en büyük boyutu bayt cinsinden girin. Bu alanı boş bırakırsanız, bu yükleme maksimum boyut yerine geçicek. -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#%0 yerine minimum yüzde tanımlamalısınız. Not şeması kayıt edilmedi. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Daire eklendi -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#Terimler sayısı tanımlar sayısından büyük veya eşit olmalı assessment#:#msg_poly_added#:#Poligon eklendi assessment#:#msg_questions_moved#:#Soru(lar) taşındı @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#Burada tanımladığınız cevap sırası doğru çözüm sırası olarak alınacak assessment#:#ordertext#:#Metin Sıralama assessment#:#ordertext_info#:#Yatay sıralanabilen metni giriniz. Sıralama metni, metin içinde boşluk(whitespace) ile ayrılacaktır.. Eğer farklı bir ayraç kullanmak isterseniz, text birimlerini ayırmak için %s ayracını kullanabilirsiniz. -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Çıktı assessment#:#output_mode#:#Çıkış Modu assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Aşağıdaki soruları silmek istediğinizden emin misiniz? assessment#:#qpl_copy_insert_clipboard#:#Seçilen soru(lar) panoya kopyalandı assessment#:#qpl_copy_select_none#:#Panoya kopyalamak için en az bir soruyu seçiniz assessment#:#qpl_delete_rbac_error#:#Bu soruyu silmek için yetkiniz yok ! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Düzenlemek üzere olduğunuz soru test %s içinde var Eğer bu soruyu değiştirirseniz, test(ler) içindeki soruyu değiştiremezsiniz, çünkü sistem soru teste eklendiğinde bir kopyasını alır. assessment#:#qpl_questions_deleted#:#Soru(lar) silindi. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Deneme Sayısı @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Soru Başlığı assessment#:#question_type#:#Soru Türü assessment#:#questionpool_not_entered#:#Soru havuzu için bir ad girin! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#sorular assessment#:#questions_per_page_view#:#Sayfa Görünümü assessment#:#random_accept_sample#:#Örnek Kabul assessment#:#random_another_sample#:#Başka bir örnek alın assessment#:#random_selection#:#Rasgele Seçim assessment#:#range#:#Aralık -assessment#:#range_lower_limit#:#Alt Sınır assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Üst Sınır assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Rolleri Ara assessment#:#search_term#:#Terim Ara assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Boşluk Seç assessment#:#select_max_one_item#:#Yalnızca bir öğe seçiniz assessment#:#select_one_user#:#En az bir kullanıcı seçiniz. assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Geçti İşaretlilere Genel Bakışı Göster assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Kullanıcının işaretli cevaplarını göster -assessment#:#shuffle_answers#:#Cevapları Karıştır assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Metin @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Boşluklu Metin +qsts#:#cloze_textgapcase_insensitive#:#Büyük/Küçük Harf Duyarsız +qsts#:#cloze_textgapcase_sensitive#:#Büyük/Küçük Harf Duyarlı +qsts#:#cloze_textgaplevenshtein_of#:#%S in Levenshtein Uzaklığı +qsts#:#confirm_delete_questions#:#Aşağıdaki soruları silmek istediğinizden emin misiniz? +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Boşluk +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Alt Sınır +qsts#:#range_upper_limit#:#Üst Sınır +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Boşluk Seç +qsts#:#shuffle_answers#:#Cevapları Karıştır +qsts#:#suggested_learning_content#:#Önerilen çözüm ekle rating#:#rat_not_rated_yet#:#Henüz Oylanmadı rating#:#rat_nr_ratings#:#%s Puan rating#:#rat_one_rating#:#Bir Değerlendirme @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Soru Blok survey#:#questionblock_inserted#:#Soru Blok takılı survey#:#questionblocks#:#Soru Blokları survey#:#questionblocks_inserted#:#Takılı Soru Blokları -survey#:#questions#:#Sorular survey#:#questions_inserted#:#Soru (ler) eklenir! survey#:#questions_removed#:#Soru (ler) ve / veya söz konusu bloğun (ler) kaldırıldı! survey#:#questiontype#:#Soru Türü @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Sorular survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_uk.lang b/lang/ilias_uk.lang index ed5334ea9912..09f1856bd91e 100644 --- a/lang/ilias_uk.lang +++ b/lang/ilias_uk.lang @@ -422,7 +422,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Активувати логування Тестів & Оцінок assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Додати текст з проміжком @@ -447,7 +446,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Ваша відповідь вірна assessment#:#answer_is_wrong#:#Вага відповідь невірна assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Текст з відповідю assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -501,7 +499,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -571,10 +568,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Закрити текст###20 Jun 2005 content changed -assessment#:#cloze_textgap_case_insensitive#:#Case insensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_case_sensitive#:#Case sensitive###03 Nov 2005 new variable -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable assessment#:#code#:#Код assessment#:#codebase#:#Codebase###25 02 2007 new variable assessment#:#concatenation#:#Об'єднання @@ -757,9 +750,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Проміжок assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Термін глосарію assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -777,7 +768,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Вставити після assessment#:#insert_before#:#Вставити перед -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Внутрішні Посилання assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -889,7 +879,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Підтримка assessment#:#manscoring#:#Manual Scoring###25 02 2007 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -916,7 +905,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Ви досялги максимал assessment#:#maximum_points#:#Maxium Available Points###25 02 2007 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Ви маєете вказати мінімальне процентне співвідношення до нуля процентів! Схема оцінок не збережена. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -925,7 +913,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -984,7 +971,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 02 2007 new variable assessment#:#output_mode#:#Output Mode###25 02 2007 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1053,7 +1039,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Ви впевнені що бажаєте видалити наступні питання ? Якщо ви видалите заблоковані питання то результати усіх тестів що містять залоковані питання буде також видалено. assessment#:#qpl_copy_insert_clipboard#:#The selected question(s) are copied to the clipboard###03 Nov 2005 new variable assessment#:#qpl_copy_select_none#:#Please check at least one question to copy it to the clipboard###03 Nov 2005 new variable assessment#:#qpl_delete_rbac_error#:#У вас немає прав видалити це питання! @@ -1106,7 +1091,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Питання що ви збираєтесь редагувати існує в %s тесті(ах). Якщо ви зміните це питання, ви НЕ зміните питання в тесті(ах), тому що система створює копію питання коли воно вставляється в тест! assessment#:#qpl_questions_deleted#:#Питання виделено(і). -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1136,14 +1120,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1167,17 +1143,14 @@ assessment#:#question_title#:#Назва питання assessment#:#question_type#:#Тип Питання assessment#:#questionpool_not_entered#:#Будб ласка введіть ім'я для пула з питаннями! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#питання з assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Прийняти приклад assessment#:#random_another_sample#:#Взяти інший приклад assessment#:#random_selection#:#Випадковий вибір assessment#:#range#:#Range###25 02 2007 new variable -assessment#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1253,7 +1226,6 @@ assessment#:#search_roles#:#Пошук Ролей assessment#:#search_term#:#Пошук Термінів assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Вибрати проміжок assessment#:#select_max_one_item#:#Будь ласка виберіть тільки один пункт assessment#:#select_one_user#:#Please select at least one user###23 Dec 2005 new variable assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1280,7 +1252,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Перерозташувати відповіді assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13971,6 +13942,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Закрити текст###20 Jun 2005 content changed +qsts#:#cloze_textgapcase_insensitive#:#Case insensitive###03 Nov 2005 new variable +qsts#:#cloze_textgapcase_sensitive#:#Case sensitive###03 Nov 2005 new variable +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s###03 Nov 2005 new variable +qsts#:#confirm_delete_questions#:#Ви впевнені що бажаєте видалити наступні питання ? Якщо ви видалите заблоковані питання то результати усіх тестів що містять залоковані питання буде також видалено. +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Проміжок +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable +qsts#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Вибрати проміжок +qsts#:#shuffle_answers#:#Перерозташувати відповіді +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16620,7 +16620,6 @@ survey#:#questionblock#:#Блок питань survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Блоки питань survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Питання survey#:#questions_inserted#:#Питання вставлені! survey#:#questions_removed#:#Питання та/або блоки питань видалені! survey#:#questiontype#:#Тип питання @@ -16921,6 +16920,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Питання survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_vi.lang b/lang/ilias_vi.lang index 333e15b4fa3c..60c7391a064d 100644 --- a/lang/ilias_vi.lang +++ b/lang/ilias_vi.lang @@ -424,7 +424,6 @@ adve#:#adve_use_tiny_mce#:#Enable TinyMCE for WYSIWYG Editing###01 10 2011 new v assessment#:#activate_logging#:#Kích hoạt ghi nhật ký Kiểm tra và Đánh giá assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#Add suggested solution###24 02 2009 new variable assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#Add circle area###24 07 2009 new variable assessment#:#add_gap#:#Thêm từ @@ -449,7 +448,6 @@ assessment#:#answer_is_not_correct_but_positive#:#You've got points for your sol assessment#:#answer_is_right#:#Giải pháp của bạn đúng assessment#:#answer_is_wrong#:#Giải pháp của bạn sai assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#Phương án trả lời assessment#:#answer_types#:#Answer Types###24 07 2009 new variable @@ -503,7 +501,6 @@ assessment#:#ass_completion_by_submission#:#Completed by Submission###12 06 2011 assessment#:#ass_completion_by_submission_info#:#If enabled, the submission of at least one file causes the completion of this question by granting the maximum score for this question. The score could be manually changed later. Switching this setting does not effect already submitted solutions.###12 06 2011 new variable assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -573,10 +570,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#Text Field Length###02 04 2007 new variable assessment#:#cloze_fixed_textlength_description#:#If you enter a value greater than 0, all text and numeric gap text fields will be created with the fixed length of this value.###02 04 2007 new variable assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#Đoạn văn điền chỗ trống -assessment#:#cloze_textgap_case_insensitive#:#Không phân biệt chữ hoa chữ thường -assessment#:#cloze_textgap_case_sensitive#:#Phân biệt chữ hoa chữ thường -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s/// assessment#:#code#:#Mã assessment#:#codebase#:#Codebase###25 02 2007 new variable assessment#:#concatenation#:#Kết hợp @@ -759,9 +752,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#Chỗ trống assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#Bảng chú giải thuật ngữ assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -779,7 +770,6 @@ assessment#:#info_answer_type_change#:#The question already contains images. You assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#Chèn vào sau assessment#:#insert_before#:#Chèn vào trước -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#Liên kết nội bộ assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -891,7 +881,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Bảo quản - bảo trì assessment#:#manscoring#:#Manual Scoring###25 02 2007 new variable assessment#:#manscoring_done#:#Scored Participants###24 02 2009 new variable @@ -918,7 +907,6 @@ assessment#:#maximum_nr_of_tries_reached#:#Bạn đã hết số lần có thể assessment#:#maximum_points#:#Maxium Available Points###25 02 2007 new variable assessment#:#maxsize#:#Maximum file upload size ###24 02 2009 new variable assessment#:#maxsize_info#:#Enter the maximum size in bytes that should be allowed for file uploads. If you leave this field empty, the maximum size of this installation will be chosen instead.###24 02 2009 new variable -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#Bạn phải định nghĩa phần trăm tối thiếu là 0 phần trăm! Giản đồ điểm không được ghi. assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -927,7 +915,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#Circle added###24 07 2009 new variable -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#The number of terms must be greater or equal to the number of definitions.###12 08 2009 new variable assessment#:#msg_poly_added#:#Polygon added###24 07 2009 new variable assessment#:#msg_questions_moved#:#Question(s) moved###06 08 2009 new variable @@ -986,7 +973,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#The answer sequence you define here will be taken as the correct solution sequence.###10 09 2010 new variable assessment#:#ordertext#:#Ordering Text###24 02 2009 new variable assessment#:#ordertext_info#:#Please enter the text that should be ordered horizontally. The ordering text will be separated by the whitespace signs in the text. If you need a different separation, you may use the separator %s to separate your text units.###24 02 2009 new variable -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#Output###25 02 2007 new variable assessment#:#output_mode#:#Output Mode###25 02 2007 new variable assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1055,7 +1041,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#Bạn có chắc chắn muốn xóa (các) câu hỏi sau không? Nếu bạn xóa các câu hỏi đã khóa kết quả của tất cả bài kiểm tra chứa câu hỏi đã khóa sẽ bị xóa theo. assessment#:#qpl_copy_insert_clipboard#:#(Những) câu hỏi bạn chọn đã được sao chép vào bộ nhớ đệm assessment#:#qpl_copy_select_none#:#Hãy chọn ít nhất một câu hỏi để sao chép vào bộ nhớ đệm assessment#:#qpl_delete_rbac_error#:#Bạn không có quyền xóa câu hỏi này! @@ -1108,7 +1093,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#Câu hỏi bạn muốn sửa đang tồn tại trong %s bài kiểm tra. Nếu bạn thay đổi câu hỏi này, bạn sẽ không thay đổi những câu hỏi trong các bài kiểm tra, vì hệ thống tạo ra bản sao của câu hỏi khi chèn câu hỏi vào bài kiểm tra! assessment#:#qpl_questions_deleted#:#Đã xóa (các) câu hỏi. -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1138,14 +1122,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#Number of tries###15 05 2009 new variable @@ -1169,17 +1145,14 @@ assessment#:#question_title#:#Tiêu đề câu hỏi assessment#:#question_type#:#Kiểu câu hỏi assessment#:#questionpool_not_entered#:#Hãy nhập vào tên thư viện câu hỏi! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#câu hỏi từ assessment#:#questions_per_page_view#:#Page View###12 06 2011 new variable assessment#:#random_accept_sample#:#Chấp nhận mẫu assessment#:#random_another_sample#:#Lấy mẫu khác assessment#:#random_selection#:#Lựa chọn ngẫu nhiên assessment#:#range#:#Range###25 02 2007 new variable -assessment#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1255,7 +1228,6 @@ assessment#:#search_roles#:#Tìm kiếm vai trò assessment#:#search_term#:#Tìm kiếm thuật ngữ assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#Lựa chọn từ điền assessment#:#select_max_one_item#:#Hãy chọn chỉ một mục assessment#:#select_one_user#:#Hãy chọn ít nhất một người sử dụng assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1282,7 +1254,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#Show Marked Pass Overview###31 05 2007 new variable assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#Show User's Marked Anwers###31 05 2007 new variable -assessment#:#shuffle_answers#:#Xáo trộn các lựa chọn assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text###24 02 2009 new variable @@ -13973,6 +13944,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#Đoạn văn điền chỗ trống +qsts#:#cloze_textgapcase_insensitive#:#Không phân biệt chữ hoa chữ thường +qsts#:#cloze_textgapcase_sensitive#:#Phân biệt chữ hoa chữ thường +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s/// +qsts#:#confirm_delete_questions#:#Bạn có chắc chắn muốn xóa (các) câu hỏi sau không? Nếu bạn xóa các câu hỏi đã khóa kết quả của tất cả bài kiểm tra chứa câu hỏi đã khóa sẽ bị xóa theo. +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#Chỗ trống +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#Lower Bound###25 02 2007 new variable +qsts#:#range_upper_limit#:#Upper Bound###25 02 2007 new variable +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#Lựa chọn từ điền +qsts#:#shuffle_answers#:#Xáo trộn các lựa chọn +qsts#:#suggested_learning_content#:#Add suggested solution###24 02 2009 new variable rating#:#rat_not_rated_yet#:#Not Rated Yet###12 06 2011 new variable rating#:#rat_nr_ratings#:#%s Ratings###12 06 2011 new variable rating#:#rat_one_rating#:#One Rating###12 06 2011 new variable @@ -16622,7 +16622,6 @@ survey#:#questionblock#:#Khối câu hỏi survey#:#questionblock_inserted#:#Question Block inserted###06 08 2009 new variable survey#:#questionblocks#:#Khối câu hỏi survey#:#questionblocks_inserted#:#Question Blocks inserted###06 08 2009 new variable -survey#:#questions#:#Câu hỏi survey#:#questions_inserted#:#Đã thêm (các) câu hỏi! survey#:#questions_removed#:#Câu hỏi và/hoặc khối câu hỏi đã xóa! survey#:#questiontype#:#Kiểu câu hỏi @@ -16923,6 +16922,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#Câu hỏi survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable diff --git a/lang/ilias_zh.lang b/lang/ilias_zh.lang index dbdeaeaf4282..f906601c469c 100644 --- a/lang/ilias_zh.lang +++ b/lang/ilias_zh.lang @@ -421,7 +421,6 @@ adve#:#adve_use_tiny_mce#:#为所见即所得编辑启用TinyMCE。 assessment#:#activate_logging#:#激测试 & 评估日志 assessment#:#activate_manual_scoring#:#Enable Manual Scoring###28 10 2024 new variable assessment#:#activate_manual_scoring_desc#:#Enables manual Scoring for all question types.###28 10 2024 new variable -assessment#:#addSuggestedSolution#:#添加建议解决方案 assessment#:#add_answers#:#Add Antworten ###23 12 2015 new variable assessment#:#add_circle#:#添加圆区域 assessment#:#add_gap#:#添加填空文字 @@ -446,7 +445,6 @@ assessment#:#answer_is_not_correct_but_positive#:#您已经获取了解决方案 assessment#:#answer_is_right#:#回答正确 assessment#:#answer_is_wrong#:#回答错误 assessment#:#answer_of#:#Answer of###07 02 2020 new variable -assessment#:#answer_options#:#Answer Options: ###23 12 2015 new variable assessment#:#answer_question#:#Answer Question###30 08 2015 new variable assessment#:#answer_text#:#答案文本 assessment#:#answer_types#:#答案类型 @@ -500,7 +498,6 @@ assessment#:#ass_completion_by_submission#:#提交已完成 assessment#:#ass_completion_by_submission_info#:#如果启用,至少一个文件因为通过为这个题目授予最高分完成这个题目而提交。这个分数以后可以手工修改。切换这个设置不影响已提交的方案。 assessment#:#ass_create_export_file_with_results#:#Create Test Export File (incl. Participant Results)###25 10 2016 new variable assessment#:#ass_create_export_test_archive#:#Create Test Archive File###24 10 2013 new variable -assessment#:#ass_create_question#:#Create Question###24 10 2013 new variable assessment#:#ass_imap_hint#:#Hint to be shown as Tooltip###25 10 2016 new variable assessment#:#ass_imap_map_file_not_readable#:#The uploaded image map could not be read.###25 10 2016 new variable assessment#:#ass_imap_no_map_found#:#Could not find any form in the uploaded image map.###25 10 2016 new variable @@ -570,10 +567,6 @@ assessment#:#cloze_answer_text_info#:#Spaces preceding or following the answer t assessment#:#cloze_fixed_textlength#:#文本域长度 assessment#:#cloze_fixed_textlength_description#:#如果输入一个大于0的值,所有文本和数字的文本域将以这个值的固定长度被创建。 assessment#:#cloze_gap_size_info#:#If you enter a value greater than 0, this gap text field will be created with the fixed length of this value. If you do not enter a value the gap text fiel will be created with the value of the global fixed length.###26 09 2014 new variable -assessment#:#cloze_text#:#填充测验文本 -assessment#:#cloze_textgap_case_insensitive#:#不区分大小写 -assessment#:#cloze_textgap_case_sensitive#:#区分大小定 -assessment#:#cloze_textgap_levenshtein_of#:#Levenshtein distance of %s assessment#:#code#:#代码 assessment#:#codebase#:#代码库 assessment#:#concatenation#:#级联 @@ -756,9 +749,7 @@ assessment#:#fq_formula_desc#:#You may enter predefined variables ($v1 to $vn), assessment#:#fq_no_restriction_info#:#Both decimals and fractions are accepted as input.###26 03 2014 new variable assessment#:#fq_precision_info#:#Enter the number of desired decimal places.###26 03 2014 new variable assessment#:#fq_question_desc#:#You can define variables by inserting $v1, $v2 ... $vn, results by inserting $r1, $r2 .... $rn at the desired position in the question text. Click on the button "Parse Question" to create editing forms for variables and results.###06 11 2013 new variable -assessment#:#gap#:#间隔 assessment#:#gap_combination#:#Gap Combination###26 09 2014 new variable -assessment#:#gaps#:#Gaps###29 10 2025 new variable assessment#:#glossary_term#:#术语表 assessment#:#goto_first_question#:#Show First Question###29 10 2025 new variable assessment#:#grading_mark_msg#:#Your resulting mark is: "[mark]"###26 09 2014 new variable @@ -776,7 +767,6 @@ assessment#:#info_answer_type_change#:#这道题已经包含图片。您不能 assessment#:#info_text_upload#:#Choose an answer file to upload###30 08 2015 new variable assessment#:#insert_after#:#后插 assessment#:#insert_before#:#前插 -assessment#:#insert_gap#:#Insert Gap###24 10 2013 new variable assessment#:#interaction_type#:#Interaction Type###26 08 2024 new variable assessment#:#internal_links#:#内部链接 assessment#:#intprecision#:#Divisible By###24 10 2013 new variable @@ -888,7 +878,6 @@ assessment#:#logs_wrong_test_password_provided#:#Participant entered wrong test assessment#:#longmenu#:#Longmenu###10 11 2018 new variable assessment#:#longmenu_answeroptions_differ#:#This question does not work correctly, as there are not the same amount of gaps in the text as in the correction options.###26 08 2024 new variable assessment#:#longmenu_text#:#Long Menu Text###30 08 2015 new variable -assessment#:#mainbar_button_label_questionlist#:#Questionlist###26 08 2024 new variable assessment#:#maintenance#:#Maintenance维护 assessment#:#manscoring#:#Manual Scoring手动评分 assessment#:#manscoring_done#:#Scored Participants评分参加者 @@ -915,7 +904,6 @@ assessment#:#maximum_nr_of_tries_reached#:#用户已经达到了每日所限制 assessment#:#maximum_points#:#最大有效分值 assessment#:#maxsize#:#文件上传的最大尺寸 assessment#:#maxsize_info#:#输入将被允许上传的文件的最大值(单位字节)。如果这个域留空,这次安装的最大尺寸将被选择替代。 -assessment#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable assessment#:#min_ip_label#:#Lowest IP With Access###26 08 2024 new variable assessment#:#min_percentage_ne_0#:#您必须定义一个最低百分比0%!这个标记模式没被保存。 assessment#:#misc#:#Misc Options###24 10 2013 new variable @@ -924,7 +912,6 @@ assessment#:#mode_onebyone#:#One by One###29 10 2025 new variable assessment#:#mode_question#:#Question oriented###29 10 2025 new variable assessment#:#mode_user#:#Participant oriented###29 10 2025 new variable assessment#:#msg_circle_added#:#加入圈子 -assessment#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable assessment#:#msg_number_of_terms_too_low#:#项目的数量必需大于或等于定义的数量。 assessment#:#msg_poly_added#:#添加多边形 assessment#:#msg_questions_moved#:#题目被移除 @@ -983,7 +970,6 @@ assessment#:#order#:#Order###26 08 2024 new variable assessment#:#ordering_answer_sequence_info#:#您定义在这里的答案序列将被视为正确的方案序列。 assessment#:#ordertext#:#文本排序 assessment#:#ordertext_info#:#请输入水平排序的文本。这个排序文本将被空白标志分隔开。如果您需要一个不同的分隔方式,您可以使用分隔器 %s 去分隔您的文本单元。 -assessment#:#out_of_range#:#Out of range###27 01 2015 new variable assessment#:#output#:#输出 assessment#:#output_mode#:#输出模式 assessment#:#parseQuestion#:#Parse Question###24 10 2013 new variable @@ -1052,7 +1038,6 @@ assessment#:#qpl_bulk_save_add#:#Add###28 10 2024 new variable assessment#:#qpl_bulk_save_overwrite#:#Overwrite###28 10 2024 new variable assessment#:#qpl_bulkedit_success#:#Modifications saved.###28 10 2024 new variable assessment#:#qpl_cancel_skill_assigns_update#:#Cancel###30 08 2015 new variable -assessment#:#qpl_confirm_delete_questions#:#您确认删除下面的题目?如果您删除已经锁定的题目,所有包含被锁定题目的测试结果也将被删除。 assessment#:#qpl_copy_insert_clipboard#:#被选择的题目已经复制到了剪贴板上 assessment#:#qpl_copy_select_none#:#请至少勾选一个题目,然后复制到剪贴板上 assessment#:#qpl_delete_rbac_error#:#您无权删除此题目! @@ -1105,7 +1090,6 @@ assessment#:#qpl_qst_skl_usg_skill_col#:#Competence###30 08 2015 new variable assessment#:#qpl_qst_skl_usg_sklpnt_col#:#Total Sum of Competence-Points per Competence###30 08 2015 new variable assessment#:#qpl_question_is_in_use#:#您即将编辑的题目已存在于 %s 测试中。如果您改变这个题目,您将不能改变这个测试中的题目,因为当一个题目插入到一个测试中时,系统会创建一个它的复本。 assessment#:#qpl_questions_deleted#:#题目已删除 -assessment#:#qpl_reset_preview#:#Reset Preview###26 09 2014 new variable assessment#:#qpl_save_skill_assigns_update#:#Save Competence Assignments###30 08 2015 new variable assessment#:#qpl_settings_availability#:#Availability###26 08 2024 new variable assessment#:#qpl_settings_general_form_prop_show_tax_desc#:#When enabled, possibly created taxonomies are shown for filtering.###24 10 2013 new variable @@ -1135,14 +1119,6 @@ assessment#:#qst_essay_chars_remaining#:#Remaining characters:###07 02 2020 new assessment#:#qst_essay_wordcounter_enabled#:#Count Words###07 02 2020 new variable assessment#:#qst_essay_wordcounter_enabled_info#:#The entered words are counted. The number of written words is shown to the participants below the text input field.###07 02 2020 new variable assessment#:#qst_essay_written_words#:#Number of entered words:###07 02 2020 new variable -assessment#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable -assessment#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable -assessment#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable -assessment#:#qst_lifecycle_final#:#Final###07 02 2020 new variable -assessment#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable -assessment#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable -assessment#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable -assessment#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable assessment#:#qst_nested_nested_answers_off#:#No indents, just order###29 07 2022 new variable assessment#:#qst_nested_nested_answers_on#:#Use indents in anwers###29 07 2022 new variable assessment#:#qst_nr_of_tries#:#尝试次数。 @@ -1166,17 +1142,14 @@ assessment#:#question_title#:#题目标题 assessment#:#question_type#:#题目类型 assessment#:#questionpool_not_entered#:#请为题目池输入一个名字! assessment#:#questionpool_not_selected#:#Please select a question pool!###07 02 2020 new variable -assessment#:#questions#:#Questions###26 08 2024 new variable assessment#:#questions_from#:#题目表单 assessment#:#questions_per_page_view#:#页面视图 assessment#:#random_accept_sample#:#Accept sample assessment#:#random_another_sample#:#获取其它示例 assessment#:#random_selection#:#随机选择 assessment#:#range#:#范围 -assessment#:#range_lower_limit#:#下限 assessment#:#range_max#:#Range (Maximum)###24 10 2013 new variable assessment#:#range_min#:#Range (Minimum)###24 10 2013 new variable -assessment#:#range_upper_limit#:#上限 assessment#:#rated_sign#:#Sign###24 10 2013 new variable assessment#:#rated_unit#:#Unit###24 10 2013 new variable assessment#:#rated_value#:#Value###24 10 2013 new variable @@ -1252,7 +1225,6 @@ assessment#:#search_roles#:#搜索角色 assessment#:#search_term#:#搜索条目 assessment#:#select_at_least_one_feedback_type_and_trigger#:#Please Select at least one type of feedback and a trigger.###26 08 2024 new variable assessment#:#select_at_least_one_lock_answer_type#:#Please select at least one type of answer lock.###29 10 2025 new variable -assessment#:#select_gap#:#选择间隔 assessment#:#select_max_one_item#:#请仅选择一个条目 assessment#:#select_one_user#:#请至少选择一个用户 assessment#:#select_question#:#Select a Question###28 10 2024 new variable @@ -1279,7 +1251,6 @@ assessment#:#show_old_introduction#:#Show old introduction###26 08 2024 new vari assessment#:#show_pass_overview#:#展示已标注通过的概况 assessment#:#show_results#:#Show Results###28 10 2024 new variable assessment#:#show_user_answers#:#展示用户的已标注答案 -assessment#:#shuffle_answers#:#简答题 assessment#:#skip_question#:#Do not Answer and Next###08 10 2015 new variable assessment#:#solution#:#Solution###28 10 2024 new variable assessment#:#solutionText#:#Text文档 @@ -13970,6 +13941,35 @@ qpl#:#qpl_page_type_qfbg#:#General Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qfbs#:#Special Feedback###29 07 2022 new variable qpl#:#qpl_page_type_qht#:#Hint###29 07 2022 new variable qpl#:#qpl_page_type_qpl#:#Question Page###29 07 2022 new variable +qsts#:#answer_options#:#Answer Options ###23 12 2015 new variable +qsts#:#cloze_text#:#填充测验文本 +qsts#:#cloze_textgapcase_insensitive#:#不区分大小写 +qsts#:#cloze_textgapcase_sensitive#:#区分大小定 +qsts#:#cloze_textgaplevenshtein_of#:#Levenshtein distance of %s +qsts#:#confirm_delete_questions#:#您确认删除下面的题目?如果您删除已经锁定的题目,所有包含被锁定题目的测试结果也将被删除。 +qsts#:#create_question#:#Create Question###24 10 2013 new variable +qsts#:#gap#:#间隔 +qsts#:#gaps#:#Gaps###29 10 2025 new variable +qsts#:#insert_gap#:#Insert Gap###24 10 2013 new variable +qsts#:#min_auto_complete#:#Autocomplete###25 10 2016 new variable +qsts#:#msg_no_questions_selected#:#No questions were selected.###26 08 2024 new variable +qsts#:#out_of_range#:#Out of range###27 01 2015 new variable +qsts#:#qst_lifecycle#:#Lifecycle###07 02 2020 new variable +qsts#:#qst_lifecycle_draft#:#Draft###07 02 2020 new variable +qsts#:#qst_lifecycle_filter_all#:#All Lifecycles###07 02 2020 new variable +qsts#:#qst_lifecycle_final#:#Final###07 02 2020 new variable +qsts#:#qst_lifecycle_outdated#:#Outdated###07 02 2020 new variable +qsts#:#qst_lifecycle_rejected#:#Rejected###07 02 2020 new variable +qsts#:#qst_lifecycle_review#:#To be Reviewed###07 02 2020 new variable +qsts#:#qst_lifecycle_sharable#:#Sharable###07 02 2020 new variable +qsts#:#questionlist#:#Questionlist###26 08 2024 new variable +qsts#:#questions#:#Questions###26 08 2024 new variable +qsts#:#range_lower_limit#:#下限 +qsts#:#range_upper_limit#:#上限 +qsts#:#reset_preview#:#Reset Preview###26 09 2014 new variable +qsts#:#select_gap#:#选择间隔 +qsts#:#shuffle_answers#:#简答题 +qsts#:#suggested_learning_content#:#添加建议解决方案 rating#:#rat_not_rated_yet#:#还没有评级 rating#:#rat_nr_ratings#:#%s 评级 rating#:#rat_one_rating#:#One 评级 @@ -16619,7 +16619,6 @@ survey#:#questionblock#:#题目块 survey#:#questionblock_inserted#:#问题块已插入 survey#:#questionblocks#:#题目块 survey#:#questionblocks_inserted#:#题目块已插入 -survey#:#questions#:#题目。 survey#:#questions_inserted#:#题目已插入! survey#:#questions_removed#:#题目和/或题目块已删除! survey#:#questiontype#:#题目类型 @@ -16920,6 +16919,7 @@ survey#:#svy_please_select_unused_codes#:#Please select at least one unused code survey#:#svy_print_hide_labels#:#Hide labels###31 08 2017 new variable survey#:#svy_print_show_labels#:#Show labels###31 08 2017 new variable survey#:#svy_privacy_info#:#Privacy###29 07 2022 new variable +survey#:#svy_questions#:#题目。 survey#:#svy_rater#:#Rater###29 07 2022 new variable survey#:#svy_rater_see_app_info#:#The names of appraisees will be presented to raters to enable them evaluating the questions.###29 07 2022 new variable survey#:#svy_reminder_mail_template#:#Mail Template###25 10 2016 new variable From bd3ea12cfa6a1aff0b0b7a9b7e83c79d4ca10659 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 5 Jun 2026 14:43:02 +0200 Subject: [PATCH 101/108] Questions: Fix Testing Issues in Combinations --- .../Properties/Combinations/Combination.php | 22 +-- .../Cloze/Properties/Combinations/Edit.php | 2 +- .../Cloze/Properties/Combinations/Factory.php | 141 +++++++++++------- .../Properties/Combinations/MatchingValue.php | 8 +- .../Properties/Combinations/Overview.php | 4 +- .../Cloze/Properties/Gaps/Gaps.php | 6 + 6 files changed, 108 insertions(+), 75 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index c01acc6e87cc..831e580682fc 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -47,9 +47,13 @@ class Combination */ public function __construct( private readonly Uuid $id, - private ?float $available_points, + private readonly ?float $available_points, private array $matching_values = [] ) { + uasort( + $this->matching_values, + fn(MatchingValue $a, MatchingValue $b): int => $a->getGap()->getPosition() <=> $b->getGap()->getPosition() + ); } public function getId(): Uuid @@ -62,14 +66,6 @@ public function getAvailablePoints(): ?float return $this->available_points; } - public function withAdditionalMatchingValue( - MatchingValue $matching_value - ): self { - $clone = clone $this; - $clone->matching_values[] = $matching_value; - return $clone; - } - public function getValuePresentation( Language $lng ): string { @@ -259,13 +255,12 @@ public function buildPointsInputs( return $field_factory->section( [ 'values' => $this->buildValuesInputs( - $lng, $field_factory, $properties ), 'points' => $field_factory->numeric( $lng->txt('points') - )->withSubActionSize(0.01) + )->withStepSize(0.01) ->withRequired(true) ->withValue($this->getAvailablePoints()) ], @@ -306,7 +301,6 @@ public function buildCarryString(): string } private function buildValuesInputs( - Language $lng, FieldFactory $field_factory, Properties $properties ): Group { @@ -318,7 +312,6 @@ function ( MatchingValue $v ) use ( $field_factory, - $lng, $properties ): array { $gap_id = $v->getGap()->getAnswerInputId(); @@ -328,7 +321,8 @@ function ( $gap->getType()->getCombinationsSelectValues($gap) )->withRequired(true) ->withValue( - $v->getAnswerOption()?->getAnswerOptionId()->toString() + $v->getInRange()?->value ?? $v->getAnswerOption() + ?->getAnswerOptionId()->toString() ); return $c; }, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php index 39df9a022512..fb7117472686 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Edit.php @@ -29,7 +29,7 @@ class Edit { private const string SUB_ACTION_EDIT_COMBINATIONS_OVERVIEW = 'eco'; - private const string LANG_VAR_EDIT_COMBINATIONS = 'edit_combinations'; + private const string LANG_VAR_EDIT_COMBINATIONS = 'gap_combinations'; public function __construct( private readonly Factory $combinations_factory diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php index bde794e3f0bf..d6f13e896c44 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -23,8 +23,11 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; use ILIAS\Questions\AnswerForm\TypeGenericProperties; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableSubNameSpace; @@ -53,14 +56,13 @@ public function getCombinations( $combinations_enabled, !$combinations_enabled || $gaps === null || $query === null ? [] - : $this->retrieveMatchingValuesFromQuery( - $type_generic_properties, - $gaps, - $this->retrieveCombinationsFromQuery( - $type_generic_properties - ->getDefinition() - ->getTableDefinitions() - ->getTableSubNameSpace(), + : $this->retrieveCombinationsFromQuery( + $type_generic_properties + ->getDefinition() + ->getTableDefinitions() + ->getTableSubNameSpace(), + $this->retrieveMatchingValuesFromQuery( + $gaps, $query ), $query @@ -124,11 +126,13 @@ function (array $c, string $v) use ( $gap = $properties->getGaps()->getGapById( $this->uuid_factory->fromString($v) ); - $answer_option = - $gap->getAnswerOptions() - ->getAnswerOptionById( - $this->uuid_factory->fromString($values_array[$v]) - ); + + $range = Range::tryFrom($values_array[$v]); + $answer_option = $this->retrieveAnswerOptionForFormValues( + $gap, + $values_array[$v], + $range + ); if ($answer_option === null) { return $c; @@ -138,7 +142,7 @@ function (array $c, string $v) use ( $combination_id, $gap, $answer_option, - null + $range ); return $c; }, @@ -172,6 +176,7 @@ public function buildCombinationFromCarryValue( private function retrieveCombinationsFromQuery( TableSubNameSpace $table_sub_name_space, + array $matching_values, Query $query ): array { return $query->retrieveCurrentRecord( @@ -187,14 +192,16 @@ private function retrieveCombinationsFromQuery( array_filter( $vs, fn(array $v): bool => $v['answer_form_id'] !== null - ) + ), + $matching_values ) ) ); } private function buildCombinationsFromQuery( - array $values + array $values, + array $matching_values ): array { if ($values === []) { return []; @@ -202,14 +209,17 @@ private function buildCombinationsFromQuery( return array_reduce( $values, - function (array $c, array $v): array { + function (array $c, array $v) use ($matching_values): array { if (array_key_exists($v['id'], $c)) { return $c; } $c[$v['id']] = new Combination( $this->uuid_factory->fromString($v['id']), - $v['points'] + $v['points'], + isset($matching_values[$v['id']]) + ? array_values($matching_values[$v['id']]) + : null ); return $c; @@ -219,60 +229,79 @@ function (array $c, array $v): array { } private function retrieveMatchingValuesFromQuery( - TypeGenericProperties $type_generic_properties, Gaps $gaps, - array $combinations, Query $query ): array { return $query->retrieveCurrentRecord( $this->persistence_factory->table( $query->getTableNameBuilder( - $type_generic_properties - ->getDefinition() - ->getTableDefinitions() - ->getTableSubNameSpace(), + $this->table_definitions->getTableSubNameSpace(), ), AnswerFormSpecificTableTypes::Additional, $this->table_definitions->getCombinationToAnswerOptionsTableIdentifier() ), $query->getRefinery()->custom()->transformation( - function (array $vs) use ( - $gaps, - $combinations - ): array { - $already_added = []; - foreach ($vs as $v) { - if (!array_key_exists($v['combination_id'], $combinations) - || in_array( - $v['combination_id'] . $v['gap_id'], - $already_added - ) - ) { - continue; - } + function (array $vs) use ($gaps): array { + return $this->buildMatchingValuesArray( + $gaps, + $vs + ); + } + ) + ); + } - $already_added[] = $v['combination_id'] . $v['gap_id']; + private function retrieveAnswerOptionForFormValues( + Gap $gap, + string $value, + ?Range $range + ): AnswerOption { + if ($range === null) { + return $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($value) + ); + } - $gap = $gaps->getGapById( - $this->uuid_factory->fromString($v['gap_id']) - ); + $answer_options_awarding_points = $gap + ->getAnswerOptions() + ->getAnswerOptionsAwardingPoints(); - $combinations[$v['combination_id']] = $combinations[$v['combination_id']] - ->withAdditionalMatchingValue( - new MatchingValue( - $this->uuid_factory->fromString($v['combination_id']), - $gap, - $gap->getAnswerOptions() - ->getAnswerOptionById( - $this->uuid_factory->fromString($v['answer_option_id']) - ) - ) - ); - } + return array_shift($answer_options_awarding_points); + } - return array_values($combinations); + private function buildMatchingValuesArray( + Gaps $gaps, + array $values + ): array { + return array_reduce( + $values, + function (array $c, array $v) use ($gaps): array { + if (isset($c[$v['combination_id']][$v['gap_id']])) { + return $c; } - ) + + if (!array_key_exists($v['combination_id'], $c)) { + $c[$v['combination_id']] = []; + } + + $gap = $gaps->getGapById( + $this->uuid_factory->fromString($v['gap_id']) + ); + + $c[$v['combination_id']][$v['gap_id']] = new MatchingValue( + $this->uuid_factory->fromString($v['combination_id']), + $gap, + $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($v['answer_option_id']) + ), + Range::tryFrom($v['in_range'] ?? '') + ); + + return $c; + }, + [] ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index 644e6f102ac8..a64b77b30d69 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -24,6 +24,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; @@ -54,6 +55,11 @@ public function getAnswerOption(): ?AnswerOption return $this->answer_option; } + public function getInRange(): ?Range + { + return $this->in_range; + } + public function buildPresentationString( Language $lng ): string { @@ -102,7 +108,7 @@ public function toStorage( ), $persistence_factory->value( FieldDefinition::T_TEXT, - $this->answer_option->getAnswerOptionId()->toString() + $this->answer_option?->getAnswerOptionId()->toString() ), $persistence_factory->value( FieldDefinition::T_TEXT, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php index 0766f0775148..11b269ce6bce 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Overview.php @@ -269,8 +269,7 @@ private function processSetCombinationValues(): self|Properties ->withRequest($this->environment->getHttpServices()->request()); $data = $set_values_modal->getData(); if ($data === null) { - $this->modal = $this->initializeModal($set_values_modal) - ->withOnLoad($set_values_modal->getShowSignal()); + $this->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); $inputs_builder->persistCarry(); return $this; } @@ -315,7 +314,6 @@ private function buildInputsBuilder( ?Combination $combination, ): InputsBuilderSession { $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( - $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), $this->environment->getRefinery()->custom()->transformation( function (?string $v) use ($combination): ?Section { $properties = $this->environment->getAnswerFormProperties(); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 69370b7ea8ff..ad5253f27852 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -63,6 +63,11 @@ public function __construct( private Uuid $answer_form_id, array $gaps ) { + uasort( + $gaps, + fn(Gap $a, Gap $b): int => $a->getPosition() <=> $b->getPosition() + ); + $this->gaps = array_reduce( $gaps, function (array $c, Gap $v): array { @@ -130,6 +135,7 @@ public function withNewGap( $new_gap = $this->factory->getNewGap($answer_form_id, $position); $clone = clone $this; $clone->gaps[$new_gap->getAnswerInputId()->toString()] = $new_gap; + return $clone; } From 8988aeb39f11617bb4b99cc403e41ebba89c1653 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 8 Jun 2026 11:28:55 +0200 Subject: [PATCH 102/108] Questions: Fix Migrations --- .../class.ilObjQuestionsGUI.php | 2 +- components/ILIAS/Questions/Questions.php | 3 +- .../Capabilities/Definitions/Migration.php | 5 +- .../Definitions/PageMigrationProvider.php | 26 ++ .../src/AnswerForm/Capabilities/Factory.php | 7 + .../Capabilities/RequiredCapabilities.php | 11 + .../SuggestedLearningContent/Migration.php | 25 +- .../Capabilities/TextFeedback/Capability.php | 47 +++- .../Capabilities/TextFeedback/Migration.php | 222 +++++++++++------- .../Capabilities/TextFeedback/Overview.php | 13 +- .../TextFeedback/SpecificTextFeedback.php | 33 ++- .../TextFeedback/TextFeedback.php | 58 ++++- .../src/AnswerForm/Migration/Migration.php | 3 - .../AnswerForm/Migration/MigrationInsert.php | 15 +- .../Cloze/Capabilities/TextFeedback.php | 2 +- .../Migration/BasicMigrationFunctions.php | 4 +- .../Cloze/Migration/MigrationCloze.php | 50 ++-- .../Cloze/Migration/MigrationLongMenu.php | 34 +-- .../Cloze/Migration/MigrationNumeric.php | 22 +- .../Cloze/Migration/MigrationTextSubset.php | 33 ++- .../Cloze/Properties/Gaps/Text.php | 2 +- .../Cloze/Properties/Gaps/Type.php | 3 +- .../Cloze/TableDefinitions.php | 4 +- .../Persistence/DatabaseStatementBuilder.php | 11 +- .../ILIAS/Questions/src/Question/Question.php | 9 + .../ILIAS/Questions/src/Setup/Agent.php | 2 +- .../src/Setup/ClozeQuestionTables.php | 2 +- .../src/Setup/QuestionsMigration.php | 14 +- .../classes/class.ilAssQuestionPage.php | 13 +- .../classes/class.ilObjQuestionPoolGUI.php | 9 - 30 files changed, 465 insertions(+), 219 deletions(-) create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/PageMigrationProvider.php diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php index 4a55bb6a7b5d..65ffe901b138 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -34,7 +34,7 @@ * @ilCtrl_isCalledBy ilObjQuestionsGUI: ilAdministrationGUI * @ilCtrl_Calls ilObjQuestionsGUI: ilPermissionGUI * @ilCtrl_Calls ilObjQuestionsGUI: ILIAS\Questions\Administration\ConfigurationGUI - * @ilCtrl_Calls ilObjQuestionsGUI: QstsQuestionPageGUI + * @ilCtrl_Calls ilObjQuestionsGUI: QstsQuestionPageGUI, ilAssGenFeedbackPageGUI, ilAssSpecFeedbackPageGUI */ class ilObjQuestionsGUI extends ilObjectGUI { diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 083e344234cf..888a349bbada 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -34,6 +34,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\TableSubNameSpace; +use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; use ILIAS\Questions\Setup\Agent; use ILIAS\Setup\Agent as AgentInterface; @@ -59,7 +60,7 @@ public function init( new Agent( $internal[PersistenceFactory::class], new TableNameBuilder( - 'qsts', + QuestionRepository::COMPONENT_NAMESPACE, null ), $seek[AnswerFormMigration::class], diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Migration.php index fc6417ef6974..da1518da2d10 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Migration.php @@ -22,16 +22,13 @@ use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; -use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Setup\Environment; interface Migration { - public function getTableNameSpace(): TableNameSpace; - public function completeMigrationInsert( Environment $environment, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert - ): ?MigrationInsert; + ): MigrationInsert; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/PageMigrationProvider.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/PageMigrationProvider.php new file mode 100644 index 000000000000..e6359f1d49fa --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/PageMigrationProvider.php @@ -0,0 +1,26 @@ +available_capabilities[$capability_identifier])) { @@ -95,6 +97,10 @@ public function get( $form_step_action = $capability->getAnswerFormEditAdditionalStep(); $required_form_step_actions[$form_step_action->getIdentifier()] = $form_step_action; } + + if ($capability instanceof PageMigrationProvider) { + $required_page_migration_providers[] = $capability; + } } if (count($participant_view_providers) !== 1 || count($marking_providers) > 1) { @@ -110,6 +116,7 @@ public function get( $required_feedback_providers, $required_actions_with_tab, $required_form_step_actions, + $required_page_migration_providers, $marking_providers[0] ?? null ); } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php index 932f294fae7d..9a4ea5742f66 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php @@ -25,6 +25,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Marking; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\MarkingProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\PageMigrationProvider; use ILIAS\Questions\AnswerForm\Capabilities\ParticipantViewProvider; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit as AnswerFormEditView; @@ -40,6 +41,7 @@ class RequiredCapabilities * @param array $required_feedback_views * @param array $required_actions_with_tab * @param array $required_step_actions + * @param array $required_step_actions */ public function __construct( private readonly array $capabilities, @@ -47,6 +49,7 @@ public function __construct( private array $required_feedback_providers, private array $required_actions_with_tab, private array $required_form_step_actions, + private array $required_page_migration_providers, private ?MarkingProvider $marking_provider ) { @@ -90,6 +93,14 @@ public function getRequiredFormStepActions(): array return $this->required_form_step_actions; } + /** + * @return array + */ + public function getRequiredPageMigrationProviders(): array + { + return $this->required_page_migration_providers; + } + public function isMarkingRequired(): bool { return $this->marking_provider !== null; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php index 86036dc8c9e4..8137cd0f410e 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Migration.php @@ -25,7 +25,6 @@ use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerForm\Migration\SanitizeLegacyText; use ILIAS\Questions\Persistence\Insert; -use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolution; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionFile; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -44,18 +43,12 @@ public function __construct( ) { } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert - ): ?MigrationInsert { + ): MigrationInsert { if ($this->old_repository === null) { $this->old_repository = new SuggestedSolutionsDatabaseRepository( $migration_insert->getDb() @@ -71,16 +64,16 @@ public function completeMigrationInsert( )[0] ?? null; if ($old_suggested_learning_content === null) { - return null; + return $migration_insert; } $insert = $this->buildInsertFromOldSuggestedSolution( - $migration_insert->getAnswerFormId(), + $migration_insert, $old_suggested_learning_content ); if ($insert === null) { - return null; + return $migration_insert; } return $migration_insert->withAdditionalInsert($insert); @@ -90,7 +83,7 @@ private function buildInsertFromOldSuggestedSolution( MigrationInsert $migration_insert, SuggestedSolution $old_values ): ?Insert { - $type = $this->buildNewTypeFromOld($old_values->getType())->value; + $type = $this->buildNewTypeFromOld($old_values->getType()); $content = $this->buildContentStringFromOldSuggesteSolution( $migration_insert->getDb(), $type, @@ -104,7 +97,7 @@ private function buildInsertFromOldSuggestedSolution( $pf = $migration_insert->getPersistenceFactory(); return $pf->insert( $this->table_definitions->getColumns( - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder(null), TableTypes::SuggestedLearningContent ), [ @@ -114,7 +107,7 @@ private function buildInsertFromOldSuggestedSolution( ), $pf->value( FieldDefinition::T_TEXT, - $type + $type->value ), $pf->value( FieldDefinition::T_TEXT, @@ -205,7 +198,7 @@ private function fetchQuestionParentAndOwnerFromDB( ): \stdClass { return $db->fetchObject( $db->queryF( - 'SELECT obj_fi, owner FROM qpl_questions WHERE id = %s', + 'SELECT obj_fi, owner FROM qpl_questions WHERE question_id = %s', [FieldDefinition::T_INTEGER], [$old_question_id] ) @@ -240,7 +233,7 @@ private function fetchTargetRefIdFromDB( [FieldDefinition::T_INTEGER], [$sub_object_id] ) - )?->glo_id; + )?->glo_id ?? null; } private function determineNewOwner( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php index a992fc6b53e1..b57a901c041d 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php @@ -25,13 +25,15 @@ use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalTabProvider; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Feedback; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\PageMigrationProvider; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Text\Factory as TextFactory; +use ILIAS\Data\UUID\Uuid; -class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider +class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider, PageMigrationProvider { private const string SUB_ACTION_SAVE = 's'; private const string SUB_ACTION_INSERT_LEGACY_TEXTS = 'ilt'; @@ -75,12 +77,21 @@ public function getAnswerFormEditAdditionalTab(): ActionWithTab public function getFeedback( Properties $answer_form_properties ): ?Feedback { - return $answer_form_properties - ->getTypeGenericProperties() - ->getDefinition() - ->getCapability( - self::getIdentifier() - ); + return $this->retrieveMigratedFeedback( + $answer_form_properties->getAnswerFormId(), + $answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability( + self::getIdentifier() + ) + ); + } + + #[\Override] + public function runPageMigration(): void + { + $this->repository->migratePageFeedback(); } #[\Override] @@ -127,7 +138,7 @@ private function buildOverview( return new Overview( $environment, $this->text_factory, - $this->repository->getFor( + $this->retrieveMigratedFeedback( $environment->getAnswerFormId(), $environment ->getAnswerFormProperties() @@ -168,4 +179,24 @@ private function save( $environment->withDefaultSubAction()->getUrlBuilder() ); } + + private function retrieveMigratedFeedback( + Uuid $answer_form_id, + TextFeedback $feedback + ): TextFeedback { + $feedback_with_data = $this->repository->getFor( + $answer_form_id, + $feedback + ); + + if ($feedback_with_data->requiresPageMigration()) { + $feedback_with_data = $feedback_with_data->withMigratedPageFeedbacks(); + $this->repository->store( + $answer_form_id, + $feedback_with_data + ); + } + + return $feedback_with_data; + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php index 68731851e89f..36282fb85ed8 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Migration.php @@ -27,7 +27,6 @@ use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; -use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Database\FieldDefinition; use ILIAS\Setup\Environment; @@ -40,18 +39,12 @@ public function __construct( ) { } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert - ): ?MigrationInsert { + ): MigrationInsert { $generic_feedback_insert = $this->buildGenericFeedbackInsert( $migration_insert->getPersistenceFactory(), $migration_insert @@ -88,7 +81,7 @@ private function fetchGenericFeedbackDBValues( [$old_question_id] ); - yield from $this->fetchFeedbacksForQuestions($query); + yield from $this->fetchFeedbacksForQuestions($db, $query); } private function fetchSpecificFeedbackDBValues( @@ -101,20 +94,21 @@ private function fetchSpecificFeedbackDBValues( [$old_question_id] ); - yield from $this->fetchFeedbacksForQuestions($query); + yield from $this->fetchFeedbacksForQuestions($db, $query); } private function fetchFeedbacksForQuestions( + \ilDBInterface $db, \ilDBStatement $query ): \Generator { $feedbacks_for_question = null; - while (($row = $db->fetchObject($query)) !== null) { + while (($row = $db->fetchAssoc($query)) !== null) { if ($feedbacks_for_question === null) { $feedbacks_for_question = [$row]; continue; } - if ($feedbacks_for_question['question_fi'] === $row['question_fi']) { + if ($feedbacks_for_question[0]['question_fi'] === $row['question_fi']) { $feedbacks_for_question[] = $row; continue; } @@ -123,13 +117,15 @@ private function fetchFeedbacksForQuestions( $feedbacks_for_question = [$row]; } - yield $feedbacks_for_question; + if ($feedbacks_for_question !== null) { + yield $feedbacks_for_question; + } } private function buildGenericFeedbackInsert( PersistenceFactory $persistence_factory, MigrationInsert $migration_insert - ): Insert { + ): ?Insert { $insert = null; foreach ($this->fetchGenericFeedbackDBValues( $migration_insert->getDb(), @@ -149,7 +145,7 @@ private function buildGenericFeedbackInsert( private function addGenericFeedbackToInsert( PersistenceFactory $persistence_factory, MigrationInsert $migration_insert, - Insert $insert, + ?Insert $insert, array $feedback_for_question ): Insert { $values = $this->buildNewGenericFeedbackValuesFromOld( @@ -161,7 +157,7 @@ private function addGenericFeedbackToInsert( if ($insert === null) { return $persistence_factory->insert( $this->table_definitions->getColumns( - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder(null), TableTypes::FeedbackGeneric ), $values @@ -176,14 +172,22 @@ private function buildNewGenericFeedbackValuesFromOld( MigrationInsert $migration_insert, array $feedback_for_question ): array { + $page_editor_used = $migration_insert->wasIliasPageEditorUsedForAdditionalTexts(); + $feedback_best_response = ''; $feedback_other_response = ''; foreach ($feedback_for_question as $feedback_row) { + $feedback_text = $this->generateTextValueForFeedback( + $migration_insert->getDb(), + $page_editor_used, + $feedback_row['feedback_id'], + $feedback_row['feedback'] + ); if ($feedback_row['correctness'] === '1') { - $feedback_best_response = $feedback_row['feedback']; + $feedback_best_response = $feedback_text; continue; } - $feedback_other_response = $feedback_row['feedback']; + $feedback_other_response = $feedback_text; } return [ @@ -197,11 +201,7 @@ private function buildNewGenericFeedbackValuesFromOld( ), $persistence_factory->value( FieldDefinition::T_TEXT, - $this->sanitizeLegacyText( - $migration_insert->getDb(), - $feedback_best_response, - $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() - ) + $feedback_best_response, ), $persistence_factory->value( FieldDefinition::T_TEXT, @@ -209,12 +209,8 @@ private function buildNewGenericFeedbackValuesFromOld( ), $persistence_factory->value( FieldDefinition::T_TEXT, - $this->sanitizeLegacyText( - $migration_insert->getDb(), - $feedback_other_response, - $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() - ) - ), + $feedback_other_response + ) ]; } @@ -222,7 +218,7 @@ private function buildSpecificFeedbackInsert( PersistenceFactory $persistence_factory, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert - ): Insert { + ): ?Insert { $insert = null; foreach ($this->fetchSpecificFeedbackDBValues( $migration_insert->getDb(), @@ -244,9 +240,9 @@ private function addSpecificFeedbackToInsert( PersistenceFactory $persistence_factory, AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert, - Insert $insert, + ?Insert $insert, array $feedback_for_question - ): Insert { + ): ?Insert { $values = $this->buildNewSpecificFeedbackValuesFromOld( $persistence_factory, $answer_form_migration, @@ -254,14 +250,14 @@ private function addSpecificFeedbackToInsert( $feedback_for_question ); - if ($values === null) { + if ($values === []) { return $insert; } if ($insert === null) { $insert = $persistence_factory->insert( $this->table_definitions->getColumns( - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder(null), TableTypes::FeedbackSpecific ), array_shift($values) @@ -270,7 +266,7 @@ private function addSpecificFeedbackToInsert( return array_reduce( $values, - fn(MigrationInsert $c, array $v): MigrationInsert + fn(Insert $c, array $v): Insert => $c->withAdditionalValues($v), $insert ); @@ -281,67 +277,117 @@ private function buildNewSpecificFeedbackValuesFromOld( AnswerFormMigration $answer_form_migration, MigrationInsert $migration_insert, array $feedback_for_question - ): ?array { - $parent_id = $answer_form_migration->getNewAnswerInputIdForOld( - $feedback_for_question - ); - $conditions = $answer_form_migration->getConditionForFeedbackFromOldValues( - $feedback_for_question['answer'], - $feedback_for_question['question'] - ) ?? ''; - - if ($parent_id === null - || $conditions === null) { - return null; - } + ): array { + $conditions = array_reduce( + $feedback_for_question, + function ( + array $c, + array $v + ) use ( + $answer_form_migration, + $migration_insert + ): array { + $parent_id = $answer_form_migration->getNewAnswerInputIdForOld( + $v['question'] + ); - return array_map( - fn(string $v): array => [ - $persistence_factory->value( - FieldDefinition::T_TEXT, - $migration_insert->getUuid()->toString() - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $migration_insert->getAnswerFormId()->toString() - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $parent_id - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $v - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - '' - ), - $persistence_factory->value( - FieldDefinition::T_TEXT, - $this->sanitizeLegacyText( + $conditions = $answer_form_migration->getConditionsForFeedbackFromOldValues( + $v['answer'], + $v['question'] + ); + + if ($parent_id === null || $conditions === null) { + return $c; + } + + if (!array_key_exists($parent_id->toString(), $c)) { + $c[$parent_id->toString()] = []; + } + + /* + * sk, 2026-06-09: We got rid of the option to set a value and a + * range on numeric gaps. We now need to make sure that we do not + * have feedback for both. If there is feedback for both only the + * first one is kept. I do not see a better solution. + */ + if ($conditions[0] === Range::InRange->value + && array_filter( + $c[$parent_id->toString()], + fn(array $v): bool => $v['conditions'][0] === Range::InRange->value + ) !== []) { + return $c; + } + + $c[$parent_id->toString()][] = [ + 'conditions' => $conditions, + 'text' => $this->generateTextValueForFeedback( $migration_insert->getDb(), - $feedback_for_question['feedback'], - $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts(), + $v['feedback_id'], + $v['feedback'] ) - ), - ], - $conditions + ]; + + return $c; + }, + [] ); - } - private function buildRangeValue( - bool $is_numeric, - string $value - ): ?string { - if ($is_numeric === null) { - return null; + $inserts = []; + foreach ($conditions as $parent_id => $definitions) { + foreach ($definitions as $definition) { + foreach ($definition['conditions'] as $condition) { + $inserts[] = [ + $persistence_factory->value( + FieldDefinition::T_TEXT, + $migration_insert->getUuid()->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $migration_insert->getAnswerFormId()->toString() + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $parent_id + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $condition + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + '' + ), + $persistence_factory->value( + FieldDefinition::T_TEXT, + $this->sanitizeLegacyText( + $migration_insert->getDb(), + $definition['text'], + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + ), + ]; + } + } } - if ($value === 'out_of_bounds') { - return Range::OutOfRange->value; + return $inserts; + } + + private function generateTextValueForFeedback( + \ilDBInterface $db, + bool $page_editor_used, + int $feedback_id, + string $feedback_text + ): string { + if ($page_editor_used) { + return "####{$feedback_id}####"; } - return Range::InRange->value; + return $this->sanitizeLegacyText( + $db, + $feedback_text, + $page_editor_used + ); } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php index be0a04297744..3d6dd44f9351 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Overview.php @@ -57,7 +57,8 @@ public function getUI(): array $content = []; - if ($this->feedback->hasLegacyTexts()) { + if ($this->feedback->hasLegacyTexts() + && !$this->set_legacy_texts_as_values) { $content[] = $ui_factory->messageBox()->info( $lng->txt('insert_legacy_texts_info') )->withButtons([ @@ -204,8 +205,9 @@ private function buildGenericFormInputs(): array Types::BestResponse->getTranslatedOptionName($lng) )->withValue( $this->set_legacy_texts_as_values - ? $this->feedback->getFeedbackBestResponseLegacy() - : $this->feedback->getFeedbackBestResponse() + ? $this->environment->getRefinery()->string()->stripTags()->transform( + $this->feedback->getFeedbackBestResponseLegacy() + ) : $this->feedback->getFeedbackBestResponse() ?->getRawRepresentation() ?? '' ), 'not_max_points' => $if->field()->markdown( @@ -213,8 +215,9 @@ private function buildGenericFormInputs(): array Types::OtherResponse->getTranslatedOptionName($lng) )->withValue( $this->set_legacy_texts_as_values - ? $this->feedback->getFeedbackOtherResponseLegacy() - : $this->feedback->getFeedbackOtherResponse() + ? $this->environment->getRefinery()->string()->stripTags()->transform( + $this->feedback->getFeedbackOtherResponseLegacy() + ) : $this->feedback->getFeedbackOtherResponse() ?->getRawRepresentation() ?? '' ), ], diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php index 54597df7cb68..504342c8e4e3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php @@ -33,7 +33,7 @@ public function __construct( private readonly Uuid $parent_id, private readonly string $condition, private ?Markdown $feedback_text = null, - private readonly string $feedback_legacy = '' + private string $feedback_legacy = '' ) { } @@ -65,6 +65,12 @@ public function withFeedbackText( return $clone; } + public function hasLegacyFeedback(): bool + { + return $this->feedback_text === null + && $this->feedback_legacy !== ''; + } + public function toStorage( PersistenceFactory $persistence_factory ): array { @@ -101,4 +107,29 @@ public function toStorage( ) ]; } + + public function requiresPageMigration(): bool + { + return preg_match('/####\d+####/', $this->feedback_legacy) === 1; + } + + public function withMigratedPageFeedbacks(): self + { + $clone = clone $this; + $feedback = trim($clone->feedback_legacy, '#'); + if (!is_numeric($feedback)) { + return $clone; + } + + $feedback_page = (new \ilAssSpecFeedbackPageGUI( + (int) $feedback + ))->presentation(); + + $clone->feedback_legacy = ''; + if (strip_tags($feedback_page) !== '') { + $clone->feedback_legacy = $feedback_page; + } + + return $clone; + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php index c24bfaf3c9bc..980b6e0dc3dc 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php @@ -284,6 +284,48 @@ public function getParticipantOutput( ); } + public function requiresPageMigration(): bool + { + if (preg_match('/####\d+####/', $this->feedback_best_response_legacy) === 1 + || preg_match('/####\d+####/', $this->feedback_other_response_legacy) === 1) { + return true; + } + + foreach ($this->specific_feedbacks as $feedback) { + if ($feedback->requiresPageMigration()) { + return true; + } + } + + return false; + } + + public function withMigratedPageFeedbacks(): self + { + $clone = clone $this; + $feedback_best = trim($clone->feedback_best_response_legacy, '#'); + if (is_numeric($feedback_best)) { + $clone->feedback_best_response_legacy = $this->migrateGenericFeedbackPage( + (int) $feedback_best + ); + } + + $feedback_other = trim($clone->feedback_other_response_legacy, '#'); + if (is_numeric($feedback_other)) { + $clone->feedback_other_response_legacy = $this->migrateGenericFeedbackPage( + (int) $feedback_other + ); + } + + $clone->specific_feedbacks = array_map( + fn(SpecificTextFeedback $v): SpecificTextFeedback + => $v->withMigratedPageFeedbacks(), + $clone->specific_feedbacks + ); + + return $clone; + } + public function toStorage( PersistenceFactory $persistence_factory, TableDefinitions $feedback_table_definitions, @@ -446,8 +488,22 @@ private function getRenderedFeedbackOtherResponse( return $ui_factory->messageBox()->info( $rendered_markdown === '' - ? $lng->txt('orther_response_given') + ? $lng->txt('other_response_given') : $rendered_markdown ); } + + private function migrateGenericFeedbackPage( + int $page_id + ): string { + $feedback_page = (new \ilAssGenFeedbackPageGUI( + $page_id + ))->presentation(); + + if (strip_tags($feedback_page) === '') { + return ''; + } + + return $feedback_page; + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php index c2643a4a8ed6..b85b7bd314ec 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -20,7 +20,6 @@ namespace ILIAS\Questions\AnswerForm\Migration; -use ILIAS\Questions\Persistence\TableNameSpace; use ILIAS\Data\UUID\Uuid; use ILIAS\Setup\Environment; @@ -34,8 +33,6 @@ public function getOldQuestionTypeIdentifier(): string; public function getDefinitionClass(): string; - public function getTableNameSpace(): TableNameSpace; - public function completeMigrationInsert( Environment $environment, MigrationInsert $migration_insert diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php index eee64a939653..80d373ea1863 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationInsert.php @@ -25,6 +25,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; @@ -44,7 +45,7 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly PersistenceFactory $persistence_factory, private readonly AnswerFormGenericTableDefinitions $answer_form_generic_table_definitions, - private readonly TableNameBuilder $table_names_builder, + private readonly string $component_name_space, private array $inserts, private readonly int $old_question_id, private readonly Uuid $new_question_id, @@ -74,9 +75,13 @@ public function getPersistenceFactory(): PersistenceFactory return $this->persistence_factory; } - public function getTableNameBuilder(): TableNameBuilder - { - return $this->table_names_builder; + public function getTableNameBuilder( + ?TableSubNameSpace $table_sub_name_space + ): TableNameBuilder { + return new TableNameBuilder( + $this->component_name_space, + $table_sub_name_space + ); } public function getOldQuestionId(): int @@ -171,7 +176,7 @@ private function buildCoreAnswerFormInsertStatement(): Insert { return $this->persistence_factory->insert( $this->answer_form_generic_table_definitions->getColumns( - $this->table_names_builder, + $this->getTableNameBuilder(null), AnswerFormGenericTableTypes::AnswerForms ), [ diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php index a6f7711085ec..f3a517576061 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php @@ -96,7 +96,7 @@ function ( $gaps, $response ): array { - $gap_id = $v[0]->getAnswerInputId(); + $gap_id = $v[0]->getParentId(); /** @var ?AnswerInputResponse $answer_for_gap */ $answer_for_gap = $response?->getResponseForInput($gap_id); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php index b028a6cd3b13..274a57f2dff5 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -64,6 +64,8 @@ private function buildGapInsertStatement( ?int $min_autocomplete, ?int $shuffle ): Insert { + $this->answer_inputs_mapping[$position] = $answer_input_id; + if ($gaps_insert === null) { return $persistence_factory->insert( $table_definitions->getColumns( @@ -139,8 +141,6 @@ private function buildAnswerOptionInsertStatement( ?float $lower_limit, ?float $upper_limit ): Insert { - $this->answer_options_mapping[$position] = $answer_option_id; - if ($options_insert === null) { return $persistence_factory->insert( $table_definitions->getColumns( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php index 89046844bc48..7ab1ed084c22 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -30,7 +30,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; -use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Setup\Environment; @@ -59,12 +59,6 @@ public function getDefinitionClass(): string return Definition::class; } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, @@ -74,6 +68,7 @@ public function completeMigrationInsert( $answer_options_mapping = []; $gaps_insert = null; $answer_options_insert = null; + $available_points = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), @@ -91,7 +86,9 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $gaps_insert, $answer_input_mapping[$db_row->gap_id], $answer_form_id, @@ -112,10 +109,17 @@ public function completeMigrationInsert( ]; $this->answer_options_mapping_for_feedback[$db_row->gap_id]['answer_options'][$db_row->aorder] = $answer_option_id; + if (!isset($available_points[$answer_option_id->toString()]) + || $available_points[$answer_option_id->toString()] < $db_row->points) { + $available_points[$answer_option_id->toString()] = $db_row->points; + } + $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_options_insert, $answer_option_id, $answer_input_mapping[$db_row->gap_id], @@ -145,7 +149,9 @@ public function completeMigrationInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_form_id, $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), $db_row->combinations_enabled @@ -162,6 +168,12 @@ public function completeMigrationInsert( $answer_input_mapping, $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() ) + )->withAvailablePoints( + array_reduce( + $available_points, + fn(float $c, float $v): float => $c + $v, + 0.0 + ) ); } @@ -188,8 +200,12 @@ public function getConditionsForFeedbackFromOldValues( return [Types::NoResponse->value]; } + if ($answer === -2) { + return [Types::OtherResponse->value]; + } + if ($gap['is_numeric']) { - return [$this->buildRangeValue(true, $answer)]; + return [$this->buildRangeValue(true, $answer)->value]; } $answer_option_id = array_filter( @@ -199,10 +215,10 @@ public function getConditionsForFeedbackFromOldValues( ); if ($answer_option_id !== []) { - return [$answer_option_id->toString()]; + return [array_shift($answer_option_id)->toString()]; } - return ['']; + return null; } private function fetchDBValues( @@ -266,7 +282,9 @@ private function addCombinationInsertStatements( $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); $combinations_insert = $this->buildCombinationsInsert( $persistence_factory, - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $combinations_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id], $migration_insert->getAnswerFormId(), @@ -276,7 +294,9 @@ private function addCombinationInsertStatements( $combinations_to_answer_options_insert = $this->buildCombinationsToAnswerOptionsInsert( $persistence_factory, - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $combinations_to_answer_options_insert, $combination_mapping[$db_row->combination_id . $db_row->row_id], $answer_input_mapping[$db_row->gap_fi], diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php index 9830dd3ef6e0..8fe8575e950e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -24,7 +24,7 @@ use ILIAS\Questions\AnswerForm\Migration\MigrationInsert; use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; -use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Setup\Environment; class MigrationLongMenu implements Migration @@ -48,12 +48,6 @@ public function getDefinitionClass(): string return Definition::class; } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, @@ -64,6 +58,7 @@ public function completeMigrationInsert( $answer_input_mapping = []; $gaps_insert = null; $answer_options_to_insert = []; + $available_points = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), @@ -76,7 +71,9 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $gaps_insert, $answer_input_id, $answer_form_id, @@ -116,10 +113,8 @@ public function completeMigrationInsert( if ($position !== null) { $answer_options_to_insert[$answer_input_id_string][$position]['points'] = $db_row->points; - ; + $available_points[$answer_input_id_string] = $db_row->points; } - - } if (!isset($db_row)) { @@ -135,7 +130,9 @@ public function completeMigrationInsert( $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_options_insert, $answer['answer_option_id'], $answer['answer_input_id'], @@ -152,7 +149,9 @@ public function completeMigrationInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_form_id, $this->buildScoringIdenticalFromOld($db_row->identical_scoring), 0 @@ -166,7 +165,14 @@ public function completeMigrationInsert( $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() ) )->withAdditionalInsert($gaps_insert) - ->withAdditionalInsert($answer_options_insert); + ->withAdditionalInsert($answer_options_insert) + ->withAvailablePoints( + array_reduce( + $available_points, + fn(float $c, float $v): float => $c + $v, + 0.0 + ) + ); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php index cb555f9ac174..6649cc95aff8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -26,7 +26,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; -use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Setup\Environment; class MigrationNumeric implements Migration @@ -52,12 +52,6 @@ public function getDefinitionClass(): string return Definition::class; } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, @@ -78,7 +72,9 @@ public function completeMigrationInsert( $this->buildGapInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), null, $gap_id, $answer_form_id, @@ -94,7 +90,9 @@ public function completeMigrationInsert( $this->buildAnswerOptionInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), null, $migration_insert->getUuid(), $gap_id, @@ -108,14 +106,16 @@ public function completeMigrationInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_form_id, ScoringIdentical::ScoreAll, 0 ) )->withAdditionalText( '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . $gap_id->toString() . '}}' - ); + )->withAvailablePoints($db_row->points); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php index 7d5da8dcc3dd..27e0a58182b9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -25,7 +25,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Definition; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; -use ILIAS\Questions\Persistence\TableNameSpace; +use ILIAS\Questions\Persistence\TableSubNameSpace; use ILIAS\Setup\Environment; class MigrationTextSubset implements Migration @@ -49,12 +49,6 @@ public function getDefinitionClass(): string return Definition::class; } - #[\Override] - public function getTableNameSpace(): TableNameSpace - { - return $this->table_definitions->getTableNameSpace(); - } - #[\Override] public function completeMigrationInsert( Environment $environment, @@ -63,6 +57,7 @@ public function completeMigrationInsert( $answer_form_id = $migration_insert->getAnswerFormId(); $answer_options_insert = null; $gaps = []; + $available_points = []; foreach ($this->fetchDBValues( $migration_insert->getDb(), @@ -77,7 +72,9 @@ public function completeMigrationInsert( $gaps_insert = $this->buildGapInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $gaps_insert, $gap_id, $answer_form_id, @@ -94,11 +91,15 @@ public function completeMigrationInsert( $migration_insert = $migration_insert->withAdditionalInsert($gaps_insert); } + $available_points[] = $db_row->points; + foreach ($gaps as $gap_id) { $answer_options_insert = $this->buildAnswerOptionInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_options_insert, $migration_insert->getUuid(), $gap_id, @@ -115,12 +116,16 @@ public function completeMigrationInsert( return null; } + rsort($available_points); + return $migration_insert ->withAdditionalInsert( $this->buildAnswerFormInsertStatement( $this->table_definitions, $migration_insert->getPersistenceFactory(), - $migration_insert->getTableNameBuilder(), + $migration_insert->getTableNameBuilder( + $this->table_definitions->getTableSubNameSpace() + ), $answer_form_id, ScoringIdentical::OnlyScoreDistinct, 0 @@ -129,6 +134,14 @@ public function completeMigrationInsert( $answer_options_insert )->withAdditionalText( $this->buildAdditionalTextFromGapsArray($gaps) + )->withAvailablePoints( + array_sum( + array_slice( + $available_points, + 0, + count($gaps) + ) + ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index e6782979f607..1e1fd2048a4f 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -221,7 +221,7 @@ public function getSpecificFeedbackParticipantOutput( ): ?Component { $specific_feedbacks_by_condition = array_reduce( $specific_feedbacks, - function (array $c, SpecificFeedback $v): array { + function (array $c, SpecificTextFeedback $v): array { if (!array_key_exists($v->getCondition(), $c)) { $c[$v->getCondition()] = []; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index c38994d17aa1..821c6a41ae3d 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Definitions\Range; +use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; @@ -304,7 +305,7 @@ private function getSpecificFeedbackParticipantOutputForAnswerOption( return $ui_factory->legacy()->content( $this->refinery->string()->markdown()->toHTML()->transform( - $specific_feedback + $specific_feedback->getRawRepresentation() ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php index 1beb39643c14..298f5cdcdd21 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/TableDefinitions.php @@ -155,7 +155,7 @@ public function getIdColumn( if ($table_type === AnswerFormSpecificTableTypes::TypeSpecificAnswerForms) { return $this->persistence_factory->column( $table, - self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + self::ANSWER_FORM_TABLE_ID_COLUMN ); } @@ -193,7 +193,7 @@ public function getForeignKeyColumn( return match($table_type) { AnswerFormSpecificTableTypes::TypeSpecificAnswerForms => $this->persistence_factory->column( $table, - self::ANSWER_FORM_TABLE_ID_COLUMN + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN ), AnswerFormSpecificTableTypes::AnswerInputs => $this->persistence_factory->column( $table, diff --git a/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php b/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php index 026cd51dedec..76128f81e761 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php +++ b/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Manipulate; @@ -139,9 +140,11 @@ public function addUpdateStatementsToManipulation( if ($page_id_updated) { $manipulate = $manipulate->withAdditionalStatement( - $this->buildUpdatePageIdStatement(), - $question_id, - $page_id + $this->buildUpdatePageIdStatement( + $table_names_builder, + $question_id, + $page_id + ) ); } @@ -177,7 +180,7 @@ public function addDeleteAnswerFormsStatementsToManipulate( } public function buildDeleteQuestionStatement( - TableNamesBuilder $table_names_builder, + TableNameBuilder $table_names_builder, Uuid $question_id ): Delete { $table_type = TableTypes::Questions; diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index b8384e0a817d..6c9c5e01773c 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -210,6 +210,15 @@ public function getCreated(): ?\DateTimeImmutable return $this->created; } + /** + * @todo skergomard, 2026-06-08: This we only need while the migrations exist, after + * this a question MUST never change the page assigned to it after its creation! + */ + public function getFirstAnswerFormIdForMigration(): Uuid + { + return reset($this->answer_forms)->getAnswerFormId(); + } + public function getAnswerFormPropertiesByIdString( string $form_id ): ?AnswerFormProperties { diff --git a/components/ILIAS/Questions/src/Setup/Agent.php b/components/ILIAS/Questions/src/Setup/Agent.php index 94caae1dc9bc..266e1c1071f1 100644 --- a/components/ILIAS/Questions/src/Setup/Agent.php +++ b/components/ILIAS/Questions/src/Setup/Agent.php @@ -117,7 +117,7 @@ public function getStatusObjective( $storage, new ClozeQuestionTables( new SetupTableNameBuilder( - new TableNameSpaceCore('cloze') + new TableSubNameSpaceCore('cloze') ) ) ) diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php index ccbb9ae6dcb4..ab3f17b0db7c 100644 --- a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -250,7 +250,7 @@ public function step_5(): void ]); } - if (!$this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id'])) { + if (!$this->db->primaryExistsByFields($table_name, ['combination_id', 'gap_id'])) { $this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id']); } diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php index 56ca766f733a..afc5b0741b8a 100644 --- a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -20,7 +20,7 @@ namespace ILIAS\Questions\Setup; -use ILIAS\Questions\AnswerForm\Capabilities\Migration as CapabilityMigration; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Migration as CapabilityMigration; use ILIAS\Questions\AnswerForm\Migration\Migration as AnswerFormMigration; use ILIAS\Questions\AnswerForm\Migration\MigrationInsert as AnswerFormMigrationInsert; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormGenericTableDefinitions; @@ -30,6 +30,7 @@ use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Question\Definitions\Lifecycle; +use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; @@ -172,10 +173,11 @@ public function step( array_reduce( $this->capability_migrations, - fn(AnswerFormMigrationInsert $c, CapabilityMigration $v): AnswerFormMigration + fn(AnswerFormMigrationInsert $c, CapabilityMigration $v): AnswerFormMigrationInsert => $v->completeMigrationInsert( $environment, - $migration_insert + $answer_form_migration, + $c ), $migration_insert )->run(); @@ -411,7 +413,7 @@ private function buildInsertMigrationStatement( ?Uuid $new_question_id ): Insert { return $this->persistence_factory->insert( - $this->persistence_factory->getColumns( + $this->question_table_definitions->getColumns( $this->question_table_names_builder, QuestionTableTypes::MigrationsTable ), @@ -446,9 +448,7 @@ private function buildMigrationInsert( $this->uuid_factory, $this->persistence_factory, $this->answer_form_generic_table_definitions, - new TableNameBuilder( - $answer_form_migration->getTableNameSpace() - ), + QuestionRepository::COMPONENT_NAMESPACE, $question_inserts, $db_values->question_id, $new_question_id, diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index 3fab06b00082..80b2a2a72bb1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -61,16 +61,15 @@ public function copyToAnswerForm( private function migrateQuestionElementToAnswerForm(): void { global $DIC; - $dom_util = $DIC->copage()->internal()->domain()->domUtil(); - - /** @var \ILIAS\Questions\AnswerForm\Properties $answer_form_properties */ - $answer_form_properties = $this->question->getAnswerFormProperties(); - - $dom_util->path($this->getDomDoc(), '//Question') + $DIC->copage() + ->internal() + ->domain() + ->domUtil() + ->path($this->getDomDoc(), '//Question') ->item(0)->parentNode->replaceWith( $this->buildLegacyAnswerFormTextNode(), $this->buildAnswerFormNode( - reset($answer_form_properties)->getAnswerFormId() + $this->question->getFirstAnswerFormIdForMigration() ) ); $this->xml = $this->getXMLFromDom(); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 53f618b9e152..cd4181f2e853 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -372,15 +372,6 @@ public function executeCommand(): void $question_gui->setObject($question); $question_gui->setQuestionTabs(); - if ($this->questionrepository->isInActiveTest($question_gui->getObject()->getObjId())) { - $this->tpl->setOnScreenMessage( - 'failure', - $this->lng->txt('question_is_part_of_running_test'), - true - ); - $this->ctrl->redirectByClass('ilAssQuestionPreviewGUI', ilAssQuestionPreviewGUI::CMD_SHOW); - } - $this->help->setScreenIdComponent('qpl'); if ($this->object->getType() == 'qpl' && $write_access) { From 9ae70e34c82360a260cf25547b5b6e2dc9f3aceb Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Fri, 12 Jun 2026 10:04:36 +0200 Subject: [PATCH 103/108] Questions: Change Name of Getter in Query --- .../SuggestedLearningContent/Repository.php | 2 +- .../Capabilities/TextFeedback/Repository.php | 2 +- components/ILIAS/Questions/src/Attempt/Repository.php | 4 ++-- components/ILIAS/Questions/src/Persistence/Query.php | 10 +++++----- .../Questions/src/Question/Persistence/Repository.php | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php index 234569797dc3..04d577786171 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php @@ -67,7 +67,7 @@ public function getFor( ): Content { $database_values = $this->buildQuery( $answer_form_id - )->loadNextRecord()->current(); + )->getRecords()->current(); if ($database_values === null) { return $this->getNew( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php index 80432745ed70..7ce2e93eaaba 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php @@ -74,7 +74,7 @@ public function getFor( $this->feedback_table_names_builder, TableTypes::FeedbackGeneric ) - )->loadNextRecord()->current(); + )->getRecords()->current(); if ($database_values === null) { return $feedback; diff --git a/components/ILIAS/Questions/src/Attempt/Repository.php b/components/ILIAS/Questions/src/Attempt/Repository.php index 8438b2386fbc..52f770630d73 100644 --- a/components/ILIAS/Questions/src/Attempt/Repository.php +++ b/components/ILIAS/Questions/src/Attempt/Repository.php @@ -76,7 +76,7 @@ public function getAttemptFor( $base_table_id_column ), $this->buildQuery($attempt_id) - )->loadNextRecord() + )->getRecords() ->current(); if ($database_values === null) { @@ -248,7 +248,7 @@ private function getAllResponsesFor( $database_values = $question->completeResponseQuery( $this->buildQuery($attempt_id), $base_table_id_column - )->loadNextRecord() + )->getRecords() ->current(); if ($database_values === null) { diff --git a/components/ILIAS/Questions/src/Persistence/Query.php b/components/ILIAS/Questions/src/Persistence/Query.php index 8f8599cfbd91..c00c9078f9c2 100644 --- a/components/ILIAS/Questions/src/Persistence/Query.php +++ b/components/ILIAS/Questions/src/Persistence/Query.php @@ -113,7 +113,7 @@ public function withGroupBy( return $clone; } - public function loadNextRecord(): \Generator + public function getRecords(): \Generator { $result = $this->toSql(); @@ -124,11 +124,11 @@ public function loadNextRecord(): \Generator if ($this->group_by === null) { yield $this; - yield from $this->loadNextRecordUngrouped($result); + yield from $this->getRecordsUngrouped($result); return; } - yield from $this->loadNextRecordGrouped( + yield from $this->getRecordsGrouped( $result, $this->group_by->getColumnAlias() ); @@ -214,7 +214,7 @@ private function addValueToBinding( } } - private function loadNextRecordGrouped( + private function getRecordsGrouped( \ilDBStatement $result, string $group_by ): \Generator { @@ -229,7 +229,7 @@ private function loadNextRecordGrouped( yield $this; } - private function loadNextRecordUngrouped( + private function getRecordsUngrouped( \ilDBStatement $result ): \Generator { while (($db_record = $this->db->fetchAssoc($result)) !== null) { diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index ed7f9ce1655f..286a7d6713de 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -97,7 +97,7 @@ public function getQuestionDataOnlyForAllQuestions( foreach ($questions_query->withGroupBy( $this->buildGroupByColumn() - )->loadNextRecord() as $query_with_record) { + )->getRecords() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, $this->retrieveAnswerFormsFromQuery($query_with_record, true) @@ -141,7 +141,7 @@ public function getQuestionDataOnlyForQuestionIds( ) )->withGroupBy( $this->buildGroupByColumn() - )->loadNextRecord() as $query_with_record) { + )->getRecords() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, $this->retrieveAnswerFormsFromQuery($query_with_record, true) @@ -273,7 +273,7 @@ private function getForBaseQuery( foreach ($query_with_answer_forms->withGroupBy( $this->buildGroupByColumn() - )->loadNextRecord() as $query_with_record) { + )->getRecords() as $query_with_record) { yield $this->retrieveQuestionFromQuery( $query_with_record, $this->retrieveAnswerFormsFromQuery($query_with_record) From a06b4d88295af116d24201aebc8fdbd2f55d8f39 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 22 Jun 2026 14:31:16 +0200 Subject: [PATCH 104/108] Questions: Allow More Flexibility in EditOverview --- .../Questions/src/AnswerForm/Properties.php | 10 ++-- .../Cloze/Properties/Properties.php | 17 +++--- .../src/AnswerFormTypes/Cloze/Views/Edit.php | 11 ++-- .../src/Presentation/Layout/EditOverview.php | 56 +++++++++++++++++-- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index 8075edaf1fc6..ba13c12f8156 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -22,9 +22,8 @@ use ILIAS\Questions\Persistence\Storable; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Data\UUID\Uuid; -use ILIAS\UI\Component\Table\Data as DataTable; -use ILIAS\UI\Component\Table\Ordering as OrderingTable; interface Properties extends Storable { @@ -40,7 +39,8 @@ public function getBasicPropertiesForListing( Environment $lng ): array; - public function getOverviewTable( - Environment $environment - ): DataTable|OrderingTable; + public function completeEditOverview( + Environment $environment, + EditOverview $edit_overview + ): EditOverview; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index 455a03a62fce..e2cb6983bade 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -39,10 +39,10 @@ use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\UI\Component\Input\Field\Section; -use ILIAS\UI\Component\Table\Data as DataTable; class Properties implements PropertiesInterface { @@ -198,12 +198,15 @@ public function getBasicPropertiesForListing( } #[\Override] - public function getOverviewTable( - Environment $environment - ): DataTable { - return new OverviewTable( - $environment - )->getTable(); + public function completeEditOverview( + Environment $environment, + EditOverview $edit_overview + ): EditOverview { + return $edit_overview->withTable( + new OverviewTable( + $environment + )->getTable() + ); } public function buildBasicEditingInputs( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php index ab3f5203d226..582affb3d234 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -81,11 +81,14 @@ public function edit( } if ($sub_action === '') { - return $environment->getPresentationFactory()->getEditOverview( + return $environment->getAnswerFormProperties()->completeEditOverview( $environment, - $environment->withSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) - ->withFormStartSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) - ->getUrlBuilder() + $environment->getPresentationFactory()->getEditOverview( + $environment, + $environment->withSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) + ->withFormStartSubActionParameter(self::SUB_ACTION_EDIT_BASIC_PROPERTIES) + ->getUrlBuilder() + ) ); } diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php index 42d5b1f2570c..003eed35deab 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -22,11 +22,22 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\UI\Component\Button\Standard as StandardButton; use ILIAS\UI\Component\Panel\Standard as StandardPanel; +use ILIAS\UI\Component\Table\Data as DataTable; +use ILIAS\UI\Component\Table\Ordering as OrderingTable; +use ILIAS\UI\Component\ViewControl\Mode as ViewControlMode; use ILIAS\UI\URLBuilder; class EditOverview implements Viewable { + private ?ViewControlMode $view_control = null; + /** + * @var list $buttons + */ + private array $buttons = []; + private DataTable|OrderingTable|null $table = null; + public function __construct( private readonly Environment $environment, private readonly URLBuilder $target_to_edit_basic_answer_form_properties @@ -36,12 +47,49 @@ public function __construct( #[\Override] public function getUI(): array { - return [ + $ui = [ $this->buildBasicAnswerFormPanel(), - $this->environment->getAnswerFormProperties()->getOverviewTable( - $this->environment - ) + + ]; + + if ($this->view_control !== null) { + $ui[] = $this->view_control; + } + + $ui_with_buttons = [ + ...$ui, + ...$this->buttons ]; + + if ($this->table !== null) { + $ui_with_buttons[] = $this->table; + } + + return $ui_with_buttons; + } + + public function withViewControl( + ViewControlMode $view_control + ): self { + $clone = clone $this; + $clone->view_control = $view_control; + return $clone; + } + + public function withAdditionalButton( + StandardButton $button + ): self { + $clone = clone $this; + $clone->buttons[] = $button; + return $clone; + } + + public function withTable( + DataTable|OrderingTable $table + ): self { + $clone = clone $this; + $clone->table = $table; + return $clone; } private function buildBasicAnswerFormPanel(): StandardPanel From 89e6d2685914c813d9586d2b0dcd857b4ce06a09 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 22 Jun 2026 15:09:56 +0200 Subject: [PATCH 105/108] Questions: Add Deletion to Capabilities --- .../Capabilities/AsyncView/Capability.php | 6 +++ .../AnswerForm/Capabilities/Capability.php | 4 ++ .../Capabilities/DefaultView/Capability.php | 6 +++ .../Capability.php | 6 +++ .../SuggestedLearningContent/Capability.php | 9 ++++ .../Capabilities/TextFeedback/Capability.php | 8 ++++ .../Capabilities/TextFeedback/Repository.php | 48 +++++++++++++++++++ .../MarkingAllowingPartialPoints.php | 30 ++++++------ 8 files changed, 102 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php index 12f83237f732..de0098d8878b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php @@ -59,4 +59,10 @@ public function onAnswerFormUpdate( Properties $answer_form_properties ): void { } + + #[\Override] + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void { + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index caaea9974839..caee129d78d3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -33,4 +33,8 @@ public function isAvailableFor( public function onAnswerFormUpdate( Properties $answer_form_properties ): void; + + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php index f09e3e51cee8..b1de3f137898 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php @@ -59,4 +59,10 @@ public function onAnswerFormUpdate( Properties $answer_form_properties ): void { } + + #[\Override] + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void { + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php index a4772b8c9501..2191071322ea 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php @@ -80,4 +80,10 @@ public function onAnswerFormUpdate( Properties $answer_form_properties ): void { } + + #[\Override] + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void { + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php index 13467e8a749d..48a730afc1e3 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php @@ -86,6 +86,15 @@ public function onAnswerFormUpdate( ): void { } + #[\Override] + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void { + $this->repository->delete( + $answer_form_properties->getAnswerFormId() + ); + } + #[\Override] public function getParticipantOutput( Language $lng, diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php index b57a901c041d..6a546cf24b14 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php @@ -112,6 +112,14 @@ public function onAnswerFormUpdate( ); } + public function onAnswerFormDelete( + Properties $answer_form_properties + ): void { + $this->repository->delete( + $answer_form_properties->getAnswerFormId() + ); + } + private function buildDoEditActionClosure(): \Closure { return function ( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php index 7ce2e93eaaba..3404ddc3b7f9 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php @@ -123,6 +123,54 @@ public function store( )->run(); } + public function delete( + Uuid $answer_form_id + ): void { + (new Manipulate( + $this->db, + ManipulationType::Delete, + QuestionRepository::COMPONENT_NAMESPACE + ))->withAdditionalStatement( + $this->persistence_factory->delete( + $this->persistence_factory->table( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ), + [ + $this->persistence_factory->where( + $this->feedback_table_definitions->getForeignKeyColumn( + $this->feedback_table_names_builder, + TableTypes::FeedbackGeneric + ) + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $answer_form_id->toString() + ) + ] + ) + )->withAdditionalStatement( + $this->persistence_factory->delete( + $this->persistence_factory->table( + $this->feedback_table_names_builder, + TableTypes::FeedbackSpecific + ), + [ + $this->persistence_factory->where( + $this->feedback_table_definitions->getForeignKeyColumn( + $this->feedback_table_names_builder, + TableTypes::FeedbackSpecific + ) + ), + $this->persistence_factory->value( + FieldDefinition::T_TEXT, + $answer_form_id->toString() + ) + ] + ) + )->run(); + } + private function buildQuery(): Query { return $this->feedback_table_definitions->completeQuery( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php index 6f84e1d2fd8c..ed3bf963c056 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/MarkingAllowingPartialPoints.php @@ -37,6 +37,21 @@ public function __construct( ) { } + #[\Override] + public function calculateAwardedPoints( + Properties $properties, + Response $response + ): float { + return $response->calculateAwardedPoints($properties); + } + + #[\Override] + public function getBestResponse( + Properties $properties + ): Response { + return $this->response_factory->getBestResponse($properties); + } + #[\Override] public function getEditFormInputsBuilder( Environment $environment, @@ -61,19 +76,4 @@ function (?string $carry) use ($environment): Section { ) ); } - - #[\Override] - public function calculateAwardedPoints( - Properties $properties, - Response $response - ): float { - return $response->calculateAwardedPoints($properties); - } - - #[\Override] - public function getBestResponse( - Properties $properties - ): Response { - return $this->response_factory->getBestResponse($properties); - } } From 3e6d6120af05b56369236ac97f02402fda517962 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 23 Jun 2026 08:58:41 +0200 Subject: [PATCH 106/108] Questions: Implement Async View --- .../Administration/ViewConfiguration.php | 8 + .../class.ilObjQuestionPreviewGUI.php | 25 +- .../ILIAS/Questions/Legacy/LocalDIC.php | 13 +- .../Legacy/PageEditor/QstsQuestionPage.php | 37 +-- .../PageEditor/class.QstsQuestionPageGUI.php | 9 +- .../PageEditor/class.ilPCAnswerForm.php | 172 ++----------- components/ILIAS/Questions/Questions.php | 2 +- .../js/dist/ParticipantViewLongMenu.js | 115 --------- .../Questions/resources/js/dist/questions.js | 15 ++ .../js/src/AsyncView/showFeedback.js | 77 ++++++ .../resources/js/src/Cloze/feedback.js | 140 +++++++++++ .../js/src/Cloze/longmenuAutocomplete.js | 127 ++++++++++ .../src/SuggestedLearningContent/retrieve.js | 25 ++ .../resources/js/src/TextFeedback/retrieve.js | 81 +++++++ .../Questions/resources/js/src/questions.js | 34 +++ .../resources/js/src/rollup.config.js | 27 +++ .../Capabilities/AsyncView/AsyncView.php | 225 +++++++++++++++++- .../Capabilities/AsyncView/Capability.php | 3 +- .../Capabilities/DefaultView/Capability.php | 3 +- .../Capabilities/DefaultView/DefaultView.php | 170 ++++++++++++- .../Capabilities/Definitions/Feedback.php | 27 +++ .../Capabilities/RequiredCapabilities.php | 2 - .../SuggestedLearningContent/Capability.php | 44 +--- .../SuggestedLearningContent/Feedback.php | 112 +++++++++ .../TextFeedback/SpecificTextFeedback.php | 15 +- .../TextFeedback/TextFeedback.php | 82 ++++++- .../Questions/src/AnswerForm/Response.php | 3 +- .../src/AnswerForm/Views/Participant.php | 20 +- .../Cloze/Capabilities/AsyncView.php | 54 ++++- .../Cloze/Capabilities/DefaultView.php | 54 ++++- .../Cloze/Capabilities/TextFeedback.php | 98 +++++--- .../TextFeedbackOverviewDataRetrieval.php | 6 +- .../Cloze/Properties/Gaps/Gaps.php | 18 +- .../Cloze/Properties/Gaps/LongMenu.php | 6 +- .../Cloze/Properties/Gaps/Numeric.php | 153 +++++++++--- .../Cloze/Properties/Gaps/Select.php | 4 +- .../Cloze/Properties/Gaps/Text.php | 19 +- .../Cloze/Properties/Gaps/Type.php | 36 ++- .../Cloze/Response/AnswerForm.php | 13 + .../Cloze/Response/AnswerInput.php | 14 ++ .../Cloze/Views/Participant.php | 76 ------ .../Questions/src/DefaultPublicInterface.php | 1 - .../ILIAS/Questions/src/Definitions/Range.php | 29 --- ...de.php => HasClientSideRepresentation.php} | 6 +- .../Definitions/ViewConfiguration.php | 65 +++++ .../Questions/src/Presentation/Views/Edit.php | 11 +- .../ILIAS/Questions/src/Question/Question.php | 11 +- .../Questions/src/Question/Views/Edit.php | 10 +- .../src/Question/Views/Participant.php | 12 +- .../default/tpl.cloze_gap_static.html | 2 +- .../templates/default/tpl.qsts_async.html | 7 + .../tpl.qsts_question_presentation.html | 5 - templates/default/070-components/_index.scss | 1 + .../legacy/Modules/_component_questions.scss | 19 ++ .../legacy/Modules/_component_test.scss | 6 +- templates/default/delos.css | 17 +- templates/default/delos.scss.map | 1 + 57 files changed, 1712 insertions(+), 655 deletions(-) delete mode 100644 components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js create mode 100644 components/ILIAS/Questions/resources/js/dist/questions.js create mode 100644 components/ILIAS/Questions/resources/js/src/AsyncView/showFeedback.js create mode 100644 components/ILIAS/Questions/resources/js/src/Cloze/feedback.js create mode 100644 components/ILIAS/Questions/resources/js/src/Cloze/longmenuAutocomplete.js create mode 100644 components/ILIAS/Questions/resources/js/src/SuggestedLearningContent/retrieve.js create mode 100644 components/ILIAS/Questions/resources/js/src/TextFeedback/retrieve.js create mode 100644 components/ILIAS/Questions/resources/js/src/questions.js create mode 100644 components/ILIAS/Questions/resources/js/src/rollup.config.js create mode 100644 components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Feedback.php delete mode 100644 components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php rename components/ILIAS/Questions/src/Presentation/Definitions/{ViewMode.php => HasClientSideRepresentation.php} (86%) create mode 100644 components/ILIAS/Questions/src/Presentation/Definitions/ViewConfiguration.php create mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_async.html delete mode 100644 components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html create mode 100755 templates/default/070-components/legacy/Modules/_component_questions.scss create mode 100644 templates/default/delos.scss.map diff --git a/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php b/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php index 194a8d3be806..13992e69967f 100755 --- a/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php +++ b/components/ILIAS/Questions/Legacy/Administration/ViewConfiguration.php @@ -104,6 +104,14 @@ function (array $c, string $v) use ($available_capabilities): array { ); } + public function isAsync(): bool + { + return in_array( + AsyncView::getIdentifier(), + $this->current_configuration + ); + } + public function getCurrentConfiguration(): array { return $this->current_configuration; diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php index dbe9911e8c00..a2ab66f9c24d 100755 --- a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionPreviewGUI.php @@ -59,6 +59,10 @@ class ilObjQuestionPreviewGUI implements DataRetrieval private const string CMD_RESPOND = 'respond'; private const string CMD_DELETE_RESPONSES = 'deleteResponses'; + private const string TEMPLATE_VARIABLE_FORM_ACTION = 'FORM_ACTION'; + private const string TEMPLATE_VARIABLE_QUESTION_OUTPUT = 'QUESTION_OUTPUT'; + private const string TEMPLATE_VARIABLE_SUBMIT_BUTTON_LABEL = 'SUBMIT_BUTTON_LABEL'; + private readonly Repository $repository; private readonly AnswerFormFactory $answer_form_factory; private readonly Participant $participant_view; @@ -206,6 +210,7 @@ private function showCmd(): void private function showQuestionCmd(): void { + $is_async = $this->view_configuration->isAsync(); $question_id = $this->retrieveQuestionIdFromQuery(); $attempt = $this->temp_attempt_repository->get( $this->current_user->getId() @@ -244,8 +249,8 @@ private function showQuestionCmd(): void $attempt?->getAttemptId(), true, false, - false, - false + $is_async ? true : false, + $is_async ? true : false ); if ($attempt === null) { @@ -258,13 +263,15 @@ private function showQuestionCmd(): void $content = [ $this->ui_factory->panel()->standard( $this->lng->txt('question'), - $this->ui_factory->legacy()->content( - $this->buildQuestionForm($view, $question_id) - ) + $is_async + ? $view->getUI() + : $this->ui_factory->legacy()->content( + $this->buildQuestionForm($view, $question_id) + ) ) ]; - if ($attempt?->isQuestionSolved($question_id) ?? false) { + if (!$is_async && $attempt?->isQuestionSolved($question_id) ?? false) { $content[] = $this->ui_factory->panel()->standard( $this->lng->txt('feedback'), $this->participant_view->getQuestionView( @@ -489,7 +496,7 @@ private function buildQuestionForm( ); $tpl->setVariable( - 'FORM_ACTION', + self::TEMPLATE_VARIABLE_FORM_ACTION, $this->view_configuration->getURLBuilderWithPreservedConfigurationParameter( $this->getUrlBuilderWithPreservedQuestionParameter( $question_id, @@ -500,12 +507,12 @@ private function buildQuestionForm( ); $tpl->setVariable( - 'QUESTION_OUTPUT', + self::TEMPLATE_VARIABLE_QUESTION_OUTPUT, $this->ui_renderer->render($view->getUI()) ); $tpl->setVariable( - 'SUBMIT_BUTTON_LABEL', + self::TEMPLATE_VARIABLE_SUBMIT_BUTTON_LABEL, $this->lng->txt('send') ); diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php index 3d158c45f5b0..52836d70a14d 100755 --- a/components/ILIAS/Questions/Legacy/LocalDIC.php +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -257,12 +257,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Properties\ClozeText\Factory::class], $c[Cloze\Properties\Gaps\Edit::class] ); - $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant - => new Cloze\Views\Participant( - $DIC['tpl'], - $c[MustacheEngine::class], - $c[Cloze\Response\Factory::class] - ); $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( $c[Cloze\TableDefinitions::class], [ @@ -275,10 +269,13 @@ protected static function buildDIC(ILIASContainer $DIC): self $c[Cloze\Response\Factory::class] ), new Cloze\Capabilities\DefaultView( - $c[Cloze\Views\Participant::class] + $c[MustacheEngine::class], + $c[Cloze\Response\Factory::class] ), new Cloze\Capabilities\AsyncView( - $c[Cloze\Views\Participant::class] + $DIC['ui.renderer'], + $c[MustacheEngine::class], + $c[Cloze\Response\Factory::class] ) ], $c[Cloze\Properties\Factory::class], diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 3d4088177fcc..198f8034dd1c 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -20,6 +20,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Question; @@ -31,9 +32,7 @@ class QstsQuestionPage extends ilPageObject private ?Attempt $attempt_data = null; private ?Viewable $participant_view = null; private ?RequiredCapabilities $required_capabilites = null; - private bool $interactive = true; - private bool $show_best_response = true; - private bool $show_feedback = false; + private ?ViewConfiguration $view_configuration = null; #[\Override] public function getParentType(): string @@ -96,37 +95,15 @@ public function setRequiredCapabilities( $this->required_capabilites = $required_capabilities; } - public function getInteractive(): bool + public function getViewConfiguration(): ?ViewConfiguration { - return $this->interactive; + return $this->view_configuration; } - public function setInteractive( - bool $interactive + public function setViewConfiguration( + ViewConfiguration $view_configuration ): void { - $this->interactive = $interactive; - } - - public function getShowBestResponse(): bool - { - return $this->show_best_response; - } - - public function setShowBestResponse( - bool $show_best_response - ): void { - $this->show_best_response = $show_best_response; - } - - public function getShowFeedback(): bool - { - return $this->show_feedback; - } - - public function setShowFeedback( - bool $show_feedback - ): void { - $this->show_feedback = $show_feedback; + $this->view_configuration = $view_configuration; } public function addQuestionText( diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php index 80d2992a3d75..31746c812b8a 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -20,6 +20,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Question; @@ -39,9 +40,7 @@ public function __construct( Question $question, int $obj_id, RequiredCapabilities $required_capabilities, - bool $interactive, - bool $show_best_response, - bool $show_feedback + ViewConfiguration $view_configuration ) { parent::__construct('qsts', $question->getPageId()); @@ -50,9 +49,7 @@ public function __construct( $this->obj->setQuestion($question); $this->obj->setParentId($obj_id); $this->obj->setRequiredCapabilities($required_capabilities); - $this->obj->setInteractive($interactive); - $this->obj->setShowBestResponse($show_best_response); - $this->obj->setShowFeedback($show_feedback); + $this->obj->setViewConfiguration($view_configuration); } #[\Override] diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index 05d6dc31fa73..f057179a041a 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -19,17 +19,14 @@ declare(strict_types=1); use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; -use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Legacy\LocalDIC; -use ILIAS\Questions\Presentation\Definitions\ViewMode; use ILIAS\Questions\Question\Persistence\Repository; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\UI\Component\Legacy\Content as LegacyContent; +use ILIAS\UICore\GlobalTemplate; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -39,9 +36,6 @@ class ilPCAnswerForm extends ilPageContent private const string ANSWER_FORM_ID_ATTRIBUTE = 'Uuid'; private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_([0-9a-f\-]+)\]\]\]'; - private const string TEMPLATE_VARIABLE_MAIN = 'OUTPUT'; - - public function init(): void { $this->setType('answf'); @@ -64,6 +58,7 @@ public function modifyPageContentPostXsl( } global $DIC; + $global_tpl = $DIC['tpl']; $lng = $DIC['lng']; $ui_factory = $DIC['ui.factory']; $ui_renderer = $DIC['ui.renderer']; @@ -72,6 +67,7 @@ public function modifyPageContentPostXsl( return mb_ereg_replace_callback( self::ANSWER_FORM_PLACEHOLDER, fn(array $matches): string => $this->renderAnswerForm( + $global_tpl, $lng, $ui_factory, $ui_renderer, @@ -186,6 +182,7 @@ public function getAnswerFormIdStringFromAttribute(): string } private function renderAnswerForm( + GlobalTemplate $global_tpl, Language $lng, UIFactory $ui_factory, UIRenderer $ui_renderer, @@ -197,165 +194,30 @@ private function renderAnswerForm( return $lng->txt('broken_answer_form'); } - $template = new \ilTemplate( - 'tpl.qsts_question_presentation.html', - true, - true, - 'components/ILIAS/Questions' - ); + /** @var RequiredCapabilities $required_capabilities */ + $required_capabilities = $this->pg_obj->getRequiredCapabilities(); - $question_response = $attempt_data?->getResponseForQuestion( + $answer_form_response = $attempt_data?->getResponseForQuestion( $answer_form_properties->getQuestionId() - ); - - $answer_form_response = $question_response?->getAnswerFormResponse( + )?->getAnswerFormResponse( $answer_form_properties->getAnswerFormId() ); - $main_content = $this->buildMainContent( - $lng, - $ui_factory, - $answer_form_properties, - $attempt_data, - $answer_form_response - ); - - if (!$this->pg_obj->getShowFeedback()) { - return $this->renderTemplate( - $template, - $ui_renderer->render($main_content) - ); - } - - return $this->renderTemplate( - $template, - $ui_renderer->render( - $this->addFeedbackSubPanelsToContent( + return $ui_renderer->render( + $required_capabilities + ->getParticipantViewProvider() + ->getParticipantView($answer_form_properties) + ->show( + $global_tpl, $lng, $refinery, $ui_factory, - $answer_form_properties, - $answer_form_response, - [ - $ui_factory->panel()->standard( - $this->buildMainContentLabel($lng), - $main_content - ) - ] - ) - ) - ); - - - } - - private function renderTemplate( - \ilTemplate $template, - string $content - ): string { - $template->setVariable( - self::TEMPLATE_VARIABLE_MAIN, - $content - ); - - return $template->get(); - } - - private function buildMainContentLabel( - Language $lng - ): string { - if ($this->pg_obj->getShowBestResponse()) { - return $lng->txt('best_response'); - } - - return $lng->txt('question'); - } - - private function buildMainContent( - Language $lng, - UIFactory $ui_factory, - ?AnswerFormProperties $answer_form_properties, - ?Attempt $attempt_data, - ?AnswerFormResponse $answer_form_response - ): LegacyContent { - /** @var RequiredCapabilities $required_capabilities */ - $required_capabilities = $this->pg_obj->getRequiredCapabilities(); - $participant_view = $required_capabilities - ->getParticipantViewProvider() - ->getParticipantView($answer_form_properties); - - if ($this->pg_obj->getShowBestResponse()) { - $best_response = $this->pg_obj->getRequiredCapabilities()->getMarking( - $answer_form_properties - )?->getBestResponse( - $answer_form_properties - ); - - return $ui_factory->legacy()->content( - $participant_view->show( - $lng, + $required_capabilities, + $this->pg_obj->getViewConfiguration(), $answer_form_properties, $attempt_data, - $best_response, - ViewMode::ViewBestResponse + $answer_form_response ) - ); - } - - return $ui_factory->legacy()->content( - $participant_view->show( - $lng, - $answer_form_properties, - $attempt_data, - $answer_form_response, - $this->pg_obj->getInteractive() - ? ViewMode::Respond - : ViewMode::ViewResponse - ) - ); - } - - private function addFeedbackSubPanelsToContent( - Language $lng, - Refinery $refinery, - UIFactory $ui_factory, - AnswerFormProperties $answer_form_properties, - AnswerFormResponse $answer_form_response, - array $content - ): array { - $required_capabilities = $this->pg_obj->getRequiredCapabilities(); - return array_reduce( - $required_capabilities->getRequiredFeedbackProviders(), - function ( - array $c, - FeedbackProvider $v - ) use ( - $lng, - $refinery, - $ui_factory, - $answer_form_properties, - $answer_form_response, - $required_capabilities - ): array { - $output = $v->getFeedback( - $answer_form_properties - )->getParticipantOutput( - $lng, - $refinery, - $ui_factory, - $answer_form_properties, - $answer_form_response, - $required_capabilities - ); - - if ($output === null) { - return $c; - } - - $c[] = $output->getUI(); - return $c; - }, - $content ); } } diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php index 888a349bbada..01c8fae14235 100644 --- a/components/ILIAS/Questions/Questions.php +++ b/components/ILIAS/Questions/Questions.php @@ -125,7 +125,7 @@ public function init( $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS( $this, - 'js/dist/ParticipantViewLongMenu.js' + 'js/dist/questions.js' ); $contribute[User\Settings\UserSettings::class] = fn() => new Questions\UserSettings\Settings(); diff --git a/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js b/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js deleted file mode 100644 index 47938f3bcc47..000000000000 --- a/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * This file is part of ILIAS, a powerful learning management system - * published by ILIAS open source e-Learning e.V. - * - * ILIAS is licensed with the GPL-3.0, - * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with the - * source code, too. - * - * If this is not the case or you just want to try ILIAS, you'll find - * us at: - * https://www.ilias.de - * https://github.com/ILIAS-eLearning - * - *********************************************************************/ - -(function () { - const longmenu = () => { - const init = (input, autocompleteLength, answerOptions) => { - if (input.nodeName === 'INPUT') { - let longest = answerOptions.reduce((a, b) => { - return a.length > b.length ? a : b; - }); - input.setAttribute('size', longest.length); - input.addEventListener( - 'keyup', - (e) => { keyHandler(autocompleteLength, answerOptions, e); } - ); - }; - }; - - const keyHandler = (autocompleteLength, answerOptions, e) => { - if (e.key === 'Enter' && e.target.nodeName === 'LI') { - e.stopImmediatePropagation(); - e.preventDefault(); - onSelectHandler(e); - return; - } - - if (e.key === 'ArrowDown') { - e.stopImmediatePropagation(); - e.preventDefault(); - if (e.target.nextElementSibling?.nodeName === 'UL') { - e.target.nextElementSibling.firstElementChild.focus(); - } - - if (e.target.nodeName === 'LI' && e.target.nextElementSibling !== null) { - e.target.nextElementSibling.focus(); - } - return; - } - - if (e.key === 'ArrowUp' && e.target.nodeName === 'LI') { - e.stopImmediatePropagation(); - e.preventDefault(); - if (e.target.previousElementSibling === null) { - e.target.parentElement.previousElementSibling.focus(); - } else { - e.target.previousElementSibling.focus(); - } - return; - } - - onChangeHandler(autocompleteLength, answerOptions, e); - }; - - const onChangeHandler = (autocompleteLength, answerOptions, e) => { - if (e.target.nextElementSibling?.nodeName === 'UL') { - e.target.nextElementSibling.remove(); - } - - if (e.key === 'Tab' || e.target.value.length < autocompleteLength) { - return; - } - - const matchingAnswers = answerOptions.filter((answer) => { - return answer.toLowerCase().includes(e.target.value.toLowerCase()) - }); - - if (matchingAnswers.length === 0) { - return; - } - - let list = document.createElement('ul'); - matchingAnswers.forEach((answer) => { - let listElement = document.createElement('li'); - listElement.tabIndex = 0; - listElement.textContent = answer; - list.appendChild(listElement); - }); - list.addEventListener('click', onSelectHandler); - list.addEventListener('keyup', onSelectHandler); - e.target.parentNode.appendChild(list); - }; - - const onSelectHandler = (e) => { - if (e.type === 'keydown' && e.key !== 'Enter') { - return; - } - e.target.parentNode.previousElementSibling.value = e.target.textContent; - e.target.parentNode.previousElementSibling.focus(); - e.target.parentNode.remove(); - }; - - const public_interface = { - init - }; - return public_interface; - }; - - il = il || {}; - il.test = il.test || {}; - il.test.player = il.test.player || {}; - il.test.player.longmenu = longmenu(); -}()); diff --git a/components/ILIAS/Questions/resources/js/dist/questions.js b/components/ILIAS/Questions/resources/js/dist/questions.js new file mode 100644 index 000000000000..ca6fc283a510 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/dist/questions.js @@ -0,0 +1,15 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ +!function(e){"use strict";function t(e,t,i){return"Enter"===i.key&&"LI"===i.target.nodeName?(i.stopImmediatePropagation(),i.preventDefault(),void n(i)):"ArrowDown"===i.key?(i.stopImmediatePropagation(),i.preventDefault(),"UL"===i.target.nextElementSibling?.nodeName&&i.target.nextElementSibling.firstElementChild.focus(),void("LI"===i.target.nodeName&&null!==i.target.nextElementSibling&&i.target.nextElementSibling.focus())):"ArrowUp"===i.key&&"LI"===i.target.nodeName?(i.stopImmediatePropagation(),i.preventDefault(),void(null===i.target.previousElementSibling?i.target.parentElement.previousElementSibling.focus():i.target.previousElementSibling.focus())):void function(e,t,i){"UL"===i.target.nextElementSibling?.nodeName&&i.target.nextElementSibling.remove();if("Tab"===i.key||i.target.value.lengthe.toLowerCase().includes(i.target.value.toLowerCase()));if(0===o.length)return;const r=document.createElement("ul");o.forEach(e=>{const t=document.createElement("li");t.tabIndex=0,t.textContent=e,r.appendChild(t)}),r.addEventListener("click",n),r.addEventListener("keyup",n),i.target.parentNode.appendChild(r)}(e,t,i)}function n(e){"keydown"===e.type&&"Enter"!==e.key||(e.target.parentNode.previousElementSibling.value=e.target.textContent,e.target.parentNode.previousElementSibling.focus(),e.target.parentNode.remove())}const i="gap_type",o="lower_limit",r="upper_limit",s="response";function a(e,t){const n=parseFloat(t);return Number.isNaN(e[r])||Number.isNaN(parseFloat(e[r]))?["o"]:n<=e[r]?["o","b"]:void 0===e[r]&&n===e[o]||n>=e[o]?["i"]:["o","a"]}function c(e,t){return"numeric"===e[i]&&"i"===a(e[s],t)[0]||"numeric"!==e[i]&&t===e[s].given_response}function l(e,t){return""===t?["no_response"]:"numeric"===e[i]?a(e,t):c(e,t)?["best_response"]:["other_response"]}var u={isBestResponse(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;const n=Object.keys(t);for(let i=0;ie[t],e.target.ownerDocument.defaultView);return o[n].apply(o,i)}function g(e){const t=atob(e),n=new Uint8Array(t.length);for(let e=0;ee.length>t.length?e.length:t.length)),e.addEventListener("keyup",e=>t(n,i,e)))},e.questions.cloze.specificTextFeedback=e.questions.specificTextFeedback||{},e.questions.cloze.specificTextFeedback=u,e.questions.textFeedback=e.questions.textFeedback||{},e.questions.textFeedback.retrieve=function(e,t,n,i,o){const{panelTitle:r}=n;delete n.panelTitle;const{specificFeedbackEndPoint:s}=n;delete n.specificFeedbackEndpoint;const a=p(e,s,"isBestResponse",[i,o])?"best":"other";let c="";return Object.hasOwn(n,a)&&(c=n[a]),t.replace("-panel-title-to-replace-",r).replace("-panel-content-to-replace-",c+p(e,s,"retrieveSpecificFeedback",[n,i,o]))},e.questions.suggestedLearningContent=e.questions.suggestedLearningContent||{},e.questions.suggestedLearningContent.retrieve=function(e,t,n){return n},e.questions.asyncView=e.questions.asyncView||{},e.questions.asyncView.showFeedback=function(e,t,n,i,o,...r){const s=g(t),a=JSON.parse(g(i));let c="";void 0!==o&&(c+=g(o));const l=e.target.previousElementSibling.querySelectorAll("input, select, textarea"),u=Array.from(l).reduce((e,t)=>(e[t.name]=t.value,u),{}),p=JSON.parse(g(n));r.forEach((t,n)=>{c+=t(e,s,p[n],a,u)}),e.target.nextElementSibling.innerHTML=c}}(il); diff --git a/components/ILIAS/Questions/resources/js/src/AsyncView/showFeedback.js b/components/ILIAS/Questions/resources/js/src/AsyncView/showFeedback.js new file mode 100644 index 000000000000..475345790574 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/AsyncView/showFeedback.js @@ -0,0 +1,77 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +/** + * @param {String} base64 + * @returns {string} + */ +function base64toUtf8(base64) { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i += 1) { + bytes[i] = binaryString.charCodeAt(i); + } + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} + +/** + * @param {Event} event + * @param {string} panel Standard panel scafold rendered in php. + * @param {Object} feedbackDataArray Object containing all possible feedbacks + * @param {Object} bestResponse + * @param {Object} response + * @param {...string} Endpoints the retrieve feedbacks of different feedback providers + */ +export default function showFeedback( + event, + panel, + feedbackDataArray, + bestResponse, + bestResponseOutput, + ...feedbackCallbacks +) { + const decodedPanel = base64toUtf8(panel); + const decodedBestResponse = JSON.parse( + base64toUtf8( + bestResponse, + ), + ); + + let feedback = ''; + + if (typeof bestResponseOutput !== 'undefined') { + feedback += base64toUtf8(bestResponseOutput); + } + const inputs = event.target.previousElementSibling.querySelectorAll('input, select, textarea'); + const response = Array.from(inputs).reduce( + (resp, input) => { + resp[input.name] = input.value; + return response; + }, + {}, + ); + + const decodedFeedbackData = JSON.parse(base64toUtf8(feedbackDataArray)); + + feedbackCallbacks.forEach( + (v, i) => { + feedback += v(event, decodedPanel, decodedFeedbackData[i], decodedBestResponse, response); + }, + ); + + event.target.nextElementSibling.innerHTML = feedback; +} diff --git a/components/ILIAS/Questions/resources/js/src/Cloze/feedback.js b/components/ILIAS/Questions/resources/js/src/Cloze/feedback.js new file mode 100644 index 000000000000..47def6716b46 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/Cloze/feedback.js @@ -0,0 +1,140 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +const keyGapType = 'gap_type'; +const keyLowerLimit = 'lower_limit'; +const keyUpperLimit = 'upper_limit'; +const keyResponseObject = 'response'; +const keyGivenResponse = 'given_response'; +const keyNoResponse = 'no_response'; +const keyBestResponse = 'best_response'; +const keyOtherResponse = 'other_response'; +const keyInRange = 'i'; +const keyOutOfRange = 'o'; +const keyBelowRange = 'b'; +const keyAboveRange = 'a'; + +/** + * @param {Object} bestResponse + * @param {string} response + * @return {Array} All feedback keys triggered by given response. + */ +function getKeysForNumericValue(bestResponse, response) { + const responseAsFloat = parseFloat(response); + + if (Number.isNaN(bestResponse[keyUpperLimit]) + || Number.isNaN(parseFloat(bestResponse[keyUpperLimit]))) { + return [keyOutOfRange]; + } + + if (responseAsFloat <= bestResponse[keyUpperLimit]) { + return [keyOutOfRange, keyBelowRange]; + } + + if ((typeof bestResponse[keyUpperLimit] === 'undefined' + && responseAsFloat === bestResponse[keyLowerLimit]) + || responseAsFloat >= bestResponse[keyLowerLimit]) { + return [keyInRange]; + } + + return [keyOutOfRange, keyAboveRange]; +} + +/** + * @param {Object} bestResponse + * @param {string} response + * @return {bool} + */ +function isResponseForInputBest(bestResponse, response) { + if ((bestResponse[keyGapType] === 'numeric' + && getKeysForNumericValue(bestResponse[keyResponseObject], response)[0] === keyInRange) + || (bestResponse[keyGapType] !== 'numeric' + && response === bestResponse[keyResponseObject][keyGivenResponse])) { + return true; + } + + return false; +} + +/** + * @param {Object} bestResponse + * @param {string} response + * @return {Array} All feedback keys triggered by given response. + */ +function getKeysForOtherAnswerValue(bestResponse, response) { + if (response === '') { + return [keyNoResponse]; + } + + if (bestResponse[keyGapType] === 'numeric') { + return getKeysForNumericValue(bestResponse, response); + } + + return isResponseForInputBest(bestResponse, response) + ? [keyBestResponse] + : [keyOtherResponse]; +} + +export default { + /** + * @param {Object} bestResponse + * @param {Object} response + */ + isBestResponse(bestResponse, response) { + if (Object.keys(bestResponse).length !== Object.keys(response).length) { + return false; + } + + const keys = Object.keys(response); + for (let i = 0; i < keys.length; i += 1) { + if (!isResponseForInputBest(bestResponse[keys[i]], response[keys[i]])) { + return false; + } + } + return true; + }, + /** + * @param {Object} data + * @param {Object} bestResponse + * @param {Object} response + */ + retrieveSpecificFeedback(data, bestResponse, response) { + let feedbacks = ''; + + const responseKeys = Object.keys(response); + for (let i = 0; i < responseKeys.length; i += 1) { + const property = responseKeys[i]; + if (Object.hasOwn(data, property)) { + if (Object.hasOwn(data[property], response[property])) { + feedbacks += data[property][response[property]]; + } + + const keys = getKeysForOtherAnswerValue( + bestResponse[property], + response[property], + ); + + for (let j = 0; j < keys.length; j += 1) { + if (Object.hasOwn(data[property], keys[i])) { + feedbacks += data[property][keys[j]]; + } + } + } + } + + return feedbacks; + }, +}; diff --git a/components/ILIAS/Questions/resources/js/src/Cloze/longmenuAutocomplete.js b/components/ILIAS/Questions/resources/js/src/Cloze/longmenuAutocomplete.js new file mode 100644 index 000000000000..71b2a0846c4f --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/Cloze/longmenuAutocomplete.js @@ -0,0 +1,127 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +/** + * @param {number} autocompleteLength + * @param {array} answerOptions + * @param {Event} e + */ +function keyHandler(autocompleteLength, answerOptions, e) { + if (e.key === 'Enter' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + onSelectHandler(e); + return; + } + + if (e.key === 'ArrowDown') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.firstElementChild.focus(); + } + + if (e.target.nodeName === 'LI' && e.target.nextElementSibling !== null) { + e.target.nextElementSibling.focus(); + } + return; + } + + if (e.key === 'ArrowUp' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.previousElementSibling === null) { + e.target.parentElement.previousElementSibling.focus(); + } else { + e.target.previousElementSibling.focus(); + } + return; + } + + onChangeHandler(autocompleteLength, answerOptions, e); +} + +/** + * @param {number} autocompleteLength + * @param {array} answerOptions + * @param {Event} e + */ +function onChangeHandler(autocompleteLength, answerOptions, e) { + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.remove(); + } + + if (e.key === 'Tab' || e.target.value.length < autocompleteLength) { + return; + } + + const matchingAnswers = answerOptions.filter( + (answer) => answer.toLowerCase().includes(e.target.value.toLowerCase()), + ); + + if (matchingAnswers.length === 0) { + return; + } + + const list = document.createElement('ul'); + matchingAnswers.forEach((answer) => { + const listElement = document.createElement('li'); + listElement.tabIndex = 0; + listElement.textContent = answer; + list.appendChild(listElement); + }); + list.addEventListener('click', onSelectHandler); + list.addEventListener('keyup', onSelectHandler); + e.target.parentNode.appendChild(list); +} + +function onSelectHandler(e) { + if (e.type === 'keydown' && e.key !== 'Enter') { + return; + } + e.target.parentNode.previousElementSibling.value = e.target.textContent; + e.target.parentNode.previousElementSibling.focus(); + e.target.parentNode.remove(); +} + +/** + * @param {HTMLElement} input + * @param {number} autocompleteLength + * @param {array} answerOptions + */ +export default function longmenuAutocomplete( + input, + autocompleteLength, + answerOptions, +) { + if (input.nodeName === 'INPUT') { + input.setAttribute( + 'size', + answerOptions.reduce( + (a, b) => { + if (a.length > b.length) { + return a.length; + } + return b.length; + }, + ), + ); + input.addEventListener( + 'keyup', + (e) => keyHandler(autocompleteLength, answerOptions, e), + ); + } +} diff --git a/components/ILIAS/Questions/resources/js/src/SuggestedLearningContent/retrieve.js b/components/ILIAS/Questions/resources/js/src/SuggestedLearningContent/retrieve.js new file mode 100644 index 000000000000..60318c3fe529 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/SuggestedLearningContent/retrieve.js @@ -0,0 +1,25 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +/** + * @param {Event} event + * @param {string} panel Standard panel scafold rendered in php. + * @param {Object} data Object containing all possible feedbacks + * @returns {string} + */ +export default function retrieve(event, panel, data) { + return data; +} diff --git a/components/ILIAS/Questions/resources/js/src/TextFeedback/retrieve.js b/components/ILIAS/Questions/resources/js/src/TextFeedback/retrieve.js new file mode 100644 index 000000000000..e5c90af3105c --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/TextFeedback/retrieve.js @@ -0,0 +1,81 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +const placeholderPanelTitle = '-panel-title-to-replace-'; +const placeholderPanelContent = '-panel-content-to-replace-'; + +const keyBestResponse = 'best'; +const keyOtherResponse = 'other'; + +/** + * This function is used to call a namespaced function that has been handed in + * the form of two Strings. This is needed to be able to initialize most of the + * js in a file, but still define proper endpoints in php without some funky + * template parsing of js files. + * + * @param {Event} event + * @param {string} namespace + * @param {string} func + * @param {Array} args + * @returns {string} + */ +function executeSpecificEndPointFunction(event, namespace, func, args) { + const context = namespace.split('.').reduce( + (c, v) => c[v], + event.target.ownerDocument.defaultView, + ); + return context[func].apply(context, args); +} + +/** + * @param {Event} event + * @param {string} panel Standard panel scafold rendered in php. + * @param {Object} data Object containing all possible feedbacks + * @param {Object} bestResponse + * @param {Object} response + * @returns {string} + */ +export default function retrieve(event, panel, data, bestResponse, response) { + const { panelTitle } = data; + delete data.panelTitle; + const { specificFeedbackEndPoint } = data; + delete data.specificFeedbackEndpoint; + + const key = executeSpecificEndPointFunction( + event, + specificFeedbackEndPoint, + 'isBestResponse', + [bestResponse, response], + ) ? keyBestResponse : keyOtherResponse; + + let genericFeedback = ''; + if (Object.hasOwn(data, key)) { + genericFeedback = data[key]; + } + + return panel.replace( + placeholderPanelTitle, + panelTitle, + ).replace( + placeholderPanelContent, + genericFeedback + executeSpecificEndPointFunction( + event, + specificFeedbackEndPoint, + 'retrieveSpecificFeedback', + [data, bestResponse, response], + ), + ); +} diff --git a/components/ILIAS/Questions/resources/js/src/questions.js b/components/ILIAS/Questions/resources/js/src/questions.js new file mode 100644 index 000000000000..43194811908d --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/questions.js @@ -0,0 +1,34 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + */ + +import il from 'ilias'; +import longmenuAutocomplete from './Cloze/longmenuAutocomplete.js'; +import clozeSpecificTextFeedback from './Cloze/feedback.js'; +import textFeedbackRetrieve from './TextFeedback/retrieve.js'; +import suggestedLearningContentRetrieve from './SuggestedLearningContent/retrieve.js'; +import asyncViewShowFeedback from './AsyncView/showFeedback.js'; + +il.questions = il.questions || {}; +il.questions.cloze = il.questions.cloze || {}; +il.questions.cloze.initLongmenuGap = longmenuAutocomplete; +il.questions.cloze.specificTextFeedback = il.questions.specificTextFeedback || {}; +il.questions.cloze.specificTextFeedback = clozeSpecificTextFeedback; +il.questions.textFeedback = il.questions.textFeedback || {}; +il.questions.textFeedback.retrieve = textFeedbackRetrieve; +il.questions.suggestedLearningContent = il.questions.suggestedLearningContent || {}; +il.questions.suggestedLearningContent.retrieve = suggestedLearningContentRetrieve; +il.questions.asyncView = il.questions.asyncView || {}; +il.questions.asyncView.showFeedback = asyncViewShowFeedback; diff --git a/components/ILIAS/Questions/resources/js/src/rollup.config.js b/components/ILIAS/Questions/resources/js/src/rollup.config.js new file mode 100644 index 000000000000..8ffc0a1b98f6 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/src/rollup.config.js @@ -0,0 +1,27 @@ +import terser from '@rollup/plugin-terser'; +import copyright from '../../../../../../scripts/Copyright-Checker/copyright.js'; +import preserveCopyright from '../../../../../../scripts/Copyright-Checker/preserveCopyright.js'; + +export default { + external: [ + 'document', + 'ilias', + ], + input: './questions.js', + output: { + file: '../dist/questions.js', + format: 'iife', + banner: copyright, + globals: { + document: 'document', + ilias: 'il', + }, + plugins: [ + terser({ + format: { + comments: preserveCopyright, + }, + }), + ], + }, +}; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php index 09cf3937467f..4269aba6cb26 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/AsyncView.php @@ -20,16 +20,237 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\AsyncView; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\UI\Component\Component; +use ILIAS\UICore\GlobalTemplate; -abstract class AsyncView implements TypeSpecification +abstract class AsyncView implements TypeSpecification, Participant { - abstract public function getParticipantView(): Participant; + private const string KEY_BEST_RESPONSE = 'best_response'; + private const string KEY_BEST_RESPONSE_OUTPUT = 'best_response_output'; + + private const string JAVASCRIPT_BASE_FILE = 'assets/js/questions.js'; + + private const string TEMPLATE_VARIABLE_ANSWER_FORM = 'ANSWER_FORM'; + private const string TEMPLATE_VARIABLE_CHECK_BUTTON = 'CHECK_BUTTON'; + + private const string PLACEHOLDER_PANEL_TITLE = '-panel-title-to-replace-'; + private const string PLACEHOLDER_PANEL_CONTENT = '-panel-content-to-replace-'; + + public function __construct( + protected readonly UIRenderer $ui_renderer + ) { + } + + abstract protected function getJavascriptFiles(): array; + + abstract protected function getAnswerFormPresentation( + Language $lng, + UIFactory $ui_factory, + ViewConfiguration $view_configuration, + AnswerFormProperties $answer_form_properties, + ?AdditionalAttemptData $attempt_data, + ?Response $response_data + ): string; #[\Override] final public static function getCapabilityIdentifier(): string { return Capability::getIdentifier(); } + + #[\Override] + final public function show( + GlobalTemplate $global_tpl, + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + ViewConfiguration $view_configuration, + AnswerFormProperties $answer_form_properties, + ?AdditionalAttemptData $attempt_data, + ?Response $response_data + ): Component { + $tpl = new \ilTemplate( + 'tpl.qsts_async.html', + true, + true, + 'components/ILIAS/Questions' + ); + + $tpl->setVariable( + self::TEMPLATE_VARIABLE_ANSWER_FORM, + $this->getAnswerFormPresentation( + $lng, + $ui_factory, + $view_configuration, + $answer_form_properties, + $attempt_data, + $response_data + ) + ); + + if (!$view_configuration->isInteractive()) { + return $ui_factory->legacy()->content($tpl->get()); + } + + $global_tpl->addJavaScript(self::JAVASCRIPT_BASE_FILE); + + foreach ($this->getJavascriptFiles() as $file) { + $global_tpl->addJavaScript($file); + } + + $feedback_array = $this->buildFeedbackArray( + $lng, + $refinery, + $ui_factory, + $required_capabilities, + $view_configuration, + $answer_form_properties, + $attempt_data + ); + + $tpl->setVariable( + self::TEMPLATE_VARIABLE_CHECK_BUTTON, + $this->ui_renderer->render( + $ui_factory->button()->standard( + $lng->txt('check'), + '' + )->withAdditionalOnLoadCode( + fn(string $id) => "document.querySelector(`#{$id}`).addEventListener(" + . '"click", (e) => il.questions.asyncView.showFeedback(e, "' + . base64_encode( + $this->buildFeedbackPanelWithPlaceholders($ui_factory) + ) . '", "' . base64_encode(json_encode($feedback_array['feedback_data'])) . '", "' + . ($feedback_array[self::KEY_BEST_RESPONSE] === null + ? '' + : base64_encode(json_encode($feedback_array[self::KEY_BEST_RESPONSE]))) . '", ' + . (array_key_exists(self::KEY_BEST_RESPONSE_OUTPUT, $feedback_array) + ? '"' . base64_encode($feedback_array[self::KEY_BEST_RESPONSE_OUTPUT]) . '"' + : 'undefined') + . ', ' + . implode(', ', $feedback_array['feedback_callbacks']) + . '));' + ) + ) + ); + + return $ui_factory->legacy()->content($tpl->get()); + } + + private function buildFeedbackArray( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + ViewConfiguration $view_configuration, + AnswerFormProperties $answer_form_properties, + ?AdditionalAttemptData $attempt_data, + ): array { + $best_response = $required_capabilities + ->getMarking($answer_form_properties) + ->getBestResponse($answer_form_properties); + $feedbacks = [ + self::KEY_BEST_RESPONSE => $best_response->toClientSideRepresentation() + ]; + + if ($view_configuration->showBestResponse()) { + $feedbacks[self::KEY_BEST_RESPONSE_OUTPUT] = $this->ui_renderer->render( + $ui_factory->panel()->standard( + $lng->txt('best_response'), + $ui_factory->legacy()->content( + $this->getAnswerFormPresentation( + $lng, + $ui_factory, + $view_configuration->withShowBestResponse(true), + $answer_form_properties, + $attempt_data, + $best_response + ) + ) + ) + ); + } + + if (!$view_configuration->showFeedback()) { + return $feedbacks; + } + + return $this->addFeedbacksFromProvidersToFeedbacks( + $lng, + $refinery, + $ui_factory, + $required_capabilities, + $answer_form_properties, + $feedbacks + ); + } + + private function addFeedbacksFromProvidersToFeedbacks( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + AnswerFormProperties $answer_form_properties, + array $feedbacks + ): array { + $feedbacks['feedback_callbacks'] = []; + $feedbacks['feedback_data'] = []; + + return array_reduce( + $required_capabilities->getRequiredFeedbackProviders(), + function ( + array $c, + FeedbackProvider $v + ) use ( + $lng, + $refinery, + $ui_factory, + $required_capabilities, + $answer_form_properties + ): array { + $feedback = $v->getFeedback($answer_form_properties); + $feedback_data = $feedback->getAllFeedbacksForClientSideCode( + $lng, + $refinery, + $ui_factory, + $this->ui_renderer, + $required_capabilities, + $answer_form_properties + ); + + if ($feedback_data !== []) { + $c['feedback_callbacks'][] = $feedback->getFeedbackClientSideEndPoint(); + $c['feedback_data'][] = $feedback_data; + } + + return $c; + }, + $feedbacks + ); + } + + private function buildFeedbackPanelWithPlaceholders( + UIFactory $ui_factory + ): string { + return $this->ui_renderer->render( + $ui_factory->panel()->standard( + self::PLACEHOLDER_PANEL_TITLE, + $ui_factory->legacy()->content( + self::PLACEHOLDER_PANEL_CONTENT + ) + ) + ); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php index de0098d8878b..ae2050ee4f42 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php @@ -50,8 +50,7 @@ public function getParticipantView( ): Participant { return $answer_form_properties ->getDefinition() - ->getCapability(self::getIdentifier()) - ->getParticipantView(); + ->getCapability(self::getIdentifier()); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php index b1de3f137898..ca62921d39a9 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php @@ -50,8 +50,7 @@ public function getParticipantView( ): Participant { return $answer_form_properties ->getDefinition() - ->getCapability(self::getIdentifier()) - ->getParticipantView(); + ->getCapability(self::getIdentifier()); } #[\Override] diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php index 28fd75c0ba66..46f5feccb28b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/DefaultView.php @@ -20,16 +20,182 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\DefaultView; +use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; +use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; +use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; +use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UICore\GlobalTemplate; +use ILIAS\UI\Component\Component; +use ILIAS\UI\Factory as UIFactory; -abstract class DefaultView implements TypeSpecification +abstract class DefaultView implements TypeSpecification, Participant { - abstract public function getParticipantView(): Participant; + private const string JAVASCRIPT_BASE_FILE = 'assets/js/questions.js'; + + abstract protected function getJavascriptFiles(): array; + + abstract protected function getAnswerFormPresentation( + Language $lng, + UIFactory $ui_factory, + ViewConfiguration $view_configuration, + AnswerFormProperties $properties, + ?AdditionalAttemptData $attempt_data, + ?AnswerFormResponse $response_data + ): array|Component; #[\Override] final public static function getCapabilityIdentifier(): string { return Capability::getIdentifier(); } + + #[\Override] + final public function show( + GlobalTemplate $global_tpl, + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + ViewConfiguration $view_configuration, + AnswerFormProperties $properties, + ?AdditionalAttemptData $attempt_data, + ?AnswerFormResponse $response_data + ): array|Component { + $main_content = $this->buildMainContent( + $global_tpl, + $lng, + $ui_factory, + $required_capabilities, + $view_configuration, + $properties, + $attempt_data, + $response_data + ); + + if (!$view_configuration->showFeedback()) { + return $main_content; + } + + return $this->addFeedbackSubPanelsToContent( + $lng, + $refinery, + $ui_factory, + $required_capabilities, + $properties, + $response_data, + [ + $ui_factory->panel()->standard( + $this->buildMainContentLabel( + $lng, + $view_configuration->showBestResponse() + ), + $main_content + ) + ] + ); + } + + private function buildMainContent( + GlobalTemplate $global_tpl, + Language $lng, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + ViewConfiguration $view_configuration, + ?AnswerFormProperties $answer_form_properties, + ?AdditionalAttemptData $additional_attempt_data, + ?AnswerFormResponse $answer_form_response + ): array|Component { + if ($view_configuration->showBestResponse()) { + $best_response = $required_capabilities->getMarking( + $answer_form_properties + )?->getBestResponse( + $answer_form_properties + ); + + return $this->getAnswerFormPresentation( + $lng, + $ui_factory, + $view_configuration->withShowBestResponse(true), + $answer_form_properties, + $additional_attempt_data, + $best_response + ); + } + + $global_tpl->addJavaScript(self::JAVASCRIPT_BASE_FILE); + + foreach ($this->getJavascriptFiles() as $file) { + $global_tpl->addJavaScript($file); + } + + return $this->getAnswerFormPresentation( + $lng, + $ui_factory, + $view_configuration, + $answer_form_properties, + $additional_attempt_data, + $answer_form_response + ); + } + + private function addFeedbackSubPanelsToContent( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + AnswerFormProperties $answer_form_properties, + AnswerFormResponse $answer_form_response, + array $content + ): array { + return array_reduce( + $required_capabilities->getRequiredFeedbackProviders(), + function ( + array $c, + FeedbackProvider $v + ) use ( + $lng, + $refinery, + $ui_factory, + $answer_form_properties, + $answer_form_response, + $required_capabilities + ): array { + $output = $v->getFeedback( + $answer_form_properties + )->getParticipantOutput( + $lng, + $refinery, + $ui_factory, + $answer_form_properties, + $answer_form_response, + $required_capabilities + ); + + if ($output === null) { + return $c; + } + + $c[] = $output->getUI(); + return $c; + }, + $content + ); + } + + private function buildMainContentLabel( + Language $lng, + bool $show_best_response + ): string { + if ($show_best_response) { + return $lng->txt('best_response'); + } + + return $lng->txt('question'); + } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Feedback.php index f8cb7f6db9af..ff846c40bc20 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Feedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/Feedback.php @@ -27,6 +27,7 @@ use ILIAS\Language\Language; use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; interface Feedback { @@ -38,4 +39,30 @@ public function getParticipantOutput( ?Response $response, RequiredCapabilities $required_capabilities ): ?FeedbackView; + + /** + * + * @return string A javascript callable that will receive an array with all + * the inputs provided by the user as well as JSON generated from the return + * of getAllFeedbacksForClientSide and has to return the string that will be + * shown to the participant as feedback. + */ + public function getFeedbackClientSideEndPoint(): string; + + /** + * + * @return array An array of all possible feedbacks + * as FeedbackViews structured in a way that makes it possible to retrieve + * the correct array corresponding to the answer of the user in the callback + * provided in getClientSideFeedbackEndPoint. The component takes care of + * transporting the information to javascript. + */ + public function getAllFeedbacksForClientSideCode( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + UIRenderer $ui_renderer, + RequiredCapabilities $required_capabilities, + Properties $properties + ): array; } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php index 9a4ea5742f66..b94fc33bd98f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php @@ -20,8 +20,6 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; -use ILIAS\Questions\AnswerForm\Capabilities\Capability; -use ILIAS\Questions\AnswerForm\Capabilities\Definitions\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalFormStepAction; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Marking; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\MarkingProvider; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php index 48a730afc1e3..8c43577de892 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php @@ -23,21 +23,14 @@ use ILIAS\Questions\AnswerForm\Capabilities\Capability as CapabilityInterface; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\ActionWithTab; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\AdditionalTabProvider; -use ILIAS\Questions\AnswerForm\Capabilities\Definitions\Feedback; use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackProvider; -use ILIAS\Questions\AnswerForm\Capabilities\Definitions\FeedbackView; -use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Properties; -use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; -use ILIAS\Language\Language; -use ILIAS\Refinery\Factory as Refinery; use ILIAS\StaticURL\Services as StaticURLServices; -use ILIAS\UI\Factory as UIFactory; -class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider, Feedback +class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider { public function __construct( private readonly \ilCtrl $ctrl, @@ -77,7 +70,11 @@ public function getAnswerFormEditAdditionalTab(): ActionWithTab public function getFeedback( Properties $answer_form_properties ): ?Feedback { - return $this; + return new Feedback( + $this->ctrl, + $this->static_url, + $this->repository + ); } #[\Override] @@ -95,35 +92,6 @@ public function onAnswerFormDelete( ); } - #[\Override] - public function getParticipantOutput( - Language $lng, - Refinery $refinery, - UIFactory $ui_factory, - Properties $properties, - ?Response $response, - RequiredCapabilities $required_capabilities - ): ?FeedbackView { - $content = $this->repository->getFor( - $properties->getAnswerFormId() - )->getContentForPresentation( - $lng, - $this->ctrl, - $this->static_url, - $ui_factory - ); - if ($content === null) { - return null; - } - - return new FeedbackView( - $ui_factory->panel()->standard( - $lng->txt('suggested_learning_content'), - $content - ) - ); - } - private function buildDoEditActionClosure(): \Closure { return function ( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Feedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Feedback.php new file mode 100644 index 000000000000..c25a21052ae3 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Feedback.php @@ -0,0 +1,112 @@ +buildFeedbackView( + $lng, + $properties, + $ui_factory + ); + } + + #[\Override] + public function getFeedbackClientSideEndPoint(): string + { + return 'il.questions.suggestedLearningContent.retrieve'; + } + + #[\Override] + public function getAllFeedbacksForClientSideCode( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + UIRenderer $ui_renderer, + RequiredCapabilities $required_capabilities, + Properties $properties + ): array { + $feedback_view = $this->buildFeedbackView( + $lng, + $properties, + $ui_factory + ); + return $feedback_view === null + ? [] + : [ + $ui_renderer->render( + $feedback_view->getUI() + ) + ]; + } + + private function buildFeedbackView( + Language $lng, + Properties $properties, + UIFactory $ui_factory + ): ?FeedbackView { + $content = $this->repository->getFor( + $properties->getAnswerFormId() + )->getContentForPresentation( + $lng, + $this->ctrl, + $this->static_url, + $ui_factory + ); + if ($content === null) { + return null; + } + + return new FeedbackView( + $ui_factory->panel()->standard( + $lng->txt('suggested_learning_content'), + $content + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php index 504342c8e4e3..b8e63e6d23d7 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php @@ -24,6 +24,7 @@ use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; +use ILIAS\Refinery\Factory as Refinery; class SpecificTextFeedback { @@ -65,10 +66,16 @@ public function withFeedbackText( return $clone; } - public function hasLegacyFeedback(): bool - { - return $this->feedback_text === null - && $this->feedback_legacy !== ''; + public function getFeedbackTextForPresentation( + Refinery $refinery + ): string { + if ($this->feedback_text !== null) { + return $refinery->string()->markdown()->toHTML()->transform( + $this->feedback_text->getRawRepresentation() + ); + } + + return $this->feedback_legacy; } public function toStorage( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php index 980b6e0dc3dc..a3977456fdd2 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php @@ -42,9 +42,15 @@ use ILIAS\Refinery\Factory as Refinery; use ILIAS\UI\Component\Component; use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; abstract class TextFeedback implements TypeSpecification, Feedback { + private const string KEY_PANEL_TITEL = 'panelTitle'; + private const string KEY_SPECIFIC_FEEDBACK_END_POINT = 'specificFeedbackEndpoint'; + private const string KEY_BEST_RESPONSE = 'best'; + private const string KEY_OTHER_RESPONSE = 'other'; + private ?Markdown $feedback_best_response = null; private string $feedback_best_response_legacy = ''; private ?Markdown $feedback_other_response = null; @@ -65,12 +71,12 @@ abstract public function getSpecificFeedbackTable( Environment $environment ): ?OverviewTable; - abstract public function specificFeedbackInputsHaveLegacyTexts(): bool; - abstract public function onAnswerFormUpdate( Properties $answer_form_properties ): static; + abstract protected function specificFeedbackInputsHaveLegacyTexts(): bool; + /** * @return list */ @@ -78,7 +84,24 @@ abstract protected function getSpecificFeedbackParticipantOutput( UIFactory $ui_factory, Refinery $refinery, Properties $properties, - Response $response + ?Response $response + ): array; + + /** + * The endpoint must point to a javascript namespace containing the functions + * `retrieveSpecificFeedback(data, bestResponse, response)` where `data` is + * the array produced by `getSpecificFeedbackForClientSideCode()`, `bestResponse` + * it the array produced by `Reponse::toClientSideRepresentation()` and `response` + * is an object containing all the inputs and their values as set in the + * interface. + */ + abstract protected function getSpecificFeedbackClientSideEndPoint(): string; + + abstract protected function getSpecificFeedbackForClientSideCode( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + Properties $properties ): array; #[\Override] @@ -233,9 +256,7 @@ public function withoutSpecificFeedback( return $clone; } - /** - * @return list - */ + #[\Override] public function getParticipantOutput( Language $lng, Refinery $refinery, @@ -284,6 +305,53 @@ public function getParticipantOutput( ); } + #[\Override] + public function getFeedbackClientSideEndPoint(): string + { + return 'il.questions.textFeedback.retrieve'; + } + + #[\Override] + public function getAllFeedbacksForClientSideCode( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + UIRenderer $ui_renderer, + RequiredCapabilities $required_capabilities, + Properties $properties + ): array { + $feedbacks = []; + if ($required_capabilities->isMarkingRequired()) { + $feedbacks[self::KEY_BEST_RESPONSE] = $ui_renderer->render( + $this->getRenderedFeedbackBestResponse( + $lng, + $refinery, + $ui_factory + ) + ); + + $feedbacks[self::KEY_OTHER_RESPONSE] = $ui_renderer->render( + $this->getRenderedFeedbackOtherResponse( + $lng, + $refinery, + $ui_factory + ) + ); + } + + return [ + self::KEY_PANEL_TITEL => $lng->txt('feedback'), + self::KEY_SPECIFIC_FEEDBACK_END_POINT => $this->getSpecificFeedbackClientSideEndPoint(), + ...$feedbacks, + ...$this->getSpecificFeedbackForClientSideCode( + $lng, + $refinery, + $ui_factory, + $properties + ) + ]; + } + public function requiresPageMigration(): bool { if (preg_match('/####\d+####/', $this->feedback_best_response_legacy) === 1 @@ -454,7 +522,7 @@ private function getRenderedFeedbackBestResponse( ): Component { if ($this->feedback_best_response === null) { return $ui_factory->messageBox()->success( - $this->feedback_other_response_legacy === '' + $this->feedback_best_response_legacy === '' ? $lng->txt('best_response_given') : $this->feedback_best_response_legacy ); diff --git a/components/ILIAS/Questions/src/AnswerForm/Response.php b/components/ILIAS/Questions/src/AnswerForm/Response.php index 7139066f1deb..a9ecd60efca8 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Response.php +++ b/components/ILIAS/Questions/src/AnswerForm/Response.php @@ -21,9 +21,10 @@ namespace ILIAS\Questions\AnswerForm; use ILIAS\Questions\Persistence\Storable; +use ILIAS\Questions\Presentation\Definitions\HasClientSideRepresentation; use ILIAS\Data\UUID\Uuid; -interface Response extends Storable +interface Response extends Storable, HasClientSideRepresentation { public function getAnswerFormId(): Uuid; diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php index 4e597183ebc7..d4ef39fe240a 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Participant.php @@ -20,23 +20,35 @@ namespace ILIAS\Questions\AnswerForm\Views; +use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\AnswerForm\Response; use ILIAS\Questions\Attempt\AdditionalAttemptData; -use ILIAS\Questions\Presentation\Definitions\ViewMode; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Component; +use ILIAS\UICore\GlobalTemplate; interface Participant { + /** + * @return list|Component + */ public function show( + GlobalTemplate $global_template, Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + RequiredCapabilities $required_capabilities, + ViewConfiguration $view_configuration, Properties $properties, ?AdditionalAttemptData $attempt_data, - ?Response $response_data, - ViewMode $view_mode - ): string; + ?Response $response_data + ): array|Component; public function retrieveResponse( Uuid $response_id, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php index 0a710cc5a334..c40fde5b4da4 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/AsyncView.php @@ -21,18 +21,64 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\AsyncView\AsyncView as AsyncViewInterface; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; +use ILIAS\HTTP\Wrapper\RequestWrapper; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Renderer as UIRenderer; +use Mustache\Engine as MustacheEngine; class AsyncView extends AsyncViewInterface { public function __construct( - private readonly Participant $participant_view + UIRenderer $ui_renderer, + private readonly MustacheEngine $mustache_engine, + private readonly ResponseFactory $response_factory ) { + parent::__construct($ui_renderer); } #[\Override] - public function getParticipantView(): Participant + public function getJavascriptFiles(): array { - return $this->participant_view; + return []; + } + + #[\Override] + public function getAnswerFormPresentation( + Language $lng, + UIFactory $ui_factory, + ViewConfiguration $view_configuration, + Properties $properties, + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data + ): string { + return $this->mustache_engine->render( + $properties->getClozeTextForPresentation(), + $properties->getGaps()->getPlaceholderArray( + $lng, + $view_configuration, + $additional_attempt_data, + $response_data + ) + ); + } + + #[\Override] + public function retrieveResponse( + Uuid $response_id, + Properties $properties, + RequestWrapper $post_wrapper + ): Response { + return $this->response_factory->fromPost( + $response_id, + $properties, + $post_wrapper + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php index ec6dac55e68e..f046c904b5c3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/DefaultView.php @@ -21,18 +21,64 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Capabilities; use ILIAS\Questions\AnswerForm\Capabilities\DefaultView\DefaultView as DefaultViewInterface; -use ILIAS\Questions\AnswerFormTypes\Cloze\Views\Participant; +use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerFormTypes\Cloze\Response\Factory as ResponseFactory; +use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; +use ILIAS\HTTP\Wrapper\RequestWrapper; +use ILIAS\Data\UUID\Uuid; +use ILIAS\Language\Language; +use ILIAS\UI\Factory as UIFactory; +use ILIAS\UI\Component\Component; +use Mustache\Engine as MustacheEngine; class DefaultView extends DefaultViewInterface { public function __construct( - private readonly Participant $participant_view + private readonly MustacheEngine $mustache_engine, + private readonly ResponseFactory $response_factory ) { } #[\Override] - public function getParticipantView(): Participant + public function getJavascriptFiles(): array { - return $this->participant_view; + return []; + } + + #[\Override] + public function getAnswerFormPresentation( + Language $lng, + UIFactory $ui_factory, + ViewConfiguration $view_configuration, + Properties $properties, + ?AdditionalAttemptData $additional_attempt_data, + ?Response $response_data + ): array|Component { + return $ui_factory->legacy()->content( + $this->mustache_engine->render( + $properties->getClozeTextForPresentation(), + $properties->getGaps()->getPlaceholderArray( + $lng, + $view_configuration, + $additional_attempt_data, + $response_data + ) + ) + ); + } + + #[\Override] + public function retrieveResponse( + Uuid $response_id, + Properties $properties, + RequestWrapper $post_wrapper + ): Response { + return $this->response_factory->fromPost( + $response_id, + $properties, + $post_wrapper + ); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php index f3a517576061..4d35bdb89769 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php @@ -41,7 +41,6 @@ public function __construct( private readonly UuidFactory $uuid_factory, private readonly TextFactory $text_factory, ) { - } #[\Override] @@ -71,7 +70,42 @@ public function getSpecificFeedbackTable( } #[\Override] - public function getSpecificFeedbackParticipantOutput( + public function onAnswerFormUpdate( + Properties $answer_form_properties + ): static { + $gaps = $answer_form_properties->getGaps(); + + return array_reduce( + $this->getSpecificFeedbacks(), + function ( + TextFeedback $c, + SpecificTextFeedback $v + ) use ($gaps): self { + $gap = $gaps->getGapById($v->getParentId()); + if ($gap === null + || !$gap->getType()->isValidFeedbackCondition( + $this->uuid_factory, + $gap, + $v->getCondition() + ) + ) { + return $c->withoutSpecificFeedback($v); + } + + return $c; + }, + clone $this + ); + } + + #[\Override] + public function specificFeedbackInputsHaveLegacyTexts(): bool + { + return false; + } + + #[\Override] + protected function getSpecificFeedbackParticipantOutput( UIFactory $ui_factory, Refinery $refinery, Properties $properties, @@ -129,37 +163,35 @@ function ( } #[\Override] - public function specificFeedbackInputsHaveLegacyTexts(): bool + protected function getSpecificFeedbackClientSideEndPoint(): string { - return false; + return 'il.questions.cloze.specificTextFeedback'; } #[\Override] - public function onAnswerFormUpdate( - Properties $answer_form_properties - ): static { - $gaps = $answer_form_properties->getGaps(); - - return array_reduce( - $this->getSpecificFeedbacks(), - function ( - TextFeedback $c, - SpecificTextFeedback $v - ) use ($gaps): self { - $gap = $gaps->getGapById($v->getParentId()); - if ($gap === null - || !$gap->getType()->isValidFeedbackCondition( - $this->uuid_factory, - $gap, - $v->getCondition() - ) - ) { - return $c->withoutSpecificFeedback($v); - } + protected function getSpecificFeedbackForClientSideCode( + Language $lng, + Refinery $refinery, + UIFactory $ui_factory, + Properties $properties + ): array { + $specific_feedbacks = $this->getSpecificFeedbacks(); + if ($specific_feedbacks === []) { + return []; + } - return $c; - }, - clone $this + return array_map( + fn(array $v): array => array_reduce( + $v, + function (array $c, SpecificTextFeedback $v) use ($refinery): array { + $c[$v->getCondition()] = $v->getFeedbackTextForPresentation( + $refinery + ); + return $c; + }, + [] + ), + $this->orderFeedbacksByAnswerInputId($specific_feedbacks) ); } @@ -210,16 +242,14 @@ private function addFeedbackForNoSelectionToParticipantOutput( ): array { $feedback_nothing_selected = array_filter( $specific_feedbacks, - fn(SpecificFeedback $v): bool => TextFeedbackTypes::tryFrom($v->getCondition()) === TextFeedbackTypes::NoResponse + fn(SpecificTextFeedback $v): bool => TextFeedbackTypes::tryFrom($v->getCondition()) === TextFeedbackTypes::NoResponse ); if ($feedback_nothing_selected !== []) { $participant_output[] = $ui_factory->legacy()->content( - $refinery->string()->markdown()->toHTML() - ->transform( - $feedback_nothing_selected[0]->getFeedbackText() - ->getRawRepresentation() - ) + $feedback_nothing_selected[0]->getFeedbackTextForPresentation( + $refinery + ) ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php index 4608f3c82f52..af35a05e3964 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedbackOverviewDataRetrieval.php @@ -127,10 +127,8 @@ private function buildTableRow( [ 'gap' => $gap->buildShortenedGapRepresentation(), 'answer_options' => implode('
', $answer_options), - 'feedback' => $this->refinery->string()->markdown()->toHTML()->transform( - $feedbacks[0]->getFeedbackText() - ->getRawRepresentation() - ) + 'feedback' => $feedbacks[0] + ->getFeedbackTextForPresentation($this->refinery) ] ); } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index ad5253f27852..85faaaacebff 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -34,7 +34,7 @@ use ILIAS\Questions\Persistence\Operator; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; -use ILIAS\Questions\Presentation\Definitions\ViewMode; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; @@ -200,7 +200,7 @@ public function getAddedGaps( public function getPlaceholderArray( Language $lng, - ViewMode $view_mode, + ViewConfiguration $view_configuration, ?AdditionalAttemptData $additional_attempt_data, ?Response $response_data ): array { @@ -211,11 +211,11 @@ function ( Gap $v ) use ( $lng, - $view_mode, + $view_configuration, $additional_attempt_data, $response_data ): array { - $c[$v->buildGapPlaceholderNameWithId($v)] = $view_mode === ViewMode::Respond + $c[$v->buildGapPlaceholderNameWithId($v)] = $view_configuration->isInteractive() ? $v->buildParticipantViewLegacyInput( $lng, $this->refinery, @@ -223,7 +223,7 @@ function ( $response_data ) : $this->buildStaticGapReplacement( $lng, - $view_mode, + $view_configuration->showBestResponse(), $response_data, $v ); @@ -676,7 +676,7 @@ function (array $c, Gap $v): array { private function buildStaticGapReplacement( Language $lng, - ViewMode $view_mode, + bool $show_best_response, ?Response $response_data, Gap $gap ): string { @@ -690,7 +690,7 @@ private function buildStaticGapReplacement( 'SOLUTION_VALUE', $this->retrieveStaticGapReplacementValue( $lng, - $view_mode, + $show_best_response, $response_data, $gap ) @@ -700,12 +700,12 @@ private function buildStaticGapReplacement( private function retrieveStaticGapReplacementValue( Language $lng, - ViewMode $view_mode, + bool $show_best_response, ?Response $response_data, Gap $gap ): string { $empty_gap_text = $lng->txt( - $view_mode === ViewMode::ViewBestResponse + $show_best_response ? 'no_best_response_available' : 'no_response_given' ); diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php index f1e8759f11ae..55b4980079e3 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -64,7 +64,7 @@ public function getParticipantViewLegacyInput( ?AdditionalAttemptData $additional_attempt_data, ?Response $response_data ): string { - $gap_name = $this->buildGapName($gap); + $gap_name = $gap->getAnswerInputId()->toString(); $gaptemplate = new \ilTemplate( 'tpl.cloze_gap_longmenu.html', @@ -92,7 +92,7 @@ public function getParticipantViewLegacyInput( ); } - $this->global_tpl->addOnLoadCode('il.test.player.longmenu.init(' + $this->global_tpl->addOnLoadCode('il.questions.cloze.initLongmenuGap(' . "document.querySelector('input[name=\"{$gap_name}\"]'), " . "{$gap->getMinAutocomplete()}, " . json_encode( @@ -256,7 +256,7 @@ private function retrieveResponseValueFromPost( Gap $gap ): Uuid|string { return $post_wrapper->retrieve( - $this->buildGapName($gap), + $gap->getAnswerInputId()->toString(), $this->refinery->byTrying([ $this->refinery->custom()->transformation( function (?string $v) use ($gap): Uuid|string { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index 69b75cb37a97..ea66f9207a97 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps; use ILIAS\Questions\AnswerForm\Response; +use ILIAS\Questions\AnswerForm\Capabilities\TextFeedback\SpecificTextFeedback; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Response\AnswerInput as AnswerInputResponse; @@ -33,6 +34,7 @@ use ILIAS\HTTP\Wrapper\RequestWrapper; use ILIAS\Refinery\Constraint; use ILIAS\Refinery\Transformation; +use ILIAS\UI\Component\Component; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Input\Field\Numeric as NumericInput; @@ -40,6 +42,10 @@ class Numeric extends Type { private const float DEFAULT_SUB_ACTION_SIZE = 0.0001; + private const string KEY_LOWER_LIMIT = 'lower_limit'; + private const string KEY_UPPER_LIMIT = 'upper_limit'; + private const string KEY_STEP_SIZE = 'upper_limit'; + #[\Override] public function getIdentifier(): string { @@ -61,7 +67,7 @@ public function getParticipantViewLegacyInput( $gaptemplate->setVariable( 'GAP_NAME', - $this->buildGapName($gap) + $gap->getAnswerInputId()->toString() ); $response = $response_data?->getResponseForInput($gap->getAnswerInputId()); @@ -87,14 +93,14 @@ public function getEditAnswerOptionsInputs( $ff = $environment->getUIFactory()->input()->field(); return [ - 'lower_limit' => $ff->numeric($environment->getLanguage()->txt('range_lower_limit')) + self::KEY_LOWER_LIMIT => $ff->numeric($environment->getLanguage()->txt('range_lower_limit')) ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withRequired(true) ->withValue($answer_option->getLowerLimit()), - 'upper_limit' => $ff->numeric($environment->getLanguage()->txt('range_upper_limit')) + self::KEY_UPPER_LIMIT => $ff->numeric($environment->getLanguage()->txt('range_upper_limit')) ->withStepSize($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) ->withValue($answer_option->getUpperLimit()), - 'step_size' => $ff->numeric($environment->getLanguage()->txt('step_size')) + self::KEY_STEP_SIZE => $ff->numeric($environment->getLanguage()->txt('step_size')) ->withStepSize(0.000001) ->withRequired(true) ->withValue($gap->getStepSize() ?? self::DEFAULT_SUB_ACTION_SIZE) @@ -105,9 +111,9 @@ public function getEditAnswerOptionsInputs( public function getEditAnswerOptionsSectionConstraint(): ?Constraint { return $this->refinery->custom()->constraint( - fn(array $vs): bool => $vs['upper_limit'] === null - || $vs['upper_limit'] >= $vs['lower_limit'] - && $vs['upper_limit'] >= $vs['step_size'], + fn(array $vs): bool => $vs[self::KEY_UPPER_LIMIT] === null + || $vs[self::KEY_UPPER_LIMIT] >= $vs[self::KEY_LOWER_LIMIT] + && $vs[self::KEY_UPPER_LIMIT] >= $vs[self::KEY_LOWER_LIMIT], $this->lng->txt('upper_limit_bigger_than_lower') ); } @@ -207,7 +213,7 @@ public function retrieveResponseFromPost( $gap, null, $post_wrapper->retrieve( - $this->buildGapName($gap), + $gap->getAnswerInputId()->toString(), $this->refinery->byTrying([ $this->refinery->in()->series([ $this->refinery->kindlyTo()->float(), @@ -236,20 +242,10 @@ public function isBestResponse( return false; } - $response_as_float = $this->refinery->kindlyTo()->float()->transform( + return $this->getRangeValueFromResponseValue( + $answer_option, $response->getResponse() - ); - - $upper_limit = $answer_option->getUpperLimit(); - $lower_limit = $answer_option->getLowerLimit(); - if ($upper_limit === null - && $response_as_float === $lower_limit - || $response_as_float >= $lower_limit - && $response_as_float <= $upper_limit) { - return true; - } - - return false; + ) === Range::InRange; } #[\Override] @@ -257,34 +253,83 @@ public function calculateAwardedPointsForResponse( Gap $gap, Uuid|string|null $response ): float { - /** @var ?AnswerOption $answer_option */ - $answer_options_awarding_points = $gap->getAnswerOptions() - ->getAnswerOptionsAwardingPoints(); + if ($response === null) { + return 0.0; + } - $answer_option = $answer_options_awarding_points === null - ? null - : array_shift($answer_options_awarding_points); + $answer_option = $this->getAnswerOptionAwardingPoints($gap); if ($answer_option === null) { return 0.0; } - $response_as_float = $this->refinery->kindlyTo()->float()->transform( + if ($this->getRangeValueFromResponseValue( + $answer_option, $response - ); - - $upper_limit = $answer_option->getUpperLimit(); - $lower_limit = $answer_option->getLowerLimit(); - if ($upper_limit === null - && $response_as_float === $lower_limit - || $response_as_float >= $lower_limit - && $response_as_float <= $upper_limit) { + ) === Range::InRange) { return $answer_option->getAvailablePoints(); } return 0.0; } + #[\Override] + public function buildClientSideRepresentationOfResponse( + Gap $gap, + AnswerInputResponse $response + ): array { + $answer_option = $this->getAnswerOptionAwardingPoints($gap); + + return [ + self::KEY_RESPONSE => $response->getResponse(), + self::KEY_LOWER_LIMIT => $answer_option->getLowerLimit(), + self::KEY_UPPER_LIMIT => $answer_option->getUpperLimit() + ]; + } + + #[\Override] + public function getSpecificFeedbackParticipantOutput( + UIFactory $ui_factory, + Gap $gap, + array $specific_feedbacks, + Uuid|string $answer_input_response + ): ?Component { + $specific_feedbacks_by_condition = array_reduce( + $specific_feedbacks, + function (array $c, SpecificTextFeedback $v): array { + if (!array_key_exists($v->getCondition(), $c)) { + $c[$v->getCondition()] = []; + } + + $c[$v->getCondition()] = $v->getFeedbackTextForPresentation($this->refinery); + + return $c; + }, + [] + ); + + $range_value = $this->getRangeValueFromResponseValue( + $this->getAnswerOptionAwardingPoints($gap), + $answer_input_response + ); + + if (isset($specific_feedbacks_by_condition[$range_value->value])) { + return $ui_factory->legacy()->content( + $specific_feedbacks_by_condition[$range_value->value] + ); + } + + if (($range_value === Range::BelowRange + || $range_value === Range::BelowRange) + && isset($specific_feedbacks_by_condition[Range::OutOfRange->value])) { + return $ui_factory->legacy()->content( + $specific_feedbacks_by_condition[Range::OutOfRange->value] + ); + } + + return null; + } + #[\Override] protected function retrieveResponseTextFromAnswerOption( AnswerOption $answer_option @@ -298,4 +343,40 @@ protected function retrieveResponseTextFromAnswerOption( ? $lower_limit_string : "{$lower_limit_string} - {$trafo->transform($upper_limit)}"; } + + private function getAnswerOptionAwardingPoints( + Gap $gap + ): AnswerOption { + /** @var ?AnswerOption $answer_option */ + $answer_options_awarding_points = $gap->getAnswerOptions() + ->getAnswerOptionsAwardingPoints(); + + return $answer_options_awarding_points === null + ? null + : array_shift($answer_options_awarding_points); + } + + private function getRangeValueFromResponseValue( + AnswerOption $answer_option, + string $response + ): Range { + $response_as_float = $this->refinery->kindlyTo()->float()->transform( + $response + ); + + $upper_limit = $answer_option->getUpperLimit(); + $lower_limit = $answer_option->getLowerLimit(); + + if ($response < $lower_limit) { + return Range::BelowRange; + } + + if ($upper_limit === null + && $response_as_float === $lower_limit + || $response_as_float <= $upper_limit) { + return Range::InRange; + } + + return Range::AboveRange; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php index 02d2ba1203f8..8e0dfb2a98ed 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -96,7 +96,7 @@ public function getParticipantViewLegacyInput( $gaptemplate->setVariable( 'GAP_NAME', - $this->buildGapName($gap) + $gap->getAnswerInputId()->toString() ); return $gaptemplate->get(); @@ -176,7 +176,7 @@ public function retrieveResponseFromPost( return new AnswerInputResponse( $gap, $post_wrapper->retrieve( - $this->buildGapName($gap), + $gap->getAnswerInputId()->toString(), $this->refinery->byTrying([ $this->refinery->custom()->transformation( function (?string $v) use ($uuid_factory, $gap): ?Uuid { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index 1e1fd2048a4f..af6ff686aa4a 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -69,7 +69,7 @@ public function getParticipantViewLegacyInput( } $gaptemplate->setVariable( 'GAP_NAME', - $this->buildGapName($gap) + $gap->getAnswerInputId()->toString() ); $response = $response_data?->getResponseForInput($gap->getAnswerInputId()); @@ -175,7 +175,7 @@ public function retrieveResponseFromPost( $gap, null, $post_wrapper->retrieve( - $this->buildGapName($gap), + $gap->getAnswerInputId()->toString(), $this->refinery->byTrying([ $this->refinery->kindlyTo()->string(), $this->refinery->always('') @@ -212,6 +212,16 @@ public function calculateAwardedPointsForResponse( return array_shift($answer_option)->getAvailablePoints(); } + #[\Override] + public function buildClientSideRepresentationOfResponse( + Gap $gap, + AnswerInputResponse $response + ): array { + return [ + self::KEY_RESPONSE => $response->getResponse() + ]; + } + #[\Override] public function getSpecificFeedbackParticipantOutput( UIFactory $ui_factory, @@ -222,12 +232,7 @@ public function getSpecificFeedbackParticipantOutput( $specific_feedbacks_by_condition = array_reduce( $specific_feedbacks, function (array $c, SpecificTextFeedback $v): array { - if (!array_key_exists($v->getCondition(), $c)) { - $c[$v->getCondition()] = []; - } - $c[$v->getCondition()] = $v->getFeedbackText(); - return $c; }, [] diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php index 821c6a41ae3d..ee4905f35631 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -29,7 +29,6 @@ use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Definitions\Range; -use ILIAS\Data\Text\Markdown; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\FileUpload\FileUpload; @@ -44,6 +43,8 @@ abstract class Type { + protected const string KEY_RESPONSE = 'given_response'; + public function __construct( protected readonly Language $lng, protected readonly Refinery $refinery @@ -217,6 +218,15 @@ public function calculateAwardedPointsForResponse( return $answer_option->getAvailablePoints() ?? 0.0; } + public function buildClientSideRepresentationOfResponse( + Gap $gap, + AnswerInputResponse $response + ): array { + return [ + self::KEY_RESPONSE => $response->getResponse()->toString() + ]; + } + public function getSpecificFeedbackParticipantOutput( UIFactory $ui_factory, Gap $gap, @@ -230,7 +240,7 @@ function (array $c, SpecificTextFeedback $v): array { $c[$v->getCondition()] = []; } - $c[$v->getCondition()] = $v->getFeedbackText(); + $c[$v->getCondition()] = $v->getFeedbackTextForPresentation($this->refinery); return $c; }, @@ -251,17 +261,13 @@ function (array $c, SpecificTextFeedback $v): array { if ($this->getBestResponse($gap)->getResponse() === $answer_input_response) { return isset($specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]) ? $ui_factory->legacy()->content( - $this->refinery->string()->markdown()->toHTML()->transform( - $specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value]->getRawRepresentation() - ) + $specific_feedbacks_by_condition[TextFeedbackTypes::BestResponse->value] ) : null; } return isset($specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse->value]) ? $ui_factory->legacy()->content( - $this->refinery->string()->markdown()->toHTML()->transform( - $specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse]->getRawRepresentation() - ) + $specific_feedbacks_by_condition[TextFeedbackTypes::OtherResponse] ) : null; } @@ -289,24 +295,14 @@ protected function retrieveResponseTextFromAnswerOption( return ''; } - final protected function buildGapName( - Gap $gap - ): string { - return "gap_{$gap->getAnswerInputId()->toString()}"; - } - private function getSpecificFeedbackParticipantOutputForAnswerOption( UIFactory $ui_factory, - ?Markdown $specific_feedback + ?string $specific_feedback ): ?Component { if ($specific_feedback === null) { return null; } - return $ui_factory->legacy()->content( - $this->refinery->string()->markdown()->toHTML()->transform( - $specific_feedback->getRawRepresentation() - ) - ); + return $ui_factory->legacy()->content($specific_feedback); } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php index 7ad4bb5cafe1..f2907206978e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php @@ -136,6 +136,19 @@ public function toDelete( ); } + #[\Override] + public function toClientSideRepresentation(): array + { + return array_reduce( + $this->answer_input_responses, + function (array $c, AnswerInput $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->toClientSideRepresentation(); + return $c; + }, + [] + ); + } + public function getResponseForInput( Uuid $answer_input_id ): Uuid|string|null { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php index 6c686e3a62e2..8d0b0471fcb8 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerInput.php @@ -35,6 +35,9 @@ class AnswerInput public const string KEY_SELECTED_ANSWER_OPTION = 'selected_answer_option'; public const string KEY_TEXT = 'text'; + private const string KEY_RESPONSE = 'response'; + private const string KEY_GAP_TYPE = 'gap_type'; + public function __construct( private readonly Gap $gap, private readonly ?Uuid $selected_answer_option, @@ -65,6 +68,17 @@ public function isBest(): bool return $this->gap->getType()->isBestResponse($this->gap, $this); } + public function toClientSideRepresentation(): array + { + return [ + self::KEY_GAP_TYPE => $this->gap->getType()->getIdentifier(), + self::KEY_RESPONSE => $this->gap->getType()->buildClientSideRepresentationOfResponse( + $this->gap, + $this + ) + ]; + } + public function toPreviewStorage(): array { return [ diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php deleted file mode 100644 index aa21db9459be..000000000000 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php +++ /dev/null @@ -1,76 +0,0 @@ -global_tpl->addJavaScript('assets/js/ParticipantViewLongMenu.js'); - return $this->mustache_engine->render( - $properties->getClozeTextForPresentation(), - $properties->getGaps()->getPlaceholderArray( - $lng, - $view_mode, - $additional_attempt_data, - $response_data - ) - ); - } - - #[\Override] - public function retrieveResponse( - Uuid $response_id, - Properties $properties, - RequestWrapper $post_wrapper - ): Response { - return $this->response_factory->fromPost( - $response_id, - $properties, - $post_wrapper - ); - } -} diff --git a/components/ILIAS/Questions/src/DefaultPublicInterface.php b/components/ILIAS/Questions/src/DefaultPublicInterface.php index 3332591b11a3..0e76ef76ede0 100644 --- a/components/ILIAS/Questions/src/DefaultPublicInterface.php +++ b/components/ILIAS/Questions/src/DefaultPublicInterface.php @@ -21,7 +21,6 @@ namespace ILIAS\Questions; use ILIAS\Questions\Administration\ConfigurationRepository; -use ILIAS\Questions\AnswerForm\Capabilities\Edit as CapabilitiesEditView; use ILIAS\Questions\AnswerForm\Capabilities\Factory as CapabilitiesFactory; use ILIAS\Questions\AnswerForm\Factory as AnswerFormFactory; use ILIAS\Questions\Attempt\Repository as AttemptRepository; diff --git a/components/ILIAS/Questions/src/Definitions/Range.php b/components/ILIAS/Questions/src/Definitions/Range.php index 5c398e065404..d40537683f6f 100644 --- a/components/ILIAS/Questions/src/Definitions/Range.php +++ b/components/ILIAS/Questions/src/Definitions/Range.php @@ -39,33 +39,4 @@ public function getLabel( self::AboveRange => $lng->txt('above_range') }; } - - public function isValueInRange( - float $lower_limit, - float $upper_limit, - float $value - ): self { - if ($value < $lower_limit - || $value > $upper_limit) { - return self::OutOfRange; - } - - return self::InRange; - } - - public function checkValue( - float $lower_limit, - float $upper_limit, - float $value - ): self { - if ($value < $lower_limit) { - return self::BelowRange; - } - - if ($value > $upper_limit) { - return self::AboveRange; - } - - return self::InRange; - } } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php b/components/ILIAS/Questions/src/Presentation/Definitions/HasClientSideRepresentation.php similarity index 86% rename from components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php rename to components/ILIAS/Questions/src/Presentation/Definitions/HasClientSideRepresentation.php index ab49fdbe8576..23044ae7c3fd 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/ViewMode.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/HasClientSideRepresentation.php @@ -20,9 +20,7 @@ namespace ILIAS\Questions\Presentation\Definitions; -enum ViewMode +interface HasClientSideRepresentation { - case Respond; - case ViewResponse; - case ViewBestResponse; + public function toClientSideRepresentation(): array; } diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/ViewConfiguration.php b/components/ILIAS/Questions/src/Presentation/Definitions/ViewConfiguration.php new file mode 100644 index 000000000000..787c44159bc4 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/ViewConfiguration.php @@ -0,0 +1,65 @@ +interactive; + } + + public function showMarks(): bool + { + return $this->show_marks; + } + + public function showBestResponse(): bool + { + return $this->show_best_response; + } + + public function withShowBestResponse(): self + { + $clone = clone $this; + $clone->interactive = false; + $clone->show_best_response = true; + return $clone; + } + + public function showFeedback(): bool + { + return $this->show_feedback; + } + + public function getViewMode(): ViewMode + { + return $this->view_mode; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 33991505991b..e2c34a10e51f 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -32,6 +32,7 @@ use ILIAS\Questions\Presentation\Definitions\Editability; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Definitions\ForImmediateStorage; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Factory as LayoutFactory; @@ -171,10 +172,12 @@ public function forwardPageCmds( ), $this->owner_object_id, $this->required_capabilities, - true, - false, - false, - false + new ViewConfiguration( + true, + false, + false, + false + ) )->withEditView( $this )->withReturnURI( diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index 6c9c5e01773c..c6d508ecd9b5 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -35,6 +35,7 @@ use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Presentation\Definitions\DefaultEnvironment; use ILIAS\Questions\Presentation\Definitions\OverviewTableColumns; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Questions\Question\Definitions\Lifecycle; use ILIAS\Questions\Question\Persistence\DatabaseStatementBuilder; use ILIAS\Questions\UserSettings\CreateModes; @@ -331,10 +332,12 @@ public function getParticipantView( $required_capabilities, $this, $attempt_data, - $interactive, - $show_marks, - $show_best_response, - $show_feedback + new ViewConfiguration( + $interactive, + $show_marks, + $show_best_response, + $show_feedback + ) ); } diff --git a/components/ILIAS/Questions/src/Question/Views/Edit.php b/components/ILIAS/Questions/src/Question/Views/Edit.php index 7c54405b51e6..25582f7ee9ec 100644 --- a/components/ILIAS/Questions/src/Question/Views/Edit.php +++ b/components/ILIAS/Questions/src/Question/Views/Edit.php @@ -48,6 +48,10 @@ class Edit private const string SESSION_VAR_RESPONSE_DATA = 'response_data'; + private const string TEMPLATE_VARIABLE_FORM_ACTION = 'FORM_ACTION'; + private const string TEMPLATE_VARIABLE_QUESTION_OUTPUT = 'QUESTION_OUTPUT'; + private const string TEMPLATE_VARIABLE_SUBMIT_BUTTON_LABEL = 'SUBMIT_BUTTON_LABEL'; + public function __construct( private readonly \ilObjUser $current_user, private readonly \ilCtrl $ctrl, @@ -331,7 +335,7 @@ private function buildQuestionForm( ); $tpl->setVariable( - 'FORM_ACTION', + self::TEMPLATE_VARIABLE_FORM_ACTION, $environment ->withSubActionParameter(self::CMD_SEND_RESPONSE) ->getUrlBuilder() @@ -340,7 +344,7 @@ private function buildQuestionForm( ); $tpl->setVariable( - 'QUESTION_OUTPUT', + self::TEMPLATE_VARIABLE_QUESTION_OUTPUT, $this->ui_renderer->render( $this->question->getParticipantView( $environment->getLanguage(), @@ -353,7 +357,7 @@ private function buildQuestionForm( ); $tpl->setVariable( - 'SUBMIT_BUTTON_LABEL', + self::TEMPLATE_VARIABLE_SUBMIT_BUTTON_LABEL, $environment->getLanguage()->txt('send') ); diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php index 3533c364d733..40385e1e1f26 100644 --- a/components/ILIAS/Questions/src/Question/Views/Participant.php +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -22,6 +22,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\RequiredCapabilities; use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Presentation\Definitions\ViewConfiguration; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Question\Question; use ILIAS\Data\UUID\Uuid; @@ -38,10 +39,7 @@ public function __construct( private readonly RequiredCapabilities $required_capabilities, private readonly Question $question, private readonly ?Attempt $attempt_data, - private readonly bool $interactive, - private readonly bool $show_marks, - private readonly bool $show_best_response, - private readonly bool $show_feedback + private readonly ViewConfiguration $view_configuration ) { } @@ -57,14 +55,12 @@ public function getUI(): array $this->question, $this->question->getParentObjId(), $this->required_capabilities, - $this->interactive, - $this->show_best_response, - $this->show_feedback + $this->view_configuration )->withAttemptData($this->attempt_data); $question_page->setPresentationTitle($this->question->getTitle()); - if ($this->show_marks) { + if ($this->view_configuration->showMarks()) { $content[] = $this->ui_factory->listing()->characteristicValue()->text( [ $this->lng->txt('awarded_points') => $this->refinery->kindlyTo()->string()->transform( diff --git a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html index b428c058b060..e5cf7503b46f 100755 --- a/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html +++ b/components/ILIAS/Questions/templates/default/tpl.cloze_gap_static.html @@ -1 +1 @@ -{SOLUTION_VALUE} \ No newline at end of file +{SOLUTION_VALUE} \ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_async.html b/components/ILIAS/Questions/templates/default/tpl.qsts_async.html new file mode 100644 index 000000000000..2df34a01b580 --- /dev/null +++ b/components/ILIAS/Questions/templates/default/tpl.qsts_async.html @@ -0,0 +1,7 @@ +
+
+ {ANSWER_FORM} +
+ {CHECK_BUTTON} + +
\ No newline at end of file diff --git a/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html b/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html deleted file mode 100644 index 70665a7d4e63..000000000000 --- a/components/ILIAS/Questions/templates/default/tpl.qsts_question_presentation.html +++ /dev/null @@ -1,5 +0,0 @@ -
-
- {OUTPUT} -
-
\ No newline at end of file diff --git a/templates/default/070-components/_index.scss b/templates/default/070-components/_index.scss index fa625b1ab3aa..7cc0aefd786c 100755 --- a/templates/default/070-components/_index.scss +++ b/templates/default/070-components/_index.scss @@ -88,6 +88,7 @@ @use "./legacy/Modules/_component_orgunit.scss"; @use "./legacy/Modules/_component_poll.scss"; @use "./legacy/Modules/_component_portfolio.scss"; +@use "./legacy/Modules/_component_questions.scss"; @use "./legacy/Modules/_component_scormaicc.scss"; @use "./legacy/Modules/_component_survey.scss"; @use "./legacy/Modules/_component_test.scss"; diff --git a/templates/default/070-components/legacy/Modules/_component_questions.scss b/templates/default/070-components/legacy/Modules/_component_questions.scss new file mode 100755 index 000000000000..06791bee6405 --- /dev/null +++ b/templates/default/070-components/legacy/Modules/_component_questions.scss @@ -0,0 +1,19 @@ +@use "../../../010-settings/" as *; +@use "../../../050-layout/basics" as *; + +$il-questions-solution-value-background: $il-highlight-bg; +$il-questions-solution-value-padding: $il-padding-base-vertical; +$il-questions-async-answerform-padding: $il-padding-large-vertical; + +.c-questions__solution-value { + background: $il-questions-solution-value-background; + padding: $il-questions-solution-value-padding; +} + +.c-questions__async-answerform { + padding-bottom: $il-questions-async-answerform-padding; +} + +.c-questions_async-answerform-feedback { + padding-top: $il-questions-async-answerform-padding; +} diff --git a/templates/default/070-components/legacy/Modules/_component_test.scss b/templates/default/070-components/legacy/Modules/_component_test.scss index c1ab0123e04d..8ca27135fdda 100755 --- a/templates/default/070-components/legacy/Modules/_component_test.scss +++ b/templates/default/070-components/legacy/Modules/_component_test.scss @@ -398,8 +398,4 @@ $cons-scoring-bottom-fade-height: $il-padding-xxxlarge-vertical * 2; } } } -} - -.c-test__solution-value { - background: $il-test-solution-value-background -} +} \ No newline at end of file diff --git a/templates/default/delos.css b/templates/default/delos.css index df11de7dde69..dc942606aae3 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -15942,6 +15942,19 @@ body.ilPrtfPdfBody .ilPCMyCoursesToggle img { visibility: hidden; } +.c-questions__solution-value { + background: rgb(226.2857142857, 231.6428571429, 238.7142857143); + padding: 3px; +} + +.c-questions__async-answerform { + padding-bottom: 6px; +} + +.c-questions_async-answerform-feedback { + padding-top: 6px; +} + /* Modules/ScormAicc */ table.il_ScormTable { color: #161616; @@ -17076,10 +17089,6 @@ div.ilc_Page.readonly textarea[disabled] { height: 50vh; } -.c-test__solution-value { - background: rgb(226.2857142857, 231.6428571429, 238.7142857143); -} - /* Modules/Wiki */ a.ilWikiPageMissing:link, a.ilWikiPageMissing:visited { color: #d00; diff --git a/templates/default/delos.scss.map b/templates/default/delos.scss.map new file mode 100644 index 000000000000..11f17f06e2dc --- /dev/null +++ b/templates/default/delos.scss.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["020-dependencies/_index.scss","020-dependencies/modifications/datetimepicker/bootstrap-datetimepicker.scss","030-tools/_tool_browser-prefixes.scss","010-settings/_settings_typography.scss","030-tools/_tool_screen-reader-only.scss","010-settings/_settings_borders.scss","010-settings/_settings_color-palette.scss","010-settings/_settings_button.scss","010-settings/legacy-settings/_legacy-settings_menu.scss","070-components/UI-framework/Dropdown/_ui-component_dropdown.scss","030-tools/legacy-bootstrap-mixins/_nav-divider.scss","050-layout/basics/_layout_spacing-variables.scss","010-settings/legacy-settings/_legacy-settings_form.scss","020-dependencies/modifications/_jquery-autocomplete.scss","020-dependencies/modifications/_additions_tinymce.scss","020-dependencies/modifications/_additions_yui2.scss","040-normalize/_index.scss","040-normalize/_normalize_print.scss","040-normalize/_normalize_typography.scss","040-normalize/_normalize_input.scss","040-normalize/_normalize_structure.scss","040-normalize/_normalize_table.scss","050-layout/_layout_grid.scss","050-layout/_layout_container.scss","050-layout/_layout_element-bar.scss","050-layout/_layout_visibility-utilities.scss","060-elements/_index.scss","060-elements/_elements_dialog.scss","060-elements/_elements_html-body.scss","060-elements/_elements_input.scss","030-tools/_tool_focus-outline.scss","060-elements/_elements_lists.scss","060-elements/_elements_media.scss","060-elements/_elements_objects.scss","060-elements/_elements_tables.scss","060-elements/_elements_typography.scss","060-elements/_elements_details-summary.scss","070-components/_index.scss","070-components/UI-framework/_ui-component_tooltip.scss","030-tools/_tool_typography-mixins.scss","070-components/UI-framework/Breadcrumbs/_ui-component_breadcrumbs.scss","050-layout/standardpage/_layout_standardpage.scss","070-components/UI-framework/Button/_ui-component_button.scss","030-tools/_tool_buttons.scss","070-components/UI-framework/Button/_ui-component_tag.scss","070-components/UI-framework/Button/_ui-component_toggle.scss","070-components/UI-framework/Card/_ui-component_card.scss","010-settings/legacy-settings/_legacy-settings_panel.scss","070-components/UI-framework/Chart/_ui-component_chart.scss","010-settings/legacy-settings/_legacy-settings_chart.scss","070-components/UI-framework/Counter/_ui-component_counter.scss","070-components/UI-framework/Deck/_ui-component_deck.scss","070-components/UI-framework/Divider/_ui-component_divider.scss","070-components/UI-framework/Dropzone/_ui-component_dropzone.scss","010-settings/legacy-settings/_legacy-settings_dropzone.scss","070-components/UI-framework/Entity/_ui-component_entity.scss","070-components/UI-framework/Item/_ui-component_item.scss","070-components/UI-framework/Launcher/_ui-component_launcher.scss","010-settings/legacy-settings/_legacy-settings_symbol.scss","070-components/UI-framework/MainControls/_ui-component_metabar.scss","050-layout/standardpage/_layout_standardpage-mobile.scss","070-components/UI-framework/Layout/_ui-component_standardpage.scss","050-layout/basics/_layout_z-index.scss","010-settings/_settings_header.scss","050-layout/_layout_container-query.scss","070-components/UI-framework/Layout/_ui-component_alignment.scss","070-components/UI-framework/Link/_ui-component_link.scss","070-components/UI-framework/Listing/_ui-component_properties.scss","030-tools/_tool_clearfix.scss","070-components/UI-framework/Listing/_ui-component_characteristic_value.scss","050-layout/basics/_layout_positioning.scss","070-components/UI-framework/Listing/_ui-component_workflow.scss","070-components/UI-framework/Listing/_ui-component_entitylisting.scss","070-components/UI-framework/MainControls/Slate/_ui-component_slate.scss","030-tools/_tool_multi-line-cap.scss","070-components/legacy/_component_screen-reader-only.scss","070-components/UI-framework/MainControls/_ui-component_mainbar.scss","010-settings/_settings_mainbar.scss","070-components/UI-framework/MainControls/_ui-component_footer.scss","010-settings/_settings_footer.scss","050-layout/_layout_breakpoints.scss","070-components/UI-framework/MainControls/_ui-component_mode_info.scss","010-settings/_settings_shadows.scss","070-components/UI-framework/MainControls/_ui-component_system_info.scss","070-components/UI-framework/Menu/_ui-component_drilldown.scss","070-components/UI-framework/Input/_ui-component_tag.scss","070-components/UI-framework/Input/_ui-component_password.scss","070-components/UI-framework/Input/_ui-component_radio.scss","070-components/UI-framework/Input/_ui-component_multiselect.scss","070-components/UI-framework/Input/_ui-component_filter.scss","070-components/UI-framework/Input/_ui-component_file.scss","010-settings/legacy-settings/_legacy-settings_ui-input-file.scss","070-components/UI-framework/Input/_ui-component_markdown.scss","050-layout/_layout_form.scss","070-components/UI-framework/Input/_ui-component_option-filter.scss","070-components/UI-framework/Input/_ui-component_rating.scss","070-components/UI-framework/Input/_ui-component_section.scss","070-components/UI-framework/Input/_ui-component_numeric.scss","070-components/UI-framework/Input/_ui-component_optionalgroups.scss","070-components/UI-framework/Input/_ui-component_tree_select.scss","070-components/UI-framework/Input/_ui-component_input.scss","070-components/UI-framework/MessageBox/_ui-component_messagebox.scss","070-components/UI-framework/Modal/_ui-component_modal.scss","030-tools/_tool_dialog-patterns.scss","070-components/UI-framework/Navigation/_ui-component_sequence.scss","070-components/UI-framework/Panel/_ui-component_panel.scss","030-tools/_tool_border-radius.scss","070-components/UI-framework/Player/_ui-component_player.scss","020-dependencies/modifications/webui-popover/jquery.webui-popover.scss","070-components/UI-framework/Popover/_ui-component_popover.scss","070-components/UI-framework/Progress/_ui-component_progress_bar.scss","070-components/UI-framework/Symbol/_ui-component_icon.scss","070-components/UI-framework/Symbol/_ui-component_glyph.scss","070-components/UI-framework/Symbol/_ui-component_avatar.scss","070-components/UI-framework/Table/_ui-component_table.scss","030-tools/_tool_highlighted-box.scss","070-components/UI-framework/Toast/_ui-component_toast.scss","070-components/UI-framework/Tree/_ui-component_tree.scss","010-settings/legacy-settings/_legacy-settings_tree.scss","070-components/UI-framework/ViewControl/_ui-component_viewcontrol.scss","070-components/legacy/_component_agreement.scss","070-components/legacy/_component_alert.scss","070-components/legacy/_component_bottom-center-area.scss","070-components/legacy/_component_headline.scss","070-components/legacy/_component_helpsidebar.scss","070-components/legacy/_component_icon.scss","070-components/legacy/_component_LeftNavSpace.scss","070-components/legacy/_component_link.scss","070-components/legacy/_component_map.scss","070-components/legacy/_component_media-object.scss","070-components/legacy/_component_rightPanel.scss","070-components/legacy/_component_delostable.scss","070-components/legacy/_component_well.scss","070-components/legacy/_component_php.scss","070-components/legacy/_component_animated-collapse-fade.scss","070-components/legacy/_component_btn-group.scss","050-layout/_layout_responsive-img.scss","070-components/legacy/_component_carousel.scss","070-components/legacy/_component_input-group.scss","070-components/legacy/Modules/_component_bibliographic.scss","070-components/legacy/Modules/_component_blog.scss","070-components/legacy/Modules/_component_bookingmanager.scss","070-components/legacy/Modules/_component_chatroom.scss","070-components/legacy/Modules/_component_course.scss","070-components/legacy/Modules/_component_datacollection.scss","070-components/legacy/Modules/_component_excercise.scss","070-components/legacy/Modules/_component_forum.scss","070-components/legacy/Modules/_component_learningmodule.scss","070-components/legacy/Modules/_component_learningsequence.scss","070-components/legacy/Modules/_component_lticonsumer.scss","070-components/legacy/Modules/_component_mediacast.scss","070-components/legacy/Modules/_component_mediapool.scss","070-components/legacy/Modules/_component_orgunit.scss","070-components/legacy/Modules/_component_poll.scss","070-components/legacy/Modules/_component_portfolio.scss","070-components/legacy/Modules/_component_questions.scss","070-components/legacy/Modules/_component_scormaicc.scss","070-components/legacy/Modules/_component_survey.scss","070-components/legacy/Modules/_component_test_legacy.scss","070-components/legacy/Modules/_component_test.scss","030-tools/_tool_fade-edge.scss","070-components/legacy/Modules/_component_wiki.scss","070-components/legacy/Modules/_component_workspacefolder.scss","070-components/legacy/Modules/_component_studyprogramme.scss","070-components/legacy/Services/_component_accesscontrol.scss","070-components/legacy/Services/_component_accordion.scss","070-components/legacy/Services/_component_awareness.scss","070-components/legacy/Services/_component_cron.scss","070-components/legacy/Services/_component_badge.scss","070-components/legacy/Services/_component_block.scss","070-components/legacy/Services/_component_bookmarks.scss","070-components/legacy/Services/_component_calendar.scss","070-components/legacy/Services/_component_chart.scss","070-components/legacy/Services/_component_container.scss","070-components/legacy/Services/_component_copage.scss","070-components/legacy/Services/_component_fileupload.scss","070-components/legacy/Services/_component_form.scss","070-components/legacy/Services/_component_help.scss","070-components/legacy/Services/_component_infoscreen.scss","070-components/legacy/Services/_component_init.scss","070-components/legacy/Services/_component_learninghistory.scss","070-components/legacy/Services/_component_like.scss","070-components/legacy/Services/_component_mail.scss","070-components/legacy/Services/_component_openlayers.scss","070-components/legacy/Services/_component_mediaobjects.scss","070-components/legacy/Services/_component_membership.scss","070-components/legacy/Services/_component_navigation.scss","070-components/legacy/Services/_component_news.scss","070-components/legacy/Services/_component_notes.scss","070-components/legacy/Services/_component_object.scss","070-components/legacy/Services/_component_onscreenchat.scss","070-components/legacy/Services/_component_rating.scss","070-components/legacy/Services/_component_search.scss","070-components/legacy/Services/_component_skill.scss","070-components/legacy/Services/_component_style.scss","070-components/legacy/Services/_component_table.scss","070-components/legacy/Services/_component_tags.scss","070-components/legacy/Services/_component_termsofservice.scss","070-components/legacy/Services/UIComponent/_component_checklist.scss","070-components/legacy/Services/UIComponent/_component_explorer2.scss","070-components/legacy/Services/UIComponent/_component_lightbox.scss","070-components/legacy/Services/UIComponent/_component_modal.scss","070-components/legacy/Services/UIComponent/_component_progressbar.scss","070-components/legacy/Services/UIComponent/_component_tabs.scss","070-components/legacy/Services/UIComponent/_component_toolbar.scss","070-components/legacy/Services/_component_user.scss","070-components/legacy/Services/_component_webdav.scss","080-hacks/_index.scss","050-layout/_layout_pull-float.scss"],"names":[],"mappings":";AAAA;AAAA;AAAA;ACKA;AAAA;AAAA;AAAA;AAAA;AAmBA;EACI;;AAEA;EACI;EACA;EACA;;AAGI;EADJ;IAEQ;;;AAGJ;EALJ;IAMQ;;;AAGJ;EATJ;IAUQ;;;AAIR;EACI;EACA;EACA;;AAIA;EACI;EACA;EACA;EACA,qBAtCiC;EAuCjC;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;EACA,kBAzDiC;EA0DjC;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;;AAGJ;EACI;EACA;;AAKZ;EACI;;AAGJ;EACI;;AAGJ;ECvCF,oBDwCM;ECvCE,YDuCF;;AAGJ;EACI;EACA,aE5EiB;EF6EjB,WEnGc;EFoGd;;AAGJ;EACI;;AAGJ;EGvHA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHiHI;;AAGJ;EG5HA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHsHI;;AAGJ;EGjIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EH2HI;;AAGJ;EGtIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHgII;;AAGJ;EG3IA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHqII;;AAGJ;EGhJA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EH0II;;AAGJ;EGrJA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EH+II;;AAGJ;EG1JA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHoJI;;AAGJ;EG/JA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHyJI;;AAGJ;EACI;;AAEA;EGvKJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHiKQ;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAKZ;EACI;EACA;;AAGA;EAEI;EACA,eIjMY;;AJoMhB;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EAEI;EACA,OKhMS;ELiMT;;AAGJ;EGtNR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHgNY;;AAGJ;EG3NR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHqNY;;AAIR;EACI;;AAEA;EACI,YKtME;ELuMF,OK/LM;;ALmMd;EACI;EACA;EACA;;AAEA;EACI,WExOM;EFyON;EACA;EACA,OKjOS;;ALoOb;EACI;EACA;EACA;;AAGJ;EAII,YKjOE;ELkOF,OK1NM;EL2NN;;AAGJ;EAEI,OKrPS;;ALwPb;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA,qBMnQA;ENoQA,kBAvQ6B;EAwQ7B;EACA;EACA;;AAIR;EAEI,kBM7QI;EN8QJ,OMhRO;ENiRP,aAhRiB;;AAmRrB;EACI;;AAGJ;EAEI;EACA,OKtRS;ELuRT;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA,eIjTQ;;AJmTR;EACI,YKvRF;ELwRE,OKhRE;;ALmRN;EACI,kBM5SA;EN6SA,OM/SG;ENgTH,aA/Sa;;AAkTjB;EACI,OK/SK;;ALkTT;EAEI;EACA,OKrTK;ELsTL;;AAOZ;EACI;EACA;;AAIX;EACC;;AAGD;EACO;;;AAKJ;EACI;EACA;;AACA;EACI;;;AOxWZ;AC2CA;EACC;EACA;EACA;EACA;EACA;EACA;;;AAIC;AAAA;EAED;EACA;EACA;;;AAIC;EACC;EACF;EACA;EACA,SAtD0B;EAuD1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WN1DsB;EM2DtB;EACA;EACA,kBHlDY;EGmDZ;EACA;EACA,eJxEuB;EH+DtB,oBOUD;EPTS,YOST;;AAKA;EACE;EACA;;AAGF;EACE;EACA;;AAEF;EACE;EACA;;AAKF;EC/FC;EACA;EACA;EACA,kBDU+B;;AAqFhC;EACC;;AAID;EACE;EACA;EACA;EACA;;;AAMF;EAGE,OHhGU;EGiGV;EACA,kBHvHa;EGwHb;;;AASF;EAGE,OHlHsB;;AGsHxB;EAEE;EACA,QAzGe;EA0Gf;EACA;;;AAQF;EACE;;AAIF;EACE;;;AAQD;EACD;EACA;;;AAQC;EACD;EACA;;;AAIC;EACD;EACA;EACA;EACA;EACA;EACA;;;AAIC;EACD;EACA;;;AAWA;AAAA;EACE;EACA;EACA;EACA;;AAGF;AAAA;EACE;EACA;EACA;;;AAKH;EACC;EACA,OHzLe;EG0Lf,kBD7NoB;EC8NpB;EACA;;AACA;EACC;EACA,eE/MuB;EFgNvB,WNtNoB;;;AMyNtB;EACC,kBHjNY;EGkNZ,aNvMwB;EMwMxB;EPvKC,oBOwKD;EPvKS,YOuKT;;AAEA;EACC;;AACA;EACC;;AAKF;AAAA;EAEC;EACA;EACA;EACA;EACA,aNzNuB;EM0NvB,WNhPqB;EMiPrB,aNtOqB;EMuOrB,kBD9PiB;EC+PjB,OH3Nc;EG4Nd,YG3NoB;EH4NpB;EACA;EACA;EACA;;AACA;AAAA;AAAA;EAEC,OH9NmB;EG+NnB,kBHvOe;EGwOf;;AAED;AAAA;EACC,kBFvOkB;;AE0OpB;EACC;EACA;EACA;EACA;EACA,SD7Qa;;;ACkRf;EACC;EACA;;;AAGA;EACC;;;AAIF;EPhOE,oBOiOE;EPhOM,YOgON;;;AASJ;EACC;;;AAMA;EACC;EACA;;;AIrTF;AACA;EACC;EACA;EACA;EACA;EACA;EACA,kBPeY;EOdZ;EACA;EACA;EXwDC,oBWvDE;EXwDM,YWxDN;;AACH;EACC;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA,OPWa;EOVb,kBL1BgB;;AK2BhB;EACC,kBPKc;EOJd,OPOY;;AOJd;EACC,OPGa;;AOFb;EACC,OPMkB;;AOFrB;EACC;EACA;EACA;EACA;EACA;EACA;EACA,aVVuB;;AUYxB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;;AAED;EACC;;;AAKH;EACC;EACA;AACA;EACA;;AAEA;EACC;;;AC7EF;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;AACA;EACC;EACA;;;ACjCF;EACC,OTsCe;;;ASlCf;EACC;;AAED;EACC;EACA;;;ACZF;AAAA;AAAA;ACEA;AACA;EAEE;AACE;AACA;AAAA;AAAA;AAAA;;EAMF;AAAA;IAEE;;EAGF;IACE;;EAGF;IACE;;EAIF;AAAA;IAEE;;EAGF;AAAA;IAEE;IACA;;EAGF;IACE;;EAGF;AAAA;IAEE;;EAGF;IACE;;EAGF;AAAA;AAAA;IAGE;IACA;;EAGF;AAAA;IAEE;;EAKF;IACE;;EAIF;IACE;;EAGA;AAAA;IAEE;;EAKF;AAAA;IACE;;EAGJ;IACE;;EAGF;IACE;;EAGA;AAAA;IAEE;;EAKJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;IAUE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAGF;IACE;;EAIF;IACE;;EAGF;IACE;;EAGF;IACE;IACA;;;ACvIJ;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;EAEA;EACA;;AAKD;EACC;EACA;EAEA;;AAGD;EACC;EACA;EAGA;EACA;;ACjGD;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;ACNF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQI;;;ACNJ;EACI;EACA;;;AAGJ;AAAA;EAEI;;;ACuQA;EAnJA;EACA;EACA;EACA;EAEA;EACA;EACA;;AA+II;EAtIJ;EACA;EACA;EACA;EACA;EACA;;;AAxCI;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AA9HZ;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AA9HZ;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AA9HZ;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AA9HZ;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AA9HZ;EAqFI;IACI;;EAGJ;IAhCR;IACA;;EASA;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAFJ;IACI;IACA;;EAgCI;IA5CR;IACA;;EAiDgB;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EA6DY;IA9DZ;IACA;;EAyEQ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;EAPJ;AAAA;IAEI;;EAGJ;AAAA;IAEI;;;AC/MhB;AAAA;EAZA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAWA;ACvBJ;EACI;EACA;EACA;;;AAOJ;AAAA;EAGI;EACA;EACA;EACA;;;AAGJ;EACI;;AACA;EACI;;;AAIR;AAAA;EAEI;;AACA;AAAA;EACI;;;AAIR;AAAA;EAEI;;;AAGJ;AAAA;EAEI;;AACA;AAAA;EACI;;;AC7BR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAaE;;;AAGF;EAzBE;IACE;;EAEF;IAAmB;;EACnB;IAAmB;;EACnB;AAAA;IACmB;;;AAuBnB;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIJ;EA5CE;IACE;;EAEF;IAAmB;;EACnB;IAAmB;;EACnB;AAAA;IACmB;;;AA0CnB;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIJ;EA/DE;IACE;;EAEF;IAAmB;;EACnB;IAAmB;;EACnB;AAAA;IACmB;;;AA6DnB;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIJ;EAlFE;IACE;;EAEF;IAAmB;;EACnB;IAAmB;;EACnB;AAAA;IACmB;;;AAgFnB;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAIF;EADF;IAEI;;;;AAWJ;EALE;IACE;;;AAQJ;EATE;IACE;;;AAYJ;EAbE;IACE;;;AAgBJ;EAjBE;IACE;;;AADF;EACE;;;AA6BJ;EArIE;IACE;;EAEF;IAAmB;;EACnB;IAAmB;;EACnB;AAAA;IACmB;;;AAkIrB;EACE;;AAEA;EAHF;IAII;;;;AAGJ;EACE;;AAEA;EAHF;IAII;;;;AAGJ;EACE;;AAEA;EAHF;IAII;;;;AAIJ;EAvDE;IACE;;;ACpHJ;AAAA;AAAA;ACEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;IAAO;;EACP;IAAK;;;AAGP;EACE;IAAO;IAA6B;IAAY;;EAChD;IAAK;IAA4B;IAAY;;;ACvB/C;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;AACA;EAFD;IAGE;;;;AAIF;EACC,azBZ2B;EyBa3B,WzBJsB;EyBKtB,azBMsB;EyBLtB,OtBkBe;EsBjBf,kBtBIY;;AsBHZ;EAND;IAOE;IACA;IACA;;;;AAIF;AACA;EACC;;;AC5BD;EACC;;ACmCA;EACC,SAHwB;EAIxB;;;ADhCD;EADD;IAEE;;;;AAKD;EADD;IAEE;;;;AAIF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOC,YjBYqB;;;AiBTtB;AAAA;EAEC;EACA;EACA;;AAGA;AAAA;AAAA;AAAA;EAGC,QjBJgB;;;AkB5BjB;AAAA;AAAA;EACC;EACG;;AAEJ;AAAA;AAAA;EACC;EAEA,SARwB;EASxB;;AAEA;AAAA;AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;;AD6C1B;AAAA;AAAA;AAAA;AAAA;AAAA;EAMC;;AClBA;AAAA;AAAA;AAAA;AAAA;AAAA;EACC,SAHwB;EAIxB;;;AC1CF;EACC;;;AAGD;EACC,cpBO8B;EoBN3B;;;AAGJ;AAAA;EAEC;EACA;;;AAGD;EACC;IACC;;;ACjBF;EACC;;AACA;EAFD;AAGE;IACA;;;;ACJF;EACC;;;AAGD;EACC;EACA;EACA;;;ACPD;EACC,W/BcsB;E+BbnB;;;AAGJ;EACC;;;AAGD;EACI;EACH;EACG;;;ACAJ;AAAA;EAEE,ahCR0B;EgCS1B,ahC4BwB;EgC3BxB,ahCcwB;EgCbxB,O7BwBkB;;A6BtBlB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;EACA;EACA,O7BkBgB;;;A6BdpB;AAAA;AAAA;EAGE,YhCHwB;EgCIxB;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAGJ;AAAA;AAAA;EAGE;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAIJ;EAAU,WhC3BW;;;AgC4BrB;EAAU,WhC9BgB;;;AgC+B1B;EAAU,WhCjCe;;;AgCkCzB;EAAU,WhCpCY;;;AgCqCtB;EAAU,WhCvCa;;;AgCwCvB;EAAU,WhC1CY;;;AgCgDtB;EACE;;;AAKF;EACE,O7B/Dc;E6BgEd;EACA;AACA;AAAA;AAAA;AAAA;EAID;AACA;;AACC;EAEE,O7B1BkB;E6B2BlB,iBhClCuB;;A2B6B1B;EACC;EACA,QAJwB;EAKxB;;;AKYF;AAAA;EAGE,WhClFqB;;;AgCsFvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AAGvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AAGvB;EACE,O7B5FiB;;;A6BqGjB;AAAA;AAAA;AAAA;EAEE;;;AAYJ;EAJE;EACA;;;AAQF;EACE;EACA,ehCjHwB;;;AgCmH1B;AAAA;EAEE,ahCtHqB;;;AgCwHvB;EACE;;;AAEF;EACE;;;AAOF;EACE;EACA;EACA,WhCjJqB;EgCkJrB;;AAKE;AAAA;AAAA;EACE;;;AAMN;EACE,ehCnJwB;EgCoJxB;EACA,ahCtJqB;;;AgCyJvB;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC,ahClKwB;;;AgCqKzB;EACC;EACA,WhCjMqB;EgCkMrB,O7BjKqB;;;A6BoKtB;EACC;;;AAGD;EACC;;;AAGD;EACC;IACC;;;AC3NF;EACE,e/BGe;E+BFf,ezBc6B;;AyBb7B;EACE;EACA;;AACA;EACE,czBawB;EyBZxB;EACA;;AAGJ;EACE;;AAGA;EACE;;;ACpBN;AAAA;AAAA;AAQA;ACKA;EACC;;;AAGD;EACC;;;AAKD;EACC;EACA;EACA;EACA;EACA;EACA,qBhCtBe;EgCuBf,SAnBmB;;;AAuBpB;EACC;EACA;EACA;EACA;EACA;EACA;EACA,SA9BmB;;;AAkCpB;AAAA;EAEC;;;AAID;AAAA;AAAA;EAGC;EACA;;;AAID;EACC,kBhCrDe;EgCsDf;;;AAID;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OhCjCe;EgCkCf,YhC1CkB;EgC2ClB;EACA,WAzEqB;EA0ErB,YAzEsB;EA0EtB;EACA;EACA,SAtEmB;;ACTlB;EACC;;AAED;EACC;;;AD+EH;EACC;;;AAGD;AACA;EACC;;;AEtFD;EACC;EACA;EACA,YlCoBY;;AkClBZ;EACC;EACA;EACA;EACA;EACA;EACA;EACA,arCwBuB;EqCvBvB,WrCHoB;EqCIpB;EACA,cCNiC;;ADQjC;EACC,OlChBa;;AkCiBb;EACC,OlC8BkB;;AwBmCrB;EACC;EACA;EACA;EACG;;AAEF;EACC;;AAIH;EACC;EACA;EACA;EACG;;AAEF;EACC;;AAlEH;EACC,SAHwB;EAIxB;;AUbA;EACC,SCWsC;EDVtC,OlCRsB;EkCStB;EACA,arCIsB;EqCHtB;;;AA4BH;AAEA;EACC;EACA,WrCrDqB;;AqCuDrB;EACC;;AAGD;EARD;IASE;IACA;;;;AAIF;AACA;EACC;;AAEA;EACC;;AAED;EACC;EACA,OlChEuB;EkCiEvB,SC/CuC;EDgDvC;;;AEtDF;AAAA;ECmFY;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,aAfa;;ADzEzB;AAAA;EC4FQ;EACA;EACA,QA3BW;EA4BX;EACA;EAGA,axChIoB;EwCiIpB;EACA,aD3HQ;EC4HR;EACA,axCrGiB;EwCsGjB,iBDlIY;;AZ+BnB;AAAA;EACC,SAHwB;EAIxB;;AYPF;AAAA;EC+GQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;;A+BgB5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAKF;AAAA;AAAA;AAAA;EAEE;;;AAKJ;AAAA;EACE,W9BjB0B;;A8BmB1B;EAHF;AAAA;IAII;;;;AAIJ;ECuFQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBrC3JQ;EqC4JR,OpCtJgB;EoCuJhB,cDvJS;ECwJT;EACA,crC/JQ;EqCiKJ,etC/JY;;AsCmKhB;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OpCnKY;EoCoKZ,cDpKK;ECqKL;EACA,cA5GS;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OpC9KY;EoC+KZ,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;EACI,OpCtLY;EoCuLZ,iBDzLQ;;AC6LZ;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;AAqBJ;EACI,kBpC1MQ;EoC2MR,cD3Je;EC4Jf;EACA,crClOI;EqCmOJ,OrCjMI;;;AoC2ChB;ECgEQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBpC5IY;EoC6IZ,OpC/Ie;EoCgJf,cDvJS;ECwJT;EACA,cpC9IgB;EoCgJZ,etC/JY;;AsCmKhB;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OpC5JW;EoC6JX,cDpKK;ECqKL;EACA,cA5GS;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OpCvKW;EoCwKX,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;EACI,OpC/KW;EoCgLX,iBDzLQ;;AC6LZ;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;AAqBJ;EACI,kBpC1MQ;EoC2MR,cDpIe;ECqIf;EACA,cpCjNY;EoCkNZ,OrCjMI;;;AoC6DhB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECkBY;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,aAfa;;ADRzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EC2BQ;EACA;EACA,QA3BW;EA4BX;EACA;EAGA,axChIoB;EwCiIpB;EACA,aD3HQ;EC4HR;EACA,axCrGiB;EwCsGjB,iBDlIY;;AZ+BnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACC,SAHwB;EAIxB;;AY0DF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EC8CQ,YpC/GmB;EoCmHf,WpCnHe;EoCqHnB,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBrC5HU;EqC6HV,OrC5JQ;EqC6JR,cDvJS;ECwJT;EACA,crChIU;EqCkIN,etC1JuB;;AsC8J3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OrCzKI;EqC0KJ,cDpKK;ECqKL;EACA,crC5KI;;AqCgLR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OrCpLI;EqCqLJ,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,OrC5LI;EqC6LJ,iBDzLQ;;AC6LZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;AAqBJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,kBA/IW;EAgJX,cAvIe;EAwIf;EACA,crCnMM;EqCoMN,OAlJa;;ADyBvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;EACA,kBpCxFS;;AoC0FX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;;;AAKF;AAAA;AAAA;EAGE,a/BrGuB;;;A+BoH3B;EACE;EACD;EACA;EACC;EACA;EACA,avC7GuB;EuC8GvB,OpC9Ic;;AoCgJd;EAKE;ExCpFF,oBwCqFE;ExCpFM,YwCoFN;;AAEF;EAGE;;AAEF;EACE,OpC9GkB;EoC+GlB,iBvCtHuB;EuCuHvB;;AAEF;EAEE,kBnCjIiB;EmCkIjB,OnCnIoB;EmCoIpB;EACA,Q9BvIc;;A8BwId;EACE;;AAGJ;EACE,OpC3IY;EoC4IZ,kBpC/Ic;;;AoCyJlB;AAAA;EC3CQ,YDtHiB;ECwHb;EAIJ,WxCzIe;EwC2If;EACA,KhCnJoB;EgCwJpB,kBrC/HY;EqCgIZ,OrC1HQ;EqC2HR,cDvJS;ECwJT;EACA,crCnIY;EqCqIR,etC/JY;;AsCmKhB;AAAA;EACI,iBDlKQ;ECoKR,kBrCzIM;EqC0IN,OrCvII;EqCwIJ,cDpKK;ECqKL;EACA,crC7IM;;AqCiJV;AAAA;EAGI,kBAvGU;EAwGV,OrClJI;EqCmJJ,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;AAAA;EACI,OrC1JI;EqC2JJ,iBDzLQ;;AC6LZ;AAAA;AAAA;AAAA;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;;AAyHd;AAAA;EACI,kBrChMM;EqCiMN,cDhBe;ECiBf;EACA,crCtMQ;EqCuMR,ODrBa;;ACqCb;AAAA;EACI;EACA;EACA,KhCnPY;EgCoPZ;;AAGJ;AAAA;EACI;;AbrLf;AAAA;EACC;EACA,QAJwB;EAKxB;;;AYoJA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE,eD/LuB;;ACgMvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;EACA;;;AAQN;EC7FQ,YDtHiB;EC0Hb,WD1Ha;EC4HjB,WxCvIc;EwCyId;EACA,KhCtIsB;;;A+BkO9B;ECrGQ,YDyGe;ECrGX,WDqGW;ECnGf,WxC7Ie;EwC+If;EACA,KhCtIsB;;;A+B4O9B;EACE;;;AAGF;EACE;;;AAOF;EACC;EACC,kBnCvOmB;EmCwOnB,cnCvOuB;;AmCwOvB;EACE,kBnC1OiB;EmC2OjB,cnC1OqB;;AmCoOzB;EAQC;EACA;EACA;;;AAGD;EACE;EACA,avCrPuB;EuCsPvB;EACA,OpCvQuB;EoCwQvB;EACA;;AAEA;EAEE,OpClRe;EoCmRf;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;;;AEzSF;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AArCR;ED0IQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBrC3JQ;EqC4JR,OrCvIK;EqCwIL,cDvJS;ECwJT;EACA,crC/JQ;EqCiKJ,etCzJuB;;AsC6J3B;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OrCpJC;EqCqJD,cDpKK;ECqKL;EACA,cA5GS;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OrC/JC;EqCgKD,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;EACI,OrCvKC;EqCwKD,iBDzLQ;;AC6LZ;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;AAqBJ;EACI,kBpC1MQ;EoC2MR,cC3KmB;ED4KnB;EACA,crClOI;EqCmOJ,OrCjMI;;;AuCehB;EACE;EACA;EACA,QA3CqB;EA4CrB,cA3CsB;EA4CtB,eA5CsB;EA6CtB,eA3C4B;EA4C1B;;;AAGJ;EACE;EACA,KA5CkC;EA6ClC,MAzC4C;EA0C5C,OA7C0B;EA8C1B,QA9C0B;EA+C1B,eArD4B;EAsD5B,oBA/CgC,uBA+CsB;EACtD,YAhDgC;;;AAoDhC;EACE,YAnEoC;EAoEpC;;AACA;EACE;EACA,KApD4B;EAqD5B,MApDiC;EAqDjC,a1C5CmB;E0C6CnB,W1CvEgB;E0CwEhB,OvC3DO;;AuC6DT;EACE,MA/DuC;EAgEvC,YvC/DO;EuCgEP;;AAGJ;EACE,YxC5FmB;EwC6FnB;;AACA;EACE;EACA,KArE4B;EAsE5B,MApEkC;EAqElC,a1C7DmB;E0C8DnB,W1CxFgB;;A0C0FlB;EACE,YvC9EO;EuC+EP;;AAGJ;EACE,YvCtFqB;EuCuFrB;;AACA;EACE,YvC9Fa;EuC+Fb;;;AAOJ;EACE,QlCpHuB;;AkCsHzB;EACE,W1C/GkB;;;A0CmHtB;EACE;;AAEA;EACE;EACA,elChH0B;;;AmCC9B;EACC;EACA;EACA;EACA;EACA,QCtBiB;EDuBjB,eARuB;EASvB,YCzBiB;;AD2BjB;EACC;EACA;;AAEA;EACC;;AAIF;EACC;EACA;EACA;;AArBF;AAwBC;;AACA;EACC;;AAGD;EACC,W3CrCqB;;A2CwCtB;EACC;EACA,W3C5CoB;E2C6CpB,ezC7CiC;;AyC+CjC;EACC;;AAGD;EACC;;AAGF;EACC,YxCxDiB;EwCyDjB,QnC9D0B;EmC+D1B;;AAED;EACC;EACA,QnCnE0B;EmCoE1B;;AAGD;EACC;EAGA;EACA;;AAEA;EACC;;AAKA;EACC,a3CxDqB;E2CyDrB,OxChDkB;EwCiDlB,anCvFwB;;AmC4F3B;EACC;EACA;;AAEA;EACC;EACA;;AAGK;EACI,OxCzGI;;AwC4Gd;EACC;EACA;;AAEA;EACC;;AAKF;EACC;EACA;;AAGC;EACC;EACA;EACA;EACA,QAvHgC;EAwHhC;EACA;;AAEA;EACC;EACA;EACA;;;AAQN;EACC,kBxCtHiB;;;AwCyHlB;EAEE;IACC;IACA;;EAEA;IACC;;;AAMJ;AACA;EACC;IACC,WAtJqB;IAuJrB;IACA;;;AEnKF;AACA;AACA;AACA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE,W7CpBqB;E6CqBrB;EACA,O1CMc;E0CLd;EACA,kBC/B0B;EDgC1B;EACA;EACA;;;AAGF;EACE,OCvC2B;EDwC3B,kB1CtCc;;;A0CyChB;AACA;AACA;AACA;EACE;EACA;;AAEA;EACE,OCdgC;;ADiBlC;EACE;EACA,WClD+B;EDmD/B,WCjD+B;EDkD/B;EACA;;AAEA;EACE;EACA;EACA,WC1D6B;ED2D7B,YCzD6B;;AD2D7B;EACE,QClD0B;EDmD1B;EACA,cC5DiC;ED6DjC;;AAGF;EACE;EACA;;AAGA;EACE,cClEkC;;ADsEpC;EACE,cCrEiC;;ADyErC;EACE,QChE4B;;ADkE9B;EACE,QCrEyB;;ADuE3B;EACE,QCpE4B;;ADsE9B;EACE,QCrEwB;;ADwE1B;EACE;;AAGA;EACE,W7CnGa;E6CoGb,a7CxEe;E6C0Ef,MC9EgC;;ADgFlC;EACE;EACA,MClFgC;;ADoFlC;AAAA;EAEE;EACA,MCrF+B;;ADyFnC;EAEE;;AAEA;EACE,QC1GmC;ED2GnC;;AAEF;EACE,MChH4B;EDiH5B;;AAIJ;EACE;;AAOJ;EACE,WC1GkC;ED2GlC,WCzGkC;;AD2GlC;EACE,WC9GgC;ED+GhC,YC7GgC;;AD+GhC;EACE,QC1G6B;ED2G7B,cC/IoC;;ADkJtC;EACE,cCnJoC;;ADsJtC;EACE,QC/G+B;;ADiHjC;EACE,QChH4B;;ADmH9B;EACE,QCxHiC;EDyHjC;EACA,cC9HwC;ED+HxC;EAEA;;AAEF;EACE;;;AAQV;AACA;AACA;AACA;EACE,arCxL4B;EqCyL5B,gBrCzL4B;;;AqC4L9B;EACE,arC7L4B;EqC8L5B,gBrC9L4B;;;AuCa9B;EACE;EACA,W/CbqB;E+CcrB,aAjBuB;EAkBvB;EACA;EACA;EACA;EACA,O5CJW;E4CKX;EACA,e7CpBiC;;A6CqBjC;EACE;;;AAIJ;EACE;EACA,W/CzBqB;;;A+C4BvB;EACE,SAtCmB;EAuCnB;EACA,KA1BgC;EA2BhC,kB5ClCiB;;;A4CoCnB;EACE,SA5CmB;EA6CnB;EACA,QAtCkC;EAuClC,kB5C9BuB;;;A4CkCvB;EACA,aAlDuB;EAmDvB,W/ChDqB;E+CiDrB,SAtDmB;EAuDnB;;;AC3DA;EACE;EACA;EACA,exCS0B;;AwCP1B;EACE,cxCgBqB;EwCfrB,exCeqB;EwCdrB,exCFsB;;AwCMtB;EACE;;AAGF;EACE;;;AAMR;EAEE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AC1BJ;EACC;EACA;;;AAGD;AAAA;EAEC;EACA;EACA;EACA;;;AAGD;EACC;;;AAED;EACC;;;AAGD;EACC;EACA;EACA,kB9CLY;E8CMZ;EACA,WjDpBqB;EiDqBrB,ajDHsB;EiDItB,O9CIe;;;A8CDhB;EACC;;;AAGD;EACC;EACA,czCvB6B;EyCwB7B,ezCxB6B;;;A0Cb9B;EACC;EACA,eCF2B;EDG3B,Y/CeY;E+CdZ;EACA,e1CW0B;E0CV1B;;AACA;EACC;EACA,ehDNgC;EgDOhC,kB/CYgB;E+CXhB;;;AAIF;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA,YChC0B;;;ADmC3B;EACC;EACA,kBCnCsB;;;ADsCvB;EACC,WlD/BsB;;;AkDkCvB;EACC;;;AAID;AAAA;EAEC;;;AErDG;EACI;EACA,qBACI;EAQJ;EACA,kBjDWK;EiDVL;;AACA;EACI;;AAIR;EACI;EACA,WpDLiB;;AoDQrB;EACI;EACA;EACA;;AACA;EACI;;AAKJ;EAGI;;AAEJ;EACI;;AAEJ;EACI;;AAVR;EAYI;;AAGJ;EACI;EACA,apDdiB;EoDejB,WpDjCkB;;AoDoCtB;EACI;EACA,WpDxCiB;;AoD2CrB;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EAGI;;AAGJ;EACI;EACA;EACA;EACA;;;ACnER;EACC,enDjBgB;;;AmDoBjB;EACC;EACA;EACA,S7ChB8B;;A6CkB9B;EACC,WrDhBqB;EqDiBrB;;AACA;EACC;EACA,arDToB;;AqDWpB;EACC;;AAKH;EACC;;AAGD;EACC;;AAGD;EACC;EACA,SA7CwB;EA8CxB,QA7CuB;EA8CvB;EACA;EACA,eA/C8B;;AAkD/B;EACC;EACA;;AAGD;EACC,SAvD4B;EAwD5B,WrDtDoB;EqDuDpB;;AAGD;EACC,WrD3DoB;EqD4DpB,OlD3BoB;EkD4BpB;;AACA;EACC;;AAIF;EACC,WrDpEoB;EqDqEpB;;AAGD;EACC,WP7EiC;EO8EjC;EACA;;AAIA;EADD;IAEE,a7CrFyB;;;A6CyF3B;EACC;;AAGD;EACC;;AAGD;EACC,SA9FsB;;;AAmGxB;EACC,aAlG4B;;;AAqG7B;EACC;EACA,kBlDvFiB;;AkDyFjB;EACC;EACA,OlDlFc;EkDmFd,WrDzGoB;EqD0GpB,arDtFuB;EqDuFvB,ST1HyB;ES2HzB;;AAGD;EACC;;;AAIF;EACC;;AAEA;EACC;EACA;EACA;;;AAID;EACC,Y7CxI0B;;;A6C4I5B;EAEC;;AAEA;EACC,WrDxIoB;EqDyIpB;EACA;;AAGD;EACC;;AAGD;EAEC;;AAGD;EACC;;;AAKF;EACC;EACA,KA9JmB;;AAgKnB;EACC;EACA;EACA,qBACC;EAID;;AAEA;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;EACA;EACA;EACA,KA3LgB;;;AAgMnB;AACA;EACC;;;AAGD;EAEE;IACC,SAhNuB;IAiNvB;IACA;;EAKD;IACC;;;AC5NC;Ed4JI,kBpC5IY;EoC6IZ,OpC/Ie;EoCgJf,cDvJS;ECwJT;EACA,cpChJY;EoCkJR,etC1JuB;;AsC8J3B;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OpC5JW;EoC6JX,cDpKK;ECqKL;EACA,cA5GS;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OpCvKW;EoCwKX,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;EACI,OpC/KW;EoCgLX,iBDzLQ;;AC6LZ;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;AAqBJ;EACI,kBA/IW;EAgJX,cAvIe;EAwIf;EACA,cpCnNQ;EoCoNR,OAlJa;;AclFrB;EAUI;EACA;;AACA;EACI;;AAEJ;EACI,OnDMC;;AmDJL;EAlBJ;IAmBQ;;;;AAKZ;EACI;EACA;EACA,e9CpByB;;;A8CuB7B;EACI;EACA;;AACA;EACI;;;AAIR;EACI,c9CtB0B;E8CuB1B;EACA;;AACA;EACI,QCxCa;EDyCb,OCzCa;;;ACarB;EACC;EACG;EACA;EAEH;;AAGA;EvDvBG;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AuDmBH;EACC;EACA;EACA,WlBD0B;EkBE1B,chDf4B;EgDgB5B;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC,WApDsB;;A7BkEvB;EACC;EACA,QAJwB;EAKxB;;A6BdA;EAEA,QA5DwB;EA6DxB;EACA,QA7DwB;EA8DxB,WA7DuB;;AA+DvB;EAPA;IAQC,QCzEoC;ID0EpC,WC1EoC;;;AD4ErC;EAEC;;AAED;EACC,YlB9BuB;EkB+BvB,kBAtE0B;EAuE1B;EACA,QA7EuB;;A7BqEzB;EACC;EACA,QAJwB;EAKxB;;A6BQA;EACC,OrDhEsB;EqDiEtB,WA/EqB;;AAmFvB;EACC;IACC,ehD5E8B;;EgD6E9B;IACC;;EACA;IACC,chD7EyB;;EgD+E1B;IACC;IACA,chD9EsB;;EgD+EtB;IACC;;EAED;IACC,YhD7FwB;;;;AgDqG9B;EACC,YlBlEyB;EkBmEtB,kBA1GyB;EA2G5B;EAEA,WA9GyB;;AA+GzB;EACC,WAhHwB;;AAyG1B;EASC;EACA;EACA;EAEA,KAzHyB;;AA2HzB;EACC;EACA;;AAGD;EApBD;IAqBE;IACA,KC1IqC;ID2IrC;;EAEA;IACC,OrDtHsB;IqDuHtB,WA5H2B;;EA+H5B;IACC;;EAIA;IACC;IAEA;IACA;IACA,WlBzHwB;IkB0HxB,OhDnJ0B;IgDoJ1B,KhD5I2B;IgD6I3B,axDnIsB;;EwDuIxB;IACC;;EAIA;IACC;;;;AEhKJ;AAAA;AAAA;AAAA;AAAA;AAOA;EACC,YvDUY;EuDTZ;EACA;EACG;EACA;EACA;EACH;EACA;EACA;EACA;EACA;EACA;;AAEC;EACC;;AAKF;EACC;;AAEA;EACC;;AAGA;EACC;EACA;EACA,SCzCqC;;AD0CrC;EACC;EACA;EACA;;AAIF;EACC;;;AAOJ;EACC;EACA;EACA;EACA,SCjEgC;EDkEhC,YpBduC;;;AoBkBxC;EACC;EACA;EACA,YpBjCqC;EoBkCrC;EACA,kBvDjDY;EuDkDZ;EACA;EACA,SCzEqC;ED0ErC,YpB3BuC;;;AoB+BxC;EACC;EACA;EACA;EACA,SCtFoC;;;AD0FrC;EACI;EACA,YvDnES;EuDoET;EACA;EACA,QE7F6B;EF8F7B;EACA;EACA;EACA;;;AAIJ;EACC,QExG8B;EFyG9B;EACA;EACA;EACE;;AACF;EACC;;;AAIF;EACC,a1D7EwB;E0D8ExB;EACA,W1DrGqB;E0DsGrB;EACA;EACA;EACA,OvDnFe;;;AuDsFhB;EACC;;;AAGD;EACC;EACA;EACA,SClIsC;;;ADsIvC;EACC;EACA;EACA;EACA;EACA,OpBzHsB;;AoBgIvB;EACC;EACA;EACA,SCpJiC;;;ADwJlC;EACC;EACA;;;AAID;EACC;;;AAIA;EACC;;AAFF;EAIC;EACA;EACA;;AACA;EACI;EACA;;;AAIJ;EGtJO,gBHuJyC;EGtJzC;;AAuBA;EHiIL;IACC;;;;AAMJ;AAAA;AAAA;AAAA;AAAA;AAOA;EACC;AAAA;IAEC;IACA;;;AAIF;EAEC;IACC,YvDpLW;IuDqLX;IACA;IACA;;EACA;IACC;IACA;IACA,SCnNoC;IDoNpC;;EACA;IACC;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACC;;EAED;IACC;IACA;IACA;IACA;IACA,YpBrLmB;;EoBuLpB;IACC;IACA;IACA;IACA;;EAIH;IACC;IACA;;EACA;IACC;IACA;IACA;IACA;IACA;;EACA;IACC;IACG;IACH;IACA;IACA;IACA;;EACA;IACC,YpBhNkB;;EoBmNnB;IACC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,SC5QmC;;ED6QnC;IACC;;EAIF;IAEC;;EAQL;IACC;IACA;IACA;IACA;;EAID;IACC;IACG,QD3SkC;IC4SlC;IACH,YpBxPsC;;EoB4PvC;IACC,ODhTkC;;ECoTnC;IACC;;EAID;IACC;;EAGD;IACC;;EAEA;IACC;IACA;IACA;IACA;;EAED;IACC;IACA;;EAGD;IACC;IACA;IACA;IACA;IACA,OvD/TgB;IuDgUhB,W1DjUmB;I0DkUnB;IACA;IACA;IACA;IACA;;EACA;IACI;IACA;IACA,W1D1Ue;I0D2Uf,clDzVW;;EkD6VhB;IACC;;EAED;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;IAQC,YvDjVU;IuDkVV,cvDlVU;IuDmVV,OvD3VgB;IuD4VhB;;EAGD;IACC;;EAKF;IACC;IACA;;EAID;IACC;IACA;IACA;IACA,YDjYsC;ICkYtC,YA5XyD;IA6XzD;IACA,SCjYgC;;EDqYjC;IACC;IACG;IACH;IACG;;EAGJ;IACC,QD9Y4B;IC+Y5B,OD9Y2B;;ECkZ3B;IACC,QDpZ2B;ICqZ3B,ODpZ0B;;ECsZ3B;IACC;IACA;IACA;IACA;;EAGD;IACI,QD/ZwB;;EC0a7B;IACC;IACA;IACA;IACA;;EAQD;IACC;;EACA;IACC;;EAKF;IACC;;EAGD;IACC;IACA;IACA;IACA,gBDxcsC;;ECyctC;IACC;;EAIF;IACC,epBrcwB;;;AoByc1B;AAAA;AAAA;AAAA;AAAA;AAMA;EACC;IACC;IACA;;EAEA;IACC;IACA;IACA;;EAEA;AAAA;AAAA;AAAA;AAAA;IAKC;;EAGF;IACC;;;AI3eH;EACC;EACA;EACA;EACA,KtDI6B;;AsDD5B;EACC;;AACA;EAFD;IAGE;;;AAMF;EACC;;AAIF;EACC;EACA;;;ACvBF;EACC;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC,O5D0Bc;;;A4DtBhB;AAAA;EAEC;EACA;EACA;EACA;;;ACtBD;EACI;;;AAGJ;EACI,YxDGwB;;;AyDT1B;EACE;EACA;EACA;;;ACKJ;ADRE;EACE;EACA;EACA;;ACMJ;AACqB;EACnB;EACA;;;AAEF;EACE;;;AAEF;ECjBI;EDmBF;EACA;;AACA;EAJF;IAKC;IACA;;;;AAGD;ECtBI;EDwBF,c1DlB4B;E0DmB5B;EACA;;AACA;EALF;IAMC;IACA;;;;AAGD;AACA;EACE;EACA;;;AAEF;EACE,c1DpB4B;E0DqB5B;EACA;;;AAEF;E9BlCC;E8BoCC;EACA;;;AEnCF;EACC;;AACA;EACC,oBxBZgB;EwBahB,YxBbgB;;;AwBmBjB;EACC,kBjEMgB;EiELhB,OjEcc;EiEbd,WpEPuB;EoEQvB,apEUuB;EoETvB,e5DpB0B;E4DqB1B;EACA;;;AAGF;EACC;EACA;;AAEC;EACC,kBA7B2B;EA8B3B;EACA;EACA,OAhC2B;EAiC3B;EACA;EACA;EACA,QArCqB;EAsCrB,aAtCqB;EAuCrB;EACA;EACA;EACA;EACA,OA3CqB;;AA6CtB;EACC;;AAGD;EACC,WpE3CoB;EoE4CpB,apExBuB;;AoE8BxB;AAAA;EACC;EACA,kBjEzCU;EiE0CV;;AAGA;AAAA;EACC,kBjEjDqB;;AiEmDtB;AAAA;EACC,OAhEiC;;AAoElC;AAAA;EACC,OArEiC;;AAwEnC;AAAA;EACC,OAzEkC;;AA6EnC;EACC;;AAMD;AAAA;EACC,kBjEzEsB;;AiE6EvB;EACC;;AAID;EACC;;AAMD;AAAA;AAAA;EACC,kBjE5Ga;;AiEiHd;EACC;EACA;EACA;EACA,WpEvGsB;;AoEyGvB;EAEI;EACH,kBjE1Ha;;AiE6Hb;AAAA;EAEC,OjE/HY;EiEgIZ,WpEpHkB;EoEqHlB,apEjGqB;;AoEsGxB;EACC;EACA;EACA;EACA;;AACA;EACC;;AAGF;AAAA;EAEC,OjE/Gc;EiEgHd,WpEtIoB;EoEuIpB,apEnHuB;EoEoHvB;;;AAIF;EAEC,QxB1JiB;EwB2JjB,elEnJkC;EkEoJlC,oBxB7JiB;EwB8JjB,YxB9JiB;;AwBgKjB;EACC,kBjEvIgB;EiEwIhB;EACA,SxBrKyB;;AwBuKzB;EACC;EACA;EACA,OjErIa;EiEsIb,WpE1JsB;EoE2JtB,apEjJqB;EoEkJrB,apExIsB;;AoE4IxB;EACC;;;ACtLF;EACI;EACA;;;ACSH;EACC;;AAMA;AAAA;AAAA;EAEE;EACA,WhCewB;;AgCb1B;AAAA;EACE;;AAEF;AAAA;EACE;;AAEF;AAAA;EACC;;AAGF;AAAA;EAEC;;AAGD;EACC,kBnEPgB;;AmEWhB;EACC,S9DjC2B;;A8DoCzB;EACC;EACA;EACA,WhCbqB;EgCcrB;;AANH;EASC;EACA;EACA;EACA,oBnEzBc;EmE0Bd,S9D/C0B;E8DiD1B;EAEA;;AAUA;EACC;EACA;EACA,WhCtCsB;;AgC8CzB;EACC;EACA;EACA;EACA;EACA;;;AAUD;AAAA;EAEC,kBnEpEe;;AmEwEhB;AAAA;EAEC;;AAID;AAAA;EAEC;;;AAMF;EACC;EACA;EACA;;AACA;EACC;;AAGF;EACC;;AAED;EACC;;AAED;EACC,SA7HsB;;AA+HvB;EACC;;AAGD;AAAA;EAEC;;;AAMD;AAAA;EACC;;;AAIF;EACC;;AACA;EACC;;AAED;EACC;EACA;EACA;;;AAKF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACC;;AAED;EACC,kBnErJW;EmEsJX;EACA;EACA;EACA;EACA;;AACA;EACC;;AAED;EACC,kBnE/JU;EmEgKV;;AAED;EAEC;;;AAQH;EAEE;IACC;IACA;;EAED;IACC;;EAED;IACC;;;Ad5LH;EACC;EACG;EACA;EAEH;;AAGA;EvDvBG;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AuDmBH;EACC;EACA;EACA,WlBD0B;EkBE1B,chDf4B;EgDgB5B;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC,WApDsB;;A7BkEvB;EACC;EACA,QAJwB;EAKxB;;A6BdA;EAEA,QA5DwB;EA6DxB;EACA,QA7DwB;EA8DxB,WA7DuB;;AA+DvB;EAPA;IAQC,QCzEoC;ID0EpC,WC1EoC;;;AD4ErC;EAEC;;AAED;EACC,YlB9BuB;EkB+BvB,kBAtE0B;EAuE1B;EACA,QA7EuB;;A7BqEzB;EACC;EACA,QAJwB;EAKxB;;A6BQA;EACC,OrDhEsB;EqDiEtB,WA/EqB;;AAmFvB;EACC;IACC,ehD5E8B;;EgD6E9B;IACC;;EACA;IACC,chD7EyB;;EgD+E1B;IACC;IACA,chD9EsB;;EgD+EtB;IACC;;EAED;IACC,YhD7FwB;;;;AgDqG9B;EACC,YlBlEyB;EkBmEtB,kBA1GyB;EA2G5B;EAEA,WA9GyB;;AA+GzB;EACC,WAhHwB;;AAyG1B;EASC;EACA;EACA;EAEA,KAzHyB;;AA2HzB;EACC;EACA;;AAGD;EApBD;IAqBE;IACA,KC1IqC;ID2IrC;;EAEA;IACC,OrDtHsB;IqDuHtB,WA5H2B;;EA+H5B;IACC;;EAIA;IACC;IAEA;IACA;IACA,WlBzHwB;IkB0HxB,OhDnJ0B;IgDoJ1B,KhD5I2B;IgD6I3B,axDnIsB;;EwDuIxB;IACC;;EAIA;IACC;;;;AevKJ;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;ACKA;EvEDI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AwEeN;EACC,YnCMyB;;;AmCKzB;AAAA;AAAA;AAAA;AAAA;AAAA;EAEC,QnCtCsB;EmCuCtB,OnCtCqB;;;AmC6CtB;AAAA;AAAA;AAAA;AAAA;AAAA;EAEC,SA3DuB;EA4DvB;EACA,eAxDsB;EAyDtB,evE/DsB;EuEgEtB;EACA;EACA,KjElDuB;EiEmDvB,WzE/DqB;EyEgErB;;A9CAD;AAAA;AAAA;AAAA;AAAA;AAAA;EACC;EACA,QAJwB;EAKxB;;A4CnED;AAAA;AAAA;AAAA;AAAA;AAAA;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;AAAA;AAAA;AAAA;AAAA;AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AEmDA;AAAA;AAAA;AAAA;AAAA;AAAA;AFhDD;;AACA;EACC;AAAA;AAAA;AAAA;AAAA;AAAA;IACC;IACA;IACA;IACA,oBE2C4C;IF1C5C;;EAGD;AAAA;AAAA;AAAA;AAAA;AAAA;IACC;;;AEqCD;AAAA;AAAA;AAAA;AAAA;AAAA;AFjCD;;AACA;EACC;AAAA;AAAA;AAAA;AAAA;AAAA;IACC;IACA;IACA;IACA,iBE4B4C;IF3B5C;;EAGD;AAAA;AAAA;AAAA;AAAA;AAAA;IACC;;;AEsBD;AAAA;AAAA;AAAA;AAAA;AAAA;EAEC;EACA;EACA;EACA;EACA;;;AAWF;EAEC;EACA;EACA;EACA;;AAKD;AAAA;EAEC,YtExFiB;;AsEyFjB;AAAA;EACC,OtElFU;EsEmFV;;AAED;AAAA;EACC,QCvG0B;;ADwG1B;AAAA;EACC,OAnG2B;EAoG3B,QApG2B;;AAyH7B;AAAA;EAbC;EACA;;AACA;AAAA;EACC;;AAED;AAAA;EACC;;AAED;AAAA;EACC;;AAOF;AAAA;EAhBC;EACA;;AACA;AAAA;EACC;;AAED;AAAA;EACC;;AAED;AAAA;EACC;;AAUF;AAAA;EACC;EApBA;EACA;;AACA;AAAA;EACC;;AAED;AAAA;EACC;;AAED;AAAA;EACC;;AAcF;AAAA;EACC,OtEzHU;;AsE4HZ;EACC,YtErIiB;;AsEwIlB;EAGE;AAAA;IACC,kBtE5Ie;IsE6If,OtE7Ie;;EsE8If;AAAA;IACC,QCxJwB;;ED0JzB;AAAA;IACC,OtE1IQ;;EsE6IV;AAAA;IACC,kBtE9IS;IsE+IT,OtEvJe;;EsEwJf;AAAA;IACC,QChKgC;;EDkKjC;AAAA;IACC,OtE5Jc;;;;AsEwKnB;EACC,YnCzIyB;EmC0IzB,YtElKY;EsEmKZ;EACA;;;AAGD;EACC,kBtEhLkB;;;AsEqLlB;AAAA;EACC,enCnKyB;;AmCoKzB;AAAA;EACC,YtE5Ke;;;AsEmLjB;AAAA;EACC;;;AAUF;EACC;;AACA;AAAA;EAEC,kBtEnNiB;EsEoNjB;;AACA;AAAA;EACC,QC1N0B;;AD6N5B;EACC,kBA9MgC;EA+MhC,OtEtNiB;;AsEuNjB;EACC;;;AAOH;EACC;;AACA;EACC;EACA,QnClOsB;EmCmOtB;;AAED;AAAA;EAEC,kBtErPc;EsEsPd,OtEjOW;;AsEkOX;AAAA;EACC,QCpP0B;;ADsP3B;AAAA;EACC;EACA;;AAED;AAAA;EACC,kBA1OmC;;AA2OnC;AAAA;EACC;;AAHF;AAAA;EAKC,OtEtPgB;;AsEuPhB;AAAA;EACC;;AAIH;EACC;EACA,cA5P0B;;AA6P1B;EACC;;;AAKH;EACC,kBA/PiC;EAgQjC;EACA,QnCvQuB;EmCwQvB;EAEA;EACA;;;AAKD;EACC;;AACA;AACC;EACA;EACG;EACA;;AAEJ;EACC,OtEvSc;EsEwSX;EACA,azE3QqB;EyE4QxB,QnC5RsB;EmC6RtB;EACA;;;AAMF;EACC;;AACA;EACC,YnC3RkC;EmC4RlC;EACA,YA/SsB;;AAgTtB;ExExTE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AwEoTD;EACC;EACA;EACA;EACA;;AAED;EACC;;;AAMJ;EACC;EACA;EACA;EACA;;;AAGC;EACC;;AAFF;EAIC;;AAEA;EACC;EACA;;;AAQH;EACC;IACI;IACA;IACA;IACA;;EAGJ;IACC,YnCzTsC;;EmC4TvC;IACG;IACA;;EAKF;AAAA;AAAA;AAAA;IAEM;IACH;IACA,QhBzXyB;IgB0XzB,OhBzXwB;;EgB0XxB;AAAA;AAAA;AAAA;IACC,QlB3Xe;IkB4Xf,OlB5Xe;;EkBkYnB;IACC,QhBpY2B;;EgBqY3B;IACC;;EACA;IACC;IACA,QhBzYyB;;;AgBoZ9B;EAOE;AAAA;AAAA;AAAA;AAAA;IACC;;;AzClZH;AAAA;EAEE,ahCR0B;EgCS1B,ahC4BwB;EgC3BxB,ahCcwB;EgCbxB,O7BwBkB;;;A6BtBlB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;EACA;EACA,O7BkBgB;;;A6BdpB;AAAA;AAAA;EAGE,YhCHwB;EgCIxB;;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAGJ;AAAA;AAAA;EAGE;EACA;;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAIJ;EAAU,WhC3BW;;;AgC4BrB;EAAU,WhC9BgB;;;AgC+B1B;EAAU,WhCjCe;;;AgCkCzB;EAAU,WhCpCY;;;AgCqCtB;EAAU,WhCvCa;;;AgCwCvB;EAAU,WhC1CY;;;AgCgDtB;EACE;;;AAKF;EACE,O7B/Dc;E6BgEd;EACA;AACA;AAAA;AAAA;AAAA;EAID;AACA;;;AACC;EAEE,O7B1BkB;E6B2BlB,iBhClCuB;;;A2B6B1B;EACC;EACA,QAJwB;EAKxB;;;AKYF;AAAA;EAGE,WhClFqB;;;AgCsFvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AAGvB;EAAuB;;;AACvB;EAAuB;;;AACvB;EAAuB;;;AAGvB;EACE,O7B5FiB;;;A6BqGjB;AAAA;AAAA;AAAA;EAEE;;;AAYJ;EAJE;EACA;;;AAQF;EACE;EACA,ehCjHwB;;;AgCmH1B;AAAA;EAEE,ahCtHqB;;;AgCwHvB;EACE;;;AAEF;EACE;;;AAOF;EACE;EACA;EACA,WhCjJqB;EgCkJrB;;;AAKE;AAAA;AAAA;EACE;;;AAMN;EACE,ehCnJwB;EgCoJxB;EACA,ahCtJqB;;;AgCyJvB;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC,ahClKwB;;;AgCqKzB;EACC;EACA,WhCjMqB;EgCkMrB,O7BjKqB;;;A6BoKtB;EACC;;;AAGD;EACC;;;AAGD;EACC;IACC;;;A2CvNF;EACC,kBxEoBY;EwEnBZ,W3EQsB;E2EPtB,OxE+Be;EwE9Bf,YzEJgB;;AyEMhB;EAEC,OxERc;;AwESd;EAEC,OxEqCmB;;AwEjCrB;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA,YCxBmB;EDyBnB;;AAGD;EACC;EACA;EACA;;AAGD;E3C0FC;EACA;E2CzFA;;AAGD;EACC;;AEYE;EFpDJ;IA4CE,W3EpCoB;;E2EqCpB;IACC;;;AECC;EF/CJ;IAmDE,W3EzCqB;;E2E0CrB;IACC;;EAED;IACC;;;;AJ5DH;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AOYA;EACE;EACA;EACA,SANoB;EAOpB;EACA;EACA;EACA;;AAEA;EACE,QAhBkB;EAiBlB;EACA;EACA;EACA;EACA,SAlBkB;;AAqBpB;EACE;EACA;EACA;EACA,kB3EtBe;E2EuBf,2B5ExB+B;E4EyB/B,4B5EzB+B;E4E0B/B,oBCnCe;EDoCf,iBCpCe;EDqCf,YCrCe;EDsCf;EACA,KtExB6B;EsEyB7B,YAnCkB;EAoClB;EACA;;AAEA;EAhBF;IAiBI;IACA;IACA,YrBnDiC;IqBoDjC;;;AAIJ;EAEE,OAhDsB;EAiDtB,W9EzCkB;E8E0ClB;EACA;EACA;EACA;EACA;;AACA;AAAA;AAAA;EAEE,OAzDoB;;APEzB;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AO4CA;APzCD;;AACA;EACC;IACC;IACA;IACA;IACA,oBOoCkC;IPnClC;;EAGD;IACC;;;AO8BD;AP1BD;;AACA;EACC;IACC;IACA;IACA;IACA,iBOqBkC;IPpBlC;;EAGD;IACC;;;AOeD;EAEE;;AAIA;EADF;IAEM,QrB9E+B;;;;AuBIvC;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAoBA;AAGA;AAEA;AAIA;AAEA;AAEA;AAQI;EAEE,oBAnBgC;EAoBhC,iBApBgC;EAqBhC,YArBgC;EAsBhC,kBAZ+C;EAc/C;EACA;EACA;EACA;EAEA,OAbe;;AAef;EACE;;AAEF;EACE,OAnBa;;AACjB;EAEE,oBAnBgC;EAoBhC,iBApBgC;EAqBhC,YArBgC;EAsBhC,kBAXiD;EAajD;EACA;EACA;EACA;EAEA,OAbe;;AAef;EACE;;AAEF;EACE,OAnBa;;AACjB;EAEE,oBAnBgC;EAoBhC,iBApBgC;EAqBhC,YArBgC;EAsBhC,kBAVgD;EAYhD;EACA;EACA;EACA;EAEA,OAbe;;AAef;EACE;;AAEF;EACE,OAnBa;;AAFrB;EAgCE,ahF9CwB;EgF+CxB,axElE0B;EwEmE1B,gBxEnE0B;EwEoE1B;EAIA;EACA;EACA;EACA;;AAGA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAIA;EACE;;AAGF;EACE,gBAhF4C;EAiF5C,ahFlEmB;EgFmEnB,cxEvGY;;AwE+GhB;EACE;;AAGF;EACE,WhFtGkB;EgFuGlB;EACA;EACA;EACA;;AAEA;EACE,exErHwB;;AwEwH1B;EACE;;AAIJ;EACE;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAMA;EACE;IACE;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAMA;EACE;IACE;;;AzC5HJ;AAAA;ECmFY;EACA;;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,aAfa;;;ADzEzB;AAAA;EC4FQ;EACA;EACA,QA3BW;EA4BX;EACA;EAGA,axChIoB;EwCiIpB;EACA,aD3HQ;EC4HR;EACA,axCrGiB;EwCsGjB,iBDlIY;;;AZ+BnB;AAAA;EACC,SAHwB;EAIxB;;;AYPF;AAAA;EC+GQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;;;A+BgB5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;;AAKF;AAAA;AAAA;AAAA;EAEE;;;AAKJ;AAAA;EACE,W9BjB0B;;;A8BmB1B;EAHF;AAAA;IAII;;;AAIJ;ECuFQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBrC3JQ;EqC4JR,OpCtJgB;EoCuJhB,cDvJS;ECwJT;EACA,crC/JQ;EqCiKJ,etC/JY;;;AsCmKhB;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OpCnKY;EoCoKZ,cDpKK;ECqKL;EACA,cA5GS;;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OpC9KY;EoC+KZ,cD/KK;ECgLL;EACA,cA3GU;;;AA+Gd;EACI,OpCtLY;EoCuLZ,iBDzLQ;;;AC6LZ;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;;AAqBJ;EACI,kBpC1MQ;EoC2MR,cD3Je;EC4Jf;EACA,crClOI;EqCmOJ,OrCjMI;;;AoC2ChB;ECgEQ,Y/B1Gc;E+B8GV,W/B9GU;E+BgHd,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBpC5IY;EoC6IZ,OpC/Ie;EoCgJf,cDvJS;ECwJT;EACA,cpC9IgB;EoCgJZ,etC/JY;;;AsCmKhB;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OpC5JW;EoC6JX,cDpKK;ECqKL;EACA,cA5GS;;;AAgHb;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OpCvKW;EoCwKX,cD/KK;ECgLL;EACA,cA3GU;;;AA+Gd;EACI,OpC/KW;EoCgLX,iBDzLQ;;;AC6LZ;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;;AAqBJ;EACI,kBpC1MQ;EoC2MR,cDpIe;ECqIf;EACA,cpCjNY;EoCkNZ,OrCjMI;;;AoC6DhB;AAAA;AAAA;AAAA;AAAA;ECkBY;EACA;;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI,aAfa;;;ADRzB;AAAA;AAAA;AAAA;AAAA;EC2BQ;EACA;EACA,QA3BW;EA4BX;EACA;EAGA,axChIoB;EwCiIpB;EACA,aD3HQ;EC4HR;EACA,axCrGiB;EwCsGjB,iBDlIY;;;AZ+BnB;AAAA;AAAA;AAAA;AAAA;EACC,SAHwB;EAIxB;;;AY0DF;AAAA;AAAA;AAAA;AAAA;EC8CQ,YpC/GmB;EoCmHf,WpCnHe;EoCqHnB,WxC3Ic;EwC6Id;EACA,KhCtIsB;EgC2ItB,kBrC5HU;EqC6HV,OrC5JQ;EqC6JR,cDvJS;ECwJT;EACA,crChIU;EqCkIN,etC1JuB;;;AsC8J3B;AAAA;AAAA;AAAA;AAAA;EACI,iBDlKQ;ECoKR,kBAxGS;EAyGT,OrCzKI;EqC0KJ,cDpKK;ECqKL;EACA,crC5KI;;;AqCgLR;AAAA;AAAA;AAAA;AAAA;EACI,WDjKgB;ECmKhB,kBAvGU;EAwGV,OrCpLI;EqCqLJ,cD/KK;ECgLL;EACA,cA3GU;;;AA+Gd;AAAA;AAAA;AAAA;AAAA;EACI,OrC5LI;EqC6LJ,iBDzLQ;;;AC6LZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;EAoGV;;;AAqBJ;AAAA;AAAA;AAAA;AAAA;EACI,kBA/IW;EAgJX,cAvIe;EAwIf;EACA,crCnMM;EqCoMN,OAlJa;;;ADyBvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;EACA,kBpCxFS;;;AoC0FX;AAAA;AAAA;AAAA;AAAA;EACE;;;AAKF;AAAA;AAAA;EAGE,a/BrGuB;;;A+BoH3B;EACE;EACD;EACA;EACC;EACA;EACA,avC7GuB;EuC8GvB,OpC9Ic;;;AoCgJd;EAKE;ExCpFF,oBwCqFE;ExCpFM,YwCoFN;;;AAEF;EAGE;;;AAEF;EACE,OpC9GkB;EoC+GlB,iBvCtHuB;EuCuHvB;;;AAEF;EAEE,kBnCjIiB;EmCkIjB,OnCnIoB;EmCoIpB;EACA,Q9BvIc;;;A8BwId;EACE;;;AAGJ;EACE,OpC3IY;EoC4IZ,kBpC/Ic;;;AoCyJlB;AAAA;EC3CQ,YDtHiB;ECwHb;EAIJ,WxCzIe;EwC2If;EACA,KhCnJoB;EgCwJpB,kBrC/HY;EqCgIZ,OrC1HQ;EqC2HR,cDvJS;ECwJT;EACA,crCnIY;EqCqIR,etC/JY;;;AsCmKhB;AAAA;EACI,iBDlKQ;ECoKR,kBrCzIM;EqC0IN,OrCvII;EqCwIJ,cDpKK;ECqKL;EACA,crC7IM;;;AqCiJV;AAAA;EAGI,kBAvGU;EAwGV,OrClJI;EqCmJJ,cD/KK;ECgLL;EACA,cA3GU;;;AA+Gd;AAAA;EACI,OrC1JI;EqC2JJ,iBDzLQ;;;AC6LZ;AAAA;AAAA;AAAA;AAAA;AAAA;EAEI,kBpChKS;EoCiKT,cD9LK;EC+LL;EACA,cpClKa;EoCmKb,OpCrKY;EoCsKZ,QAnGU;;;AAyHd;AAAA;EACI,kBrChMM;EqCiMN,cDhBe;ECiBf;EACA,crCtMQ;EqCuMR,ODrBa;;;ACqCb;AAAA;EACI;EACA;EACA,KhCnPY;EgCoPZ;;;AAGJ;AAAA;EACI;;;AbrLf;AAAA;EACC;EACA,QAJwB;EAKxB;;;AYoJA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE,eD/LuB;;;ACgMvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;EACA;;;AAQN;EC7FQ,YDtHiB;EC0Hb,WD1Ha;EC4HjB,WxCvIc;EwCyId;EACA,KhCtIsB;;;A+BkO9B;ECrGQ,YDyGe;ECrGX,WDqGW;ECnGf,WxC7Ie;EwC+If;EACA,KhCtIsB;;;A+B4O9B;EACE;;;AAGF;EACE;;;AAOF;EACC;EACC,kBnCvOmB;EmCwOnB,cnCvOuB;;;AmCwOvB;EACE,kBnC1OiB;EmC2OjB,cnC1OqB;;;AmCoOzB;EAQC;EACA;EACA;;;AAGD;EACE;EACA,avCrPuB;EuCsPvB;EACA,OpCvQuB;EoCwQvB;EACA;;;AAEA;EAEE,OpClRe;EoCmRf;;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;;;A0CtRF;EACI;EACA;EACA;;AAGA;EACI;EACA;EACA;;AAGJ;EACI,YA1BiB;EA2BjB;;AAGJ;EACI;EAEA;EACA;EACA,Y3C9BgB;E2CgChB,e3CdmB;E2CenB;EACA;EACA;;AAEA;EACI,WjFtCa;EiFuCb;EACA;;AAEJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEJ;EACI;EACA;EAEA;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;;AtDPf;EACC;EACA,QAJwB;EAKxB;;AsDQM;EACI,cAhEc;;AAiEd;EACI;;AAEJ;EACI;;AASR;EACI;;AAMJ;AAAA;AAAA;AAAA;AAAA;EAKI;;AAGJ;EACI;;AAKJ;AAAA;AAAA;AAAA;EAGI;;AAEJ;EACI;;AAIR;EACI;;AAIA;EACI;;AAKI;AAAA;AAAA;AAAA;AAAA;AAAA;EAGI;;AAIZ;EACI,Y9EnHM;;A8EoHN;EACI;;AAKZ;EAEQ;IACI;;EAEJ;IACI;;EAGA;IACI;;EAEJ;IACI;;EAGR;IACI,cAxJU;;EAyJV;IACI;;EAEJ;IACI;;EAMR;IACI;;EAGI;IACI,kBA7KC;;EA+KL;IACI;;EAGA;AAAA;AAAA;IAGI;IACA,kB9ErKV;I8EsKU;IACA;;EACA;AAAA;AAAA;IACI,kBA3LP;IA4LO,cA5LP;;EAiMD;IACI,kBA3LO;IA4LP,cA5LO;;EA6LP;IACI,kBArMP;IAsMO,cAtMP;;EA0MG;AAAA;AAAA;IAGI;;EAOhB;AAAA;AAAA;AAAA;IAGI;;EAEJ;IACI;;EAGR;IACI;IACA;IACA,Y1CtNS;I0CuNT;IACA;;EAEA;IACI;IACA;;EAIJ;AAAA;IAEI;;EAGR;IACI,YAhPS;;EAkPb;IACI;;EAEA;IACI,Y9EpOF;;E8EuOE;AAAA;AAAA;AAAA;IAGI,kB9E1ON;I8E2OM;IACA;;EACA;AAAA;AAAA;AAAA;IACI,kBAhQH;IAiQG,cAjQH;;;AA0QrB;EACI;;AAEJ;AAAA;AAAA;EAGI,kBAzQ2B;EA0Q3B,cA1Q2B;;AA2Q3B;AAAA;AAAA;EACI,kBAnRa;EAoRb,cApRa;EAsRb;;AAGR;AAAA;EAEI;EACA;EACA,cAxRkB;EA0RlB,e3C5QmB;;A2C+QvB;AAAA;EAEI;EACA;EACA,cAjSkB;EAmSlB,e3CrRmB;;A2CwRvB;EACI,azEtToB;EyEuTpB,gBzEvToB;;;A0EF5B;EACC;EACA,kB/EkBY;E+EhBZ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,alFMsB;;;AmF3BrB;EACE;EACA;EACA;;AACA;EACE;;AAEF;EACE;;AAKF;EACE;;AAEF;EACE;;;ACfL;EACC;EACA;EAEA;EACA,Y5EE0B;;A4ED1B;EACC;;AAED;EACC;EACA,Y5EPwB;E4EQxB,c5EVe;E4EWf;EACA;EACA;EACA;;AAED;EACC;EACA;;AAED;EACC;EACA;;;ACzBH;EACC;EACA;EACA;;AACA;EACC;;AACA;EACC;;AAED;EACC;;AACA;EACC,c7ETc;E6EUd;;AAED;EACC;;;ACsBH;EACC;;AAED;EACC,a9ElC0B;E8EmC1B,gB9EnC0B;E8EoC1B;;AAID;EACC;EACA;;AAGD;EACC;EACA;EACA;;AAGD;EACC,OA3CqC;EA4CrC,QA3CsC;EA4CtC;;AAEA;EACC,OAhDoC;EAiDpC,QAhDqC;EAiDrC,QAhDsC;EAiDtC,kBnF1CU;;AmF6CX;EACS;EACR,QAvDqC;;AA0DtC;EACC,QA3DqC;EA4DrC,SArD0C;;AAwD3C;EACC,kBnFjDiB;;AmFqDnB;EACC;;AAGD;EACC;;AAGD;EACC;EACA;EACA;EACA,OA9E+B;;AAiFhC;EACC;EACA;EACA;EACA,OApFsC;;AAsFtC;EACC;;AAIF;EACC,c9E9GgB;;A8EiHjB;AAAA;EAEC;EACA,QAtFiD;EAuFjD,SAtFkD;;AA2FjD;EACC;;AAIF;EACC;EACA;EACA,OAlGsD;;AAsGxD;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;;AAIF;EACC;EACA;EACA,kBnFxIiB;;AmF0IjB;EACC;;AAEA;EACC,kBnF3IiB;;AmF8IlB;EACC;EACA,OAhKoC;EAiKpC;EACA;EACA;EACA,WtFtKoB;EsFuKpB;;AAEA;EACC,eAhLyC;;AAqL5C;EACC,cArL2C;;;AAyL7C;EACC,QA7KoC;EA8KpC,c9ElMiB;E8EmMjB,gBA7K4C;;AA+K5C;EACC;EACA;EACA;EACA,WtF7LqB;;AsF+LrB;EACC;EACA,kBnF/KiB;;;AmFqLnB;EACC,WtFxMqB;;;AsF6MvB;EACC,kBnF/LiB;EmFgMjB;;AAEA;EACC;EACA;EACA;EACA;EACA,kBnF3MW;EmF4MX,WtFzNoB;EsF0NpB;EACA;EACA;EACA;;;AAIF;EACC,YAlO6C;EAmO7C;EACA,e9E9OiB;E8E+OjB,QAnOyC;EAoOzC,YnFrNiB;;AmFuNjB;EACC,WtFzOoB;EsF0OpB;;AAGD;EACC,QA3OoD;;AA8OrD;EACC,Q9EjP4B;;;A8EqP9B;EACC;EACA;EACA;EACA;;;AAIA;EACC,OnF9NoB;;;AmFkOtB;EACE;EACA,WtFnQqB;EsFoQrB,atF9OuB;EsF+OvB;EACA,OnF9Oc;EmF+Od;EACA,kBnFzPgB;EmF0PhB;EACA;;;AClRF;EACC;EACA;;;AAGD;EACC,WvFEsB;EuFDtB,a/EXiB;;;A+EclB;EACC,WvFHsB;EuFItB,OpFJiB;;;AoFOlB;AAAA;EAEC,OnFgBoB;EmFfpB;EACA;;;AAGD;EACC;EACA,e/EvB2B;E+EwB3B,S/E7BiB;;;A+EgClB;EACC,WvFrBsB;;AuFsBtB;EACC;;AAED;EACC,a/EtCgB;E+EuChB;;AAED;EACC,c/E1CgB;;A+E4CjB;EACC,c/E7CgB;E+E8ChB;;;AAIF;EACC,QClD+B;EDmD/B,kBpFtBmB;EoFuBnB,Y/ErDiB;E+EsDjB;EACA;EACA;;AAEA;EACC,kBrF1D0B;EqF2D1B;EACA;EACA;EACA;EACA;EACA;;AACA;EACC,kBpF7DgB;;AoF+DjB;EACC,kBpF5DgB;;;AsFfnB;AAAA;AAAA;AASA;EACE;EACA;EACA;EACA,KjFNyB;EiFOzB,ejFPyB;;;AiFa3B;AAAA;EAEE,OtFMW;;;AsFHb;EACE,YCpBmB;EDqBnB,QvFpBe;EuFqBf;EACA;EACA,cjFnB4B;EiFoB5B,ejFpB4B;EiFqB5B;;;AAIF;EACE;;;AAGF;EACE;;AAEA;EACE;;;AErCF;AAAA;EACE;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACE;;AAEF;AAAA;EACE;;AAGF;AAAA;EACE,YnFZsB;EmFatB;;AAGF;AAAA;EACE;EACA;EACA;EACA;EACA;;AACA;AAAA;EACE;EACA;EACA;EACA,enF1BoB;EmF2BpB;;AACA;AAAA;EACE,cnF5BoB;;AmF8BtB;AAAA;EACE;;AAGJ;AAAA;EACE,anFnCsB;EmFoCtB;;AAIJ;AAAA;EACE,YnF1CsB;EmF2CtB;;AAIF;AAAA;EACE;EACA;;AAEA;AAAA;EACE;;AAEA;AAAA;EACE;EACA;;AAKN;AAAA;EACE;EACA;;AAEA;AAAA;EACE;;AAEA;AAAA;EACE;EACA;EACA,enFvEkB;;AmFgFxB;AAAA;EACE;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAIE;;AAIF;AAAA;AAAA;AAAA;EAEE;;AAIF;AAAA;EACE;;AAMN;AAAA;EACE;EACA,QzF/Ga;;AyFiHb;AAAA;EACE;;AAGF;AAAA;EACE;;AAGF;AAAA;EACE,YnFvHsB;EmFwHtB;EACA,QzF5HW;EyF6HX;EACA,YAlIY;EAmIZ;;;AAMN;EACE;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;;AAKJ;EACE;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;;AAKJ;EACE;IACE;;EAEF;IACE,YA9Lc;;;ACChB;EACE;;AAEF;EACE;;AAEF;EACE;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA,OzFEc;;AyFAhB;EACE;EACA,OzFCY;EyFAZ;;AACA;EACE;;AAIF;EACE,OzFOgB;;AyFJlB;EACE;;AAIJ;AAAA;EAEE,OzFHkB;;AyFKpB;EACI;;AAIF;AAAA;AAAA;EAGE,QnF7BY;EmF8BZ,OzF5CmB;;AyF+CrB;EACE;;AAIJ;EACE;EACA;EACA,kBzF1Cc;;AyF4ChB;EACE;EACA,kBzF3DqB;EyF4DrB;;;AClFJ;EACE;EACA;EACA;;AAEA;EACE;EACA;;AACA;EACE;;AAEF;EACE;EACA,W7FKmB;E6FJnB,a7FwBmB;;A6FpBvB;EACE;EACA;;AAGF;EAEE;;AAGF;EACE,YrFrB0B;EqFsB1B;EACA;;;AClCJ;EACC;;;ACkBD;AAAA;EAEC;EACA;;AAEA;AAAA;EACC;EACA;EACA;;AACA;AAAA;EACC;;AACA;AAAA;EACC,YvFrBwB;;AuFkB1B;AAAA;EAKC;EACA,kB5FFe;;A4F1BjB;AAAA;AAAA;EAEE,YA2BiC;EA1BjC,kB5F0BiB;;A4FxBlB;AAAA;AAAA;AAAA;EAEC;EACA;;AAyBF;AAAA;EACC;;AAGD;AAAA;EACC;;AAzBF;AAAA;EA4BC;EACA;EACA;EACA;;AACA;AAAA;EACC;EACA;EACA;;;AAMD;EACC;;AAIA;EACC;;AA5DF;EAEE,YA4DgC;EA3DhC,kBA2DsC;;AAzDvC;AAAA;EAEC;EACA;;;AA0DH;EACC;;AAKA;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAGD;EACC;EACA;;;AlBtCC;EkB4CH;AAAA;IAEC;IACA;;EAIA;AAAA;IACC;IACA;;;AAOH;AAAA;EAEC;;AACA;AAAA;EACC;;;AdzFF;EACI;EACA;EACA;;AAGA;EACI;EACA;EACA;;AAGJ;EACI,YA1BiB;EA2BjB;;AAGJ;EACI;EAEA;EACA;EACA,Y3C9BgB;E2CgChB,e3CdmB;E2CenB;EACA;EACA;;AAEA;EACI,WjFtCa;EiFuCb;EACA;;AAEJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEJ;EACI;EACA;EAEA;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;;AtDPf;EACC;EACA,QAJwB;EAKxB;;AsDQM;EACI,cAhEc;;AAiEd;EACI;;AAEJ;EACI;;AASR;EACI;;AAMJ;AAAA;AAAA;AAAA;AAAA;EAKI;;AAGJ;EACI;;AAKJ;AAAA;AAAA;AAAA;EAGI;;AAEJ;EACI;;AAIR;EACI;;AAIA;EACI;;AAKI;AAAA;AAAA;AAAA;AAAA;AAAA;EAGI;;AAIZ;EACI,Y9EnHM;;A8EoHN;EACI;;AAKZ;EAEQ;IACI;;EAEJ;IACI;;EAGA;IACI;;EAEJ;IACI;;EAGR;IACI,cAxJU;;EAyJV;IACI;;EAEJ;IACI;;EAMR;IACI;;EAGI;IACI,kBA7KC;;EA+KL;IACI;;EAGA;AAAA;AAAA;IAGI;IACA,kB9ErKV;I8EsKU;IACA;;EACA;AAAA;AAAA;IACI,kBA3LP;IA4LO,cA5LP;;EAiMD;IACI,kBA3LO;IA4LP,cA5LO;;EA6LP;IACI,kBArMP;IAsMO,cAtMP;;EA0MG;AAAA;AAAA;IAGI;;EAOhB;AAAA;AAAA;AAAA;IAGI;;EAEJ;IACI;;EAGR;IACI;IACA;IACA,Y1CtNS;I0CuNT;IACA;;EAEA;IACI;IACA;;EAIJ;AAAA;IAEI;;EAGR;IACI,YAhPS;;EAkPb;IACI;;EAEA;IACI,Y9EpOF;;E8EuOE;AAAA;AAAA;AAAA;IAGI,kB9E1ON;I8E2OM;IACA;;EACA;AAAA;AAAA;AAAA;IACI,kBAhQH;IAiQG,cAjQH;;;AA0QrB;EACI;;AAEJ;AAAA;AAAA;EAGI,kBAzQ2B;EA0Q3B,cA1Q2B;;AA2Q3B;AAAA;AAAA;EACI,kBAnRa;EAoRb,cApRa;EAsRb;;AAGR;AAAA;EAEI;EACA;EACA,cAxRkB;EA0RlB,e3C5QmB;;A2C+QvB;AAAA;EAEI;EACA;EACA,cAjSkB;EAmSlB,e3CrRmB;;A2CwRvB;EACI,azEtToB;EyEuTpB,gBzEvToB;;;AwFWtB;AAAA;EAEE;;AAON;EAME;EACA;EACA;EACA;EACA;EACA,KxF/BwB;EwFgCxB;EACA;EACA;EACA;;AAKA;EAIE;;AAEA;EACE,cfrCiB;;AeyCrB;EAEE;EACA;;ArEaL;EACC;EACA,QAJwB;EAKxB;;AqEbE;ExDiFI,YDtHiB;EC0Hb,WD1Ha;EC4HjB,WxCzIe;EwC2If;EACA,KhCnJoB;EgCwJpB,kByC/Ie;EzCgJf,OrC1HQ;EqC2HR,cDvJS;ECwJT;EACA,cyCnJe;EzCqJX,etC/JY;;AsCmKhB;EACI,iBDlKQ;ECoKR,kByC3Ja;EzC4Jb,OrCvII;EqCwIJ,cDpKK;ECqKL;EACA,cyC/Ja;;AzCmKjB;EAGI,kBAvGU;EAwGV,OrClJI;EqCmJJ,cD/KK;ECgLL;EACA,cA3GU;;AA+Gd;EACI,OrC1JI;EqC2JJ,iBDzLQ;;AC6LZ;AAAA;EAEI,kBA/HY;EAgIZ,cD9LK;EC+LL;EACA,cAhIgB;EAiIhB,OAlIc;EAmId,QAnGU;;AAyHd;EACI,kBA/IW;EAgJX,cAvIe;EAwIf;EACA,cyCtNW;EzCuNX,OAlJa;;AwDrBrB;EAmBE,e1D/CqB;E0DgDrB;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;;AAGF;EACE;;AAGF;EAEE;IACE,YfvFiB;;Ee0FnB;IACE;;EAIA;IACE,YfhGe;;EemGf;IACE,kB7FlFM;I6FmFN;IACA;;EACA;IACE,kBfxGW;IeyGX,cfzGW;;EeiHf;IACE,kBflHa;;EeqHb;IACE;IACA,kB7FrGI;I6FsGJ;IACA;;EACA;IACE,kBf3HS;Ie4HT,cf5HS;;EeiIb;IACE,kBf3HqB;Ie4HrB,cf5HqB;;Ee6HrB;IACE,kBfrIS;IesIT,cftIS;;Ee8IjB;IACE;;EAMI;IACE;;EAKA;IACE;;;;ACnIlB;EACE,ezF9B4B;;AyFgC5B;AAAA;EAEE;EACA;EACA;;AAEA;AAAA;EACE;;AAGF;AAAA;EACE;;AAGJ;EACE,YzF/C0B;;;AyFoD9B;EACE;EACA;EACA;EAIA,YzF3D4B;;AyF4D5B;EACE;;AAGF;EACE;EACA;EACA,ezFxE0B;;AyF0E1B;EACE,czF/EqB;;AyFmFzB;EACE;;AAGF;EACE;;AAGF;EACE;;ApB7CA;EoBcJ;IAmCI;IACA;;EAIA;IACE,YzFtGqB;;;;AyF+GzB;EACE;EACA,kB9FxFc;;A8F0FhB;EACE;EACA,kB9FtFc;;;A8F2FhB;EACE,YzFxHwB;EyFyHxB;;AAEF;EACE,kB9F5DiB;E8F6DjB;;AAEF;EACE,YzFpHwB;EyFqHxB,WjG5HkB;;AiG8HpB;EACE,YzFxHwB;;AyFyHxB;EACE;;AAHJ;EAKE;;AAEF;EACE;EACA,czFjJc;;AyFmJhB;AAAA;EAEA,QPpJmB;EOqJnB;;AAEA;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EACA,WjGrJkB;;AiGuJpB;AAAA;EAEE;EACA,WjG1JkB;;AiG4JpB;EACE;;;AChKH;EACC,YANyB;;AAQ1B;EACC,YARmC;EASnC,kB/FegB;E+FdhB;EACA,WlGJoB;EkGKpB,O/F4BoB;;;AgGPtB;AAAA;EAEC;EACA;EpG4BC,oBoG3BD;EpG4BS,YoG5BT;;AAGC;AAAA;EACC,kBhG7BgB;EgG8BhB,OhGtBU;;AgGuBV;AAAA;EACC,qBhGxBS;;AgG0BV;AAAA;EACC;;AAED;AAAA;EACC;;AAED;AAAA;EACC,OhGjCS;EgGkCT;;AAGF;AAAA;EACC,OhGtCU;EgGuCV,kBhG/CgB;EgGgDhB;;;AAWH;EACC;;;AAIC;EACD;EACA;EACA;EACA;EACA;EACA,SA/E0B;EAgF1B;EACA;EACA;EAKA;;AAGA;EACC;EACA,qBAvE0B;EAwE1B;EACA;;AAED;EpGkCC;EACI;EACC;EACG;EAkER;EACG;EACE;EACG;;AoGtGT;EpG8BC;EACI;EACC;EACG;;AoG7BR;EACC,kBhGhGgB;EgGiGhB,OhGzFU;;AgG0FV;EACC,qBhG3FS;;AgG6FV;EACC;;AAED;EACC;;AAED;EACC,OhGpGS;EgGqGT;;AAGF;EACC,OhGzGU;EgG0GV,kBhGlHgB;EgGmHhB;;;AAID;EACD;EACA;;;AAIC;EACD;EACA;EACA;;;AAIC;EACD;EACA,kBA5I8C;EA6I9C;EACA;EACA;EACA,ejGpJwB;EiGuJxB;;;AAGC;EACC;;;AAID;EACD;EACA;EACA;EACA;EACA;EACA,SArK0B;EAsK1B,kBC/K6B;;ADiL7B;EpGqDC;EACA,SoGtDyB;;AAC1B;EpGoDC;EACA,SqGnO4B;;;ADiL5B;AAAA;AAAA;AAAA;AAAA;AAAA;EAMC,SAjKmB;;AlC1BpB;AAAA;AAAA;AAAA;AAAA;AAAA;EACE;EACA;EACA;;;AkC8LF;EACD;;;AAGC;EACD;;;AAIC;EACD,WnGzLqB;EmG0LrB;EACA,anGlLsB;EmGmLtB;;;AAKC;EACD;;AAGG;EACC;;;AAMH;EACD;EACA;;AlChOC;EACE;EACA;EACA;;AkCiOH;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;;AAKD;EACD;EACA;EACA;EACA;EACA;;;AAOA;EAEC;IACC,OA3OkC;IA4OlC;;EAED;IpG9LA,oBoG+LC;IpG9LO,YoG8LP;;EAID;IAAmB,OAlPgB;;;AAqPpC;EACC;IAAmB,OAvPgB;;;AtB8BjC;EsB+NJ;IAEE;IACA;IACA;IACA;;;AAED;AAAA;AAAA;EAEC;EACA;;AACA;AAAA;AAAA;EACC;EACA;;AAED;AAAA;AAAA;EACC;EACA;;AtBhPC;EsBkPF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;IAEE;IACA;IACA;IACA;;;;AAUA;EACC;;AAED;EACC;;;AAOH;EACC,e3FhT4B;;A2FkT3B;EACC;EACA;EACA;;AAED;EACC;EACA;EACA;EACA;EACA,OhG/RiB;EgGgSjB,c3FzTyB;;;A6FhB9B;EACE;EACA,QnGFe;EmGGf,enGIiC;EmGHjC,kBlGiBW;EkGhBX;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,K7FL4B;E6FM5B,a7FVyB;E6FWzB,kBlGIS;EkGHT,YApBuB;EAqBvB;;AAEA;EACE;EACA;EACA;EACA,K7FrBwB;E6FsBxB;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAEA;EACE;;AAIJ;EACE;;AxBIJ;EwB1BA;IA0BI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AAGF;EACE;;AAIN;EACE;;AxBpBA;EwBsBF;IAEI;;;;AAMJ;EACE,K/D3CkC;;;AgE5BtC;EACC,etGcyB;EsGbzB,Q1DTiB;E0DUjB,epGFkC;EoGGlC,oB1DZiB;E0DajB,Y1DbiB;;A0DejB;EACC,S1DlByB;E0DmBzB;EACA;ECrBA,wBDsB2B;ECrB3B,yBDqB2B;;AAE3B;EACC,OlGfqB;;AkGmBvB;AAAA;EAEC,kBnGFgB;EmGGhB;EACA;;AAEA;AAAA;AAAA;EACC;EACA,OnGCa;EmGAb,WtGpBsB;EsGqBtB,atGXqB;EsGYrB,atGFsB;;AsGKvB;AAAA;AAAA;EACC;EACA,OnGPa;EmGQb,WtGhCoB;;A6EqCnB;EyBDH;IAEE;IACA;IACA;;;AzBDC;EyBHH;IAOE;IACA;IACA;;;AzBHC;EyBNH;IAYE;IACA;IACA;IACA;;;AAIF;EACC;EACA,kBnG9CW;;A8D3BX;EACE;EACA;EACA;;;AqC2EJ;EACC,S1DvEyB;E0DwEzB,kBnGjDiB;EmGkDjB;ECvEC,4BDwE6B;ECvE7B,2BDuE6B;;;AAK9B;EACC;EACA;EACA;EACA,K9FvFgB;;A8FyFjB;EACC;;AAED;EACC;EACA,K9F9FgB;E8F+FhB;EACA;;AzB/CE;EyB2CH;IAME;;;AzB/CC;EyByCH;IASE;;;AzB/CC;EyBsCH;IAYE;;;AAGF;EACC;EACA;EACA;EACA;;AAED;EACC;EACA;EACA;;;AAMD;EACC,atGvFuB;;;AsG4FzB;EACC,oB1DhIiB;E0DiIjB,Y1DjIiB;;A0DmIhB;EACC,WtGvHmB;;AsG0HpB;EACC,WtG3HmB;;AsGgIpB;EACC;;AACA;EACC;;AAED;EACC;;AAKA;EACC;EACA;;AAIF;EACC;;AAGD;EACC;;AAGD;EACC,c9F/J2B;;A8FiK3B;EACC;;AAMJ;EACC;EACA;EACA,WtGxKqB;EsGyKrB,OnGjJc;EmGkJd;;AAGD;EACC,epGxLe;;;AoG6LjB;EACC;EACA;EACA;EAEA;;AAEA;EACC;;AAIA;EACC,S1D7MwB;;;A0DoN1B;EACC;;AAED;EACC,e9FlN0B;;A8FoN3B;EACC;;;AAMF;EACC,kBnGtMiB;EmGuMjB,ehEzNkC;;AgE2NlC;EACC,OnGjMc;EmGkMd,WtGtNuB;EsGuNvB,atGnMuB;EsGoMvB;EACA,S1D1OyB;E0D2OzB;EACA,atGjNsB;;AsGoNvB;EACC;;AAED;EACC,S1DnPyB;;A0DqPzB;EACC,gB9FpOyB;;;AgGd1B;EACE,OANoB;;;AASxB;EACE;EACA,QAVuB;EAWvB,OAZsB;;;AAetB;EACE;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;;;AC4DJ;EACE;;;AAGF;EACG;EACA;;;AAGH;AACA;EACE;EACA;EACA;EACA,SArDgB;EAsDhB;EACA,WA7FoC;EA8FpC,YA5FoC;EA6FpC;EACA;EACA;EACA,kBAxGkC;EAyGlC;EACA;EACA;EACA,eA3EkC;EAelC,oBA6DoB;EA5DZ,YA4DY;;AAEpB;EAAmC;;AACnC;EAAuC,aA3FH;;AA4FpC;EAAwC,YA5FJ;;AA6FpC;EAAqC;;AAGrC;EAnDE,mBAoDmB;EAnDd,cAmDc;EAlDX,WAkDW;EA9CpB,oBA+CqB;EA9Cf,eA8Ce;EA7CZ,YA6CY;EAlEtB,SAmEmB;EAhEnB;;AAkEA;EA5CE,6BA6C6B;EA5CxB,wBA4CwB;EA3CrB,qBA2CqB;EAnD9B,oBAoDqB;EAnDf,eAmDe;EAlDZ,YAkDY;EAvEtB,SAwEmB;EArEnB;;AAwEA;EAxDC,oBAyDqB;EAxDf,eAwDe;EAvDZ,YAuDY;EA5EtB,SA6EmB;EA1EnB;;AA4EA;EA/EA,SAgFmB;EA7EnB;;AAgFA;EAtEE,mBAsEuB;EArElB,cAqEkB;EApEf,WAoEe;EAnFzB,SAmFiD;EAhFjD;;AAkFA;EACE;EACA;EACA;;AACA;EACE;;;AAON;EACE;EACA;EACA;EACA,WAjHkC;EAkHlC;EACA,aAnHkC;EAoHlC,OAnHkC;EAoHlC;EAzGA,SA0GiB;EAvGjB;EAwGA;;AACA;EA5GA,SA6GkB;EA1GlB;;AA4GA;EACE;EACA;EACA;EACA;EAEA;;;AAIJ;EACE;EACA;EACA,WA7IgC;EA8IhC;EACA;EACA,kBApLkC;EAqLlC;EACA;;;AAGF;EACE;EACA;EACA;;;AAIF;EACE,kBA/LsB;EAgMtB,OA9LyB;;AAgMzB;EACC,YAnMqB;EAoMrB;EACA,OAnMwB;;;AAwM1B;EACC;;AAED;EACC;EACE;;AACA;EACD;;AAEC;EACD;;;AAOD;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIF;EACE,cA9MoC;;;AAgNtC;EACE,cAtNoC;EAuNpC;;;AAIA;EAIE;EACD;EACA;EACA,kBAzNmC;EA0NnC,kBA5NmC;EA6NnC;;AACA;EACG;EACA;EACF;EACA,kBAvOkC;EAwOlC;;AAGF;EAGE;EACA;EACA;EACA;EACA,oBA3OkC;EA4OlC,oBA9OkC;;AA+OlC;EACE;EACA;EACA;EACA;EACA,oBAzPgC;;AA4PpC;EAIE;EACD;EACA;EACA,qBA5PmC;EA6PnC,qBA/PmC;EAgQnC;;AACA;EACG;EACA;EACF;EACA,qBA1QkC;EA2QlC;;AAGF;EAGE;EACA;EACA;EACA;EACA,mBA9QkC;EA+QlC,mBAjRkC;;AAkRlC;EACE;EACA;EACA;EACA,mBA3RgC;EA4RhC;;;AASJ;EACC,kBA3TqB;;AAiUtB;EACC,oBAlUqB;;AAwUtB;EACC,qBAzUqB;;AA+UtB;EACC,mBAhVqB;;;AAqVxB;EACE;;;AAGF;EACG;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIH;EACE;IAAM;;;AAGR;EACE;IAAM;;;AAGR;EACE;EACA;EACA;EACA;EACA;EACA;EACA,SA3UyB;;;AAgVvB;EACE;EACA;EACA;EACA;EACA;EACA;;;AC5VN;EACE,WA9BqB;;AA+BrB;EACD;;AAEC;EACD,YArBuB;EAsBvB;;AACA;EACE;;AACA;EACD;EACA;EACA;EACA;;AAIA;EACD;EACA;;AAGC;EACD,kBvGhCmB;;AuGiCnB;EACE,W1GlDmB;E0GmDnB,a1G/BsB;E0GgCtB;EACA;EACA;EACA,OvGjCa;;AuGqCd;EACD;;;AC1ED;EACE;EACA;;AAEA;EACE,W3GSkB;E2GRlB;;AAEA;ExCRA;;AwCaF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACE,YxGKc;EwGJd,ezGrBmB;;AyGuBrB;EACE,YxG3BU;;AwG6BZ;EACE;EACA,YxGbmB;;AwGiBrB;EACE,YxGpCU;EwGqCV,ezGlCmB;;AyGoCrB;EACE;EACA,YxGvBmB;;AwG4BrB;EACE,OxG3Ca;;AwG4Cb;EACE,OxG7CW;;AwGkDf;EACE,YxGnDa;;AwGuDf;EACE,YxGxDa;;AwG6Df;EACE,OxGxDY;;AwGyDZ;EACE,OxG1DU;;AwG+Dd;EACE,YxGhEY;;AwGoEd;EACE,YxGrEY;;AwGyEhB;EACE,W3G5EkB;E2G6ElB;EACA;;AAEA;EACE;EACA;;;AClFN;EACC;EACA;;AAEA;EACC,QrDfmB;EqDgBnB,arDhBmB;;AqDkBpB;EACC,QrDjBoB;EqDkBpB,arDlBoB;;AqDoBrB;EACC,QrDnBmB;EqDoBnB,arDpBmB;;AqDsBpB;EACC;EACA,arD1BoB;;AqD6BrB;EACC,gBA1BiC,iBA0BkB;EACnD,QA3BiC;;AA8BlC;EACC;IACC;;;;ACvBF;EACE;EACA;EACA;;AASH;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAIqC;EAAW;;;AACX;EAAW;;;AAEX;AAAA;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AASX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AACX;EAAW;;;AAMjD;EACC,O1G3Te;;A0G6Tf;EACC,OA9T2B;;AA+T3B;EACC,O1GxTgB;;A0G4TlB;EACC,O1GnTuB;E0GoTvB;;AAGD;EACC,O1G1RoB;E0G2RpB;;AAGE;EACE;;;AAKL;EACC;;;AAIF;AAAA;EAEC;;;AAGD;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAED;EACC;;;AAID;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACC;EACA;;;AAED;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAGF;EACC;EACA;;;AAED;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;ACjgBF;EACC,QvD3BoB;EuD4BpB,OvD5BoB;EuD8BpB;EAEA,eA5ByB;EA6BzB,cAxBwB;EAyBxB,cA5BuB;EA8BvB;EACA;EACA;EACA;;AAGA;EACC,c5GhDqB;;A4GkDrB;EACC,QvD9CkB;EuD+ClB,OvD/CkB;EuDgDlB;EACA,QA7CuB;EA8CvB;EACA;;AAKD;EACC,a9G7BuB;E8G8BvB,gBAxC4C;EAyC5C;EACA;EACA;EACA;;AAIA;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;AALD;EAGC,kBAFQ;EAGR,cAFW;EAGX;;;AAQJ;EACC;IACC,QxEjF4B;IwEkF5B,OxElF4B;IwEmF5B,cA5E4B;;EA+E3B;IACC,QxEvF0B;IwEwF1B,OxExF0B;;EwE6F3B;IACC;IACA;;;AClFJ;EACI,evGbwB;;AuGcxB;EAFJ;IAGQ;;;;AAIR;EACI,kB5GFS;E4GGT,YAnBkC;EAoBlC,avGhB0B;EuGiB1B,gBvGjB0B;EuGkB1B;EACA;;AAGI;EACI;;AAEJ;AAAA;EAEI;;AAEJ;AAAA;EAEI;;AAIJ;EACI;;AAEJ;AAAA;EAEI;;AAEJ;AAAA;EAEI;;AAIR;EACI;EACA;;AAGJ;EAEI;;AACA;EACI,W/GtDU;E+GuDV,a/GnCa;E+GoCb;EACA;EACA;;AlCvBR;EkCeA;IAYQ;;;AlC3BR;EkC+BA;IAEQ;IACA,YvGxEkB;;;AuG6E1B;AAAA;EAEI;;AAGJ;EACI;;AlC7CJ;EkC4CA;IAIQ;;;AAIA;EACI,evGjGY;;AuGoGhB;EC5FV,YAPW;EAQX,SxGC+B;EwGA/B,kB7GekB;E6GdlB,Q9Gde;E8Gef,e9GbsB;E8GctB,Y7GKW;EJ4CX,oBiHhDA;EjHiDQ,YiHjDR;;AACA;EACE,c9GtBmB;;A6G0GX;EAEI;EACA,W/GlGM;;A+GmGN;EACI,O5GnEE;;;A4GuFlB;AAAA;EACI,evG9HoB;;;AuGkI5B;EACI,kB5GjHS;;A4GkHT;EACI;;;AAIR;EACI,kB5GxHS;E4GyHT;EACA;;AACA;EACI,gBvG9IoB;;;AqE8CxB;EkCqGJ;AAAA;IAGQ,kB5G7HY;;;;A4GiIpB;EACI;;;AAOJ;EACI;;AlCrHA;EkCoHJ;IAIQ;IACA;IACA;IACA;;EACA;IACI;IACA;IAEA;IACA;IACA;IAEA;IACA;;EAEJ;IACI;;;;AlCrIR;EkC4IJ;IAEQ;;;;AlCjJJ;EkCuJJ;IAEQ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;;;AAKR;EACI;EACA;EACA;EACA;EACA;EAEA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AASR;EACI;EACA;EACA;;AAEA;EACI;EACA,O7G5Pe;;;A2EoDnB;EkC6MJ;IAEQ;IACA;IACA;IACA;IACA;IACA;IACA;;;;AAQR;AAAA;EAEI;EACA;EACA;;;AAIA;EACI;;AACA;EACI;;AAEJ;EACI;;;AASZ;EACI;;;AAWA;AAAA;AAAA;EACI;EACA;;;AAMJ;EACI;EACA;;;AAGR;EACI;;;AAMA;AAAA;EACI;EACA;;;AAGR;EACI;;;AAKA;EAEI;EACA;;;AAOJ;AAAA;EAEI;EACA;;;AAKR;EACI,kB5G1Uc;;;A0EwBd;EkCqTJ;AAAA;IAGQ;;;;AAKR;EACI;;AACA;EACI;EACA;;;AASR;EACI;;;AlC7UA;EkCuVI;AAAA;AAAA;AAAA;AAAA;AAAA;IAGI;IACA;IACA;;EAEJ;AAAA;IAEI;IACA;;EAIJ;AAAA;IACI;IACA;;EAEJ;AAAA;IACI;;EASJ;AAAA;IACI;;EAGJ;AAAA;IACI;IACA;;EAOJ;AAAA;AAAA;AAAA;IAEI;;EAIJ;AAAA;IACI,W/Gtac;I+Guad;;EAIA;AAAA;IACI;IACA;;EACA;AAAA;IACI;IACA;;EAEJ;AAAA;IACI;;EAQZ;AAAA;AAAA;AAAA;IAEI;IACA;;EAEJ;AAAA;IACI;;;;AAOR;EAEI;EACA;EACA;EACA,kB5GxcK;E4GycL,YhCheW;;AgCkef;EACI;EACA,kB5GtcY;E4GucZ,YA3W4B;;AA4W5B;EACI;;AAGR;EACI,YAjX4B;EAkX5B;;AACA;EACI;;AAGR;EACI;EACA;;;AAIR;EACI;IACI;;EAEJ;IACI;;;AElfR;EACI;EACA,SANe;EAOf,KrDb6B;EqDc7B;EACA,OAVa;;;AAajB;EACI;;;AAGJ;EACI;;;AAGJ;EACI,azGT0B;EyGU1B,YzGXwB;EyGYxB,ejHLsB;EiHMtB,czGrB2B;EyGsB3B,SzG/Bc;EyGgCd;EACA;EACA,qBACA;EAGA,uBAnC2B;EAoC3B,UzGvCc;EyGwCd,YAhCc;EAiCd;EACA,YAtCkB;;AAwClB;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA,WjHpDc;;AiHuDlB;EACI;EACA;EACA;;;AClER;EACC;EACA;EACA;;AACA;EACC,clHKqB;EkHJrB;;AAED;EACC;;AAEA;EACC;EACA;EACA;EACA;EACA;;AACA;EACC,cClB8B;;ADmB9B;EACC;;AAGF;EACC,cCxB8B;ED0B9B;EACA;EACA;EACA;EAEA;EACA,WlHxBkB;EkHyBlB,O/GQkB;;A+GNnB;EACC,cCpC8B;EDqC9B;;AAGF;EACC,kB/GVe;;A+Gaf;EACC,O/G3BqB;E+G6BrB;EACA;EACA;EACA;;AAED;EAEC;EACA;;AAGF;EACC;;;AEnDH;AAAA;AAAA;AAAA;AAAA;EAKC;EACA;EACA,K5GI0B;;;A4GA3B;AAAA;AAAA;AAAA;EAIC;EACA,YhHM0B;EgHL1B,WhHK0B;EgHJ1B;EACA,S5GR0B;E4GS1B,kBjHGiB;EiHFjB,elHtBkC;;;AkHuClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACC,YhHhB6B;EgHiB7B,WhHjB6B;EgHkB7B,elH1CiC;;AkH6ClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACC;EACA;EACA;EACA,kBjHzBgB;;;AiHkCjB;AAAA;AAAA;EAEC,elH5DiC;;AkH8DlC;AAAA;AAAA;EACC;;AAGD;AAAA;AAAA;EACC;EACA,M5GvE4B;E4GwE5B;EACA,OjH7Ec;EiH8Ed;;;AAIF;EACC;;;AAID;AAAA;AAAA;EAGC;EACA;EACA;;;AAKA;EACC;;;AAID;EACC;EACA;EACA;;;AAID;EACC;EACA;EACA;;;AAMD;EACC;EACA;;;AAKF;EACC,elHvHkC;;AkHwHlC;EACC;;AAED;EACC;;AAGD;EACC;;;AAKF;EACC,SzD5IoB;;;AzBuCrB;AmF/CA;EACC;EACA;EACA;EACA;;;ACWD;EACC,SAbe;EAcf,etHYyB;EsHXzB;EACA,epHTwB;;AoHYxB;EACE;EACA;;AAIF;EACE,atHUsB;;AsHNxB;AAAA;EAEE;;AAGF;EACE;;;AASD;AAAA;EAED;;AAGA;AAAA;EACE;EACA;EACA;EACA;;;AAsBD;EAbD,OnHzBe;EmH0Bf,kBnHLqB;EmHMrB,cnHNqB;;AmHQrB;EACE;;AAGF;EACE;;;AAQD;EAjBD,OnHzBe;EmH0Bf,kBnHDkB;EmHElB,cnHFkB;;AmHIlB;EACE;;AAGF;EACE;;;AAYD;EArBD,OnHzBe;EmH0Bf,kBnHGqB;EmHFrB,cnHEqB;;AmHArB;EACE;;AAGF;EACE;;;AAgBD;EAzBD,OnHzBe;EmH0Bf,kBnHOoB;EmHNpB,cnHMoB;;AmHJpB;EACE;;AAGF;EACE;;;AAwBH;EACC,OnHrFkB;;;AmHwFnB;EACC,OnHzFkB;;;AmH4FnB;EACC,WtH7FqB;EsH8FrB;EACA,OnH/FkB;;;AmHmGlB;EACC;;;AAID;EACC;;;ACtHF;AACA;EACC;EACA;EACA;EACA;EACA,kBpHoBY;EoHnBZ;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;ACrCD;AACA;EACC,OrHekB;EqHdlB;EACA,WxHkBoB;EwHjBpB,axHiCwB;;AwHhCxB;EALD;IAMQ,WxHakB;;;;AwHT1B;EACC;;AACA;EAFD;IAGQ;;;;AAIR;EACI;EACA;;;AAGJ;EACC,axHcwB;EwHbxB;EACA;EACA;EACA,WxHVwB;EwHWxB,OrHZkB;;;AqHenB;EACC;EACA;EACA,YhH5B2B;EgH6B3B,chHhB6B;EgHiB7B,ehH9B2B;EgH+B3B;;AACA;EAPD;IAQQ;IACN;IACA;IACA;;;;AAIF;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AAGD;EACC,WxHpDsB;EwHqDtB;EACA,OrH9Be;;;AqHiChB;EACC;EACA;EACA;;AACA;EAJD;IAKE,OrHtCc;;;AqHwCf;EACC;;;AAIF;EACC;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;AACA;AACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;AACA;EAHD;IAIE;IACA;IACA;;;;AAIF;EACC;;;AAGD;EACC;;;AChID;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC,WzHGqB;EyHFrB,OtHwBe;EsHvBf;EACA;;;AAGD;EACC;;;AAED;AACA;EACC,WzHZqB;EyHarB,azHWwB;;;A0HvCzB;AACA;EACC;EACA;;;AAGD;AACA;EACC;EACA;;;ACPD;AACC;EACA;;AACA;EAHD;IAIE;;;;ACNF;AACA;EACC;EACA;;;AAGD;EACC;;;ACJA;EADD;IAKE;;;;AAKD;EADD;IAEQ;;;;ACRR;EAEI,YtHGwB;;AsHDxB;EACI;;;AAIR;AAAA;EAEI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;AAAA;EAEI,ctHnB0B;;;AsHsB9B;AAAA;EAEI,etHxB0B;;;AsH2B9B;AAAA;AAAA;EAGI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAIJ;EACI;EACA,etHjC0B;;;AuHvB9B;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;ACTA;EADD;IAEE;IACA;;;;AAKD;EADD;IAEQ;;;;AAIR;AAEA;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACG,O7HYa;E6HXb,kB7HEe;E6HDf;EACA;EACA;;;AAIH;EACG,O7HGa;E6HFb,kB7HPe;E6HQf;EACA;;;AAGH;EACG,O7HJa;E6HKb;EACA;EACA;;;AAIH;EACC;EACA;EACA;AACA;;;AAGD;EACG;EACA;EACA;EACA;EACA;EACA;;;AAGH;EACG;;;AAGH;EACG;EACA,O7HjCa;E6HkCb;;;AAGH;EACG;EACA;EACA,ahI1CsB;;;AgI6CzB;EACG;EACA;;;AAGH;EACG;;;AAGH;EACG,kB7HlEU;E6HmEV,O7HtDa;E6HuDb;EACA,ahIxDsB;EgIyDtB;EACA;EACA;;;AAGH;EACG;EACA,O7HhEa;E6HiEb;EACA,ahIlEsB;EgImEtB;EACA;;;AAGH;EACG;EACA,O7HzEa;E6H0Eb;EACA;EACA;EACA;;;AAGH;EACG;EACA,O7HlFa;E6HmFb;EACA;EACA;EACA;;;AAGH;EACG;EACA,O7H3Fa;E6H4Fb;EACA;EACA,ahIhGsB;EgIiGtB;EACA;;;AAGH;EACG;;;AAGH;AACA;AAAA;EAEG,WhIpImB;EgIqInB;EACA;EACA;EACA,ahIhHsB;;;AgImHzB;AAAA;EAEG;;;AAGH;EACG;EACA;;;AAGH;EACG;;;AAGH;EACG;EACA;EACA,ahIpIsB;EgIqItB,WhI3JoB;EgI4JpB;;;AAGH;EACG;EACA;EACA;;;AAGH;EACG;EACA;EACA,ahIlJsB;EgImJtB,WhI3KmB;;;AgI8KtB;EACG;EACA;EACA,ahIzJsB;EgI0JtB,WhIlLmB;EgImLnB;;;AAGH;EACG;EACA,ahIhKsB;;;AgImKzB;EACG;EACA;;;AAKH;AACA;AACA;EACG;EACA,WhItMmB;EgIuMnB,ahI/KsB;EgIgLtB;;;AAGH;EACG,WhI5MmB;EgI6MnB;EACA;;;AAGH;EACG,WhIlNmB;EgImNnB;;;ACzNH;EjBSE,YAPW;EAQX,SxGC+B;EwGA/B,kB7GekB;E6GdlB,Q9Gde;E8Gef,e9GbsB;E8GctB,Y7GKW;EJ4CX,oBiHhDA;EjHiDQ,YiHjDR;;AACA;EACE,c9GtBmB;;;AgIMvB;ElBQE,YAPW;EAQX,SxGC+B;EwGA/B,kB7GYgB;E6GXhB,Q9Gde;E8Gef,e9GbsB;E8GctB,Y7GKW;EJ4CX,oBiHhDA;EjHiDQ,YiHjDR;;AACA;EACE,c9GtBmB;;;AgC4DvB;AiGpDA;EACE;EpIgLA,oBoI/KA;EpIgLK,eoIhLL;EpIiLQ,YoIjLR;;AAEA;EACE;;;AAIJ;EACE;;AAEA;EAAY;;;AAKd;EAAoB;;;AAEpB;EAAoB;;;AAEpB;EACE;EACA;EACA;EpI8JA,6BoI7JA;EpI8JQ,qBoI9JR;EpIqKA,6BoIpKA;EpIqKQ,qBoIrKR;EpIwKA,oCoIvKoC;EpIwK5B,4BoIxK4B;;;ACxBtC;AAAA;EAEE;EACA;EACA;;AACA;AAAA;EACE;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAIE;;;AAOJ;AAAA;AAAA;AAAA;EAIE;;;AAKJ;EACE;;AnE5CA;EACE;EACA;EACA;;AmE4CF;AAAA;AAAA;EAGE;;AAEF;AAAA;AAAA;EAGE;;;AAIJ;EACE;;;AAIF;EACE;;AACA;E7B5DA,yB6B6D+B;E7B5D/B,4B6B4D+B;;;AAIjC;AAAA;E7BzDE,wB6B2D4B;E7B1D5B,2B6B0D4B;;;AAI9B;EACE;;;AAEF;EACE;;;AAGA;AAAA;E7B9EA,yB6BgF+B;E7B/E/B,4B6B+E+B;;;AAGjC;E7B3EE,wB6B4E4B;E7B3E5B,2B6B2E4B;;;AAM5B;AAAA;EACE;;AzGrBH;AAAA;EACC;EACA,QAJwB;EAKxB;;;AyGsCF;EACE;EACA;;;AAEF;EACE;EACA;;;AAKF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAQA;AAAA;AAAA;EAGE;EACA;EACA;EACA;;AnExJF;EACE;EACA;EACA;;AmE2JA;EACE;;AAIJ;AAAA;AAAA;AAAA;EAIE;EACA;;;AAKF;EACE;;AAEF;E7B9KA,wBrGMsB;EqGLtB,yBrGKsB;EqGEtB,4B6BwKgC;E7BvKhC,2B6BuKgC;;AAEhC;E7BlLA,wB6BmL6B;E7BlL7B,yB6BkL6B;E7B3K7B,4BrGFsB;EqGGtB,2BrGHsB;;;AkIiLxB;EACE;;;AAGA;AAAA;E7BnLA,4B6BqLgC;E7BpLhC,2B6BoLgC;;;AAGlC;E7BhME,wB6BiM2B;E7BhM3B,yB6BgM2B;;;AAO7B;EACE;EACA;EACA;EACA;;AACA;AAAA;EAEE;EACA;EACA;;AAEF;EACE;;AAGF;EACE;;;AAoBA;AAAA;AAAA;AAAA;EAEE;EACA;EACA;;;AC5ON;EANI,SAD4B;EAE5B;EACA;EACA;;;ACoBJ;EACI;;;AAGF;EACE;EACA;EACA;EACA;EACA,SAd0C;;AAgB1C;EACI;EACA;;AAEI;EACJ;;AAGJ;EACE;EACA;EvIgJJ,oBuI/II;EvIgJC,euIhJD;EvIiJI,YuIjJJ;;AAGA;AAAA;AAAA;AAAA;EDhDF,SAD4B;EAE5B;EACA;EACA;ECkDI;;AAIF;EAfF;IvIuKF;IACG;IACE;IACG;IAxJR,6BuIDmC;IvIEhC,0BuIFgC;IvIG3B,qBuIH2B;IvI6GnC,qBuI5G2B;IvI6GxB,kBuI7GwB;IvI8GnB,auI9GmB;;EAErB;IvIoFN;IACQ;IuIlFA;;EAEF;IvI+EN;IACQ;IuI7EA;;EAEF;IvI0EN;IACQ;IuIvEA;;EACA;IACE;;;AAMR;AAAA;AAAA;EAGE;;AAGF;EACE;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;;AAEF;EACE;;AAEF;AAAA;EAEE;;AAGF;EACE;;AAEF;EACE;;;AAQJ;EACE;EACA;EACA;EACA;EACA,OAvH0C;EvI4N5C;EACA,SuI9N4C;EA0H1C,WAzH0C;EA0H1C,OnI9HY;EmI+HZ;EACA,aAhI0C;EAiI1C;;AAQA;EACE;EACA;;AAKF;EAEE;EACA,OnInJU;EmIoJV;EvI4EJ;EACA,SuI5EqB;;AAInB;AAAA;AAAA;AAAA;EAIE;EACA;EACA;EACA;EACA;;AAEF;AAAA;EAEE;EACA;;AAEF;AAAA;EAEE;EACA;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;AAKA;EACE;;AAIF;EACE;;;AAUN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,kBnI3NU;;AmI6NZ;EACE;EACA;EACA;EACA,kBnIjLgB;;;AmIwLpB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OnIhPY;EmIiPZ;EACA,aAlP0C;;AAmP1C;EACE;;;AAMJ;EAII;AAAA;AAAA;AAAA;IAIE;IACA;IACA;IACA;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;;EAKJ;IACE;IACA;IACA;;EAIF;IACE;;;AAKN;EAIM;AAAA;AAAA;AAAA;IAIE;IACA;IACA;IACA;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;;EAKJ;IACE;IACA;IACA;;EAIF;IACE;;;ACtTN;EACI;EACA;EACA;;AAGA;EACE;EACA;EACA;;AAGF;EAGE;EACA;EAKA;EAEA;EACA;;AAEA;EACE;;;AAQN;AAAA;AAAA;EAGE;;AAEA;AAAA;AAAA;EACE;;;AAIJ;AAAA;EAEE;EACA;EACA;;;AAKF;EACE;EACA,WvIvDmB;EuIwDnB;EACA;EACA,OpIlCY;EoImCZ;EACA,kBpI1CgB;EoI2ChB;EACA,erItEoB;;AqIyEpB;EACE;EACA,WvIrEgB;EuIsEhB,erI1EmB;;AqI4ErB;EACE;EACA,WvItEgB;EuIuEhB,erIhFmB;;AqIoFrB;AAAA;EAEE;;;AAKJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EhC9FA,yBgCqG+B;EhCpG/B,4BgCoG+B;;;AAE/B;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EhClGA,wBgCyG8B;EhCxG9B,2BgCwG8B;;;AAE9B;EACE;;;AAKF;EACE;EAGA;EACA;;AAIA;EACE;;AACA;EACE;;AAGF;EAGE;;AAMF;AAAA;EAEE;;AAIF;AAAA;EAEE;EACA;;;A/DzJR;EvEDI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AiC0CN;AsGxEA;AAEA;EACC;;;ACAD;AAEA;EACC;EACA;EACA,kBtIoBY;;;AsIjBb;EACC;EACA;;AACA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA,OtImBqB;EsIlBrB,WzIfqB;EyIgBrB;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA,WzIpCqB;EyIqCrB;EACA;;;AAGD;EACC;;;AAGD;EACC,WzI9CqB;EyI+CrB,OtIdqB;EsIerB;EACA;EACA,kBtIjCiB;EsIkCjB;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC,OtIxEkB;EsIyElB;EACA,WzI1EqB;EyI2ErB;EACA;EACA,kBtIhEY;;;AsImEb;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;IACC;;;ACtHF;AAKA;EACC;;;AAGD;EACC;EACA;EACA;;AACA;EACC,clITgB;;;AkIkBlB;EAHC;;;AAMD;EANC;;;AASD;EATC;;;AAYD;EAZC;;;AAeD;EAfC;;;AAkBD;EAlBC;;;AAqBD;EArBC;;;AAwBD;EAxBC;;;AA2BD;EA3BC;;;AAsCD;EANC;EACA;EACA;EAlCA;;;AAyCD;EATC;EACA;EACA;EAlCA;;;AA4CD;EAZC;EACA;EACA;EAlCA;;;AA+CD;EAfC;EACA;EACA;EAlCA;;;AAkDD;EAlBC;EACA;EACA;EAlCA;;;AAqDD;EArBC;EACA;EACA;EAlCA;;;AAwDD;EAxBC;EACA;EACA;EAlCA;;;AA2DD;EA3BC;EACA;EACA;EAlCA;;;AA8DD;EA9BC;EACA;EACA;EAlCA;;;ACjBD;AAEA;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;;AAMF;EACC;;AAFF;EAIC;EACA;EACA;EACA;;;AAGD;EACC;IACC;;;AAIF;EACC;EACA;EACO;;;AAGR;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACC;;AAEA;EACC;;AAGD;EACC;;AAGM;EACI;;AAGX;EACC,OxI/GoB;EwIgHpB,W3IjJoB;E2IkJpB;EACA;EACA;;AAGD;EACC;EACA,OxIxHoB;EwIyHpB;EACA;EACA;;AAGM;EACI;;AAEA;EACI;;AAIf;EACC,OxI9Ic;;AwIiJf;EACC,OxI7IoB;;AwIgJrB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC,kBxInKgB;;AwIsKjB;EACI;;AAGJ;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAIF;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;AAAA;EAEI;EACA;;;AAGJ;EACI;;;AAGJ;AAAA;EAEI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACC;EACA;;;AAGD;EACC;EACA;;;AAOD;EACC;;;AClVD;AAEA;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA,OzIsBe;EyIrBf;EACA;EACA;;;AAED;EACC;EACA,OzIee;EyIdf;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;EACC;;;AAID;EACC,W5IxCqB;;;A4I2CtB;EACC,W5IxCqB;E4IyCrB;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAID;AAEA;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;E7IfE,oB6IgBD;E7IfS,Y6IeT;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC,kBzI3EwB;;;AyI8EzB;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AC7GD;AAEA;EACC;;;AAGD;EACC;EACA,W7IIqB;;;A6IDtB;EACC;EACA,W7ICsB;;;A6IEvB;EACE;;;AAGF;EACC,a7IewB;E6IdxB;EACA,W7IXqB;E6IYlB;EACA;;;AAGJ;EACE;;;AAGF;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AChDD;AAEA;EACC,ctIU+B;EsIT/B,etIS+B;;;AsINhC;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA,W9IbqB;;;A8IgBtB;EACC;EACA;EACA;;;AAED;EACC,kB3ILiB;E2IMjB;;;AAGD;EACC;;AACA;EACC;EACA;;AAED;EACC;;AAED;EACC;EACA;;;AAKF;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA,O3I7DwB;E2I8DxB;EACA;EACA;EACA;;;ACtFD;AAQA;EACC,a/I0BwB;;;A+IvBzB;EACC,a/IwBwB;;;A+IrBzB;EACC;EACA,a/ImBwB;;;A+IhBzB;EACC;EACA;EACA;EACA,W/IdqB;;;A+IiBtB;EACC,a/IQwB;E+IPxB,W/InBqB;E+IoBrB;;;AAGD;AACA;EACC;EACA;EACA;EACA,kB5IfY;;;A4IkBb;EACC;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;;AACA;EALD;IAME;IACA;;;;AAKH;EACC;EACA;EACA;EACA;;;AAGD;EACC,YvInD8B;EuIoD9B,W/IpDqB;;A+IqDrB;EAHD;IAIE,YvI/D0B;IuIgE1B,W/IzDqB;;;;A+I6DvB;EACC,O5I/BqB;;;A4IkCtB;EACC;EACA;EACA;;AACA;EAJD;IAKE,cvIhE4B;IuIiE5B;;;AAGA;EADD;IAEE;IACA;;;;AAKH;EACC;EACA;;AAEA;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;;;AAIF;EACC,YvIrG8B;;;AuIwG/B;EACC;EACA,evI1G8B;EuI2G9B;;AAEA;EACC,kB5IrGW;E4IsGX;;AAGD;EACC;EACA;;AACA;EACC;;AAED;EACC;;AAGA;EADD;IAEE;;;AAKF;EADD;IAEE;IACA;;;;AAKH;EAEE;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;EAED;IACC;;;AAKH;EACC;EACA;EACA,a/ItJwB;E+IuJxB,O5IjLkB;;;A4IoLnB;EACC;EACA,a/I5JwB;;A+I6JxB;EACC,a/IhKuB;;A+IkKxB;EACC;;;AAIF;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC,YvIzM8B;;;AuI4M/B;EACC;;;AAGD;EACC,O5IrNkB;;;A4IwNnB;EACC,a/I/LwB;;;A+IkMzB;EACC;EACA;EACA;EACA;EACA;EACA;EACA,O5IzMe;E4I0Mf;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;ACrQD;AAEA;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;AACA;EACA,kB7IJiB;E6IKjB;AACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAID;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AC7GD;EACI,kB9IqBS;E8IpBT,oBrGFc;EqGGd,YrGHc;;AqGIjB;AAAA;EAEC;EACA;EACA;;;AAKD;EACC;;;AAGF;EACC;EACA,kB9IOiB;;A8INjB;EACC;;AAED;EACC;;;AAMA;EACC;;;AAKH;EACC,kB9IXiB;;A8IajB;EACC;;;AAGF;AAEA;EACC;;;AClDD;AAUA;EACE;EACA;;;AAEF;EACE;EACA,SARmB;EASnB,OAfiB;EAgBjB,QAfkB;;;AAiBpB;ACpBA;EACE;;AACA;EACE;;;AAKF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;EACA;EACA;;AAGF;EACE;;AACA;EACE;;AAHJ;EAKE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAIA;EACE;;AAIJ;EACE;;;ACjEJ;AAEA;EACC;EACA;EACA;EACA;;;AAGD;EACC,kBjJoBiB;EiJnBjB;;;AAGD;EACC;;;AChBC;EACD;EACA;EACA;;AAGC;EACD;;AAGC;EACD;EACA;;AAGC;EACD;EACA;;AAGC;EACD;EACA;EACA;;;AAID;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;ACtDF;AAEA;EACC;EACA,WtJSqB;EsJRrB,OnJQkB;;;AmJLnB;EACC;EACA,WtJGqB;EsJFrB,OnJmCqB;;;AmJhCtB;EACC;EACA;EACA;EACA,WtJLqB;EsJMrB;;;AAGD;EACC;EACA;EACA;EACA,WtJbqB;EsJcrB,OnJmBqB;;;AmJhBtB;EACC;EACA;;;AAGD;EACC;EACA,WtJxBqB;;;AsJ2BtB;EACC;;;AAGD;EACC;;;AC7CD;AAEA;EACC;;AACA;EACC;EACA;;;AAIF;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA,WvJRqB;AuJSrB;;;AAGD;EAEC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAIA;EACC;EACA;;AAHF;EAKC;;;AAGD;EACC;;;AAGD;EACC;;;ACvDD;EACG,YrJ6Be;EqJ5Bf;;;AAGH;EACI,gBhJJwB;;;AiJR5B;AAEA;EACC,OtJoCe;EsJnCf,kBtJ0BiB;EsJzBjB;EACA;;;AAGD;EACC,kBtJoBiB;EsJnBjB,OtJ4Be;EsJ3Bf;EACA;EACA;;;AAGD;EACC;EACA,OtJoBe;EsJnBf;EACA;EACA;;;ACpBD;AAEA;EAEC;IACC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGD;IACC;IACA;IACA;IACA;;EAGD;IACC;IACA;IACA;IACA;;EAGD;IACC;IACA;;EAGD;IAEC;IACA;AACA;AAAA;AAAA;;EAKD;IAEC;IACA;IACA;;EAGD;IAEC;IACA,elJ9C4B;;EkJiD7B;IACC;;EAGD;IACC;IACA;;EAGD;IACC;IACA;IACA;IACA;IACA;;EAGD;IACC;IACA,a1JtCuB;I0JuCvB;IACA;IACA;;EAGD;IACC;IACA;;EAGD;IACC;IACA;IACA;IACA;IACA;IACA;IACA;;EAGD;IACC;;EAGD;IACC;;EAGD;IACC;;EAGD;IACC;IACA,a1JzEuB;I0J0EvB;;EAGD;IACC;;EAGD;IACC;;EAGD;IACC;;EAGD;IACC;;EAGD;IACC;IACA;IACA,a1JhGuB;I0JiGvB;IACA;;;AAIF;AAEA;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA,kBvJxIiB;EuJyIjB;;;AAGD;EACC;EACA;;;AAGD;EACC;;AACA;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAMA;EACC;;;AC/LH;AAcA;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAIA;EACC;EACA,W3JtBqB;E2JuBrB,a3JDuB;;A2JGxB;EACC,W3JtBuB;E2JuBvB,a3JHuB;;;A2JOzB;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EAEI;EACH;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;AAEA;EAHD;IAIE;;;;AAIF;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EAEI;EACA;EACA;;;AAGJ;EACC;EACA;EACA;EAEA,SnJtH+B;;AmJuH/B;EACC,cnJxH8B;EmJyH9B,enJzH8B;;;AmJ6HhC;EACI,W3J9HmB;E2J+HnB,Y3J/HmB;;;A2JkIvB;EACC;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;AAGD;AAEA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EAEC;EACA;EACA;;;AAGD;EAEC;EACA;EACA;;;AAGD;EAEC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAID;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOC;;;AAGD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASI;;;AAEJ;AAAA;EAGI;;;AAEJ;EAEI;EACA;;;AAEJ;EAEI;EACA;EACA;;;AAKJ;EAEI;;;AAEJ;EAEI;;;AAEJ;AAAA;EAGI;;;AAGJ;EAEC;;;AAED;EAEC;;;AAED;EAEC;;;AAED;AAAA;EAGC;;;AAED;EAEC;;;AAED;EAEC;;;AAED;EAEC;;;AAED;EAEC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAED;EACC;EACA;EACA;;;AAOD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAGC;EACA;;;AAGD;EAEC;EACA;EACA;;;AAGD;EAEC;;;AAGD;AAAA;EAGC;;;AAGD;EAEC;EACA;;;AAGD;EAEI;EACA;EACA;;;AAGJ;EACC;;;AAGD;EAEI;;;AAGJ;EAEI;;;AAGJ;AAAA;EAGC;;;AAGD;AAAA;EAGI;EACA;;;AAEJ;EAEI;;;AAGJ;EACI;EACA;EACA;;;AAEJ;EACI;;;AAGJ;AAAA;EAGI;EACA;EACA;;;AAGJ;EAEC;;;AAGD;EACC;;;AAGD;AAEA;EACI;;;AAGJ;EACC;EACA;;;AAGD;EACI;EACA;EACA;EACA;;;AAGJ;EACI;;;AAEJ;AAEA;EAEC;;;AAED;EAEC;;;AAGD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASC;EACA;;;AAGD;EAEC;EACA;EACA,KnJzjB4B;EmJ0jB5B,OnJzjB8B;EmJ0jB9B,Y5EnkBkB;;A4EokBlB;EACC;;;AAIF;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAED;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;AACA;EACI;EACA;EACA;EACA,YxJ5kBc;EwJ6kBd,QzJtmBa;;AyJwmBb;EACI,YxJ1kBU;;AwJ6kBd;EACI;;;AAIR;EACI;;;AAGJ;EACI;;;AAGJ;EACI,QzJ1nBa;EyJ2nBb,kBxJ/lBgB;EwJgmBhB;EACA,SnJ1mBuB;;;AmJ6mB3B;EACI,QzJjoBa;EyJkoBb,kBxJnmBc;EwJomBd;EACA,SnJjnBuB;EmJknBvB;;;AAIA;EACI;;;AAIR;EACI,kBvJ5mBiB;;;AuJ+mBrB;EACI;EACA;;AACA;EACI;EACA,kBxJloBK;EwJmoBL,azJxpBS;EyJypBT,czJzpBS;EyJ0pBT,ezJ1pBS;EyJ2pBT;EACA;EACA,SnJ1oBmB;EmJ2oBnB;EACA;EACA;;AAEJ;EACI,kBxJpoBU;;;AyJjBlB;EACI;;AACH;EACC;;AAGE;EACI;EACA,WAtBsB;EAuBtB,cpJnBsB;;AoJoBtB;EACI;EACA;;AAEJ;EARJ;IASQ;IACA;;;AAGR;EACI;;AACA;EACI,kBzJfC;EyJgBD;;AAGR;EA1BJ;IA2BQ;;;;AAIR;EACC;IACC;;EACA;IACC;IACS;;;AAMZ;EACI;EACA;EACA;EACA,kBzJnCc;;;AyJwCd;EACI;EACA;;AACA;EACI;EACA;EACA;EACA;;AACA;EACI;;AAEJ;EACI;;AAEJ;EACI,a5J9CS;;;A4JsDxB;EACO;EACA;;AACA;EACI;EACA;;AAEJ;EACI,W5JpFU;E4JqFV,a5J/Da;;A4JkErB;EACI,epJlGoB;;;AoJwGxB;EACI,W5JlGe;;A4JqGnB;EACI;EACA;;;AAIR;EACE;;;AAWA;EACE;;AAEF;EACE;;;AAIF;EACE;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;;ACvJA;EACE;EACA;EACA;EACA;EACA;EACA;EACA,QD0H4B;ECzH5B;EACA;;ADsJJ;EACE;;AAGF;EACE;EACA;EACA,gBArC8B;;AAuC9B;EACE;;AAIF;EACE;;AAIJ;EACE;EACA,a1JjLa;E0JkLb;EACA;EAEA;EACA;EACA;EACA;;A/EzIA;E+EgIF;IAYI;IACA;IACA;;;AAGF;EACE;;AACA;EACE,W5JvLc;;A4J2LlB;EACE;;AAGF;EACE;EACA,YpJzMwB;EoJ0MxB;;ACjNF;EACE;EACA;EACA;EACA;EACA;EACA;EACA,QD0H4B;ECzH5B;EACA;;ADiNA;EACE,gBA1F0B;EA2F1B;EACA;;;A/E3KJ;E+EkLF;IACE;IACA;;EACA;IAEE;;;AAUN;EACE;EACA;EAEA;EACA;EACA;EACA,OA3HkB;;A/E7EhB;E+EiMJ;IAUI;;;AAGF;EACE;;AAGF;EACE;EACA;;AACA;EAEE;;AAGF;EACE;EACA;;AAGF;AAAA;AAAA;EAGE;;AAGF;EACE;EACA;EACA;;AACA;EACE;EACA;;AAEF;EACE;EACA;EACA,YpJnRuB;EoJoRvB,epJ1RqB;EoJ2RrB;EACA;;AAKN;EACE;EACA;;AAGF;EACE;EACA;;;AAQF;AAAA;EAEE;EACA,WA7LgB;EA8LhB;;AAIA;EACE,YpJlT2B;;;AoJuTjC;AAAA;EAEE,WA1MkB;EA2MlB;;;AAKE;EACE,epJ1UsB;;AoJ6U1B;EACE;;;AASA;EACE,epJxVsB;EoJyVtB,YpJnVwB;;AoJqV1B;EACE;;AAEF;EACE;;AACA;EACE;;AAEF;EACE;;;AAUJ;EACE;;;AASF;AAAA;EACE;;AAEA;AAAA;EACE;;;AEnYR;AAEA;EACC,O3JWiB;;;A2JRlB;EACC,O3JoCqB;;;A2JjCtB;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA,atJjB2B;EsJkB3B,gBtJlB2B;EsJmB3B,W9JdqB;;;A8JiBtB;AAEC;EACC;;;ACjCF;AAEA;EACC;;;AAGD;EACC,kB5JuBiB;E4JtBjB;EACA;;;ACPD;AAIE;AAAA;EACE;EACA;EACA;EACA,QpHLc;EoHMd,e9JE+B;E8JD/B,oBpHRc;EoHSd,YpHTc;;AoHYhB;AAAA;EACE,kB7Jac;E6JZd,SpHhBuB;EoHiBvB,ahKkBqB;EgKjBrB;EACA;;AACA;AAAA;EACE;EACA,O7JeU;;A6JXd;AAAA;EACE,e9JxBa;E8JyBb,kB7JJS;;A6JOX;AAAA;EACE,SpHhCuB;EoHiCvB;EACA;EACA;;AAGF;AAAA;EACE;EACA;EACA;EACA,cxJzCc;;AyJFlB;EACE,WjKWoB;;AiKTpB;EACE;EACA,czJe0B;;;A0JlB9B;AAEA;EACC;EACA;EACA;EACA,O/JJe;E+JKf,WlKOqB;EkKNrB;EACA;EACA,kB/JiBiB;E+JhBjB;EACA;EACA;;AACA;EACC,kB/JkBgB;;;A+JdlB;EACC;EACA;;;AAGD;EACC,QtHvBiB;EsHwBjB,e1Jf6B;E0JgB7B,ehKjBkC;;;AgKoBnC;EACC,a1JvB4B;;A0JwB5B;EACC;;;AAGF;EACC;EACA,kB/JLiB;;;A+JQlB;EACC;EACA;EACA;;;AAWD;EACI;;;ACzDJ;AAEA;EACC;EACA,OhKiCe;EgKhCf;;;AAGD;EACC;;AACA;EACC,kBhKiBgB;EgKhBhB;;;AAIF;EACC;;;AAGD;EACC;EACA;;;AAED;EACC;;;AAED;EACC;;;AAIA;EACC;;AAED;EACC,kBhKFgB;;AgKIjB;EACC;;;AAKF;EACC;EACA;EpKsBC,oBoKrBD;EpKsBS,YoKtBT;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA,kBhKrCY;;AwBjBZ;EACC;EACG;;AAEJ;EACC;EAEA,SARwB;EASxB;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;AwI8DzB;EACC;EACA;;AAEA;EACC;EACA;;;AAKH;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC,OhK5CqB;EgK6CrB,WnK9EqB;EmK+ErB;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC,kBhK5EiB;EgK6EjB;EACA,WnK/FqB;;;AmKkGtB;EACC,OhKzEe;;;AgK4EhB;EACC,OhKxEqB;;;AgK2EtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;AACC;EACA;EACA;EACA;AACA;AACA;AACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAIA;EACC;EACA;EACA;;;AAIF;EACC;EACA;;;AAGD;EACC,kBhK3JiB;EgK4JjB,OhKlJqB;;;AgKqJtB;EACC;;;AAGD;AACA;EACC;EACA;EACA;;;AAGD;EACC;;;ACjND;AAIA;EACC;;;ACLD;AAEA;EACC;EACA;;;AAKG;EACE;EACA;EACA;;;AAKN;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;;AAIF;EACC;EACA;EACA;;;AAKD;EACC;EACA;EACA;;;ACjDF;AAEA;EACC;EACA;EACA;EACA,etKoByB;EsKnBzB;EACA;EACA;EACA,kBnKmBiB;EmKlBjB;EACA;EACA,S9JXiB;;;A8JclB;EACC,kBnKYiB;;;AmKTlB;AAAA;AAEC;EACA,atKewB;EsKdxB;EACA;EACA;EACA,OnKVkB;EmKWlB;AACA;;;AAGD;EACC,WtKnBsB;;;AsKsBvB;EACC,atKCwB;EsKAxB;EACA;EACA,WtKxBqB;EsKyBrB;;;AAGD;EACC,atKPwB;EsKQxB;EACA;EACA,WtKlCsB;EsKmCtB;;;AAID;AACA;EACC,WtK3CqB;EsK4CrB,OnKXqB;;;AmKctB;AACA;EACC,WtKjDqB;EsKkDrB,OnKjBqB;EmKkBrB;EACA,kBnKnCiB;EmKoCjB;;;AAGD;EACC,StKvDsB;;;AsK0DvB;EACC,S9JlD0B;E8JmD1B;;;AAGD;EACC,OnKjCqB;;;AoKhDtB;AAEA;EACC;EACA;;;AAGD;EACC;EACA;;;ACND;AAIA;EACC,OrKiCe;EqKhCf,kBrKuBiB;EqKtBjB;;;AAGD;EACC,OrK2Be;EqK1Bf,kBrKoBmB;EqKnBnB;;;AAGD;EACC,kBrKemB;;;AqKXpB;EACC,kBrKOiB;;;AqKJlB;EACC,kBrKGiB;;;AqKAlB;EACC;;;AAGD;EACC,WxKtBqB;EwKuBrB;;;AAGD;EACC,WxK7BsB;EwK8BtB;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;AACA;EACA;EACA;AACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAMD;EACC;EACA,WxK1EqB;EwK2ErB;EACA;;;AAGD;EACC;EACA,WxKjFqB;;;AwKoFtB;EACC;EACA,WxKtFqB;EwKyFrB;EACA,kBrKzEiB;EqK0EjB;EACA,ehK3F6B;;AgK4F7B;EACC;EACA;;AACA;EAHD;IAG4C;;;AAH5C;EAIC;EACA;EACA,OrKzEc;EqK0Ed;;AAED;EACC,kBrK1FW;;AqK4FZ;EACC,axKhFuB;EwKiFvB,WxK3GoB;;AwK+GrB;EACC,kBrKhHiB;;AqKiHjB;EACC;;AAGF;AAAA;EAEC,OrK7Fc;EqK8Fd;;AAED;EACO;EACA;;;AAIR;EACC;EACA;;;AAID;EACC;EACA;EACA,WxKvIsB;;;AwKyIvB;EACC;EACA;EACA,WxK5IsB;;;AwKgJvB;EACC,YhKxJ2B;EgKyJ3B,OrK1He;EqK2Hf;EACA;EACA,WxKrJsB;;;AwKwJvB;EACC,OrKjIe;EqKkIf,axKpIwB;EwKqIxB,kBrK5IiB;;;AqKgJlB;EACC;EACA,kBrKlJiB;EqKmJjB,axK5IwB;EwK6IxB;EACA;EACA;EACA;EACA,ctKpLsB;;;AsKuLvB;EACC,kBrK5JiB;EqK6JjB;EACA;EACA;EACA;EACA,ctK7LsB;EsK8LtB;EACA;;;AAGD;EACC;EACA;EACA,kBrKtKmB;EqKuKnB,WxKzLsB;EwK0LtB;;;AAGD;EACC;;;AAED;EACC;EACA;EACA;EACA,oBtKjNsB;;;AsKoNvB;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA,qBtK7NsB;;;AsKgOvB;AACC;EACA;EACA;;;AAGD;AAAA;EAEC,kBrKhNY;;;AqKmNb;EACC;EACA,ctK7OsB;EsK+OtB;EACA;EAEA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA,oBtKrQsB;;;AsKyQvB;EACC;EACA,kBrKnPY;EqKoPZ;EACA;EACA;EACA;EACA,ctKhRsB;EsKiRtB;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA,ctK3RsB;;;AsK+RvB;EACC;EACA;EACA;EACA;EACA;EACA;EACA,ctKtSsB;;;AsKySvB;EACC;EACA,kBrK/QiB;EqKgRjB;EACA;EACA;EACA;EACA,ctKhTsB;;;AsKmTvB;EACC;EACA,kBrKzRiB;EqK0RjB;EACA;EACA;EACA;EACA,ctK1TsB;;;AsK6TvB;EACC;EACA;EACA;EACA;EACA,WxKrTsB;;AwKuTtB;EACC;EACA;EACA;;;AAIF;EACC,WxK/TsB;;;AwKkUvB;EAEC;AACA;EACA;EACA;EACA;EACA;EACA,ctKvVsB;EsKwVtB,WxK3UsB;AwK4UtB;;;AAGD;EACC,WxKhVsB;EwKiVtB;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA,axKvUwB;;;AwK0UzB;EACC;EACA;;;AAID;EAEC;;;AAID;EACC,OrKrVe;EqKsVf,kBrK/ViB;;;AqKkWlB;EACC;EACA,WxKrXqB;EwKsXrB,axK9VwB;EwK+VxB,kBrK1WY;;;AqKgXb;EACC,axKlWyB;;;AwKqW1B;EACC;EACA;;;AAGD;EACC;EACA;EACA,kBrK5XY;EqK6XZ;EACA;EACA;EACA,ctKxZsB;EsKyZtB,WxK9YqB;EwK+YrB;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA,ctKnasB;EsKoatB,WxKzZqB;EwK0ZrB;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA,ctK9asB;EsK+atB,WxKpaqB;EwKqarB;EACA;;;AAGD;EACC;EACA,kBrK1ZiB;EqK2ZjB;EACA;EACA,ctKzbsB;EsK0btB,WxK/aqB;EwKgbrB;EACA;;;AAGD;EACC;EACA,kBrKraiB;EqKsajB;EACA;EACA,ctKpcsB;EsKqctB,WxK1bqB;EwK2brB;EACA;;;AAGD;EACC;EACA,kBrKpbY;EqKqbZ,WxKpcsB;EwKqctB;EACA;;;AAGD;EACC,OrK9bwB;;;AqKgczB;EACC;EACA;;;AAGD;EACC;EACA,YhKtd2B;;;AgKyd5B;EACC;;AACA;EACC;EACA;;;AAMF;EACC;EACA;;;AAGD;EACC;;;ACnfD;AAEA;EACC;;;AAGD;EACC;;;ACDD;AACA;EACC;;;AAGD;EACC;EACA;EACA,W1KCqB;;;A0KEtB;EACC;;;AAGD;AACA;EACC;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;EACA;;;AAIA;EACC,W1KtBqB;E0KuBrB;EACA;EACA;;AACA;EACC;EACA;EACA;;AAED;EACC;EACA;EACA;EACA;;;AAKH;EACC;;;AAGD;EACC;EACG;;;AAGJ;EACC,kBvK7BiB;EuK8BjB;;;AAID;EACC;;;AAGD;AACC;AAAA;EAEA;EACA;EACA,elKnE8B;;AkKqE9B;EACC;;;AAIF;AACC;AAAA;AAAA;EAGA;EACA;;;AAED;EACC;EACA;;;AAGD;EACC,alKvF8B;;;AkK0F/B;AAAA;EAEC,OnHnGqB;EmHoGrB,QnHpGqB;EmHqGrB;;;AAGD;AACC;EACA;EACA;EACA;;;AAGD;AACA;AACC;;;AAGD;EACC;EACA,clKxHiB;EkKyHjB;;;AAGD;EACC,kBvKlGiB;EuKmGjB,S9H/H0B;E8HgI1B,a1K7FwB;E0K8FxB;EACA;;AACA;EACC;EACA,OvKhGc;;AuKmGf;EACC;EACA;;;AAIF;EACC;EACA;EACA;EACA;AACA;EACA,kBvKxHiB;EuKyHjB;;;AAID;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA,Q9HlKiB;E8HmKjB,exK3JkC;EwK4JlC,oB9HrKiB;E8HsKd,Y9HtKc;;;A8HyKlB;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC,kBvKnKY;;;AuKsKb;EACC,kBvKnKiB;EuKoKjB;;;AAID;EACC;EACA,kBvK9KY;;;AuKiLb;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA,kBvK3LiB;EuK4LjB;EACA;;;AAGD;EACO;;;AAGP;EACO;;;AAGP;EACC;;;AAGD;EACC;;;AAED;EACC,W1K7NqB;;;A2KdtB;AAEA;EACC;EACA;EACA;EACA;EACA,qBxKawB;EwKZxB,oBxKYwB;EwKXxB;EACA,W3KFsB;E2KGtB;EACA;;;AAGD;EACC;EACA;EACA;EACA,W3KTqB;E2KUrB;EACA;EACA;EACA;EACA,qBxKJwB;EwKKxB,oBxKLwB;;AwKMxB;EACC,OxKcoB;;;AwKVtB;EACC;EACA;EACA,W3K1BsB;E2K2BtB,kBxKZY;EwKaZ;;;AAGD;EACC;EACA;EACA;EACA;EACA,W3KlCqB;;;A2KqCtB;EACC;;;AAGD;EACC;;;AAID;AAAA;AAAA;EAGC;EACA;EACA,W3KnDqB;E2KoDrB;EACA;EACA;EACA;EACA;;;AAQA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACC,OxKzEc;EwK0Ed,W3K9DoB;;;A2KkEtB;EACC;EACA,OxKvCqB;;;AwK0CtB;EACC;;;AAED;EACC;EACA;;;AAGD;EACC;EACA;EACA,OxKrDqB;EwKsDrB;EACA,W3KxFqB;E2KyFrB;EACA;EACA;;;AAID;EACC;EACA;EACA;;;AAGD;EACC;EACA,OxKtEqB;;;AwK0EtB;EACC;;;AAGD;EACC;EACA;EACA;;;AAIA;EACC;;AAED;EACC;;;AAIF;EACC;EACA,kBxKxIe;EwKyIf;EACA,W3KlIqB;E2KmIrB;EACA;EACA;EACA;EACA,Q3KzHyB;E2K0HzB;EACA;;;AAIA;AAAA;AAAA;AAAA;AAAA;EAGC;;AAED;EACC;;;AAIF;EACC,kBxK9JoB;;;AwKkKrB;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC,OxKtKkB;EwKuKlB;EACA,W3KxKqB;E2KyKrB;EACA;EACA,kBxK1JiB;;;AwK8JlB;EACC;EACA;EACA;;;AAGD;EACC,cxK5LoB;;;AwK+LrB;EAEC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC,kBxKxLY;EwKyLZ;EACA;EACA;EACA;EACA;EACA,W3KzMsB;E2K0MtB;;;AAGD;EACC;;;AAMD;EACC;EACA;;;AAGD;EACC,kBxK/MY;EwKgNZ;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA,kBxK9NY;;;AwKiOb;EACC;;;AAED;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;AACA;AACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;;AAIF;EACC;EACA,YxKjQY;EwKkQZ;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;AAGE;EACC;EACA;EACA;EACA;EACA;;AAED;EACC,OxKpRkB;EwKqRlB;;AAKF;EACC;;AAIF;EACC;;;AAOF;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;;;AAGD;AACA;EACC,W3KzWqB;E2K0WrB;EACA,kBxK1ViB;EwK2VjB;EACA;;;AAGD;EACC;EACA,W3KhXsB;E2KiXtB;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC,OxK7Ye;EwK8Yf;EACA;;;AAGD;EACC,OxKnWqB;;;AwKsWtB;EACC,OxKjUqB;;;AwKoUtB;AACA;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC,kBxKrZY;EwKsZZ;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;AACA;EACC;EACA;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC,kBxKzbY;EwK0bZ;EACA;EACA;EACA;;;AAGD;EACC,kBxKjcY;;;AwKocb;EACC;EACA,W3K/cqB;E2KgdrB;;;AAGD;EACC;EACA,kBxK5cY;EwK6cZ;EACA;;;AAGD;AAEA;EACC;EACA;;;AAGD;EACC,cxK9ee;;;AwKifhB;EACC,cxK1ekB;;;AwK6enB;EACC,cxKpfoB;;;AwKufrB;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA,OxKleqB;;;AwKqetB;EACC,OxKteqB;;;AwKyetB;EACC;;;AAGD;EACC;;;AAGD;EACC,cnKlhB8B;EmKmhB9B,enKnhB8B;EmKohB9B;;AACA;EACC;;AAGA;EACC;EACA;EACA;EACA;EACA,kBxK5gBe;EwK6gBf;EACA;;AAIF;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;;AAIF;EACC;EACA;EACA;EACA;EACA,W3KvjBqB;E2KwjBrB;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA,kBxKxjBiB;EwKyjBjB;;;AAGD;EACC,kBxKvjBiB;;;AwK0jBlB;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AAID;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;AAAA;EAGC;;;AAGD;EACC;;;AAID;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EAEC;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;;AAIF;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;AAEA;EACC;EACA;;AAGD;EACC;EACA;;AACA;EAHD;IAIE;;;AAIF;EACC,W3K7rBoB;E2K8rBpB,YnK9rB6B;EmK+rB7B,enKxsB0B;;AmK2sB3B;EACC;;AAED;EACC,enK5sB2B;;AmK6sB3B;EACC;;AACA;EACC,W3K5sBmB;;A2K+sBrB;EACC;;AAED;EACC;EACA;EACA;EACA;EACA;;AAED;EACC;EACA,kBxKhtBU;;AwKotBZ;EACC;;;AAIF;AAAA;EAEC;;;AAGD;EACC,YnK3uB6B;;;AmK8uB9B;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAED;EACC;;;AAID;AACA;EACC;;AAEA;EACC;EACA;EACA;;AACA;EACC;EACA;;;AAKH;AAAA;AAAA;AAAA;AAAA;AAKA;EACC;;;AAGD;EAEC,kBxK7wBY;;AwK+wBZ;EACC,kBxK5wBgB;;AwK6wBhB;EACC,kBxK9wBe;EwK+wBf;EACA,OxKvwBa;EwKwwBb;EACA;;AAIF;EACC;;AAGD;EACC;;;AAIF;AAAA;EAEC;EACA,OxKlxBqB;EwKmxBrB;;AAEA;AAAA;EACC;;;AAIF;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;AACA;EACC;EACA;EACA;;;AAMA;EACC;EACA;;AAED;EACC;;;AhJ91BF;EACC;EACG;;AAEJ;EACC;EAEA,SARwB;EASxB;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;;AiJP1B;AAEA;EACC,W5KQsB;;;A4KLvB;EACC,W5KMqB;;;A4KHtB;EACC;;;ACaD;EACC;EACA;EACA;EAIA;;;AAGD;EACC;EACA;EACA;EACA,a7KDwB;;;A6KKxB;EACC;;;AAYF;E9KqBE,oB8KpBmB;E9KqBhB,iB8KrBgB;E9KsBX,Y8KtBW;EACpB;EACA;;;AAMA;AAAA;EAEC;;;AAIF;EACC;;;AAID;EACC;EACA;;;AAID;AAAA;EAEC;;;AAyBD;EACC;EACA;EACA,QpKvEqB;EoKwErB;EACA,W7KpGqB;E6KqGrB,a7KxFsB;E6KyFtB,O1K5Ee;E0K6Ef,kB1K1FY;E0K2FZ;EACA;EACA,e3KhHuB;EH+DtB,oB8KkDD;E9KjDS,Y8KiDT;E9KmEC,oB8KlED;E9KmEM,e8KnEN;E9KoES,Y8KpET;;AlJjFA;EACC,SAHwB;EAIxB;;A5B8DA;EACE,OI3DkB;EJ4DlB;;AAEF;EAA0B,OI9DN;;AJ+DpB;EAAgC,OI/DZ;;AJ0DpB;EACE,OIlEY;EJmEZ;;AAEF;EAA0B,OIrEZ;;AJsEd;EAAgC,OItElB;;A0K+Ff;EACC;EACA;;AAQD;EAGC,kB1KrHgB;E0KsHhB;;AAGD;EAEC,QAnJgB;;AAuJjB;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACI;;;AAWJ;EACC;;;AAaD;EAME;AAAA;AAAA;AAAA;IACC,apKpKyB;;;AoK+K5B;EACC,erK5M4B;;;AqKoN7B;AAAA;EAEC;EACA;EACA;EACA;;AAKC;AAAA;AAAA;EACC,QApOe;;AAwOjB;AAAA;EACC,Y7KpNwB;E6KqNxB;EACA;EACA,a7K7MuB;E6K8MvB;;;AAIF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;EACA;;AACA;EARF;AAAA;AAAA;AAAA;IASI;;;;AAGJ;AAAA;EAEE;;;AAGF;AAAA;EAEC;;;AAID;AAAA;EAEC;EACA;EACA;EACA;EACA;EACA,apK9O2B;EoK+O3B,a7KjPwB;E6KkPxB;;;AAGD;EACI,apKpPwB;;;AoKuP5B;AAAA;EAEC;EACA;;;AAOD;AAAA;EAEC;;AAEA;AAAA;AAAA;AAAA;EAGC,QAzSgB;;;AAiTjB;AAAA;AAAA;EAEC,QAnTgB;;;AA6ThB;AAAA;AAAA;EACC,QA9Te;;;AAyUlB;EACC;EACA,O1K1Se;E0K2Sf,W7KrUqB;E6KuUrB;EAEA;;AAEA;EACC;;AAGD;EACC,erKzU0B;;;AqK4V3B;EAGC;IACC;IACA;IACA;;EAID;IACC;IACA;IACA;;EAID;IACC;;EAGD;IACC;IACA;;EAEA;AAAA;AAAA;IAGC;;EAKF;IACC;;EAGD;IACC;IACA;;EAKD;AAAA;IAEC;IACA;IACA;IACA;;EAEA;AAAA;IACC;;EAIF;AAAA;IAEC;IACA;;EAID;IACC;;;;AAiBH;EACC,erKjb8B;EqKkb9B,Y1KzaY;;A0K2aZ;EACC;EACA,arKlcyB;EqKmczB,QrKncyB;EqKoczB,kB1K/aW;;A0KqbV;EACC,arKjc4B;;AqKqc9B;EACC,arKtc6B;;AqKyc9B;EACC;EACA;;AAQF;AAAA;AAAA;AAAA;EAIC;EACA;EACA;EACG;;AAKJ;AAAA;EAEC;;AAGD;EACC,O1K7cc;E0K8cd;EACA;;AAGD;EACC,YpK1dyB;EoK6dzB;EACA;;AACA;EAND;IAOG;IACA;IACA;;;AAEF;EACE;;AAGI;EACE,crKpgBiB;;AqK4b3B;AA2EC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAeD;EACC,SpKngBiC;;AoKqgBjC;EACC,QpKpgB+B;EoKqgB/B,YpKngBkC;;AoKsgBnC;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;;AAIF;AAAA;EAEC,O1K5gBe;E0K6gBf,kB1K1hBY;;;A0K6hBb;EACC,kB1KvhBmB;E0KwhBnB;EACA,erK/iB4B;EqKgjB5B,arK5iB+B;;AqK8iB/B;EACC,kB1K7hBkB;E0K8hBlB;;AAEA;EAJD;IAKE;;;AAIF;EACC;;;AAKD;EACC,gBrKrjByB;;;AqKyjB3B;EACC;EACA;;;AAGD;EACC;;;AAGD;AACA;AAAA;EAEC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC,O1K7lBkB;A0K8lBlB;;;AAGD;EACC;;;AAGD;AACA;AAAA;EAEC,O1K9kBe;E0K+kBf,kB1K5lBY;E0K6lBZ;;;AAGD;EACC;;AAEA;EACC;;;AAIF;EACC;EACA;;;AAGD;AAAA;EAEC;;;AASD;AAAA;AAAA;EAGC;EACA;;;AAGD;EACC;;;AAKD;AAAA;EAEC;EACA;;;AAGD;AAAA;EAEC;;;AAGD;EACC;;;AAID;EACC;EACA;;;AAGD;AACA;EACC;EACA;;AlJ9nBA;EACC,SAHwB;EAIxB;;;AkJioBF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA,c1K/rBiB;;;A0KksBlB;EACC;;;AAGD;AACA;EACC;;;AAGD;EACI;EACA,kB1KlsBS;E0KmsBT,a3KxtBa;E2KytBb,c3KztBa;E2K0tBb,e3K1tBa;E2K2tBb;EACA,SrKzsBuB;EqK0sBvB;EACA;EACA;;AAEA;EACI,kB1KnsBU;;;A2KpClB;AAEA;EACC,a9KoCwB;;;A8KjCzB;EACC;;;AAGD;EACC;EACA,W9KCqB;;;A8KEtB;AAAA;EAEC;EACA;;;AAGD;AAAA;AAAA;AAAA;EAIC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA,W9KxBqB;E8KyBrB,a9KCwB;E8KAxB;;;AAGD;EACC;;;AAGD;EACC;EACA;;AACA;EAHD;IAIE;;;;AAIF;EACC;EACA;;;AClDD;EACC,QtKoBgC;EsKnBhC,YtKqBmC;;;AsKhBnC;EACC;;AAGD;EACC;EACA;EACA,W/KLqB;;;AgLbvB;AAGC;EADD;IAEE;;;;AAMD;EACE;;AACD;EACC;;AALH;EASC,ahLQyB;EgLPzB;EACA;EACA;;AACA;EAbD;IAcM;IAEA;;EAEA;IACE;;EACA;IACE;;;;AAOV;EACC;;AACA;EACC;;;AAIF;EACC;;AACA;EACC;;;AAIF;EACC,QpIhDiB;EoIiDjB,e9KzCkC;;A8K2ClC;EACC;EACA,kB7K5BgB;;A6K+BjB;EACC;;AAVF;EAaC;EACA;;AACA;EAfD;IAgBE;;;;ACnEF;EACC;;;AAGD;EACC,kB9KoBY;E8KnBZ;;;ACPD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA,WlLbsB;EkLctB;EACA;;;AAGD;EACC;AACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;AACA;EACA,kB/KlBmB;;;A+KqBpB;EACC;EACA;;;ACxDD;AAEA;EACC,anLkCwB;;;AmL/BzB;EACC,anLgCwB;;;AmL7BzB;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAIA;EACC;;AAED;EACC;EACA;;;AAOC;EACC;EACA;EACA;EACA;;;ACvDJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;EAEE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAIA;EACE;EACA;;;AC/KJ;AAEA;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA,WrLHqB;EqLIrB;EACA;EACA;AACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;A1J/BA;AAAA;EACC;EACG;;AAEJ;AAAA;EACC;EAEA,SARwB;EASxB;;AAEA;AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;;AAEzB;EACC;EACG;;AAEJ;EACC;EAEA,SARwB;EASxB;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;;AAoEzB;EACC;EACA,QAJwB;EAKxB;;;AAUD;AAAA;AAAA;EACC;EACA;EACA;EACG;;AAOJ;AAAA;AAAA;EACC;EACA;EACA;EACG;;AA5BJ;AAAA;AAAA;EACC;EACA,QAJwB;EAKxB;;;A0JfF;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AC3ED;AAEA;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;ACnBD;AAEA;EACC;EACA;EACA;EACA;EACA;EACA,WvLGsB;;;AuLAvB;EACC;EACA;EACA;EACA;EACA;EACA,kBpLgBmB;;;AoLbpB;EACC;EACA;EACA;EACA;;;ACtBD;AAEA;EACC;;AACA;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;EACA;;AAED;EACC;;;AAIF;EACC;IACC;;;AAIF;EACC,kBrLlBkB;EqLmBlB;EACA;EACA,WxLvBsB;EwLwBtB;EACA;EACA;;AACA;EACC;EACA;;;AAIF;AAEA;EACC;EACA;EACA;;AACA;EACC;EACA;EACA;EACA;EACA;EACA,kBtLpD0B;EsLqD1B;EACA;;AACA;EATD;IAUE;;;AAGF;EACC;EACA;EACA;;AACA;EACC;EACA;;AACA;EAHD;IAIE;;;AAED;EACC;EACA;EACA;EACA;;AACA;EALD;IAME;IACA;;;AAIH;EArBD;IAsBE;;;AAED;EACC;EACA;;AAED;EACC;;AAED;EACC;EACA;EACA;EACA;EACA,YrLrEe;EqLsEf,Q5I/Fe;;A4IgGf;EAPD;IAQE;;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAED;EACC;;AAGF;EACC;EACA;EACA;EACA;EACA;EACA,WxL9GoB;EwL+GpB;EACA;EACA;EACA,kBrL1GsB;EqL2GtB;EACA;EACA;EACA;;AACA;EAfD;IAgBE;IACA;IACA;;;AAED;EACC,WxL7HmB;;AwL+HpB;EACC,WxLpImB;;AwLsIpB;EACC;EACA;;AAGF;EACC;;AACA;EAFD;IAGE;;;AAED;EACC;EACA;EACA;EACA;;AACA;EALD;IAME;IACA;IACA;IACA;;;AAGF;EACC;EACA;EACA;EACA;;AACA;EALD;IAME;IACA;IACA;IACA;;;AAKJ;EACC;EACA;;AAED;EACC,kBrL5JgB;;;AqLgKlB;AACC;AAAA;AAAA;;;AAKD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;;AAEA;EALD;IAME;;;;AAIF;EACC;EACA;;AAEA;EAJD;IAKE;IACA;;;;AAIF;EACC;;AACA;EAFD;IAGE;;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;AACA;EACC;EACA;EzL9DC,oByL+DD;EzL9DM,eyL8DN;EzL7DS,YyL6DT;EACA;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AClRD;AAEA;AACI;;;AAEJ;EACI;;;AAEJ;EACC,kBtLeY;EsLdZ;;;AAGD;EACC,kBtLUY;;;AsLPb;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;AACA;EACC;EACA;;;AAIF;EACC,WzLzBqB;EyL0BrB;;;AAGD;EACC,kBtLbiB;EsLcjB;EACA;;AAEC;EACC;EACA;;;AAMF;EACC;;;AAIF;EACC;;;AAGD;EACC,WzLhDqB;EyLiDrB,OtL3Be;EsL4Bf;;;AAGD;EACC,WzLxDsB;EyLyDtB;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAIA;EACC;;AAED;EACC;;;AAIF;EACC,ejLzF4B;;;AiL6F5B;EACC;EACA;;;AAIF;EACC,Y7I1GiB;;A6I2GjB;EACC;;;AAIF;EACC;;;ACtHD;AAcA;EACC;EACA;;;AAID;EACC,W1LVsB;E0LWtB;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC,exL7BgB;EwL8BhB,kBvLTY;;AuLUT;EACI;;;AAKR;EACI;;AAEI;EACI,kBvLpBC;;AuLuBD;EACI;;AAGA;EACI;;;ACtDpB;EACC;;;AAIA;EACC;EACA;EACA;EACA;;;AAIF;EACC;EACA;EACA;EACA;EACA;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;AACA;EACC;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA,kBxLjCW;;AwLmCX;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAED;EACC;EACA;EACA;;AAED;EACC;EACA;EACA;;AAED;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;AAEA;EACC;EACA;;AAGD;EACC;EACA;EACA;;AAGD;EACC;EACA,kBxLxIa;;AwL0Ib;EACC,W3L5JgB;E2L6JhB;EACA;;AAIF;EACC;EACA;;AAIA;EACC;EACA;EACA;;AAKD;EACC;EACA;;AAMA;EACC,W3L3LgB;;A2L+LlB;EACC;EACA,OxL9JgB;EwL+JhB,W3LhMgB;;A2LsMpB;EACC;EACA;EACA;;AAEA;EACC,W3L9MmB;E2L+MnB;EACA;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;EACA;EACA;;AAGD;EACC;;AAGD;EACC;EACA;;AAIF;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;AAKH;EACC;EACA;EACA;EACA;;;AAOH;EACC;;AAED;EACC;;;AAIF;EACC;EACA;EACA;EACA;EACA;;;AChTD;AAEA;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;AAEA;EACC;;;AAIF;EACG;EACA;EACA;;;AAGH;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AC7BD;EACC;;AACA;EACC,a7L+BuB;;;A6L1BxB;EACC;EACA;EACA,W7LGoB;E6LFpB;;AAED;EACC;;;AAIF;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA,O1LIe;E0LHf;EACA;EACA;EACA;EACA;E9L8BC,oB8L7BE;E9L8BM,Y8L9BN;EACH;;;AAGD;EACC,kB1LhBiB;E0LiBjB;EACA,W7LrCsB;;A6LsCtB;EACC;;AAED;EACC;EACA;;;AAKD;EACC,arLpD0B;;AqLsD3B;EACC;;AAED;EACC;;;AAIF;EACC;E9LDC,oB8LEE;E9LDM,Y8LCN;EACH,O1LlCe;;;A0LqChB;EACC,a7LtCwB;;;A6LyCzB;EACC;EACA,W7LvEsB;E6LwEtB,a7L9CwB;E6L+CxB;;;AAGD;EACC;;;AAGD;EACC;;AACA;EACC;;;AC9FF;AAEA;EACC,Y3L2BiB;E2L1BjB;EACA;EACA;;;AAGD;EACC;EACA;EACA,W9LKqB;E8LJrB;;;AAGD;EACC;EACA;EACA,W9LJsB;E8LKtB,O3L0BqB;E2LzBrB;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC,W9LrCsB;E8LsCtB;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC,kB3LjCiB;;;A2LoClB;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;AAEA;EACC;EACA;;;AAKF;EACC,c3LVqB;;;A2LatB;EACC,c3LhBqB;;;A2LmBtB;EACC;;AACA;EACC;EACA;;;AAKD;EACC;;AAED;EACC;;AACA;EACC;EACA;;;AAOH;EACC;;;AAGD;EACC,O3L7CqB;;;A2LgDtB;EACC,O3LnDqB;;;A2LsDtB;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;AACA;EACC;EACA;;;AAIF;EACC;;;AC9JD;AAEA;EACC;;;AAGD;AAEA;EACC;EACA;EACA,QvLP0B;EuLQ1B,kB5LaY;E4LZZ;EACA;;;AAGD;AAAA;AAAA;AAAA;EAIC;EACA;EACA;EACA,W/LdsB;E+LetB;EACA,kB5LDY;E4LEZ,a/LWwB;;;A+LRzB;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC,W/L/BqB;E+LgCrB,a/LNwB;E+LOxB;EACA;EACA,O5LTe;E4LUf;;;AChCD;EACI;;AAMA;EACI;EACA;EACA;;AAKA;AAAA;EACI;EACA;EACA;;;AAKZ;EACI,axLtCc;EwLuCd,gBxLvCc;EwLwCd,O7LzBe;E6L0Bf;;;AAGJ;EACI;;;AAKF;EACD;EACA;EACA;;AAMC;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE,SxL7Dc;EwL8Dd,ahMvCmB;EgMwCnB;EACA;;AAKH;EACE;EACA;;AAOD;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE;;AAKH;EACE;;AAIF;EACE,kB7LpEU;;;A6LgFX;AAAA;AAAA;AAAA;AAAA;AAAA;EAEE,SxLvF0B;;;AwLkG7B;EACE,kB7L9FU;;;A6LwGX;EACD;EACA;;AAEA;EAJC;IAKC;IACA;IACA;IACA;IACA;;EAGA;IACD;;EAOC;AAAA;AAAA;AAAA;AAAA;AAAA;IAEE;;;;AAWL;AAEA;EACC,ahMjIwB;EgMkIxB,kB7LzIiB;E6L0IjB;EACA,WhM9JsB;EgM+JtB;;;AAGD;EACC,ahMzIwB;EgM0IxB,exLvK2B;EwLwK3B;EACA,WhMtKsB;EgMuKtB;EAEA,ahMzJyB;;AgM4JxB;EACC;;AAED;EACC;;AAIF;EACC;;AAED;EAEC;;;AAIF;EACC,O7LlKe;E6LmKf,ahMrKwB;EgMsKxB,kB7L7KiB;E6L8KjB;;;AAGD;EACC;EACA;EACA,kB7LpLiB;;;A6LuLlB;EACC,ahM/KwB;EgMgLxB;EACA,WhM3MqB;EgM4MrB,O7LlLe;E6LmLf;;;AAGD;EACC;EACA;;AACA;EACC;;;AAIF;EACC,ahM/LwB;EgMgMxB,WhMxNsB;EgMyNtB;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA,kB7LrNiB;;;A6LwNlB;EACC,kB7L7NY;E6L8NZ;EACA,WhM5OqB;EgM6OrB;;;AAGD;EACC;;;AAGD;EACC,kB7LpOiB;E6LqOjB;EACA,WhMvPqB;;;AgM0PtB;EACC,O7LjOe;;;A6LoOhB;EACC;;;AAGD;EACC;;;AAID;EACC;EACA;;;AAGD;EACC,WhM/QsB;EgMgRtB;EACA;EACA;EACA,ahMzPwB;EgM0PxB;;AACA;EACC;EACA;EACA;;;AAIF;EACC,kB7L1QiB;E6L2QjB;EACA;;;AAGD;EACC;EACA,kB7LjRiB;E6LkRjB;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA,WhM7TsB;EgM8TtB;EACA;EACA;;;AAID;EACC,WhMrUsB;EgMsUtB,ahM1SwB;EgM2SxB;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC,ahMvTwB;;;AgM0TzB;EACC,ahM3TwB;EgM4TxB,kB7LnUiB;E6LoUjB,WhMrVqB;EgMsVrB;;;AAGD;AACA;EACC,kB7LvUmB;E6LwUnB,O7LlUe;E6LmUf;;;AAGD;EACC,kB7LpVY;E6LqVZ,O7LxUe;E6LyUf;;;AAGD;EACC;;;AAGD;EACC,kB7LpViB;E6LqVjB,O7LlVe;E6LmVf;;;AAGD;EACC,kB7L7VmB;E6L8VnB,O7LxVe;E6LyVf;EACA;;;AAGD;EACC,kB7L3WY;E6L4WZ,O7L/Ve;E6LgWf;EACA;;;AAGD;EACC,kB7LxWiB;E6LyWjB,O7LtWe;E6LuWf;EACA;;;AAMD;EACC;EACA,kB7L7XY;;;A6LgYb;EACC,kB7LjYY;;A6LqYV;EACC;;;AAMJ;EACI;;;AAKF;EACC;;;AAeH;EACC;IACC;IACA;;;AAIF;EACC;IACC;;;AClcF;AAEA;EACC,WjMOqB;EiMNrB;;;AAGA;EACC;;;AAGF;EACC,kB7L6Be;A6L5Bf;EACA;EACA;EACA;EACA;EACA;EACA,e/LTkC;;A+LclC;EACC;EACA;;AAED;EACC;EACA;;AAED;EACC;EACA;;AAED;EACC;EACA;;AAGD;EACC;EACA;;AAEA;EACC;;;AAKH;EACC;EACA;EACA;;;AAGD;EACC;;;AC9DC;EACE;;;ACAJ;AAEA;EACC;EACA;EACA;;;AAGD;EACC;EACA,WnMGqB;;;AmMAtB;EACC;EACA;EACA;;;AAGD;EACC;EACA,OhMhBe;;;AgMmBhB;EACC,kBhMWiB;;;AgMRlB;EACC,OhMiBqB;EgMhBrB,WnMnBsB;EmMoBtB;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;ACzCD;AAEA;EACC;EACA,kBjMwBiB;;;AiMflB;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA,kBjMFiB;;;AiMKlB;EACC,OjMGe;EiMFf;EACA;EACA;EACA;EACA,apMFwB;EoMGxB,WpM7BqB;;;AoMgCtB;EACC,WpMjCqB;;;AoMoCtB;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAID;EACC,kBjMjCiB;EiMkCjB;;;AAGD;EACC;EACA;EACA;;;AAID;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC,WpMhFsB;EoMiFtB;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAEC;EACA;ErM7CC,oBqM8CE;ErM7CM,YqM6CN;;;AAKJ;EACC;;;AAGD;AAAA;EAEC;EACA;EACA;EACA,OjMtFqB;EiMuFrB,elMtHkC;;;AkMyHnC;AAAA;EAEC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACI;;;AAGJ;EACC;;AAEA;EACC;;;AAIF;EACC;;;AAIA;EACC;;AAED;EACC,apMzKqB;EoM0KrB;;;ACzLF;AAEA;EACC;EACA;EACA;EACA;EACA;EACA,kBlMUkB;EkMTlB;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA,OlMqBe;EkMpBf;;;AAGD;EACC,OlMqBqB;EkMpBrB;;;AAGD;EACC;EACA;EACA;;;AAIA;EACC;EACA;;;ACpCF;AACA;EACC;EACA;;;AAGD;EACC;;;AAED;EACC,OnM6Be;;AmMzBd;EACC;;AAED;EACC;;;ACdH;EACC;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC,WvMHqB;EuMIrB;EACA;EACA;EACA,OpMamB;EoMZnB,kBpMhBe;;;AoMmBhB;EACC,kBpMlBoB;;;AqMDrB;AAEA;EACC;EACA;EACA;EACA,eAPiB;;AASjB;EACC;EAEA;;AAGA;AAAA;EAEC;EACA;EACA,WxMToB;EwMUpB,axMCoB;EwMApB;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAEC,qBrMMY;;AwBiDf;AAAA;EACC;EACA;EACA;EACG;;AAEF;AAAA;EACC;;AAIH;AAAA;EACC;EACA;EACA;EACG;;AAEF;AAAA;EACC;;AAjGH;AAAA;EACC;EACG;;AAEJ;AAAA;EACC;EAEA,SARwB;EASxB;;AAEA;AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;A6KmCvB;EAGC,OrMnBS;EqMoBT;EACA,kBrMRY;EqMSZ,qBrMTY;;;AqMehB;EACC;;;AAGD;EACC;;;AAGD;AACA;EACC;EACA;;AAEA;AAAA;EAEC,chM5D4B;;AgMgE7B;EACC;;AAEA;AAAA;EAEC;EACA;EACA;;AAEA;AAAA;EACC;EACA;;A7KIH;AAAA;EACC;EACA;EACA;EACG;;AAEF;AAAA;EACC;;AAIH;AAAA;EACC;EACA;EACA;EACG;;AAEF;AAAA;EACC;;AAjGH;AAAA;EACC;EACG;;AAEJ;AAAA;EACC;EAEA,SARwB;EASxB;;AAEA;AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;A6KoFvB;EAGC,OrMzFY;EqM0FZ;EACA;;;AAMJ;EACC,ehMzF6B;;;AiMN9B;AAyBA;EACC;EACA;EACA;;AxIrCC;EACE;EACA;EACA;;AwIqCH;EACC;EACA;;AAEA;EACC;EACA;EACA,SA/BgB;;AAiChB;EAEC;;AAKF;EACC,OtM1Be;;AsM4Bf;EAEC,OtM9Bc;EsM+Bd;EACA,QA3Cc;EA4Cd;;AAQF;EAGC,kBtMzCiB;EsM0CjB,ctMtEa;;AsM+Ef;ElM9EC;EACA;EACA;EACA,kBAJyB;;AkMsF1B;EACC;;;AAIF;EACC;EACA;;;AAOD;EAEC;ElG1GC,wBkG4G0B;ElG3G1B,yBkG2G0B;;;AAI5B;EACC;EACA,kBtMrFiB;EsMsFjB,ejMnG8B;;AiMoG9B;EACC,aA/FkB;EAgGlB;EACA,ejM7G2B;;AiM+G5B;EACC;EACA;EACA;;AAED;EACC;EACA;;AACA;EACC;;;AClIH;AAEA;EACC;EACA;EACA,kBvMwBiB;EuMvBjB,SlMU8B;;AkMT9B;EACC,YlMQ6B;;AkML9B;EACC,YlMI6B;;AkMH7B;EACC;;;AAKH;EACC;;;AAGD;EACC;EACA;EACA,a1MUwB;E0MTxB,W1MTwB;;;A0MYzB;EACC,YlMf8B;EkMgB9B,W1MpBqB;E0MqBrB,OvMYqB;;;AuMTtB;EACC;EACA;EACA,a1MHwB;E0MIxB,W1MxBqB;E0MyBrB,OvMHe;;;AuMMhB;EACC,kBvMpBY;;;AuMuBb;EACC;;;AClDD;EACC;;AhLQA;EACC;EACG;;AAEJ;EACC;EAEA,SARwB;EASxB;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EAEA,QAnBuB;EAqBvB,SAtBuB;;;A4CN1B;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AqIKA;AAAA;AAAA;AAMA;AACA;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;;;A3IpCC;EACE;EACA;EACA;;;A2IwCJ;AAAA;EC3CI;;;ADgDJ;AAAA;EC5CI;;;ADiDJ;EACC;;;AAGD;EACC;;;AAGD;AACA;EACC;;;AAGD;EACC;;;AAGD;AAAA;AAEA;EACC;;;AAED;AAEA;AACA;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;AACA;EAHD;IAIE;;;;AAIF;AACA;EACC;;;AAKA;EADD;IAEE;;;;AAKF;EACC;EACA;EACA;EACA,W5MrGqB;E4MsGrB;EACA;;;AAED;EACC,czM/DgB;;;AyMmEjB;EACI;EACA;;;AAIJ;EACI;;;AAIJ;EACC;EACA;EACA;;;AAID;EACI,W5MjIkB;E4MkIrB;;;AAKD;EACC;;;AAUD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkEA;EACC;EACA;;;AAID;EACC;EACA;EACA;EACA;E7MrKC,oB6MsKE;E7MrKM,Y6MqKN;EACH;EACA,kBzMhNiB;EyMiNjB;;AACA;EATD;IAUE;IACA;;;;AASC;EACC;;;ArIjPH;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AqI0OF;ArIvOC;;AACA;EACC;IACC;IACA;IACA;IACA,oBqIkO+B;IrIjO/B;;EAGD;IACC;;;AqI4NH;ArIxNC;;AACA;EACC;IACC;IACA;IACA;IACA,iBqImN+B;IrIlN/B;;EAGD;IACC;;;;AA5CF;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AqI8OF;ArI3OC;;AACA;EACC;IACC;IACA;IACA;IACA,oBqIsO+B;IrIrO/B;;EAGD;IACC;;;AqIgOH;ArI5NC;;AACA;EACC;IACC;IACA;IACA;IACA,iBqIuN+B;IrItN/B;;EAGD;IACC;;;;AA5CF;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AqIkPF;ArI/OC;;AACA;EACC;IACC;IACA;IACA;IACA,oBqI0O+B;IrIzO/B;;EAGD;IACC;;;AqIoOH;ArIhOC;;AACA;EACC;IACC;IACA;IACA;IACA,iBqI2N+B;IrI1N/B;;EAGD;IACC;;;;AA5CF;EACC;EACA;EACA;EACA,avEgBqB;;AuEbtB;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AqIsPF;ArInPC;;AACA;EACC;IACC;IACA;IACA;IACA,oBqI8O+B;IrI7O/B;;EAGD;IACC;;;AqIwOH;ArIpOC;;AACA;EACC;IACC;IACA;IACA;IACA,iBqI+N+B;IrI9N/B;;EAGD;IACC;;;;AqIgOH;ExKpQC;;;AwKwQD;ExKhRC;;;AwKoRD;ExKhRC;;;AwKoRD;EACC;;;AAGD;EACC;;;AAGD;AACA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACA;EACC;EACA,W5M3SsB;;;A4M8SvB;EACC;EACA,W5MlTqB;;;A4MqTtB;EACC;EACA,W5MzTsB;;;A4M4TvB;EACC;EACA,W5M5TqB;E4M6TrB;;;AAGD;EACC;EACA,W5MlUqB;E4MmUrB,OzMjUiB;;;AyMoUlB;EACC,a5M/SwB;E4MgTxB;EACA;;;AAGD;EACC;EACA,a5MpTwB;E4MqTxB,OzM7UiB;;;AyMgVlB;EACC;EACA,a5M1TwB;E4M2TxB;EACA,OzMpViB;;;AyMuVlB;EACC,OzMxViB;EyMyVjB,W5M3VqB;;;A4M8VtB;EACC;EACA,a5MxUwB;;;A4M4UzB;EACC;EACA,a5M9UwB;;;A4MiVzB;EACC;EACA,a5MnVwB;E4MoVxB,W5M5WqB;;;A4M+WtB;EACC,a5MtVwB;;;A4MyVzB;EACC,a5M1VwB;;;A4M6VzB;EACC,OzMvVqB","file":"delos.scss"} \ No newline at end of file From 13659d9c02da4c01adfcbd061d6868b75b733852 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Tue, 30 Jun 2026 19:24:11 +0200 Subject: [PATCH 107/108] Questions: Implement Copying --- .../Legacy/PageEditor/QstsQuestionPage.php | 67 +++++++++- .../PageEditor/class.ilPCAnswerForm.php | 108 ++++++++------- .../Capabilities/AsyncView/Capability.php | 9 ++ .../AnswerForm/Capabilities/Capability.php | 7 + .../Capabilities/DefaultView/Capability.php | 9 ++ .../Definitions/AdditionalFormStepAction.php | 2 +- .../Capability.php | 9 ++ .../Capabilities/RequiredCapabilities.php | 15 +++ .../SuggestedLearningContent/Capability.php | 18 +++ .../SuggestedLearningContent/Content.php | 16 ++- .../SuggestedLearningContent/Repository.php | 5 - .../Capabilities/TextFeedback/Capability.php | 23 ++++ .../Capabilities/TextFeedback/Repository.php | 4 - .../TextFeedback/SpecificTextFeedback.php | 10 +- .../TextFeedback/TextFeedback.php | 6 + .../Questions/src/AnswerForm/Definition.php | 3 +- .../Questions/src/AnswerForm/Properties.php | 3 +- .../src/AnswerForm/TypeGenericProperties.php | 3 +- .../Cloze/Capabilities/TextFeedback.php | 45 +++++++ .../src/AnswerFormTypes/Cloze/Definition.php | 2 +- .../Cloze/Properties/ClozeText/Text.php | 10 ++ .../Properties/Combinations/Combination.php | 24 +++- .../Properties/Combinations/Combinations.php | 24 +++- .../Properties/Combinations/MatchingValue.php | 16 ++- .../Gaps/AnswerOptions/AnswerOption.php | 18 ++- .../Gaps/AnswerOptions/AnswerOptions.php | 29 +++- .../Properties/Gaps/AnswerOptions/Factory.php | 4 + .../Cloze/Properties/Gaps/Gap.php | 24 +++- .../Cloze/Properties/Gaps/Gaps.php | 27 +++- .../Cloze/Properties/Gaps/Numeric.php | 2 +- .../Cloze/Properties/Gaps/Text.php | 2 +- .../Cloze/Properties/Properties.php | 51 ++++++- .../Cloze/Response/AnswerForm.php | 2 + .../Questions/src/Attempt/Repository.php | 4 +- .../ILIAS/Questions/src/Attempt/Response.php | 3 + .../Questions/src/Definitions/Clonable.php | 17 +-- .../Questions/src/Persistence/Manipulate.php | 6 - .../Questions/src/Persistence/Storable.php | 1 + .../Definitions/DefaultEnvironment.php | 6 +- .../Presentation/Layout/QuestionsTable.php | 6 + .../Questions/src/Presentation/Views/Edit.php | 37 +++++- .../Persistence/DatabaseStatementBuilder.php | 125 ++++++++++-------- .../src/Question/Persistence/Repository.php | 81 +++++++----- .../ILIAS/Questions/src/Question/Question.php | 69 +++++++++- 44 files changed, 746 insertions(+), 206 deletions(-) diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php index 198f8034dd1c..c8abce9710bb 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -24,16 +24,27 @@ use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Questions\Presentation\Views\Edit; use ILIAS\Questions\Question\Question; +use ILIAS\Data\UUID\Factory as UuidFactory; class QstsQuestionPage extends ilPageObject { + private UuidFactory $uuid_factory; + private readonly Edit $edit_view; - private readonly Question $question; + private Question $question; private ?Attempt $attempt_data = null; private ?Viewable $participant_view = null; private ?RequiredCapabilities $required_capabilites = null; private ?ViewConfiguration $view_configuration = null; + private static array $answer_form_mapping = []; + + #[\Override] + public function afterConstructor(): void + { + $this->uuid_factory = new UuidFactory(); + } + #[\Override] public function getParentType(): string { @@ -106,6 +117,60 @@ public function setViewConfiguration( $this->view_configuration = $view_configuration; } + #[\Override] + public function pasteContents( + string $page_content_id, + bool $a_self_ass = false + ): array|bool { + $answer_form_element = $this->page_manager + ->content($this->getDomDoc()) + ->getContentDomNode(...explode(':', $page_content_id)) + ?->getElementsByTagName(ilPCAnswerForm::ANSWER_FORM_ELEMENT_TAG); + + if ($answer_form_element?->length > 0) { + foreach ($answer_form_element->getIterator() as $node) { + self::$answer_form_mapping[$node->getAttribute(ilPCAnswerForm::ANSWER_FORM_ID_ATTRIBUTE)] + = $this->uuid_factory->uuid4AsString(); + } + } + + return parent::pasteContents($page_content_id, $a_self_ass); + } + + #[\Override] + public function cutContents( + array $page_content_ids + ): array|bool { + return parent::cutContents( + array_filter( + $page_content_ids, + fn(string $v): bool => $this->page_manager + ->content($this->getDomDoc()) + ->getContentDomNode(...explode(':', $v)) + ?->getElementsByTagName('AnswerForm') + ?->length < 1 + ) + ); + } + + /** + * 2026-06-30, sk:This is awfull, but as there are statics used in the copy + * process this is the only way I found to do this. If somebody has a better + * idea... + * + * @return array|null + */ + public static function getAnswerFormMapping(): array + { + return self::$answer_form_mapping; + } + + public static function setAnswerFormMapping( + array $answer_form_mapping + ): void { + self::$answer_form_mapping = $answer_form_mapping; + } + public function addQuestionText( string $text ): void { diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php index f057179a041a..70ed2a3181b1 100644 --- a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -29,11 +29,12 @@ use ILIAS\UICore\GlobalTemplate; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; +use ILIAS\Data\UUID\Factory as UuidFactory; class ilPCAnswerForm extends ilPageContent { - private const string ANSWER_FORM_ELEMENT_TAG = 'AnswerForm'; - private const string ANSWER_FORM_ID_ATTRIBUTE = 'Uuid'; + public const string ANSWER_FORM_ELEMENT_TAG = 'AnswerForm'; + public const string ANSWER_FORM_ID_ATTRIBUTE = 'Uuid'; private const string ANSWER_FORM_PLACEHOLDER = '\[\[\[ANSWER_FORM_([0-9a-f\-]+)\]\]\]'; public function init(): void @@ -92,74 +93,83 @@ public static function afterPageUpdate( global $DIC; $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + /** @var Repository $question_repository */ $question_repository = LocalDIC::dic()[Repository::class]; + $uuid_factory = new UuidFactory(); + /** @var \ILIAS\Questions\Question\Question $question */ $question = $page->getQuestion(); + $answer_form_mapping = \QstsQuestionPage::getAnswerFormMapping(); + \QstsQuestionPage::setAnswerFormMapping([]); + $answer_forms = []; foreach ($dom_util->path($domdoc, '//AnswerForm') as $node) { - $answer_forms[] = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); + $answer_form_id = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); + $answer_form = $question->getAnswerFormPropertiesByIdString($answer_form_id); + + if ($answer_form === null && in_array($answer_form_id, $answer_form_mapping)) { + $old_answer_form_id = array_search($answer_form_id, $answer_form_mapping); + $old_answer_form = $question->getAnswerFormPropertiesByIdString( + $old_answer_form_id + ); + + if ($old_answer_form === null) { + $old_answer_form_uuid = $uuid_factory->fromString($answer_form_id); + $old_answer_form = $question_repository->getQuestionForAnswerFormId( + $old_answer_form_uuid + )->getAnswerFormPropertiesByIdString( + $old_answer_form_uuid + ); + } + + $question = $question->withAnswerFormProperties( + $old_answer_form->clone( + $uuid_factory, + [ + 'new_question_id' => $question->getId(), + 'answer_form_id' => $uuid_factory->fromString( + $answer_form_id + ) + ] + ) + ); + } + + $answer_forms[] = $answer_form_id; } + $page->setQuestion($question->withoutDeletedAnswerForms($answer_forms)); + $question_repository->update( - [$question->withoutDeletedAnswerForms($answer_forms)] + [$page->getQuestion()] ); } #[\Override] public static function handleCopiedContent( - DOMDocument $a_domdoc, - bool $a_self_ass = true, - bool $a_clone_mobs = false, + DOMDocument $domdoc, + bool $self_ass = true, + bool $clone_mobs = false, int $new_parent_id = 0, int $obj_copy_id = 0 ): void { global $DIC; - $dom_util = $DIC->copage()->internal()->domain()->domUtil(); - // handle question elements - if ($a_self_ass) { - // copy questions - $path = "//Question"; - $nodes = $dom_util->path($a_domdoc, $path); - foreach ($nodes as $node) { - $qref = $node->getAttribute("QRef"); - - $inst_id = ilInternalLink::_extractInstOfTarget($qref); - $q_id = ilInternalLink::_extractObjIdOfTarget($qref); - - if (!($inst_id > 0)) { - if ($q_id > 0) { - $question = null; - try { - $question = assQuestion::instantiateQuestion($q_id); - } catch (Exception $e) { - } - // check due to #16557 - if (is_object($question) && $question->isComplete()) { - // check if page for question exists - // due to a bug in early 4.2.x version this is possible - if (!ilPageObject::_exists("qpl", $q_id)) { - $question->createPageObject(); - } - - // now copy this question and change reference to - // new question id - $duplicate_id = $question->duplicate(false); - $node->setAttribute("QRef", "il__qst_" . $duplicate_id); - } - } - } - } - } else { - // remove question - $path = "//Question"; - $nodes = $dom_util->path($a_domdoc, $path); - foreach ($nodes as $node) { - $parent = $node->parentNode; - $parent->parentNode->removeChild($parent); + $answer_form_mapping = \QstsQuestionPage::getAnswerFormMapping(); + if ($answer_form_mapping === []) { + return; + } + + foreach ($dom_util->path($domdoc, '//AnswerForm') as $node) { + $old_answer_form_id = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); + if (isset($answer_form_mapping[$old_answer_form_id])) { + $node->setAttribute( + self::ANSWER_FORM_ID_ATTRIBUTE, + $answer_form_mapping[$old_answer_form_id] + ); } } } diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php index ae2050ee4f42..d4ee1aacf344 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/AsyncView/Capability.php @@ -24,6 +24,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\ParticipantViewProvider; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Data\UUID\Factory as UuidFactory; class Capability implements CapabilityInterface, ParticipantViewProvider { @@ -53,6 +54,14 @@ public function getParticipantView( ->getCapability(self::getIdentifier()); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void { + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php index caee129d78d3..00c6a767a56b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -21,6 +21,7 @@ namespace ILIAS\Questions\AnswerForm\Capabilities; use ILIAS\Questions\AnswerForm\Properties; +use ILIAS\Data\UUID\Factory as UuidFactory; interface Capability { @@ -30,6 +31,12 @@ public function isAvailableFor( Properties $answer_form_properties ): bool; + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void; + public function onAnswerFormUpdate( Properties $answer_form_properties ): void; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php index ca62921d39a9..dee61978573d 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/DefaultView/Capability.php @@ -24,6 +24,7 @@ use ILIAS\Questions\AnswerForm\Capabilities\ParticipantViewProvider; use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\AnswerForm\Views\Participant; +use ILIAS\Data\UUID\Factory as UuidFactory; class Capability implements CapabilityInterface, ParticipantViewProvider { @@ -53,6 +54,14 @@ public function getParticipantView( ->getCapability(self::getIdentifier()); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void { + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalFormStepAction.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalFormStepAction.php index b0559d4be458..fe46ced4a7f1 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalFormStepAction.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Definitions/AdditionalFormStepAction.php @@ -160,7 +160,7 @@ private function buildForm( $environment->withSubActionParameter( self::SUB_ACTION_SAVE )->getUrlBuilder(), - $environment->getFormStartSubAction() === '' + $this->previous === null ? null : $environment->withSubActionParameter( self::SUB_ACTION_BACK diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php index 2191071322ea..308201d116f8 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/MarkingAllowingPartialPoints/Capability.php @@ -28,6 +28,7 @@ use ILIAS\Questions\AnswerForm\Properties; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Tools\InputsBuilderSession; +use ILIAS\Data\UUID\Factory as UuidFactory; class Capability implements CapabilityInterface, AdditionalStepProvider, MarkingProvider { @@ -75,6 +76,14 @@ public function getMarking( ->getCapability(self::getIdentifier()); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void { + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php index b94fc33bd98f..96cbe7b84e2f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/RequiredCapabilities.php @@ -31,6 +31,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\EditForm; use ILIAS\Questions\Presentation\Layout\Viewable; +use ILIAS\Data\UUID\Factory as UuidFactory; class RequiredCapabilities { @@ -116,6 +117,20 @@ public function isCapabilityRequired( return array_key_exists($identifier, $this->capabilities); } + public function onAnswerFormClone( + UuidFactory $uuid_factory, + AnswerFormProperties $old_properties, + AnswerFormProperties $new_properties + ): void { + foreach ($this->capabilities as $capability) { + $capability->onAnswerFormClone( + $uuid_factory, + $old_properties, + $new_properties + ); + } + } + public function onAnswerFormUpdate( AnswerFormProperties $properties ): void { diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php index 8c43577de892..3c6fd632099c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Capability.php @@ -28,6 +28,7 @@ use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\StaticURL\Services as StaticURLServices; class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider @@ -77,6 +78,23 @@ public function getFeedback( ); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void { + $suggested_learning_content = $this->repository->getFor( + $old_answer_form_properties->getAnswerFormId() + ); + $this->repository->store( + $suggested_learning_content->clone( + $uuid_factory, + ['answer_form_id' => $new_answer_form_properties->getAnswerFormId()] + ) + ); + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php index 60b40e44bdd0..805fc7ecf949 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Content.php @@ -20,8 +20,10 @@ namespace ILIAS\Questions\AnswerForm\Capabilities\SuggestedLearningContent; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\ResourceStorage\Identification\ResourceIdentification; @@ -30,7 +32,7 @@ use ILIAS\UI\Component\Link\Standard as StandardLink; use ILIAS\UI\Factory as UIFactory; -class Content +class Content implements Clonable { public const string KEY_TARGET_REF_ID = 'target_ref_id'; public const string KEY_SUB_OBJECT_ID = 'sub_object_id'; @@ -44,7 +46,7 @@ class Content public function __construct( private readonly IRSS $irss, - private readonly Uuid $answer_form_id, + private Uuid $answer_form_id, private Types $type, string $content ) { @@ -144,6 +146,16 @@ public function getListing( ]; } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_form_id = $environment['answer_form_id']; + return $clone; + } + public function toStorage( PersistenceFactory $persistence_factory ): array { diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php index 04d577786171..79571ff0d84c 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/SuggestedLearningContent/Repository.php @@ -22,12 +22,9 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; -use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; -use ILIAS\Data\Order as DataOrder; -use ILIAS\Data\Range as DataRange; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; @@ -97,7 +94,6 @@ public function store( ): void { (new Manipulate( $this->db, - ManipulationType::Replace, QuestionRepository::COMPONENT_NAMESPACE ))->withAdditionalStatement( $this->persistence_factory->replace( @@ -117,7 +113,6 @@ public function delete( ): void { (new Manipulate( $this->db, - ManipulationType::Delete, QuestionRepository::COMPONENT_NAMESPACE ))->withAdditionalStatement( $this->persistence_factory->delete( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php index 6a546cf24b14..ca02e412dae1 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Capability.php @@ -31,6 +31,7 @@ use ILIAS\Questions\Presentation\Layout\Async; use ILIAS\Questions\Presentation\Layout\Viewable; use ILIAS\Data\Text\Factory as TextFactory; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; class Capability implements CapabilityInterface, AdditionalTabProvider, FeedbackProvider, PageMigrationProvider @@ -94,6 +95,28 @@ public function runPageMigration(): void $this->repository->migratePageFeedback(); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): void { + $this->repository->store( + $new_answer_form_properties->getAnswerFormId(), + $this->repository->getFor( + $old_answer_form_properties->getAnswerFormId(), + $old_answer_form_properties + ->getTypeGenericProperties() + ->getDefinition() + ->getCapability(self::getIdentifier()) + )->onAnswerFormClone( + $uuid_factory, + $old_answer_form_properties, + $new_answer_form_properties + ) + ); + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php index 3404ddc3b7f9..50561481a4ff 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/Repository.php @@ -26,8 +26,6 @@ use ILIAS\Questions\Persistence\Query; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Question\Persistence\Repository as QuestionRepository; -use ILIAS\Data\Order as DataOrder; -use ILIAS\Data\Range as DataRange; use ILIAS\Data\Text\Factory as TextFactory; use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; @@ -117,7 +115,6 @@ public function store( $answer_form_id, new Manipulate( $this->db, - ManipulationType::Replace, QuestionRepository::COMPONENT_NAMESPACE ) )->run(); @@ -128,7 +125,6 @@ public function delete( ): void { (new Manipulate( $this->db, - ManipulationType::Delete, QuestionRepository::COMPONENT_NAMESPACE ))->withAdditionalStatement( $this->persistence_factory->delete( diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php index b8e63e6d23d7..dc3b12acb48f 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/SpecificTextFeedback.php @@ -32,7 +32,7 @@ public function __construct( private readonly Uuid $id, private readonly Uuid $answer_form_id, private readonly Uuid $parent_id, - private readonly string $condition, + private string $condition, private ?Markdown $feedback_text = null, private string $feedback_legacy = '' ) { @@ -53,6 +53,14 @@ public function getCondition(): string return $this->condition; } + public function withCondition( + string $condition + ): self { + $clone = clone $this; + $clone->condition = $condition; + return $clone; + } + public function getFeedbackText(): Markdown { return $this->feedback_text; diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php index a3977456fdd2..9d209fcc3c0b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/TextFeedback/TextFeedback.php @@ -71,6 +71,12 @@ abstract public function getSpecificFeedbackTable( Environment $environment ): ?OverviewTable; + abstract public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): static; + abstract public function onAnswerFormUpdate( Properties $answer_form_properties ): static; diff --git a/components/ILIAS/Questions/src/AnswerForm/Definition.php b/components/ILIAS/Questions/src/AnswerForm/Definition.php index 5723d3a90434..018863c4d914 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Definition.php +++ b/components/ILIAS/Questions/src/AnswerForm/Definition.php @@ -20,6 +20,7 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Questions\AnswerForm\Capabilities\TypeSpecification; use ILIAS\Questions\AnswerForm\Properties as AnswerFormProperties; use ILIAS\Questions\AnswerForm\Views\Edit; use ILIAS\Questions\Attempt\Attempt; @@ -57,7 +58,7 @@ public function hasCapability( public function getCapability( string $capability_identifier - ): mixed; + ): ?TypeSpecification; public function initializeAttemptData( Attempt $attempt, diff --git a/components/ILIAS/Questions/src/AnswerForm/Properties.php b/components/ILIAS/Questions/src/AnswerForm/Properties.php index ba13c12f8156..997c6ba3a79b 100644 --- a/components/ILIAS/Questions/src/AnswerForm/Properties.php +++ b/components/ILIAS/Questions/src/AnswerForm/Properties.php @@ -20,12 +20,13 @@ namespace ILIAS\Questions\AnswerForm; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Storable; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\EditOverview; use ILIAS\Data\UUID\Uuid; -interface Properties extends Storable +interface Properties extends Storable, Clonable { public function getAnswerFormId(): Uuid; diff --git a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php index 7c64920b5efe..a9ba9a17f8d9 100644 --- a/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php +++ b/components/ILIAS/Questions/src/AnswerForm/TypeGenericProperties.php @@ -88,6 +88,7 @@ public function getAdditionalTextLegacy(): string public function toStorage( PersistenceFactory $persistence_factory, AnswerFormGenericTableDefinitions $answer_form_generic_definitions, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate { if ($this->definition === null) { @@ -99,7 +100,7 @@ public function toStorage( $table_names_builder = $manipulate->getTableNameBuilder(null); return $manipulate->withAdditionalStatement( - $manipulate->getManipulationType() === ManipulationType::Create + $manipulation_type === ManipulationType::Create ? $this->buildInsertStatement( $persistence_factory, $answer_form_generic_definitions, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php index 4d35bdb89769..d87840761547 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Capabilities/TextFeedback.php @@ -69,10 +69,55 @@ public function getSpecificFeedbackTable( ); } + #[\Override] + public function onAnswerFormClone( + UuidFactory $uuid_factory, + Properties $old_answer_form_properties, + Properties $new_answer_form_properties + ): static { + /** @var Gaps $old_gaps */ + $old_gaps = $old_answer_form_properties->getGaps(); + /** @var Gaps $new_gaps */ + $new_gaps = $new_answer_form_properties->getGaps(); + + return array_reduce( + $this->getSpecificFeedbacks(), + function ( + TextFeedback $c, + SpecificTextFeedback $v + ) use ($uuid_factory, $old_gaps, $new_gaps): self { + $old_gap = $old_gaps->getGapById($v->getParentId()); + try { + $answer_option_position = $old_gap + ->getAnswerOptions() + ->getAnswerOptionById( + $uuid_factory->fromString($v->getCondition()) + )->getPosition(); + $new_answer_option_id = $new_gaps + ->getGapByPosition($old_gap->getPosition()) + ->getAnswerOptions() + ->getAnswerOptionForPositionOrNew($answer_option_position) + ->getAnswerOptionId(); + + return $c->withoutSpecificFeedback($v) + ->withSpecificFeedback( + $v->withCondition( + $new_answer_option_id->toString() + ) + ); + } catch (InvalidUuidStringException $e) { + return $c; + } + }, + clone $this + ); + } + #[\Override] public function onAnswerFormUpdate( Properties $answer_form_properties ): static { + /** @var Gaps $gaps */ $gaps = $answer_form_properties->getGaps(); return array_reduce( diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php index 018e1729da13..c9460bd3b1dd 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Definition.php @@ -122,7 +122,7 @@ public function hasCapability( #[\Override] public function getCapability( string $capability_identifier - ): mixed { + ): ?TypeSpecification { return $this->available_capabilities[$capability_identifier] ?? null; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php index efbf18568f6a..2bab1bf870a2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -153,6 +153,16 @@ function (Gaps $c, array $v) use ($answer_form_id, $pre_existing_gaps, &$positio ); } + public function withUpdateMarkdownAfterCloning( + \Closure $replace_gaps + ): self { + $clone = clone $this; + $clone->cloze_text = $this->text_factory->markdown( + $replace_gaps($clone->cloze_text->getRawRepresentation()) + ); + return $clone; + } + public function withIdsOfNewGapsInClozeText( array $new_gaps ): self { diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php index 831e580682fc..9c0d9656bfb9 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -24,11 +24,13 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\MatchingValue; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Language\Language; @@ -39,14 +41,14 @@ use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; -class Combination +class Combination implements Clonable { /** * @param Uuid $id * @param list $matching_values */ public function __construct( - private readonly Uuid $id, + private Uuid $id, private readonly ?float $available_points, private array $matching_values = [] ) { @@ -90,6 +92,24 @@ public function containsAnswerOptionsExactly( ) === []; } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $environment['combination_id'] = $uuid_factory->uuid4(); + $clone = clone $this; + $clone->id = $environment['combination_id']; + $clone->matching_values = array_map( + fn(MatchingValue $v): MatchingValue => $v->clone( + $uuid_factory, + $environment + ), + $clone->matching_values + ); + return $clone; + } + public function toStorage( Uuid $answer_form_id, PersistenceFactory $persistence_factory, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php index d5d03e943e6a..9eee90c60fbb 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -21,14 +21,16 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\TableNameBuilder; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Language\Language; use ILIAS\UI\Component\Table\DataRowBuilder; -class Combinations +class Combinations implements Clonable { private array $combinations; @@ -37,7 +39,7 @@ class Combinations public function __construct( private readonly Factory $combinations_factory, private readonly PersistenceFactory $persistence_factory, - private readonly Uuid $answer_form_id, + private Uuid $answer_form_id, private bool $enabled, array $combinations ) { @@ -105,6 +107,24 @@ public function getEditView(): Edit ); } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_form_id = $environment['answer_form_id']; + $clone->combinations = array_map( + fn(Combination $v): Combination => $v->clone( + $uuid_factory, + $environment + ), + $this->combinations + ); + return + $clone; + } + public function toStorage( Manipulate $manipulate, TableDefinitions $table_definitions, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php index a64b77b30d69..519c536430a2 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -24,15 +24,17 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOption; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Definitions\Range; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Language\Language; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; -class MatchingValue +class MatchingValue implements Clonable { public const string SEPARATOR_IDS = '/'; public const string SEPARATOR_IN_RANGE = '|'; @@ -79,6 +81,18 @@ public function buildPresentationString( return mb_substr($value, 0, 10) . '...'; } + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->combination_id = $environment['combination_id']; + $clone->gap = $environment['gaps']->getGapByPosition( + $this->gap->getPosition() + ); + return $clone; + } + public function toStorage( TableDefinitions $table_definitions, PersistenceFactory $persistence_factory, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php index d3f6d1c820ad..85de10e513cd 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -20,12 +20,14 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; -class AnswerOption +class AnswerOption implements Clonable { public const string FORM_KEY_ID = 'id'; public const string FORM_KEY_POSITION = 'position'; @@ -35,8 +37,8 @@ class AnswerOption public const string FORM_KEY_AVAILABLE_POINTS = 'points'; public function __construct( - private readonly Uuid $answer_option_id, - private readonly Uuid $answer_input_id, + private Uuid $answer_option_id, + private Uuid $answer_input_id, private int $position, private string $text_value = '', private ?float $lower_limit = null, @@ -138,6 +140,16 @@ public function toCarry(): array return $values; } + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_input_id = $environment['answer_input_id']; + $clone->answer_option_id = $uuid_factory->uuid4(); + return $clone; + } + public function buildReplace( PersistenceFactory $persistence_factory, ?Replace $replace, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php index 90d61cde1fe7..ea7be6a34b1e 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -21,18 +21,20 @@ namespace ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions; use ILIAS\Questions\AnswerForm\Persistence\AnswerFormSpecificTableTypes; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; use ILIAS\UI\Component\Input\Field\Factory as FieldFactory; -class AnswerOptions +class AnswerOptions implements Clonable { private const string KEY_IS_INCOMPLETE = 'is_incomplete'; private const string KEY_ANSWER_OPIONS = 'answer_options'; @@ -44,7 +46,7 @@ class AnswerOptions public function __construct( private readonly Factory $factory, - private readonly Uuid $answer_input_id, + private Uuid $answer_input_id, private array $answer_options ) { $this->answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); @@ -86,9 +88,13 @@ public function withIsIncomplete( public function getAnswerOptionById( Uuid $answer_option_id ): ?AnswerOption { + $answer_option_id_string = $answer_option_id->toString(); return array_find( $this->answer_options, - fn(AnswerOption $v): bool => $v->getAnswerOptionId()->toString() === $answer_option_id->toString() + function (AnswerOption $v) use ($answer_option_id_string): bool { + $id_string = $v->getAnswerOptionId()->toString(); + return $id_string === $answer_option_id_string; + } ); } @@ -284,6 +290,23 @@ function (array $c, AnswerOption $v) use ($ff, $build_label): array { ); } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_input_id = $environment['answer_input_id']; + $clone->answer_options = array_map( + fn(AnswerOption $v): AnswerOption => $v->clone( + $uuid_factory, + $environment + ), + $this->answer_options + ); + return $clone; + } + public function buildReplace( ?Replace $replace, TableDefinitions $table_definitions, diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php index 51072025880c..c87b3a82beac 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -91,6 +91,10 @@ public function fromDatabase( ), $query->getRefinery()->custom()->transformation( function (array $vs): array { + if ($vs === []) { + return []; + } + $previous_answer_input_id = null; $return_array = []; $answer_options = []; diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php index 20d8cdbde5b8..1cf35f5f059c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -26,11 +26,13 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\AnswerOptions\AnswerOptions; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Attempt\AdditionalAttemptData; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Definitions\TextMatchingOptions; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Replace; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\FileUpload\FileUpload; @@ -41,7 +43,7 @@ use ILIAS\UI\Component\Table\DataRow; use ILIAS\UI\Component\Table\DataRowBuilder; -class Gap +class Gap implements Clonable { public const string GAP_PLACEHOLDER_NAME = 'GAP'; @@ -58,8 +60,8 @@ class Gap * @param list $answer_options */ public function __construct( - private readonly Uuid $answer_input_id, - private readonly Uuid $answer_form_id, + private Uuid $answer_input_id, + private Uuid $answer_form_id, private int $position, private AnswerOptions $answer_options, private ?Type $type = null, @@ -372,6 +374,22 @@ public function getEditPointsSection( ); } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $environment['answer_input_id'] = $uuid_factory->uuid4(); + $clone = clone $this; + $clone->answer_input_id = $environment['answer_input_id']; + $clone->answer_form_id = $environment['answer_form_id']; + $clone->answer_options = $clone->answer_options->clone( + $uuid_factory, + $environment + ); + return $clone; + } + public function toTableRow( DataRowBuilder $row_builder, Language $lng diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php index 85faaaacebff..d6429956a733 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -27,6 +27,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Attempt\AdditionalAttemptData; use ILIAS\Questions\Attempt\Attempt; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Delete; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Junctor; @@ -50,7 +51,7 @@ use ILIAS\UI\Component\Table\DataRowBuilder; use ILIAS\UI\Factory as UIFactory; -class Gaps +class Gaps implements Clonable { /** * @var array @@ -110,6 +111,17 @@ public function getGapByTagName( return $this->gaps[$this->extractIdFromTagName($tag_name)] ?? null; } + public function getGapByPosition( + int $position + ): ?Gap { + foreach ($this->gaps as $gap) { + if ($gap->getPosition() === $position) { + return $gap; + } + } + return null; + } + public function getNumberOfGaps( ): int { return count($this->gaps); @@ -498,6 +510,19 @@ function ( ); } + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_form_id = $environment['answer_form_id']; + $clone->gaps = array_map( + fn(Gap $v): Gap => $v->clone($uuid_factory, $environment), + $this->gaps + ); + return $clone; + } + public function toTableRows( DataRowBuilder $row_builder, Language $lng diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php index ea66f9207a97..d00df6dc1a2b 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -44,7 +44,7 @@ class Numeric extends Type private const string KEY_LOWER_LIMIT = 'lower_limit'; private const string KEY_UPPER_LIMIT = 'upper_limit'; - private const string KEY_STEP_SIZE = 'upper_limit'; + private const string KEY_STEP_SIZE = 'step_size'; #[\Override] public function getIdentifier(): string diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php index af6ff686aa4a..aa79fc0a8b59 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -104,7 +104,7 @@ public function getEditAnswerOptionsInputs( )->withRequired(true) ->withValue($gap->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), 'max_chars' => $ff->numeric( - $environment->getLanguage()->txt('max_chars'), + $environment->getLanguage()->txt('max_characters'), )->withValue($gap->getMaxChars()) ]; } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php index e2cb6983bade..3813ed31d417 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -29,6 +29,7 @@ use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\ClozeText\Factory as ClozeTextFactory; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\Combinations; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Definitions\ScoringIdentical; +use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap; use ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gaps; use ILIAS\Questions\AnswerFormTypes\Cloze\TableDefinitions; use ILIAS\Questions\Persistence\Delete; @@ -40,6 +41,7 @@ use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Presentation\Definitions\Environment; use ILIAS\Questions\Presentation\Layout\EditOverview; +use ILIAS\Data\UUID\Factory as UuidFactory; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; use ILIAS\UI\Component\Input\Field\Section; @@ -58,12 +60,12 @@ class Properties implements PropertiesInterface * @param array $gaps */ public function __construct( - private readonly Uuid $answer_form_id, - private readonly Uuid $question_id, + private Uuid $answer_form_id, + private Uuid $question_id, private readonly Definition $definition, private readonly ?float $available_points, private Text $cloze_text, - private readonly string $legacy_cloze_text, + private string $legacy_cloze_text, private ScoringIdentical $scoring_identical, private Gaps $gaps, private Combinations $combinations @@ -297,9 +299,33 @@ public function withValuesFromCarry( return $clone; } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->answer_form_id = $environment['answer_form_id']; + $clone->question_id = $environment['new_question_id']; + $environment['gaps'] = $clone->gaps->clone($uuid_factory, $environment); + + $update_text_closure = $this->buildReplaceClonedGapsInTextsClosure( + $environment['gaps'] + ); + $clone->cloze_text = $clone->cloze_text->withUpdateMarkdownAfterCloning( + $update_text_closure + ); + $clone->legacy_cloze_text = $update_text_closure($this->legacy_cloze_text); + + $clone->gaps = $environment['gaps']; + $clone->combinations->clone($uuid_factory, $environment); + return $clone; + } + #[\Override] public function toStorage( PersistenceFactory $persistence_factory, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate { $table_definitions = $this->definition->getTableDefinitions(); @@ -308,7 +334,7 @@ public function toStorage( $table_definitions->getTableSubNameSpace() ); - $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create + $answer_form_statement = $manipulation_type === ManipulationType::Create ? $this->buildInsertAnswerFormStatement( $table_definitions, $persistence_factory, @@ -478,4 +504,21 @@ private function buildAvailablePointsForGenericProperties(): ?float return 1.1; } + + private function buildReplaceClonedGapsInTextsClosure( + Gaps $new_gaps + ): \Closure { + return function ($text) use ($new_gaps): string { + $position = 0; + return mb_ereg_replace_callback( + '{{' . Gap::GAP_PLACEHOLDER_NAME + . '_[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}}}', + function ($matches) use ($new_gaps, &$position): string { + return '{{' . Gap::GAP_PLACEHOLDER_NAME + . "_{$new_gaps->getGapByPosition($position++)->getAnswerInputId()->toString()}}}"; + }, + $text + ); + }; + } } diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php index f2907206978e..c7b90e807d3c 100644 --- a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Response/AnswerForm.php @@ -27,6 +27,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; @@ -87,6 +88,7 @@ public function toPreviewStorage(): array #[\Override] public function toStorage( PersistenceFactory $persistence_factory, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate { return $manipulate->withAdditionalStatement( diff --git a/components/ILIAS/Questions/src/Attempt/Repository.php b/components/ILIAS/Questions/src/Attempt/Repository.php index 52f770630d73..52333a04ec99 100644 --- a/components/ILIAS/Questions/src/Attempt/Repository.php +++ b/components/ILIAS/Questions/src/Attempt/Repository.php @@ -121,9 +121,9 @@ public function storeResponse( $this->persistence_factory, $this->table_definitions, $this->table_names_builder, + ManipulationType::Create, new Manipulate( $this->db, - ManipulationType::Create, QuestionRepository::COMPONENT_NAMESPACE ) )->run(); @@ -135,7 +135,6 @@ public function deleteResponsesFor( ): void { $manipulate = new Manipulate( $this->db, - ManipulationType::Delete, QuestionRepository::COMPONENT_NAMESPACE ); foreach ($this->getAllResponsesFor($attempt_id, $question) as $response) { @@ -193,7 +192,6 @@ private function storeAttempt( ): void { $manipulate = (new Manipulate( $this->db, - ManipulationType::Create, QuestionRepository::COMPONENT_NAMESPACE ))->withAdditionalStatement( $attempt->basicDataToStorage( diff --git a/components/ILIAS/Questions/src/Attempt/Response.php b/components/ILIAS/Questions/src/Attempt/Response.php index e759bd0542d6..2eb7acd5f895 100644 --- a/components/ILIAS/Questions/src/Attempt/Response.php +++ b/components/ILIAS/Questions/src/Attempt/Response.php @@ -23,6 +23,7 @@ use ILIAS\Questions\AnswerForm\Response as AnswerFormResponse; use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Data\UUID\Uuid; use ILIAS\Database\FieldDefinition; @@ -115,6 +116,7 @@ public function toStorage( PersistenceFactory $persistence_factory, TableDefinitions $table_definitions, TableNameBuilder $table_names_builder, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate { return array_reduce( @@ -122,6 +124,7 @@ public function toStorage( fn(Manipulate $c, AnswerFormResponse $v): Manipulate => $v->toStorage( $persistence_factory, + $manipulation_type, $c ), $manipulate->withAdditionalStatement( diff --git a/components/ILIAS/Questions/src/Definitions/Clonable.php b/components/ILIAS/Questions/src/Definitions/Clonable.php index b77e2ee1b97e..1fde6df69cdd 100644 --- a/components/ILIAS/Questions/src/Definitions/Clonable.php +++ b/components/ILIAS/Questions/src/Definitions/Clonable.php @@ -18,17 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Questions\Presentation\Definitions; +namespace ILIAS\Questions\Definitions; -use ILIAS\Questions\Presentation\Layout\Async; +use ILIAS\Data\UUID\Factory as UuidFactory; -interface Actor +interface Clonable { - public function can( - string $action - ): bool; - - public function do( - string $action - ): Async; + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static; } diff --git a/components/ILIAS/Questions/src/Persistence/Manipulate.php b/components/ILIAS/Questions/src/Persistence/Manipulate.php index dffad5d7fe79..82cee474c942 100644 --- a/components/ILIAS/Questions/src/Persistence/Manipulate.php +++ b/components/ILIAS/Questions/src/Persistence/Manipulate.php @@ -26,16 +26,10 @@ class Manipulate public function __construct( private readonly \ilDBInterface $db, - private readonly ManipulationType $type, private readonly string $component_name_space ) { } - public function getManipulationType(): ManipulationType - { - return $this->type; - } - public function getTableNameBuilder( ?TableSubNameSpace $table_sub_name_space ): TableNameBuilder { diff --git a/components/ILIAS/Questions/src/Persistence/Storable.php b/components/ILIAS/Questions/src/Persistence/Storable.php index 82e4478aed65..261687414e9d 100644 --- a/components/ILIAS/Questions/src/Persistence/Storable.php +++ b/components/ILIAS/Questions/src/Persistence/Storable.php @@ -24,6 +24,7 @@ interface Storable { public function toStorage( Factory $persistence_factory, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate; diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php index 4d7423cfd80c..d900232a24cf 100644 --- a/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php +++ b/components/ILIAS/Questions/src/Presentation/Definitions/DefaultEnvironment.php @@ -83,7 +83,7 @@ public function __construct( private readonly Factory $presentation_factory, private readonly Editability $editability, private readonly RequiredCapabilities $required_capabilities, - private readonly int $owner_obj_id, + private readonly int $parent_obj_id, URI $base_uri ) { $this->acquireURLBuilderAndParameters($base_uri); @@ -332,9 +332,9 @@ public function withIsInCreationContext( return $clone; } - public function getOwnerObjId(): int + public function getParentObjId(): int { - return $this->owner_obj_id; + return $this->parent_obj_id; } public function getAction(): string diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php index 799c17a44ddd..c3bfbec58915 100644 --- a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -172,6 +172,12 @@ private function buildFilter( private function getActions(): array { return [ + 'clone' => $this->environment->getUIFactory()->table()->action()->standard( + $this->environment->getLanguage()->txt('copy'), + $this->environment->withActionParameter(Edit::ACTION_CLONE_QUESTION) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ), 'delete' => $this->environment->getUIFactory()->table()->action()->standard( $this->environment->getLanguage()->txt('delete'), $this->environment->withActionParameter(Edit::ACTION_DELETE_QUESTIONS) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index e2c34a10e51f..8df4becdf418 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -61,6 +61,7 @@ class Edit { private const string ACTION_CREATE_QUESTION = 'create'; public const string ACTION_EDIT_QUESTION = 'edit'; + public const string ACTION_CLONE_QUESTION = 'clone'; public const string ACTION_DELETE_QUESTIONS = 'delete'; private const string ACTION_CREATE_ANSWER_FORM = 'create_af'; public const string ACTION_OTHER_ANSWER_FORM = 'other_af'; @@ -133,6 +134,7 @@ public function getUI( $environment->withIsInCreationContext(true) ), self::ACTION_EDIT_QUESTION => $this->editQuestion($environment), + self::ACTION_CLONE_QUESTION => $this->cloneQuestion($environment), self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), default => $this->showTable($environment) }; @@ -338,7 +340,7 @@ private function createQuestion( ); $create = $this->questions_repository->getNew( - $environment->getOwnerObjId() + $environment->getParentObjId() )->getEditView( $this->current_user, $this->ctrl, @@ -408,6 +410,37 @@ private function editQuestion( ); } + private function cloneQuestion( + DefaultEnvironment $environment + ): QuestionsTable { + $question_to_clone = $environment->getQuestionIds(); + + if (!isset($question_to_clone[0])) { + return $this->showTable($environment); + } + + $question = $this->questions_repository->getForQuestionId( + $question_to_clone[0] + ); + $this->questions_repository->create( + [ + $question->clone( + $this->uuid_factory, + [ + 'parent_obj_id' => $environment->getParentObjId(), + 'new_question_page_id' => $this->questions_repository + ->getNextAvailableQuestionPageId(), + 'required_capabilities' => $this->required_capabilities + ] + )->withParentObjId($environment->getParentObjId()) + ] + ); + + $this->ctrl->redirectToURL( + $environment->getUrlBuilder()->buildURI()->__toString() + ); + } + private function deleteQuestions( DefaultEnvironment $environment ): Async { @@ -609,7 +642,7 @@ private function createAnswerFormAndRedirect( Question $question, \ilPCAnswerForm $content_object ): never { - $this->questions_repository->create( + $this->questions_repository->update( [$question] ); diff --git a/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php b/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php index 76128f81e761..06143480bae1 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php +++ b/components/ILIAS/Questions/src/Question/Persistence/DatabaseStatementBuilder.php @@ -27,6 +27,7 @@ use ILIAS\Questions\Persistence\Factory as PersistenceFactory; use ILIAS\Questions\Persistence\Insert; use ILIAS\Questions\Persistence\Manipulate; +use ILIAS\Questions\Persistence\ManipulationType; use ILIAS\Questions\Persistence\TableNameBuilder; use ILIAS\Questions\Persistence\Update; use ILIAS\Database\FieldDefinition; @@ -51,46 +52,32 @@ public function addInsertStatementsToManipulation( string $remarks, ?Uuid $original_id, int $parent_obj_id, - ?int $position + ?int $position, + array $answer_forms ): Manipulate { - if ($this->created === null) { - $manipulate = $manipulate - ->withAdditionalStatement( - $this->buildInsertLinkingStatement( - $manipulate->getTableNameBuilder(null), - $question_id, - $parent_obj_id, - $position - ) - )->withAdditionalStatement( - $this->buildInsertQuestionStatement( - $manipulate->getTableNameBuilder(null), - $question_id, - $page_id, - $title, - $author, - $lifecycle, - $remarks, - $original_id - ) - ); - } - - if ($this->updated_answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $this->updated_answer_forms - ); - } - - if ($this->answer_forms !== []) { - return $this->addAnswerFormStatementsToManipulate( - $manipulate, - $this->answer_forms - ); - } - - return $manipulate; + return $this->addAnswerFormStatementsToManipulate( + $manipulate + ->withAdditionalStatement( + $this->buildInsertLinkingStatement( + $manipulate->getTableNameBuilder(null), + $question_id, + $parent_obj_id, + $position + ) + )->withAdditionalStatement( + $this->buildInsertQuestionStatement( + $manipulate->getTableNameBuilder(null), + $question_id, + $page_id, + $title, + $author, + $lifecycle, + $remarks, + $original_id + ) + ), + $answer_forms + ); } public function addUpdateStatementsToManipulation( @@ -107,6 +94,7 @@ public function addUpdateStatementsToManipulation( ?Uuid $original_id, int $parent_obj_id, ?int $position, + array $new_answer_forms, array $updated_answer_forms, array $deleted_answer_forms ): Manipulate { @@ -157,6 +145,7 @@ public function addUpdateStatementsToManipulation( return $this->addAnswerFormStatementsToManipulate( $manipulate, + $new_answer_forms, $updated_answer_forms ); } @@ -260,27 +249,49 @@ public function buildDeleteMigrationStatement( private function addAnswerFormStatementsToManipulate( Manipulate $manipulate, - array $answer_forms + array $new_answer_forms, + array $updated_answer_forms = [] ): Manipulate { + $manipulate_with_new_answer_form_statements = array_reduce( + $new_answer_forms, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => + $this->addAnswerFormStatementToManipulate( + $v, + ManipulationType::Create, + $c + ), + $manipulate + ); + return array_reduce( - $answer_forms, - function ( - Manipulate $c, - AnswerFormProperties $v - ): Manipulate { - $manipulate_with_generic_properties = $v->getTypeGenericProperties() - ->toStorage( - $this->persistence_factory, - $this->answer_form_generic_table_definitions, - $c - ); + $updated_answer_forms, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => + $this->addAnswerFormStatementToManipulate( + $v, + ManipulationType::Update, + $c + ), + $manipulate_with_new_answer_form_statements + ); + } - return $v->toStorage( - $this->persistence_factory, - $manipulate_with_generic_properties - ); - }, - $manipulate + private function addAnswerFormStatementToManipulate( + AnswerFormProperties $answer_form_properties, + ManipulationType $manipulation_type, + Manipulate $manipulate + ): Manipulate { + $manipulate_with_generic_properties = $answer_form_properties->getTypeGenericProperties() + ->toStorage( + $this->persistence_factory, + $this->answer_form_generic_table_definitions, + $manipulation_type, + $manipulate + ); + + return $answer_form_properties->toStorage( + $this->persistence_factory, + $manipulation_type, + $manipulate_with_generic_properties ); } diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 286a7d6713de..5a4410ec620f 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -207,13 +207,16 @@ public function create( ): void { $this->store( array_map( - fn(Question $v): Question => $v - ->withPageId($this->buildQuestionPage($v->getParentObjId())), + fn(Question $v): Question => $v->getPageId() === null + ? $v->withPageId( + $this->buildQuestionPage( + $v->getParentObjId() + ) + ) : $v, $questions ), - $this->buildManipulate( - ManipulationType::Create - ) + ManipulationType::Create, + $this->buildManipulate() ); } @@ -225,9 +228,8 @@ public function update( ): void { $this->store( $questions, - $this->buildManipulate( - ManipulationType::Update - ) + ManipulationType::Update, + $this->buildManipulate() ); } @@ -241,9 +243,7 @@ public function delete( $this->database_statement_builder, $c ), - $this->buildManipulate( - ManipulationType::Delete - ) + $this->buildManipulate() )->run(); foreach ($questions as $question) { @@ -251,6 +251,41 @@ public function delete( } } + public function getQuestionForAnswerFormId( + Uuid $answer_form_id + ): ?Question { + $question_id = $this->db->fetchObject( + $this->db->query( + "SELECT question_id FROM qsts_answer_forms" . PHP_EOL + . "WHERE id = {$this->db->quote($answer_form_id->toString(), FieldDefinition::T_TEXT)}" + ) + )?->question_id; + + if ($question_id === null) { + return null; + } + + return $this->getForQuestionId( + $this->uuid_factory->fromString($question_id) + ); + } + + public function getNextAvailableQuestionPageId(): int + { + + $last_id = $this->db->fetchObject( + $this->db->query( + 'SELECT MAX(page_id) AS last FROM page_object ' + . 'WHERE parent_type = "qsts"' + ) + )->last; + if ($last_id === null) { + return 1; + } + + return $last_id + 1; + } + /** * @param array<\ILIAS\Data\Uuid> $question_ids * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> @@ -397,12 +432,14 @@ private function getAnswerFormTypesForQuestionIds( */ private function store( array $questions, + ManipulationType $manipulation_type, Manipulate $manipulate ): void { array_reduce( $questions, fn(Manipulate $c, Question $v): Manipulate => $v->toStorage( $this->database_statement_builder, + $manipulation_type, $c ), $manipulate @@ -479,12 +516,10 @@ private function addFilterToQuery( return $question_query; } - private function buildManipulate( - ManipulationType $manipulation_type - ): Manipulate { + private function buildManipulate(): Manipulate + { return new Manipulate( $this->db, - $manipulation_type, self::COMPONENT_NAMESPACE ); } @@ -553,22 +588,6 @@ private function buildQuestionPage( return $page->getId(); } - private function getNextAvailableQuestionPageId(): int - { - - $last_id = $this->db->fetchObject( - $this->db->query( - 'SELECT MAX(page_id) AS last FROM page_object ' - . 'WHERE parent_type = "qsts"' - ) - )->last; - if ($last_id === null) { - return 1; - } - - return $last_id + 1; - } - private function migrateQuestionPage( Question $question ): Question { diff --git a/components/ILIAS/Questions/src/Question/Question.php b/components/ILIAS/Questions/src/Question/Question.php index c6d508ecd9b5..c590e0821808 100644 --- a/components/ILIAS/Questions/src/Question/Question.php +++ b/components/ILIAS/Questions/src/Question/Question.php @@ -29,6 +29,7 @@ use ILIAS\Questions\Attempt\Attempt; use ILIAS\Questions\Attempt\Repository as AttemptRepository; use ILIAS\Questions\Attempt\Response; +use ILIAS\Questions\Definitions\Clonable; use ILIAS\Questions\Persistence\Column; use ILIAS\Questions\Persistence\Manipulate; use ILIAS\Questions\Persistence\ManipulationType; @@ -51,22 +52,23 @@ use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; -class Question +class Question implements Clonable { private ?CreateModes $create_mode = null; private bool $linking_information_updated = false; private bool $self_updated = false; private bool $page_id_updated = false; + private array $new_answer_forms = []; private array $updated_answer_forms = []; private array $deleted_answer_forms = []; private array $answer_forms; /** - * @param array{string, \ILIAS\Questions\AnswerForm\Properties} $answer_forms + * @param array{string, AnswerFormProperties} $answer_forms */ public function __construct( - private readonly Uuid $id, + private Uuid $id, private int $parent_obj_id, private ?int $position = null, private ?int $page_id = null, @@ -211,6 +213,54 @@ public function getCreated(): ?\DateTimeImmutable return $this->created; } + #[\Override] + public function clone( + UuidFactory $uuid_factory, + array $environment = [] + ): static { + $clone = clone $this; + $clone->id = $uuid_factory->uuid4(); + $clone->parent_obj_id = $environment['parent_obj_id']; + $clone->page_id = $environment['new_question_page_id']; + $mapping = []; + $clone->answer_forms = array_map( + function ( + AnswerFormProperties $v + ) use ( + $uuid_factory, + $environment, + $clone, + &$mapping + ): AnswerFormProperties { + $old_answer_form_id = $v->getAnswerFormId()->toString(); + + $cloned_answer_form = $v->clone( + $uuid_factory, + [ + 'new_question_id' => $clone->id, + 'answer_form_id' => $uuid_factory->uuid4() + ] + ); + + $mapping[$old_answer_form_id] = $cloned_answer_form + ->getAnswerFormId()->toString(); + + $environment['required_capabilities']->onAnswerFormClone( + $uuid_factory, + $v, + $cloned_answer_form + ); + + return $cloned_answer_form; + }, + $this->answer_forms + ); + \QstsQuestionPage::setAnswerFormMapping($mapping); + $question_page = new \QstsQuestionPage($this->page_id); + $question_page->copy($clone->page_id); + return $clone; + } + /** * @todo skergomard, 2026-06-08: This we only need while the migrations exist, after * this a question MUST never change the page assigned to it after its creation! @@ -230,8 +280,12 @@ public function withAnswerFormProperties( AnswerFormProperties $answer_form ): self { $clone = clone $this; + if (isset($clone->answer_forms[$answer_form->getAnswerFormId()->toString()])) { + $clone->updated_answer_forms[] = $answer_form; + } else { + $clone->new_answer_forms[] = $answer_form; + } $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; - $clone->updated_answer_forms[] = $answer_form; return $clone; } @@ -398,9 +452,10 @@ public function toTableRow( public function toStorage( DatabaseStatementBuilder $database_statement_builder, + ManipulationType $manipulation_type, Manipulate $manipulate ): Manipulate { - return $manipulate->getManipulationType() === ManipulationType::Create + return $manipulation_type === ManipulationType::Create ? $database_statement_builder->addInsertStatementsToManipulation( $manipulate, $this->id, @@ -411,7 +466,8 @@ public function toStorage( $this->remarks, $this->original_id, $this->parent_obj_id, - $this->position + $this->position, + $this->answer_forms ) : $database_statement_builder->addUpdateStatementsToManipulation( $manipulate, $this->self_updated, @@ -426,6 +482,7 @@ public function toStorage( $this->original_id, $this->parent_obj_id, $this->position, + $this->new_answer_forms, $this->updated_answer_forms, $this->deleted_answer_forms ); From 1c2cd08a23bfbbc9a62c5f0a87d535d720ec8949 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 30 Jul 2026 13:27:24 +0200 Subject: [PATCH 108/108] Migration: Avoid Crash on Missing Old Question --- .../Questions/src/Presentation/Views/Edit.php | 2 + .../src/Question/Persistence/Repository.php | 52 ++++++++++++------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/components/ILIAS/Questions/src/Presentation/Views/Edit.php b/components/ILIAS/Questions/src/Presentation/Views/Edit.php index 8df4becdf418..d1de2a0fea59 100644 --- a/components/ILIAS/Questions/src/Presentation/Views/Edit.php +++ b/components/ILIAS/Questions/src/Presentation/Views/Edit.php @@ -479,6 +479,8 @@ private function deleteQuestions( private function showTable( DefaultEnvironment $environment ): QuestionsTable { + $this->questions_repository->migrateQuestionPages(); + return new QuestionsTable( $this->ui_services, $this->answer_form_factory, diff --git a/components/ILIAS/Questions/src/Question/Persistence/Repository.php b/components/ILIAS/Questions/src/Question/Persistence/Repository.php index 5a4410ec620f..ab7f8c41d34d 100644 --- a/components/ILIAS/Questions/src/Question/Persistence/Repository.php +++ b/components/ILIAS/Questions/src/Question/Persistence/Repository.php @@ -270,6 +270,35 @@ public function getQuestionForAnswerFormId( ); } + public function migrateQuestionPages(): void + { + $questions_table_name = $this->core_table_names_builder + ->getTableNameFor(TableTypes::Questions); + $migration_table_name = $this->core_table_names_builder + ->getTableNameFor(TableTypes::MigrationsTable); + + $query = $this->db->query( + "SELECT new_question_id, old_question_id, question_id" . PHP_EOL + . "FROM {$questions_table_name}" . PHP_EOL + . "INNER JOIN {$migration_table_name} ON id = new_question_id" . PHP_EOL + . 'LEFT OUTER JOIN qpl_questions ON old_question_id = question_id' . PHP_EOL + . "WHERE page_id = 0" + ); + while (($row = $this->db->fetchObject($query)) !== null) { + $question = $this->getForQuestionId( + $this->uuid_factory->fromString($row->new_question_id) + ); + if ($row->question_id === null) { + $this->delete([$question]); + continue; + } + $this->migrateQuestionPage( + $question, + $row->old_question_id + ); + } + } + public function getNextAvailableQuestionPageId(): int { @@ -331,7 +360,7 @@ private function retrieveQuestionFromQuery( $this->refinery->identity() ); - $question = $query->retrieveCurrentRecord( + return $query->retrieveCurrentRecord( $this->persistence_factory->table( $this->core_table_names_builder, TableTypes::Questions, @@ -355,12 +384,6 @@ private function retrieveQuestionFromQuery( ) ) ); - - if ($answer_forms === [] || $question->getPageId() !== 0) { - return $question; - } - - return $this->migrateQuestionPage($question); } /** @@ -589,20 +612,11 @@ private function buildQuestionPage( } private function migrateQuestionPage( - Question $question + Question $question, + int $old_question_id ): Question { - $table_name = $this->core_table_names_builder - ->getTableNameFor(TableTypes::MigrationsTable); - - $old_page_id = $this->db->fetchObject( - $this->db->query( - "SELECT old_question_id FROM {$table_name}" . PHP_EOL - . "WHERE new_question_id = {$this->db->quote($question->getId(), FieldDefinition::T_TEXT)}" - ) - )->old_question_id; - $new_page_id = $this->getNextAvailableQuestionPageId(); - $old_qsts_page = new \ilAssQuestionPage($old_page_id); + $old_qsts_page = new \ilAssQuestionPage($old_question_id); $old_qsts_page->setQuestion($question); $old_qsts_page->copyToAnswerForm($new_page_id, $question);