From 72bbd9c14667c9a7d30b57a5fa012296b8f45f05 Mon Sep 17 00:00:00 2001 From: Fred Neumann Date: Fri, 24 Feb 2023 02:41:19 +0100 Subject: [PATCH 1/4] new plugin slot (started) --- Modules/Test/classes/class.ilObjTest.php | 10 +- .../classes/class.assQuestion.php | 33 +- .../classes/class.assQuestionGUI.php | 13 +- .../classes/Core/ilQuestionTypeFactory.php | 18 + .../Question/classes/Core/ilQuestionTypes.php | 69 ++ .../PluginSlot/class.ilQuestionTypePlugin.php | 64 ++ .../classes/TA/class.assWrappedQuestion.php | 622 ++++++++++++++++++ .../TA/class.assWrappedQuestionGUI.php | 354 ++++++++++ .../TA/class.ilAssWrappedQuestionFeedback.php | 79 +++ Services/Question/service.xml | 9 + .../default/tpl.qtype_ta_output.html | 12 + .../default/tpl.qtype_ta_solution.html | 17 + 12 files changed, 1289 insertions(+), 11 deletions(-) create mode 100644 Services/Question/classes/Core/ilQuestionTypeFactory.php create mode 100644 Services/Question/classes/Core/ilQuestionTypes.php create mode 100644 Services/Question/classes/PluginSlot/class.ilQuestionTypePlugin.php create mode 100644 Services/Question/classes/TA/class.assWrappedQuestion.php create mode 100644 Services/Question/classes/TA/class.assWrappedQuestionGUI.php create mode 100644 Services/Question/classes/TA/class.ilAssWrappedQuestionFeedback.php create mode 100644 Services/Question/service.xml create mode 100644 Services/Question/templates/default/tpl.qtype_ta_output.html create mode 100644 Services/Question/templates/default/tpl.qtype_ta_solution.html diff --git a/Modules/Test/classes/class.ilObjTest.php b/Modules/Test/classes/class.ilObjTest.php index 0f504f819d3d..4f83f15b386e 100755 --- a/Modules/Test/classes/class.ilObjTest.php +++ b/Modules/Test/classes/class.ilObjTest.php @@ -4669,8 +4669,14 @@ public function createQuestionGUI($question_type, $question_id = -1): ?assQuesti assQuestion::_includeClass($question_type, 1); - $question_type_gui = $question_type . 'GUI'; - $question = new $question_type_gui(); + if (ilQuestionTypes::instance()->hasFactory($question_type)) { + $question = new assWrappedQuestionGUI(); + $question->init(ilQuestionTypes::instance()->getFactory($question_type)); + } + else { + $question_type_gui = $question_type . 'GUI'; + $question = new $question_type_gui(); + } if ($question_id > 0) { $question->object->loadFromDb($question_id); diff --git a/Modules/TestQuestionPool/classes/class.assQuestion.php b/Modules/TestQuestionPool/classes/class.assQuestion.php index 3fef2d3cd3d8..9f9c16cedee6 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestion.php +++ b/Modules/TestQuestionPool/classes/class.assQuestion.php @@ -2617,9 +2617,17 @@ public static function instantiateQuestion(int $question_id): assQuestion throw new InvalidArgumentException('No question with ID ' . $question_id . ' exists'); } - assQuestion::_includeClass($question_type); - $question = new $question_type(); - $question->loadFromDb($question_id); + if (ilQuestionTypes::instance()->hasFactory($question_type)) { + $question = new assWrappedQuestion(); + $question->init(ilQuestionTypes::instance()->getFactory($question_type)); + $question->loadFromDb($question_id); + } + else { + assQuestion::_includeClass($question_type); + $question = new $question_type(); + $question->loadFromDb($question_id); + } + $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type); $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng); @@ -3171,6 +3179,9 @@ public static function _includeClass(string $question_type, int $gui = 0): void public static function getFeedbackClassNameByQuestionType(string $questionType): string { + if (ilQuestionTypes::instance()->hasFactory($questionType)) { + return 'ilAssWrappedQuestionFeedback'; + } return str_replace('ass', 'ilAss', $questionType) . 'Feedback'; } @@ -3203,7 +3214,8 @@ public static function _getQuestionTypeName($type_tag): string return $pl->getQuestionTypeTranslation(); } } - return ""; + + return (string) ilQuestionTypes::instance()->getTypeTranslation((string) $type_tag); } /** @@ -3230,9 +3242,16 @@ public static function instantiateQuestionGUI(int $a_question_id): assQuestionGU assQuestion::_includeClass($question_type, 1); - $question_type_gui = $question_type . 'GUI'; - $question_gui = new $question_type_gui(); - $question_gui->object->loadFromDb($a_question_id); + if (ilQuestionTypes::instance()->hasFactory($question_type)) { + $question_gui = new assWrappedQuestionGUI(); + $question_gui->init(ilQuestionTypes::instance()->getFactory($question_type)); + $question_gui->object->loadFromDb($a_question_id); + } + else { + $question_type_gui = $question_type . 'GUI'; + $question_gui = new $question_type_gui(); + $question_gui->object->loadFromDb($a_question_id); + } $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type); $question_gui->object->feedbackOBJ = new $feedbackObjectClassname($question_gui->object, $ilCtrl, $ilDB, $lng); diff --git a/Modules/TestQuestionPool/classes/class.assQuestionGUI.php b/Modules/TestQuestionPool/classes/class.assQuestionGUI.php index a24a55b8ca5c..dc67e52973f4 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestionGUI.php +++ b/Modules/TestQuestionPool/classes/class.assQuestionGUI.php @@ -434,8 +434,14 @@ public static function _getQuestionGUI(string $question_type = '', int $question assQuestion::_includeClass($question_type, 1); - $question_type_gui = $question_type . 'GUI'; - $question = new $question_type_gui(); + if (ilQuestionTypes::instance()->hasFactory($question_type)) { + $question = new assWrappedQuestionGUI(); + $question->init(ilQuestionTypes::instance()->getFactory($question_type)); + } + else { + $question_type_gui = $question_type . 'GUI'; + $question = new $question_type_gui(); + } $feedbackObjectClassname = assQuestion::getFeedbackClassNameByQuestionType($question_type); $question->object->feedbackOBJ = new $feedbackObjectClassname($question->object, $ilCtrl, $ilDB, $lng); @@ -462,6 +468,9 @@ public static function _getGUIClassNameForId($a_q_id): string */ public static function _getClassNameForQType($q_type): string { + if (ilQuestionTypes::instance()->hasFactory($q_type)) { + return 'assWrappedQuestionGUI'; + } return $q_type . "GUI"; } diff --git a/Services/Question/classes/Core/ilQuestionTypeFactory.php b/Services/Question/classes/Core/ilQuestionTypeFactory.php new file mode 100644 index 000000000000..f031e70d70d2 --- /dev/null +++ b/Services/Question/classes/Core/ilQuestionTypeFactory.php @@ -0,0 +1,18 @@ +dic = $dic; + $this->component_factory = $dic['component.factory']; + + /** @var ilQuestionTypePlugin $pl */ + foreach ($this->component_factory->getActivePluginsInSlot("qtype") as $pl) { + $factory = $pl->factory(); + $this->factories[$factory->getTypeTag()] = $pl->factory(); + } + } + + /** + * Check if it is a new question type by the existence of a factory + */ + public function hasFactory(string $type_tag) + { + return $this->factories[$type_tag]; + } + + /** + * Get the factory of a question type + */ + public function getFactory($type_tag) : ?ilQuestionTypeFactory + { + return $this->factories[$type_tag] ?? null; + } + + /** + * Get a translated title of a question type that can be used in lists + */ + public function getTypeTranslation(string $type_tag) :?string + { + return $this->getFactory($type_tag)?->getTypeTranslation(); + } + +} \ No newline at end of file diff --git a/Services/Question/classes/PluginSlot/class.ilQuestionTypePlugin.php b/Services/Question/classes/PluginSlot/class.ilQuestionTypePlugin.php new file mode 100644 index 000000000000..bcb9f53f50d8 --- /dev/null +++ b/Services/Question/classes/PluginSlot/class.ilQuestionTypePlugin.php @@ -0,0 +1,64 @@ +addQuestionType(); + } + + /** + * @todo: migrate to a general question type repository + */ + private function addQuestionType() + { + $query = "SELECT * FROM qpl_qst_type WHERE type_tag =" . $this->db->quote($this->factory()->getTypeTag(), 'text'); + + if (empty($row = $this->db->fetchAssoc($this->db->query($query)))) { + + $query2 = "SELECT MAX(question_type_id) maxid FROM qpl_qst_type"; + if ($row = $this->db->fetchAssoc($this->db->query($query2))) { + $max = (int) $row["maxid"] + 1; + } + else { + $max = 1; + } + + $this->db->insert('qpl_qst_type', [ + 'question_type_id' => ['integer', $max], + 'type_tag' => ['string', $this->factory()->getTypeTag()], + 'plugin' => ['integer', 1] + ]); + } + } +} diff --git a/Services/Question/classes/TA/class.assWrappedQuestion.php b/Services/Question/classes/TA/class.assWrappedQuestion.php new file mode 100644 index 000000000000..7b68908abcda --- /dev/null +++ b/Services/Question/classes/TA/class.assWrappedQuestion.php @@ -0,0 +1,622 @@ +factory = $factory; + return $this; + } + + /** + * Make the question types factory available for the feedback object (question is injected there) + */ + public function getTypeFactory() : ilQuestionTypeFactory + { + return $this->factory; + } + + + /** + * Returns the question type of the question + * + * @return string The question type of the question + */ + public function getQuestionType() : string + { + return $this->factory->getTypeTag(); + } + + /** + * Returns the names of the additional question data tables + * + * All tables must have a 'question_fi' column. + * Data from these tables will be deleted if a question is deleted + * + * @return mixed the name(s) of the additional tables (array or string) + */ + public function getAdditionalTableName() + { + return ''; + } + + /** + * Collects all texts in the question which could contain media objects + * which were created with the Rich Text Editor + */ + protected function getRTETextWithMediaObjects(): string + { + $text = parent::getRTETextWithMediaObjects(); + + // eventually add the content of question type specific text fields + // .. + + return (string) $text; + } + + /** + * Returns true, if the question is complete + * + * @return boolean True, if the question is complete for use, otherwise false + */ + public function isComplete(): bool + { + // Please add here your own check for question completeness + // The parent function will always return false + if(!empty($this->title) && !empty($this->author) && !empty($this->question) && $this->getMaximumPoints() > 0) + { + return true; + } + else + { + return false; + } + } + + /** + * Saves a question object to a database + * + * @param string $original_id + * @access public + * @see assQuestion::saveToDb() + */ + function saveToDb($original_id = ''): void + { + + // save the basic data (implemented in parent) + // a new question is created if the id is -1 + // afterwards the new id is set + if ($original_id == '') { + $this->saveQuestionDataToDb(); + } else { + $this->saveQuestionDataToDb($original_id); + } + + // Now you can save additional data + // ... + + // save stuff like suggested solutions + // update the question time stamp and completion status + parent::saveToDb(); + } + + /** + * Loads a question object from a database + * This has to be done here (assQuestion does not load the basic data)! + * + * @param integer $question_id A unique key which defines the question in the database + * @see assQuestion::loadFromDb() + */ + public function loadFromDb(int $question_id): void + { + global $DIC; + $ilDB = $DIC->database(); + + // load the basic question data + $result = $ilDB->query("SELECT qpl_questions.* FROM qpl_questions WHERE question_id = " + . $ilDB->quote($question_id, 'integer')); + + if ($result->numRows() > 0) { + $data = $ilDB->fetchAssoc($result); + $this->setId($question_id); + $this->setObjId($data['obj_fi']); + $this->setOriginalId($data['original_id']); + $this->setOwner($data['owner']); + $this->setTitle((string) $data['title']); + $this->setAuthor($data['author']); + $this->setPoints($data['points']); + $this->setComment((string) $data['description']); + + $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data['question_text'], 1)); + $this->setEstimatedWorkingTime(substr($data['working_time'], 0, 2), substr($data['working_time'], 3, 2), substr($data['working_time'], 6, 2)); + + // now you can load additional data + // ... + + try + { + $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']); + } + catch(ilTestQuestionPoolException $e) + { + } + } + + // loads additional stuff like suggested solutions + parent::loadFromDb($question_id); + } + + + /** + * Duplicates a question + * This is used for copying a question to a test + * + * @param bool $for_test + * @param string $title + * @param string $author + * @param string $owner + * @param integer|null $testObjId + * + * @return void|integer Id of the clone or nothing. + */ + public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int + { + if ($this->getId() <= 0) + { + // The question has not been saved. It cannot be duplicated + return 0; + } + + // make a real clone to keep the actual object unchanged + $clone = clone $this; + + $original_id = assQuestion::_getOriginalId($this->getId()); + $clone->setId(-1); + + if( (int) $testObjId > 0 ) + { + $clone->setObjId($testObjId); + } + + if (!empty($title)) + { + $clone->setTitle($title); + } + if (!empty($author)) + { + $clone->setAuthor($author); + } + if (!empty($owner)) + { + $clone->setOwner($owner); + } + + if ($for_test) + { + $clone->saveToDb($original_id); + } + else + { + $clone->saveToDb(); + } + + // copy question page content + $clone->copyPageOfQuestion($this->getId()); + // copy XHTML media objects + $clone->copyXHTMLMediaObjectsOfQuestion($this->getId()); + + // call the event handler for duplication + $clone->onDuplicate($this->getObjId(), $this->getId(), $clone->getObjId(), $clone->getId()); + + return $clone->getId(); + } + + /** + * Copies a question + * This is used when a question is copied on a question pool + * + * @param integer $target_questionpool_id + * @param string $title + * + * @return void|integer Id of the clone or nothing. + */ + function copyObject($target_questionpool_id, $title = '') + { + if ($this->getId() <= 0) + { + // The question has not been saved. It cannot be duplicated + return; + } + + // make a real clone to keep the object unchanged + $clone = clone $this; + + $original_id = assQuestion::_getOriginalId($this->getId()); + $source_questionpool_id = $this->getObjId(); + $clone->setId(-1); + $clone->setObjId($target_questionpool_id); + if (!empty($title)) + { + $clone->setTitle($title); + } + + // save the clone data + $clone->saveToDb(); + + // copy question page content + $clone->copyPageOfQuestion($original_id); + // copy XHTML media objects + $clone->copyXHTMLMediaObjectsOfQuestion($original_id); + + // call the event handler for copy + $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId()); + + return $clone->getId(); + } + + /** + * Create a new original question in a question pool for a test question + * @param int $targetParentId id of the target question pool + * @param string $targetQuestionTitle + * @return int|void + */ + public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = '') + { + if ($this->id <= 0) + { + // The question has not been saved. It cannot be duplicated + return; + } + + $sourceQuestionId = $this->id; + $sourceParentId = $this->getObjId(); + + // make a real clone to keep the object unchanged + $clone = clone $this; + $clone->setId(-1); + + $clone->setObjId($targetParentId); + + if (!empty($targetQuestionTitle)) + { + $clone->setTitle($targetQuestionTitle); + } + + $clone->saveToDb(); + // copy question page content + $clone->copyPageOfQuestion($sourceQuestionId); + // copy XHTML media objects + $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId); + + $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId()); + + return $clone->getId(); + } + + /** + * Synchronize a question with its original + * You need to extend this function if a question has additional data that needs to be synchronized + * + * @access public + */ + function syncWithOriginal(): void + { + parent::syncWithOriginal(); + } + + + /** + * Get a submitted solution array from $_POST + * + * In general this may return any type that can be stored in a php session + * The return value is used by: + * savePreviewData() + * saveWorkingData() + * calculateReachedPointsForSolution() + * + * @return array ('value1' => string|null, 'value2' => float|null) + */ + protected function getSolutionSubmit() + { + $value1 = trim(ilUtil::stripSlashes($_POST['question'.$this->getId().'value1'])); + $value2 = trim(ilUtil::stripSlashes($_POST['question'.$this->getId().'value2'])); + + return array( + 'value1' => empty($value1)? null : (string) $value1, + 'value2' => empty($value2)? null : (float) $value2 + ); + } + + /** + * Get a stored solution for a user and test pass + * This is a wrapper to provide the same structure as getSolutionSubmit() + * + * @param int $active_id active_id of hte user + * @param int $pass number of the test pass + * @param bool $authorized get the authorized solution + * + * @return array ('value1' => string|null, 'value2' => float|null) + */ + public function getSolutionStored($active_id, $pass, $authorized = null) + { + // This provides an array with records from tst_solution + // The example question should only store one record per answer + // Other question types may use multiple records with value1/value2 in a key/value style + if (isset($authorized)) + { + // this provides either the authorized or intermediate solution + $solutions = $this->getSolutionValues($active_id, $pass, $authorized); + } + else + { + // this provides the solution preferring the intermediate + // or the solution from the previous pass + $solutions = $this->getTestOutputSolutions($active_id, $pass); + } + + + if (empty($solutions)) + { + // no solution stored yet + $value1 = null; + $value2 = null; + } + else + { + // If the process locker isn't activated in the Test and Assessment administration + // then we may have multiple records due to race conditions + // In this case the last saved record wins + $solution = end($solutions); + + $value1 = $solution['value1']; + $value2 = $solution['value2']; + } + + return array( + 'value1' => empty($value1)? null : (string) $value1, + 'value2' => empty($value2)? null : (float) $value2 + ); + } + + + /** + * Calculate the reached points from a solution array + * + * @param array ('value1' => string, 'value2' => float) + * @return float reached points + */ + protected function calculateReachedPointsForSolution($solution) + { + // in our example we take the points entered by the student + // and adjust them to be in the allowed range + $points = (float) $solution['value2']; + if ($points <= 0 || $points > $this->getMaximumPoints()) + { + $points = 0; + } + + // return the raw points given to the answer + // these points will afterwards be adjusted by the scoring options of a test + return $points; + } + + + /** + * Returns the points, a learner has reached answering the question + * The points are calculated from the given answers. + * + * @param int $active_id + * @param integer $pass The Id of the test pass + * @param bool $authorizedSolution + * @param boolean $returndetails (deprecated !!) + * @return int + * + * @throws ilTestException + */ + public function calculateReachedPoints($active_id, $pass = NULL, $authorizedSolution = true, $returndetails = false) + { + if( $returndetails ) + { + throw new ilTestException('return details not implemented for '.__METHOD__); + } + + if(is_null($pass)) + { + $pass = $this->getSolutionMaxPass($active_id); + } + + // get the answers of the learner from the tst_solution table + // the data is saved by saveWorkingData() in this class + $solution = $this->getSolutionStored($active_id, $pass, $authorizedSolution); + + return $this->calculateReachedPointsForSolution($solution); + } + + + /** + * Saves the learners input of the question to the database. + * + * @param integer $active_id Active id of the user + * @param integer $pass Test pass + * @param boolean $authorized The solution is authorized + * + * @return boolean $status + */ + function saveWorkingData($active_id, $pass = NULL, $authorized = true): bool + { + if (is_null($pass)) + { + $pass = ilObjTest::_getPass($active_id); + } + + // get the submitted solution + $solution = $this->getSolutionSubmit(); + + $entered_values = 0; + + // save the submitted values avoiding race conditions + $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function() use (&$entered_values, $solution, $active_id, $pass, $authorized) { + + + $entered_values = isset($solution['value1']) || isset($solution['value2']); + + if ($authorized) + { + // a new authorized solution will delete the old one and the intermediate + $this->removeExistingSolutions($active_id, $pass); + } + elseif ($entered_values) + { + // an new intermediate solution will only delete a previous one + $this->removeIntermediateSolution($active_id, $pass); + } + + if ($entered_values) + { + $this->saveCurrentSolution($active_id, $pass, $solution['value1'], $solution['value2'], $authorized); + } + }); + + + // Log whether the user entered values + if (ilObjAssessmentFolder::_enabledAssessmentLogging()) + { + assQuestion::logAction($this->lng->txtlng( + 'assessment', + $entered_values ? 'log_user_entered_values' : 'log_user_not_entered_values', + ilObjAssessmentFolder::_getLogLanguage() + ), + $active_id, + $this->getId() + ); + } + + // submitted solution is valid + return true; + } + + + /** + * Reworks the allready saved working data if neccessary + * @param integer $active_id + * @param integer $pass + * @param boolean $obligationsAnswered + * @param boolean $authorized + */ + protected function reworkWorkingData($active_id, $pass, $obligationsAnswered, $authorized) + { + // normally nothing needs to be reworked + } + + + /** + * Creates an Excel worksheet for the detailed cumulated results of this question + * + * @param object $worksheet Reference to the parent excel worksheet + * @param int $startrow Startrow of the output in the excel worksheet + * @param int $active_id Active id of the participant + * @param int $pass Test pass + * + * @return int + */ + public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int + { + $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(0) . $startrow, $this->factory->getTypeTranslation()); + $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(1) . $startrow, $this->getTitle()); + + $solution = $this->getSolutionStored($active_id, $pass, true); + $value1 = isset($solution['value1']) ? $solution['value1'] : ''; + $value2 = isset($solution['value2']) ? $solution['value2'] : ''; + + $row = $startrow + 1; + + $worksheet->setCell($row, 0, $this->plugin->txt('label_value1')); + $worksheet->setBold($worksheet->getColumnCoord(0) . $row); + $worksheet->setCell($row, 1, $value1); + $row++; + + $worksheet->setCell($row, 0, $this->plugin->txt('label_value2')); + $worksheet->setBold($worksheet->getColumnCoord(0) . $row); + $worksheet->setCell($row, 1, $value2); + $row++; + + return $row + 1; + } + + /** + * Creates a question from a QTI file + * + * Receives parameters from a QTI parser and creates a valid ILIAS question object + * + * @param object $item The QTI item object + * @param integer $questionpool_id The id of the parent questionpool + * @param integer $tst_id The id of the parent test if the question is part of a test + * @param object $tst_object A reference to the parent test object + * @param integer $question_counter A reference to a question counter to count the questions of an imported question pool + * @param array $import_mapping An array containing references to included ILIAS objects + * @access public + */ + function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array + { + $import = new assExampleQuestionImport($this); + $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping); + + return $import_mapping; + } + + /** + * Returns a QTI xml representation of the question and sets the internal + * domxml variable with the DOM XML representation of the QTI xml representation + * + * @return string The QTI xml representation of the question + * @access public + */ + function toXML( + bool $a_include_header = true, + bool $a_include_binary = true, + bool $a_shuffle = false, + bool $test_output = false, + bool $force_image_references = false + ): string + { + $export = new assExampleQuestionExport($this); + return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references); + } +} + +?> diff --git a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php new file mode 100644 index 000000000000..197a8015b785 --- /dev/null +++ b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php @@ -0,0 +1,354 @@ + + * @version $Id: $ + * @ingroup ModulesTestQuestionPool + * + * @ilctrl_iscalledby assWrappedQuestionGUI: ilObjQuestionPoolGUI, ilObjTestGUI, ilQuestionEditGUI, ilTestExpressPageObjectGUI + * @ilctrl_calls assWrappedQuestionGUI: ilFormPropertyDispatchGUI + */ +class assWrappedQuestionGUI extends assQuestionGUI +{ + protected ilQuestionTypeFactory $factory; + + + /** + * @var assWrappedQuestion The question object + */ + public assQuestion $object; + + /** + * Constructor + * + * @param integer $id The database id of a question object + * @access public + */ + public function __construct($id = -1) + { + global $DIC; + + parent::__construct(); + + $this->object = new assWrappedQuestion(); + if ($id >= 0) + { + $this->object->loadFromDb($id); + } + } + + /** + * This function should be called directly after the constructor + */ + public function init(ilQuestionTypeFactory $factory): self + { + $this->factory = $factory; + return $this; + } + + + /** + * Creates an output of the edit form for the question + * + * @param bool $checkonly + * @return bool + */ + public function editQuestion($checkonly = false) + { + global $DIC; + $lng = $DIC->language(); + + $form = new ilPropertyFormGUI(); + $form->setFormAction($this->ctrl->getFormAction($this)); + $form->setTitle($this->outQuestionType()); + $form->setMultipart(TRUE); + $form->setTableWidth("100%"); + $form->setId($this->factory->getTypeTag()); + + // Title, author, description, question, working time + $this->addBasicQuestionFormProperties($form); + + // Here you can add question type specific form properties + // We only add an input field for the maximum points + // NOTE: in complex question types the maximum points are summed up by partial points + $points = new ilNumberInputGUI($lng->txt('maximum_points'),'points'); + $points->setSize(3); + $points->setMinValue(1); + $points->allowDecimals(0); + $points->setRequired(true); + $points->setValue($this->object->getPoints()); + $form->addItem($points); + + $this->populateTaxonomyFormSection($form); + $this->addQuestionFormCommandButtons($form); + + $errors = false; + if ($this->isSaveCommand()) + { + $form->setValuesByPost(); + $errors = !$form->checkInput(); + $form->setValuesByPost(); // again, because checkInput now performs the whole stripSlashes handling and we need this if we don't want to have duplication of backslashes + if ($errors) + { + $checkonly = false; + } + } + + if (!$checkonly) + { + $this->getQuestionTemplate(); + $this->tpl->setVariable("QUESTION_DATA", $form->getHTML()); + } + + return $errors; + } + + /** + * Evaluates a posted edit form and writes the form data in the question object + * + * @param bool $always + * @return integer A positive value, if one of the required fields wasn't set, else 0 + */ + protected function writePostData($always = false): int + { + $hasErrors = (!$always) ? $this->editQuestion(true) : false; + if (!$hasErrors) + { + $this->writeQuestionGenericPostData(); + + // Here you can write the question type specific values + // Some question types define the maximum points directly, + // other calculate them from other properties + $this->object->setPoints((int) $_POST["points"]); + + $this->saveTaxonomyAssignments(); + return 0; + } + return 1; + } + + + /** + * Get the HTML output of the question for a test + * (this function could be private) + * + * @param integer $active_id The active user id + * @param integer $pass The test pass + * @param boolean $is_postponed Question is postponed + * @param boolean $use_post_solutions Use post solutions + * @param boolean $show_specific_inline_feedback Show a specific inline feedback + * @return string + */ + public function getTestOutput($active_id, $pass = NULL, $is_postponed = FALSE, $use_post_solutions = FALSE, $show_specific_inline_feedback = FALSE): string + { + if (is_null($pass)) + { + $pass = ilObjTest::_getPass($active_id); + } + + $solution = $this->object->getSolutionStored($active_id, $pass, null); + $value1 = isset($solution["value1"]) ? $solution["value1"] : ""; + $value2 = isset($solution["value2"]) ? $solution["value2"] : ""; + + // fill the question output template + // in out example we have 1:1 relation for the database field + $template = new ilTemplate("tpl.qtype_ta_output.html", true, true, "Services/Question"); + + $template->setVariable("QUESTION_ID", $this->object->getId()); + $questiontext = $this->object->getQuestion(); + $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, TRUE)); + $template->setVariable("LABEL_VALUE1", $this->lng->txt('label_value1')); + $template->setVariable("LABEL_VALUE2", $this->lng->txt('label_value2')); + + $template->setVariable("VALUE1", ilLegacyFormElementsUtil::prepareFormOutput($value1)); + $template->setVariable("VALUE2", ilLegacyFormElementsUtil::prepareFormOutput($value2)); + + $questionoutput = $template->get(); + $pageoutput = $this->outQuestionPage("", $is_postponed, $active_id, $questionoutput); + return $pageoutput; + } + + + /** + * Get the output for question preview + * (called from ilObjQuestionPoolGUI) + * + * @param boolean $show_question_only show only the question instead of embedding page (true/false) + * @param boolean $show_question_only + * @return string + */ + public function getPreview($show_question_only = FALSE, $showInlineFeedback = FALSE) + { + if( is_object($this->getPreviewSession()) ) + { + $solution = $this->getPreviewSession()->getParticipantsSolution(); + } + else + { + $solution = array('value1' => null, 'value2' => null); + } + + // Fill the template with a preview version of the question + $template = new ilTemplate("tpl.qtype_ta_output.html", true, true, "Services/Question"); + $questiontext = $this->object->getQuestion(); + $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, TRUE)); + $template->setVariable("QUESTION_ID", $this->object->getId()); + $template->setVariable("LABEL_VALUE1", $this->lng->txt('label_value1')); + $template->setVariable("LABEL_VALUE2", $this->lng->txt('label_value2')); + + $template->setVariable("VALUE1", ilLegacyFormElementsUtil::prepareFormOutput($solution['value1'] ?? '')); + $template->setVariable("VALUE2", ilLegacyFormElementsUtil::prepareFormOutput($solution['value2'] ?? '')); + + $questionoutput = $template->get(); + if(!$show_question_only) + { + // get page object output + $questionoutput = $this->getILIASPage($questionoutput); + } + return $questionoutput; + } + + /** + * Get the question solution output + * @param integer $active_id The active user id + * @param integer $pass The test pass + * @param boolean $graphicalOutput Show visual feedback for right/wrong answers + * @param boolean $result_output Show the reached points for parts of the question + * @param boolean $show_question_only Show the question without the ILIAS content around + * @param boolean $show_feedback Show the question feedback + * @param boolean $show_correct_solution Show the correct solution instead of the user solution + * @param boolean $show_manual_scoring Show specific information for the manual scoring output + * @param bool $show_question_text + + * @return string solution output of the question as HTML code + */ + function getSolutionOutput( + $active_id, + $pass = NULL, + $graphicalOutput = FALSE, + $result_output = FALSE, + $show_question_only = TRUE, + $show_feedback = FALSE, + $show_correct_solution = FALSE, + $show_manual_scoring = FALSE, + $show_question_text = TRUE + ): string + { + // get the solution of the user for the active pass or from the last pass if allowed + if (($active_id > 0) && (!$show_correct_solution)) + { + $solution = $this->object->getSolutionStored($active_id, $pass, true); + $value1 = isset($solution["value1"]) ? $solution["value1"] : ""; + $value2 = isset($solution["value2"]) ? $solution["value2"] : ""; + } + else + { + // show the correct solution + $value1 = $this->lng->txt("any_text"); + $value2 = $this->object->getMaximumPoints(); + } + + // get the solution template + $template = new ilTemplate("tpl.qtype_ta_solution.html", true, true, "Services/Question"); + $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", TRUE, TRUE, "Modules/TestQuestionPool"); + + if (($active_id > 0) && (!$show_correct_solution)) + { + if ($graphicalOutput) + { + // copied from assNumericGUI, yet not really understood + if($this->object->getStep() === NULL) + { + $reached_points = $this->object->getReachedPoints($active_id, $pass); + } + else + { + $reached_points = $this->object->calculateReachedPoints($active_id, $pass); + } + + // output of ok/not ok icons for user entered solutions + // in this example we have ony one relevant input field (points) + // so we just need to set the icon beneath this field + // question types with partial answers may have a more complex output + if ($reached_points == $this->object->getMaximumPoints()) + { + $template->setCurrentBlock("icon_ok"); + $template->setVariable("ICON_OK", ilUtil::getImagePath("icon_ok.svg")); + $template->setVariable("TEXT_OK", $this->lng->txt("answer_is_right")); + $template->parseCurrentBlock(); + } + else + { + $template->setCurrentBlock("icon_ok"); + $template->setVariable("ICON_NOT_OK", ilUtil::getImagePath("icon_not_ok.svg")); + $template->setVariable("TEXT_NOT_OK", $this->lng->txt("answer_is_wrong")); + $template->parseCurrentBlock(); + } + } + } + + // fill the template variables + // adapt this to your structure of answers + $template->setVariable("LABEL_VALUE1", $this->lng->txt('label_value1')); + $template->setVariable("LABEL_VALUE2", $this->lng->txt('label_value2')); + + $template->setVariable("VALUE1", empty($value1) ? "     " : ilLegacyFormElementsUtil::prepareFormOutput($value1)); + $template->setVariable("VALUE2", empty($value2) ? "     " : ilLegacyFormElementsUtil::prepareFormOutput($value2)); + + $questiontext = $this->object->getQuestion(); + if ($show_question_text==true) + { + $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, TRUE)); + } + + $questionoutput = $template->get(); + + $feedback = ($show_feedback && !$this->isTestPresentationContext()) ? $this->getGenericFeedbackOutput($active_id, $pass) : ""; + if (strlen($feedback)) + { + $cssClass = ( $this->hasCorrectSolution($active_id, $pass) ? + ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG + ); + + $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass); + $solutiontemplate->setVariable("FEEDBACK", $this->object->prepareTextareaOutput( $feedback, true )); + + } + $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput); + + $solutionoutput = $solutiontemplate->get(); + if(!$show_question_only) + { + // get page object output + $solutionoutput = $this->getILIASPage($solutionoutput); + } + return $solutionoutput; + } + + /** + * Returns the answer specific feedback for the question + * + * @param array $userSolution Array with the user solutions + * @return string HTML Code with the answer specific feedback + * @access public + */ + public function getSpecificFeedbackOutput($userSolution): string + { + // By default no answer specific feedback is defined + $output = ''; + return $this->object->prepareTextareaOutput($output, TRUE); + } + + + /** + * Sets the ILIAS tabs for this question type + * called from ilObjTestGUI and ilObjQuestionPoolGUI + */ + public function setQuestionTabs(): void + { + parent::setQuestionTabs(); + } +} +?> diff --git a/Services/Question/classes/TA/class.ilAssWrappedQuestionFeedback.php b/Services/Question/classes/TA/class.ilAssWrappedQuestionFeedback.php new file mode 100644 index 000000000000..593958f29bbb --- /dev/null +++ b/Services/Question/classes/TA/class.ilAssWrappedQuestionFeedback.php @@ -0,0 +1,79 @@ + + + + + + + + diff --git a/Services/Question/templates/default/tpl.qtype_ta_output.html b/Services/Question/templates/default/tpl.qtype_ta_output.html new file mode 100644 index 000000000000..b6817732cf77 --- /dev/null +++ b/Services/Question/templates/default/tpl.qtype_ta_output.html @@ -0,0 +1,12 @@ +
+
{QUESTIONTEXT}
+
+
+ +
+
+
+ + +
+
\ No newline at end of file diff --git a/Services/Question/templates/default/tpl.qtype_ta_solution.html b/Services/Question/templates/default/tpl.qtype_ta_solution.html new file mode 100644 index 000000000000..2fb84fd3e6e4 --- /dev/null +++ b/Services/Question/templates/default/tpl.qtype_ta_solution.html @@ -0,0 +1,17 @@ +
+
{QUESTIONTEXT}
+
+ {LABEL_VALUE1}
+ {VALUE1} +
+
+ {LABEL_VALUE2}
+ {VALUE2} + + {TEXT_OK} + + + {TEXT_NOT_OK} + +
+
\ No newline at end of file From dce46c8cc328b5c4d18316b5121b43354358a413 Mon Sep 17 00:00:00 2001 From: Fred Neumann Date: Fri, 24 Feb 2023 19:41:35 +0100 Subject: [PATCH 2/4] new plugin slot (continued) --- Modules/Test/classes/class.ilObjTest.php | 16 +- .../tables/class.ilTestQuestionsTableGUI.php | 2 +- .../classes/class.assQuestion.php | 54 ++---- .../classes/class.assQuestionGUI.php | 18 +- .../classes/class.ilAssQuestionList.php | 27 +-- .../classes/class.ilObjQuestionPool.php | 29 +-- .../questions/class.ilAssQuestionType.php | 24 +-- .../class.ilQuestionBrowserTableGUI.php | 2 +- .../Core/class.ilQuestionFactories.php | 56 ++++++ .../Question/classes/Core/ilQuestionTypes.php | 69 ------- ...ry.php => interface.ilQuestionFactory.php} | 2 +- .../interface.ilQuestionOfflineFactory.php | 5 + .../PluginSlot/class.ilQuestionTypePlugin.php | 12 +- .../classes/TA/class.assWrappedQuestion.php | 8 +- .../TA/class.assWrappedQuestionGUI.php | 5 +- .../TA/class.ilQuestionPoolPlugins.php | 53 ++++++ .../classes/TA/class.ilTestQuestions.php | 177 ++++++++++++++++++ 17 files changed, 340 insertions(+), 219 deletions(-) create mode 100644 Services/Question/classes/Core/class.ilQuestionFactories.php delete mode 100644 Services/Question/classes/Core/ilQuestionTypes.php rename Services/Question/classes/Core/{ilQuestionTypeFactory.php => interface.ilQuestionFactory.php} (91%) create mode 100644 Services/Question/classes/Core/interface.ilQuestionOfflineFactory.php create mode 100644 Services/Question/classes/TA/class.ilQuestionPoolPlugins.php create mode 100644 Services/Question/classes/TA/class.ilTestQuestions.php diff --git a/Modules/Test/classes/class.ilObjTest.php b/Modules/Test/classes/class.ilObjTest.php index 4f83f15b386e..e58b6919c827 100755 --- a/Modules/Test/classes/class.ilObjTest.php +++ b/Modules/Test/classes/class.ilObjTest.php @@ -4667,16 +4667,7 @@ public function createQuestionGUI($question_type, $question_id = -1): ?assQuesti return null; } - assQuestion::_includeClass($question_type, 1); - - if (ilQuestionTypes::instance()->hasFactory($question_type)) { - $question = new assWrappedQuestionGUI(); - $question->init(ilQuestionTypes::instance()->getFactory($question_type)); - } - else { - $question_type_gui = $question_type . 'GUI'; - $question = new $question_type_gui(); - } + $question = ilTestQuestions::instance()->getQuestionGUI($question_type); if ($question_id > 0) { $question->object->loadFromDb($question_id); @@ -10081,9 +10072,8 @@ public function areObligationsEnabled(): bool */ public static function isQuestionObligationPossible($questionId): bool { - $classConcreteQuestion = assQuestion::_getQuestionType($questionId); - - assQuestion::_includeClass($classConcreteQuestion, 0); + $question_type = assQuestion::_getQuestionType($questionId); + $classConcreteQuestion = ilTestQuestions::instance()->getQuestionClass($question_type); // static binder is not at work yet (in PHP < 5.3) //$obligationPossible = $classConcreteQuestion::isObligationPossible(); diff --git a/Modules/Test/classes/tables/class.ilTestQuestionsTableGUI.php b/Modules/Test/classes/tables/class.ilTestQuestionsTableGUI.php index 72a7d344498d..7e07ee215ea9 100644 --- a/Modules/Test/classes/tables/class.ilTestQuestionsTableGUI.php +++ b/Modules/Test/classes/tables/class.ilTestQuestionsTableGUI.php @@ -254,7 +254,7 @@ public function fillRow(array $a_set): void ); if ($this->isQuestionManagingEnabled()) { - $editHref = $this->getQuestionEditLink($a_set, $a_set['type_tag'] . 'GUI', 'editQuestion'); + $editHref = $this->getQuestionEditLink($a_set, ilTestQuestions::instance()->getQuestionGUIClass($a_set['type_tag']), 'editQuestion'); $actions->addItem($this->lng->txt('edit_question'), '', $editHref); $editPageHref = $this->getQuestionEditLink($a_set, 'ilAssQuestionPageGUI', 'edit'); diff --git a/Modules/TestQuestionPool/classes/class.assQuestion.php b/Modules/TestQuestionPool/classes/class.assQuestion.php index 9f9c16cedee6..ddbcd046771f 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestion.php +++ b/Modules/TestQuestionPool/classes/class.assQuestion.php @@ -2617,17 +2617,8 @@ public static function instantiateQuestion(int $question_id): assQuestion throw new InvalidArgumentException('No question with ID ' . $question_id . ' exists'); } - if (ilQuestionTypes::instance()->hasFactory($question_type)) { - $question = new assWrappedQuestion(); - $question->init(ilQuestionTypes::instance()->getFactory($question_type)); - $question->loadFromDb($question_id); - } - else { - assQuestion::_includeClass($question_type); - $question = new $question_type(); - $question->loadFromDb($question_id); - } - + $question = ilTestQuestions::instance()->getQuestion($question_type); + $question->loadFromDb($question_id); $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type); $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng); @@ -3170,6 +3161,9 @@ public function getActiveUserData(int $active_id): array return array(); } + /** + * @deprecated this class does nothing + */ public static function _includeClass(string $question_type, int $gui = 0): void { if (self::isCoreQuestionType($question_type)) { @@ -3179,10 +3173,7 @@ public static function _includeClass(string $question_type, int $gui = 0): void public static function getFeedbackClassNameByQuestionType(string $questionType): string { - if (ilQuestionTypes::instance()->hasFactory($questionType)) { - return 'ilAssWrappedQuestionFeedback'; - } - return str_replace('ass', 'ilAss', $questionType) . 'Feedback'; + return ilTestQuestions::instance()->getFeedbackClass($questionType); } public static function isCoreQuestionType(string $questionType): bool @@ -3190,6 +3181,9 @@ public static function isCoreQuestionType(string $questionType): bool return file_exists("Modules/TestQuestionPool/classes/class.{$questionType}GUI.php"); } + /** + * @deprecated this class does nothing + */ public static function includeCoreClass($questionType, $withGuiClass): void { if ($withGuiClass) { @@ -3202,20 +3196,7 @@ public static function includeCoreClass($questionType, $withGuiClass): void public static function _getQuestionTypeName($type_tag): string { - global $DIC; - if (file_exists("./Modules/TestQuestionPool/classes/class." . $type_tag . ".php")) { - $lng = $DIC['lng']; - return $lng->txt($type_tag); - } - $component_factory = $DIC['component.factory']; - - foreach ($component_factory->getActivePluginsInSlot("qst") as $pl) { - if ($pl->getQuestionType() === $type_tag) { - return $pl->getQuestionTypeTranslation(); - } - } - - return (string) ilQuestionTypes::instance()->getTypeTranslation((string) $type_tag); + return ilTestQuestions::instance()->getTypeTranslation($type_tag); } /** @@ -3240,19 +3221,8 @@ public static function instantiateQuestionGUI(int $a_question_id): assQuestionGU if (strcmp($a_question_id, "") != 0) { $question_type = assQuestion::_getQuestionType($a_question_id); - assQuestion::_includeClass($question_type, 1); - - if (ilQuestionTypes::instance()->hasFactory($question_type)) { - $question_gui = new assWrappedQuestionGUI(); - $question_gui->init(ilQuestionTypes::instance()->getFactory($question_type)); - $question_gui->object->loadFromDb($a_question_id); - } - else { - $question_type_gui = $question_type . 'GUI'; - $question_gui = new $question_type_gui(); - $question_gui->object->loadFromDb($a_question_id); - } - + $question_gui = ilTestQuestions::instance()->getQuestionGUI($question_type); + $question_gui->object->loadFromDb($a_question_id); $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type); $question_gui->object->feedbackOBJ = new $feedbackObjectClassname($question_gui->object, $ilCtrl, $ilDB, $lng); diff --git a/Modules/TestQuestionPool/classes/class.assQuestionGUI.php b/Modules/TestQuestionPool/classes/class.assQuestionGUI.php index dc67e52973f4..8407167daa90 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestionGUI.php +++ b/Modules/TestQuestionPool/classes/class.assQuestionGUI.php @@ -432,16 +432,7 @@ public static function _getQuestionGUI(string $question_type = '', int $question return null; } - assQuestion::_includeClass($question_type, 1); - - if (ilQuestionTypes::instance()->hasFactory($question_type)) { - $question = new assWrappedQuestionGUI(); - $question->init(ilQuestionTypes::instance()->getFactory($question_type)); - } - else { - $question_type_gui = $question_type . 'GUI'; - $question = new $question_type_gui(); - } + $question = ilTestQuestions::instance()->getQuestionGUI($question_type); $feedbackObjectClassname = assQuestion::getFeedbackClassNameByQuestionType($question_type); $question->object->feedbackOBJ = new $feedbackObjectClassname($question->object, $ilCtrl, $ilDB, $lng); @@ -468,10 +459,7 @@ public static function _getGUIClassNameForId($a_q_id): string */ public static function _getClassNameForQType($q_type): string { - if (ilQuestionTypes::instance()->hasFactory($q_type)) { - return 'assWrappedQuestionGUI'; - } - return $q_type . "GUI"; + return ilTestQuestions::instance()->getQuestionGUIClass($q_type); } public function populateJavascriptFilesRequiredForWorkForm(ilGlobalTemplateInterface $tpl): void @@ -1720,7 +1708,7 @@ protected function setDefaultTabs(ilTabsGUI $ilTabs): void $q_type = $this->object->getQuestionType(); if (strlen($q_type)) { - $classname = $q_type . "GUI"; + $classname = ilTestQuestions::instance()->getQuestionGUIClass($q_type); $this->ctrl->setParameterByClass(strtolower($classname), "sel_question_types", $q_type); $this->ctrl->setParameterByClass(strtolower($classname), "q_id", $this->request->getQuestionId()); } diff --git a/Modules/TestQuestionPool/classes/class.ilAssQuestionList.php b/Modules/TestQuestionPool/classes/class.ilAssQuestionList.php index 9ed0559d8162..4f41b096050a 100644 --- a/Modules/TestQuestionPool/classes/class.ilAssQuestionList.php +++ b/Modules/TestQuestionPool/classes/class.ilAssQuestionList.php @@ -678,32 +678,7 @@ private function loadTaxonomyAssignmentData($parentObjId, $questionId): array private function isActiveQuestionType(array $questionData): bool { - if (!isset($questionData['plugin'])) { - return false; - } - - if (!$questionData['plugin']) { - return true; - } - - if (!$this->component_repository->getComponentByTypeAndName( - ilComponentInfo::TYPE_MODULES, - 'TestQuestionPool' - )->getPluginSlotById('qst')->hasPluginName($questionData['plugin_name'])) { - return false; - } - - return $this->component_repository - ->getComponentByTypeAndName( - ilComponentInfo::TYPE_MODULES, - 'TestQuestionPool' - ) - ->getPluginSlotById( - 'qst' - ) - ->getPluginByName( - $questionData['plugin_name'] - )->isActive(); + return ilTestQuestions::instance()->isActive($questionData['type_tag']); } public function getDataArrayForQuestionId($questionId) diff --git a/Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php b/Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php index 59bd2c9f8cbc..5f802b11c5f1 100755 --- a/Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php +++ b/Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php @@ -1380,18 +1380,8 @@ public static function _getQuestionTypes($all_tags = false, $fixOrder = false, $ $types = array(); while ($row = $ilDB->fetchAssoc($result)) { if ($all_tags || (!in_array($row["question_type_id"], $forbidden_types))) { - $ilLog = $DIC['ilLog']; - - if ($row["plugin"] == 0) { - $types[$lng->txt($row["type_tag"])] = $row; - } else { - $component_factory = $DIC['component.factory']; - //$plugins = $component_repository->getPluginSlotById("qst")->getActivePlugins(); - foreach ($component_factory->getActivePluginsInSlot("qst") as $pl) { - if (strcmp($pl->getQuestionType(), $row["type_tag"]) == 0) { - $types[$pl->getQuestionTypeTranslation()] = $row; - } - } + if (ilTestQuestions::instance()->isActive($row["type_tag"])) { + $types[ilTestQuestions::instance()->getTypeTranslation($row["type_tag"])] = $row; } } } @@ -1432,15 +1422,7 @@ public static function getQuestionTypeTranslations(): array $result = $ilDB->query("SELECT * FROM qpl_qst_type"); $types = array(); while ($row = $ilDB->fetchAssoc($result)) { - if ($row["plugin"] == 0) { - $types[$row['type_tag']] = $lng->txt($row["type_tag"]); - } else { - foreach ($component_factory->getActivePluginsInSlot("qst") as $pl) { - if (strcmp($pl->getQuestionType(), $row["type_tag"]) == 0) { - $types[$row['type_tag']] = $pl->getQuestionTypeTranslation(); - } - } - } + $types[$row['type_tag']] = ilTestQuestions::instance()->getTypeTranslation($row['type_tag']); } ksort($types); return $types; @@ -1478,6 +1460,7 @@ public static function &_getSelfAssessmentQuestionTypes($all_tags = false): arra "assErrorText" => 10, "assLongMenu" => 11 ); + $next = 12; $satypes = array(); $qtypes = ilObjQuestionPool::_getQuestionTypes($all_tags); foreach ($qtypes as $k => $t) { @@ -1486,6 +1469,10 @@ public static function &_getSelfAssessmentQuestionTypes($all_tags = false): arra $t["order"] = $allowed_types[$t["type_tag"]]; $satypes[$k] = $t; } + elseif (ilTestQuestions::instance()->supportsOffline($t["type_tag"])) { + $t["order"] = $next++; + $satypes[$k] = $t; + } } return $satypes; } diff --git a/Modules/TestQuestionPool/classes/questions/class.ilAssQuestionType.php b/Modules/TestQuestionPool/classes/questions/class.ilAssQuestionType.php index 5b84a773d483..98d950a52aaf 100644 --- a/Modules/TestQuestionPool/classes/questions/class.ilAssQuestionType.php +++ b/Modules/TestQuestionPool/classes/questions/class.ilAssQuestionType.php @@ -124,29 +124,7 @@ public function setPluginName($pluginName): void */ public function isImportable(): bool { - if (!$this->isPlugin()) { - return true; - } - - // Plugins MAY overwrite this method an report back their activation status - if (!$this->component_repository->getComponentByTypeAndName( - ilComponentInfo::TYPE_MODULES, - 'TestQuestionPool' - )->getPluginSlotById('qst')->hasPluginName($this->getPluginName())) { - return false; - } - - return $this->component_repository - ->getComponentByTypeAndName( - ilComponentInfo::TYPE_MODULES, - 'TestQuestionPool' - ) - ->getPluginSlotById( - 'qst' - ) - ->getPluginByName( - $this->getPluginName() - )->isActive(); + return ilTestQuestions::instance()->isImportable($this->getTag()); } /** diff --git a/Modules/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php b/Modules/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php index 663f8ba192ff..250cba11b941 100644 --- a/Modules/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php +++ b/Modules/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php @@ -449,7 +449,7 @@ public function fillRow(array $a_set): void $this->ctrl->getLinkTargetByClass('ilAssQuestionPreviewGUI', ilAssQuestionPreviewGUI::CMD_STATISTICS) ); if ($this->getEditable()) { - $editHref = $this->ctrl->getLinkTargetByClass($a_set['type_tag'] . 'GUI', 'editQuestion'); + $editHref = $this->ctrl->getLinkTargetByClass($class, 'editQuestion'); $actions->addItem($this->lng->txt('edit_question'), '', $editHref); $editPageHref = $this->ctrl->getLinkTargetByClass('ilAssQuestionPageGUI', 'edit'); diff --git a/Services/Question/classes/Core/class.ilQuestionFactories.php b/Services/Question/classes/Core/class.ilQuestionFactories.php new file mode 100644 index 000000000000..ef92331a9c75 --- /dev/null +++ b/Services/Question/classes/Core/class.ilQuestionFactories.php @@ -0,0 +1,56 @@ +component_factory = $component_factory; + $this->load(); + } + + /** + * Load the instances of question factories + * Must be calle before the internal factories array is accesed + * @todo load the factories of core question types when they are migrated + */ + protected function load() + { + if (!isset($this->factories)) { + $this->factories = []; + /** @var ilQuestionTypePlugin $pl */ + foreach ($this->component_factory->getActivePluginsInSlot("qtype") as $pl) { + $factory = $pl->factory(); + $this->factories[$factory->getTypeTag()] = $pl->factory(); + } + } + } + + /** + * Check if a factory for the question types exists + */ + public function has(string $type) : bool + { + return isset($this->factories[$type]); + } + + /** + * Get the factory of a question type + */ + public function get($type) : ?ilQuestionFactory + { + return $this->factories[$type] ?? null; + } +} \ No newline at end of file diff --git a/Services/Question/classes/Core/ilQuestionTypes.php b/Services/Question/classes/Core/ilQuestionTypes.php deleted file mode 100644 index 6c7ec99e0a87..000000000000 --- a/Services/Question/classes/Core/ilQuestionTypes.php +++ /dev/null @@ -1,69 +0,0 @@ -dic = $dic; - $this->component_factory = $dic['component.factory']; - - /** @var ilQuestionTypePlugin $pl */ - foreach ($this->component_factory->getActivePluginsInSlot("qtype") as $pl) { - $factory = $pl->factory(); - $this->factories[$factory->getTypeTag()] = $pl->factory(); - } - } - - /** - * Check if it is a new question type by the existence of a factory - */ - public function hasFactory(string $type_tag) - { - return $this->factories[$type_tag]; - } - - /** - * Get the factory of a question type - */ - public function getFactory($type_tag) : ?ilQuestionTypeFactory - { - return $this->factories[$type_tag] ?? null; - } - - /** - * Get a translated title of a question type that can be used in lists - */ - public function getTypeTranslation(string $type_tag) :?string - { - return $this->getFactory($type_tag)?->getTypeTranslation(); - } - -} \ No newline at end of file diff --git a/Services/Question/classes/Core/ilQuestionTypeFactory.php b/Services/Question/classes/Core/interface.ilQuestionFactory.php similarity index 91% rename from Services/Question/classes/Core/ilQuestionTypeFactory.php rename to Services/Question/classes/Core/interface.ilQuestionFactory.php index f031e70d70d2..0467062da6c2 100644 --- a/Services/Question/classes/Core/ilQuestionTypeFactory.php +++ b/Services/Question/classes/Core/interface.ilQuestionFactory.php @@ -3,7 +3,7 @@ /** * Factory for all classes needed to implements a question type */ -interface ilQuestionTypeFactory +interface ilQuestionFactory { /** * Get a unique string identifier of the question type diff --git a/Services/Question/classes/Core/interface.ilQuestionOfflineFactory.php b/Services/Question/classes/Core/interface.ilQuestionOfflineFactory.php new file mode 100644 index 000000000000..f43696ad8c1b --- /dev/null +++ b/Services/Question/classes/Core/interface.ilQuestionOfflineFactory.php @@ -0,0 +1,5 @@ +db->quote($this->factory()->getTypeTag(), 'text'); + $this->db->manipulate($query); + } } diff --git a/Services/Question/classes/TA/class.assWrappedQuestion.php b/Services/Question/classes/TA/class.assWrappedQuestion.php index 7b68908abcda..2826fd18daa0 100644 --- a/Services/Question/classes/TA/class.assWrappedQuestion.php +++ b/Services/Question/classes/TA/class.assWrappedQuestion.php @@ -6,7 +6,7 @@ class assWrappedQuestion extends assQuestion { - protected ilQuestionTypeFactory $factory; + protected ilQuestionFactory $factory; /** @@ -37,16 +37,16 @@ public function __construct( /** * This function should be called directly after the constructor */ - public function init(ilQuestionTypeFactory $factory): self + public function init(ilQuestionFactory $factory): self { $this->factory = $factory; return $this; } /** - * Make the question types factory available for the feedback object (question is injected there) + * Make the questionfactory available for the feedback object (question is injected there) */ - public function getTypeFactory() : ilQuestionTypeFactory + public function getFactory() : ilQuestionFactory { return $this->factory; } diff --git a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php index 197a8015b785..b4eb94ce3ef2 100644 --- a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php +++ b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php @@ -12,7 +12,7 @@ */ class assWrappedQuestionGUI extends assQuestionGUI { - protected ilQuestionTypeFactory $factory; + protected ilQuestionFactory $factory; /** @@ -42,9 +42,10 @@ public function __construct($id = -1) /** * This function should be called directly after the constructor */ - public function init(ilQuestionTypeFactory $factory): self + public function init(ilQuestionFactory $factory): self { $this->factory = $factory; + $this->object->init($factory); return $this; } diff --git a/Services/Question/classes/TA/class.ilQuestionPoolPlugins.php b/Services/Question/classes/TA/class.ilQuestionPoolPlugins.php new file mode 100644 index 000000000000..d6b348f87f29 --- /dev/null +++ b/Services/Question/classes/TA/class.ilQuestionPoolPlugins.php @@ -0,0 +1,53 @@ +component_factory = $component_factory; + $this->load(); + } + + /** + * Load the instances of classic question pool plugins + */ + protected function load() + { + if (!isset($this->plugins)) { + $this->plugins = []; + /** @var ilQuestionsPlugin $plugin */ + foreach ($this->component_factory->getActivePluginsInSlot("qst") as $plugin) { + $this->plugins[$plugin->getQuestionType()] = $plugin; + } + } + } + + /** + * Check if an active plugin for the question type exists + */ + public function has(string $type) : bool + { + return isset($this->plugins[$type]); + } + + /** + * Get the plugin for a question type + */ + public function get($type) : ?ilQuestionsPlugin + { + return $this->plugins[$type] ?? null; + } +} \ No newline at end of file diff --git a/Services/Question/classes/TA/class.ilTestQuestions.php b/Services/Question/classes/TA/class.ilTestQuestions.php new file mode 100644 index 000000000000..f57236b2e497 --- /dev/null +++ b/Services/Question/classes/TA/class.ilTestQuestions.php @@ -0,0 +1,177 @@ +language(), + new ilQuestionFactories($DIC['component.factory']), + new ilQuestionPoolPlugins($DIC['component.factory']) + ); + } + return self::$instance; + } + + /** + * Constructor + */ + public function __construct( + ilLanguage $lng, + ilQuestionFactories $factories, + ilQuestionPoolPlugins $plugins + ) + { + $this->lng = $lng; + $this->factories = $factories; + $this->plugins = $plugins; + } + + /** + * Get the actual class name for a question type + * New question types with factory share the same wrapper class + */ + public function getQuestionClass(string $type_tag) : string + { + if ($this->factories->has($type_tag)) { + return 'assWrappedQuestion'; + } + return $type_tag; // classic type + } + + /** + * Get the actual gui class name for a question type + * New question types with factory share the same wrapper class + */ + public function getQuestionGUIClass(string $type) : string + { + if ($this->factories->has($type)) { + return 'assWrappedQuestionGUI'; + } + return $type . 'GUI'; // classic type + } + + /** + * Get the actual feedback class name for a question type + * New question types with factory share the same wrapper class + */ + public function getFeedbackClass(string $type) : string + { + if ($this->factories->has($type)) { + return 'ilAssWrappedQuestionFeedback'; + } + return str_replace('ass', 'ilAss', $type) . 'Feedback'; // classic type + } + + /** + * Get an instance of the question class for a question type + */ + public function getQuestion(string $type) : assQuestion + { + if ($this->factories->has($type)) { + $question = new assWrappedQuestion(); + return $question->init($this->factories->get($type)); + } + else { + $question_class = $type; + return new $question_class(); + } + } + + /** + * Get an instance of the question GUI class for a question type + */ + public function getQuestionGUI(string $type) : assQuestionGUI + { + if ($this->factories->has($type)) { + $question_gui = new assWrappedQuestionGUI(); + return $question_gui->init($this->factories->get($type)); + } + else { + $gui_class = $type . 'GUI'; + return new $gui_class(); + } + return $question; + } + + /** + * Get the translated title of the question type + */ + public function getTypeTranslation(string $type) : string + { + if (in_array($type, $this->classic)) { + return $this->lng->txt($type); + } + if ($this->plugins->has($type)) { + return $this->plugins->get($type)->getQuestionTypeTranslation(); + } + if ($this->factories->has($type)) { + return $this->factories->get($type)->getTypeTranslation(); + } + return ''; + } + + /** + * Check if a question type is active + */ + public function isActive(string $type) : bool + { + return in_array($type, $this->classic) + || $this->plugins->has($type) + || $this->factories->has($type); + } + + /** + * Check if a question of a type can be imported + */ + public function isImportable(string $type) : bool + { + return $this->isActive($type); + } + + /** + * Check if question type supports an offline use directly + * classic core question types are handdeled in the caller separately + */ + public function supportsOffline(string $type) : bool + { + return $this->factories->has($type) + && $this->factories->get($type) instanceof ilQuestionOfflineFactory; + } +} \ No newline at end of file From c7d86a0d6aba52c1ba2e39d62efe6ece9d0b44fb Mon Sep 17 00:00:00 2001 From: Fred Neumann Date: Mon, 27 Feb 2023 10:26:04 +0100 Subject: [PATCH 3/4] new plugin slot (continued) --- .../classes/Dependencies/InitUIFramework.php | 7 +- .../classes/Core/class.ilQuestionBaseRepo.php | 52 ++++ .../Core/class.ilQuestionBaseSettings.php | 254 ++++++++++++++++++ .../class.ilQuestionSolutionValuePair.php | 33 +++ .../Core/interface.ilQuestionFactory.php | 33 +++ .../Core/interface.ilQuestionGrader.php | 15 ++ .../Core/interface.ilQuestionSolution.php | 25 ++ .../interface.ilQuestionSolutionHandler.php | 20 ++ .../Core/interface.ilQuestionTypeFeedback.php | 8 + .../Core/interface.ilQuestionTypeSettings.php | 10 + .../classes/TA/class.assWrappedQuestion.php | 192 ++++++------- .../TA/class.assWrappedQuestionGUI.php | 86 ++---- .../classes/TA/class.ilTestQuestions.php | 2 +- src/UI/Component/Question/Canvas/Active.php | 8 + src/UI/Component/Question/Canvas/Factory.php | 51 ++++ src/UI/Component/Question/Canvas/Inactive.php | 30 +++ src/UI/Component/Question/Factory.php | 59 ++++ src/UI/Component/Question/Grader/Factory.php | 40 +++ src/UI/Component/Question/Grader/Standard.php | 8 + .../Question/Presentation/Active.php | 8 + .../Question/Presentation/Factory.php | 50 ++++ .../Question/Presentation/Inactive.php | 42 +++ src/UI/Factory.php | 50 ++++ .../Component/Question/Canvas/Factory.php | 40 +++ .../Component/Question/Canvas/Inactive.php | 41 +++ .../Component/Question/Canvas/Renderer.php | 83 ++++++ .../Component/Question/Factory.php | 44 +++ .../Component/Question/Grader/Factory.php | 30 +++ .../Question/Presentation/Factory.php | 41 +++ .../Question/Presentation/Inactive.php | 51 ++++ src/UI/Implementation/Factory.php | 13 +- .../default/Question/tpl.canvas_active.html | 3 + .../default/Question/tpl.canvas_inactive.html | 3 + 33 files changed, 1253 insertions(+), 179 deletions(-) create mode 100644 Services/Question/classes/Core/class.ilQuestionBaseRepo.php create mode 100644 Services/Question/classes/Core/class.ilQuestionBaseSettings.php create mode 100644 Services/Question/classes/Core/class.ilQuestionSolutionValuePair.php create mode 100644 Services/Question/classes/Core/interface.ilQuestionGrader.php create mode 100644 Services/Question/classes/Core/interface.ilQuestionSolution.php create mode 100644 Services/Question/classes/Core/interface.ilQuestionSolutionHandler.php create mode 100644 Services/Question/classes/Core/interface.ilQuestionTypeFeedback.php create mode 100644 Services/Question/classes/Core/interface.ilQuestionTypeSettings.php create mode 100644 src/UI/Component/Question/Canvas/Active.php create mode 100644 src/UI/Component/Question/Canvas/Factory.php create mode 100644 src/UI/Component/Question/Canvas/Inactive.php create mode 100644 src/UI/Component/Question/Factory.php create mode 100644 src/UI/Component/Question/Grader/Factory.php create mode 100644 src/UI/Component/Question/Grader/Standard.php create mode 100644 src/UI/Component/Question/Presentation/Active.php create mode 100644 src/UI/Component/Question/Presentation/Factory.php create mode 100644 src/UI/Component/Question/Presentation/Inactive.php create mode 100644 src/UI/Implementation/Component/Question/Canvas/Factory.php create mode 100644 src/UI/Implementation/Component/Question/Canvas/Inactive.php create mode 100644 src/UI/Implementation/Component/Question/Canvas/Renderer.php create mode 100644 src/UI/Implementation/Component/Question/Factory.php create mode 100644 src/UI/Implementation/Component/Question/Grader/Factory.php create mode 100644 src/UI/Implementation/Component/Question/Presentation/Factory.php create mode 100644 src/UI/Implementation/Component/Question/Presentation/Inactive.php create mode 100644 src/UI/templates/default/Question/tpl.canvas_active.html create mode 100644 src/UI/templates/default/Question/tpl.canvas_inactive.html diff --git a/Services/Init/classes/Dependencies/InitUIFramework.php b/Services/Init/classes/Dependencies/InitUIFramework.php index c6c443275eab..0f0ff88e2680 100644 --- a/Services/Init/classes/Dependencies/InitUIFramework.php +++ b/Services/Init/classes/Dependencies/InitUIFramework.php @@ -50,7 +50,8 @@ public function init(\ILIAS\DI\Container $c): void $c["ui.factory.menu"], $c["ui.factory.symbol"], $c["ui.factory.toast"], - $c["ui.factory.legacy"] + $c["ui.factory.legacy"], + $c["ui.factory.question"], ); }; $c["ui.upload_limit_resolver"] = function ($c) { @@ -285,5 +286,9 @@ public function init(\ILIAS\DI\Container $c): void $c["ui.pathresolver"] = function ($c): ILIAS\UI\Implementation\Render\ImagePathResolver { return new ilImagePathResolver(); }; + $c["ui.factory.question"] = function ($c) { + return new ILIAS\UI\Implementation\Component\Question\Factory(); + }; + } } diff --git a/Services/Question/classes/Core/class.ilQuestionBaseRepo.php b/Services/Question/classes/Core/class.ilQuestionBaseRepo.php new file mode 100644 index 000000000000..ece5622b00f4 --- /dev/null +++ b/Services/Question/classes/Core/class.ilQuestionBaseRepo.php @@ -0,0 +1,52 @@ +db = $db; + } + + + public function getBaseSettingsForId(int $question_id): ?ilQuestionBaseSettings + { + $query = "SELECT qpl_questions.* FROM qpl_questions WHERE question_id = " + . $this->db->quote($question_id, 'integer'); + + $result = $this->db->query($query); + + if ($row = $this->db->fetchAssoc($result)) { + + return new ilQuestionBaseSettings( + (int) $row['question_id'], + (int) $row['question_type_fi'], + (int) $row['obj_fi'], + (string) $row['title'], + (string) $row['description'], + (string) $row['question_text'], + (string) $row['author'], + (int) $row['owner'], + (int) substr($row['working_time'], 0, 2) * 3600 + + (int) substr($row['working_time'], 3, 2) * 60 + + (int) substr($row['working_time'], 6, 2), + (int) $row['points'], + (int) $row['nr_of_tries'], + (bool) $row['complete'], + (int) $row['created'], + (int) $row['tstamp'], + $row['original_id'] ? (int) $row['original_id'] : null, + (string) $row['external_id'], + (string) $row['add_cont_edit_mode'], + (string) $row['lifecycle'] + ); + } + return null; + } + + public function saveBaseSettings(ilQuestionBaseSettings $settings) : void + { + // todo + } + +} \ No newline at end of file diff --git a/Services/Question/classes/Core/class.ilQuestionBaseSettings.php b/Services/Question/classes/Core/class.ilQuestionBaseSettings.php new file mode 100644 index 000000000000..d6e2a7e9ce9e --- /dev/null +++ b/Services/Question/classes/Core/class.ilQuestionBaseSettings.php @@ -0,0 +1,254 @@ +question_id = $question_id; + $this->type_id = $type_id; + $this->obj_id = $obj_id; + $this->title = $title; + $this->comment = $comment; + $this->question = $question; + $this->author = $author; + $this->owner = $owner; + $this->working_time = $working_time; + $this->max_points = $max_points; + $this->nr_of_tries = $nr_of_tries; + $this->complete = $complete; + $this->created = $created; + $this->modified = $modified; + $this->original_id = $original_id; + $this->external_id = $external_id; + $this->additional_content_editiong_mode = $additional_content_editiong_mode; + $this->lifecycle = $lifecycle; + } + + /** + * Get the JSON encoded version of the settings + */ + public function toJSON(): string + { + return json_encode($this); + } + + /** + * @return int + */ + public function getQuestionId() : int + { + return $this->question_id; + } + + /** + * @param int $id + * @return ilQuestionBaseSettings + */ + public function withQuestionId(int $id) : ilQuestionBaseSettings + { + $clone = clone $this; + $clone->question_id = $id; + return $clone; + } + + /** + * @return int + */ + public function getTypeId() : int + { + return $this->type_id; + } + + /** + * @return int + */ + public function getObjId() : int + { + return $this->obj_id; + } + + /** + * @return string + */ + public function getTitle() : string + { + return $this->title; + } + + /** + * @return string + */ + public function getComment() : string + { + return $this->comment; + } + + /** + * @return string + */ + public function getQuestion() : string + { + return $this->question; + } + + /** + * @param string $question + * @return ilQuestionBaseSettings + */ + public function withQuestion(string $question) : ilQuestionBaseSettings + { + $clone = clone $this; + $clone->question = $question; + return $clone; + } + + /** + * @return string + */ + public function getAuthor() : string + { + return $this->author; + } + + /** + * @return int + */ + public function getOwner() : int + { + return $this->owner; + } + + /** + * @return int + */ + public function getEstimatedWorkingTime() : int + { + return $this->working_time; + } + + /** + * Get a three-element array with estimated working time in hours, minutes and seconds + * @return int[] + */ + public function getEstimatedWorkingTimeParts() : array + { + $hours = floor($this->working_time / 3600); + $minutes = floor(($this->working_time % 3600) / 60); + $seconds = $this->working_time % 60; + + return [$hours, $minutes, $seconds]; + } + + /** + * @return int + */ + public function getMaxPoints() : int + { + return $this->max_points; + } + + /** + * @return int + */ + public function getNrOfTries() : int + { + return $this->nr_of_tries; + } + + /** + * @return bool + */ + public function isComplete() : bool + { + return $this->complete; + } + + /** + * @return int + */ + public function getCreated() : int + { + return $this->created; + } + + /** + * @return int + */ + public function getModified() : int + { + return $this->modified; + } + + /** + * @return int|null + */ + public function getOriginalId() : ?int + { + return $this->original_id; + } + + /** + * @return string + */ + public function getExternalId() : string + { + return $this->external_id; + } + + /** + * @return string + */ + public function getAdditionalContentEditiongMode() : string + { + return $this->additional_content_editiong_mode; + } + + /** + * @return string + */ + public function getLifecycle() : string + { + return $this->lifecycle; + } + +} \ No newline at end of file diff --git a/Services/Question/classes/Core/class.ilQuestionSolutionValuePair.php b/Services/Question/classes/Core/class.ilQuestionSolutionValuePair.php new file mode 100644 index 000000000000..853b454751b7 --- /dev/null +++ b/Services/Question/classes/Core/class.ilQuestionSolutionValuePair.php @@ -0,0 +1,33 @@ +value1 = $value1; + $this->value2 = $value2; + } + + /** + * @return string|null + */ + public function getValue1() : ?string + { + return $this->value1; + } + + /** + * @return string|null + */ + public function getValue2() : ?string + { + return $this->value2; + } + +} \ No newline at end of file diff --git a/Services/Question/classes/Core/interface.ilQuestionFactory.php b/Services/Question/classes/Core/interface.ilQuestionFactory.php index 0467062da6c2..a597a9650065 100644 --- a/Services/Question/classes/Core/interface.ilQuestionFactory.php +++ b/Services/Question/classes/Core/interface.ilQuestionFactory.php @@ -15,4 +15,37 @@ public function getTypeTag(): string; */ public function getTypeTranslation(): string; + /** + * Get the type specific question settings for a question id + */ + public function getTypeSettings(int $question_id) : ilQuestionTypeSettings; + + /** + * Get the class that calculate the reached points and the feedback + * This is the backend version, implemented in php + */ + public function getBackendGrader( + ilQuestionBaseSettings $base_settings, + ilQuestionTypeSettings $type_settings, + ) : ilQuestionGrader; + + /** + * Get the handler that converts solutions between frontend and backend + */ + public function getSolutionHandler(): ilQuestionSolutionHandler; + + /** + * Get the renderer for question type specific UI components + */ + public function getRenderer() : ILIAS\UI\Implementation\Render\ComponentRenderer; + + /** + * Get the inactive question presentation + */ + public function getInactivePresentation( + ilQuestionBaseSettings $base_settings, + ilQuestionTypeSettings $type_settings, + ?ilQuestionSolution $solution, + ?ilQuestionTypeFeedback $feedback + ) : ILIAS\UI\Component\Question\Presentation\Inactive; } \ No newline at end of file diff --git a/Services/Question/classes/Core/interface.ilQuestionGrader.php b/Services/Question/classes/Core/interface.ilQuestionGrader.php new file mode 100644 index 000000000000..acae6bd04a0b --- /dev/null +++ b/Services/Question/classes/Core/interface.ilQuestionGrader.php @@ -0,0 +1,15 @@ +factory; } + /** + * @return ilQuestionBaseSettings|null + */ + public function getStoredBasicSettings() + { + global $DIC; + $repo = new ilQuestionBaseRepo($DIC->database()); + return $repo->getBaseSettingsForId($this->getId()); + } /** * Returns the question type of the question @@ -62,18 +71,6 @@ public function getQuestionType() : string return $this->factory->getTypeTag(); } - /** - * Returns the names of the additional question data tables - * - * All tables must have a 'question_fi' column. - * Data from these tables will be deleted if a question is deleted - * - * @return mixed the name(s) of the additional tables (array or string) - */ - public function getAdditionalTableName() - { - return ''; - } /** * Collects all texts in the question which could contain media objects @@ -145,36 +142,35 @@ function saveToDb($original_id = ''): void public function loadFromDb(int $question_id): void { global $DIC; - $ilDB = $DIC->database(); - - // load the basic question data - $result = $ilDB->query("SELECT qpl_questions.* FROM qpl_questions WHERE question_id = " - . $ilDB->quote($question_id, 'integer')); - - if ($result->numRows() > 0) { - $data = $ilDB->fetchAssoc($result); - $this->setId($question_id); - $this->setObjId($data['obj_fi']); - $this->setOriginalId($data['original_id']); - $this->setOwner($data['owner']); - $this->setTitle((string) $data['title']); - $this->setAuthor($data['author']); - $this->setPoints($data['points']); - $this->setComment((string) $data['description']); - - $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data['question_text'], 1)); - $this->setEstimatedWorkingTime(substr($data['working_time'], 0, 2), substr($data['working_time'], 3, 2), substr($data['working_time'], 6, 2)); - - // now you can load additional data - // ... - - try - { - $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']); - } - catch(ilTestQuestionPoolException $e) - { - } + + $repo = new ilQuestionBaseRepo($DIC->database()); + if (!empty($base_settings = $repo->getBaseSettingsForId($question_id))) { + $this->setId($base_settings->getQuestionId()); + $this->setObjId($base_settings->getObjId()); + $this->setTitle($base_settings->getTitle()); + $this->setComment($base_settings->getComment()); + $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($base_settings->getQuestion(), 1)); + $this->setAuthor($base_settings->getAuthor()); + $this->setOwner($base_settings->getOwner()); + list($hours, $minutes, $seconds) = $base_settings->getEstimatedWorkingTimeParts(); + $this->setEstimatedWorkingTime($hours, $minutes, $seconds); + $this->setPoints($base_settings->getMaxPoints()); + $this->setNrOfTries($base_settings->getNrOfTries()); + $this->setLastChange($base_settings->getModified()); + $this->setOriginalId($base_settings->getOriginalId()); + $this->setExternalId($base_settings->getExternalId()); + + try { + $this->setAdditionalContentEditingMode($base_settings->getAdditionalContentEditiongMode()); + } + catch(ilTestQuestionPoolException $e) { + } + + try { + $this->setLifecycle(ilAssQuestionLifecycle::getInstance($base_settings->getLifecycle())); + } catch (ilTestQuestionPoolInvalidArgumentException $e) { + $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + } } // loads additional stuff like suggested solutions @@ -348,31 +344,22 @@ function syncWithOriginal(): void * savePreviewData() * saveWorkingData() * calculateReachedPointsForSolution() - * - * @return array ('value1' => string|null, 'value2' => float|null) */ - protected function getSolutionSubmit() + protected function getSolutionSubmit() : ilQuestionSolution { - $value1 = trim(ilUtil::stripSlashes($_POST['question'.$this->getId().'value1'])); - $value2 = trim(ilUtil::stripSlashes($_POST['question'.$this->getId().'value2'])); - - return array( - 'value1' => empty($value1)? null : (string) $value1, - 'value2' => empty($value2)? null : (float) $value2 - ); + // this has to be provided by the active question canvas + $json = trim(ilUtil::stripSlashes($_POST['question'.$this->getId().'json'])); + return $this->factory->getSolutionHandler()->getSolutionFromJSON($json); } /** * Get a stored solution for a user and test pass - * This is a wrapper to provide the same structure as getSolutionSubmit() * * @param int $active_id active_id of hte user * @param int $pass number of the test pass * @param bool $authorized get the authorized solution - * - * @return array ('value1' => string|null, 'value2' => float|null) */ - public function getSolutionStored($active_id, $pass, $authorized = null) + public function getSolutionStored($active_id, $pass, $authorized = null) : ilQuestionSolution { // This provides an array with records from tst_solution // The example question should only store one record per answer @@ -389,50 +376,35 @@ public function getSolutionStored($active_id, $pass, $authorized = null) $solutions = $this->getTestOutputSolutions($active_id, $pass); } - - if (empty($solutions)) - { - // no solution stored yet - $value1 = null; - $value2 = null; - } - else - { - // If the process locker isn't activated in the Test and Assessment administration - // then we may have multiple records due to race conditions - // In this case the last saved record wins - $solution = end($solutions); - - $value1 = $solution['value1']; - $value2 = $solution['value2']; + $pairs = []; + foreach ($solutions as $row) { + $pairs = new ilQuestionSolutionValuePair($row['value1'], $row['value2']); } - - return array( - 'value1' => empty($value1)? null : (string) $value1, - 'value2' => empty($value2)? null : (float) $value2 - ); + return $this->factory->getSolutionHandler()->getSolutionFromValuePairs($pairs); } /** - * Calculate the reached points from a solution array + * Calculate the reached points from a solution + * The json representation is coing from a preview session * - * @param array ('value1' => string, 'value2' => float) + * @param ilQuestionSolution|string $solution object or json representation of the solution * @return float reached points */ protected function calculateReachedPointsForSolution($solution) { - // in our example we take the points entered by the student - // and adjust them to be in the allowed range - $points = (float) $solution['value2']; - if ($points <= 0 || $points > $this->getMaximumPoints()) - { - $points = 0; + if (is_string($solution)) { + $solution = $this->factory->getSolutionHandler()->getSolutionFromJSON($solution); } + $grader = $this->factory->getBackendGrader( + $this->getStoredBasicSettings(), + $this->factory->getTypeSettings($this->getId()) + ); + // return the raw points given to the answer // these points will afterwards be adjusted by the scoring options of a test - return $points; + return $grader->getReachedPoints($solution); } @@ -492,8 +464,7 @@ function saveWorkingData($active_id, $pass = NULL, $authorized = true): bool // save the submitted values avoiding race conditions $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function() use (&$entered_values, $solution, $active_id, $pass, $authorized) { - - $entered_values = isset($solution['value1']) || isset($solution['value2']); + $entered_values = !$solution->isEmpty(); if ($authorized) { @@ -530,17 +501,13 @@ function saveWorkingData($active_id, $pass = NULL, $authorized = true): bool return true; } - /** - * Reworks the allready saved working data if neccessary - * @param integer $active_id - * @param integer $pass - * @param boolean $obligationsAnswered - * @param boolean $authorized + * Save a posted solution in the preview session + * This must be JSON because objects can't be stred directly */ - protected function reworkWorkingData($active_id, $pass, $obligationsAnswered, $authorized) + protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void { - // normally nothing needs to be reworked + $previewSession->setParticipantsSolution($this->getSolutionSubmit()->toJSON()); } @@ -560,21 +527,20 @@ public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $star $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(1) . $startrow, $this->getTitle()); $solution = $this->getSolutionStored($active_id, $pass, true); - $value1 = isset($solution['value1']) ? $solution['value1'] : ''; - $value2 = isset($solution['value2']) ? $solution['value2'] : ''; $row = $startrow + 1; - - $worksheet->setCell($row, 0, $this->plugin->txt('label_value1')); - $worksheet->setBold($worksheet->getColumnCoord(0) . $row); - $worksheet->setCell($row, 1, $value1); - $row++; - - $worksheet->setCell($row, 0, $this->plugin->txt('label_value2')); - $worksheet->setBold($worksheet->getColumnCoord(0) . $row); - $worksheet->setCell($row, 1, $value2); - $row++; - + foreach ($solution->toValuePairs() as $pair) + { + $worksheet->setCell($row, 0, 'label_value1'); + $worksheet->setBold($worksheet->getColumnCoord(0) . $row); + $worksheet->setCell($row, 1, $pair->getValue1()); + $row++; + + $worksheet->setCell($row, 0, 'label_value2'); + $worksheet->setBold($worksheet->getColumnCoord(0) . $row); + $worksheet->setCell($row, 1, $pair->getValue2()); + $row++; + } return $row + 1; } @@ -593,9 +559,7 @@ public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $star */ function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array { - $import = new assExampleQuestionImport($this); - $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping); - + // todo return $import_mapping; } @@ -614,8 +578,8 @@ function toXML( bool $force_image_references = false ): string { - $export = new assExampleQuestionExport($this); - return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references); + // todo + return ''; } } diff --git a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php index b4eb94ce3ef2..9b6ff457b955 100644 --- a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php +++ b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php @@ -13,7 +13,7 @@ class assWrappedQuestionGUI extends assQuestionGUI { protected ilQuestionFactory $factory; - + protected \ILIAS\DI\Container $dic; /** * @var assWrappedQuestion The question object @@ -32,6 +32,7 @@ public function __construct($id = -1) parent::__construct(); + $this->dic = $DIC; $this->object = new assWrappedQuestion(); if ($id >= 0) { @@ -237,74 +238,35 @@ function getSolutionOutput( $show_question_text = TRUE ): string { - // get the solution of the user for the active pass or from the last pass if allowed - if (($active_id > 0) && (!$show_correct_solution)) - { - $solution = $this->object->getSolutionStored($active_id, $pass, true); - $value1 = isset($solution["value1"]) ? $solution["value1"] : ""; - $value2 = isset($solution["value2"]) ? $solution["value2"] : ""; - } - else - { - // show the correct solution - $value1 = $this->lng->txt("any_text"); - $value2 = $this->object->getMaximumPoints(); - } - // get the solution template - $template = new ilTemplate("tpl.qtype_ta_solution.html", true, true, "Services/Question"); - $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", TRUE, TRUE, "Modules/TestQuestionPool"); + $base_settings = $this->object->getStoredBasicSettings(); + $type_settings = $this->factory->getTypeSettings($this->object->getId()); + $grader = $this->factory->getBackendGrader($base_settings, $type_settings); - if (($active_id > 0) && (!$show_correct_solution)) - { - if ($graphicalOutput) - { - // copied from assNumericGUI, yet not really understood - if($this->object->getStep() === NULL) - { - $reached_points = $this->object->getReachedPoints($active_id, $pass); - } - else - { - $reached_points = $this->object->calculateReachedPoints($active_id, $pass); - } - - // output of ok/not ok icons for user entered solutions - // in this example we have ony one relevant input field (points) - // so we just need to set the icon beneath this field - // question types with partial answers may have a more complex output - if ($reached_points == $this->object->getMaximumPoints()) - { - $template->setCurrentBlock("icon_ok"); - $template->setVariable("ICON_OK", ilUtil::getImagePath("icon_ok.svg")); - $template->setVariable("TEXT_OK", $this->lng->txt("answer_is_right")); - $template->parseCurrentBlock(); - } - else - { - $template->setCurrentBlock("icon_ok"); - $template->setVariable("ICON_NOT_OK", ilUtil::getImagePath("icon_not_ok.svg")); - $template->setVariable("TEXT_NOT_OK", $this->lng->txt("answer_is_wrong")); - $template->parseCurrentBlock(); - } - } + if ($show_correct_solution) { + $solution = $grader->getCorrectSolution(); + $feedback = null; + } + else { + $solution = $this->object->getSolutionStored($active_id, $pass, true); + $feedback = $graphicalOutput ? $grader->getTypeFeedback($solution) : null; } - // fill the template variables - // adapt this to your structure of answers - $template->setVariable("LABEL_VALUE1", $this->lng->txt('label_value1')); - $template->setVariable("LABEL_VALUE2", $this->lng->txt('label_value2')); + if (!$show_question_text) { + $base_settings = $base_settings->withQuestion(''); + } - $template->setVariable("VALUE1", empty($value1) ? "     " : ilLegacyFormElementsUtil::prepareFormOutput($value1)); - $template->setVariable("VALUE2", empty($value2) ? "     " : ilLegacyFormElementsUtil::prepareFormOutput($value2)); + $canvas = $this->dic->ui()->factory()->question()->canvas()->inactive() + ->withPresentation($this->factory->getInactivePresentation( + $base_settings, + $type_settings, + $solution, + $feedback + ))->withPresentationRenderer($this->factory->getRenderer()); - $questiontext = $this->object->getQuestion(); - if ($show_question_text==true) - { - $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($questiontext, TRUE)); - } + $questionoutput = $this->dic->ui()->renderer()->render($canvas); - $questionoutput = $template->get(); + $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", TRUE, TRUE, "Modules/TestQuestionPool"); $feedback = ($show_feedback && !$this->isTestPresentationContext()) ? $this->getGenericFeedbackOutput($active_id, $pass) : ""; if (strlen($feedback)) diff --git a/Services/Question/classes/TA/class.ilTestQuestions.php b/Services/Question/classes/TA/class.ilTestQuestions.php index f57236b2e497..b1a7ad5cbed3 100644 --- a/Services/Question/classes/TA/class.ilTestQuestions.php +++ b/Services/Question/classes/TA/class.ilTestQuestions.php @@ -2,7 +2,7 @@ /** * Service class for handling test questions - * Hide the differences between classic core and plugin questiin types an the new factory based types + * Hide the differences between classic core and plugin question types an the new factory based types * * @see ilAssQuestionType, ilAssQuestionTypeList */ diff --git a/src/UI/Component/Question/Canvas/Active.php b/src/UI/Component/Question/Canvas/Active.php new file mode 100644 index 000000000000..3133ff8038b0 --- /dev/null +++ b/src/UI/Component/Question/Canvas/Active.php @@ -0,0 +1,8 @@ +presentation; + } + + public function getPresentationRenderer() : ?ComponentRenderer + { + return $this->presentation_rederer; + } + + public function withPresentation(InactivePresentation $presentation) : InactiveCanvas + { + $clone = clone $this; + $clone->presentation = $presentation; + return $clone; + } + + public function withPresentationRenderer(ComponentRenderer $renderer) : InactiveCanvas + { + $clone = clone $this; + $clone->presentation_rederer = $renderer; + return $clone; + } +} \ No newline at end of file diff --git a/src/UI/Implementation/Component/Question/Canvas/Renderer.php b/src/UI/Implementation/Component/Question/Canvas/Renderer.php new file mode 100644 index 000000000000..f001badf64d5 --- /dev/null +++ b/src/UI/Implementation/Component/Question/Canvas/Renderer.php @@ -0,0 +1,83 @@ +checkComponent($component); + + if ($component instanceof Component\Question\Canvas\Inactive) { + return $this->renderInactive($component, $default_renderer); + } + if ($component instanceof Component\Question\Canvas\Active) { + return $this->renderActive($component, $default_renderer); + } + + return $default_renderer->render($component); + } + + + protected function renderInactive(Component\Question\Canvas\Inactive $component, RendererInterface $default_renderer) : string + { + $tpl = $this->getTemplate("tpl.canvas_inactive.html", true, true); + + if (!empty($presentation = $component->getPresentation())) { + if (!empty($renderer = $component->getPresentationRenderer())) { + $tpl->setVariable('PRESENTATION', $renderer->render($presentation, $default_renderer)); + } + else { + // todo: provide more generic info if no specific renderer is given + $tpl->setVariable('PRESENTATION', $presentation->getBaseSettings()->getTitle()); + } + } + else { + $tpl->setVariable('PRESENTATION', 'No question presentation available'); + } + + return $tpl->get(); + } + + + protected function renderActive(Component\Question\Canvas\Active $component, RendererInterface $default_renderer) : string + { + return ''; + } +} diff --git a/src/UI/Implementation/Component/Question/Factory.php b/src/UI/Implementation/Component/Question/Factory.php new file mode 100644 index 000000000000..5ca7853e4527 --- /dev/null +++ b/src/UI/Implementation/Component/Question/Factory.php @@ -0,0 +1,44 @@ +base_settings = $base_settings; + $this->type_settings = $type_settings; + $this->solution = $solution; + $this->feedback = $feedback; + } + + + public function getBaseSettings() : \ilQuestionBaseSettings + { + return $this->base_settings; + } + + + public function getTypeSettings() : \ilQuestionTypeSettings + { + return $this->type_settings; + } + + public function getSolution() : ?\ilQuestionSolution + { + return $this->solution; + } + + public function getFeedback() : ?\ilQuestionFeedback + { + return $this->feedback; + } +} \ No newline at end of file diff --git a/src/UI/Implementation/Factory.php b/src/UI/Implementation/Factory.php index f39ba2e735b9..ffab752fbf37 100644 --- a/src/UI/Implementation/Factory.php +++ b/src/UI/Implementation/Factory.php @@ -52,6 +52,7 @@ class Factory implements \ILIAS\UI\Factory protected C\Symbol\Factory $symbol_factory; protected C\Toast\Factory $toast_factory; protected C\Legacy\Factory $legacy_factory; + protected C\Question\Factory $question_factory; public function __construct( C\Counter\Factory $counter_factory, @@ -78,7 +79,8 @@ public function __construct( C\Menu\Factory $menu_factory, C\Symbol\Factory $symbol_factory, C\Toast\Factory $toast_factory, - C\Legacy\Factory $legacy_factory + C\Legacy\Factory $legacy_factory, + C\Question\Factory $question_factory ) { $this->counter_factory = $counter_factory; $this->button_factory = $button_factory; @@ -105,6 +107,7 @@ public function __construct( $this->symbol_factory = $symbol_factory; $this->toast_factory = $toast_factory; $this->legacy_factory = $legacy_factory; + $this->question_factory = $question_factory; } /** @@ -325,4 +328,12 @@ public function toast(): C\Toast\Factory { return $this->toast_factory; } + + /** + * @inheritdoc + */ + public function question(): C\Question\Factory + { + return $this->question_factory; + } } diff --git a/src/UI/templates/default/Question/tpl.canvas_active.html b/src/UI/templates/default/Question/tpl.canvas_active.html new file mode 100644 index 000000000000..0ee60bed833c --- /dev/null +++ b/src/UI/templates/default/Question/tpl.canvas_active.html @@ -0,0 +1,3 @@ +
+ {PRESENTATION} +
\ No newline at end of file diff --git a/src/UI/templates/default/Question/tpl.canvas_inactive.html b/src/UI/templates/default/Question/tpl.canvas_inactive.html new file mode 100644 index 000000000000..0ee60bed833c --- /dev/null +++ b/src/UI/templates/default/Question/tpl.canvas_inactive.html @@ -0,0 +1,3 @@ +
+ {PRESENTATION} +
\ No newline at end of file From 75ca3e463af28a2df6ed8eaab99456d9c4387b4b Mon Sep 17 00:00:00 2001 From: Fred Neumann Date: Mon, 27 Feb 2023 12:48:57 +0100 Subject: [PATCH 4/4] new plugin slot (continued) --- .../TA/class.assWrappedQuestionGUI.php | 15 ++-- src/UI/Component/Question/Canvas/Active.php | 77 ++++++++++++++++++- src/UI/Component/Question/Grader/Standard.php | 31 +++++++- .../Question/Presentation/Active.php | 44 ++++++++++- .../Question/Presentation/Inactive.php | 4 +- .../Question/Presentation/Factory.php | 4 +- .../Question/Presentation/Inactive.php | 6 +- 7 files changed, 163 insertions(+), 18 deletions(-) diff --git a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php index 9b6ff457b955..34567da121df 100644 --- a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php +++ b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php @@ -151,8 +151,8 @@ public function getTestOutput($active_id, $pass = NULL, $is_postponed = FALSE, $ } $solution = $this->object->getSolutionStored($active_id, $pass, null); - $value1 = isset($solution["value1"]) ? $solution["value1"] : ""; - $value2 = isset($solution["value2"]) ? $solution["value2"] : ""; + $value1 = 'Demo'; + $value2 = $this->object->getStoredBasicSettings()->getMaxPoints(); // fill the question output template // in out example we have 1:1 relation for the database field @@ -187,10 +187,9 @@ public function getPreview($show_question_only = FALSE, $showInlineFeedback = FA { $solution = $this->getPreviewSession()->getParticipantsSolution(); } - else - { - $solution = array('value1' => null, 'value2' => null); - } + + $value1 = 'Demo'; + $value2 = $this->object->getStoredBasicSettings()->getMaxPoints(); // Fill the template with a preview version of the question $template = new ilTemplate("tpl.qtype_ta_output.html", true, true, "Services/Question"); @@ -200,8 +199,8 @@ public function getPreview($show_question_only = FALSE, $showInlineFeedback = FA $template->setVariable("LABEL_VALUE1", $this->lng->txt('label_value1')); $template->setVariable("LABEL_VALUE2", $this->lng->txt('label_value2')); - $template->setVariable("VALUE1", ilLegacyFormElementsUtil::prepareFormOutput($solution['value1'] ?? '')); - $template->setVariable("VALUE2", ilLegacyFormElementsUtil::prepareFormOutput($solution['value2'] ?? '')); + $template->setVariable("VALUE1", ilLegacyFormElementsUtil::prepareFormOutput($value1 ?? '')); + $template->setVariable("VALUE2", ilLegacyFormElementsUtil::prepareFormOutput($value2 ?? '')); $questionoutput = $template->get(); if(!$show_question_only) diff --git a/src/UI/Component/Question/Canvas/Active.php b/src/UI/Component/Question/Canvas/Active.php index 3133ff8038b0..1c8c9dd15f9a 100644 --- a/src/UI/Component/Question/Canvas/Active.php +++ b/src/UI/Component/Question/Canvas/Active.php @@ -2,7 +2,82 @@ namespace ILIAS\UI\Component\Question\Canvas; -interface Active +use ILIAS\UI\Component\Component; +use ILIAS\UI\Component\JavaScriptBindable; +use ILIAS\UI\Component\Triggerable; +use ILIAS\UI\Component\Onloadable; +use ILIAS\UI\Component\Signal; +use ILIAS\UI\Component\Question\Presentation\Active as ActivePresentation; +use ILIAS\UI\Implementation\Render\ComponentRenderer; +use ILIAS\UI\Component\Triggerer; + +interface Active extends Component, Triggerable, Triggerer { + /** + * Get the question presentation + */ + public function getPresentation(): ?ActivePresentation; + + /** + * Get the question presentation renderer + */ + public function getPresentationRenderer(): ?ComponentRenderer; + + /** + * Inject the question presentation + */ + public function withPresentation(ActivePresentation $presentation): Inactive; + + /** + * Inject the question presentation renderer + */ + public function withPresentationRenderer(ComponentRenderer $renderer) : Inactive; + + + /** + * Register the signal for the question presentation to show a user solution + * This signal is triggered by the canvas to send a user solution to the presentation + */ + public function withShowSolutionSignal(Signal $signal); + + + /** + * Register the signal for the question presentation to show a type specific feedback + * This signal is triggered by the canvas to send a feedback to the presentation + */ + public function withShowFeedbackSignal(Signal $signal); + + + /** + * Register the signal for the question presentation to provide a user solution + * This signal is triggered by the canvas to request a user solution from the presentation + * The presentation should repond by triggering a ReceiveSolution for the canvas + */ + public function withProvideSolutionSignal(Signal $signal); + + + /** + * Register the signal for the grader to provide a feedback for the user solution + * This signal is triggered by the canvas to request a feedback from the grader + * The grader should repond by triggering a ReceiveFeedback for the canvas + */ + public function withProvideFeedbackSignal(Signal $signal); + + + /** + * Provide the signal to receive the user solution + * The signal is triggered by the presentation + * It must carry the JSON representation of a user input + * After receiving the canvas should update its own JSON representation of the user solution + */ + public function getReceiveSolutionSignal(): Signal; + + /** + * Provide the signal to receive the feedback for a user solution + * The signal is triggered by the grader after receiving a user solution + * It must carry the JSON representation of a feedback + * After receiving the canvas should trigger the ShowFeedback signal of its presentation + */ + public function getReceiveFeedbackSignal(): Signal; } \ No newline at end of file diff --git a/src/UI/Component/Question/Grader/Standard.php b/src/UI/Component/Question/Grader/Standard.php index c3744cfdaac5..fc57860345d4 100644 --- a/src/UI/Component/Question/Grader/Standard.php +++ b/src/UI/Component/Question/Grader/Standard.php @@ -2,7 +2,36 @@ namespace ILIAS\UI\Component\Question\Grader; -interface Standard +use ILIAS\UI\Component\Component; +use ILIAS\UI\Component\Triggerable; +use ILIAS\UI\Component\Triggerer; +use ILIAS\UI\Component\Signal; + +interface Standard extends Component, Triggerable, Triggerer { + public function __construct( + \ilQuestionBaseSettings $base_settings, + \ilQuestionTypeSettings $type_settings, + ); + + public function getBaseSettings() : \ilQuestionBaseSettings; + public function getTypeSettings() : \ilQuestionTypeSettings; + + + /** + * Register the signal for the canvas to receive the feedback + * The signal is triggered by the grader after receiving a user solution + * It must carry the JSON representation of a feedback + * After receiving the canvas should trigger the ShowFeedback signal of its presentation + */ + public function withReceiveFeedbackSignal(Signal $signal); + + + /** + * Provide the signal to request a feedback for a user solution + * This signal is triggered by the canvas to request a feedback from the grader + * The grader should repond by triggering a ReceiveFeedback for the canvas + */ + public function getProvideFeedbackSignal() : Signal; } \ No newline at end of file diff --git a/src/UI/Component/Question/Presentation/Active.php b/src/UI/Component/Question/Presentation/Active.php index ec21a9aef923..575dea06e229 100644 --- a/src/UI/Component/Question/Presentation/Active.php +++ b/src/UI/Component/Question/Presentation/Active.php @@ -2,7 +2,49 @@ namespace ILIAS\UI\Component\Question\Presentation; -interface Active +use ILIAS\UI\Component\Component; +use ILIAS\UI\Component\Triggerable; +use ILIAS\UI\Component\Triggerer; +use ILIAS\UI\Component\Signal; + +interface Active extends Component, Triggerable, Triggerer { + public function __construct( + \ilQuestionBaseSettings $base_settings, + \ilQuestionTypeSettings $type_settings, + ); + + public function getBaseSettings() : \ilQuestionBaseSettings; + public function getTypeSettings() : \ilQuestionTypeSettings; + + + /** + * Register the signal for the canvas to receive the user solution + * The signal is triggered by the presentation + * It must carry the JSON representation of a user input + * After receiving the canvas should update its own JSON representation of the user solution + */ + public function withReceiveSolutionSignal(Signal $signal); + + + /** + * Provide the signal to show a user solution + * This signal is triggered by the canvas to send a user solution to the presentation + */ + public function getShowSolutionSignal() : Signal; + + + /** + * Provide the signal to show a type specific feedback + * This signal is triggered by the canvas to send a feedback to the presentation + */ + public function getShowFeedbackSignal() : Signal; + + /** + * Provide the signal to send the current user solution + * This signal is triggered by the canvas to request a user solution from the presentation + * The presentation should repond by triggering a ReceiveSolution for the canvas + */ + public function getProvideSolutionSignal() : Signal; } \ No newline at end of file diff --git a/src/UI/Component/Question/Presentation/Inactive.php b/src/UI/Component/Question/Presentation/Inactive.php index 8b3ddbc070d2..960d2b705247 100644 --- a/src/UI/Component/Question/Presentation/Inactive.php +++ b/src/UI/Component/Question/Presentation/Inactive.php @@ -32,11 +32,11 @@ public function __construct( \ilQuestionBaseSettings $base_settings, \ilQuestionTypeSettings $type_settings, ?\ilQuestionSolution $solution, - ?\ilQuestionFeedback $feedback + ?\ilQuestionTypeFeedback $feedback ); public function getBaseSettings() : \ilQuestionBaseSettings; public function getTypeSettings() : \ilQuestionTypeSettings; public function getSolution() : ?\ilQuestionSolution; - public function getFeedback() : ?\ilQuestionFeedback; + public function getFeedback() : ?\ilQuestionTypeFeedback; } \ No newline at end of file diff --git a/src/UI/Implementation/Component/Question/Presentation/Factory.php b/src/UI/Implementation/Component/Question/Presentation/Factory.php index e69920ce0d50..c368738346ee 100644 --- a/src/UI/Implementation/Component/Question/Presentation/Factory.php +++ b/src/UI/Implementation/Component/Question/Presentation/Factory.php @@ -31,11 +31,11 @@ class Factory implements Question\Presentation\Factory public function active() : Active { - // TODO: Implement active() method. + throw new \ILIAS\UI\NotImplementedException(); } public function inactive() : Inactive { - // TODO: Implement inactive() method. + throw new \ILIAS\UI\NotImplementedException(); } } diff --git a/src/UI/Implementation/Component/Question/Presentation/Inactive.php b/src/UI/Implementation/Component/Question/Presentation/Inactive.php index 1f7eb56bedec..9d56a3a539a7 100644 --- a/src/UI/Implementation/Component/Question/Presentation/Inactive.php +++ b/src/UI/Implementation/Component/Question/Presentation/Inactive.php @@ -13,13 +13,13 @@ class Inactive implements InactivePresentation protected \ilQuestionBaseSettings $base_settings; protected \ilQuestionTypeSettings $type_settings; protected ?\ilQuestionSolution $solution; - protected ?\ilQuestionFeedback $feedback; + protected ?\ilQuestionTypeFeedback $feedback; public function __construct( \ilQuestionBaseSettings $base_settings, \ilQuestionTypeSettings $type_settings, ?\ilQuestionSolution $solution, - ?\ilQuestionFeedback $feedback + ?\ilQuestionTypeFeedback $feedback ) { $this->base_settings = $base_settings; $this->type_settings = $type_settings; @@ -44,7 +44,7 @@ public function getSolution() : ?\ilQuestionSolution return $this->solution; } - public function getFeedback() : ?\ilQuestionFeedback + public function getFeedback() : ?\ilQuestionTypeFeedback { return $this->feedback; }