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 @@
+
+