diff --git a/Modules/Test/classes/class.ilObjTest.php b/Modules/Test/classes/class.ilObjTest.php index 0f504f819d3d..e58b6919c827 100755 --- a/Modules/Test/classes/class.ilObjTest.php +++ b/Modules/Test/classes/class.ilObjTest.php @@ -4667,10 +4667,7 @@ public function createQuestionGUI($question_type, $question_id = -1): ?assQuesti return null; } - assQuestion::_includeClass($question_type, 1); - - $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); @@ -10075,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 3fef2d3cd3d8..ddbcd046771f 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestion.php +++ b/Modules/TestQuestionPool/classes/class.assQuestion.php @@ -2617,8 +2617,7 @@ 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 = ilTestQuestions::instance()->getQuestion($question_type); $question->loadFromDb($question_id); $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type); @@ -3162,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)) { @@ -3171,7 +3173,7 @@ public static function _includeClass(string $question_type, int $gui = 0): void public static function getFeedbackClassNameByQuestionType(string $questionType): string { - return str_replace('ass', 'ilAss', $questionType) . 'Feedback'; + return ilTestQuestions::instance()->getFeedbackClass($questionType); } public static function isCoreQuestionType(string $questionType): bool @@ -3179,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) { @@ -3191,19 +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 ""; + return ilTestQuestions::instance()->getTypeTranslation($type_tag); } /** @@ -3228,12 +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); - - $question_type_gui = $question_type . 'GUI'; - $question_gui = new $question_type_gui(); + $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 a24a55b8ca5c..8407167daa90 100755 --- a/Modules/TestQuestionPool/classes/class.assQuestionGUI.php +++ b/Modules/TestQuestionPool/classes/class.assQuestionGUI.php @@ -432,10 +432,7 @@ public static function _getQuestionGUI(string $question_type = '', int $question return null; } - assQuestion::_includeClass($question_type, 1); - - $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); @@ -462,7 +459,7 @@ public static function _getGUIClassNameForId($a_q_id): string */ public static function _getClassNameForQType($q_type): string { - return $q_type . "GUI"; + return ilTestQuestions::instance()->getQuestionGUIClass($q_type); } public function populateJavascriptFilesRequiredForWorkForm(ilGlobalTemplateInterface $tpl): void @@ -1711,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/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.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/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 new file mode 100644 index 000000000000..a597a9650065 --- /dev/null +++ b/Services/Question/classes/Core/interface.ilQuestionFactory.php @@ -0,0 +1,51 @@ +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] + ]); + } + } + + /** + * @todo: migrate to a general question type repository + * @todo: should types be removedon uninstall - their id relation gets lost! + */ + private function removeQuestionType() + { + $query = "DELETE FROM qpl_qst_type WHERE type_tag =" . $this->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 new file mode 100644 index 000000000000..01faae982a28 --- /dev/null +++ b/Services/Question/classes/TA/class.assWrappedQuestion.php @@ -0,0 +1,586 @@ +factory = $factory; + return $this; + } + + /** + * Make the questionfactory available for the feedback object (question is injected there) + */ + public function getFactory() : ilQuestionFactory + { + return $this->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 + * + * @return string The question type of the question + */ + public function getQuestionType() : string + { + return $this->factory->getTypeTag(); + } + + + /** + * 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; + + $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 + 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() + */ + protected function getSolutionSubmit() : ilQuestionSolution + { + // 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 + * + * @param int $active_id active_id of hte user + * @param int $pass number of the test pass + * @param bool $authorized get the authorized solution + */ + 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 + // 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); + } + + $pairs = []; + foreach ($solutions as $row) { + $pairs = new ilQuestionSolutionValuePair($row['value1'], $row['value2']); + } + return $this->factory->getSolutionHandler()->getSolutionFromValuePairs($pairs); + } + + + /** + * Calculate the reached points from a solution + * The json representation is coing from a preview session + * + * @param ilQuestionSolution|string $solution object or json representation of the solution + * @return float reached points + */ + protected function calculateReachedPointsForSolution($solution) + { + 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 $grader->getReachedPoints($solution); + } + + + /** + * 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 = !$solution->isEmpty(); + + 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; + } + + /** + * Save a posted solution in the preview session + * This must be JSON because objects can't be stred directly + */ + protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void + { + $previewSession->setParticipantsSolution($this->getSolutionSubmit()->toJSON()); + } + + + /** + * 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); + + $row = $startrow + 1; + 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; + } + + /** + * 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 + { + // todo + 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 + { + // todo + return ''; + } +} + +?> diff --git a/Services/Question/classes/TA/class.assWrappedQuestionGUI.php b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php new file mode 100644 index 000000000000..34567da121df --- /dev/null +++ b/Services/Question/classes/TA/class.assWrappedQuestionGUI.php @@ -0,0 +1,316 @@ + + * @version $Id: $ + * @ingroup ModulesTestQuestionPool + * + * @ilctrl_iscalledby assWrappedQuestionGUI: ilObjQuestionPoolGUI, ilObjTestGUI, ilQuestionEditGUI, ilTestExpressPageObjectGUI + * @ilctrl_calls assWrappedQuestionGUI: ilFormPropertyDispatchGUI + */ +class assWrappedQuestionGUI extends assQuestionGUI +{ + protected ilQuestionFactory $factory; + protected \ILIAS\DI\Container $dic; + + /** + * @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->dic = $DIC; + $this->object = new assWrappedQuestion(); + if ($id >= 0) + { + $this->object->loadFromDb($id); + } + } + + /** + * This function should be called directly after the constructor + */ + public function init(ilQuestionFactory $factory): self + { + $this->factory = $factory; + $this->object->init($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 = 'Demo'; + $value2 = $this->object->getStoredBasicSettings()->getMaxPoints(); + + // 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(); + } + + $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"); + $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($value1 ?? '')); + $template->setVariable("VALUE2", ilLegacyFormElementsUtil::prepareFormOutput($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 + { + + $base_settings = $this->object->getStoredBasicSettings(); + $type_settings = $this->factory->getTypeSettings($this->object->getId()); + $grader = $this->factory->getBackendGrader($base_settings, $type_settings); + + if ($show_correct_solution) { + $solution = $grader->getCorrectSolution(); + $feedback = null; + } + else { + $solution = $this->object->getSolutionStored($active_id, $pass, true); + $feedback = $graphicalOutput ? $grader->getTypeFeedback($solution) : null; + } + + if (!$show_question_text) { + $base_settings = $base_settings->withQuestion(''); + } + + $canvas = $this->dic->ui()->factory()->question()->canvas()->inactive() + ->withPresentation($this->factory->getInactivePresentation( + $base_settings, + $type_settings, + $solution, + $feedback + ))->withPresentationRenderer($this->factory->getRenderer()); + + $questionoutput = $this->dic->ui()->renderer()->render($canvas); + + $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)) + { + $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 @@ +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..b1a7ad5cbed3 --- /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 diff --git a/Services/Question/service.xml b/Services/Question/service.xml new file mode 100644 index 000000000000..d63225dba6a5 --- /dev/null +++ b/Services/Question/service.xml @@ -0,0 +1,9 @@ + + + + + + + + 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 diff --git a/src/UI/Component/Question/Canvas/Active.php b/src/UI/Component/Question/Canvas/Active.php new file mode 100644 index 000000000000..1c8c9dd15f9a --- /dev/null +++ b/src/UI/Component/Question/Canvas/Active.php @@ -0,0 +1,83 @@ +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() : ?\ilQuestionTypeFeedback + { + 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