diff --git a/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php b/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php index 3f9baaa54ae2..660af1b3f27f 100755 --- a/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php +++ b/components/ILIAS/Administration/GlobalScreen/classes/AdministrationMainBarProvider.php @@ -225,7 +225,7 @@ private function getGroups(): array // admin menu layout $layout = array( "maintenance" => - array("adma", "serv", "cron", "bnmk", "lngf", "hlps", "wfe", 'fils', 'logs', 'sysc', "recf", "root"), + array("adma", "serv", "cron", "bnmk", "lngf", "hlps", "wfe", 'fils', 'qsts', 'logs', 'sysc', "recf", "root"), "layout_and_navigation" => array("mme", "gsfo", "dshs", "stys", "adve", "stus"), "repository_and_objects" => diff --git a/components/ILIAS/AdministrativeNotification/classes/Table.php b/components/ILIAS/AdministrativeNotification/classes/Table.php index 7e3abc76c9a1..8eceeba2e233 100755 --- a/components/ILIAS/AdministrativeNotification/classes/Table.php +++ b/components/ILIAS/AdministrativeNotification/classes/Table.php @@ -68,7 +68,7 @@ public function __construct( $this->components[] = $this->ui_factory->table()->data( $data_retrieval, - $this->lng->txt('notifications'), + $this->lng->txt('msg_table_title'), $columns, )->withActions($actions)->withRequest( $DIC->http()->request() diff --git a/components/ILIAS/AuthShibboleth/classes/Config/class.ilShibbolethSettings.php b/components/ILIAS/AuthShibboleth/classes/Config/class.ilShibbolethSettings.php index 99f437040b0b..e91b6d221460 100755 --- a/components/ILIAS/AuthShibboleth/classes/Config/class.ilShibbolethSettings.php +++ b/components/ILIAS/AuthShibboleth/classes/Config/class.ilShibbolethSettings.php @@ -34,7 +34,7 @@ class ilShibbolethSettings /** * @var string */ - private const DEFAULT_LOGIN_BUTTON = "assets/images/auth/shib_login_button.svg"; + private const DEFAULT_LOGIN_BUTTON = "./assets/images/auth/shib_login_button.svg"; /** * @var string */ diff --git a/components/ILIAS/AuthShibboleth/classes/Config/class.shibConfig.php b/components/ILIAS/AuthShibboleth/classes/Config/class.shibConfig.php index 85b998149ab2..dd49b4f85b2c 100755 --- a/components/ILIAS/AuthShibboleth/classes/Config/class.shibConfig.php +++ b/components/ILIAS/AuthShibboleth/classes/Config/class.shibConfig.php @@ -457,9 +457,6 @@ public function setUpdateTitle(bool $update_title): void $this->update_title = $update_title; } - /** - * @return mixed - */ public function getUpdateTitle(): bool { return $this->update_title; diff --git a/components/ILIAS/AuthShibboleth/classes/class.ilAuthProviderShibboleth.php b/components/ILIAS/AuthShibboleth/classes/class.ilAuthProviderShibboleth.php index 706b56ada627..0216138fd379 100755 --- a/components/ILIAS/AuthShibboleth/classes/class.ilAuthProviderShibboleth.php +++ b/components/ILIAS/AuthShibboleth/classes/class.ilAuthProviderShibboleth.php @@ -110,7 +110,7 @@ public function doAuthentication(ilAuthStatus $status): bool return false; } - ilSession::set('shibboleth_session_id', $_SERVER['Shib-Session-ID']); + ilSession::set('shibboleth_session_id', $_SERVER['Shib-Session-ID'] ?? ''); return true; } } diff --git a/components/ILIAS/AuthShibboleth/resources/shib_logout.php b/components/ILIAS/AuthShibboleth/resources/shib_logout.php index 850aae9f2d73..2207637d7541 100644 --- a/components/ILIAS/AuthShibboleth/resources/shib_logout.php +++ b/components/ILIAS/AuthShibboleth/resources/shib_logout.php @@ -144,14 +144,15 @@ function LogoutNotification($SessionID): ?\SoapFault while ($session = $r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) { $session_data = unserializesession($session['data']); - if (is_array($session_data) + // Delete this session entry + if ( + is_array($session_data) && array_key_exists('shibboleth_session_id', $session_data) - && $session_data['shibboleth_session_id'] == $SessionID + && $session_data['shibboleth_session_id'] === $SessionID + && !ilSession::_destroy($session['session_id'] + ) ) { - // Delete this session entry - if (ilSession::_destroy($session['session_id']) !== true) { - return new SoapFault('LogoutError', 'Could not delete session entry in database.'); - } + return new SoapFault('LogoutError', 'Could not delete session entry in database.'); } } // If no SoapFault is returned, all is fine diff --git a/components/ILIAS/AuthShibboleth/src/LoginPerformer.php b/components/ILIAS/AuthShibboleth/src/LoginPerformer.php index 417f2473f451..fc61d0a17194 100755 --- a/components/ILIAS/AuthShibboleth/src/LoginPerformer.php +++ b/components/ILIAS/AuthShibboleth/src/LoginPerformer.php @@ -75,12 +75,12 @@ public function doShibbolethAuthentication(): void // no break case ilAuthStatus::STATUS_ACCOUNT_MIGRATION_REQUIRED: - $this->ctrl->redirect($this, 'showAccountMigration'); + $this->ctrl->redirectByClass([ilStartUpGUI::class], 'showAccountMigration'); // no break case ilAuthStatus::STATUS_AUTHENTICATION_FAILED: $this->template->setOnScreenMessage('failure', $status->getTranslatedReason(), true); - $this->ctrl->redirect($this, 'showLoginPage'); + $this->ctrl->redirectByClass([ilStartUpGUI::class], 'showLoginPage'); } $this->template->setOnScreenMessage('failure', $this->lng->txt('err_wrong_login'), true); diff --git a/components/ILIAS/Authentication/classes/Cron/class.ilAuthDestroyExpiredSessionsCron.php b/components/ILIAS/Authentication/classes/Cron/class.ilAuthDestroyExpiredSessionsCron.php index 20e7b429c4d0..7f38fe762170 100755 --- a/components/ILIAS/Authentication/classes/Cron/class.ilAuthDestroyExpiredSessionsCron.php +++ b/components/ILIAS/Authentication/classes/Cron/class.ilAuthDestroyExpiredSessionsCron.php @@ -87,7 +87,7 @@ public function run(): JobResult $result->setStatus(JobResult::STATUS_OK); $num_destroyed_sessions = ilSession::_destroyExpiredSessions(); - ilSessionStatistics::aggretateRaw(time()); + ilSessionStatistics::aggregateRaw(time()); $result->setMessage('Number of destroyed sessions: ' . $num_destroyed_sessions); return $result; diff --git a/components/ILIAS/Authentication/classes/class.ilSession.php b/components/ILIAS/Authentication/classes/class.ilSession.php index 2279aef3d375..cf9cef39f946 100755 --- a/components/ILIAS/Authentication/classes/class.ilSession.php +++ b/components/ILIAS/Authentication/classes/class.ilSession.php @@ -195,7 +195,7 @@ public static function _writeData(string $a_session_id, string $a_data): bool if ($r->getInt(0, 50) === 2) { // get time _before_ destroying expired sessions self::_destroyExpiredSessions(); - ilSessionStatistics::aggretateRaw($now); + ilSessionStatistics::aggregateRaw($now); } } diff --git a/components/ILIAS/Authentication/classes/class.ilSessionStatistics.php b/components/ILIAS/Authentication/classes/class.ilSessionStatistics.php index 8ed679c53efe..9c0b97ede4c3 100755 --- a/components/ILIAS/Authentication/classes/class.ilSessionStatistics.php +++ b/components/ILIAS/Authentication/classes/class.ilSessionStatistics.php @@ -22,25 +22,25 @@ class ilSessionStatistics { private const int SLOT_SIZE = 15; - /** - * Is session statistics active at all? - */ + private static ?ilDBStatement $number_of_active_raw_sessions_statement = null; + private static ?ilDBStatement $aggregated_raw_data_statement = null; + private static ?ilDBStatement $raw_data_statement = null; + public static function isActive(): bool { global $DIC; + /** @var ilSetting $ilSetting */ $ilSetting = $DIC['ilSetting']; return (bool) $ilSetting->get('session_statistics', '1'); } - /** - * Create raw data entry - */ public static function createRawEntry(string $a_session_id, int $a_session_type, int $a_timestamp, int $a_user_id): void { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; if (!$a_user_id || !$a_session_id || !self::isActive()) { @@ -49,31 +49,28 @@ public static function createRawEntry(string $a_session_id, int $a_session_type, // #9669: if a session was destroyed and somehow the session id is still // in use there will be a id-collision for the raw-entry - $ilDB->replace( 'usr_session_stats_raw', [ - 'session_id' => ['text', $a_session_id] + 'session_id' => [ilDBConstants::T_TEXT, $a_session_id] ], [ - 'type' => ['integer', $a_session_type], - 'start_time' => ['integer', $a_timestamp], - 'user_id' => ['integer', $a_user_id] + 'type' => [ilDBConstants::T_INTEGER, $a_session_type], + 'start_time' => [ilDBConstants::T_INTEGER, $a_timestamp], + 'user_id' => [ilDBConstants::T_INTEGER, $a_user_id] ] ); } /** - * Close raw data entry - * - * @param int|array $a_session_id - * @param int $a_context + * @param string|list $a_session_id * @param int|bool $a_expired_at */ public static function closeRawEntry($a_session_id, ?int $a_context = null, $a_expired_at = null): void { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; if (!self::isActive()) { @@ -88,22 +85,22 @@ public static function closeRawEntry($a_session_id, ?int $a_context = null, $a_e $end_time = time(); } $sql = 'UPDATE usr_session_stats_raw' . - ' SET end_time = ' . $ilDB->quote($end_time, 'integer'); + ' SET end_time = ' . $ilDB->quote($end_time, ilDBConstants::T_INTEGER); if ($a_context) { - $sql .= ',end_context = ' . $ilDB->quote($a_context, 'integer'); + $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER); } - $sql .= ' WHERE session_id = ' . $ilDB->quote($a_session_id, 'text') . + $sql .= ' WHERE session_id = ' . $ilDB->quote($a_session_id, ilDBConstants::T_TEXT) . ' AND end_time IS NULL'; $ilDB->manipulate($sql); } // batch closing elseif (!$a_expired_at) { $sql = 'UPDATE usr_session_stats_raw' . - ' SET end_time = ' . $ilDB->quote(time(), 'integer'); + ' SET end_time = ' . $ilDB->quote(time(), ilDBConstants::T_INTEGER); if ($a_context) { - $sql .= ',end_context = ' . $ilDB->quote($a_context, 'integer'); + $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER); } - $sql .= ' WHERE ' . $ilDB->in('session_id', $a_session_id, false, 'text') . + $sql .= ' WHERE ' . $ilDB->in('session_id', $a_session_id, false, ilDBConstants::T_TEXT) . ' AND end_time IS NULL'; $ilDB->manipulate($sql); } @@ -111,11 +108,11 @@ public static function closeRawEntry($a_session_id, ?int $a_context = null, $a_e else { foreach ($a_session_id as $id => $ts) { $sql = 'UPDATE usr_session_stats_raw' . - ' SET end_time = ' . $ilDB->quote($ts, 'integer'); + ' SET end_time = ' . $ilDB->quote($ts, ilDBConstants::T_INTEGER); if ($a_context) { - $sql .= ',end_context = ' . $ilDB->quote($a_context, 'integer'); + $sql .= ', end_context = ' . $ilDB->quote($a_context, ilDBConstants::T_INTEGER); } - $sql .= ' WHERE session_id = ' . $ilDB->quote($id, 'text') . + $sql .= ' WHERE session_id = ' . $ilDB->quote($id, ilDBConstants::T_TEXT) . ' AND end_time IS NULL'; $ilDB->manipulate($sql); } @@ -124,18 +121,17 @@ public static function closeRawEntry($a_session_id, ?int $a_context = null, $a_e /** * Get next slot to aggregate - * - * @return array begin, end + * @return array{0: int, 1: int}|null */ - protected static function getCurrentSlot(int $a_now): ?array + private static function getCurrentSlot(int $a_now): ?array { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; // get latest slot in db - $sql = 'SELECT MAX(slot_end) previous_slot_end' . - ' FROM usr_session_stats'; + $sql = 'SELECT MAX(slot_end) previous_slot_end FROM usr_session_stats'; $res = $ilDB->query($sql); $row = $ilDB->fetchAssoc($res); $previous_slot_end = $row['previous_slot_end']; @@ -162,55 +158,53 @@ protected static function getCurrentSlot(int $a_now): ?array if ($current_slot_end < $a_now) { return [$current_slot_begin, $current_slot_end]; } + return null; } - protected static function getNumberOfActiveRawSessions(int $a_time): int + private static function getNumberOfActiveRawSessions(int $a_time): int { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; - $sql = 'SELECT COUNT(*) counter FROM usr_session_stats_raw' . - ' WHERE (end_time IS NULL OR end_time >= ' . $ilDB->quote($a_time, 'integer') . ')' . - ' AND start_time <= ' . $ilDB->quote($a_time, 'integer') . - ' AND ' . $ilDB->in('type', ilSessionControl::$session_types_controlled, false, 'integer'); - $res = $ilDB->query($sql); - $row = $ilDB->fetchAssoc($res); - return (int) $row['counter']; + return (int) $ilDB->fetchAssoc( + $ilDB->execute( + self::getNumberOfActiveRawSessionsPreparedStatement(), + [$a_time, $a_time] + ) + )['counter']; } /** - * Read raw data for timespan + * @return Generator */ - protected static function getRawData(int $a_begin, int $a_end): array + private static function getRawData(int $a_begin, int $a_end): Generator { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; - $sql = 'SELECT start_time,end_time,end_context FROM usr_session_stats_raw' . - ' WHERE start_time <= ' . $ilDB->quote($a_end, 'integer') . - ' AND (end_time IS NULL OR end_time >= ' . $ilDB->quote($a_begin, 'integer') . ')' . - ' AND ' . $ilDB->in('type', ilSessionControl::$session_types_controlled, false, 'integer') . - ' ORDER BY start_time'; - $res = $ilDB->query($sql); - $all = []; + $res = $ilDB->execute( + self::getRawDataPreparedStatement(), + [$a_end, $a_begin] + ); while ($row = $ilDB->fetchAssoc($res)) { - $all[] = $row; + yield $row; } - return $all; } /** * Create new slot (using table lock) - * - * @return array begin, end + * @return array{0: int, 1: int}|null */ - protected static function createNewAggregationSlot(int $a_now): ?array + private static function createNewAggregationSlot(int $a_now): ?array { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; $ilAtomQuery = $ilDB->buildAtomQuery(); @@ -226,8 +220,8 @@ protected static function createNewAggregationSlot(int $a_now): ?array // save slot to mark as taken $fields = [ - 'slot_begin' => ['integer', $slot[0]], - 'slot_end' => ['integer', $slot[1]], + 'slot_begin' => [ilDBConstants::T_INTEGER, $slot[0]], + 'slot_end' => [ilDBConstants::T_INTEGER, $slot[1]], ]; $ilDB->insert('usr_session_stats', $fields); }); @@ -237,10 +231,7 @@ protected static function createNewAggregationSlot(int $a_now): ?array return $slot; } - /** - * Aggregate raw session data (older than given time) - */ - public static function aggretateRaw(int $a_now): void + public static function aggregateRaw(int $a_now): void { if (!self::isActive()) { return; @@ -256,14 +247,90 @@ public static function aggretateRaw(int $a_now): void self::deleteAggregatedRaw($a_now); } - /** - * Aggregate statistics data for one slot - * - */ - public static function aggregateRawHelper(int $a_begin, int $a_end): void + private static function getNumberOfActiveRawSessionsPreparedStatement(): ilDBStatement + { + if (self::$number_of_active_raw_sessions_statement === null) { + global $DIC; + + /** @var ilDBInterface $ilDB */ + $ilDB = $DIC['ilDB']; + + self::$number_of_active_raw_sessions_statement = $ilDB->prepare( + 'SELECT COUNT(*) counter FROM usr_session_stats_raw ' + . 'WHERE (end_time IS NULL OR end_time >= ?) ' + . 'AND start_time <= ? ' + . 'AND ' . $ilDB->in('type', ilSessionControl::$session_types_controlled, false, ilDBConstants::T_INTEGER), + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER] + ); + } + + return self::$number_of_active_raw_sessions_statement; + } + + private static function getAggregatedRawDataPreparedStatement(): ilDBStatement + { + if (!self::$aggregated_raw_data_statement) { + global $DIC; + + /** @var ilDBInterface $ilDB */ + $ilDB = $DIC['ilDB']; + + self::$aggregated_raw_data_statement = $ilDB->prepareManip( + 'UPDATE usr_session_stats ' + . 'SET active_min = ?, ' + . 'active_max = ?, ' + . 'active_avg = ?, ' + . 'active_end = ?, ' + . 'opened = ?, ' + . 'closed_manual = ?, ' + . 'closed_expire = ?, ' + . 'closed_login = ?, ' + . 'closed_misc = ? ' + . 'WHERE slot_begin = ? AND slot_end = ?', + [ + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER, + ilDBConstants::T_INTEGER + ] + ); + } + + return self::$aggregated_raw_data_statement; + } + + private static function getRawDataPreparedStatement(): ilDBStatement + { + if (!self::$raw_data_statement) { + global $DIC; + + /** @var ilDBInterface $ilDB */ + $ilDB = $DIC['ilDB']; + + self::$raw_data_statement = $ilDB->prepare( + 'SELECT start_time, end_time, end_context FROM usr_session_stats_raw' . + ' WHERE start_time <= ?' . + ' AND (end_time IS NULL OR end_time >= ?)' . + ' AND ' . $ilDB->in('type', ilSessionControl::$session_types_controlled, false, ilDBConstants::T_INTEGER) . + ' ORDER BY start_time', + [ilDBConstants::T_INTEGER, ilDBConstants::T_INTEGER] + ); + } + return self::$raw_data_statement; + } + + private static function aggregateRawHelper(int $a_begin, int $a_end): void { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; // "relevant" closing types @@ -274,7 +341,8 @@ public static function aggregateRawHelper(int $a_begin, int $a_end): void ]; // gather/process data (build event timeline) - $closed_counter = $events = []; + $events = []; + $closed_counter = $events; $opened_counter = 0; foreach (self::getRawData($a_begin, $a_end) as $item) { // open/close counters are _not_ time related @@ -304,11 +372,14 @@ public static function aggregateRawHelper(int $a_begin, int $a_end): void } } - // initialising active statistical values + // initializing active statistical values $active_begin = self::getNumberOfActiveRawSessions($a_begin - 1); - $active_end = $active_min = $active_max = $active_avg = $active_begin; + $active_avg = $active_begin; + $active_max = $active_begin; + $active_min = $active_begin; + $active_end = $active_begin; - // parsing events / building avergages + // parsing events / building averages if (count($events)) { $last_update_avg = $a_begin - 1; $slot_seconds = self::SLOT_SIZE * 60; @@ -355,35 +426,29 @@ public static function aggregateRawHelper(int $a_begin, int $a_end): void } unset($events); - // save aggregated data - $fields = [ - 'active_min' => ['integer', $active_min], - 'active_max' => ['integer', $active_max], - 'active_avg' => ['integer', $active_avg], - 'active_end' => ['integer', $active_end], - 'opened' => ['integer', $opened_counter], - 'closed_manual' => ['integer', (int) ($closed_counter[ilSession::SESSION_CLOSE_USER] ?? 0)], - 'closed_expire' => ['integer', (int) ($closed_counter[ilSession::SESSION_CLOSE_EXPIRE] ?? 0)], - 'closed_login' => ['integer', (int) ($closed_counter[ilSession::SESSION_CLOSE_LOGIN] ?? 0)], - 'closed_misc' => ['integer', (int) ($closed_counter[0] ?? 0)], - ]; - $ilDB->update( - 'usr_session_stats', - $fields, + $ilDB->execute( + self::getAggregatedRawDataPreparedStatement(), [ - 'slot_begin' => ['integer', $a_begin], - 'slot_end' => ['integer', $a_end] + 'active_min' => $active_min, + 'active_max' => $active_max, + 'active_avg' => $active_avg, + 'active_end' => $active_end, + 'opened' => $opened_counter, + 'closed_manual' => (int) ($closed_counter[ilSession::SESSION_CLOSE_USER] ?? 0), + 'closed_expire' => (int) ($closed_counter[ilSession::SESSION_CLOSE_EXPIRE] ?? 0), + 'closed_login' => (int) ($closed_counter[ilSession::SESSION_CLOSE_LOGIN] ?? 0), + 'closed_misc' => (int) ($closed_counter[0] ?? 0), + 'slot_begin' => $a_begin, + 'slot_end' => $a_end ] ); } - /** - * Remove already aggregated raw data - */ - protected static function deleteAggregatedRaw(int $a_now): void + private static function deleteAggregatedRaw(int $a_now): void { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; // we are rather defensive here - 7 days BEFORE current aggregation @@ -391,31 +456,33 @@ protected static function deleteAggregatedRaw(int $a_now): void $ilDB->manipulate( 'DELETE FROM usr_session_stats_raw' . - ' WHERE start_time <= ' . $ilDB->quote($cut, 'integer') + ' WHERE start_time <= ' . $ilDB->quote($cut, ilDBConstants::T_INTEGER) ); } /** - * Get session counters by type (opened, closed) + * @return array{opened: int, closed_manual: int, closed_expire: int, closed_login: int, closed_misc: int} */ public static function getNumberOfSessionsByType(int $a_from, int $a_to): array { global $DIC; + /** @var ilDBInterface $ilDB */ $ilDB = $DIC['ilDB']; $sql = 'SELECT SUM(opened) opened, SUM(closed_manual) closed_manual,' . ' SUM(closed_expire) closed_expire,' . ' SUM(closed_login) closed_login, SUM(closed_misc) closed_misc' . ' FROM usr_session_stats' . - ' WHERE slot_end > ' . $ilDB->quote($a_from, 'integer') . - ' AND slot_begin < ' . $ilDB->quote($a_to, 'integer'); + ' WHERE slot_end > ' . $ilDB->quote($a_from, ilDBConstants::T_INTEGER) . + ' AND slot_begin < ' . $ilDB->quote($a_to, ilDBConstants::T_INTEGER); $res = $ilDB->query($sql); + return $ilDB->fetchAssoc($res); } /** - * Get active sessions aggregated data + * @return list */ public static function getActiveSessions(int $a_from, int $a_to): array { @@ -426,18 +493,16 @@ public static function getActiveSessions(int $a_from, int $a_to): array $sql = 'SELECT slot_begin, slot_end, active_min, active_max, active_avg' . ' FROM usr_session_stats' . - ' WHERE slot_end > ' . $ilDB->quote($a_from, 'integer') . - ' AND slot_begin < ' . $ilDB->quote($a_to, 'integer') . + ' WHERE slot_end > ' . $ilDB->quote($a_from, ilDBConstants::T_INTEGER) . + ' AND slot_begin < ' . $ilDB->quote($a_to, ilDBConstants::T_INTEGER) . ' ORDER BY slot_begin'; $res = $ilDB->query($sql); + $all = []; while ($row = $ilDB->fetchAssoc($res)) { - $entry = []; - foreach ($row as $key => $value) { - $entry[$key] = (int) $value; - } - $all[] = $entry; + $all[] = array_map(intval(...), $row); } + return $all; } @@ -450,12 +515,13 @@ public static function getLastAggregation(): ?int $ilDB = $DIC['ilDB']; - $sql = 'SELECT max(slot_end) latest FROM usr_session_stats'; + $sql = 'SELECT MAX(slot_end) latest FROM usr_session_stats'; $res = $ilDB->query($sql); $row = $ilDB->fetchAssoc($res); - if ($row['latest']) { + if ($row['latest'] !== null) { return (int) $row['latest']; } + //TODO check if return null as timestamp causes issues return null; } diff --git a/components/ILIAS/Authentication/classes/class.ilSessionStatisticsGUI.php b/components/ILIAS/Authentication/classes/class.ilSessionStatisticsGUI.php index f376c171789f..890937ba001e 100755 --- a/components/ILIAS/Authentication/classes/class.ilSessionStatisticsGUI.php +++ b/components/ILIAS/Authentication/classes/class.ilSessionStatisticsGUI.php @@ -736,7 +736,7 @@ protected function adminSync(): void // see ilSession::_writeData() $now = time(); ilSession::_destroyExpiredSessions(); - ilSessionStatistics::aggretateRaw($now); + ilSessionStatistics::aggregateRaw($now); $this->tpl->setOnScreenMessage('success', $this->lng->txt('trac_sync_session_stats_success'), true); $this->ilCtrl->redirect($this); diff --git a/components/ILIAS/Blog/Notification/NotificationManager.php b/components/ILIAS/Blog/Notification/NotificationManager.php index 351a0edbfec9..76628f3c5436 100644 --- a/components/ILIAS/Blog/Notification/NotificationManager.php +++ b/components/ILIAS/Blog/Notification/NotificationManager.php @@ -140,7 +140,7 @@ protected function sendSystemNotification( $notified = $ntf->sendMailAndReturnRecipients( $users, - "_" . $posting->getId(), + (string) $posting->getId(), ($admin_only ? "write" : "read") ); diff --git a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php index 0b46a7eb0160..57075b41b008 100755 --- a/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php +++ b/components/ILIAS/Blog/Posting/class.ilBlogPostingGUI.php @@ -788,6 +788,14 @@ protected function getFirstMediaObjectAsTag( $mob_obj = new ilObjMediaObject((int) $mob_id); $mob_item = $mob_obj->getMediaItem("Standard"); if (stripos($mob_item->getFormat(), "image") !== false) { + if ($mob_item->getFormat() === "image/svg+xml") { + $location = $mob_obj->getStandardSrc(); + return ''; + } $mob_size = $mob_item->getOriginalSize(); if (is_null($mob_size)) { continue; diff --git a/components/ILIAS/COPage/Editor/js/src/components/paragraph/ui/tiny-wrapper.js b/components/ILIAS/COPage/Editor/js/src/components/paragraph/ui/tiny-wrapper.js index aa390ff7e560..9690d37bf72c 100755 --- a/components/ILIAS/COPage/Editor/js/src/components/paragraph/ui/tiny-wrapper.js +++ b/components/ILIAS/COPage/Editor/js/src/components/paragraph/ui/tiny-wrapper.js @@ -212,8 +212,6 @@ export default class TinyWrapper { plugins: 'save,lists', license_key: 'gpl', smart_paste: false, - save_onsavecallback: 'saveParagraph', - mode: 'exact', selector: `#${this.id}`, content_css: this.content_css, fix_list_elements: true, @@ -226,8 +224,6 @@ export default class TinyWrapper { removeformat_selector: 'span,code', remove_linebreaks: true, convert_newlines_to_brs: false, - force_p_newlines: true, - force_br_newlines: false, /* not found in 3 docu (anymore?) */ cleanup_on_startup: true, cleanup: true, @@ -1133,7 +1129,11 @@ export default class TinyWrapper { children = dummy.childNodes; for (let k = 0; k < children.length; k++) { if (children[k].nodeName === 'P') { // paragraphs - contents.push(html.p2br(children[k].innerHTML)); + if (children[k].textContent === '') { // see #42980 + contents.push(''); + } else { + contents.push(html.p2br(children[k].innerHTML)); + } } else if (children[k].nodeType === 3) { // text nodes (seems to be only \n) // contents.push(html.p2br(children[k].textContent)); } else { diff --git a/components/ILIAS/COPage/Layout/Administration/class.ilPageLayoutAdministrationGUI.php b/components/ILIAS/COPage/Layout/Administration/class.ilPageLayoutAdministrationGUI.php index aea01ff22e0e..5a8c536657c5 100755 --- a/components/ILIAS/COPage/Layout/Administration/class.ilPageLayoutAdministrationGUI.php +++ b/components/ILIAS/COPage/Layout/Administration/class.ilPageLayoutAdministrationGUI.php @@ -238,6 +238,7 @@ public function addPageLayout(?ilPropertyFormGUI $a_form = null): void public function initAddPageLayoutForm(): ilPropertyFormGUI { $this->lng->loadLanguageModule("content"); + $this->lng->loadLanguageModule("copg"); $form_gui = new ilPropertyFormGUI(); $form_gui->setFormAction($this->ctrl->getFormAction($this)); @@ -257,7 +258,7 @@ public function initAddPageLayoutForm(): ilPropertyFormGUI // modules - $mods = new ilCheckboxGroupInputGUI($this->lng->txt("modules"), "module"); + $mods = new ilCheckboxGroupInputGUI($this->lng->txt("copg_obj_types"), "module"); // $mods->setRequired(true); foreach (ilPageLayout::getAvailableModules() as $mod_id => $mod_caption) { $mod = new ilCheckboxOption($mod_caption, $mod_id); diff --git a/components/ILIAS/COPage/Layout/classes/class.ilPageLayoutGUI.php b/components/ILIAS/COPage/Layout/classes/class.ilPageLayoutGUI.php index 07ef4ae4782d..efcc3f544fdd 100755 --- a/components/ILIAS/COPage/Layout/classes/class.ilPageLayoutGUI.php +++ b/components/ILIAS/COPage/Layout/classes/class.ilPageLayoutGUI.php @@ -17,6 +17,7 @@ *********************************************************************/ use ILIAS\UI\Component\Input\Field\Radio; +use ILIAS\Repository\Form\FormAdapterGUI; /** * Class ilPageLayoutGUI GUI class @@ -216,6 +217,33 @@ public static function getTemplateSelection(string $module, bool $include_none = return $radio; } + public static function addTemplateSelection(string $module, FormAdapterGUI $form, bool $include_none = false): FormAdapterGUI + { + global $DIC; + $ui = $DIC->ui(); + $f = $ui->factory(); + $lng = $DIC->language(); + $arr_templates = ilPageLayout::activeLayouts($module); + if (count($arr_templates) == 0) { + return $form; + } + $form = $form->radio("template_id", $lng->txt("cont_page_template")); + $radio = $f->input()->field()->radio($lng->txt("cont_page_template"), ""); + $first = "0"; + if ($include_none) { + $form = $form->radioOption("0", $lng->txt("none")); + } + /** @var ilPageLayout $templ */ + foreach ($arr_templates as $templ) { + if ($first == "0" && !$include_none) { + $first = $templ->getId(); + } + $templ->readObject(); + $form = $form->radioOption($templ->getId(), $templ->getPreview(), $templ->getTitle()); + } + return $form; + } + public function finishEditing(): void { $this->ctrl->redirectByClass("ilpagelayoutadministrationgui", "listLayouts"); diff --git a/components/ILIAS/COPage/PC/MediaObject/class.ilPCMediaObjectEditorGUI.php b/components/ILIAS/COPage/PC/MediaObject/class.ilPCMediaObjectEditorGUI.php index f3835e7b3115..92b181b8325e 100755 --- a/components/ILIAS/COPage/PC/MediaObject/class.ilPCMediaObjectEditorGUI.php +++ b/components/ILIAS/COPage/PC/MediaObject/class.ilPCMediaObjectEditorGUI.php @@ -221,6 +221,8 @@ public function getRenderedUrlForm( public function getUrlForm( ilLanguage $lng ): ilPropertyFormGUI { + global $DIC; + $media_types = $DIC->mediaObjects()->internal()->domain()->mediaType(); $form = new ilPropertyFormGUI(); $form->setShowTopButtons(false); @@ -240,8 +242,11 @@ public function getUrlForm( $form->addItem($hi3); // standard reference + $lng->loadLanguageModule("mob"); $ti = new ilTextInputGUI($lng->txt("url"), "standard_reference"); - $ti->setInfo($lng->txt("cont_url_info")); + $info = $lng->txt("mob_url_info1") . " " . implode(", ", iterator_to_array($media_types->getAllowedSuffixes())) . "."; + $info .= " " . $lng->txt("mob_url_info_video"); + $ti->setInfo($info); $ti->setRequired(true); $form->addItem($ti); diff --git a/components/ILIAS/COPage/PC/Question/QuestionManager.php b/components/ILIAS/COPage/PC/Question/QuestionManager.php index e0fc3829caa7..10f4206b9c29 100755 --- a/components/ILIAS/COPage/PC/Question/QuestionManager.php +++ b/components/ILIAS/COPage/PC/Question/QuestionManager.php @@ -47,7 +47,11 @@ public function resolveQuestionReferences( foreach ($nodes as $node) { $qref = $node->getAttribute("QRef"); if (isset($a_mapping[$qref])) { - $node->setAttribute("QRef", "il__qst_" . $a_mapping[$qref]["pool"]); + $new_id = (int) ($a_mapping[$qref]["pool"] ?? 0); + if ($new_id === 0 && isset($a_mapping[$qref]["test"])) { // changed with 10 + $new_id = $a_mapping[$qref]["test"]; + } + $node->setAttribute("QRef", "il__qst_" . $new_id); $updated = true; } } diff --git a/components/ILIAS/COPage/Page/class.PageContentManager.php b/components/ILIAS/COPage/Page/class.PageContentManager.php index 6fffc87827ed..94b8f5333757 100755 --- a/components/ILIAS/COPage/Page/class.PageContentManager.php +++ b/components/ILIAS/COPage/Page/class.PageContentManager.php @@ -668,7 +668,7 @@ public function moveContentAfter( string $a_tpcid = "" ): void { // nothing to do... - if ($a_source === $a_target) { + if ($a_source === $a_target || $a_spcid === $a_tpcid) { return; } diff --git a/components/ILIAS/COPage/Page/class.PageQueryActionHandler.php b/components/ILIAS/COPage/Page/class.PageQueryActionHandler.php index 4e29040eea21..e75693a289df 100755 --- a/components/ILIAS/COPage/Page/class.PageQueryActionHandler.php +++ b/components/ILIAS/COPage/Page/class.PageQueryActionHandler.php @@ -370,39 +370,30 @@ public function getActionsDropDown(): \ILIAS\UI\Component\Dropdown\Standard $ctrl->getLinkTargetByClass([get_class($this->page_gui), "ilnewsitemgui"], "editNews") ); } + } + if ($this->page_gui->use_meta_data) { if (($md_link = $this->page_gui->getMetaDataLink()) !== "") { $items[] = $ui->factory()->link()->standard( $lng->txt("meta_data"), $md_link ); - } - } - - if ($this->page_gui->use_meta_data) { - $mdgui = new \ilObjectMetaDataGUI( - $this->page_gui->meta_data_rep_obj, - $this->page_gui->meta_data_type, - $this->page_gui->meta_data_sub_obj_id - ); - $mdtab = $mdgui->getTab(); - if ($mdtab) { - $items[] = $ui->factory()->link()->standard( - $lng->txt("meta_data"), - $mdtab + } else { + $mdgui = new \ilObjectMetaDataGUI( + $this->page_gui->meta_data_rep_obj, + $this->page_gui->meta_data_type, + $this->page_gui->meta_data_sub_obj_id ); + $mdtab = $mdgui->getTab(); + if ($mdtab) { + $items[] = $ui->factory()->link()->standard( + $lng->txt("meta_data"), + $mdtab + ); + } } } - - if ($this->page_gui->getEnabledNews()) { - $items[] = $ui->factory()->link()->standard( - $lng->txt("news"), - $ctrl->getLinkTargetByClass([get_class($this->page_gui), \ilNewsItemGUI::class], "editNews") - ); - } - - // additional page actions foreach ($this->page_gui->getAdditionalPageActions() as $item) { $items[] = $item; diff --git a/components/ILIAS/COPage/classes/class.ilPageLinker.php b/components/ILIAS/COPage/classes/class.ilPageLinker.php index 51d164e4a204..f1330627497a 100755 --- a/components/ILIAS/COPage/classes/class.ilPageLinker.php +++ b/components/ILIAS/COPage/classes/class.ilPageLinker.php @@ -28,6 +28,7 @@ class ilPageLinker implements \ILIAS\COPage\PageLinker protected string $profile_back_url = ""; protected ilCtrl $ctrl; protected string $cmd_gui; + protected \ILIAS\StaticURL\Services $static_url; public function __construct( string $cmd_gui_class, @@ -44,6 +45,7 @@ public function __construct( $this->ctrl = (is_null($ctrl)) ? $DIC->ctrl() : $ctrl; + $this->static_url = $DIC["static_url"]; } public function setOffline(bool $offline = true): void @@ -112,10 +114,18 @@ public function getLinkXML(array $int_links): string case "PageObject": case "StructureObject": $lm_id = ilLMObject::_lookupContObjID($target_id); - if ($type == "PageObject") { - $href = "./goto.php?target=pg_" . $target_id . $anc_add; + if ($type === "PageObject") { + $href = (string) $this->static_url->builder()->build( + "pg", + null, + [$target_id] + ) . $anc_add; } else { - $href = "./goto.php?target=st_" . $target_id; + $href = (string) $this->static_url->builder()->build( + "st", + null, + [$target_id] + ) . $anc_add; } if ($lm_id == "") { $href = ""; @@ -159,8 +169,14 @@ public function getLinkXML(array $int_links): string case "RepositoryItem": $obj_type = ilObject::_lookupType((int) $target_id, true); - $obj_id = ilObject::_lookupObjId((int) $target_id); - $href = "./goto.php?target=" . $obj_type . "_" . $target_id; + if ((int) $target_id > 0) { + $href = (string) $this->static_url->builder()->build( + $obj_type, + new \ILIAS\Data\ReferenceId($target_id) + ); + } else { + $href = "#"; + } break; case "File": diff --git a/components/ILIAS/COPage/classes/class.ilPageObject.php b/components/ILIAS/COPage/classes/class.ilPageObject.php index c845f460a2f8..92a0d77988ce 100755 --- a/components/ILIAS/COPage/classes/class.ilPageObject.php +++ b/components/ILIAS/COPage/classes/class.ilPageObject.php @@ -75,7 +75,7 @@ abstract class ilPageObject public string $xml = ""; public string $encoding = ""; public DomNode $node; - public string $cur_dtd = "ilias_pg_9.dtd"; + public string $cur_dtd = "ilias_pg_12.dtd"; public bool $contains_int_link = false; public bool $needs_parsing = false; public string $parent_type = ""; @@ -2369,6 +2369,7 @@ public static function getParentObjectContributors( global $DIC; $db = $DIC->database(); + $profile = $DIC->copage()->internal()->domain()->profile(); $and_lang = ""; if ($a_lang != "") { @@ -2411,7 +2412,7 @@ public static function getParentObjectContributors( $c = array(); foreach ($contributors as $k => $co) { - if (ilObject::_lookupType($k) == "usr") { + if ($profile->exists($k)) { $name = ilObjUser::_lookupName($k); $c[] = array("user_id" => $k, "pages" => $co, diff --git a/components/ILIAS/COPage/css/content.css b/components/ILIAS/COPage/css/content.css index c78b7bd65b5c..badb230ae15f 100755 --- a/components/ILIAS/COPage/css/content.css +++ b/components/ILIAS/COPage/css/content.css @@ -1,1912 +1,1552 @@ /* this should be derived from zip in libs/ilias/Style */ - -@image_path: '../basic_style/images/'; - -a.ilc_qetitem_ErrorTextItem -{ - padding: 2px; - text-decoration: none; -} - -a.ilc_qetitem_ErrorTextItem:hover -{ - color: #000000; - text-decoration: none; - background-color: #D0D0D0; -} - -a.ilc_qetitem_ErrorTextSelected -{ - border-style: none; - background-color: #E2E8EF; - color: #161616; -} - -a.ilc_link_ExtLink -{ - text-decoration: underline; -} - -a.ilc_link_FileLink -{ - text-decoration: underline; -} - -a.ilc_link_GlossaryLink -{ - text-decoration: underline; -} - -a.ilc_glo_ovclink_GlossaryOvCloseLink -{ - text-decoration: underline; - font-weight: normal; -} - -a.ilc_glo_ovuglistlink_GlossaryOvUGListLink -{ - font-weight: normal; -} - -a.ilc_glo_ovuglink_GlossaryOvUnitGloLink -{ - font-weight: normal; -} - -a.ilc_qimgd_ImageDetailsLink -{ - font-size: 90%; -} - -a.ilc_link_IntLink -{ - text-decoration: underline; -} - -a.ilc_marker_Marker -{ - background-image: url("@{image_path}icon_pin.svg"); - background-repeat: no-repeat; - cursor: pointer; - display: block; - height: 32px; - width: 24px; - position: absolute; -} - -a.ilc_marker_Marker:hover -{ - background-repeat: no-repeat; - background-image: url("@{image_path}icon_pin_on.svg"); -} - - - -a.ilc_rte_mlink_RTELink -{ - font-size: 100%; - margin: 1px; - margin-top: 10px; - margin-right: 5px; - margin-bottom: 10px; - margin-left: 5px; - padding-right: 3mm; - padding-top: 1mm; - border-style: none; - padding-bottom: 1mm; - text-decoration: none; - padding-left: 3mm; - border-width: 1px; - background-color: #4C6586; - text-align: center; - height: 50px; - color: #ffffff !important; - text-decoration: none !important; -} - -a.ilc_rte_mlink_RTELink:hover -{ - background-color: #577399; - color: #FFFFFF; - text-decoration: none !important; -} - -a.ilc_rte_mlink_RTELinkDisabled -{ -} - -a.ilc_rte_mlink_RTELinkDisabled:hover -{ -} - -a.ilc_rte_texp_RTETreeCollapsed -{ - background-image: url("@{image_path}arrow_right.svg"); - background-repeat: no-repeat; - background-position: 0px 0px; - background-size: 15px 15px !important; -} - -a.ilc_rte_tclink_RTETreeControlLink -{ - color: #4C6586; - color: #4C6586 !important; -} - -a.ilc_rte_tlink_RTETreeCurrent -{ - background-color: #FFFF9D; - color: #161616 !important; - background-color: #E2E8EF !important; -} - -a.ilc_rte_texp_RTETreeExpanded -{ - background-image: url("@{image_path}arrow_down.svg"); - background-size: 15px 15px; -} - -a.ilc_rte_tlink_RTETreeLink -{ - color: #4C6586; - color: #4C6586 !important; -} - -div.ilc_va_cntr_AccordCntr -{ - margin-top: 5px; -} - -div.ilc_va_icntr_AccordICntr -{ - margin-bottom: 5px; -} - -div.ilc_va_icont_AccordICont -{ - margin-top: -1px; - margin-bottom: 12px; - padding-top: 3px; - padding-right: 3px; - padding-bottom: 3px; - padding-left: 15px; - border-width: 1px; - border-color: #DDDDDD; - border-style: solid; - background-color: #FFFFFF; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} - -div.ilc_va_ihead_AccordIHead -{ - text-align: left; - margin-bottom: 12px; - padding-top: 8px; - padding-right: 8px; - padding-bottom: 8px; - padding-left: 38px; - border-width: 1px; - border-color: #DDDDDD; - border-style: solid; - background-color: #F9F9F9; - background-image: url("@{image_path}arrow_right.svg"); - background-repeat: no-repeat; - background-position: 15px center; - cursor: pointer; - background-size: 20px 20px; - border-radius: 3px; - font-weight: 600; -} - -div.ilc_va_ihead_AccordIHead:hover -{ - background-color: #E2E8EF; -} - -div.ilc_va_ihcap_AccordIHeadCap -{ - font-size: 1em; - font-weight: 600; -} - -div.ilc_section_Additional -,a.ilc_section_Additional -{ - padding-left: 20px; - margin-top: 40px; - margin-bottom: 10px; - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - padding-top: 35px; - padding-right: 30px; - padding-bottom: 15px; - border-radius: 3px; - position: relative !important; - background-position: left top; -} - -div.ilc_section_Additional::before -,a.ilc_section_Additional::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #eceff4; - background-position: center center; - left: 15px; - background-image: url("@{image_path}additional.svg"); - background-repeat: no-repeat; - position: absolute; - width: 60px; - height: 60px; - top: -32px; - right: 0px; - content: ""; - display: block; - border-radius: 50px; -} - -div.ilc_section_AdvancedKnowledge -,a.ilc_section_AdvancedKnowledge -{ - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - padding-left: 20px; - padding-right: 30px; - padding-top: 35px; - padding-bottom: 15px; - margin-top: 40px; - margin-bottom: 10px; - background-position: left top; - border-radius: 3px; - position: relative !important; -} - -div.ilc_section_AdvancedKnowledge::before -,a.ilc_section_AdvancedKnowledge::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #eceff4; - background-position: center center; - left: 15px; - background-image: url("@{image_path}advknowledge.svg"); - position: absolute; - top: -32px; - right: 0px; - content: ""; - width: 60px; - display: block; - height: 60px; - border-radius: 50px; - background-repeat: no-repeat; -} - -div.ilc_qanswer_Answer -{ - padding-right: 10px; - border-radius: 5px; -} - -div.ilc_section_Attention -,a.ilc_section_Attention -{ - position: relative; - border-width: 2px; - border-color: #FA8228; - border-style: solid; - border-radius: 3px; - position: relative !important; - padding-left: 20px; - padding-bottom: 15px; - padding-right: 30px; - padding-top: 35px; - margin-bottom: 10px; - margin-top: 40px; -} - -div.ilc_section_Attention::before -,a.ilc_section_Attention::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #fee6d4; - background-position: center 12px; - left: 15px; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}attention.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; -} - -div.ilc_section_Background -,a.ilc_section_Background -{ - background-color: #F9F9F9; - padding-top: 10px; - margin-bottom: 20px; - padding-right: 20px; - padding-bottom: 10px; - padding-left: 20px; -} - -div.ilc_section_Block -,a.ilc_section_Block -{ - padding-bottom: 10px; - padding-right: 20px; - margin-bottom: 10px; - padding-top: 10px; - margin-top: 20px; - padding-left: 20px; -} - - -div.ilc_section_Button:hover -,a.ilc_section_Button:hover -{ -} - -div.ilc_section_Card -,a.ilc_section_Card -{ - min-height: 250px; - position: relative; - margin: 15px; - padding-top: 15px; - padding-right: 15px; - padding-bottom: 15px; - padding-left: 15px; - box-shadow: 5px 5px 40px rgba(0,0,0,.2); - border-radius: 15px; - max-width: 400px; -} - -div.ilc_section_Citation -,a.ilc_section_Citation -{ - padding-right: 30px; - padding-top: 35px; - margin-bottom: 10px; - margin-top: 40px; - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - background-color: #FFFFFF; - border-radius: 3px; - position: relative !important; - padding-left: 20px; - padding-bottom: 15px; -} - -div.ilc_section_Citation::before -,a.ilc_section_Citation::before -{ - background-color: #eceff4; - left: 15px; - background-position: center center; - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}citation.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; -} - -div.ilc_va_ihead_ColoredAccordIHead:hover -{ - background-color: #3d506b; - cursor: pointer; - transition: all 0.5s ease-out; -} - -div.ilc_section_Confirmation -,a.ilc_section_Confirmation -{ - position: relative; - border-width: 2px; - border-color: #6EA03C; - border-style: solid; - border-radius: 3px; - position: relative !important; - margin-top: 40px; - margin-bottom: 10px; - padding-top: 35px; - padding-bottom: 15px; - padding-right: 30px; - padding-left: 20px; -} - -div.ilc_section_Confirmation::before -,a.ilc_section_Confirmation::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-position: center center; - left: 15px; - background-color: #e9f3df; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}confirmation.svg"); - position: absolute; - top: -32px; - width: 60px; - height: 60px; - background-repeat: no-repeat; -} - -div.ilc_iim_ContentPopup -{ - padding-left: 10px; - padding-bottom: 5px; - padding-right: 10px; - padding-top: 5px; - border-style: solid; - border-width: 1px; - background-color: #FFFFFF; - border-color: #d8d8d8; -} - -div.ilc_qover_Correct -{ - padding-top: 20px; - padding-right: 20px; - background-image: url("@{image_path}correct.svg"); - padding-bottom: 20px; - padding-left: 60px; - background-position: 10px; - background-repeat: no-repeat; - border-style: none; - background-color: #f0f7ea; - margin-bottom: 10px; - border-radius: 3px; - margin-top: 10px; -} - -div.ilc_va_cntr_EmphasisedAccordCntr -{ - padding-top: 5px; - padding-right: 15px; - padding-left: 15px; -} - -div.ilc_va_icont_EmphasisedAccordICont -{ - padding: 15px; - background-color: #eceff4; - transform: scale(1.01); - -webkit-transform: scale(1.01); - -moz-transform: scale(1.01); - -ms-transform: scale(1.01); - transition: all 0.5s ease-out; -} - -div.ilc_va_ihead_EmphasisedAccordIHead -{ - font-size: 1.5em; - color: #FFFFFF; - padding: 15px; - background-color: #4C6586; - background-image: url("@{image_path}expand_white.svg"); - background-repeat: no-repeat; - background-position: right center; - transition: all 0.5s ease-out; -} - -div.ilc_va_ihead_EmphasisedAccordIHead:hover -{ - background-color: #3d506b; - transition: all 0.5s ease-out; -} - -div.ilc_section_Example -,a.ilc_section_Example -{ - padding-right: 30px; - padding-bottom: 15px; - padding-left: 20px; - background-position: left center; - margin-bottom: 10px; - margin-top: 40px; - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - border-radius: 3px; - position: relative !important; - padding-top: 35px; -} - -div.ilc_section_Example::before -,a.ilc_section_Example::before -{ - background-color: #eceff4; - background-position: center center; - left: 15px; - background-image: url("@{image_path}example.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - right: 0px; - content: ""; - display: block; - border-radius: 50px; -} - -div.ilc_section_Excursus -,a.ilc_section_Excursus -{ - padding-right: 30px; - margin-bottom: 10px; - margin-top: 40px; - border-style: solid; - border-width: 2px; - position: relative; - padding-top: 35px; - border-color: #4C6586; - padding-left: 20px; - border-radius: 3px; - position: relative !important; - padding-bottom: 15px; -} - -div.ilc_section_Excursus::before -,a.ilc_section_Excursus::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #eceff4; - background-position: center center; - left: 15px; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}excursus.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; -} - -div.ilc_qfeedr_FeedbackRight -{ - margin-top: 10px; - padding-top: 20px; - margin-bottom: 10px; - border-style: none; - background-color: #f0f7ea; - padding-left: 60px; - padding-bottom: 20px; - padding-right: 160px; - background-image: url("@{image_path}correct.svg"); - background-position: 10px; - background-repeat: no-repeat; - border-radius: 3px; -} - -div.ilc_qfeedw_FeedbackWrong -{ - padding-left: 60px; - padding-bottom: 20px; - background-position: 24px; - padding-right: 160px; - background-image: url("@{image_path}exclamation.svg"); - margin-top: 10px; - background-repeat: no-repeat; - padding-top: 20px; - margin-bottom: 10px; - border-style: none; - background-color: #fef2ea; - border-radius: 3px; -} - -div.ilc_flist_cont_FileListContainer -{ - border-radius: 3px; - color: #FFFFFF; - margin-top: 10px; - margin-right: 0px; - margin-bottom: 10px; - margin-left: 0px; - border-color: #4C6586; - border-style: solid; - border-width: 1px; - text-indent: 7px; - padding-bottom: 10px; -} - -div.ilc_flist_head_FileListHeading -{ - margin-bottom: 10px; - text-align: left; - color: #FFFFFF; - font-weight: bold; - background-image: url("@{image_path}download.svg"); - background-color: #4C6586; - margin-top: 0px; - background-position: 10px; - background-repeat: no-repeat; - padding-left: 50px; - padding-bottom: 10px; - padding-right: 20px; - padding-top: 10px; -} - -div.ilc_sco_fmess_FinalMessage -{ - margin: 100px; - text-align: center; - border-style: none; - border-width: 1px; - padding: 50px; - font-size: 125%; -} - -div.ilc_page_fn_Footnote -{ - margin-bottom: 5px; - margin-top: 5px; -} - -div.ilc_glo_overlay_GlossaryOverlay -{ - border-style: none; - padding-top: 5px; - border-width: 2px; - padding-left: 10px; - padding-bottom: 5px; - padding-right: 10px; -} - -div.ilc_ha_ihead_HAccordIHead:hover -{ - background-color: #E2E8EF; -} - -div.ilc_qover_Incorrect -{ - padding-left: 60px; - background-image: url("@{image_path}exclamation.svg"); - background-repeat: no-repeat; - background-position: 24px; - padding-bottom: 20px; - padding-right: 20px; - margin-bottom: 10px; - margin-top: 10px; - background-color: #fef2ea; - border-style: none; - padding-top: 20px; - border-radius: 3px; -} - -div.ilc_section_Information -,a.ilc_section_Information -{ - margin-bottom: 10px; - margin-top: 40px; - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - border-radius: 3px; - position: relative !important; - padding-left: 20px; - padding-bottom: 15px; - padding-right: 30px; - padding-top: 35px; -} - -div.ilc_section_Information::before -,a.ilc_section_Information::before -{ - background-color: #eceff4; - background-position: center center; - left: 15px; - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-image: url("@{image_path}information.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; - content: ""; - display: block; - border-radius: 50px; -} - -div.ilc_section_Interaction -,a.ilc_section_Interaction -{ - margin-top: 10px; - padding-top: 20px; - margin-bottom: 10px; - padding-right: 20px; - padding-bottom: 20px; - background-image: url("@{image_path}interaction.svg"); - padding-left: 60px; - background-position: 12px; - background-repeat: no-repeat; - border-width: 1px; - border-color: #4C6586; - border-style: solid; - border-radius: 3px; -} - -div.ilc_section_Interaction:hover -,a.ilc_section_Interaction:hover -{ - cursor: pointer; - background-color: #E2E8EF; -} - - -div.ilc_va_icont_LightAccordICont -{ - padding: 10px; - border-width: 4px; - border-color: #4C6586; - border-bottom-style: solid; -} - -div.ilc_va_ihead_LightAccordIHead -{ - color: #4C6586; - padding: 10px; - border-width: 2px; - border-color: #4C6586; - border-bottom-style: solid; - background-image: url("@{image_path}expand.svg"); - background-position: right center; - background-repeat: no-repeat; - font-size: 1.5em; - background-color: #FFFFFF; - transition: background-image 2s ease-in-out; -} - -div.ilc_va_ihead_LightAccordIHead:hover -{ - cursor: pointer; - border-width: 4px; - border-color: #4C6586; - border-bottom-style: solid; - transition: all 0.5s ease-out; -} - -div.ilc_section_Link -,a.ilc_section_Link -{ - text-decoration: underline; - margin-top: 10px; - margin-bottom: 10px; - padding-top: 20px; - padding-right: 20px; - padding-bottom: 20px; - padding-left: 60px; - border-width: 1px; - border-color: #4C6586; - border-style: dashed; - background-image: url("@{image_path}link.svg"); - background-repeat: no-repeat; - background-position: 10px; - border-radius: 3px; -} - -div.ilc_section_Literature -,a.ilc_section_Literature -{ - margin-top: 40px; - margin-bottom: 10px; - padding-top: 35px; - padding-right: 30px; - padding-bottom: 15px; - padding-left: 20px; - position: relative; - border-width: 2px; - border-color: #4C6586; - border-style: solid; - border-radius: 3px; - position: relative !important; -} - -div.ilc_section_Literature::before -,a.ilc_section_Literature::before -{ - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #eceff4; - background-position: center center; - left: 15px; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}literature.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; -} - -div.ilc_media_caption_MediaCaption -{ - font-weight: bolder; - font-size: 100%; - padding: 10px; - background-color: #F9F9F9; -} - -div.ilc_section_Mnemonic -,a.ilc_section_Mnemonic -{ - padding-right: 30px; - padding-bottom: 15px; - padding-left: 20px; - background-position: left center; - border-style: solid; - padding-top: 35px; - position: relative; - border-width: 2px; - border-color: #F3DE2C; - border-radius: 3px; - position: relative !important; - margin-top: 40px; - margin-bottom: 10px; -} - -div.ilc_section_Mnemonic::before -,a.ilc_section_Mnemonic::before -{ - border-width: 2px; - border-color: #FFFFFF; - background-color: #fdfadf; - background-position: center center; - left: 15px; - border-style: solid; - content: ""; - display: block; - border-radius: 50px; - background-image: url("@{image_path}mnemonic.svg"); - background-repeat: no-repeat; - position: absolute; - top: -32px; - width: 60px; - height: 60px; -} - -div.ilc_page_Page -{ - min-height: 300px; -} - -div.ilc_page_cont_PageContainer -{ - border-width: 1px; - border-color: #DDDDDD; - padding: 20px; - margin: 0px; - border-style: solid; - background-color: #FFFFFF; - min-height: 500px; -} - -div.ilc_section_Remark -,a.ilc_section_Remark -{ - border-style: solid; - border-width: 2px; - position: relative; - border-color: #4C6586; - border-radius: 3px; - position: relative !important; - padding-right: 30px; - padding-top: 35px; - padding-left: 20px; - padding-bottom: 15px; - margin-top: 40px; - margin-bottom: 10px; -} - -div.ilc_section_Remark::before -,a.ilc_section_Remark::before -{ - border-width: 2px; - border-color: #FFFFFF; - background-image: url("@{image_path}remark.svg"); - border-style: solid; - background-repeat: no-repeat; - background-color: #eceff4; - position: absolute; - background-position: center center; - top: -32px; - left: 15px; - width: 60px; - content: ""; - height: 60px; - display: block; - border-radius: 50px; -} - - -div.ilc_rte_menu_RTELinkBar -{ - border-top-width: 10px; - border-bottom-width: 10px; - margin-top: 10px; - margin-bottom: 10px; -} - -div.ilc_rte_menu_RTELogo -{ - float: left; -} - -div.ilc_rte_menu_RTEMenu -{ - background-color: #FFFFFF; -} - -div.ilc_section_Separator -,a.ilc_section_Separator -{ - margin-top: 10px; - margin-bottom: 10px; - padding-top: 20px; - border-width: 2px; - padding-bottom: 20px; - border-color: #4C6586; - border-top-style: solid; -} - -div.ilc_section_Special -,a.ilc_section_Special -{ - padding-left: 20px; - border-style: none; - border-width: 1px; - padding-bottom: 20px; - padding-top: 20px; - padding-right: 20px; -} - -div.ilc_question_Standard -{ - padding-left: 30px; - padding-bottom: 60px; - box-shadow: 0 3px 3px rgba(0,0,0,0.10), 0 6px 15px rgba(0,0,0,0.12); - - background-position: right top; - margin-top: 20px; - margin-bottom: 10px; - padding-top: 30px; - padding-right: 30px; -} - -div.ilc_qover_StatusMessage -{ - padding-bottom: 7px; -} - -div.ilc_qtitle_Title -{ - margin-bottom: 20px; - font-size: 140%; - font-weight: normal; -} - -div.ilc_sco_title_Title -{ - border-bottom-style: solid; - border-bottom-width: 2px; - padding-bottom: 3px; - font-size: 140%; - margin-bottom: 20px; - font-weight: bold; - margin-top: 5px; -} - - - -em.ilc_em_Emph, span.ilc_text_inline_Emph -{ - font-style: italic; -} - -figure.ilc_media_cont_MediaContainer -{ - margin: 0px; -} - -figure.ilc_media_cont_MediaContainerFull100 -{ - margin: 0px; - width: 100%; - box-shadow: 0 3px 3px rgba(0,0,0,0.10), 0 6px 15px rgba(0,0,0,0.12); -} - -figure.ilc_media_cont_MediaContainerHighlighted -{ - margin: 0px; - box-shadow: 0 3px 3px rgba(0,0,0,0.10), 0 6px 15px rgba(0,0,0,0.12); -} - -figure.ilc_media_cont_MediaContainerMax50 -{ - margin: 0px; - box-shadow: 0 3px 3px rgba(0,0,0,0.10), 0 6px 15px rgba(0,0,0,0.12); - max-width: 50%; -} - -figure.ilc_media_cont_MediaContainerSeparated -{ - margin: 40px; -} - -h1.ilc_glo_ovtitle_GlossaryOvTitle -,div.ilc_text_block_GlossaryOvTitle -,html.il-no-tiny-bg body#tinymce.ilc_text_block_GlossaryOvTitle p -{ - margin-bottom: 10px; - font-size: 1.5em; - margin-top: 10px; - font-weight: 600; -} - -h1.ilc_heading1_Headline1 -,div.ilc_text_block_Headline1 -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline1 p -{ - padding-top: 10px; - margin-bottom: 15px; - margin-top: 20px; - font-size: 1.75em; - font-weight: 600; -} - -h1.ilc_page_title_PageTitle -,div.ilc_text_block_PageTitle -,html.il-no-tiny-bg body#tinymce.ilc_text_block_PageTitle p -{ - font-weight: 600; - border-bottom-width: 1px; - border-bottom-style: none; - border-style: none; - font-size: 1.75rem; - padding-bottom: 3px; - margin-top: 15px; - margin-bottom: 15px; - text-align: left; - white-space: normal; -} - -h2.ilc_heading2_Headline2 -,div.ilc_text_block_Headline2 -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline2 p -{ - font-weight: 600; - font-size: 1.5em; - margin-top: 20px; - margin-bottom: 15px; -} - -h3.ilc_heading3_Headline3 -,div.ilc_text_block_Headline3 -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline3 p -{ - font-weight: 600; - margin-top: 15px; - margin-bottom: 10px; - font-size: 1.115em; -} - - - -img.ilc_qimg_QuestionImage -{ - margin: 10px; - box-shadow: 0 3px 3px rgba(0,0,0,0.10), 0 6px 15px rgba(0,0,0,0.12); -} - - -input.ilc_qsubmit_Submit -{ - border-style: none; - color: #ffffff !important; - padding: 0.5em; - margin-top: 20px; - background-color: #4C6586; -} - -input.ilc_qsubmit_Submit:hover -{ - cursor: pointer; - background-color: #314157; -} - -input.ilc_qinput_TextInput -{ - padding-left: 4px; -} - -li.ilc_list_item_Attention -{ - margin-bottom: 15px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 45px; - position: relative; - list-style-type: none; -} - -li.ilc_list_item_Attention::before -{ - background-image: url("@{image_path}attention_red.svg"); - background-repeat: no-repeat; - position: absolute; - width: 35px; - height: 35px; - left: 0; - top: 0; - content: ""; -} - -li.ilc_list_item_Checklist -{ - list-style-type: none; - margin-bottom: 15px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 45px; - position: relative; -} - -li.ilc_list_item_Checklist2::before -{ - background-image: url("@{image_path}wrong.svg"); - background-repeat: no-repeat; - position: absolute; - width: 35px; - height: 35px; - left: 0; - top: 0; - content: ""; -} - -li.ilc_list_item_Checklist::before -{ - content: ""; - background-image: url("@{image_path}checked.svg"); - background-repeat: no-repeat; - width: 35px; - height: 35px; - position: absolute; - left: 0; - top: 0; -} - -li.ilc_list_item_ColoredCircle::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #59A0A5; - position: absolute; - width: 35px; - height: 35px; - content: counter(section); - border-radius: 50px; - left: 0; - display: grid; - justify-content: center; - align-items: center; - top: 0; -} - -li.ilc_list_item_ColoredCircleBackground::before -{ - font-size: 1.5rem; - font-weight: bold; - color: #FFFFFF; - margin-left: 10px; - background-color: #4C6586; - position: absolute; - width: 50px; - height: 50px; - content: counter(section); - border-radius: 50px; - left: 0; - top: 13px; - display: grid; - align-items: center; - justify-content: center; -} - -li.ilc_list_item_ColoredSquare::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #F3DE2C; - position: absolute; - width: 35px; - height: 35px; - content: counter(section); - border-radius: 6px; - left: 0; - display: grid; - justify-content: center; - align-items: center; - top: 0; -} - -li.ilc_list_item_ColoredSquareBackground::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #F3DE2C; - position: absolute; - width: 35px; - height: 100%; - text-align: center; - content: counter(section); - left: 0; - top: 0; - display: grid; - justify-content: center; - align-items: center; -} - -li.ilc_flist_li_FileListItem -{ - padding-left: 3px; - padding-right: 3px; - padding-top: 5px; - padding-bottom: 5px; -} - -li.ilc_list_item_FilledCircle -{ - margin-bottom: 15px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 45px; - position: relative; - list-style-type: none; - counter-increment: section 1; -} - -li.ilc_list_item_FilledCircle::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #59A0A5; - position: absolute; - height: 35px; - min-width: 35px; - content: counter(section); - border-radius: 50px; - left: 0; - display: grid; - justify-content: center; - align-items: center; - top: 0; -} - -li.ilc_list_item_FilledCircleLarge -{ - font-size: 1.5rem; - position: relative; - padding-top: 20px; - padding-right: 20px; - padding-bottom: 20px; - padding-left: 70px; - list-style-type: none; - counter-increment: section 1; -} - -li.ilc_list_item_FilledCircleLarge::before -{ - font-weight: bold; - color: #FFFFFF; - margin-left: 10px; - position: absolute; - height: 50px; - background-color: #4C6586; - content: counter(section); - min-width: 50px; - border-radius: 50px; - left: 0; - top: 13px; - display: grid; - align-items: center; - justify-content: center; -} - -li.ilc_list_item_FilledSquare -{ - margin-bottom: 15px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 45px; - position: relative; - list-style-type: none; - counter-increment: section 1; -} - -li.ilc_list_item_FilledSquare::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #F3DE2C; - position: absolute; - height: 35px; - content: counter(section); - min-width: 35px; - border-radius: 6px; - left: 0; - display: grid; - justify-content: center; - align-items: center; - top: 0; -} - -li.ilc_list_item_FilledSquareBackground -{ - margin-top: -2px; - padding-top: 15px; - padding-bottom: 15px; - padding-left: 50px; - border-width: 2px; - border-color: #FFFFFF; - border-style: solid; - background-color: #fdfadf; - list-style-type: none; - counter-increment: section 1; - position: relative; -} - -li.ilc_list_item_FilledSquareBackground::before -{ - background-color: #F3DE2C; - font-weight: bold; - color: #FFFFFF; - min-width: 35px; - display: grid; - justify-content: center; - align-items: center; - content: counter(section); - left: 0; - top: 0; - position: absolute; - height: 100%; -} - -li.ilc_list_item_ListItem2::before -{ - width: 35px; - height: 35px; - color: #FFFFFF; - background-color: #59A0A5; - font-weight: bold; - margin-right: 10px; - content: counter(section); - display: inline-flex; - border-radius: 50px; - justify-content: center; - align-items: center; -} - -li.ilc_list_item_ListItem2Absolut::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #59A0A5; - width: 35px; - height: 35px; - position: absolute; - content: counter(section); - border-radius: 50px; - left: 0; - display: grid; - justify-content: center; - align-items: center; -} - -li.ilc_list_item_ListItem2Grid::before -{ - font-weight: bold; - color: #FFFFFF; - background-color: #59A0A5; - width: 35px; - height: 35px; - content: counter(section); - border-radius: 50px; - display: inline-grid; - justify-content: center; - align-items: center; -} - -li.ilc_list_item_ListItem3::before -{ - background-image: url("@{image_path}checked.svg"); - background-repeat: no-repeat; - width: 35px; - height: 35px; - margin-right: 5px; - display: inline-flex; - content: ""; -} - -li.ilc_list_item_ListItem4::before -{ - background-image: url("@{image_path}wrong.svg"); - background-repeat: no-repeat; - width: 35px; - height: 35px; - margin-right: 5px; - display: inline-flex; - content: ""; -} - -li.ilc_list_item_ListItem::before -{ - font-size: 1.5rem; - font-weight: bold; - color: #FFFFFF; - background-color: #4C6586; - width: 50px; - height: 50px; - vertical-align: middle; - margin-left: 10px; - position: absolute; - content: counter(section); - display: grid; - border-radius: 50px; - align-items: center; - justify-content: center; - left: 0; - top: 13px; -} - -li.ilc_qordli_OrderListItem -{ - margin-top: 5px; - margin-bottom: 5px; - margin-left: 0px; - margin-right: 0px; - cursor: move; - padding: 10px; - background-color: #E2E8EF; -} - -li.ilc_qordli_OrderListItemHorizontal -{ - float: left; - margin-top: 5px; - margin-bottom: 5px; - margin-right: 10px; - padding: 10px; - cursor: move; - background-color: #E2E8EF; -} - -li.ilc_list_item_Pointer -{ - margin-bottom: 15px; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 45px; - position: relative; - list-style-type: none; -} - -li.ilc_list_item_Pointer::before -{ - background-image: url("@{image_path}pointer.svg"); - background-repeat: no-repeat; - position: absolute; - width: 35px; - height: 35px; - left: 0; - top: 0; - content: ""; -} - -ol.ilc_list_o_NumberedListNoStyle -{ - list-style-type: none; - margin-top: 15px; - padding-top: 20px; - padding-bottom: 20px; - counter-reset: section; -} - -p.ilc_text_block_Book -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Book p -{ - hyphens: auto; - text-align: justify; -} - -p.ilc_text_block_Button:hover -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Button:hover p -{ - background-color: #5774A0; -} - -p.ilc_text_block_List -,html.il-no-tiny-bg body#tinymce.ilc_text_block_List p -{ - margin-top: 3px; - margin-bottom: 3px; -} - -p.ilc_text_block_Numbers -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Numbers p -{ - text-align: right; -} - -p.ilc_text_block_Standard -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Standard p -{ - margin-bottom: 10px; - margin-top: 10px; -} - -p.ilc_text_block_StandardLarge -,html.il-no-tiny-bg body#tinymce.ilc_text_block_StandardLarge p -{ - font-size: 1.115rem; -} - -p.ilc_text_block_TableContent -,html.il-no-tiny-bg body#tinymce.ilc_text_block_TableContent p -{ - line-height: 1.8em; - padding-bottom: 10px; - padding-top: 10px; - padding-left: 10px; - padding-right: 10px; - margin-top: 0px; - margin-bottom: 0px; - margin-right: 0px; - margin-left: 0px; -} - -p.ilc_text_block_Verse -,html.il-no-tiny-bg body#tinymce.ilc_text_block_Verse p -{ - text-align: center; -} - -pre.ilc_code_block_Code -{ - border-width: 1px; - border-color: #DDDDDD; - border-style: solid; - padding: 9px; - background-color: #F9F9F9; -} - -span.ilc_text_inline_Accent -{ - color: #087e0a; -} - -span.ilc_text_inline_Attention -{ - color: #CB4D16; -} - -span.ilc_text_inline_Comment -{ - color: #b31bff; -} - -span.ilc_qetcorr_ErrorTextCorrected -{ - text-decoration: line-through; -} - -span.ilc_text_inline_Important -{ - text-decoration: underline; -} - -span.ilc_text_inline_Mnemonic -{ - background-color: #fcf7ca; - padding-left: 4px; - padding-right: 4px; -} - -span.ilc_text_inline_Quotation -{ - letter-spacing: 0.2px; - font-style: italic; - font-family: serif; - color: #1B5FFF; - font-size: 114%; -} - -strong.ilc_strong_Strong, span.ilc_text_inline_Strong -{ - font-weight: bold; -} - -table.ilc_table_StandardTable -{ - border-collapse: collapse; - margin-top: 10px; - margin-bottom: 10px; - border-color: #9EADBA; -} - -td.ilc_table_cell_Cell1 -,th.ilc_table_cell_Cell1 -{ - padding-top: 3px; - padding-right: 10px; - padding-bottom: 3px; - padding-left: 10px; - background-color: #FFCCCC; -} - -td.ilc_table_cell_Cell2 -,th.ilc_table_cell_Cell2 -{ - background-color: #CCCCFF; - padding-top: 3px; - padding-right: 10px; - padding-bottom: 3px; - padding-left: 10px; -} - -td.ilc_table_cell_Cell3 -,th.ilc_table_cell_Cell3 -{ - padding-left: 10px; - padding-bottom: 3px; - padding-right: 10px; - padding-top: 3px; - background-color: #CCFFCC; -} - -td.ilc_table_cell_Cell4 -,th.ilc_table_cell_Cell4 -{ - padding-top: 3px; - padding-right: 10px; - padding-bottom: 3px; - padding-left: 10px; - background-color: #FFFFCC; -} - -td.ilc_table_cell_StandardCell1 -,th.ilc_table_cell_StandardCell1 -{ - background-color: #FFFFFF; - border-style: solid; - border-color: #4C6586; - border-width: 1px; - padding-left: 10px; - padding-bottom: 3px; - font-weight: normal; - padding-top: 3px; - padding-right: 10px; -} - -td.ilc_table_cell_StandardCell2 -,th.ilc_table_cell_StandardCell2 -{ - padding-top: 3px; - font-weight: normal; - background-color: #FFFFFF; - border-width: 1px; - border-style: solid; - border-color: #4C6586; - padding-left: 10px; - padding-right: 10px; - padding-bottom: 3px; -} - -td.ilc_table_cell_StandardHeader -,th.ilc_table_cell_StandardHeader -{ - font-weight: normal; - padding-top: 3px; - color: #FFFFFF; - padding-bottom: 3px; - padding-right: 10px; - border-width: 1px; - border-color: #FFFFFF; - padding-left: 10px; - border-style: solid; - background-color: #4C6586; -} - -td.ilc_table_cell_TransparentC -,th.ilc_table_cell_TransparentC -{ - font-weight: normal; - padding-top: 3px; - padding-right: 10px; - padding-bottom: 3px; - padding-left: 10px; - border-width: 1px; - border-color: #000000; - border-style: solid; -} - -td.ilc_table_cell_TransparentH -,th.ilc_table_cell_TransparentH -{ - font-weight: normal; - padding-top: 3px; - padding-bottom: 3px; - padding-right: 10px; - padding-left: 10px; - border-style: solid; - border-width: 1px; -} - -textarea.ilc_qlinput_LongTextInput -{ - padding-left: 4px; -} - -ul.ilc_flist_FileList -{ - list-style-type: none; - padding: 0px; - margin: 0px; -} - -ul.ilc_qordul_OrderList -{ - padding: 0px; - margin: 0px; - list-style: none; - list-style-position: outside; -} - -ul.ilc_qordul_OrderListHorizontal -{ - list-style: none; - margin: 0px; - padding: 0px; - list-style-position: outside; -} - -div.ilc_va_iheada_AccordIHeadActive -{ - margin-bottom: 0px; - padding-top: 8px; - padding-right: 8px; - padding-bottom: 8px; - padding-left: 38px; - border-width: 1px; - border-color: #DDDDDD; - border-style: solid; - background-color: #E2E8EF; - background-image: url("@{image_path}arrow_down.svg"); - background-repeat: no-repeat; - background-position: 10px center; - cursor: pointer; - background-size: 20px 20px; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - border-bottom-right-radius: 0px; - border-bottom-left-radius: 0px; -} - -div.ilc_va_iheada_AccordIHeadActive:hover -{ - background-position: 10px center; -} - -div.ilc_va_iheada_ColoredAccordIHeadActive:hover -{ - background-color: #3d506b; - background-image: url("@{image_path}collapse_white.svg"); - background-position: right center; - cursor: pointer; - transition: all 0.5s ease-out; -} - -div.ilc_va_iheada_EmphasisedAccordIHeadActive -{ - font-size: 1.5em; - color: #FFFFFF; - padding: 15px; - background-color: #3d506b; - background-image: url("@{image_path}collapse_white.svg"); - background-repeat: no-repeat; - background-position: right center; - border-radius: 0px; - transform: scale(1.01); - -webkit-transform: scale(1.01); - -moz-transform: scale(1.01); - -ms-transform: scale(1.01); - transition: all 0.5s ease-out; -} - -div.ilc_va_iheada_EmphasisedAccordIHeadActive:hover -{ - background-color: #3d506b; - background-image: url("@{image_path}collapse_white.svg"); - background-repeat: no-repeat; - background-position: right center; - cursor: pointer; - transition: all 0.5s ease-out; -} - -div.ilc_va_iheada_LightAccordIHeadActive -{ - background-image: url("@{image_path}collapse.svg"); - background-repeat: no-repeat; - border-radius: 0px; - transition: all 0.5s ease-out; - font-size: 1.5em; - color: #4C6586; - padding: 10px; - border-bottom-style: solid; - background-position: right center; - border-top-width: 0px; - border-right-width: 0px; - border-bottom-width: 2px; - border-left-width: 0px; - border-top-style: none; - border-right-style: none; - border-left-style: none; - background-color: #FFFFFF; - border-color: #FFFFFF; -} - -div.ilc_va_iheada_LightAccordIHeadActive:hover -{ - background-position: right center; - background-color: #FFFFFF; - cursor: pointer; - border-width: 0px; - border-top-width: 0px; - border-right-width: 0px; - border-bottom-width: 2px; - border-left-width: 0px; - border-style: none; - border-top-style: none; - border-right-style: none; - border-bottom-style: solid; - border-left-style: none; - border-color: #FFFFFF; - transition: all 0.5s ease-out; +a.ilc_qetitem_ErrorTextItem { + padding: 2px; + text-decoration: none; +} +a.ilc_qetitem_ErrorTextItem:hover { + color: #000000; + text-decoration: none; + background-color: #D0D0D0; +} +a.ilc_qetitem_ErrorTextSelected { + border-style: none; + background-color: #E2E8EF; + color: #161616; +} +a.ilc_link_ExtLink { + text-decoration: underline; +} +a.ilc_link_FileLink { + text-decoration: underline; +} +a.ilc_link_GlossaryLink { + text-decoration: underline; +} +a.ilc_glo_ovclink_GlossaryOvCloseLink { + text-decoration: underline; + font-weight: normal; +} +a.ilc_glo_ovuglistlink_GlossaryOvUGListLink { + font-weight: normal; +} +a.ilc_glo_ovuglink_GlossaryOvUnitGloLink { + font-weight: normal; +} +a.ilc_qimgd_ImageDetailsLink { + font-size: 90%; +} +a.ilc_link_IntLink { + text-decoration: underline; +} +a.ilc_marker_Marker { + background-image: url("../basic_style/images/icon_pin.svg"); + background-repeat: no-repeat; + cursor: pointer; + display: block; + height: 32px; + width: 24px; + position: absolute; +} +a.ilc_marker_Marker:hover { + background-repeat: no-repeat; + background-image: url("../basic_style/images/icon_pin_on.svg"); +} +a.ilc_rte_mlink_RTELink { + font-size: 100%; + margin: 1px; + margin-top: 10px; + margin-right: 5px; + margin-bottom: 10px; + margin-left: 5px; + padding-right: 3mm; + padding-top: 1mm; + border-style: none; + padding-bottom: 1mm; + text-decoration: none; + padding-left: 3mm; + border-width: 1px; + background-color: #4C6586; + text-align: center; + height: 50px; + color: #ffffff !important; + text-decoration: none !important; +} +a.ilc_rte_mlink_RTELink:hover { + background-color: #577399; + color: #FFFFFF; + text-decoration: none !important; +} +a.ilc_rte_texp_RTETreeCollapsed { + background-image: url("../basic_style/images/arrow_right.svg"); + background-repeat: no-repeat; + background-position: 0px 0px; + background-size: 15px 15px !important; +} +a.ilc_rte_tclink_RTETreeControlLink { + color: #4C6586; + color: #4C6586 !important; +} +a.ilc_rte_tlink_RTETreeCurrent { + background-color: #FFFF9D; + color: #161616 !important; + background-color: #E2E8EF !important; +} +a.ilc_rte_texp_RTETreeExpanded { + background-image: url("../basic_style/images/arrow_down.svg"); + background-size: 15px 15px; +} +a.ilc_rte_tlink_RTETreeLink { + color: #4C6586; + color: #4C6586 !important; +} +div.ilc_va_cntr_AccordCntr { + margin-top: 5px; +} +div.ilc_va_icntr_AccordICntr { + margin-bottom: 5px; +} +div.ilc_va_icont_AccordICont { + margin-top: -1px; + margin-bottom: 12px; + padding-top: 3px; + padding-right: 3px; + padding-bottom: 3px; + padding-left: 15px; + border-width: 1px; + border-color: #DDDDDD; + border-style: solid; + background-color: #FFFFFF; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +div.ilc_va_ihead_AccordIHead { + text-align: left; + margin-bottom: 12px; + padding-top: 8px; + padding-right: 8px; + padding-bottom: 8px; + padding-left: 38px; + border-width: 1px; + border-color: #DDDDDD; + border-style: solid; + background-color: #F9F9F9; + background-image: url("../basic_style/images/arrow_right.svg"); + background-repeat: no-repeat; + background-position: 15px center; + cursor: pointer; + background-size: 20px 20px; + border-radius: 3px; + font-weight: 600; +} +div.ilc_va_ihead_AccordIHead:hover { + background-color: #E2E8EF; +} +div.ilc_va_ihcap_AccordIHeadCap { + font-size: 1em; + font-weight: 600; +} +div.ilc_section_Additional, +a.ilc_section_Additional { + padding-left: 20px; + margin-top: 40px; + margin-bottom: 10px; + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + padding-top: 35px; + padding-right: 30px; + padding-bottom: 15px; + border-radius: 3px; + position: relative !important; + background-position: left top; +} +div.ilc_section_Additional::before, +a.ilc_section_Additional::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #eceff4; + background-position: center center; + left: 15px; + background-image: url("../basic_style/images/additional.svg"); + background-repeat: no-repeat; + position: absolute; + width: 60px; + height: 60px; + top: -32px; + right: 0px; + content: ""; + display: block; + border-radius: 50px; +} +div.ilc_section_AdvancedKnowledge, +a.ilc_section_AdvancedKnowledge { + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + padding-left: 20px; + padding-right: 30px; + padding-top: 35px; + padding-bottom: 15px; + margin-top: 40px; + margin-bottom: 10px; + background-position: left top; + border-radius: 3px; + position: relative !important; +} +div.ilc_section_AdvancedKnowledge::before, +a.ilc_section_AdvancedKnowledge::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #eceff4; + background-position: center center; + left: 15px; + background-image: url("../basic_style/images/advknowledge.svg"); + position: absolute; + top: -32px; + right: 0px; + content: ""; + width: 60px; + display: block; + height: 60px; + border-radius: 50px; + background-repeat: no-repeat; +} +div.ilc_qanswer_Answer { + padding-right: 10px; + border-radius: 5px; +} +div.ilc_section_Attention, +a.ilc_section_Attention { + position: relative; + border-width: 2px; + border-color: #FA8228; + border-style: solid; + border-radius: 3px; + position: relative !important; + padding-left: 20px; + padding-bottom: 15px; + padding-right: 30px; + padding-top: 35px; + margin-bottom: 10px; + margin-top: 40px; +} +div.ilc_section_Attention::before, +a.ilc_section_Attention::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #fee6d4; + background-position: center 12px; + left: 15px; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/attention.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; +} +div.ilc_section_Background, +a.ilc_section_Background { + background-color: #F9F9F9; + padding-top: 10px; + margin-bottom: 20px; + padding-right: 20px; + padding-bottom: 10px; + padding-left: 20px; +} +div.ilc_section_Block, +a.ilc_section_Block { + padding-bottom: 10px; + padding-right: 20px; + margin-bottom: 10px; + padding-top: 10px; + margin-top: 20px; + padding-left: 20px; +} +div.ilc_section_Card, +a.ilc_section_Card { + min-height: 250px; + position: relative; + margin: 15px; + padding-top: 15px; + padding-right: 15px; + padding-bottom: 15px; + padding-left: 15px; + box-shadow: 5px 5px 40px rgba(0, 0, 0, 0.2); + border-radius: 15px; + max-width: 400px; +} +div.ilc_section_Citation, +a.ilc_section_Citation { + padding-right: 30px; + padding-top: 35px; + margin-bottom: 10px; + margin-top: 40px; + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + background-color: #FFFFFF; + border-radius: 3px; + position: relative !important; + padding-left: 20px; + padding-bottom: 15px; +} +div.ilc_section_Citation::before, +a.ilc_section_Citation::before { + background-color: #eceff4; + left: 15px; + background-position: center center; + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/citation.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; +} +div.ilc_va_ihead_ColoredAccordIHead:hover { + background-color: #3d506b; + cursor: pointer; + transition: all 0.5s ease-out; +} +div.ilc_section_Confirmation, +a.ilc_section_Confirmation { + position: relative; + border-width: 2px; + border-color: #6EA03C; + border-style: solid; + border-radius: 3px; + position: relative !important; + margin-top: 40px; + margin-bottom: 10px; + padding-top: 35px; + padding-bottom: 15px; + padding-right: 30px; + padding-left: 20px; +} +div.ilc_section_Confirmation::before, +a.ilc_section_Confirmation::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-position: center center; + left: 15px; + background-color: #e9f3df; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/confirmation.svg"); + position: absolute; + top: -32px; + width: 60px; + height: 60px; + background-repeat: no-repeat; +} +div.ilc_iim_ContentPopup { + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; + padding-top: 5px; + border-style: solid; + border-width: 1px; + background-color: #FFFFFF; + border-color: #d8d8d8; +} +div.ilc_qover_Correct { + padding-top: 20px; + padding-right: 20px; + background-image: url("../basic_style/images/correct.svg"); + padding-bottom: 20px; + padding-left: 60px; + background-position: 10px; + background-repeat: no-repeat; + border-style: none; + background-color: #f0f7ea; + margin-bottom: 10px; + border-radius: 3px; + margin-top: 10px; +} +div.ilc_va_cntr_EmphasisedAccordCntr { + padding-top: 5px; + padding-right: 15px; + padding-left: 15px; +} +div.ilc_va_icont_EmphasisedAccordICont { + padding: 15px; + background-color: #eceff4; + transform: scale(1.01); + -webkit-transform: scale(1.01); + -moz-transform: scale(1.01); + -ms-transform: scale(1.01); + transition: all 0.5s ease-out; +} +div.ilc_va_ihead_EmphasisedAccordIHead { + font-size: 1.5em; + color: #FFFFFF; + padding: 15px; + background-color: #4C6586; + background-image: url("../basic_style/images/expand_white.svg"); + background-repeat: no-repeat; + background-position: right center; + transition: all 0.5s ease-out; +} +div.ilc_va_ihead_EmphasisedAccordIHead:hover { + background-color: #3d506b; + transition: all 0.5s ease-out; +} +div.ilc_section_Example, +a.ilc_section_Example { + padding-right: 30px; + padding-bottom: 15px; + padding-left: 20px; + background-position: left center; + margin-bottom: 10px; + margin-top: 40px; + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + border-radius: 3px; + position: relative !important; + padding-top: 35px; +} +div.ilc_section_Example::before, +a.ilc_section_Example::before { + background-color: #eceff4; + background-position: center center; + left: 15px; + background-image: url("../basic_style/images/example.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + right: 0px; + content: ""; + display: block; + border-radius: 50px; +} +div.ilc_section_Excursus, +a.ilc_section_Excursus { + padding-right: 30px; + margin-bottom: 10px; + margin-top: 40px; + border-style: solid; + border-width: 2px; + position: relative; + padding-top: 35px; + border-color: #4C6586; + padding-left: 20px; + border-radius: 3px; + position: relative !important; + padding-bottom: 15px; +} +div.ilc_section_Excursus::before, +a.ilc_section_Excursus::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #eceff4; + background-position: center center; + left: 15px; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/excursus.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; +} +div.ilc_qfeedr_FeedbackRight { + margin-top: 10px; + padding-top: 20px; + margin-bottom: 10px; + border-style: none; + background-color: #f0f7ea; + padding-left: 60px; + padding-bottom: 20px; + padding-right: 160px; + background-image: url("../basic_style/images/correct.svg"); + background-position: 10px; + background-repeat: no-repeat; + border-radius: 3px; +} +div.ilc_qfeedw_FeedbackWrong { + padding-left: 60px; + padding-bottom: 20px; + background-position: 24px; + padding-right: 160px; + background-image: url("../basic_style/images/exclamation.svg"); + margin-top: 10px; + background-repeat: no-repeat; + padding-top: 20px; + margin-bottom: 10px; + border-style: none; + background-color: #fef2ea; + border-radius: 3px; +} +div.ilc_flist_cont_FileListContainer { + border-radius: 3px; + color: #FFFFFF; + margin-top: 10px; + margin-right: 0px; + margin-bottom: 10px; + margin-left: 0px; + border-color: #4C6586; + border-style: solid; + border-width: 1px; + text-indent: 7px; + padding-bottom: 10px; +} +div.ilc_flist_head_FileListHeading { + margin-bottom: 10px; + text-align: left; + color: #FFFFFF; + font-weight: bold; + background-image: url("../basic_style/images/download.svg"); + background-color: #4C6586; + margin-top: 0px; + background-position: 10px; + background-repeat: no-repeat; + padding-left: 50px; + padding-bottom: 10px; + padding-right: 20px; + padding-top: 10px; +} +div.ilc_sco_fmess_FinalMessage { + margin: 100px; + text-align: center; + border-style: none; + border-width: 1px; + padding: 50px; + font-size: 125%; +} +div.ilc_page_fn_Footnote { + margin-bottom: 5px; + margin-top: 5px; +} +div.ilc_glo_overlay_GlossaryOverlay { + border-style: none; + padding-top: 5px; + border-width: 2px; + padding-left: 10px; + padding-bottom: 5px; + padding-right: 10px; +} +div.ilc_ha_ihead_HAccordIHead:hover { + background-color: #E2E8EF; +} +div.ilc_qover_Incorrect { + padding-left: 60px; + background-image: url("../basic_style/images/exclamation.svg"); + background-repeat: no-repeat; + background-position: 24px; + padding-bottom: 20px; + padding-right: 20px; + margin-bottom: 10px; + margin-top: 10px; + background-color: #fef2ea; + border-style: none; + padding-top: 20px; + border-radius: 3px; +} +div.ilc_section_Information, +a.ilc_section_Information { + margin-bottom: 10px; + margin-top: 40px; + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + border-radius: 3px; + position: relative !important; + padding-left: 20px; + padding-bottom: 15px; + padding-right: 30px; + padding-top: 35px; +} +div.ilc_section_Information::before, +a.ilc_section_Information::before { + background-color: #eceff4; + background-position: center center; + left: 15px; + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-image: url("../basic_style/images/information.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; + content: ""; + display: block; + border-radius: 50px; +} +div.ilc_section_Interaction, +a.ilc_section_Interaction { + margin-top: 10px; + padding-top: 20px; + margin-bottom: 10px; + padding-right: 20px; + padding-bottom: 20px; + background-image: url("../basic_style/images/interaction.svg"); + padding-left: 60px; + background-position: 12px; + background-repeat: no-repeat; + border-width: 1px; + border-color: #4C6586; + border-style: solid; + border-radius: 3px; +} +div.ilc_section_Interaction:hover, +a.ilc_section_Interaction:hover { + cursor: pointer; + background-color: #E2E8EF; +} +div.ilc_va_icont_LightAccordICont { + padding: 10px; + border-width: 4px; + border-color: #4C6586; + border-bottom-style: solid; +} +div.ilc_va_ihead_LightAccordIHead { + color: #4C6586; + padding: 10px; + border-width: 2px; + border-color: #4C6586; + border-bottom-style: solid; + background-image: url("../basic_style/images/expand.svg"); + background-position: right center; + background-repeat: no-repeat; + font-size: 1.5em; + background-color: #FFFFFF; + transition: background-image 2s ease-in-out; +} +div.ilc_va_ihead_LightAccordIHead:hover { + cursor: pointer; + border-width: 4px; + border-color: #4C6586; + border-bottom-style: solid; + transition: all 0.5s ease-out; +} +div.ilc_section_Link, +a.ilc_section_Link { + text-decoration: underline; + margin-top: 10px; + margin-bottom: 10px; + padding-top: 20px; + padding-right: 20px; + padding-bottom: 20px; + padding-left: 60px; + border-width: 1px; + border-color: #4C6586; + border-style: dashed; + background-image: url("../basic_style/images/link.svg"); + background-repeat: no-repeat; + background-position: 10px; + border-radius: 3px; +} +div.ilc_section_Literature, +a.ilc_section_Literature { + margin-top: 40px; + margin-bottom: 10px; + padding-top: 35px; + padding-right: 30px; + padding-bottom: 15px; + padding-left: 20px; + position: relative; + border-width: 2px; + border-color: #4C6586; + border-style: solid; + border-radius: 3px; + position: relative !important; +} +div.ilc_section_Literature::before, +a.ilc_section_Literature::before { + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #eceff4; + background-position: center center; + left: 15px; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/literature.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; +} +div.ilc_media_caption_MediaCaption { + font-weight: bolder; + font-size: 100%; + padding: 10px; + background-color: #F9F9F9; +} +div.ilc_section_Mnemonic, +a.ilc_section_Mnemonic { + padding-right: 30px; + padding-bottom: 15px; + padding-left: 20px; + background-position: left center; + border-style: solid; + padding-top: 35px; + position: relative; + border-width: 2px; + border-color: #F3DE2C; + border-radius: 3px; + position: relative !important; + margin-top: 40px; + margin-bottom: 10px; +} +div.ilc_section_Mnemonic::before, +a.ilc_section_Mnemonic::before { + border-width: 2px; + border-color: #FFFFFF; + background-color: #fdfadf; + background-position: center center; + left: 15px; + border-style: solid; + content: ""; + display: block; + border-radius: 50px; + background-image: url("../basic_style/images/mnemonic.svg"); + background-repeat: no-repeat; + position: absolute; + top: -32px; + width: 60px; + height: 60px; +} +div.ilc_page_Page { + min-height: 300px; +} +div.ilc_page_cont_PageContainer { + border-width: 1px; + border-color: #DDDDDD; + padding: 20px; + margin: 0px; + border-style: solid; + background-color: #FFFFFF; + min-height: 500px; +} +div.ilc_section_Remark, +a.ilc_section_Remark { + border-style: solid; + border-width: 2px; + position: relative; + border-color: #4C6586; + border-radius: 3px; + position: relative !important; + padding-right: 30px; + padding-top: 35px; + padding-left: 20px; + padding-bottom: 15px; + margin-top: 40px; + margin-bottom: 10px; +} +div.ilc_section_Remark::before, +a.ilc_section_Remark::before { + border-width: 2px; + border-color: #FFFFFF; + background-image: url("../basic_style/images/remark.svg"); + border-style: solid; + background-repeat: no-repeat; + background-color: #eceff4; + position: absolute; + background-position: center center; + top: -32px; + left: 15px; + width: 60px; + content: ""; + height: 60px; + display: block; + border-radius: 50px; +} +div.ilc_rte_menu_RTELinkBar { + border-top-width: 10px; + border-bottom-width: 10px; + margin-top: 10px; + margin-bottom: 10px; +} +div.ilc_rte_menu_RTELogo { + float: left; +} +div.ilc_rte_menu_RTEMenu { + background-color: #FFFFFF; +} +div.ilc_section_Separator, +a.ilc_section_Separator { + margin-top: 10px; + margin-bottom: 10px; + padding-top: 20px; + border-width: 2px; + padding-bottom: 20px; + border-color: #4C6586; + border-top-style: solid; +} +div.ilc_section_Special, +a.ilc_section_Special { + padding-left: 20px; + border-style: none; + border-width: 1px; + padding-bottom: 20px; + padding-top: 20px; + padding-right: 20px; +} +div.ilc_question_Standard { + padding-left: 30px; + padding-bottom: 60px; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1), 0 6px 15px rgba(0, 0, 0, 0.12); + background-position: right top; + margin-top: 20px; + margin-bottom: 10px; + padding-top: 30px; + padding-right: 30px; +} +div.ilc_qover_StatusMessage { + padding-bottom: 7px; +} +div.ilc_qtitle_Title { + margin-bottom: 20px; + font-size: 140%; + font-weight: normal; +} +div.ilc_sco_title_Title { + border-bottom-style: solid; + border-bottom-width: 2px; + padding-bottom: 3px; + font-size: 140%; + margin-bottom: 20px; + font-weight: bold; + margin-top: 5px; +} +em.ilc_em_Emph, +span.ilc_text_inline_Emph { + font-style: italic; +} +figure.ilc_media_cont_MediaContainer { + margin: 0px; +} +figure.ilc_media_cont_MediaContainerFull100 { + margin: 0px; + width: 100%; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1), 0 6px 15px rgba(0, 0, 0, 0.12); +} +figure.ilc_media_cont_MediaContainerHighlighted { + margin: 0px; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1), 0 6px 15px rgba(0, 0, 0, 0.12); +} +figure.ilc_media_cont_MediaContainerMax50 { + margin: 0px; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1), 0 6px 15px rgba(0, 0, 0, 0.12); + max-width: 50%; +} +figure.ilc_media_cont_MediaContainerSeparated { + margin: 40px; +} +h1.ilc_glo_ovtitle_GlossaryOvTitle, +div.ilc_text_block_GlossaryOvTitle, +html.il-no-tiny-bg body#tinymce.ilc_text_block_GlossaryOvTitle p { + margin-bottom: 10px; + font-size: 1.5em; + margin-top: 10px; + font-weight: 600; +} +h1.ilc_heading1_Headline1, +div.ilc_text_block_Headline1, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline1 p { + padding-top: 10px; + margin-bottom: 15px; + margin-top: 20px; + font-size: 1.75em; + font-weight: 600; +} +h1.ilc_page_title_PageTitle, +div.ilc_text_block_PageTitle, +html.il-no-tiny-bg body#tinymce.ilc_text_block_PageTitle p { + font-weight: 600; + border-bottom-width: 1px; + border-bottom-style: none; + border-style: none; + font-size: 1.75rem; + padding-bottom: 3px; + margin-top: 15px; + margin-bottom: 15px; + text-align: left; + white-space: normal; +} +h2.ilc_heading2_Headline2, +div.ilc_text_block_Headline2, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline2 p { + font-weight: 600; + font-size: 1.5em; + margin-top: 20px; + margin-bottom: 15px; +} +h3.ilc_heading3_Headline3, +div.ilc_text_block_Headline3, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Headline3 p { + font-weight: 600; + margin-top: 15px; + margin-bottom: 10px; + font-size: 1.115em; +} +img.ilc_qimg_QuestionImage { + margin: 10px; + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1), 0 6px 15px rgba(0, 0, 0, 0.12); +} +input.ilc_qsubmit_Submit { + border-style: none; + color: #ffffff !important; + padding: 0.5em; + margin-top: 20px; + background-color: #4C6586; +} +input.ilc_qsubmit_Submit:hover { + cursor: pointer; + background-color: #314157; +} +input.ilc_qinput_TextInput { + padding-left: 4px; +} +li.ilc_list_item_Attention { + margin-bottom: 15px; + padding-top: 6px; + padding-bottom: 6px; + padding-left: 45px; + position: relative; + list-style-type: none; +} +li.ilc_list_item_Attention::before { + background-image: url("../basic_style/images/attention_red.svg"); + background-repeat: no-repeat; + position: absolute; + width: 35px; + height: 35px; + left: 0; + top: 0; + content: ""; +} +li.ilc_list_item_Checklist { + list-style-type: none; + margin-bottom: 15px; + padding-top: 6px; + padding-bottom: 6px; + padding-left: 45px; + position: relative; +} +li.ilc_list_item_Checklist2::before { + background-image: url("../basic_style/images/wrong.svg"); + background-repeat: no-repeat; + position: absolute; + width: 35px; + height: 35px; + left: 0; + top: 0; + content: ""; +} +li.ilc_list_item_Checklist::before { + content: ""; + background-image: url("../basic_style/images/checked.svg"); + background-repeat: no-repeat; + width: 35px; + height: 35px; + position: absolute; + left: 0; + top: 0; +} +li.ilc_list_item_ColoredCircle::before { + font-weight: bold; + color: #FFFFFF; + background-color: #59A0A5; + position: absolute; + width: 35px; + height: 35px; + content: counter(section); + border-radius: 50px; + left: 0; + display: grid; + justify-content: center; + align-items: center; + top: 0; +} +li.ilc_list_item_ColoredCircleBackground::before { + font-size: 1.5rem; + font-weight: bold; + color: #FFFFFF; + margin-left: 10px; + background-color: #4C6586; + position: absolute; + width: 50px; + height: 50px; + content: counter(section); + border-radius: 50px; + left: 0; + top: 13px; + display: grid; + align-items: center; + justify-content: center; +} +li.ilc_list_item_ColoredSquare::before { + font-weight: bold; + color: #FFFFFF; + background-color: #F3DE2C; + position: absolute; + width: 35px; + height: 35px; + content: counter(section); + border-radius: 6px; + left: 0; + display: grid; + justify-content: center; + align-items: center; + top: 0; +} +li.ilc_list_item_ColoredSquareBackground::before { + font-weight: bold; + color: #FFFFFF; + background-color: #F3DE2C; + position: absolute; + width: 35px; + height: 100%; + text-align: center; + content: counter(section); + left: 0; + top: 0; + display: grid; + justify-content: center; + align-items: center; +} +li.ilc_flist_li_FileListItem { + padding-left: 3px; + padding-right: 3px; + padding-top: 5px; + padding-bottom: 5px; +} +li.ilc_list_item_FilledCircle { + margin-bottom: 15px; + padding-top: 6px; + padding-bottom: 6px; + padding-left: 45px; + position: relative; + list-style-type: none; + counter-increment: section 1; +} +li.ilc_list_item_FilledCircle::before { + font-weight: bold; + color: #FFFFFF; + background-color: #59A0A5; + position: absolute; + height: 35px; + min-width: 35px; + content: counter(section); + border-radius: 50px; + left: 0; + display: grid; + justify-content: center; + align-items: center; + top: 0; +} +li.ilc_list_item_FilledCircleLarge { + font-size: 1.5rem; + position: relative; + padding-top: 20px; + padding-right: 20px; + padding-bottom: 20px; + padding-left: 70px; + list-style-type: none; + counter-increment: section 1; +} +li.ilc_list_item_FilledCircleLarge::before { + font-weight: bold; + color: #FFFFFF; + margin-left: 10px; + position: absolute; + height: 50px; + background-color: #4C6586; + content: counter(section); + min-width: 50px; + border-radius: 50px; + left: 0; + top: 13px; + display: grid; + align-items: center; + justify-content: center; +} +li.ilc_list_item_FilledSquare { + margin-bottom: 15px; + padding-top: 6px; + padding-bottom: 6px; + padding-left: 45px; + position: relative; + list-style-type: none; + counter-increment: section 1; +} +li.ilc_list_item_FilledSquare::before { + font-weight: bold; + color: #FFFFFF; + background-color: #F3DE2C; + position: absolute; + height: 35px; + content: counter(section); + min-width: 35px; + border-radius: 6px; + left: 0; + display: grid; + justify-content: center; + align-items: center; + top: 0; +} +li.ilc_list_item_FilledSquareBackground { + margin-top: -2px; + padding-top: 15px; + padding-bottom: 15px; + padding-left: 50px; + border-width: 2px; + border-color: #FFFFFF; + border-style: solid; + background-color: #fdfadf; + list-style-type: none; + counter-increment: section 1; + position: relative; +} +li.ilc_list_item_FilledSquareBackground::before { + background-color: #F3DE2C; + font-weight: bold; + color: #FFFFFF; + min-width: 35px; + display: grid; + justify-content: center; + align-items: center; + content: counter(section); + left: 0; + top: 0; + position: absolute; + height: 100%; +} +li.ilc_list_item_ListItem2::before { + width: 35px; + height: 35px; + color: #FFFFFF; + background-color: #59A0A5; + font-weight: bold; + margin-right: 10px; + content: counter(section); + display: inline-flex; + border-radius: 50px; + justify-content: center; + align-items: center; +} +li.ilc_list_item_ListItem2Absolut::before { + font-weight: bold; + color: #FFFFFF; + background-color: #59A0A5; + width: 35px; + height: 35px; + position: absolute; + content: counter(section); + border-radius: 50px; + left: 0; + display: grid; + justify-content: center; + align-items: center; +} +li.ilc_list_item_ListItem2Grid::before { + font-weight: bold; + color: #FFFFFF; + background-color: #59A0A5; + width: 35px; + height: 35px; + content: counter(section); + border-radius: 50px; + display: inline-grid; + justify-content: center; + align-items: center; +} +li.ilc_list_item_ListItem3::before { + background-image: url("../basic_style/images/checked.svg"); + background-repeat: no-repeat; + width: 35px; + height: 35px; + margin-right: 5px; + display: inline-flex; + content: ""; +} +li.ilc_list_item_ListItem4::before { + background-image: url("../basic_style/images/wrong.svg"); + background-repeat: no-repeat; + width: 35px; + height: 35px; + margin-right: 5px; + display: inline-flex; + content: ""; +} +li.ilc_list_item_ListItem::before { + font-size: 1.5rem; + font-weight: bold; + color: #FFFFFF; + background-color: #4C6586; + width: 50px; + height: 50px; + vertical-align: middle; + margin-left: 10px; + position: absolute; + content: counter(section); + display: grid; + border-radius: 50px; + align-items: center; + justify-content: center; + left: 0; + top: 13px; +} +li.ilc_qordli_OrderListItem { + margin-top: 5px; + margin-bottom: 5px; + margin-left: 0px; + margin-right: 0px; + cursor: move; + padding: 10px; + background-color: #E2E8EF; +} +li.ilc_qordli_OrderListItemHorizontal { + float: left; + margin-top: 5px; + margin-bottom: 5px; + margin-right: 10px; + padding: 10px; + cursor: move; + background-color: #E2E8EF; +} +li.ilc_list_item_Pointer { + margin-bottom: 15px; + padding-top: 6px; + padding-bottom: 6px; + padding-left: 45px; + position: relative; + list-style-type: none; +} +li.ilc_list_item_Pointer::before { + background-image: url("../basic_style/images/pointer.svg"); + background-repeat: no-repeat; + position: absolute; + width: 35px; + height: 35px; + left: 0; + top: 0; + content: ""; +} +ol.ilc_list_o_NumberedListNoStyle { + list-style-type: none; + margin-top: 15px; + padding-top: 20px; + padding-bottom: 20px; + counter-reset: section; +} +p.ilc_text_block_Book, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Book p { + hyphens: auto; + text-align: justify; +} +p.ilc_text_block_Button:hover, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Button:hover p { + background-color: #5774A0; +} +p.ilc_text_block_List, +html.il-no-tiny-bg body#tinymce.ilc_text_block_List p { + margin-top: 3px; + margin-bottom: 3px; +} +p.ilc_text_block_Numbers, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Numbers p { + text-align: right; +} +p.ilc_text_block_Standard, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Standard p { + margin-bottom: 10px; + margin-top: 10px; +} +p.ilc_text_block_StandardLarge, +html.il-no-tiny-bg body#tinymce.ilc_text_block_StandardLarge p { + font-size: 1.115rem; +} +p.ilc_text_block_TableContent, +html.il-no-tiny-bg body#tinymce.ilc_text_block_TableContent p { + line-height: 1.8em; + padding-bottom: 10px; + padding-top: 10px; + padding-left: 10px; + padding-right: 10px; + margin-top: 0px; + margin-bottom: 0px; + margin-right: 0px; + margin-left: 0px; +} +p.ilc_text_block_Verse, +html.il-no-tiny-bg body#tinymce.ilc_text_block_Verse p { + text-align: center; +} +pre.ilc_code_block_Code { + border-width: 1px; + border-color: #DDDDDD; + border-style: solid; + padding: 9px; + background-color: #F9F9F9; +} +span.ilc_text_inline_Accent { + color: #087e0a; +} +span.ilc_text_inline_Attention { + color: #CB4D16; +} +span.ilc_text_inline_Comment { + color: #b31bff; +} +span.ilc_qetcorr_ErrorTextCorrected { + text-decoration: line-through; +} +span.ilc_text_inline_Important { + text-decoration: underline; +} +span.ilc_text_inline_Mnemonic { + background-color: #fcf7ca; + padding-left: 4px; + padding-right: 4px; +} +span.ilc_text_inline_Quotation { + letter-spacing: 0.2px; + font-style: italic; + font-family: serif; + color: #1B5FFF; + font-size: 114%; +} +strong.ilc_strong_Strong, +span.ilc_text_inline_Strong { + font-weight: bold; +} +table.ilc_table_StandardTable { + border-collapse: collapse; + margin-top: 10px; + margin-bottom: 10px; + border-color: #9EADBA; +} +td.ilc_table_cell_Cell1, +th.ilc_table_cell_Cell1 { + padding-top: 3px; + padding-right: 10px; + padding-bottom: 3px; + padding-left: 10px; + background-color: #FFCCCC; +} +td.ilc_table_cell_Cell2, +th.ilc_table_cell_Cell2 { + background-color: #CCCCFF; + padding-top: 3px; + padding-right: 10px; + padding-bottom: 3px; + padding-left: 10px; +} +td.ilc_table_cell_Cell3, +th.ilc_table_cell_Cell3 { + padding-left: 10px; + padding-bottom: 3px; + padding-right: 10px; + padding-top: 3px; + background-color: #CCFFCC; +} +td.ilc_table_cell_Cell4, +th.ilc_table_cell_Cell4 { + padding-top: 3px; + padding-right: 10px; + padding-bottom: 3px; + padding-left: 10px; + background-color: #FFFFCC; +} +td.ilc_table_cell_StandardCell1, +th.ilc_table_cell_StandardCell1 { + background-color: #FFFFFF; + border-style: solid; + border-color: #4C6586; + border-width: 1px; + padding-left: 10px; + padding-bottom: 3px; + font-weight: normal; + padding-top: 3px; + padding-right: 10px; +} +td.ilc_table_cell_StandardCell2, +th.ilc_table_cell_StandardCell2 { + padding-top: 3px; + font-weight: normal; + background-color: #FFFFFF; + border-width: 1px; + border-style: solid; + border-color: #4C6586; + padding-left: 10px; + padding-right: 10px; + padding-bottom: 3px; +} +td.ilc_table_cell_StandardHeader, +th.ilc_table_cell_StandardHeader { + font-weight: normal; + padding-top: 3px; + color: #FFFFFF; + padding-bottom: 3px; + padding-right: 10px; + border-width: 1px; + border-color: #FFFFFF; + padding-left: 10px; + border-style: solid; + background-color: #4C6586; +} +td.ilc_table_cell_TransparentC, +th.ilc_table_cell_TransparentC { + font-weight: normal; + padding-top: 3px; + padding-right: 10px; + padding-bottom: 3px; + padding-left: 10px; + border-width: 1px; + border-color: #000000; + border-style: solid; +} +td.ilc_table_cell_TransparentH, +th.ilc_table_cell_TransparentH { + font-weight: normal; + padding-top: 3px; + padding-bottom: 3px; + padding-right: 10px; + padding-left: 10px; + border-style: solid; + border-width: 1px; +} +textarea.ilc_qlinput_LongTextInput { + padding-left: 4px; +} +ul.ilc_flist_FileList { + list-style-type: none; + padding: 0px; + margin: 0px; +} +ul.ilc_qordul_OrderList { + padding: 0px; + margin: 0px; + list-style: none; + list-style-position: outside; +} +ul.ilc_qordul_OrderListHorizontal { + list-style: none; + margin: 0px; + padding: 0px; + list-style-position: outside; +} +div.ilc_va_iheada_AccordIHeadActive { + margin-bottom: 0px; + padding-top: 8px; + padding-right: 8px; + padding-bottom: 8px; + padding-left: 38px; + border-width: 1px; + border-color: #DDDDDD; + border-style: solid; + background-color: #E2E8EF; + background-image: url("../basic_style/images/arrow_down.svg"); + background-repeat: no-repeat; + background-position: 10px center; + cursor: pointer; + background-size: 20px 20px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; +} +div.ilc_va_iheada_AccordIHeadActive:hover { + background-position: 10px center; +} +div.ilc_va_iheada_ColoredAccordIHeadActive:hover { + background-color: #3d506b; + background-image: url("../basic_style/images/collapse_white.svg"); + background-position: right center; + cursor: pointer; + transition: all 0.5s ease-out; +} +div.ilc_va_iheada_EmphasisedAccordIHeadActive { + font-size: 1.5em; + color: #FFFFFF; + padding: 15px; + background-color: #3d506b; + background-image: url("../basic_style/images/collapse_white.svg"); + background-repeat: no-repeat; + background-position: right center; + border-radius: 0px; + transform: scale(1.01); + -webkit-transform: scale(1.01); + -moz-transform: scale(1.01); + -ms-transform: scale(1.01); + transition: all 0.5s ease-out; +} +div.ilc_va_iheada_EmphasisedAccordIHeadActive:hover { + background-color: #3d506b; + background-image: url("../basic_style/images/collapse_white.svg"); + background-repeat: no-repeat; + background-position: right center; + cursor: pointer; + transition: all 0.5s ease-out; +} +div.ilc_va_iheada_LightAccordIHeadActive { + background-image: url("../basic_style/images/collapse.svg"); + background-repeat: no-repeat; + border-radius: 0px; + transition: all 0.5s ease-out; + font-size: 1.5em; + color: #4C6586; + padding: 10px; + border-bottom-style: solid; + background-position: right center; + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 2px; + border-left-width: 0px; + border-top-style: none; + border-right-style: none; + border-left-style: none; + background-color: #FFFFFF; + border-color: #FFFFFF; +} +div.ilc_va_iheada_LightAccordIHeadActive:hover { + background-position: right center; + background-color: #FFFFFF; + cursor: pointer; + border-width: 0px; + border-top-width: 0px; + border-right-width: 0px; + border-bottom-width: 2px; + border-left-width: 0px; + border-style: none; + border-top-style: none; + border-right-style: none; + border-bottom-style: solid; + border-left-style: none; + border-color: #FFFFFF; + transition: all 0.5s ease-out; } diff --git a/components/ILIAS/COPage/xsl/page.xsl b/components/ILIAS/COPage/xsl/page.xsl index ce73de9d56d2..bdc9412fbd0b 100755 --- a/components/ILIAS/COPage/xsl/page.xsl +++ b/components/ILIAS/COPage/xsl/page.xsl @@ -3736,6 +3736,13 @@ + + [[[LEGACY_ANSWER_FORM_TEXT_]]] + + + [[[ANSWER_FORM_]]] + + diff --git a/components/ILIAS/Certificate/classes/API/UserCertificateAPI.php b/components/ILIAS/Certificate/classes/API/UserCertificateAPI.php index c031d26765a4..67f3b1dcfb07 100755 --- a/components/ILIAS/Certificate/classes/API/UserCertificateAPI.php +++ b/components/ILIAS/Certificate/classes/API/UserCertificateAPI.php @@ -86,13 +86,15 @@ public function getUserCertificateDataMaxCount(UserDataFilter $filter): int public function certificateCriteriaMetForGivenTemplate(int $usr_id, ilCertificateTemplate $template): void { if (!$template->isCurrentlyActive()) { - $this->logger->debug(sprintf( - 'Did not trigger certificate achievement for inactive template: usr_id: %s/obj_id: %s/type: %s/template_id: %s', - $usr_id, - $template->getObjId(), - $template->getObjType(), - $template->getId() - )); + $this->logger->debug( + \sprintf( + 'Did not trigger certificate achievement for inactive template: usr_id: %s/obj_id: %s/type: %s/template_id: %s', + $usr_id, + $template->getObjId(), + $template->getObjType(), + $template->getId() + ) + ); return; } @@ -106,13 +108,26 @@ public function certificateCriteriaMet(int $usr_id, int $obj_id): void { $type = $this->object_data_cache->lookupType($obj_id); if (!$this->type_class_map->typeExistsInMap($type)) { - throw new ilCertificateConsumerNotSupported(sprintf( - "Oject type '%s' is not supported by the certificate component!", - $type - )); + throw new ilCertificateConsumerNotSupported( + \sprintf( + "Oject type '%s' is not supported by the certificate component!", + $type + ) + ); } $template = $this->template_repository->fetchCurrentlyActiveCertificate($obj_id); + if ($template->getObjType() !== $type) { + $this->logger->error( + 'Object type mismatch between template and determined type for object with id {obj_id}: ' + . 'Expected {type} but got {template_type}!', + [ + 'obj_id' => $obj_id, + 'type' => $type, + 'template_type' => $template->getObjType() + ] + ); + } $this->certificateCriteriaMetForGivenTemplate($usr_id, $template); } @@ -136,13 +151,15 @@ private function processEntry( int $userId, ilCertificateTemplate $template ): void { - $this->logger->debug(sprintf( - 'Trigger persisting certificate achievement for: usr_id: %s/obj_id: %s/type: %s/template_id: %s', - $userId, - $template->getObjId(), - $template->getObjType(), - $template->getId() - )); + $this->logger->debug( + \sprintf( + 'Trigger persisting certificate achievement for: usr_id: %s/obj_id: %s/type: %s/template_id: %s', + $userId, + $template->getObjId(), + $template->getObjType(), + $template->getId() + ) + ); $entry = new ilCertificateQueueEntry( $template->getObjId(), diff --git a/components/ILIAS/Certificate/classes/Template/Action/Clone/class.ilCertificateCloneAction.php b/components/ILIAS/Certificate/classes/Template/Action/Clone/class.ilCertificateCloneAction.php index 94328b7d8755..509a3e85eeb0 100755 --- a/components/ILIAS/Certificate/classes/Template/Action/Clone/class.ilCertificateCloneAction.php +++ b/components/ILIAS/Certificate/classes/Template/Action/Clone/class.ilCertificateCloneAction.php @@ -69,8 +69,6 @@ public function cloneCertificate( )); } - $certificatePath = $this->pathFactory->create($newObject); - $templates = $this->templateRepository->fetchCertificateTemplatesByObjId($oldObject->getId()); /** @var ilCertificateTemplate $template */ diff --git a/components/ILIAS/Certificate/classes/Template/class.ilCertificateTemplate.php b/components/ILIAS/Certificate/classes/Template/class.ilCertificateTemplate.php index fa04211552c1..703d01a683e8 100755 --- a/components/ILIAS/Certificate/classes/Template/class.ilCertificateTemplate.php +++ b/components/ILIAS/Certificate/classes/Template/class.ilCertificateTemplate.php @@ -18,8 +18,6 @@ declare(strict_types=1); -use ILIAS\ResourceStorage\Identification\ResourceIdentification; - /** * @author Niels Theen */ @@ -40,6 +38,12 @@ public function __construct( private readonly ?int $id = null, private readonly bool $deleted = false ) { + if ($this->obj_type === '' || is_numeric($this->obj_type)) { + throw new ilInvalidCertificateException( + 'Certificate template object type must be a non-numeric string ' + . ', got ' . var_export($this->obj_type, true) + ); + } } public function getObjId(): int diff --git a/components/ILIAS/Certificate/classes/User/class.ilUserCertificateRepository.php b/components/ILIAS/Certificate/classes/User/class.ilUserCertificateRepository.php index e0e23ac954ae..b922c87a6cfd 100755 --- a/components/ILIAS/Certificate/classes/User/class.ilUserCertificateRepository.php +++ b/components/ILIAS/Certificate/classes/User/class.ilUserCertificateRepository.php @@ -656,7 +656,7 @@ public function fetchCertificatesForOverview( $sql_filters = []; foreach ($filter as $key => $value) { - if ($value === null) { + if ($value === null || $value === '') { continue; } @@ -745,7 +745,7 @@ public function fetchCertificatesForOverviewCount(array $filter, ?Range $range = { $sql_filters = []; foreach ($filter as $key => $value) { - if ($value === null) { + if ($value === null || $value === '') { continue; } diff --git a/components/ILIAS/Certificate/classes/class.ilCertificateAppEventListener.php b/components/ILIAS/Certificate/classes/class.ilCertificateAppEventListener.php index 51576eb483b3..abaebd213d77 100755 --- a/components/ILIAS/Certificate/classes/class.ilCertificateAppEventListener.php +++ b/components/ILIAS/Certificate/classes/class.ilCertificateAppEventListener.php @@ -195,8 +195,9 @@ private function handleLPUpdate(): void $usr_id, $courseTemplate ); - } catch (ilException $e) { - $this->logger->warning($e->getMessage()); + } catch (Throwable $e) { + $this->logger->error($e->getMessage()); + $this->logger->error($e->getTraceAsString()); continue; } } diff --git a/components/ILIAS/Certificate/tests/ilCertificateCloneActionTest.php b/components/ILIAS/Certificate/tests/ilCertificateCloneActionTest.php index e7ac2f372993..37a1526bbc86 100755 --- a/components/ILIAS/Certificate/tests/ilCertificateCloneActionTest.php +++ b/components/ILIAS/Certificate/tests/ilCertificateCloneActionTest.php @@ -98,6 +98,8 @@ public function testCloneCertificate(): void $objectHelper->method('lookupObjId') ->willReturn(1000); + $objectHelper->method('lookupType') + ->willReturn('crs'); $global_certificate_settings = $this->getMockBuilder(ilObjCertificateSettings::class) ->disableOriginalConstructor() diff --git a/components/ILIAS/Container/Content/ObjectiveView/class.ObjectiveRenderer.php b/components/ILIAS/Container/Content/ObjectiveView/class.ObjectiveRenderer.php index 0618566c71b4..06c1597db6a2 100755 --- a/components/ILIAS/Container/Content/ObjectiveView/class.ObjectiveRenderer.php +++ b/components/ILIAS/Container/Content/ObjectiveView/class.ObjectiveRenderer.php @@ -467,7 +467,7 @@ protected function renderObjective( $has_sections = true; $title = $item['title'] . - " › " . \ilLMObject::_lookupTitle($chapter['obj_id']) . + " › " . \ilLMObject::_lookupTitle($chapter['obj_id']) . " (" . $lng->txt('obj_' . $chapter['type']) . ")"; $item_list_gui2->setDefaultCommandParameters(array( diff --git a/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php b/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php index c2cd20fb9a5d..311e6fad7a19 100755 --- a/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php +++ b/components/ILIAS/Container/Content/ObjectiveView/class.ilContainerObjectiveGUI.php @@ -688,7 +688,7 @@ protected function renderObjective( $has_sections = true; $title = $item['title'] . - " › " . ilLMObject::_lookupTitle($chapter['obj_id']) . + " › " . ilLMObject::_lookupTitle($chapter['obj_id']) . " (" . $lng->txt('obj_' . $chapter['type']) . ")"; $item_list_gui2->setDefaultCommandParameters([ diff --git a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php index dbaf41e7daec..47663169ca2e 100755 --- a/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php +++ b/components/ILIAS/Course/classes/class.ilCourseParticipantsTableGUI.php @@ -113,7 +113,7 @@ public function __construct( $this->addColumn($this->lng->txt('crs_blocked'), 'blocked'); $this->addColumn($this->lng->txt('crs_notification_list_title'), 'notification'); - $this->addColumn($this->lng->txt('actions'), 'optional', '', false, 'ilMembershipRowActionsHeader'); + $this->addColumn($this->lng->txt('actions'), '', '', false, 'ilMembershipRowActionsHeader'); $this->setRowTemplate("tpl.show_participants_row.html", "components/ILIAS/Course"); diff --git a/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsGUI.php b/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsGUI.php index c02dc74d5109..b3eee10d3251 100755 --- a/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsGUI.php +++ b/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsGUI.php @@ -792,9 +792,9 @@ public function editImportForm(): ilPropertyFormGUI public function editImport(ilDidacticTemplateSetting $a_settings): void { - ilDidacticTemplateObjSettings::transferAutoGenerateStatus($a_settings->getId(), $a_settings->getId()); - $assignments = ilDidacticTemplateObjSettings::getAssignmentsByTemplateID($a_settings->getId()); - $a_settings->delete(); + ilDidacticTemplateObjSettings::transferAutoGenerateStatus($this->setting->getId(), $a_settings->getId()); + $assignments = ilDidacticTemplateObjSettings::getAssignmentsByTemplateID($this->setting->getId()); + $this->setting->delete(); foreach ($assignments as $obj) { ilDidacticTemplateObjSettings::assignTemplate($obj["ref_id"], $obj["obj_id"], $a_settings->getId()); } diff --git a/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsTableDataRetrieval.php b/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsTableDataRetrieval.php index 4d8055fe8bcb..0fe63f943b97 100755 --- a/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsTableDataRetrieval.php +++ b/components/ILIAS/DidacticTemplate/classes/Setting/class.ilDidacticTemplateSettingsTableDataRetrieval.php @@ -129,12 +129,11 @@ protected function getRecords(Order $order, Range $range): array foreach ($tpl->getAssignments() as $obj_type) { $icon_label = $this->lng->txt('objs_' . $obj_type); } - if ($icon_path) { - $icon = $this->ui_factory->symbol()->icon()->custom( - $icon_path, - $icon_label - ); - } + + $icon = ($icon_path !== "") ? $this->ui_factory->symbol()->icon()->custom( + $icon_path, + $icon_label + ) : null; $icon_active = $this->ui_factory->symbol()->icon()->custom( $tpl->isEnabled() ? diff --git a/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsDBRepository.php b/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsDBRepository.php index e48511bc58f9..ae2c3e9c7052 100755 --- a/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsDBRepository.php +++ b/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsDBRepository.php @@ -98,4 +98,20 @@ public function saveAssignmentsOfUser( } } } + + public function deleteAssignmentsOfUser( + int $user_id, + int $exc_id + ): void { + $db = $this->db; + + $db->manipulateF( + "DELETE FROM exc_mandatory_random WHERE " . + " exc_id = %s" . + " AND usr_id = %s", + array("integer", "integer"), + array($exc_id, $user_id) + ); + } + } diff --git a/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsManager.php b/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsManager.php index 388633f0561f..a137785eea54 100755 --- a/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsManager.php +++ b/components/ILIAS/Exercise/Assignment/Mandatory/class.RandomAssignmentsManager.php @@ -228,4 +228,10 @@ public function isAssignmentVisible( } return true; } + + public function deleteAssignmentsOfUser( + int $user_id + ): void { + $this->rand_ass_repo->deleteAssignmentsOfUser($user_id, $this->exc_id); + } } diff --git a/components/ILIAS/Exercise/Assignment/class.ilExAssignment.php b/components/ILIAS/Exercise/Assignment/class.ilExAssignment.php index 6ca1fd334003..45d6eb0be093 100755 --- a/components/ILIAS/Exercise/Assignment/class.ilExAssignment.php +++ b/components/ILIAS/Exercise/Assignment/class.ilExAssignment.php @@ -760,7 +760,8 @@ public function save(): void "max_char_limit" => array("integer", $this->getMaxCharLimit()), "relative_deadline" => array("integer", $this->getRelativeDeadline()), "rel_deadline_last_subm" => array("integer", $this->getRelDeadlineLastSubmission()), - "deadline_mode" => array("integer", $this->getDeadlineMode()) + "deadline_mode" => array("integer", $this->getDeadlineMode()), + "solution_rid" => array("text", ''), )); $this->setId($next_id); $exc = new ilObjExercise($this->getExerciseId(), false); diff --git a/components/ILIAS/Exercise/Members/classes/class.ilExerciseMembers.php b/components/ILIAS/Exercise/Members/classes/class.ilExerciseMembers.php index 62cf81b11030..e2e013a2a558 100755 --- a/components/ILIAS/Exercise/Members/classes/class.ilExerciseMembers.php +++ b/components/ILIAS/Exercise/Members/classes/class.ilExerciseMembers.php @@ -25,6 +25,7 @@ */ class ilExerciseMembers { + protected \ILIAS\Exercise\InternalDomainService $domain; protected IndividualDeadlineManager $individual_deadlines; protected ilDBInterface $db; public int $ref_id; @@ -45,6 +46,7 @@ public function __construct(ilObjExercise $a_exc) $this->read(); $this->recommended_content_manager = new ilRecommendedContentManager(); $this->individual_deadlines = $DIC->exercise()->internal()->domain()->individualDeadline(); + $this->domain = $DIC->exercise()->internal()->domain(); } // Get exercise ref id @@ -160,6 +162,10 @@ public function deassignMember(int $a_usr_id): void $idl->delete(); } + // delete random assignments + $random = $this->domain->assignment()->randomAssignments($this->exc); + $random->deleteAssignmentsOfUser($a_usr_id); + // @todo: delete all assignment associations (and their files) } diff --git a/components/ILIAS/Exercise/PeerReview/class.ilExPeerReview.php b/components/ILIAS/Exercise/PeerReview/class.ilExPeerReview.php index a45b7d22b538..f40da7891dff 100755 --- a/components/ILIAS/Exercise/PeerReview/class.ilExPeerReview.php +++ b/components/ILIAS/Exercise/PeerReview/class.ilExPeerReview.php @@ -96,11 +96,12 @@ public function initPeerReviews(): bool foreach ($distribution->getPeersOfRater($rater_id) as $peer_id) { $next_id = $ilDB->nextId("exc_assignment_peer"); $ilDB->manipulate("INSERT INTO exc_assignment_peer" . - " (id, ass_id, giver_id, peer_id)" . + " (id, ass_id, giver_id, peer_id, migrated)" . " VALUES (" . $ilDB->quote($next_id, "integer") . ", " . $ilDB->quote($this->assignment_id, "integer") . ", " . $ilDB->quote($rater_id, "integer") . - ", " . $ilDB->quote($peer_id, "integer") . ")"); + ", " . $ilDB->quote($peer_id, "integer") . + ", " . $ilDB->quote(1, "integer") . ")"); } } } diff --git a/components/ILIAS/Exercise/PeerReview/class.ilExPeerReviewGUI.php b/components/ILIAS/Exercise/PeerReview/class.ilExPeerReviewGUI.php index bc902b78aba9..5bd5d2eaef2b 100755 --- a/components/ILIAS/Exercise/PeerReview/class.ilExPeerReviewGUI.php +++ b/components/ILIAS/Exercise/PeerReview/class.ilExPeerReviewGUI.php @@ -378,7 +378,7 @@ public function buildSubmissionPropertiesAndActions(\ILIAS\Exercise\Assignment\P else { $builder->addProperty( $builder::SEC_PEER_FEEDBACK, - $lng->txt("exc_received_peer_feedback"), + $lng->txt("exc_received_feedback"), $lng->txt("exc_peer_review_show_received_none") ); } diff --git a/components/ILIAS/Exercise/Submission/SubmissionManager.php b/components/ILIAS/Exercise/Submission/SubmissionManager.php index 9c02439198bb..d17a454deb79 100644 --- a/components/ILIAS/Exercise/Submission/SubmissionManager.php +++ b/components/ILIAS/Exercise/Submission/SubmissionManager.php @@ -620,10 +620,12 @@ protected function copySubmissionFilesToDir( $dir = $to_path . DIRECTORY_SEPARATOR . $targetdir; \ilFileUtils::makeDirParents($dir); $file = $dir . DIRECTORY_SEPARATOR . $targetfile; - file_put_contents( - $file, - $stream->getContents() - ); + if (!is_null($stream)) { + file_put_contents( + $file, + $stream->getContents() + ); + } // unzip blog/portfolio diff --git a/components/ILIAS/Exercise/Submission/class.ilExSubmission.php b/components/ILIAS/Exercise/Submission/class.ilExSubmission.php index af3982376574..82329a2826a5 100755 --- a/components/ILIAS/Exercise/Submission/class.ilExSubmission.php +++ b/components/ILIAS/Exercise/Submission/class.ilExSubmission.php @@ -719,8 +719,8 @@ public function addResourceObject( $next_id = $ilDB->nextId("exc_returned"); $query = sprintf( "INSERT INTO exc_returned " . - "(returned_id, obj_id, user_id, filetitle, ass_id, ts, atext, late, team_id) " . - "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", + "(returned_id, obj_id, user_id, filetitle, ass_id, ts, atext, late, team_id, rid) " . + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", $ilDB->quote($next_id, "integer"), $ilDB->quote($this->assignment->getExerciseId(), "integer"), $ilDB->quote($user_id, "integer"), @@ -729,7 +729,8 @@ public function addResourceObject( $ilDB->quote(ilUtil::now(), "timestamp"), $ilDB->quote($a_text, "text"), $ilDB->quote($this->isLate(), "integer"), - $ilDB->quote($team_id, "integer") + $ilDB->quote($team_id, "integer"), + $ilDB->quote('', "text") ); $ilDB->manipulate($query); diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php index b294d1e4a60b..53db9beb92bc 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/TutorFeedbackFileRepository.php @@ -59,6 +59,13 @@ public function createCollection(int $ass_id, int $user_id): void ); } + public function createCollectionIfMissing(int $ass_id, int $user_id): void + { + if (!$this->hasCollection($ass_id, $user_id)) { + $this->createCollection($ass_id, $user_id); + } + } + public function getIdStringForAssIdAndUserId(int $ass_id, int $user_id): string { $set = $this->db->queryF( diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackFileStakeholder.php b/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackFileStakeholder.php index 6989c05bbb44..5fa12530b066 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackFileStakeholder.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackFileStakeholder.php @@ -114,4 +114,10 @@ private function initDB(): void $this->database = $DIC->database(); } } + + public function getConsumerNameForPresentation(): string + { + return "ILIAS/Exercise/TutorFeedbackFile"; + } + } diff --git a/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackZipStakeholder.php b/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackZipStakeholder.php index 7ec35c633672..c22163667cb6 100755 --- a/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackZipStakeholder.php +++ b/components/ILIAS/Exercise/TutorFeedbackFile/class.ilExcTutorFeedbackZipStakeholder.php @@ -95,4 +95,10 @@ private function initDB(): void $this->database = $DIC->database(); } } + + public function getConsumerNameForPresentation(): string + { + return "ILIAS/Exercise/TutorFeedbackZip"; + } + } diff --git a/components/ILIAS/Exercise/classes/class.ilExerciseManagementGUI.php b/components/ILIAS/Exercise/classes/class.ilExerciseManagementGUI.php index 9da79de23dfc..6efee370e777 100755 --- a/components/ILIAS/Exercise/classes/class.ilExerciseManagementGUI.php +++ b/components/ILIAS/Exercise/classes/class.ilExerciseManagementGUI.php @@ -446,13 +446,17 @@ public function membersObject(): void } } elseif ($this->exercise->hasTutorFeedbackFile()) { if (!$this->assignment->getAssignmentType()->usesTeams()) { - // multi-feedback - $ilToolbar->addButton( - $this->lng->txt("exc_multi_feedback"), - $this->ctrl->getLinkTarget($this, "showMultiFeedback") - ); - $ilToolbar->addSeparator(); + $mem_data = $this->assignment->getMemberListData(); + if (count($mem_data) > 0) { + // multi-feedback + $ilToolbar->addButton( + $this->lng->txt("exc_multi_feedback"), + $this->ctrl->getLinkTarget($this, "showMultiFeedback") + ); + + $ilToolbar->addSeparator(); + } } } diff --git a/components/ILIAS/Export/xml/SchemaValidation/ilias_lso_9_0.xsd b/components/ILIAS/Export/xml/SchemaValidation/ilias_lso_9_0.xsd index 8dc84c42f433..b56a197e941a 100644 --- a/components/ILIAS/Export/xml/SchemaValidation/ilias_lso_9_0.xsd +++ b/components/ILIAS/Export/xml/SchemaValidation/ilias_lso_9_0.xsd @@ -39,7 +39,7 @@ - + diff --git a/components/ILIAS/Export/xml/ilias_pg_12.dtd b/components/ILIAS/Export/xml/ilias_pg_12.dtd new file mode 100755 index 000000000000..64e6145fe231 --- /dev/null +++ b/components/ILIAS/Export/xml/ilias_pg_12.dtd @@ -0,0 +1,572 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/ILIAS/File/classes/Capabilities/Permissions.php b/components/ILIAS/File/classes/Capabilities/Permissions.php index e5f3f6147fbf..b4b76d39f550 100644 --- a/components/ILIAS/File/classes/Capabilities/Permissions.php +++ b/components/ILIAS/File/classes/Capabilities/Permissions.php @@ -27,7 +27,7 @@ enum Permissions: string case NONE = 'none'; case VISIBLE = 'visible'; case READ = 'read'; - case VIEW_CONTENT = 'view_content'; + case VIEW_CONTENT = 'file_view_content'; case READ_LP = 'read_learning_progress'; case EDIT_LP = 'edit_learning_progress'; case EDIT_PERMISSIONS = 'edit_permission'; diff --git a/components/ILIAS/File/classes/Settings/Form.php b/components/ILIAS/File/classes/Settings/Form.php index 596be63b313e..a531c0bc703e 100755 --- a/components/ILIAS/File/classes/Settings/Form.php +++ b/components/ILIAS/File/classes/Settings/Form.php @@ -58,8 +58,9 @@ public function asFormGroup(): Group ->withValue($this->settings->getDownloadLimitinMB()) ->withRequired(true) ->withAdditionalTransformation( - $this->refinery->custom()->transformation(function ($value): void { + $this->refinery->custom()->transformation(function ($value) { $this->settings->setDownloadLimitInMB($value); + return $value; }) ); @@ -71,8 +72,9 @@ public function asFormGroup(): Group ) ->withValue($this->settings->getInlineFileExtensions()) ->withAdditionalTransformation( - $this->refinery->custom()->transformation(function ($value): void { + $this->refinery->custom()->transformation(function ($value) { $this->settings->setInlineFileExtensions($value); + return $value; }) ); @@ -84,8 +86,9 @@ public function asFormGroup(): Group ->withValue($this->settings->isShowAmountOfDownloads()) ->withAdditionalTransformation( $this->refinery->custom()->transformation( - function ($value): void { + function ($value) { $this->settings->setShowAmountOfDownloads($value); + return $value; } ) ); @@ -97,8 +100,9 @@ function ($value): void { ) ->withValue($this->settings->isDownloadWithAsciiFileName()) ->withAdditionalTransformation( - $this->refinery->custom()->transformation(function ($value): void { + $this->refinery->custom()->transformation(function ($value) { $this->settings->setDownloadWithAsciiFileName($value); + return $value; }) ); diff --git a/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabase.php b/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabase.php index 8322abf31f91..ed5ea80ca82a 100755 --- a/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabase.php +++ b/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabase.php @@ -45,13 +45,7 @@ public function getPreconditions(Environment $environment): array "object", 2001, ["file"] - ), - new AccessRBACOperationClonedObjective( - "file", - Permissions::READ->value, - Permissions::VIEW_CONTENT->value - ), - + ) ] ); } diff --git a/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabaseSteps.php b/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabaseSteps.php index 6e24cbda965c..714682e9bd07 100755 --- a/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabaseSteps.php +++ b/components/ILIAS/File/classes/Setup/Database/class.ilFileObjectRBACDatabaseSteps.php @@ -132,4 +132,37 @@ public function step_3(): void ] ); } + + public function step_4(): void + { + // remove superfluous view_content operation + $view_content_operation = 'view_content'; + $ops_id = $this->database->queryF( + "SELECT ops_id FROM rbac_operations WHERE operation = %s", + ['text'], + [$view_content_operation] + )->fetchAssoc()['ops_id'] ?? null; + + if ($ops_id === null) { + return; + } + + $this->database->manipulateF( + 'DELETE FROM rbac_ta WHERE ops_id = %s', + ['integer'], + [$ops_id] + ); + + $this->database->manipulateF( + 'DELETE FROM rbac_templates WHERE ops_id = %s', + ['integer'], + [$ops_id] + ); + + $this->database->manipulateF( + 'DELETE FROM rbac_operations WHERE operation = %s', + ['text'], + [$view_content_operation] + ); + } } diff --git a/components/ILIAS/File/classes/trait.ilObjFileInfoProvider.php b/components/ILIAS/File/classes/trait.ilObjFileInfoProvider.php index e1bbde044fc3..c059eb57f2c4 100755 --- a/components/ILIAS/File/classes/trait.ilObjFileInfoProvider.php +++ b/components/ILIAS/File/classes/trait.ilObjFileInfoProvider.php @@ -106,7 +106,9 @@ public function getFileInfoForAuthorsAndAdmins(): array $create_date = ilDatePresentation::formatDate( new ilDateTime( $this->getFileObj()->getCreateDate(), - IL_CAL_DATETIME) + IL_CAL_DATETIME, + 'utc' // most likely the server timezone + ) ); $amount_of_downloads = $this->safeSprintf( @@ -114,7 +116,6 @@ public function getFileInfoForAuthorsAndAdmins(): array $this->getFileObj()->getAmountOfDownloads(), $create_date, ); - } return [ diff --git a/components/ILIAS/Group/classes/class.ilGroupParticipantsTableGUI.php b/components/ILIAS/Group/classes/class.ilGroupParticipantsTableGUI.php index c686b52f1f7e..3ca3d591899b 100755 --- a/components/ILIAS/Group/classes/class.ilGroupParticipantsTableGUI.php +++ b/components/ILIAS/Group/classes/class.ilGroupParticipantsTableGUI.php @@ -88,7 +88,7 @@ public function __construct( $this->addColumn($this->lng->txt('grp_mem_contacts'), 'contact'); $this->addColumn($this->lng->txt('grp_notification'), 'notification'); - $this->addColumn($this->lng->txt('actions'), 'optional', '', false, 'ilMembershipRowActionsHeader'); + $this->addColumn($this->lng->txt('actions'), '', '', false, 'ilMembershipRowActionsHeader'); $this->setDefaultOrderField('roles'); $this->setRowTemplate("tpl.show_participants_row.html", "components/ILIAS/Group"); diff --git a/components/ILIAS/Help/Administration/class.ilHelpModuleTableGUI.php b/components/ILIAS/Help/Administration/class.ilHelpModuleTableGUI.php index e2fab9b5f5aa..7a0f4bc57477 100755 --- a/components/ILIAS/Help/Administration/class.ilHelpModuleTableGUI.php +++ b/components/ILIAS/Help/Administration/class.ilHelpModuleTableGUI.php @@ -60,9 +60,8 @@ public function __construct( $this->setFormAction($ilCtrl->getFormAction($a_parent_obj)); $this->setRowTemplate("tpl.help_module_row.html", "components/ILIAS/Help/Administration"); - $this->addCommandButton("saveOrdering", $lng->txt("sorting_save")); - if ($this->has_write_permission) { + $this->addCommandButton("saveOrdering", $lng->txt("sorting_save")); $this->addMultiCommand("confirmHelpModulesDeletion", $lng->txt("delete")); } } diff --git a/components/ILIAS/ILIASObject/classes/class.ilObject.php b/components/ILIAS/ILIASObject/classes/class.ilObject.php index 0c77757f1da0..45825a269908 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObject.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObject.php @@ -503,7 +503,7 @@ final public static function _lookupOwnerName(int $owner_id): string $owner = null; if ($owner_id != -1) { - if (ilObject::_exists($owner_id)) { + if (ilObjUser::userExists([$owner_id])) { $owner = new ilObjUser($owner_id); } } diff --git a/components/ILIAS/LearningModule/Editing/SubObjectRetrieval.php b/components/ILIAS/LearningModule/Editing/SubObjectRetrieval.php index 66c9327a4b02..0689727c6f11 100644 --- a/components/ILIAS/LearningModule/Editing/SubObjectRetrieval.php +++ b/components/ILIAS/LearningModule/Editing/SubObjectRetrieval.php @@ -95,6 +95,7 @@ public function getData( if (!in_array($this->transl, ["-", ""])) { $trans_title = $this->getChildTitle($child); } + yield [ "id" => $child["child"], "deactivated_elements" => $deactivated_elements, diff --git a/components/ILIAS/LearningModule/Editing/SubObjectTableBuilder.php b/components/ILIAS/LearningModule/Editing/SubObjectTableBuilder.php index 4944684c8640..4a20231dc1cb 100644 --- a/components/ILIAS/LearningModule/Editing/SubObjectTableBuilder.php +++ b/components/ILIAS/LearningModule/Editing/SubObjectTableBuilder.php @@ -75,6 +75,7 @@ protected function transformRow(array $data_row): array { $lng = $this->domain->lng(); $f = $this->gui->ui()->factory(); + $ctrl = $this->gui->ctrl(); if ($data_row["type"] === "pg") { $img_sc = $data_row["scheduled"] ? "_sc" @@ -96,10 +97,28 @@ protected function transformRow(array $data_row): array $img = "standard/icon_st.svg"; $alt = $lng->txt("st"); } + $target = "#"; + if ($data_row["type"] === "pg") { + $ctrl->setParameterByClass(\ilLMPageGUI::class, "obj_id", $data_row["id"]); + $target = $ctrl->getLinkTargetByClass([ + \ilObjLearningModuleGUI::class, + \ilLMPageObjectGUI::class, + \ilLMPageGUI::class + ], "edit"); + } elseif ($data_row["type"] === "st") { + $ctrl->setParameterByClass(\ilStructureObjectGUI::class, "obj_id", $data_row["id"]); + $target = $ctrl->getLinkTargetByClass([ + \ilObjLearningModuleGUI::class, + \ilStructureObjectGUI::class, + EditSubObjectsGUI::class + ], "editPages"); + } + + $title = $f->link()->standard($data_row["title"], $target); return [ "id" => $data_row["id"], "type" => $f->symbol()->icon()->custom(\ilUtil::getImagePath($img), $alt), - "title" => $data_row["title"], + "title" => $title, "trans_title" => $data_row["trans_title"], ]; } @@ -111,127 +130,95 @@ protected function build(TableAdapterGUI $table): TableAdapterGUI $transl = $this->gui->editing()->request()->getTranslation(); $table = $table ->iconColumn("type", $lng->txt("type")) - ->textColumn("title", $lng->txt("title")); + ->linkColumn("title", $lng->txt("title")); if (!in_array($transl, ["-", ""])) { $table = $table->textColumn("trans_title", $lng->txt("title") . " (" . $lng->txt("meta_l_" . $transl) . ")"); } if ($this->type === "st") { - $acts = [ - [ - "editPages", - $lng->txt("edit"), - [\ilObjLearningModuleGUI::class, \ilStructureObjectGUI::class, EditSubObjectsGUI::class], - "editPages", - "obj_id" - ], - [ - "insertChapterAfter", - $lng->txt("lm_insert_chapter_after"), - [EditSubObjectsGUI::class], - "insertChapterAfter", - "target_id" - ], - [ - "insertChapterBefore", - $lng->txt("lm_insert_chapter_before"), - [EditSubObjectsGUI::class], - "insertChapterBefore", - "target_id" - ] - ]; + $table = $table->singleRedirectAction( + "editPages", + $lng->txt("lm_list_pages"), + [\ilObjLearningModuleGUI::class, \ilStructureObjectGUI::class, EditSubObjectsGUI::class], + "editPages", + "obj_id" + ); + $table = $table->singleAction( + "editTitle", + $lng->txt("cont_edit_title"), + true + ); + $table = $table->singleAction( + "insertChapterAfter", + $lng->txt("lm_insert_chapter_after"), + true + ); + $table = $table->singleAction( + "insertChapterBefore", + $lng->txt("lm_insert_chapter_before"), + true + ); if ($user->clipboardHasObjectsOfType("st")) { - $acts[] = [ + $table = $table->singleRedirectAction( "insertChapterClipAfter", $lng->txt("lm_insert_chapter_clip_after"), [EditSubObjectsGUI::class], "insertChapterClipAfter", "target_id" - ]; - $acts[] = [ + ); + $table = $table->singleRedirectAction( "insertChapterClipBefore", $lng->txt("lm_insert_chapter_clip_before"), [EditSubObjectsGUI::class], "insertChapterClipBefore", "target_id" - ]; + ); } } else { - $acts = [ - [ - "editPage", - $lng->txt("edit"), - [\ilObjLearningModuleGUI::class, \ilLMPageObjectGUI::class], - "edit", - "obj_id" - ], - [ - "insertPageAfter", - $lng->txt("lm_insert_page_after"), - [EditSubObjectsGUI::class], - "insertPageAfter", - "target_id" - ], - [ - "insertPageBefore", - $lng->txt("lm_insert_page_before"), - [EditSubObjectsGUI::class], - "insertPageBefore", - "target_id" - ] - ]; + $table = $table->singleRedirectAction( + "editPage", + $lng->txt("lm_edit_content"), + [\ilObjLearningModuleGUI::class, \ilLMPageObjectGUI::class], + "edit", + "obj_id" + ); + $table = $table->singleAction( + "editTitle", + $lng->txt("cont_edit_title"), + true + ); + $table = $table->singleAction( + "insertPageAfter", + $lng->txt("lm_insert_page_after"), + true + ); + $table = $table->singleAction( + "insertPageBefore", + $lng->txt("lm_insert_page_before"), + true + ); if ($user->clipboardHasObjectsOfType("pg")) { - $acts[] = [ + $table = $table->singleRedirectAction( "insertPageClipAfter", $lng->txt("lm_insert_page_clip_after"), [EditSubObjectsGUI::class], "insertPageClipAfter", "target_id" - ]; - $acts[] = [ + ); + $table = $table->singleRedirectAction( "insertPageClipBefore", $lng->txt("lm_insert_page_clip_before"), [EditSubObjectsGUI::class], "insertPageClipBefore", "target_id" - ]; + ); } - if (count($this->page_layouts) > 0) { - $acts[] = [ - "insertLayoutAfter", - $lng->txt("lm_insert_layout_after"), - [EditSubObjectsGUI::class], - "insertLayoutAfter", - "target_id" - ]; - $acts[] = [ - "insertLayoutBefore", - $lng->txt("lm_insert_layout_before"), - [EditSubObjectsGUI::class], - "insertLayoutBefore", - "target_id" - ]; - } - } - foreach ($acts as $a) { - $table = $table->singleRedirectAction( - $a[0], - $a[1], - $a[2], - $a[3], - $a[4] - ); } $table = $table ->standardAction( "delete", $lng->txt("delete") ) - ->singleAction( - "editTitle", - $lng->txt("cont_edit_title"), - true - ) ->standardAction( "cutItems", $lng->txt("cut") diff --git a/components/ILIAS/LearningModule/Editing/class.EditSubObjectsGUI.php b/components/ILIAS/LearningModule/Editing/class.EditSubObjectsGUI.php index dce582e697e8..0f3e0b3c07b1 100644 --- a/components/ILIAS/LearningModule/Editing/class.EditSubObjectsGUI.php +++ b/components/ILIAS/LearningModule/Editing/class.EditSubObjectsGUI.php @@ -74,7 +74,9 @@ public function executeCommand(): void "insertChapterClip", "insertChapterClipBefore", "insertChapterClipAfter", "activatePages", "insertLayoutBefore", "insertLayoutAfter", "insertPageFromLayout", - "switchToLanguage", "editMasterLanguage" + "switchToLanguage", "editMasterLanguage", + "savePageAfter", "savePageBefore", + "saveChapterAfter", "saveChapterBefore", ])) { $this->$cmd(); } @@ -133,29 +135,46 @@ protected function list(): void $ml_head = \ilObjLearningModuleGUI::getMultiLangHeader($this->lm_id, $this); - if ($retrieval->count() === 0) { - if ($this->sub_type === "st") { + if ($this->sub_type === "st") { + $modal = $this->gui->modal( + $lng->txt("lm_insert_chapter") + )->form($this->getAddPageForm("saveChapterAfter"))->getAsyncTriggerButtonComponents( + $lng->txt("lm_insert_chapter"), + $this->gui->ctrl()->getLinkTargetByClass(self::class, "insertChapterAfter"), + false + ); + $this->gui->toolbar()->addComponent($modal["button"]); + $this->gui->toolbar()->addComponent($modal["modal"]); + /* + $this->gui->button( + $lng->txt("lm_insert_chapter"), + $ctrl->getLinkTargetByClass(self::class, "insertFirstChapter") + )->toToolbar();*/ + if ($user->clipboardHasObjectsOfType("st")) { $this->gui->button( - $lng->txt("lm_insert_chapter"), - $ctrl->getLinkTargetByClass(self::class, "insertFirstChapter") + $lng->txt("lm_insert_chapter_clip"), + $ctrl->getLinkTargetByClass(self::class, "insertChapterClip") )->toToolbar(); - if ($user->clipboardHasObjectsOfType("st")) { - $this->gui->button( - $lng->txt("lm_insert_chapter_clip"), - $ctrl->getLinkTargetByClass(self::class, "insertChapterClip") - )->toToolbar(); - } - } else { + } + } else { + $modal = $this->gui->modal( + $lng->txt("lm_insert_page") + )->form($this->getAddPageForm("savePageAfter"))->getAsyncTriggerButtonComponents( + $lng->txt("lm_insert_page"), + $this->gui->ctrl()->getLinkTargetByClass(self::class, "insertPageAfter"), + false + ); + $this->gui->toolbar()->addComponent($modal["button"]); + $this->gui->toolbar()->addComponent($modal["modal"]); + /*$this->gui->button( + $lng->txt("lm_insert_page"), + $ctrl->getLinkTargetByClass(self::class, "insertFirstPage") + )->toToolbar();*/ + if ($user->clipboardHasObjectsOfType("pg")) { $this->gui->button( - $lng->txt("lm_insert_page"), - $ctrl->getLinkTargetByClass(self::class, "insertFirstPage") + $lng->txt("lm_insert_page_clip"), + $ctrl->getLinkTargetByClass(self::class, "insertPageClip") )->toToolbar(); - if ($user->clipboardHasObjectsOfType("pg")) { - $this->gui->button( - $lng->txt("lm_insert_page_clip"), - $ctrl->getLinkTargetByClass(self::class, "insertPageClip") - )->toToolbar(); - } } } $table = $this->getTable(); @@ -296,17 +315,51 @@ public function insertFirstPage(): void $this->sub_obj_id ); } - public function insertPageAfter(): void + public function insertPageAfter(int $id = 0): void { + $lng = $this->domain->lng(); + $this->gui->ctrl()->setParameterByClass( + self::class, + "target_id", + $id + ); + $this->gui->clearAsnyOnloadCode(); + $modal = $this->gui->modal($lng->txt("lm_insert_page"))->form($this->getAddPageForm("savePageAfter")); + $modal->send(); + } + + public function savePageAfter(): void + { + $mt = $this->gui->ui()->mainTemplate(); + $lng = $this->domain->lng(); $target_id = $this->request->getTargetId(); $this->insertPage( $this->sub_obj_id, - $target_id + $target_id, + $this->getTitlesFromForm(), + $this->getLayoutIdFromForm() + ); + $mt->setOnScreenMessage("success", $lng->txt("msg_obj_modified"), true); + $this->gui->ctrl()->redirect($this, "list"); + } + + public function insertPageBefore(int $id): void + { + $lng = $this->domain->lng(); + $this->gui->ctrl()->setParameterByClass( + self::class, + "target_id", + $id ); + $this->gui->clearAsnyOnloadCode(); + $modal = $this->gui->modal($lng->txt("lm_insert_page"))->form($this->getAddPageForm("savePageBefore")); + $modal->send(); } - public function insertPageBefore(): void + public function savePageBefore(): void { + $mt = $this->gui->ui()->mainTemplate(); + $lng = $this->domain->lng(); $parent = $this->sub_obj_id; $target_id = $this->request->getTargetId(); $before_target = \ilTree::POS_FIRST_NODE; @@ -319,30 +372,44 @@ public function insertPageBefore(): void } $this->insertPage( $parent, - $before_target + $before_target, + $this->getTitlesFromForm(), + $this->getLayoutIdFromForm() ); + $mt->setOnScreenMessage("success", $lng->txt("msg_obj_modified"), true); + $this->gui->ctrl()->redirect($this, "list"); } protected function insertPage( int $parent_id = 0, - int $target = \ilTree::POS_LAST_NODE + int $target = 0, + array $titles = [], + int $layout_id = 0 ): void { $lng = $this->domain->lng(); $ctrl = $this->gui->ctrl(); - $chap = new \ilLMPageObject($this->lm); - $chap->setType("pg"); - $chap->setTitle($lng->txt("cont_new_page")); - $chap->setLMId($this->lm_id); - $chap->create(); - \ilLMObject::putInTree($chap, $parent_id, $target); + $page = new \ilLMPageObject($this->lm); + $page->setType("pg"); + $page->setTitle($lng->txt("cont_new_page")); + $page->setLMId($this->lm_id); + $page->create(false, false, $layout_id); + \ilLMObject::putInTree($page, $parent_id, $target); - /* - if ($parent_id === $this->lm_tree->readRootId()) { - $ctrl->setParameterByClass(static::class, "obj_id", 0); - } else { - $ctrl->setParameterByClass(static::class, "obj_id", $parent_id); - }*/ + if (count($titles) > 0) { + \ilLMObject::saveTitle($page->getId(), $titles["-"]); + + $ot = $this->domain->translation($this->lm->getId()); + if ($ot->getContentTranslationActivated()) { + foreach ($ot->getLanguages() as $lang) { + $code = $lang->getLanguageCode(); + if ($code === $ot->getBaseLanguage()) { + continue; + } + \ilLMObject::saveTitle($page->getId(), $titles[$code], $code); + } + } + } $ctrl->redirect($this, "list"); } @@ -353,16 +420,44 @@ public function insertFirstChapter(): void $this->sub_obj_id ); } - public function insertChapterAfter(): void + + public function insertChapterAfter(int $id = 0): void + { + $lng = $this->domain->lng(); + $this->gui->ctrl()->setParameterByClass( + self::class, + "target_id", + $id + ); + $this->gui->clearAsnyOnloadCode(); + $modal = $this->gui->modal($lng->txt("lm_insert_chapter"))->form($this->getEditTitleForm(0, "saveChapterAfter")); + $modal->send(); + } + + public function saveChapterAfter(): void { $target_id = $this->request->getTargetId(); $this->insertChapter( $this->sub_obj_id, - $target_id + $target_id, + $this->getTitlesFromForm() ); } - public function insertChapterBefore(): void + public function insertChapterBefore(int $id): void + { + $lng = $this->domain->lng(); + $this->gui->ctrl()->setParameterByClass( + self::class, + "target_id", + $id + ); + $this->gui->clearAsnyOnloadCode(); + $modal = $this->gui->modal($lng->txt("lm_insert_chapter"))->form($this->getEditTitleForm(0, "saveChapterBefore")); + $modal->send(); + } + + public function saveChapterBefore(): void { $parent = $this->getCurrentParentId(); $target_id = $this->request->getTargetId(); @@ -376,13 +471,15 @@ public function insertChapterBefore(): void } $this->insertChapter( $parent, - $before_target + $before_target, + $this->getTitlesFromForm() ); } protected function insertChapter( int $parent_id = 0, - int $target = \ilTree::POS_LAST_NODE + int $target = \ilTree::POS_LAST_NODE, + array $titles = [] ): void { $lng = $this->domain->lng(); $ctrl = $this->gui->ctrl(); @@ -393,21 +490,42 @@ protected function insertChapter( $chap->create(); \ilLMObject::putInTree($chap, $parent_id, $target); - /* - if ($parent_id === $this->lm_tree->readRootId()) { - $ctrl->setParameterByClass(static::class, "obj_id", 0); - } else { - $ctrl->setParameterByClass(static::class, "obj_id", $parent_id); - }*/ + if (count($titles) > 0) { + \ilLMObject::saveTitle($chap->getId(), $titles["-"]); + + $ot = $this->domain->translation($this->lm->getId()); + if ($ot->getContentTranslationActivated()) { + foreach ($ot->getLanguages() as $lang) { + $code = $lang->getLanguageCode(); + if ($code === $ot->getBaseLanguage()) { + continue; + } + \ilLMObject::saveTitle($chap->getId(), $titles[$code], $code); + } + } + } $ctrl->redirect($this, "list"); } - protected function getEditTitleForm(int $id): FormAdapterGUI + protected function getAddPageForm($cmd): FormAdapterGUI + { + $this->domain->lng()->loadLanguageModule("copg"); + $form = $this->getEditTitleForm(0, $cmd); + $arr_templates = \ilPageLayout::activeLayouts(\ilPageLayout::MODULE_LM); + if (count($arr_templates) > 0) { + $form = $form->optional("use_template", $this->domain->lng()->txt("copg_use_template")); + $form = \ilPageLayoutGUI::addTemplateSelection((string) \ilPageLayout::MODULE_LM, $form); + $form = $form->end(); + } + return $form; + } + + protected function getEditTitleForm(int $id, $cmd = "saveTitle"): FormAdapterGUI { $lng = $this->domain->lng(); $this->gui->ctrl()->setParameterByClass(self::class, "edit_id", $id); - $ot = (new TranslationsRepository($this->domain->database()))->getFor($this->lm->getId()); + $ot = $this->domain->translation($this->lm->getId()); $ml = ""; if ($ot->getContentTranslationActivated()) { $ml = " (" . $lng->txt("meta_l_" . $ot->getBaseLanguage()) . ")"; @@ -415,7 +533,7 @@ protected function getEditTitleForm(int $id): FormAdapterGUI $form = $this ->gui - ->form([self::class], "saveTitle") + ->form([self::class], $cmd) ->text("title", $lng->txt('title') . $ml, "", ilLMObject::_lookupTitle($id), 200); if ($ot->getContentTranslationActivated()) { foreach ($ot->getLanguages() as $lang) { @@ -439,11 +557,44 @@ protected function getEditTitleForm(int $id): FormAdapterGUI public function editTitle(int $id): void { + $lng = $this->domain->lng(); $this->gui->clearAsnyOnloadCode(); - $modal = $this->gui->modal()->form($this->getEditTitleForm($id)); + $modal = $this->gui->modal($lng->txt("cont_edit_title"))->form($this->getEditTitleForm($id)); $modal->send(); } + public function getTitlesFromForm(): array + { + $titles = []; + $form = $this->getEditTitleForm($this->request->getEditId()); + if ($form->isValid()) { + $titles["-"] = $form->getData("title"); + + $ot = $this->domain->translation($this->lm->getId()); + if ($ot->getContentTranslationActivated()) { + foreach ($ot->getLanguages() as $lang) { + $code = $lang->getLanguageCode(); + if ($code === $ot->getBaseLanguage()) { + continue; + } + $titles[$code] = $form->getData("title_" . $code); + } + } + } + return $titles; + } + + public function getLayoutIdFromForm(): int + { + $form = $this->getAddPageForm(""); + if ($form->isValid()) { + if ($form->getData("use_template")) { + return (int) $form->getData("template_id"); + } + } + return 0; + } + public function saveTitle(): void { $mt = $this->gui->mainTemplate(); @@ -463,7 +614,7 @@ public function saveTitle(): void } } } - $mt->setContent("success", $lng->txt("msg_obj_modified"), true); + $mt->setOnScreenMessage("success", $lng->txt("msg_obj_modified"), true); $this->gui->ctrl()->redirect($this, "list"); } @@ -717,6 +868,8 @@ public function activatePages(array $ids): void $ctrl->redirect($this, "list"); } + /* + public function insertLayoutBefore(): void { $this->insertLayout(true); @@ -804,4 +957,6 @@ public function insertPageFromLayout(): void $ctrl->redirect($this, "list"); } + + */ } diff --git a/components/ILIAS/LearningModule/Presentation/class.ilLMPresentationLinker.php b/components/ILIAS/LearningModule/Presentation/class.ilLMPresentationLinker.php index 54370b34d588..0d7ad28141fd 100755 --- a/components/ILIAS/LearningModule/Presentation/class.ilLMPresentationLinker.php +++ b/components/ILIAS/LearningModule/Presentation/class.ilLMPresentationLinker.php @@ -41,6 +41,7 @@ class ilLMPresentationLinker implements \ILIAS\COPage\PageLinker protected bool $export_all_languages; protected string $lang; protected string $export_format; + protected \ILIAS\StaticURL\Services $static_url; public function __construct( ilObjLearningModule $lm, @@ -77,6 +78,7 @@ public function __construct( $this->embed_mode = $embed_mode; $this->frame = $frame; $this->obj_id = $obj_id; + $this->static_url = $DIC["static_url"]; } public function setOffline( @@ -384,18 +386,18 @@ public function getLinkXML( $ltarget = "_blank"; } } else { - if (!$this->offline) { - if ($type == "PageObject") { - $href = "./goto.php?target=pg_" . $target_id . $anc_add; - } else { - $href = "./goto.php?target=st_" . $target_id; - } + if ($type == "PageObject") { + $href = (string) $this->static_url->builder()->build( + "pg", + null, + [$target_id] + ) . $anc_add; } else { - if ($type == "PageObject") { - $href = ILIAS_HTTP_PATH . "/goto.php?target=pg_" . $target_id . $anc_add . "&client_id=" . CLIENT_ID; - } else { - $href = ILIAS_HTTP_PATH . "/goto.php?target=st_" . $target_id . "&client_id=" . CLIENT_ID; - } + $href = (string) $this->static_url->builder()->build( + "st", + null, + [$target_id] + ) . $anc_add; } $ltarget = ""; if ($targetframe == "New" || $this->embed_mode) { @@ -446,11 +448,13 @@ public function getLinkXML( case "RepositoryItem": $obj_type = ilObject::_lookupType((int) $target_id, true); - $obj_id = ilObject::_lookupObjId((int) $target_id); - if (!$this->offline) { - $href = "./goto.php?target=" . $obj_type . "_" . $target_id; + if ((int) $target_id > 0) { + $href = (string) $this->static_url->builder()->build( + $obj_type, + new \ILIAS\Data\ReferenceId($target_id) + ); } else { - $href = ILIAS_HTTP_PATH . "/goto.php?target=" . $obj_type . "_" . $target_id . "&client_id=" . CLIENT_ID; + $href = "#"; } if ($this->embed_mode) { $ltarget = "_blank"; diff --git a/components/ILIAS/LearningModule/Service/class.InternalDomainService.php b/components/ILIAS/LearningModule/Service/class.InternalDomainService.php index 4c697ab07622..34463b00b4d7 100755 --- a/components/ILIAS/LearningModule/Service/class.InternalDomainService.php +++ b/components/ILIAS/LearningModule/Service/class.InternalDomainService.php @@ -22,6 +22,8 @@ use ILIAS\DI\Container; use ILIAS\Repository\GlobalDICDomainServices; +use ILIAS\ILIASObject\Properties\Translations\CachedRepository; +use ILIAS\ILIASObject\Properties\Translations\Translations; class InternalDomainService { @@ -57,4 +59,9 @@ public function subObjectRetrieval( ); } + public function translation(int $lm_id): Translations + { + return (new CachedRepository($this->database()))->getFor($lm_id); + } + } diff --git a/components/ILIAS/LearningModule/classes/class.ilLMObject.php b/components/ILIAS/LearningModule/classes/class.ilLMObject.php index 6aa2d9d07f8c..0ee5f0abad44 100755 --- a/components/ILIAS/LearningModule/classes/class.ilLMObject.php +++ b/components/ILIAS/LearningModule/classes/class.ilLMObject.php @@ -854,11 +854,14 @@ public static function saveTitle(int $id, string $title, string $lang = "-"): vo $lm_id = self::_lookupContObjID($id); $type = self::_lookupType($id); if ($type !== "" && $lm_id > 0) { - $lom_services->manipulate($lm_id, $id, $type) - ->prepareCreateOrUpdate( - $lom_services->paths()->title(), - $title - )->execute(); + try { + $lom_services->manipulate($lm_id, $id, $type) + ->prepareCreateOrUpdate( + $lom_services->paths()->title(), + $title + )->execute(); + } catch (Exception $e) { + } self::_writeTitle($id, $title); } } else { diff --git a/components/ILIAS/LearningModule/classes/class.ilLearningModuleDataSet.php b/components/ILIAS/LearningModule/classes/class.ilLearningModuleDataSet.php index dd584ae7dfb2..5ddfe9523207 100755 --- a/components/ILIAS/LearningModule/classes/class.ilLearningModuleDataSet.php +++ b/components/ILIAS/LearningModule/classes/class.ilLearningModuleDataSet.php @@ -586,7 +586,7 @@ public function importRecord( } $a_mapping->addMapping("components/ILIAS/LearningModule", "lm", $a_rec["Id"], $newObj->getId()); - $a_mapping->addMapping("components/ILIAS/LearningModule", "lm_style", $newObj->getId(), $a_rec["StyleId"]); + $a_mapping->addMapping("components/ILIAS/LearningModule", "lm_style", $newObj->getId(), $a_rec["StyleId"] ?? ""); $a_mapping->addMapping("components/ILIAS/ILIASObject", "obj", $a_rec["Id"], $newObj->getId()); $a_mapping->addMapping( "components/ILIAS/MetaData", diff --git a/components/ILIAS/LearningModule/classes/class.ilLearningModuleImporter.php b/components/ILIAS/LearningModule/classes/class.ilLearningModuleImporter.php index a3c520b71d54..1c63bad94649 100755 --- a/components/ILIAS/LearningModule/classes/class.ilLearningModuleImporter.php +++ b/components/ILIAS/LearningModule/classes/class.ilLearningModuleImporter.php @@ -178,16 +178,15 @@ public function finalProcessing(ilImportMapping $a_mapping): void } } - // assign style - /* + // assign style (old values ilias 7) $alls_map = $a_mapping->getMappingsOfEntity("components/ILIAS/LearningModule", "lm_style"); foreach ($alls_map as $new_lm_id => $old_style_id) { $new_style_id = (int) $a_mapping->getMapping("components/ILIAS/Style", "sty", $old_style_id); if ($new_lm_id > 0 && $new_style_id > 0) { - $lm = new ilObjLearningModule($new_lm_id, false); - $lm->writeStyleSheetId($new_style_id); + ilObjStyleSheet::writeStyleUsage($new_lm_id, $new_style_id); + ilObjStyleSheet::writeOwner($new_lm_id, $new_style_id); } - }*/ + } // menu item ref ids $ref_mapping = $a_mapping->getMappingsOfEntity('components/ILIAS/Container', 'refs'); diff --git a/components/ILIAS/LearningSequence/classes/Export/class.ilLearningSequenceXMLParser.php b/components/ILIAS/LearningSequence/classes/Export/class.ilLearningSequenceXMLParser.php index 70f492df4257..61ce8b9ed344 100644 --- a/components/ILIAS/LearningSequence/classes/Export/class.ilLearningSequenceXMLParser.php +++ b/components/ILIAS/LearningSequence/classes/Export/class.ilLearningSequenceXMLParser.php @@ -104,7 +104,9 @@ public function handleBeginTag( case Writer::TAG_LSITEM: $this->counter = (int) $attributes["ref_id"]; $this->ls_item_data[$this->counter]["ref_id"] = $attributes["ref_id"]; - $this->ls_item_data[$this->counter]["position"] = $attributes["position"]; + if (isset($attributes["position"])) { + $this->ls_item_data[$this->counter]["position"] = $attributes["position"]; + } break; case Writer::TAG_CONDITION: diff --git a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php index 11b61a2cc49e..fdadca6e450c 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php +++ b/components/ILIAS/LearningSequence/classes/class.ilLearningSequenceImporter.php @@ -121,7 +121,9 @@ protected function buildLSItems(array $ls_data, ilImportMapping $mapping): void $item_data["condition_value"] ); $item = $item->withPostCondition($post_condition); - $item = $item->withOrderNumber((int) $item_data["position"]); + if (isset($item_data["position"])) { + $item = $item->withOrderNumber((int) $item_data["position"]); + } $updated[] = $item; } } diff --git a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAdminGUI.php b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAdminGUI.php index 582cbcd217bf..bf3cc48e2bdb 100755 --- a/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAdminGUI.php +++ b/components/ILIAS/LearningSequence/classes/class.ilObjLearningSequenceAdminGUI.php @@ -104,9 +104,12 @@ protected function getForm(array $values = []): Input\Container\Form\Form ) ->withAdditionalTransformation( $this->refinery->custom()->transformation( - fn ($v) => (float) $v + fn($v) => (float) $v ) ); + if (!$this->_checkPermission('edit_permission')) { + $poll_interval = $poll_interval->withDisabled(true); + } if (isset($values[self::F_POLL_INTERVAL])) { $poll_interval = $poll_interval->withValue($values[self::F_POLL_INTERVAL]); @@ -122,7 +125,7 @@ protected function getForm(array $values = []): Input\Container\Form\Form ->standard($target, [$section]) ->withAdditionalTransformation( $this->refinery->custom()->transformation( - fn ($data) => array_shift($data) + fn($data) => array_shift($data) ) ); } @@ -145,6 +148,11 @@ protected function edit(): void protected function save(): void { + if (!$this->_checkPermission('edit_permission')) { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('no_permission')); + $this->edit(); + return; + } $form = $this->getForm()->withRequest($this->request); $data = $form->getData(); if ($data) { @@ -155,4 +163,8 @@ protected function save(): void } $this->show($form); } + public function _checkPermission(string $permission): bool + { + return $this->rbac_system->checkAccess($permission, $this->object->getRefId()); + } } diff --git a/components/ILIAS/Maps/templates/default/tpl.openlayers_map.js b/components/ILIAS/Maps/templates/default/tpl.openlayers_map.js index 2fad41c68bcf..bb9efbf4fa49 100755 --- a/components/ILIAS/Maps/templates/default/tpl.openlayers_map.js +++ b/components/ILIAS/Maps/templates/default/tpl.openlayers_map.js @@ -13,7 +13,6 @@ let ilOLMapData = { ] }; - ilOLUserMarkers["{UMAP_ID}"][{CNT}] = new Array({ULONG}, {ULAT}, "{USER_INFO}<\/span>"); @@ -23,12 +22,43 @@ let openLayer = initIlOpenLayerMaps(jQuery, ilOLInvalidAddress, ilOLMapData, ilO openLayer.forceResize(jQuery); openLayer.init(ilOLMapData); -ilLookupAddress = function(id, address) { - return openLayer.jumpToAddress(id, address); +/* + * Multi-map support: + * This template runs once per map. On pages with multiple maps, global functions + * would otherwise be overwritten by the last rendered map. + * + * Store each map instance by its MAP_ID and dispatch calls by id. + */ +window.ilOLMapInstances = window.ilOLMapInstances || {}; +window.ilOLMapInstances["{MAP_ID}"] = { + openLayer: openLayer, + mapData: ilOLMapData +}; + +/* Define the global API only once; later template runs must not overwrite it. */ +window.ilLookupAddress = window.ilLookupAddress || function(id, address) { + const inst = window.ilOLMapInstances && window.ilOLMapInstances[id]; + if (!inst) { + console.warn("ilLookupAddress: unknown map id", id); + return; + } + return inst.openLayer.jumpToAddress(id, address); }; -ilUpdateMap = function (id) { - return openLayer.updateMap(id); + +window.ilUpdateMap = window.ilUpdateMap || function(id) { + const inst = window.ilOLMapInstances && window.ilOLMapInstances[id]; + if (!inst) { + console.warn("ilUpdateMap: unknown map id", id); + return; + } + return inst.openLayer.updateMap(id); +}; + +window.ilShowUserMarker = window.ilShowUserMarker || function(id, counter) { + const inst = window.ilOLMapInstances && window.ilOLMapInstances[id]; + if (!inst) { + console.warn("ilShowUserMarker: unknown map id", id); + return; + } + return inst.openLayer.moveToUserMarkerAndOpen(id, counter); }; -ilShowUserMarker = function(id, counter) { - return openLayer.moveToUserMarkerAndOpen(id, counter); -}; \ No newline at end of file diff --git a/components/ILIAS/MediaObjects/Creation/class.ilMediaCreationGUI.php b/components/ILIAS/MediaObjects/Creation/class.ilMediaCreationGUI.php index 7141d5766799..2e18477fb7e3 100755 --- a/components/ILIAS/MediaObjects/Creation/class.ilMediaCreationGUI.php +++ b/components/ILIAS/MediaObjects/Creation/class.ilMediaCreationGUI.php @@ -170,7 +170,7 @@ protected function getMimeTypes($local_only = false): array { $mimes = []; if (in_array(self::TYPE_ALL, $this->accept_types)) { - $mimes = iterator_to_array($this->type_manager->getAllowedMimeTypes()); + $mimes = iterator_to_array($this->type_manager->getAllowedMimeTypes($local_only)); } if (in_array(self::TYPE_VIDEO, $this->accept_types)) { $mimes = array_merge($mimes, iterator_to_array($this->type_manager->getAllowedVideoMimeTypes($local_only))); @@ -287,7 +287,7 @@ public function initUrlForm(): ilPropertyFormGUI // $ti = new \ilTextInputGUI($lng->txt("mob_url"), "url"); $info = $lng->txt("mob_url_info1") . " " . implode(", ", $this->getSuffixes()) . "."; - if (in_array(self::TYPE_VIDEO, $this->accept_types)) { + if (in_array(self::TYPE_VIDEO, $this->accept_types) || in_array(self::TYPE_ALL, $this->accept_types)) { $info .= " " . $lng->txt("mob_url_info_video"); } $ti->setInfo($info); diff --git a/components/ILIAS/MediaObjects/MediaType/class.MediaTypeManager.php b/components/ILIAS/MediaObjects/MediaType/class.MediaTypeManager.php index 0c5914549ab8..3bf78e2c7c99 100755 --- a/components/ILIAS/MediaObjects/MediaType/class.MediaTypeManager.php +++ b/components/ILIAS/MediaObjects/MediaType/class.MediaTypeManager.php @@ -220,9 +220,13 @@ public function getMimeTypes(): \Iterator } } - public function getAllowedMimeTypes(): \Iterator + public function getAllowedMimeTypes(bool $local_only = false): \Iterator { - return $this->getAllowedSubset($this->getMimeTypes()); + foreach ($this->getAllowedSubset($this->getMimeTypes()) as $mime) { + if (!$local_only || !in_array($mime, ["video/vimeo", "video/youtube"])) { + yield $mime; + } + } } public function getVideoMimeTypes(bool $local_only = false): \Iterator diff --git a/components/ILIAS/Membership/classes/class.ilAttendanceList.php b/components/ILIAS/Membership/classes/class.ilAttendanceList.php index 5a59245fa8a8..08b02216a4fb 100755 --- a/components/ILIAS/Membership/classes/class.ilAttendanceList.php +++ b/components/ILIAS/Membership/classes/class.ilAttendanceList.php @@ -37,6 +37,7 @@ class ilAttendanceList protected ilObject $parent_obj; protected ?ilParticipants $participants; protected ?ilWaitingList $waiting_list; + protected ilTree $tree; /** * @var ?callable */ @@ -67,7 +68,7 @@ public function __construct( $this->ctrl = $DIC->ctrl(); $this->tpl = $DIC->ui()->mainTemplate(); $this->profile = $DIC['user']->getProfile(); - + $this->tree = $DIC->repositoryTree(); $this->parent_gui = $a_parent_gui; $this->parent_obj = $a_parent_obj; $this->participants = $a_participants_object; @@ -137,10 +138,14 @@ protected function readOrderedExportableFields(): bool ); } + $parent_obj_type = $this->tree->checkForParentType($this->parent_obj->getRefId(), 'crs') ? 'crs' : ''; + $parent_obj_type = $this->tree->checkForParentType($this->parent_obj->getRefId(), 'grp') ? 'grp' : $parent_obj_type; + $user_defined_fields = $parent_obj_type === '' + ? $this->profile->getAllUserDefinedFields() + : $this->profile->getVisibleUserDefinedFields(Context::buildFromObjectType($parent_obj_type)); + // add udf fields - foreach ($this->profile->getVisibleUserDefinedFields( - Context::buildFromObjectType($this->parent_obj->getType()) - ) as $field) { + foreach ($user_defined_fields as $field) { $this->presets['udf_' . $field->getIdentifier()] = array( $field->getLabel($this->lng), false diff --git a/components/ILIAS/Membership/classes/class.ilMembershipGUI.php b/components/ILIAS/Membership/classes/class.ilMembershipGUI.php index 4d0b6609128a..e4022452aa3a 100755 --- a/components/ILIAS/Membership/classes/class.ilMembershipGUI.php +++ b/components/ILIAS/Membership/classes/class.ilMembershipGUI.php @@ -61,6 +61,7 @@ public function __construct(ilObjectGUI $repository_gui, ilObject $repository_ob $this->tpl = $DIC->ui()->mainTemplate(); $this->ctrl = $DIC->ctrl(); $this->lng->loadLanguageModule('trac'); + $this->lng->loadLanguageModule('mmbr'); $this->logger = $DIC->logger()->mmbr(); $this->access = $DIC->access(); $this->user = $DIC->user(); @@ -575,8 +576,13 @@ public function updateParticipants(): void $post_roles[$usr_id][] = $adminRoleId; } + if (!isset($post_roles[$usr_id]) || empty($post_roles[$usr_id])) { + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('mmbr_role_error'), true); + $this->ctrl->redirect($this, 'participants'); + } + // Validate the role ids in the post data - foreach ((array) $post_roles[$usr_id] as $role_id) { + foreach ((array) ($post_roles[$usr_id] ?? []) as $role_id) { if (!array_key_exists($role_id, $assignableLocalRoles)) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt('msg_no_perm_perm'), true); $this->ctrl->redirect($this, 'participants'); @@ -618,7 +624,7 @@ public function updateParticipants(): void } foreach ($participants as $usr_id) { - $this->getMembersObject()->updateRoleAssignments($usr_id, (array) $post_roles[$usr_id]); + $this->getMembersObject()->updateRoleAssignments($usr_id, (array) ($post_roles[$usr_id] ?? [])); // Disable notification for all of them $this->getMembersObject()->updateNotification($usr_id, false); @@ -1186,7 +1192,6 @@ public function confirmRefuseSubscribers(): void $this->tpl->setOnScreenMessage('failure', $this->lng->txt("crs_no_subscribers_selected"), true); $this->ctrl->redirect($this, 'participants'); } - $this->lng->loadLanguageModule('mmbr'); $c_gui = new ilConfirmationGUI(); // set confirm/cancel commands $c_gui->setFormAction($this->ctrl->getFormAction($this, "refuseSubscribers")); @@ -1419,7 +1424,6 @@ public function confirmRefuseFromList(): void $this->tpl->setOnScreenMessage('failure', $this->lng->txt("no_checkbox"), true); $this->ctrl->redirect($this, 'participants'); } - $this->lng->loadLanguageModule('mmbr'); $c_gui = new ilConfirmationGUI(); // set confirm/cancel commands diff --git a/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php b/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php index 8aba3b2b070d..59400714c05f 100755 --- a/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php +++ b/components/ILIAS/OpenIdConnect/classes/class.ilAuthProviderOpenIdConnect.php @@ -24,6 +24,9 @@ class ilAuthProviderOpenIdConnect extends ilAuthProvider { private const OIDC_AUTH_IDTOKEN = 'oidc_auth_idtoken'; + private const ERR_AUTH_FAILED = 'auth_oidc_failed'; + private const ERR_AUTH_WRONG_LOGIN = 'err_wrong_login'; + private readonly ilOpenIdConnectSettings $settings; /** @var array $body */ private readonly ilLogger $logger; @@ -65,6 +68,13 @@ public function handleLogout(): void public function doAuthentication(ilAuthStatus $status): bool { + if (!$this->settings->getActive()) { + $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); + $status->setTranslatedReason($this->lng->txt(self::ERR_AUTH_FAILED)); + $this->logger->info('Authentication aborted, OIDC authentication is disabled'); + return false; + } + try { $oidc = $this->initClient(); $oidc->setRedirectURL(ILIAS_HTTP_PATH . '/openidconnect.php'); @@ -107,7 +117,7 @@ public function doAuthentication(ilAuthStatus $status): bool $this->logger->warning($e->getMessage()); $this->logger->warning((string) $e->getCode()); $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); - $status->setTranslatedReason($this->lng->txt('auth_oidc_failed')); + $status->setTranslatedReason($this->lng->txt(self::ERR_AUTH_FAILED)); return false; } } @@ -121,7 +131,7 @@ private function handleUpdate(ilAuthStatus $status, $user_info): ilAuthStatus $this->logger->error('Received invalid user credentials: '); $this->logger->dump($user_info, ilLogLevel::ERROR); $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); - $status->setReason('err_wrong_login'); + $status->setReason(self::ERR_AUTH_WRONG_LOGIN); return $status; } @@ -132,7 +142,7 @@ private function handleUpdate(ilAuthStatus $status, $user_info): ilAuthStatus $this->logger->error('Could not determine valid external account, value is empty or not a string.'); $this->logger->dump($user_info, ilLogLevel::ERROR); $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); - $status->setReason('err_wrong_login'); + $status->setReason(self::ERR_AUTH_WRONG_LOGIN); return $status; } @@ -156,7 +166,7 @@ private function handleUpdate(ilAuthStatus $status, $user_info): ilAuthStatus //$_GET['target'] = $this->getCredentials()->getRedirectionTarget();// TODO PHP8-REVIEW Please eliminate this. Mutating the request is not allowed and will not work in ILIAS 8. } catch (ilOpenIdConnectSyncForbiddenException) { $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); - $status->setReason('err_wrong_login'); + $status->setReason(self::ERR_AUTH_WRONG_LOGIN); } return $status; diff --git a/components/ILIAS/PersonalWorkspace/classes/class.ilSingleUserShareGUI.php b/components/ILIAS/PersonalWorkspace/classes/class.ilSingleUserShareGUI.php index 8d989cb5d2e6..fc7e87106443 100755 --- a/components/ILIAS/PersonalWorkspace/classes/class.ilSingleUserShareGUI.php +++ b/components/ILIAS/PersonalWorkspace/classes/class.ilSingleUserShareGUI.php @@ -99,7 +99,7 @@ protected function saveShare(): void } $this->ctrl->returnToParent($this); } else { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('search_no_match'), true); + $this->tpl->setOnScreenMessage('failure', $this->lng->txt('wsp_search_no_match'), true); } } $this->ctrl->redirect($this); diff --git a/components/ILIAS/Poll/classes/class.ilObjPollGUI.php b/components/ILIAS/Poll/classes/class.ilObjPollGUI.php index 183c52b993c0..486177e82fef 100755 --- a/components/ILIAS/Poll/classes/class.ilObjPollGUI.php +++ b/components/ILIAS/Poll/classes/class.ilObjPollGUI.php @@ -274,7 +274,7 @@ public function executeCommand(): void // add entry to navigation history if (!$this->getCreationMode() && $this->getAccessHandler()->checkAccess("read", "", $this->node_id)) { - $link = $this->ctrl->getLinkTargetByClass("ilrepositorygui", "frameset"); + $link = $this->ctrl->getLinkTargetByClass("ilrepositorygui"); $this->nav_history->addItem($this->node_id, $link, "poll"); } diff --git a/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplate.php b/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplate.php index 49934f3c0c60..6070d2f502e8 100755 --- a/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplate.php +++ b/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplate.php @@ -81,9 +81,10 @@ protected function doCloneObject(ilObject2 $new_obj, int $a_target_id, ?int $a_c //copy online status if object is not the root copy object $cp_options = ilCopyWizardOptions::_getInstance($a_copy_id); + /* should be handled by object if (!$cp_options->isRootNode($this->getRefId())) { $new_obj->setOnline($this->isOnline()); - } + }*/ self::cloneBasics($this, $new_obj); diff --git a/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplateGUI.php b/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplateGUI.php index 92fda400c13b..08b9902c89a0 100755 --- a/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplateGUI.php +++ b/components/ILIAS/Portfolio/Template/class.ilObjPortfolioTemplateGUI.php @@ -311,6 +311,7 @@ public function edit(): void $this->ctrl->redirectByClass(SettingsGUI::class); } + /* protected function getEditFormCustomValues(array &$a_values): void { $a_values["online"] = $this->object->isOnline(); @@ -329,8 +330,9 @@ protected function getEditFormCustomValues(array &$a_values): void ); parent::getEditFormCustomValues($a_values); - } + }*/ + /* protected function updateCustom(ilPropertyFormGUI $form): void { $obj_service = $this->object_service; @@ -360,7 +362,7 @@ protected function updateCustom(ilPropertyFormGUI $form): void ) ); } - + */ // // PAGES diff --git a/components/ILIAS/Portfolio/classes/Setup/class.ilPortfolioDBUpdateSteps.php b/components/ILIAS/Portfolio/classes/Setup/class.ilPortfolioDBUpdateSteps.php index 97abb03c3b09..f3c6e8478011 100755 --- a/components/ILIAS/Portfolio/classes/Setup/class.ilPortfolioDBUpdateSteps.php +++ b/components/ILIAS/Portfolio/classes/Setup/class.ilPortfolioDBUpdateSteps.php @@ -63,4 +63,26 @@ public function step_2(): void [2] ); } + + public function step_3(): void + { + $db = $this->db; + + $set = $db->query("SELECT * FROM usr_portfolio"); + while ($rec = $db->fetchAssoc($set)) { + $offline = ($rec["is_online"] == 1) ? 0 : 1; + $db->update( + "object_data", + [ + "offline" => ["integer", $offline] + ], + [ + "obj_id" => ["integer", $rec["id"]], + "type" => ["text", "prtf"] + ] + ); + + } + } + } diff --git a/components/ILIAS/Portfolio/classes/class.ilObjPortfolio.php b/components/ILIAS/Portfolio/classes/class.ilObjPortfolio.php index cb2cf65dfa99..1d7f99bdc8d2 100755 --- a/components/ILIAS/Portfolio/classes/class.ilObjPortfolio.php +++ b/components/ILIAS/Portfolio/classes/class.ilObjPortfolio.php @@ -67,6 +67,7 @@ public static function getPortfoliosOfUser( " ORDER BY od.title"); $res = array(); while ($rec = $ilDB->fetchAssoc($set)) { + $rec["is_online"] = !ilObject::lookupOfflineStatus($rec["id"]); $res[] = $rec; } return $res; diff --git a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBase.php b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBase.php index 515f77479166..1f9ab7e07a92 100755 --- a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBase.php +++ b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioBase.php @@ -25,7 +25,6 @@ abstract class ilObjPortfolioBase extends ilObject2 { protected \ILIAS\Notes\Service $notes; protected ilSetting $setting; - protected bool $online = false; protected bool $comments = false; protected string $bg_color = ""; protected string $font_color = ""; @@ -53,28 +52,12 @@ public function __construct( // PROPERTIES // + /* public function setOnline(bool $a_value): void { $this->online = $a_value; - } - - public function isOnline(): bool - { - return $this->online; - } - - public static function lookupOnline(int $a_id): bool - { - global $DIC; - - $ilDB = $DIC->database(); + }*/ - $set = $ilDB->query("SELECT is_online" . - " FROM usr_portfolio" . - " WHERE id = " . $ilDB->quote($a_id, "integer")); - $row = $ilDB->fetchAssoc($set); - return (bool) $row["is_online"]; - } public function setPublicComments(bool $a_value): void { @@ -137,7 +120,6 @@ protected function doRead(): void " WHERE id = " . $ilDB->quote($this->id, "integer")); $row = $ilDB->fetchAssoc($set); - $this->setOnline((bool) $row["is_online"]); $this->setProfilePicture((bool) $row["ppic"]); $this->setBackgroundColor((string) $row["bg_color"]); $this->setFontColor((string) $row["font_color"]); @@ -159,9 +141,8 @@ protected function doCreate(bool $clone_mode = false): void { $ilDB = $this->db; - $ilDB->manipulate("INSERT INTO usr_portfolio (id,is_online)" . - " VALUES (" . $ilDB->quote($this->id, "integer") . "," . - $ilDB->quote(0, "integer") . ")"); + $ilDB->manipulate("INSERT INTO usr_portfolio (id)" . + " VALUES (" . $ilDB->quote($this->id, "integer") . ")"); } protected function doUpdate(): void @@ -169,7 +150,6 @@ protected function doUpdate(): void $ilDB = $this->db; $fields = array( - "is_online" => array("integer", $this->isOnline()), "ppic" => array("integer", $this->hasProfilePicture()), "bg_color" => array("text", $this->getBackgroundColor()), "font_color" => array("text", $this->getFontColor()) diff --git a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioGUI.php b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioGUI.php index da7b9fe9bc69..9449c09e883e 100755 --- a/components/ILIAS/Portfolio/classes/class.ilObjPortfolioGUI.php +++ b/components/ILIAS/Portfolio/classes/class.ilObjPortfolioGUI.php @@ -245,7 +245,7 @@ protected function setTitleAndDescription(): void ); if ($this->object && - !$this->object->isOnline()) { + $this->object->getOfflineStatus()) { $this->tpl->setAlertProperties(array( array("alert" => true, "property" => $this->lng->txt("status"), @@ -417,52 +417,6 @@ protected function toRepository(): void $this->ctrl->redirectByClass("ilportfoliorepositorygui", "show"); } - protected function initEditForm(): ilPropertyFormGUI - { - $form = new ilPropertyFormGUI(); - $form->setFormAction($this->ctrl->getFormAction($this)); - - // title - $ti = new ilTextInputGUI($this->lng->txt("title"), "title"); - $ti->setSize(min(40, ilObject::TITLE_LENGTH)); - $ti->setMaxLength(ilObject::TITLE_LENGTH); - $ti->setRequired(true); - $ti->setValue($this->object->getTitle()); - $form->addItem($ti); - - // :TODO: online - $online = new ilCheckboxInputGUI($this->lng->txt("online"), "online"); - $online->setChecked($this->object->isOnline()); - $form->addItem($online); - - $this->initEditCustomForm($form); - - $form->setTitle($this->lng->txt("prtf_edit_portfolio")); - $form->addCommandButton("update", $this->lng->txt("save")); - $form->addCommandButton("view", $this->lng->txt("cancel")); - - return $form; - } - - protected function getEditFormCustomValues(array &$a_values): void - { - $a_values["online"] = $this->object->isOnline(); - - parent::getEditFormCustomValues($a_values); - } - - protected function updateCustom(ilPropertyFormGUI $form): void - { - $this->object->setOnline($form->getInput("online")); - - // if portfolio is not online, it cannot be default - if (!$form->getInput("online")) { - ilObjPortfolio::setUserDefault($this->user_id, 0); - } - - parent::updateCustom($form); - } - // // PAGES @@ -874,7 +828,7 @@ protected function getOfflineMessage(): string $lng = $this->lng; $ctrl = $this->ctrl; - if (!$this->object->isOnline()) { + if ($this->object->getOfflineStatus()) { $f = $ui->factory(); $renderer = $ui->renderer(); @@ -898,7 +852,7 @@ protected function setOnlineAndShare(): void $lng = $this->lng; if (ilObjPortfolio::_lookupOwner($this->object->getId()) === $this->user_id) { - $this->object->setOnline(true); + $this->object->setOfflineStatus(false); $this->object->update(); $this->tpl->setOnScreenMessage('success', $lng->txt("prtf_has_been_set_online"), true); } diff --git a/components/ILIAS/Portfolio/classes/class.ilPortfolioAccessHandler.php b/components/ILIAS/Portfolio/classes/class.ilPortfolioAccessHandler.php index 84f2223b7466..d569175484d2 100755 --- a/components/ILIAS/Portfolio/classes/class.ilPortfolioAccessHandler.php +++ b/components/ILIAS/Portfolio/classes/class.ilPortfolioAccessHandler.php @@ -102,7 +102,7 @@ public function checkAccessOfUser( } // #11921 - if (!$pf->isOnline()) { + if ($pf->getOfflineStatus()) { return false; } diff --git a/components/ILIAS/Portfolio/classes/class.ilPortfolioRepositoryGUI.php b/components/ILIAS/Portfolio/classes/class.ilPortfolioRepositoryGUI.php index 88ddd7304f59..123506bd85df 100755 --- a/components/ILIAS/Portfolio/classes/class.ilPortfolioRepositoryGUI.php +++ b/components/ILIAS/Portfolio/classes/class.ilPortfolioRepositoryGUI.php @@ -330,7 +330,7 @@ protected function setOnline(): void $prt_id = $this->port_request->getPortfolioId(); if (ilObjPortfolio::_lookupOwner($prt_id) === $this->user_id) { $portfolio = new ilObjPortfolio($prt_id, false); - $portfolio->setOnline(true); + $portfolio->setOfflineStatus(false); $portfolio->update(); $this->tpl->setOnScreenMessage('success', $lng->txt("saved_successfully"), true); $ilCtrl->redirect($this, "show"); @@ -346,7 +346,7 @@ protected function setOffline(): void $prt_id = $this->port_request->getPortfolioId(); if (ilObjPortfolio::_lookupOwner($prt_id) === $this->user_id) { $portfolio = new ilObjPortfolio($prt_id, false); - $portfolio->setOnline(false); + $portfolio->setOfflineStatus(true); $portfolio->update(); $this->tpl->setOnScreenMessage('success', $lng->txt("saved_successfully"), true); $ilCtrl->redirect($this, "show"); @@ -368,9 +368,9 @@ protected function saveTitles(): void $portfolio->setTitle(ilUtil::stripSlashes($title)); if (in_array($id, $online)) { - $portfolio->setOnline(true); + $portfolio->setOfflineStatus(false); } else { - $portfolio->setOnline(false); + $portfolio->setOfflineStatus(true); } $portfolio->update(); diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php new file mode 100755 index 000000000000..b2cb613898d0 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestions.php @@ -0,0 +1,30 @@ +type = 'qsts'; + parent::__construct($id, $referenced); + } +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php new file mode 100755 index 000000000000..4ea07f76b5d0 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsAccess.php @@ -0,0 +1,27 @@ + + * @ingroup components\ILIASTest + */ +class ilObjQuestionsAccess extends ilObjectAccess +{ +} diff --git a/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php new file mode 100755 index 000000000000..807823abf99d --- /dev/null +++ b/components/ILIAS/Questions/Legacy/Administration/class.ilObjQuestionsGUI.php @@ -0,0 +1,202 @@ +help = $DIC['ilHelp']; + + $this->data_factory = new DataFactory(); + + $local_dic = LocalDIC::dic(); + $this->units_repository = $local_dic[UnitsRepository::class]; + $this->edit_view = $local_dic[Edit::class]; + $this->configuration_repository = $local_dic[ConfigurationRepository::class]; + + $this->type = 'qsts'; + + parent::__construct($a_data, $a_id, $a_call_by_reference, false); + + $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); + + if (!$this->rbac_system->checkAccess('read', $this->object->getRefId())) { + $this->ilias->raiseError($this->lng->txt("msg_no_perm_read_assf"), $this->ilias->error_obj->WARNING); + } + } + + public function executeCommand(): void + { + $next_class = $this->ctrl->getNextClass($this); + $cmd = $this->ctrl->getCmd(); + $this->prepareOutput(); + + switch ($next_class) { + case strtolower(UploadAnswerOptionsGUI::class): + $this->ctrl->forwardCommand(new UploadAnswerOptionsGUI()); + break; + + case strtolower(ilPermissionGUI::class): + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_PERMISSIONS); + $this->ctrl->forwardCommand(new \ilPermissionGUI($this)); + break; + + case strtolower(QstsQuestionPageGUI::class): + $this->edit_view->forwardPageCmds( + $this->tpl, + $this->buildEditQuestionsBaseUri(), + $this->obj_id, + $this->ref_id + ); + break; + + case strtolower(ConfigurationGUI::class): + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_SETTINGS); + $this->ctrl->forwardCommand( + new ConfigurationGUI( + $this->ctrl, + $this->http, + $this->lng, + $this->refinery, + $this->tpl, + $this->ui_factory, + $this->ui_renderer, + $this->configuration_repository + ) + ); + break; + + case strtolower(GlobalConfigurationGUI::class): + $this->tabs_gui->activateTab(self::TAB_IDENTIFIER_UNITS); + $this->ctrl->forwardCommand( + new GlobalConfigurationGUI( + $this->units_repository, + $this->lng, + $this->ctrl, + $this->rbac_system, + $this->tpl, + $this->toolbar, + $this->tabs_gui, + $this->help + ) + ); + break; + + default: + if ($cmd === null || $cmd === '' || $cmd === 'view') { + $cmd = 'viewQuestions'; + } + $cmd .= 'Object'; + $this->$cmd(); + break; + } + } + + public function viewQuestionsObject(): void + { + $this->tabs_gui->activateTab('questions'); + + $this->tpl->setContent( + $this->edit_view->show( + $this->toolbar, + $this->buildEditQuestionsBaseUri(), + $this->object->getId(), + $this->object->getRefId() + )->render($this->ui_renderer) + ); + } + + public function getAdminTabs(): void + { + $this->getTabs(); + } + + protected function getTabs(): void + { + if ($this->rbac_system->checkAccess('read', $this->object->getRefId())) { + $this->tabs_gui->addTab( + self::TAB_IDENTIFIER_QUESTIONS, + $this->lng->txt(self::TAB_IDENTIFIER_QUESTIONS), + $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + ); + + $this->tabs_gui->addTab( + self::TAB_IDENTIFIER_SETTINGS, + $this->lng->txt(self::TAB_IDENTIFIER_SETTINGS), + $this->ctrl->getLinkTargetByClass(ConfigurationGUI::class, ''), + ); + + $this->tabs_gui->addTab( + self::TAB_IDENTIFIER_UNITS, + $this->lng->txt(self::TAB_IDENTIFIER_UNITS), + $this->ctrl->getLinkTargetByClass(GlobalConfigurationGUI::class, ''), + ); + } + + if ($this->rbac_system->checkAccess('edit_permission', $this->object->getRefId())) { + $this->tabs_gui->addTab( + self::TAB_IDENTIFIER_PERMISSIONS, + $this->lng->txt(self::TAB_IDENTIFIER_PERMISSIONS), + $this->ctrl->getLinkTargetByClass([self::class, ilPermissionGUI::class], 'perm'), + ); + } + } + + private function buildEditQuestionsBaseUri(): URI + { + return $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'viewQuestions') + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/LocalDIC.php b/components/ILIAS/Questions/Legacy/LocalDIC.php new file mode 100755 index 000000000000..2bdcb1c4f56c --- /dev/null +++ b/components/ILIAS/Questions/Legacy/LocalDIC.php @@ -0,0 +1,204 @@ + new DataFactory(); + $dic[UuidFactory::class] = static fn($c): UuidFactory => new UuidFactory(); + $dic[MustacheEngine::class] = static fn($c): MustacheEngine + => new MustacheEngine(['escape' => static fn($v) => $v]); + + $dic[UnitsRepository::class] = static fn($c): UnitsRepository => new UnitsRepository( + $DIC['lng'], + $DIC['ilDB'] + ); + $dic[ConfigurationRepository::class] = static fn($c): ConfigurationRepository + => new ConfigurationRepository( + $DIC['ilSetting'], + $DIC['user']->getSettings()->getSettingByDefinitionClass( + CreateMode::class + ), + new \ilSetting('questions') + ); + $dic[AnswerFormFactory::class] = static fn($c): AnswerFormFactory + => new AnswerFormFactory( + $c[UuidFactory::class], + [ + $c[Cloze\Definition::class] + ] + ); + $dic[QuestionsRepository::class] = static fn($c): QuestionsRepository => + new QuestionsRepository( + $DIC['ilDB'], + $DIC['refinery'], + $c[UuidFactory::class], + new PersistenceFactory(), + $c[AnswerFormFactory::class] + ); + $dic[LayoutFactory::class] = static fn($c): LayoutFactory => + new LayoutFactory( + $DIC['ui.factory'], + $DIC['http'], + $DIC['lng'] + ); + $dic[Edit::class] = static fn($c): Edit => new Edit( + $DIC['lng'], + $c[ConfigurationRepository::class], + $DIC['user']->getLoggedInUser(), + $DIC['refinery'], + $DIC['ui.factory'], + $DIC['ui.renderer'], + $DIC['global_screen'], + $DIC['tpl'], + $DIC->contentStyle(), + $DIC['ilCtrl'], + $DIC['http'], + $DIC['ilTabs'], + $DIC->uiService(), + $c[UuidFactory::class], + $c[AnswerFormFactory::class], + $c[QuestionsRepository::class], + $c[LayoutFactory::class] + ); + + $dic[Cloze\Properties\ClozeText\Factory::class] = static fn($c): Cloze\Properties\ClozeText\Factory + => new Cloze\Properties\ClozeText\Factory( + $DIC['refinery'], + $c[MustacheEngine::class], + $c[DataFactory::class]->text() + ); + $dic[Cloze\Properties\Gaps\AnswerOptions\Factory::class] = static fn($c): Cloze\Properties\Gaps\AnswerOptions\Factory + => new Cloze\Properties\Gaps\AnswerOptions\Factory( + $c[UuidFactory::class], + $DIC['refinery'] + ); + $dic[Cloze\Properties\Gaps\Factory::class] = static fn($c): Cloze\Properties\Gaps\Factory + => new Cloze\Properties\Gaps\Factory( + $DIC['refinery'], + $c[UuidFactory::class], + $c[Cloze\Properties\Gaps\AnswerOptions\Factory::class], + [ + new Cloze\Properties\Gaps\Text( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\Numeric( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\Select( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'] + ), + new Cloze\Properties\Gaps\LongMenu( + $DIC['refinery'], + $DIC['lng'], + $DIC['ui.factory'], + $DIC['tpl'] + ) + ] + ); + $dic[Cloze\Properties\Factory::class] = static fn($c): Cloze\Properties\Factory + => new Cloze\Properties\Factory( + $c[Cloze\Properties\ClozeText\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class], + $c[Cloze\Properties\Combinations\Factory::class] + ); + $dic[Cloze\Persistence::class] = static fn($c): Cloze\Persistence + => new Cloze\Persistence( + new TableNameSpaceCore('cloze') + ); + $dic[Cloze\Views\EditGaps::class] = static fn($c): Cloze\Views\EditGaps + => new Cloze\Views\EditGaps( + $DIC['lng'], + $DIC['ui.factory'], + $DIC['refinery'], + $DIC['http'], + $c[Cloze\Properties\Factory::class], + $c[Cloze\Properties\Gaps\Factory::class] + ); + $dic[Cloze\Views\Edit::class] = static fn($c): Cloze\Views\Edit + => new Cloze\Views\Edit( + $DIC['lng'], + $DIC['ui.factory'], + $DIC['ilToolbar'], + $DIC['refinery'], + $DIC['http'], + $c[Cloze\Properties\Factory::class], + $c[Cloze\Properties\ClozeText\Factory::class], + $c[Cloze\Views\EditGaps::class] + ); + $dic[Cloze\Views\Participant::class] = static fn($c): Cloze\Views\Participant + => new Cloze\Views\Participant( + $DIC['tpl'], + $c[MustacheEngine::class] + ); + $dic[Cloze\Definition::class] = static fn($c): Cloze\Definition => new Cloze\Definition( + $c[Cloze\Properties\Factory::class], + $c[Cloze\Persistence::class], + [ + Cloze\Capabilities\Marking::class => new Cloze\Capabilities\Marking(), + Cloze\Capabilities\Feedback::class => new Cloze\Capabilities\Feedback(), + Cloze\Capabilities\Skills::class => new Cloze\Capabilities\Skills() + ], + $c[Cloze\Views\Edit::class], + $c[Cloze\Views\Participant::class] + ); + $dic[Cloze\Properties\Combinations\Factory::class] = static fn($c): Cloze\Properties\Combinations\Factory + => new Cloze\Properties\Combinations\Factory($c[UuidFactory::class]); + + return $dic; + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php new file mode 100644 index 000000000000..cc8e263462ca --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPage.php @@ -0,0 +1,61 @@ +question; + } + + public function setQuestion( + QuestionImplementation $question + ): void { + $this->question = $question; + } + + public function addQuestionText( + string $text + ): void { + $this->buildDom(); + + $lng = $this->user->getLanguage(); + if ($lng === '') { + $lng = 'de'; + } + + $page_element = new \ilPCParagraph($this); + $page_element->create($this, 'pg'); + $page_element->setLanguage($lng); + $page_element->setText($text); + + $this->update(); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php new file mode 100644 index 000000000000..5a671e51ae45 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/QstsQuestionPageConfig.php @@ -0,0 +1,31 @@ +setEnablePCType('Tabs', true); + $this->setEnablePCType('AnswerForm', true); + $this->setEnablePCType('LegacyAnswerFormText', true); + $this->setEnableInternalLinks(false); + $this->setEnablePageToc(true); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php new file mode 100644 index 000000000000..dd338ed92682 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.QstsQuestionPageGUI.php @@ -0,0 +1,56 @@ +getPageId()); + $this->obj->setParentId($obj_id); + $this->obj->setQuestion($question); + $this->setEnabledPageFocus(false); + } + + #[\Override] + public function finishEditing(): void + { + $this->ctrl->redirectToURL($this->return_uri->__toString()); + } + + public function withReturnUri( + URI $return_uri + ): self { + $clone = clone $this; + $clone->return_uri = $return_uri; + return $clone; + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php new file mode 100644 index 000000000000..9d19474ebeea --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerForm.php @@ -0,0 +1,196 @@ +setType('answf'); + } + + #[\Override] + public static function getLangVars(): array + { + return ['ed_insert_pcqst', 'empty_question', 'pc_qst']; + } + + #[\Override] + public function modifyPageContentPostXsl( + string $output, + string $mode, + bool $abstract_only = false + ): string { + if ($this->pg_obj::class !== QstsQuestionPage::class) { + return $output; + } + + global $DIC; + $ui_factory = $DIC['ui.factory']; + $ui_renderer = $DIC['ui.renderer']; + $lng = $DIC['lng']; + $question = $this->pg_obj->getQuestion(); + + return mb_ereg_replace_callback( + self::ANSWER_FORM_PLACEHOLDER, + fn(array $matches): string => $this->renderAnswerForm( + $ui_factory, + $ui_renderer, + $lng, + $question->getAnswerFormPropertiesByIdString($matches[1]) + ), + $output + ); + } + + #[\Override] + public static function afterPageUpdate( + ilPageObject $page, + DOMDocument $domdoc, + string $xml, + bool $creation + ): void { + if ($page::class !== QstsQuestionPage::class || $creation) { + return; + } + + global $DIC; + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + $question_repository = LocalDIC::dic()[Repository::class]; + + /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + $question = $page->getQuestion(); + + $answer_forms = []; + foreach ($dom_util->path($domdoc, '//AnswerForm') as $node) { + $answer_forms[] = $node->getAttribute(self::ANSWER_FORM_ID_ATTRIBUTE); + } + + $question_repository->update( + [$question->withoutDeletedAnswerForms($answer_forms)] + ); + } + + #[\Override] + public static function handleCopiedContent( + DOMDocument $a_domdoc, + bool $a_self_ass = true, + bool $a_clone_mobs = false, + int $new_parent_id = 0, + int $obj_copy_id = 0 + ): void { + global $DIC; + + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + // handle question elements + if ($a_self_ass) { + // copy questions + $path = "//Question"; + $nodes = $dom_util->path($a_domdoc, $path); + foreach ($nodes as $node) { + $qref = $node->getAttribute("QRef"); + + $inst_id = ilInternalLink::_extractInstOfTarget($qref); + $q_id = ilInternalLink::_extractObjIdOfTarget($qref); + + if (!($inst_id > 0)) { + if ($q_id > 0) { + $question = null; + try { + $question = assQuestion::instantiateQuestion($q_id); + } catch (Exception $e) { + } + // check due to #16557 + if (is_object($question) && $question->isComplete()) { + // check if page for question exists + // due to a bug in early 4.2.x version this is possible + if (!ilPageObject::_exists("qpl", $q_id)) { + $question->createPageObject(); + } + + // now copy this question and change reference to + // new question id + $duplicate_id = $question->duplicate(false); + $node->setAttribute("QRef", "il__qst_" . $duplicate_id); + } + } + } + } + } else { + // remove question + $path = "//Question"; + $nodes = $dom_util->path($a_domdoc, $path); + foreach ($nodes as $node) { + $parent = $node->parentNode; + $parent->parentNode->removeChild($parent); + } + } + } + + public function create( + Uuid $answer_form_id + ): void { + $this->createInitialChildNode( + $this->hier_id, + '', + self::ANSWER_FORM_ELEMENT_TAG, + [self::ANSWER_FORM_ID_ATTRIBUTE => $answer_form_id->toString()] + ); + } + + public function getAnswerFormIdStringFromAttribute(): string + { + return $this->getChildNode()->attributes + ->getNamedItem(self::ANSWER_FORM_ID_ATTRIBUTE)->nodeValue; + } + + private function renderAnswerForm( + UIFactory $ui_factory, + UIRenderer $ui_renderer, + Language $lng, + ?AnswerFormProperties $answer_form_properties, + ): string { + if ($answer_form_properties === null) { + return $lng->txt('broken_answer_form'); + } + + return $ui_renderer->render( + $ui_factory->legacy()->latexContent( + $answer_form_properties->getDefinition()->getParticipantView() + ->get( + $answer_form_properties, + null + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php new file mode 100644 index 000000000000..97db48fcfafe --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCAnswerFormGUI.php @@ -0,0 +1,95 @@ +tabs = $DIC['ilTabs']; + $this->ui_renderer = $DIC['ui.renderer']; + $this->data_factory = new DataFactory(); + + $local_dic = LocalDIC::dic(); + $this->edit_view = $local_dic[Edit::class]; + + parent::__construct($pg_obj, $content_obj, $hier_id, $pc_id); + } + + public function executeCommand() + { + $cmd = $this->ctrl->getCmd() . 'Cmd'; + $this->$cmd(); + } + + public function insertCmd(): void + { + $content_obj = new ilPCAnswerForm($this->pg_obj); + $content_obj->setHierId($this->hier_id); + $this->tpl->setContent( + $this->edit_view->createAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'insert') + ), + $this->pg_obj->getParentId(), + $this->pg_obj->getQuestion(), + $content_obj + )->render($this->ui_renderer) + ); + } + + public function editCmd(): void + { + /** @var \ILIAS\Questions\Question\QuestionImplementation $question */ + $question = $this->pg_obj->getQuestion(); + $answer_form_properties = $question->getAnswerFormPropertiesByIdString( + $this->getContentObject()->getAnswerFormIdStringFromAttribute() + ); + + $this->tpl->setContent( + $this->edit_view->editAnswerForm( + $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(self::class, 'edit') + ), + $this->pg_obj->getParentId(), + $question, + $answer_form_properties, + $answer_form_properties->getDefinition() + )->render($this->ui_renderer) + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php new file mode 100644 index 000000000000..a5fdabc3cbfb --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormText.php @@ -0,0 +1,61 @@ +setType('laft'); + } + + #[\Override] + public function modifyPageContentPostXsl( + string $output, + string $mode, + bool $abstract_only = false + ): string { + if ($this->pg_obj::class !== QstsQuestionPage::class) { + return $output; + } + + return mb_ereg_replace_callback( + self::TEXT_PLACEHOLDER, + static fn(array $matches): string => \ilRTE::_replaceMediaObjectImageSrc( + base64_decode($matches[1]) + ), + $output + ); + } + + public function create( + string $legacy_answer_form_text + ): void { + $this->createInitialChildNode( + $this->hier_id, + '', + self::ELEMENT_TAG, + [self::TEXT_ATTRIBUTE => $legacy_answer_form_text] + ); + } +} diff --git a/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php new file mode 100644 index 000000000000..ba24c634cdc7 --- /dev/null +++ b/components/ILIAS/Questions/Legacy/PageEditor/class.ilPCLegacyAnswerFormTextGUI.php @@ -0,0 +1,33 @@ +tpl->setOnScreenMessage(MessageBox::FAILURE, $this->lng->txt('legacy_text_cannot_be_edited'), true); + $this->ctrl->redirectByClass(\QstsQuestionPageGUI::class, 'edit'); + } +} diff --git a/components/ILIAS/Questions/Questions.php b/components/ILIAS/Questions/Questions.php new file mode 100644 index 000000000000..00a64c0ca053 --- /dev/null +++ b/components/ILIAS/Questions/Questions.php @@ -0,0 +1,78 @@ + + new Agent( + $internal[PersistenceFactory::class], + $seek[AnswerFormMigration::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationCloze( + $internal[Persistence::class], + $internal[\EvalMath::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationLongMenu( + $internal[Persistence::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationNumeric( + $internal[Persistence::class], + $internal[\EvalMath::class] + ); + $contribute[AnswerFormMigration::class] = static fn() => new MigrationTextSubset( + $internal[Persistence::class] + ); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, 'js/dist/ParticipantViewLongMenu.js'); + $contribute[User\Settings\UserSettings::class] = fn() => + new Questions\UserSettings\Settings(); + + $internal[Persistence::class] = static fn() => new Persistence( + new TableNameSpaceCore('cloze') + ); + $internal[PersistenceFactory::class] = static fn() => new PersistenceFactory(); + $internal[\EvalMath::class] = static fn() => new \EvalMath(); + } +} diff --git a/components/ILIAS/Questions/README.md b/components/ILIAS/Questions/README.md new file mode 100644 index 000000000000..6b2a3b9fda2b --- /dev/null +++ b/components/ILIAS/Questions/README.md @@ -0,0 +1 @@ +# Questions diff --git a/components/ILIAS/Questions/maintenance.json b/components/ILIAS/Questions/maintenance.json new file mode 100644 index 000000000000..462ad3856cc3 --- /dev/null +++ b/components/ILIAS/Questions/maintenance.json @@ -0,0 +1,14 @@ +{ + "maintenance_model": "Classic", + "first_maintainer": "dstrassner(48931)", + "second_maintainer": "mbecker(27266)", + "implicit_maintainers": [], + "coordinator": [ + "" + ], + "tester": "SIG EA", + "testcase_writer": "Fabian(27631)", + "path": "Modules/Test", + "belong_to_component": "Test & Assessment", + "used_in_components": [] +} \ No newline at end of file diff --git a/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js b/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js new file mode 100644 index 000000000000..47938f3bcc47 --- /dev/null +++ b/components/ILIAS/Questions/resources/js/dist/ParticipantViewLongMenu.js @@ -0,0 +1,115 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + *********************************************************************/ + +(function () { + const longmenu = () => { + const init = (input, autocompleteLength, answerOptions) => { + if (input.nodeName === 'INPUT') { + let longest = answerOptions.reduce((a, b) => { + return a.length > b.length ? a : b; + }); + input.setAttribute('size', longest.length); + input.addEventListener( + 'keyup', + (e) => { keyHandler(autocompleteLength, answerOptions, e); } + ); + }; + }; + + const keyHandler = (autocompleteLength, answerOptions, e) => { + if (e.key === 'Enter' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + onSelectHandler(e); + return; + } + + if (e.key === 'ArrowDown') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.firstElementChild.focus(); + } + + if (e.target.nodeName === 'LI' && e.target.nextElementSibling !== null) { + e.target.nextElementSibling.focus(); + } + return; + } + + if (e.key === 'ArrowUp' && e.target.nodeName === 'LI') { + e.stopImmediatePropagation(); + e.preventDefault(); + if (e.target.previousElementSibling === null) { + e.target.parentElement.previousElementSibling.focus(); + } else { + e.target.previousElementSibling.focus(); + } + return; + } + + onChangeHandler(autocompleteLength, answerOptions, e); + }; + + const onChangeHandler = (autocompleteLength, answerOptions, e) => { + if (e.target.nextElementSibling?.nodeName === 'UL') { + e.target.nextElementSibling.remove(); + } + + if (e.key === 'Tab' || e.target.value.length < autocompleteLength) { + return; + } + + const matchingAnswers = answerOptions.filter((answer) => { + return answer.toLowerCase().includes(e.target.value.toLowerCase()) + }); + + if (matchingAnswers.length === 0) { + return; + } + + let list = document.createElement('ul'); + matchingAnswers.forEach((answer) => { + let listElement = document.createElement('li'); + listElement.tabIndex = 0; + listElement.textContent = answer; + list.appendChild(listElement); + }); + list.addEventListener('click', onSelectHandler); + list.addEventListener('keyup', onSelectHandler); + e.target.parentNode.appendChild(list); + }; + + const onSelectHandler = (e) => { + if (e.type === 'keydown' && e.key !== 'Enter') { + return; + } + e.target.parentNode.previousElementSibling.value = e.target.textContent; + e.target.parentNode.previousElementSibling.focus(); + e.target.parentNode.remove(); + }; + + const public_interface = { + init + }; + return public_interface; + }; + + il = il || {}; + il.test = il.test || {}; + il.test.player = il.test.player || {}; + il.test.player.longmenu = longmenu(); +}()); diff --git a/components/ILIAS/Questions/service.xml b/components/ILIAS/Questions/service.xml new file mode 100644 index 000000000000..2f7a463b3da2 --- /dev/null +++ b/components/ILIAS/Questions/service.xml @@ -0,0 +1,21 @@ + + + + + adm + + + + + + + + + + + + + + + diff --git a/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php new file mode 100755 index 000000000000..34ddc3dd9a1a --- /dev/null +++ b/components/ILIAS/Questions/src/Administration/ConfigurationRepository.php @@ -0,0 +1,85 @@ +user_setting_create_mode->isChangeableByUser(); + } + + public function getGlobalCreateMode(): CreateModes + { + return CreateModes::tryFrom( + $this->questions_settings->get( + self::SETTINGS_KEY_CREATE_MODE, + '' + ) + ) ?? CreateModes::getDefaultMode(); + } + + public function isCreateModeSimple( + EnvironmentImplementation $environment + ): bool { + return $this->isCreateModeChangeableByUser() && $environment->isCreateModeSimple() + || $this->getGlobalCreateMode() === CreateModes::Simple; + } + + public function persistCreateMode( + CreateModes $create_mode + ): void { + $this->questions_settings->set( + self::SETTINGS_KEY_CREATE_MODE, + $create_mode->value + ); + } + + public function getInputForCreateMode( + FieldFactory $field_factory, + Language $lng, + Refinery $refinery + ): Input { + return $this->user_setting_create_mode->getInput( + $field_factory, + $lng, + $refinery, + $this->common_settings + ); + } +} diff --git a/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php new file mode 100755 index 000000000000..adf3a315a0a7 --- /dev/null +++ b/components/ILIAS/Questions/src/Administration/class.ConfigurationGUI.php @@ -0,0 +1,115 @@ +ctrl->getCmd(self::CMD_DEFAULT) . 'Cmd'; + $this->$cmd(); + } + + private function viewCmd( + ?StandardForm $form = null + ): void { + $this->tpl->setContent( + $this->ui_renderer->render( + $form ?? $this->buildSettingsForm() + ) + ); + } + + private function saveCmd(): void + { + $form = $this->buildSettingsForm()->withRequest($this->http->request()); + $data = $form->getData(); + if ($data === null) { + $this->viewCmd($form); + } + + $this->repository->persistCreateMode( + $data['default_user_settings']['create_mode'] + ); + + $this->ctrl->redirectByClass( + [ + \ilAdministrationGUI::class, + \ilObjQuestionsGUI::class, + self::class + ] + ); + } + + private function buildSettingsForm(): StandardForm + { + return $this->ui_factory->input()->container()->form()->standard( + $this->ctrl->getLinkTargetByClass( + [ + \ilAdministrationGUI::class, + \ilObjQuestionsGUI::class, + self::class + ], + self::CMD_SAVE + ), + [ + 'default_user_settings' => $this->ui_factory->input()->field()->section( + [ + 'create_mode' => $this->repository->getInputForCreateMode( + $this->ui_factory->input()->field(), + $this->lng, + $this->refinery + ) + ], + $this->lng->txt('default_user_settings') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + static fn(string $v): CreateModes => CreateModes::tryFrom($v) + ?? CreateModes::getDefaultMode() + ) + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php new file mode 100644 index 000000000000..18f055fc1589 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Capabilities/Capability.php @@ -0,0 +1,26 @@ + $available_answer_form_types + */ + private readonly array $available_answer_form_types; + + /** + * @param array<\ILIAS\Questions\AnswerForm\Definition> $available_answer_form_types + */ + public function __construct( + private readonly UuidFactory $uuid_factory, + array $available_answer_form_types + ) { + $this->available_answer_form_types = array_reduce( + $available_answer_form_types, + function (array $c, Definition $v) { + $c[$this->getHashedClass($v::class)] = $v; + return $c; + }, + [] + ); + } + + /** + * + * @return array + */ + public function getAvailableDefinitions(): array + { + return array_values($this->available_answer_form_types); + } + + public function getDefinitionForClass( + string $class + ): Definition { + $definition = $this->available_answer_form_types[$this->getHashedClass($class)] ?? null; + if ($definition === null) { + throw new InvalidArgumentException('This type of answer form does not exist.'); + } + return $definition; + } + + /** + * @return array + */ + public function getAnswerFormTypesArrayForSelect( + Language $lng + ): array { + return array_reduce( + $this->available_answer_form_types, + function (array $c, Definition $v) use ($lng): array { + $c[$this->getHashedClass($v::class)] = $v->getLabel($lng); + return $c; + }, + [] + ); + } + + public function getHashedClass(string $class): string + { + return md5($class); + } + + public function buildTypeDefinitionFromSelectValue( + string $value + ): Definition { + $type = $this->available_answer_form_types[$value] ?? null; + if ($type === null) { + throw new InvalidArgumentException('This type of answer form does not exist.'); + } + return $type; + } + + public function getDefaultTypeGenericProperties( + Uuid $question_id, + Definition $type, + ?Uuid $answer_form_id = null + ): TypeGenericProperties { + return new TypeGenericProperties( + $answer_form_id ?? $this->uuid_factory->uuid4(), + $question_id, + $type + ); + } + + public function buildTypeGenericPropertiesFromDatabase( + array $db_values + ): TypeGenericProperties { + return new TypeGenericProperties( + $this->uuid_factory->fromString($db_values['id']), + $this->uuid_factory->fromString($db_values['question_id']), + $this->getDefinitionForClass($db_values['type']), + $db_values['available_points'], + $db_values['image_size'], + $db_values['shuffle_answer_options'] === 1, + $db_values['additional_text'], + $db_values['additional_text_legacy'] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php new file mode 100644 index 000000000000..326bb0349021 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/Migration.php @@ -0,0 +1,42 @@ +db; + } + + public function getIO(): IOWrapper + { + return $this->io; + } + + public function getUuid(): Uuid + { + return $this->uuid_factory->uuid4(); + } + + public function getPersistenceFactory(): PersistenceFactory + { + return $this->persistence_factory; + } + + public function getTableNameBuilder(): TableNameBuilder + { + return $this->table_name_builder; + } + + public function getOldQuestionId(): int + { + return $this->old_question_id; + } + + public function getAnswerFormId(): Uuid + { + return $this->answer_form_id; + } + + public function wasIliasPageEditorUsedForAdditionalTexts(): bool + { + return $this->ilias_page_editor_used_for_additional_texts; + } + + public function withAvailablePoints( + float $available_points + ): self { + $clone = clone $this; + $clone->available_points = $available_points; + return $clone; + } + + public function withImageSize( + int $image_size + ): self { + $clone = clone $this; + $clone->image_size = $image_size; + return $clone; + } + + public function withShuffleAnswerOptions( + bool $shuffle_answer_options + ): self { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + + public function withAdditionalText( + string $additional_text + ): self { + $clone = clone $this; + $clone->additional_text = $additional_text; + return $clone; + } + + public function withAdditionalTextLegacy( + string $additional_text_legacy + ): self { + $clone = clone $this; + $clone->additional_text_legacy = $additional_text_legacy; + return $clone; + } + + public function withAdditionalInsert( + Insert $insert + ): self { + $clone = clone $this; + $clone->inserts[] = $insert; + return $clone; + } + + public function run(): void + { + $this->inserts[] = $this->buildCoreAnswerFormInsertStatement(); + $atom_query = $this->db->buildAtomQuery(); + + $manipulates = []; + $locked_tables = []; + foreach ($this->inserts as $statement) { + $table_to_lock = $statement->getTableToLock(); + if (!in_array($table_to_lock, $locked_tables)) { + $atom_query->addTableLock($table_to_lock); + $locked_tables[] = $table_to_lock; + } + $manipulates[] = $statement->toManipulateString($this->db); + } + $atom_query->addQueryCallable( + function (\ilDBInterface $db) use ($manipulates): void { + foreach ($manipulates as $manipulate) { + $db->manipulate($manipulate); + } + } + ); + $atom_query->run(); + } + + private function buildCoreAnswerFormInsertStatement(): Insert + { + return $this->persistence_factory->insert( + CoreTables::AnswerForms->getColumns( + $this->persistence_factory + ), + [ + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->definition_class + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->new_question_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->shuffle_answer_options === null + ? null + : ($this->shuffle_answer_options ? 1 : 0) + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text_legacy + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php new file mode 100755 index 000000000000..2db2d50a90f7 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/MigrationPurifier.php @@ -0,0 +1,117 @@ +set('HTML.DefinitionID', 'migration'); + $config->set('HTML.DefinitionRev', 2); + $config->set('Cache.SerializerPath', sys_get_temp_dir()); + $config->set('HTML.Doctype', 'XHTML 1.0 Strict'); + $config->set('HTML.AllowedElements', $this->getAllowedElements()); + $config->set('HTML.ForbiddenAttributes', 'div@style'); + $config->autoFinalize = false; + $config->set( + 'URI.AllowedSchemes', + array_merge( + $config->get('URI.AllowedSchemes'), + ['data' => true] + ) + ); + $config->autoFinalize = true; + if (($def = $config->maybeGetRawHTMLDefinition()) !== null) { + $def->addAttribute('img', 'data-id', 'Number'); + $def->addAttribute('a', 'target', 'Enum#_blank,_self,_target,_top'); + } + + return $config; + } + + private function getAllowedElements(): array + { + return $this->removeUnsupportedElements( + $this->makeElementListTinyMceCompliant( + $this->getElementsUsedForAdvancedEditing() + ) + ); + } + + private function getElementsUsedForAdvancedEditing(): array + { + $tags = $this->db->fetchObject( + $this->db->query( + 'SELECT value FROM settings' . PHP_EOL + . 'WHERE module = "advanced_editing"' . PHP_EOL + . 'AND keyword = "advanced_editing_used_html_tags_assessment"' + ) + )?->value ?? ''; + + if ($tags === '') { + return self::DEFAULT_TAGS; + } + + return unserialize($tags, ['allowed_classes' => false]); + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php b/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php new file mode 100644 index 000000000000..ea64832261d8 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Migration/SanitizeLegacyText.php @@ -0,0 +1,60 @@ +purifiery === null) { + $this->purifiery = new MigrationPurifier($db); + $this->rte_used = $this->fetchIsRteUsedFromDb($db); + } + $cleaned_text = $this->purifiery->purify($text); + + if ($ilias_page_editor_text || !$this->rte_used) { + $cleaned_text = nl2br($cleaned_text); + } + + return \ilLegacyFormElementsUtil::prepareTextareaOutput( + $cleaned_text, + true + ); + } + + private function fetchIsRteUsedFromDb( + \ilDBInterface $db + ): bool { + return $db->fetchObject( + $db->query( + 'SELECT value FROM settings' . PHP_EOL + . 'WHERE module = "advanced_editing"' . PHP_EOL + . 'AND keyword = "advanced_editing_javascript_editor"' + ) + )?->value === 'tinymce'; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Persistence.php b/components/ILIAS/Questions/src/AnswerForm/Persistence.php new file mode 100644 index 000000000000..f4aba32eca06 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Persistence.php @@ -0,0 +1,60 @@ +answer_form_id; + } + + public function getQuestionId(): Uuid + { + return $this->question_id; + } + + public function getDefinition(): ?Definition + { + return $this->definition; + } + + public function getAvailablePoints(): ?float + { + return $this->available_points; + } + + public function getImageSize(): ?int + { + return $this->image_size; + } + + public function getShuffleAnswerOptions(): ?bool + { + return $this->shuffle_answer_options; + } + + public function getAdditionalText(): string + { + return $this->additional_text; + } + + public function getAdditionalTextLegacy(): string + { + return $this->additional_text_legacy; + } + + #[\Override] + public function toStorage( + Manipulate $manipulate + ): Manipulate { + if ($this->definition === null) { + throw new \UnexpectedValueException( + 'You cannot save a Answer Form without a Type!' + ); + } + return $manipulate->withAdditionalStatement( + $manipulate->getManipulationType() === ManipulationType::Create + ? $this->buildInsertStatement($manipulate->getPersistenceFactory()) + : $this->buildUpdateStatement($manipulate->getPersistenceFactory()) + ); + } + + #[\Override] + public function toDelete( + Manipulate $manipulate + ): Manipulate { + $answer_form_table_definition = CoreTables::AnswerForms; + + return $manipulate->withAdditionalStatement( + $manipulate->getPersistenceFactory()->delete( + $answer_form_table_definition->getTable( + $manipulate->getPersistenceFactory() + ), + [ + $manipulate->getPersistenceFactory()->where( + $answer_form_table_definition->getIdColumn( + $manipulate->getPersistenceFactory() + ), + $manipulate->getPersistenceFactory()->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] + ) + ); + } + + private function buildInsertStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::AnswerForms->getColumns( + $persistence_factory + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->definition::class + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->question_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->getShuffleAnswerOptionsForStorage() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text_legacy + ) + ] + ); + } + + private function buildUpdateStatement( + PersistenceFactory $persistence_factory + ): Update { + $answer_form_table_definition = CoreTables::AnswerForms; + return $persistence_factory->update( + $answer_form_table_definition->getColumns( + $persistence_factory, + [ + 'id', + 'type', + 'question_id', + 'additional_text_legacy' + ] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->image_size + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->getShuffleAnswerOptionsForStorage() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->additional_text + ) + + ], + [ + $persistence_factory->where( + $answer_form_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] + ); + } + + private function getShuffleAnswerOptionsForStorage(): ?int + { + if ($this->shuffle_answer_options === null) { + return null; + } + + return $this->shuffle_answer_options ? 1 : 0; + } +} diff --git a/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php new file mode 100644 index 000000000000..aa4106783d8c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerForm/Views/Edit.php @@ -0,0 +1,48 @@ + $available_capabilities + */ + public function __construct( + private readonly PropertiesFactory $properties_factory, + private readonly Persistence $persistence, + private readonly array $available_capabilities, + private readonly Edit $edit_view, + private readonly Participant $participant_view + ) { + } + + #[\Override] + public function getLabel( + Language $lng + ): string { + return $lng->txt('assClozeTest'); + } + + #[\Override] + public function buildProperties( + TypeGenericProperties $type_generic_data, + ?Query $query + ): Properties { + return $this->properties_factory->fromData( + $type_generic_data, + $query + ); + } + + #[\Override] + public function getPersistence(): Persistence + { + return $this->persistence; + } + + #[\Override] + public function hasCapability( + string $capability_class_name + ): bool { + return array_key_exists($capability_class_name, $this->available_capabilities); + } + + #[\Override] + public function getCapability( + string $capability_class_name + ): ?Capability { + return $this->available_capabilities[$capability_class_name]; + } + + #[\Override] + public function getEditView(): Edit + { + return $this->edit_view; + } + + #[\Override] + public function getParticipantView(): Participant + { + return $this->participant_view; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php new file mode 100644 index 000000000000..eb9d6b9b3f82 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/CombinationsOverview.php @@ -0,0 +1,363 @@ +initializeModal($this->buildSetCombinationGapsModal()), + $this->buildTable() + ]; + if ($this->modal !== null) { + $content[] = $this->modal; + } + return $ui_renderer->render($content); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + yield from $this->environment->getAnswerFormProperties() + ->getCombinations()->toTableRows($this->lng, $row_builder); + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return $this->environment->getAnswerFormProperties() + ->getCombinations()->getNumberOfCombinations(); + } + + public function doAction(): Async|self|Properties + { + return match ($this->environment->getStep()) { + self::STEP_SET_COMBINATION_VALUES => $this->processSetCombinationGapsModal(), + self::STEP_DELETE_COMBINATION => $this->deleteCombination(), + self::STEP_SAVE => $this->processSetCombinationValues(), + default => $this->buildAction() + }; + } + + private function buildTable(): DataTable + { + return $this->ui_factory->table()->data( + $this, + $this->lng->txt('combinations'), + $this->getColumns() + )->withActions($this->getActions()) + ->withRequest($this->http->request()); + } + + private function initializeModal( + RoundTripModal $modal + ): RoundTripModal { + $this->toolbar->addComponent( + $this->ui_factory->button()->standard( + $this->lng->txt('add_combination'), + $modal->getShowSignal() + ) + ); + return $modal; + } + + private function getColumns(): array + { + $cf = $this->ui_factory->table()->column(); + return [ + 'gaps' => $cf->text($this->lng->txt('gaps')), + 'values' => $cf->text($this->lng->txt('values')), + 'available_points' => $cf->number($this->lng->txt('points'))->withDecimals(2) + ]; + } + + private function getActions(): array + { + $af = $this->ui_factory->table()->action(); + return [ + $af->single( + $this->lng->txt('edit'), + $this->environment + ->withStepParameter(self::STEP_JUMP_TO_SET_COMBINATION_VALUES) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + )->withAsync(true), + $af->single( + $this->lng->txt('delete'), + $this->environment + ->withStepParameter(self::STEP_CONFIRM_DELETE_COMBINATION) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + )->withAsync(true) + ]; + } + + private function buildAction(): Async + { + $affected_item = $this->environment->getAnswerFormProperties() + ->getCombinations()->getCombinationById( + $this->environment->getTableRowIds()[0] + ); + + if ($affected_item === null) { + return $this->buildNoItemsSelectedAsync(); + } + + return $this->environment->getPresentationFactory()->getAsync( + match ($this->environment->getStep()) { + self::STEP_JUMP_TO_SET_COMBINATION_VALUES => + $this->buildSetCombinationValuesModal( + $this->buildInputsBuilder($affected_item) + ), + self::STEP_CONFIRM_DELETE_COMBINATION => + $this->confirmDeleteCombination($affected_item) + } + ); + } + + private function buildNoItemsSelectedAsync(): Async + { + return new Async( + $this->http, + $this->ui_factory->messageBox()->failure('no_combination_selected') + ); + } + + private function buildSetCombinationGapsModal(): RoundTripModal + { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('add_combination'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps, + $properties->getLegacyClozeText() + ), + [ + 'combination' => $gaps->buildGapsMultiSelect( + $this->lng->txt('select_gaps_for_combinations'), + $this->ui_factory->input()->field() + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->constraint( + fn(array $v): bool => count($v) > 1, + $this->lng->txt('combination_needs_more_than_one') + ) + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $v): Combination => $this->combinations_factory + ->buildNewCombination($gaps, $v) + ) + ) + ], + $this->environment + ->withStepParameter(self::STEP_SET_COMBINATION_VALUES) + ->getUrlBuilder() + ->buildURI() + ->__toString() + )->withSubmitLabel($this->lng->txt('next')); + } + + private function processSetCombinationGapsModal(): self + { + $clone = clone $this; + + $set_gaps_modal = $clone->buildSetCombinationGapsModal() + ->withRequest($clone->http->request()); + $data = $set_gaps_modal->getData(); + + if ($data === null) { + $clone->modal = $set_gaps_modal->withOnLoad($set_gaps_modal->getShowSignal()); + return $clone; + } + + $set_values_modal = $clone->buildSetCombinationValuesModal( + $this->buildInputsBuilder($data['combination']) + ); + $clone->modal = $set_values_modal->withOnLoad($set_values_modal->getShowSignal()); + return $clone; + } + + private function buildSetCombinationValuesModal( + InputsBuilder $inputs_builder + ): RoundTripModal { + $properties = $this->environment->getAnswerFormProperties(); + $gaps = $properties->getGaps(); + + return $this->ui_factory->modal()->roundtrip( + $this->lng->txt('edit'), + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $gaps, + $properties->getLegacyClozeText() + ), + [ + 'values_awarding_points' => $inputs_builder->getInputs() + ], + $this->environment->withStepParameter(self::STEP_SAVE) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + private function processSetCombinationValues(): self|Properties + { + $inputs_builder = $this->buildInputsBuilder(null); + $set_values_modal = $this->buildSetCombinationValuesModal($inputs_builder) + ->withRequest($this->http->request()); + $data = $set_values_modal->getData(); + if ($data === null) { + $this->modal = $this->initializeModal($set_values_modal) + ->withOnLoad($set_values_modal->getShowSignal()); + $inputs_builder->persistCarry(); + return $this; + } + + return $data['values_awarding_points']; + } + + private function confirmDeleteCombination( + Combination $affected_item + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('delete_combination'), + $this->environment->withStepParameter( + self::STEP_DELETE_COMBINATION + )->getUrlBuilder() + ->withParameter( + $this->environment->getTableRowIdToken(), + [$affected_item->getId()->toString()] + )->buildURI()->__toString() + ); + } + + private function deleteCombination(): Properties + { + $combination_identifier = $this->environment->getTableRowIds(); + if ($combination_identifier === []) { + return $this->environment->getAnswerFormProperties(); + } + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $answer_form_properties */ + $answer_form_properties = $this->environment->getAnswerFormProperties(); + + return $answer_form_properties->withCombinations( + $answer_form_properties->getCombinations()->withoutCombination( + $combination_identifier[0] + ) + ); + } + + private function buildInputsBuilder( + ?Combination $combination, + ): InputsBuilderSession { + $builder = $this->environment->getPresentationFactory()->getSessionBasedInputsBuilder( + $this->environment->getAnswerFormProperties()->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $v) use ($combination): ?Section { + $properties = $this->environment->getAnswerFormProperties(); + if ($combination === null) { + $combination = $this->combinations_factory + ->buildCombinationFromCarryValue( + $v, + $properties + ); + } + + return $combination?->buildPointsInputs( + $this->ui_factory->input()->field(), + $this->refinery, + $this->lng, + $this->combinations_factory, + $properties + ); + } + ) + ); + + if ($combination === null) { + return $builder; + } + + $builder_with_string = $builder->withCarry($combination->buildCarryString()); + $builder_with_string->persistCarry(); + return $builder_with_string; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php new file mode 100644 index 000000000000..e9b259ab5dbe --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Layout/OverviewTable.php @@ -0,0 +1,119 @@ +table_factory->data( + $this, + $this->lng->txt('gaps'), + $this->getColums() + )->withActions($this->getActions()) + ->withRequest($this->request); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + yield from $this->environment->getAnswerFormProperties()->getGaps() + ->toTableRows($row_builder, $this->lng); + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return $this->environment->getAnswerFormProperties()->getGaps()->getNumberOfGaps(); + } + + private function getColums(): array + { + $f = $this->table_factory->column(); + + return [ + 'gap' => $f->text($this->lng->txt('title'))->withIsSortable(false), + 'type' => $f->text($this->lng->txt('cloze_type'))->withIsSortable(false), + 'answers_options_awarding_points' => $f + ->text($this->lng->txt('answer_options_awarding_points')) + ->withIsSortable(false), + 'available_points' => $f->number($this->lng->txt('available_points')) + ->withDecimals(4) + ->withIsSortable(false) + ]; + } + + private function getActions(): array + { + return [ + 'edit_gaps' => $this->table_factory->action()->standard( + $this->lng->txt('edit_gaps'), + $this->environment + ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_GAP_TYPES) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ), + 'edit_answer_options' => $this->table_factory->action()->standard( + $this->lng->txt('edit_answer_options'), + $this->environment + ->withStepParameter(EditGaps::STEP_JUMP_TO_SET_ANSWER_OPTIONS) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ), + 'edit_points' => $this->table_factory->action()->standard( + $this->lng->txt('edit_available_points'), + $this->environment + ->withStepParameter(EditGaps::STEP_JUMP_TO_ASSIGN_POINTS) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php new file mode 100644 index 000000000000..58242849b853 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/BasicMigrationFunctions.php @@ -0,0 +1,268 @@ +insert( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::AnswerInputs + ), + $this->buildGapValuesForInsert( + $persistence_factory, + $answer_input_id, + $answer_form_id, + $position, + $gap_type, + $max_chars, + $step_size, + $matching_options, + $min_autocomplete, + $shuffle + ) + ); + } + + return $gaps_insert->withAdditionalValues( + $this->buildGapValuesForInsert( + $persistence_factory, + $answer_input_id, + $answer_form_id, + $position, + $gap_type, + $max_chars, + $step_size, + $matching_options, + $min_autocomplete, + $shuffle + ) + ); + } + + private function buildGapValuesForInsert( + PersistenceFactory $persistence_factory, + Uuid $answer_input_id, + Uuid $answer_form_id, + int $position, + string $gap_type, + ?int $max_chars, + ?float $step_size, + ?TextMatchingOptions $matching_options, + ?int $min_autocomplete, + ?int $shuffle + ): array { + return [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_type), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $max_chars), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $step_size), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $matching_options?->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $min_autocomplete), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $shuffle) + ]; + } + + private function buildAnswerOptionInsertStatement( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + ?Insert $options_insert, + Uuid $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + float $points, + ?float $lower_limit, + ?float $upper_limit + ): Insert { + if ($options_insert === null) { + return $persistence_factory->insert( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::AnswerOptions + ), + $this->buildOptionValuesForInsert( + $persistence_factory, + $answer_option_id, + $answer_input_id, + $position, + $text_value, + $points, + $lower_limit, + $upper_limit + ) + ); + } + + return $options_insert->withAdditionalValues( + $this->buildOptionValuesForInsert( + $persistence_factory, + $answer_option_id, + $answer_input_id, + $position, + $text_value, + $points, + $lower_limit, + $upper_limit + ) + ); + } + + private function buildOptionValuesForInsert( + PersistenceFactory $persistence_factory, + Uuid $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + float $points, + ?float $lower_limit, + ?float $upper_limit + ): array { + return [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $text_value), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $lower_limit), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $lower_limit !== $upper_limit + ? $upper_limit + : null + ) + ]; + } + + private function buildAnswerFormInsertStatement( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + Uuid $answer_form_id, + ScoringIdentical $scoring_identical, + int $combinations_enabled + ): Insert { + return $persistence_factory->insert( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::TypeSpecificAnswerForms + ), + [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $scoring_identical->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $combinations_enabled) + ] + ); + } + + private function buildScoringIdenticalFromOld( + int $scoring_identical + ): ScoringIdentical { + if ($scoring_identical === '1') { + return ScoringIdentical::ScoreAll; + } + + return ScoringIdentical::OnlyScoreDistinct; + } + + private function buildNewTextRatingFromOld( + string $old_text_rating + ): TextMatchingOptions { + return match($old_text_rating) { + \assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE => TextMatchingOptions::CaseInsensitive, + \assClozeGap::TEXTGAP_RATING_CASESENSITIVE => TextMatchingOptions::CaseSensitive, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN1 => TextMatchingOptions::Levenstein1, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN2 => TextMatchingOptions::Levenstein2, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN3 => TextMatchingOptions::Levenstein3, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN4 => TextMatchingOptions::Levenstein4, + \assClozeGap::TEXTGAP_RATING_LEVENSHTEIN5 => TextMatchingOptions::Levenstein5 + }; + } + + private function replaceGapsAndSantizeLegacyClozeText( + \ilDBInterface $db, + string $gap_replace_regex, + string $text, + array $gaps_mapping, + bool $ilias_page_editor_text + ): string { + ksort($gaps_mapping); + + return mb_ereg_replace_callback( + $gap_replace_regex, + function (array $matches) use (&$gaps_mapping): string { + return '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . array_shift($gaps_mapping) . '}}'; + }, + $this->sanitizeLegacyText( + $db, + $text, + $ilias_page_editor_text + ) + ); + } + + private function limitToFloat( + \EvalMath $math, + string $limit + ): float { + return (float) $math->e($limit); + } + + private function getHtmlQuestionContentPurifier(): \ilHtmlPurifierInterface + { + return \ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution'); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php new file mode 100644 index 000000000000..36759ff980e7 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationCloze.php @@ -0,0 +1,329 @@ +persistence->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + MigrationInsert $migration_insert + ): ?MigrationInsert { + $answer_input_mapping = []; + $answer_options_mapping = []; + $gaps_insert = null; + $answer_options_insert = null; + + foreach ($this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + $answer_form_id = $migration_insert->getAnswerFormId(); + if (!isset($answer_input_mapping[$db_row->gap_id])) { + $answer_input_mapping[$db_row->gap_id] = $migration_insert->getUuid(); + $answer_options_mapping[$db_row->gap_id] = []; + $gaps_insert = $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $gaps_insert, + $answer_input_mapping[$db_row->gap_id], + $answer_form_id, + $db_row->gap_id, + $this->buildNewGapTypeIdentifierFromOld((int) $db_row->cloze_type), + null, + null, + $this->buildNewTextRatingFromOld($db_row->textgap_rating), + null, + $db_row->shuffle === '1' ? 1 : 0 + ); + } + + $answer_option_id = $migration_insert->getUuid(); + $answer_options_mapping[$db_row->gap_id][$db_row->answertext] = [ + 'is_numeric' => $db_row->cloze_type == \assClozeGap::TYPE_NUMERIC, + 'answer_option_id' => $answer_option_id + ]; + + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $answer_option_id, + $answer_input_mapping[$db_row->gap_id], + $db_row->aorder, + $db_row->answertext, + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) + ); + } + + if (!isset($db_row)) { + return null; + } + + if ($db_row->combinations_enabled) { + $migration_insert = $this->addCombinationInsertStatements( + $migration_insert, + $answer_input_mapping, + $answer_options_mapping + ); + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_form_id, + $this->buildScoringIdenticalFromOld((int) $db_row->identical_scoring), + $db_row->combinations_enabled + ) + )->withAdditionalInsert( + $gaps_insert + )->withAdditionalInsert( + $answer_options_insert + )->withAdditionalTextLegacy( + $this->replaceGapsAndSantizeLegacyClozeText( + $migration_insert->getDb(), + '\[gap\].+?\[\/gap\]', + $db_row->cloze_text, + $answer_input_mapping, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT *, EXISTS (' . PHP_EOL + . 'SELECT gap_fi FROM qpl_a_cloze_combi_res' . PHP_EOL + . 'WHERE question_fi = a.question_fi' . PHP_EOL + . ') combinations_enabled' . PHP_EOL + . 'FROM qpl_qst_cloze q' . PHP_EOL + . 'JOIN qpl_a_cloze a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY a.gap_id, a.aorder' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function fetchCombinationsDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT combination_id, gap_fi, answer, points, row_id FROM qpl_a_cloze_combi_res' . PHP_EOL + . "WHERE question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY combination_id, row_id' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function addCombinationInsertStatements( + MigrationInsert $migration_insert, + array $answer_input_mapping, + array $answer_options_mapping + ): MigrationInsert { + $combination_mapping = []; + $combinations_insert = null; + $combinations_to_answer_options_insert = null; + foreach ($this->fetchCombinationsDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + if ($db_row->answer !== 'out_of_bound' + && !isset($answer_options_mapping[$db_row->gap_fi][$db_row->answer])) { + continue; + } + + $answer_option = $db_row->answer === 'out_of_bound' + ? reset($answer_options_mapping[$db_row->gap_fi]) + : $answer_options_mapping[$db_row->gap_fi][$db_row->answer]; + + if (!isset($combination_mapping[$db_row->combination_id . $db_row->row_id])) { + $combination_mapping[$db_row->combination_id . $db_row->row_id] = $migration_insert->getUuid(); + $combinations_insert = $this->buildCombinationsInsert( + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $combinations_insert, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), + $migration_insert->getAnswerFormId()->toString(), + $db_row->points + ); + } + + $combinations_to_answer_options_insert = $this->buildCombinationsToAnswerOptionsInsert( + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $combinations_to_answer_options_insert, + $combination_mapping[$db_row->combination_id . $db_row->row_id]->toString(), + $answer_input_mapping[$db_row->gap_fi]->toString(), + $answer_option['answer_option_id']->toString(), + $this->buildRangeValue($answer_option['is_numeric'], $db_row->answer) + ); + } + + return $migration_insert->withAdditionalInsert($combinations_insert) + ->withAdditionalInsert($combinations_to_answer_options_insert); + } + + private function buildCombinationsInsert( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + ?Insert $combinations_insert, + Uuid $combination_id, + Uuid $answer_form_id, + float $points + ): Insert { + if ($combinations_insert === null) { + return $persistence_factory->insert( + $this->persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::Additional, + $this->persistence->getCombinationsTableIdentifier() + ), + [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + ] + ); + } + + return $combinations_insert->withAdditionalValues([ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $points), + ]); + } + + private function buildCombinationsToAnswerOptionsInsert( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + ?Insert $combinations_to_answer_options_insert, + Uuid $combination_id, + Uuid $gap_id, + Uuid $answer_option_id, + InRange $in_range + ): Insert { + if ($combinations_to_answer_options_insert === null) { + return $persistence_factory->insert( + $this->persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::Additional, + $this->persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) + ] + ); + } + + return $combinations_to_answer_options_insert->withAdditionalValues([ + $persistence_factory->value(\ilDBConstants::T_TEXT, $combination_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $gap_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_option_id), + $persistence_factory->value(\ilDBConstants::T_TEXT, $in_range) + ]); + } + + private function buildNewGapTypeIdentifierFromOld( + int $old_gap_type + ): string { + return match($old_gap_type) { + \assClozeGap::TYPE_TEXT => 'text', + \assClozeGap::TYPE_SELECT => 'select', + \assClozeGap::TYPE_NUMERIC => 'numeric' + }; + } + + private function buildRangeValue( + bool $is_numeric, + string $value + ): ?string { + if ($is_numeric === null) { + return null; + } + + if ($value === 'out_of_bounds') { + return InRange::OutOfRange->value; + } + + return InRange::InRange->value; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php new file mode 100644 index 000000000000..93a1e55cc85d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationLongMenu.php @@ -0,0 +1,214 @@ +persistence->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + MigrationInsert $migration_insert + ): ?MigrationInsert { + $answer_form_id = $migration_insert->getAnswerFormId(); + + $answer_input_mapping = []; + $gaps_insert = null; + $answer_options_to_insert = []; + + foreach ($this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + if (!isset($answer_input_mapping[$db_row->gap_number])) { + $answer_input_id = $migration_insert->getUuid(); + $answer_input_mapping[$db_row->gap_number] = $answer_input_id; + + $gaps_insert = $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $gaps_insert, + $answer_input_id, + $answer_form_id, + $db_row->gap_number, + $this->buildNewGapTypeIdentifierFromOld($db_row->type), + null, + null, + null, + $db_row->min_auto_complete, + $db_row->shuffle_answers === '1' ? 1 : 0 + ); + + $answers_from_file = $this->loadAnswersFromFile( + $environment->getResource(Environment::RESOURCE_ILIAS_INI), + $environment->getResource(Environment::RESOURCE_CLIENT_ID), + $migration_insert->getOldQuestionId(), + $db_row->gap_number + ); + + $answer_options_to_insert[$answer_input_id->toString()] = array_map( + fn(int $v) => [ + 'answer_option_id' => $migration_insert->getUuid(), + 'answer_input_id' => $answer_input_id, + 'position' => $v, + 'text' => trim($answers_from_file[$v]), + 'points' => 0.0 + ], + array_keys($answers_from_file) + ); + } + + $answer_input_id_string = $answer_input_mapping[$db_row->gap_number]->toString(); + $position = array_find_key( + $answer_options_to_insert[$answer_input_id_string], + fn(array $v, int $k): bool => trim($v['text']) === trim($db_row->answer_text) + ); + + if ($position !== null) { + $answer_options_to_insert[$answer_input_id_string][$position]['points'] = $db_row->points; + ; + } + + + } + + if (!isset($db_row)) { + return null; + } + + $answer_options_insert = null; + foreach (array_reduce( + $answer_options_to_insert, + fn(array $c, array $v): array => [...$c, ...$v], + [] + ) as $answer) { + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $answer['answer_option_id'], + $answer['answer_input_id'], + $answer['position'], + $answer['text'], + $answer['points'], + null, + null + ); + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_form_id, + $this->buildScoringIdenticalFromOld($db_row->identical_scoring), + 0 + ) + )->withAdditionalTextLegacy( + $this->replaceGapsAndSantizeLegacyClozeText( + $migration_insert->getDb(), + '\[Longmenu \d+\]', + $db_row->long_menu_text, + $answer_input_mapping, + $migration_insert->wasIliasPageEditorUsedForAdditionalTexts() + ) + )->withAdditionalInsert($gaps_insert) + ->withAdditionalInsert($answer_options_insert); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT * FROM qpl_qst_lome q' . PHP_EOL + . 'JOIN qpl_a_lome a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL + . 'ORDER BY a.gap_number, a.position' + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function loadAnswersFromFile( + \ilIniFile $ini, + string $client_id, + int $old_question_id, + int $gap_id + ): array { + $file = "{$ini->readVariable('clients', 'datadir')}/{$client_id}/assessment/longMenuQuestion/{$old_question_id}/{$gap_id}.txt"; + + if (!file_exists($file)) { + return []; + } + + return explode( + "\n", + file_get_contents($file) + ); + } + + private function buildNewGapTypeIdentifierFromOld( + int $old_gap_type + ): string { + return match($old_gap_type) { + \assLongMenu::ANSWER_TYPE_TEXT_VAL => 'long_menu', + \assLongMenu::ANSWER_TYPE_SELECT_VAL => 'select' + }; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php new file mode 100644 index 000000000000..4fc25a6fbf31 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationNumeric.php @@ -0,0 +1,134 @@ +math->suppress_errors = true; + } + + #[\Override] + public function getOldQuestionIdentifier(): string + { + return 'assNumeric'; + } + + #[\Override] + public function getDefinitionClass(): string + { + return Definition::class; + } + + #[\Override] + public function getTableNameSpace(): TableNameSpace + { + return $this->persistence->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + MigrationInsert $migration_insert + ): ?MigrationInsert { + $db_row = $this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + )?->current(); + + if ($db_row === null) { + return null; + } + + $answer_form_id = $migration_insert->getAnswerFormId(); + $gap_id = $migration_insert->getUuid(); + return $migration_insert->withAdditionalInsert( + $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + null, + $gap_id, + $answer_form_id, + 0, + 'numeric', + null, + 0.0001, + null, + null, + null + ) + )->withAdditionalInsert( + $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + null, + $migration_insert->getUuid(), + $gap_id, + 0, + '', + $db_row->points, + $this->limitToFloat($this->math, $db_row->lowerlimit), + $this->limitToFloat($this->math, $db_row->upperlimit) + ) + )->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_form_id, + ScoringIdentical::ScoreAll, + 0 + ) + )->withAdditionalText( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '_' . $gap_id->toString() . '}}' + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT points, lowerlimit, upperlimit FROM qpl_num_range' . PHP_EOL + . "WHERE question_fi = {$db->quote($old_question_id)}" + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php new file mode 100644 index 000000000000..c2ba57c42977 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Migration/MigrationTextSubset.php @@ -0,0 +1,165 @@ +persistence->getTableNameSpace(); + } + + #[\Override] + public function completeMigrationInsert( + Environment $environment, + MigrationInsert $migration_insert + ): ?MigrationInsert { + $answer_form_id = $migration_insert->getAnswerFormId(); + $answer_options_insert = null; + $gaps = []; + + foreach ($this->fetchDBValues( + $migration_insert->getDb(), + $migration_insert->getOldQuestionId() + ) as $db_row) { + if ($gaps === []) { + $gaps_insert = null; + for ($i = 0; $i < $db_row->correctanswers; $i++) { + $gap_id = $migration_insert->getUuid(); + $gaps[] = $gap_id; + + $gaps_insert = $this->buildGapInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $gaps_insert, + $gap_id, + $answer_form_id, + $i, + 'text', + null, + null, + $this->buildNewTextRatingFromOld($db_row->textgap_rating), + null, + 0 + ); + } + + $migration_insert = $migration_insert->withAdditionalInsert($gaps_insert); + } + + foreach ($gaps as $gap_id) { + $answer_options_insert = $this->buildAnswerOptionInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_options_insert, + $migration_insert->getUuid(), + $gap_id, + $db_row->aorder, + $db_row->answertext, + $db_row->points, + null, + null + ); + } + } + + if (!isset($db_row)) { + return null; + } + + return $migration_insert + ->withAdditionalInsert( + $this->buildAnswerFormInsertStatement( + $this->persistence, + $migration_insert->getPersistenceFactory(), + $migration_insert->getTableNameBuilder(), + $answer_form_id, + ScoringIdentical::OnlyScoreDistinct, + 0 + ) + )->withAdditionalInsert( + $answer_options_insert + )->withAdditionalText( + $this->buildAdditionalTextFromGapsArray($gaps) + ); + } + + private function fetchDBValues( + \ilDBInterface $db, + int $old_question_id + ): \Generator { + $query = $db->query( + 'SELECT * FROM qpl_qst_textsubset q' . PHP_EOL + . 'JOIN qpl_a_textsubset a ON q.question_fi = a.question_fi' . PHP_EOL + . "WHERE q.question_fi = {$db->quote($old_question_id)}" . PHP_EOL + ); + + while (($row = $db->fetchObject($query)) !== null) { + yield $row; + } + } + + private function buildAdditionalTextFromGapsArray( + array $gaps + ): string { + $text_array = []; + foreach ($gaps as $index => $gap) { + $position = $index + 1; + $text_array[] = "{$position}. {{GAP_{$gap->toString()}}}"; + } + + return implode( + "\n\n", + $text_array + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php new file mode 100644 index 000000000000..3c3e6072cd9e --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Persistence.php @@ -0,0 +1,363 @@ +table_namespace; + } + + #[\Override] + public function getColumns( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + TableTypes $table_type, + string $table_identifier = '', + array $columns_to_skip = [] + ): array { + $table = $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ); + $column_identifiers = match($table_type) { + TableTypes::TypeSpecificAnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, + TableTypes::AnswerInputs => self::ANSWER_INPUTS_TABLE_COLUMNS, + TableTypes::AnswerOptions => self::ANSWER_OPTIONS_TABLE_COLUMNS, + TableTypes::Additional => match($table_identifier) { + self::COMBINATION_TABLE_IDENTIFIER => self::COMBINATION_TABLE_COLUMNS, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER => self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_COLUMNS + } + }; + return array_map( + fn(string $v): Column => $persistence_factory->column($table, $v), + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + #[\Override] + public function getIdColumn( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + TableTypes $table_type, + string $table_identifier = '' + ): Column { + if ($table_type === TableTypes::TypeSpecificAnswerForms) { + return $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ); + } + + if ($table_identifier === self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER) { + return $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_ID_COLUMN + ); + } + + return $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ), + self::ID_COLUMN + ); + } + + #[\Override] + public function getForeignKeyColumn( + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + TableTypes $table_type, + string $table_identifier = '' + ): Column { + return match($table_type) { + TableTypes::TypeSpecificAnswerForms => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), + self::ANSWER_FORM_TABLE_ID_COLUMN + ), + TableTypes::AnswerInputs => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), + self::ANSWER_INPUTS_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::AnswerOptions => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder + ), + self::ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN + ), + TableTypes::Additional => $persistence_factory->column( + $table_type->getTable( + $persistence_factory, + $table_name_builder, + $table_identifier + ), + $table_identifier === self::COMBINATION_TABLE_IDENTIFIER + ? self::COMBINATION_TABLE_FOREIGN_KEY_COLUMN + : self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_FOREIGN_KEY_COLUMN + ) + }; + } + + #[\Override] + public function completeQuery( + Query $query, + Column $answer_form_id_column + ): Query { + $persistence_factory = $query->getPersistenceFactory(); + $table_name_builder = $query->getTableNameBuilder(Definition::class); + + $answer_form_specific_table_definition = TableTypes::TypeSpecificAnswerForms; + $answer_input_table_definition = TableTypes::AnswerInputs; + $answer_options_table_definition = TableTypes::AnswerOptions; + $additional_table_definition = TableTypes::Additional; + + $combinations_id_column = $this->getIdColumn( + $persistence_factory, + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER + ); + + return $query->withAdditionalJoin( + $persistence_factory->join( + $answer_form_id_column, + $this->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $answer_form_specific_table_definition + ), + JoinType::Left + ) + )->withAdditionalSelect( + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $answer_form_specific_table_definition + ) + ) + )->withAdditionalJoin( + $persistence_factory->join( + $answer_form_id_column, + $this->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $answer_input_table_definition + ), + JoinType::Left + ) + )->withAdditionalSelect( + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $answer_input_table_definition + ) + ) + )->withAdditionalJoin( + $persistence_factory->join( + $this->getIdColumn( + $persistence_factory, + $table_name_builder, + $answer_input_table_definition + ), + $this->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $answer_options_table_definition + ), + JoinType::Left + ) + )->withAdditionalOrder( + $persistence_factory->order( + $this->getIdColumn( + $persistence_factory, + $table_name_builder, + $answer_input_table_definition + ) + ) + )->withAdditionalSelect( + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $answer_options_table_definition + ) + ) + )->withAdditionalOrder( + $persistence_factory->order( + $persistence_factory->column( + $answer_options_table_definition->getTable( + $persistence_factory, + $table_name_builder + ), + 'position' + ) + ) + )->withAdditionalJoin( + $persistence_factory->join( + $answer_form_id_column, + $this->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER + ), + JoinType::Left + ) + )->withAdditionalSelect( + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TABLE_IDENTIFIER + ) + ) + )->withAdditionalJoin( + $persistence_factory->join( + $combinations_id_column, + $this->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + JoinType::Left + ) + )->withAdditionalSelect( + $persistence_factory->select( + $this->getColumns( + $persistence_factory, + $table_name_builder, + $additional_table_definition, + self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ) + ) + )->withAdditionalOrder( + $persistence_factory->order( + $combinations_id_column + ) + ); + } + + public function getCombinationsTableIdentifier(): string + { + return self::COMBINATION_TABLE_IDENTIFIER; + } + + public function getCombinationToAnswerOptionsTableIdentifier(): string + { + return self::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php new file mode 100644 index 000000000000..b24c6ab3de23 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Factory.php @@ -0,0 +1,46 @@ +refinery, + $this->mustache_engine, + $this->text_factory, + $this->text_factory->markdown($text) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php new file mode 100644 index 000000000000..54868bb3e3f7 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/ClozeText/Text.php @@ -0,0 +1,192 @@ +markdown( + new \ilUIMarkdownPreviewGUI(), + $lng->txt('cloze_text') + )->withMustacheVariables([ + Gap::GAP_PLACEHOLDER_NAME => $lng->txt('insert_a_gap') + ])->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): self => $cloze_text_factory->buildFromTextString($v) + ) + )->withAdditionalTransformation( + $this->refinery->custom()->constraint( + fn(self $v): bool => !$is_required && $v->getRawRepresentation() === '' + || $v->hasAtLeastOneGap(), + $lng->txt('no_gaps') + ) + )->withRequired($is_required) + ->withValue($this->cloze_text->getRawRepresentation()); + } + + public function getRawRepresentation(): string + { + return $this->cloze_text->getRawRepresentation(); + } + + public function getRenderedMarkdownForParticipantPresentation(): string + { + return $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ); + } + + public function buildPanelForEditing( + UIFactory $ui_factory, + Language $lng, + Gaps $gaps, + string $legacy_cloze_text + ): StandardPanel { + return $ui_factory->panel()->standard( + $lng->txt('cloze_text'), + $ui_factory->legacy()->latexContent( + $this->getRenderedMarkdownForEditingPresentation( + $gaps, + $legacy_cloze_text + ) + ) + ); + } + + public function getRenderedMarkdownForEditingPresentation( + Gaps $gaps, + string $legacy_cloze_text + ): string { + $text = $this->cloze_text->getRawRepresentation() === '' + ? $legacy_cloze_text + : $this->refinery->string()->markdown()->toHTML()->transform( + $this->cloze_text->getRawRepresentation() + ); + + return $this->mustache_engine->render( + $text, + $gaps->getPlaceholderArrayForEditFormPanel() + ); + } + + public function updateGapsFromMarkdown( + Uuid $answer_form_id, + Gaps $pre_existing_gaps + ): Gaps { + if ($this->cloze_text->getRawRepresentation() === '') { + return $pre_existing_gaps->withResetGaps(); + } + + $position = 0; + return array_reduce( + $this->mustache_engine->getTokenizer()->scan($this->cloze_text->getRawRepresentation()), + function (Gaps $c, array $v) use ($answer_form_id, $pre_existing_gaps, &$position): Gaps { + if ($v['type'] !== '_v' + || !str_starts_with($v['name'], Gap::GAP_PLACEHOLDER_NAME)) { + return $c; + } + + if ($v['name'] === Gap::GAP_PLACEHOLDER_NAME) { + return $c->withNewGap($answer_form_id, $position++); + } + + $gap = $pre_existing_gaps->getGapByTagName($v['name']); + if ($gap !== null) { + return $c->withGap( + $gap->withPosition( + $position++ + ) + ); + } + + return $c->withAdditionalGapFromTagName( + $answer_form_id, + $v['name'], + $position++ + ); + }, + $pre_existing_gaps->withResetGaps() + ); + } + + public function withIdsOfNewGapsInClozeText( + array $new_gaps + ): self { + if ($new_gaps === []) { + return $this; + } + + $clone = clone $this; + $clone->cloze_text = $this->text_factory->markdown( + mb_ereg_replace_callback( + '{{' . Gap::GAP_PLACEHOLDER_NAME . '}}', + function (array $matches) use (&$new_gaps): string { + return array_shift($new_gaps)->getGapPlaceholder(); + }, + $this->cloze_text->getRawRepresentation() + ) + ); + return $clone; + } + + private function hasAtLeastOneGap(): bool + { + if ($this->cloze_text->getRawRepresentation() === '') { + return false; + } + + foreach ($this->mustache_engine->getTokenizer() + ->scan($this->cloze_text->getRawRepresentation()) as $token) { + if ($token['type'] === '_v' + && str_starts_with($token['name'], Gap::GAP_PLACEHOLDER_NAME)) { + return true; + } + } + + return false; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php new file mode 100644 index 000000000000..8f0f87d4076d --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combination.php @@ -0,0 +1,330 @@ + $matching_values + */ + public function __construct( + private readonly Uuid $id, + private ?float $available_points, + private array $matching_values = [] + ) { + } + + public function getId(): Uuid + { + return $this->id; + } + + public function getAvailablePoints(): ?float + { + return $this->available_points; + } + + public function withAdditionalMatchingValue( + MatchingValue $matching_value + ): self { + $clone = clone $this; + $clone->matching_values[] = $matching_value; + return $clone; + } + + public function getValuePresentation( + Language $lng + ): string { + return implode( + '
', + array_map( + fn(MatchingValue $v): string => $v->buildPresentationString($lng), + $this->matching_values + ) + ); + } + + public function containsAnswerOptionsExactly( + array $vs + ): bool { + return array_diff( + $vs, + array_map( + fn(MatchingValue $v): string => $v->getAnswerOption()->getAnswerOptionId()->toString(), + $this->matching_values + ) + ) === []; + } + + public function toStorage( + Uuid $answer_form_id, + Persistence $persistence, + TableNameBuilder $table_name_builder, + Manipulate $manipulate + ): Manipulate { + if ($this->matching_values === []) { + throw new \UnexpectedValueException( + 'A Combination without MatchingValues cannot be stored.' + ); + } + + return array_reduce( + $this->matching_values, + fn(Manipulate $c, MatchingValue $v): Manipulate => $c->withAdditionalStatement( + $v->toStorage( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ), + $manipulate->withAdditionalStatement( + $this->buildReplace( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder, + $answer_form_id + ) + ) + ); + } + + public function toDelete( + Persistence $persistence, + TableNameBuilder $table_name_builder, + Manipulate $manipulate + ): Manipulate { + return $manipulate->withAdditionalStatement( + $this->buildDelete( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + )->withAdditionalStatement( + $this->buildDeleteForLinkedValues( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ); + } + + private function buildReplace( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder, + Uuid $answer_form_id + ): Replace { + $table_definition = TableTypes::Additional; + return $persistence_factory->replace( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition, + $persistence->getCombinationsTableIdentifier() + ), + [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->available_points) + ] + ); + } + + private function buildDelete( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::Additional; + return $persistence_factory->delete( + $persistence_factory->table( + $table_definition, + $table_name_builder, + $persistence->getCombinationsTableIdentifier() + ), + [ + $persistence_factory->where( + $persistence->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_definition, + $persistence->getCombinationsTableIdentifier() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildDeleteForLinkedValues( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::Additional; + return $persistence_factory->delete( + $persistence_factory->table( + $table_definition, + $table_name_builder, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + $persistence_factory->where( + $persistence->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_definition, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + public function toTableRow( + Language $lng, + DataRowBuilder $data_row_builder + ): DataRow { + return $data_row_builder->buildDataRow( + $this->id->toString(), + [ + 'gaps' => $this->buildGapsString(), + 'values' => $this->getValuePresentation($lng), + 'available_points' => $this->getAvailablePoints() + ] + ); + } + + public function buildGapsString(): string + { + return implode( + '
', + array_map( + fn(MatchingValue $v): string => $v->getGap()->buildShortenedGapRepresentation(), + $this->matching_values + ) + ); + } + + public function buildPointsInputs( + FieldFactory $field_factory, + Refinery $refinery, + Language $lng, + Factory $combinations_factory, + Properties $properties + ): Section { + return $field_factory->section( + [ + 'values' => $this->buildValuesInputs( + $field_factory, + $properties + ), + 'points' => $field_factory->numeric( + $lng->txt('points') + )->withStepSize(0.01) + ->withRequired(true) + ->withValue($this->getAvailablePoints()) + ], + $lng->txt('values') + )->withAdditionalTransformation( + $refinery->custom()->constraint( + fn(array $vs): bool => !$properties->getCombinations() + ->hasMatchingCombinationForAnswerOptionIds($vs['values']), + $lng->txt('combination_already_exists') + ) + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $v): Properties => $properties->withCombinations( + $properties->getCombinations()->withAdditionalCombination( + $combinations_factory->buildCombination( + $this->id, + $v['points'], + $combinations_factory->buildMatchingValuesFromForm( + $properties, + $this->id, + $v['values'] + ) + ) + ) + ) + ) + ); + } + + public function buildCarryString(): string + { + return json_encode([ + $this->id->toString() => array_map( + fn(MatchingValue $v) => $v->getGap()->getAnswerInputId()->toString(), + $this->matching_values + ) + ]); + } + + private function buildValuesInputs( + FieldFactory $field_factory, + Properties $properties + ): Group { + return $field_factory->group( + array_reduce( + $this->matching_values, + function (array $c, MatchingValue $v) use ($field_factory, $properties): array { + $gap_id = $v->getGap()->getAnswerInputId(); + $gap = $properties->getGaps()->getGapById($gap_id); + $c[$gap_id->toString()] = $field_factory->select( + $gap->buildShortenedGapName(), + $gap->getType()->getCombinationsSelectValues($gap) + )->withRequired(true) + ->withValue( + $v->getAnswerOption()?->getAnswerOptionId()->toString() + ); + return $c; + }, + [] + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php new file mode 100644 index 000000000000..e3efa80e6f82 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Combinations.php @@ -0,0 +1,176 @@ +combinations = array_reduce( + $combinations, + function (array $c, Combination $v): array { + $c[$v->getId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function areCombinationsEnabled(): bool + { + return $this->enabled; + } + + public function withCombinationsEnabled( + bool $combinations_enabled + ): self { + $clone = clone $this; + $clone->enabled = $combinations_enabled; + return $clone; + } + + public function getCombinationById( + string $id + ): ?Combination { + return $this->combinations[$id] ?? null; + } + + public function withAdditionalCombination( + Combination $combination + ): self { + $clone = clone $this; + $clone->combinations[$combination->getId()->toString()] = $combination; + return $clone; + } + + public function withoutCombination( + string $id + ): self { + $clone = clone $this; + $clone->deleted_combinations[] = $clone->combinations[$id]; + unset($clone->combinations[$id]); + return $clone; + } + + public function hasMatchingCombinationForAnswerOptionIds( + array $vs + ): bool { + foreach ($this->combinations as $combination) { + if ($combination->containsAnswerOptionsExactly($vs)) { + return true; + } + } + return false; + } + + public function getEditView( + UIFactory $ui_factory, + \ilToolbarGUI $toolbar, + Refinery $refinery, + Language $lng, + HttpServices $http + ): EditCombinations { + return new EditCombinations( + $ui_factory, + $toolbar, + $refinery, + $lng, + $http, + $this->combinations_factory + ); + } + + public function toStorage( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->combinations, + fn(Manipulate $c, Combination $v): Manipulate => $v->toStorage( + $this->answer_form_id, + $persistence, + $table_name_builder, + $c + ), + array_reduce( + $this->deleted_combinations, + fn(Manipulate $c, Combination $v): Manipulate => $v->toDelete( + $persistence, + $table_name_builder, + $manipulate + ), + $manipulate + ) + ); + } + + public function toDelete( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->combinations, + fn(Manipulate $c, Combination $v): Manipulate => $c->withAdditionalStatement( + $v->toDelete( + $this->answer_form_id, + $manipulate, + $persistence, + $table_name_builder + ) + ), + $manipulate + ); + } + + public function toTableRows( + Language $lng, + DataRowBuilder $row_builder, + ): \Generator { + foreach ($this->combinations as $combination) { + yield $combination->toTableRow($lng, $row_builder); + } + } + + public function getNumberOfCombinations(): int + { + return count($this->combinations); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php new file mode 100644 index 000000000000..031189a70094 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/EditCombinations.php @@ -0,0 +1,94 @@ +addEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::LANG_VAR_EDIT_COMBINATIONS + ); + } + + public function show( + Environment $environment + ): Async|Renderable|Properties { + $environment->addEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW, + self::LANG_VAR_EDIT_COMBINATIONS + ); + + $environment->activateEditAnswerFormSubTab( + self::STEP_EDIT_COMBINATIONS_OVERVIEW + ); + + $combinations_overview = $this->buildCombinationsOverview($environment); + + $step = $environment->getStep(); + if ($step === self::STEP_EDIT_COMBINATIONS_OVERVIEW + || $step === '') { + return $combinations_overview; + } + + return $combinations_overview->doAction(); + } + + private function buildCombinationsOverview( + Environment $environment + ): CombinationsOverview { + return new CombinationsOverview( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http, + $environment, + $this->combinations_factory + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php new file mode 100644 index 000000000000..5eba4b2ba5e5 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/Factory.php @@ -0,0 +1,272 @@ +getAnswerFormId(), + $combinations_enabled, + !$combinations_enabled || $gaps === null || $query === null + ? [] + : $this->retrieveMatchingValuesFromQuery( + $type_generic_properties, + $gaps, + $this->retrieveCombinationsFromQuery( + $type_generic_properties, + $query + ), + $query + ) + ); + } + + /** + * @param array $gap_ids + */ + public function buildNewCombination( + Gaps $gaps, + array $gap_ids, + ): Combination { + $combination_id = $this->uuid_factory->uuid4(); + return $this->buildCombination( + $combination_id, + null, + array_map( + fn(string $v): MatchingValue => new MatchingValue( + $combination_id, + $gaps->getGapById( + $this->uuid_factory->fromString($v) + ) + ), + $gap_ids + ) + ); + } + + /** + * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Combinations\MatchingValue> $matching_values + */ + public function buildCombination( + Uuid $combination_id, + ?float $points, + array $matching_values + ): Combination { + return new Combination( + $combination_id, + $points, + $matching_values + ); + } + + /** + * @param array $values_array + */ + public function buildMatchingValuesFromForm( + Properties $properties, + Uuid $combination_id, + array $values_array + ): array { + return array_reduce( + array_keys($values_array), + function (array $c, string $v) use ( + $properties, + $values_array, + $combination_id + ): array { + $gap = $properties->getGaps()->getGapById( + $this->uuid_factory->fromString($v) + ); + $answer_option = + $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($values_array[$v]) + ); + + if ($answer_option === null) { + return $c; + } + + $c[] = new MatchingValue( + $combination_id, + $gap, + $answer_option, + null + ); + return $c; + }, + [] + ); + } + + public function buildCombinationFromCarryValue( + string $carry, + Properties $properties + ): Combination { + $values_array = json_decode($carry, true); + $combination_id = $this->uuid_factory->fromString( + array_key_first($values_array) + ); + + return new Combination( + $combination_id, + null, + array_map( + fn(string $v): MatchingValue => new MatchingValue( + $combination_id, + $properties->getGaps()->getGapById( + $this->uuid_factory->fromString($v) + ) + ), + $values_array[$combination_id->toString()] + ) + ); + } + + private function retrieveCombinationsFromQuery( + TypeGenericProperties $type_generic_properties, + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::Additional->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder( + $type_generic_properties->getDefinition()::class + ), + Persistence::COMBINATION_TABLE_IDENTIFIER + ), + $query->getRefinery()->custom()->transformation( + fn(array $vs): array => $this->buildCombinationsFromQuery( + array_filter( + $vs, + fn(array $v): bool => $v['answer_form_id'] !== null + ) + ) + ) + ); + } + + private function buildCombinationsFromQuery( + array $values + ): array { + if ($values === []) { + return []; + } + + return array_reduce( + $values, + function (array $c, array $v): array { + if (array_key_exists($v['id'], $c)) { + return $c; + } + + $c[$v['id']] = new Combination( + $this->uuid_factory->fromString($v['id']), + $v['points'] + ); + + return $c; + }, + [] + ); + } + + private function retrieveMatchingValuesFromQuery( + TypeGenericProperties $type_generic_properties, + Gaps $gaps, + array $combinations, + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::Additional->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder( + $type_generic_properties->getDefinition()::class + ), + Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ), + $query->getRefinery()->custom()->transformation( + function (array $vs) use ( + $gaps, + $combinations + ): array { + $already_added = []; + foreach ($vs as $v) { + if (!array_key_exists($v['combination_id'], $combinations) + || in_array( + $v['combination_id'] . $v['gap_id'], + $already_added + ) + ) { + continue; + } + + $already_added[] = $v['combination_id'] . $v['gap_id']; + + $gap = $gaps->getGapById( + $this->uuid_factory->fromString($v['gap_id']) + ); + + $combinations[$v['combination_id']] = $combinations[$v['combination_id']] + ->withAdditionalMatchingValue( + new MatchingValue( + $this->uuid_factory->fromString($v['combination_id']), + $gap, + $gap->getAnswerOptions() + ->getAnswerOptionById( + $this->uuid_factory->fromString($v['answer_option_id']) + ) + ) + ); + } + + return array_values($combinations); + } + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php new file mode 100644 index 000000000000..a32b62eff39c --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/InRange.php @@ -0,0 +1,38 @@ + $lng->txt('in_range'), + self::OutOfRange => $lng->txt('out_of_range') + }; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php new file mode 100644 index 000000000000..50eb350fbece --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Combinations/MatchingValue.php @@ -0,0 +1,114 @@ +gap; + } + + public function getAnswerOption(): ?AnswerOption + { + return $this->answer_option; + } + + public function buildPresentationString( + Language $lng + ): string { + if ($this->answer_option === null) { + return ''; + } + + if ($this->in_range !== null) { + return $this->in_range->getLabel($lng); + } + + $value = $this->answer_option->getTextValue(); + if (strlen($value) < 11) { + return $value; + } + + return mb_substr($value, 0, 10) . '...'; + } + + public function toStorage( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Replace { + if ($this->answer_option === null) { + throw new \UnexpectedValueException( + 'A MatchingValue without AnswerOption cannot be stored.' + ); + } + + $table_definition = TableTypes::Additional; + return $persistence_factory->replace( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition, + $persistence->getCombinationToAnswerOptionsTableIdentifier() + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->combination_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->gap->getAnswerInputId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_option->getAnswerOptionId()->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->in_range?->value + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php new file mode 100644 index 000000000000..5a1903432530 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Definitions/ScoringIdentical.php @@ -0,0 +1,68 @@ +txt($this->value); + } + + public static function buildInput( + Language $lng, + FieldFactory $ff, + Refinery $refinery, + self $default_value + ): Select { + return $ff->select( + $lng->txt('scoring_of_identical_responses'), + self::buildOptionsList($lng) + )->withRequired(true) + ->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(string $v): self => self::tryFrom($v) ?? $default_value + ) + ); + } + + private static function buildOptionsList( + Language $lng + ): array { + return array_reduce( + self::cases(), + function (array $c, self $v) use ($lng): array { + $c[$v->value] = $lng->txt($v->value); + return $c; + }, + [] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php new file mode 100644 index 000000000000..c5112d5622ce --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Factory.php @@ -0,0 +1,171 @@ +getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + ScoringIdentical::ScoreAll, + $this->gaps_factory->getEmptyGapsObject( + $type_generic_properties->getAnswerFormId() + ), + $this->combinations_factory->getCombinations( + $type_generic_properties, + false + ) + ); + } + + [ + 'scoring_identical_responses' => $scoring_identical_responses, + 'combinations_enabled' => $combinations_enabled + ] = $query->retrieveCurrentRecord( + TableTypes::TypeSpecificAnswerForms->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder($type_generic_properties->getDefinition()::class) + ), + $query->getRefinery()->custom()->transformation( + function (array $vs) use ($type_generic_properties): array { + $values = $this->retrieveFirstMatchingRowFromDBRecords( + $type_generic_properties->getAnswerFormId()->toString(), + $vs + ); + return [ + 'scoring_identical_responses' => ScoringIdentical::tryFrom($values['scoring_identical_responses']), + 'combinations_enabled' => $values['combinations_enabled'] === 1 + ]; + } + ) + ); + + $gaps = $this->gaps_factory->fromDatabase( + $type_generic_properties->getAnswerFormId(), + $query + ); + + return new Properties( + $type_generic_properties->getAnswerFormId(), + $type_generic_properties->getQuestionId(), + $type_generic_properties->getDefinition(), + $this->cloze_text_factory->buildFromTextString( + $type_generic_properties->getAdditionalText() + ), + $type_generic_properties->getAdditionalTextLegacy(), + $scoring_identical_responses, + $gaps, + $this->combinations_factory->getCombinations( + $type_generic_properties, + $combinations_enabled, + $gaps, + $query + ) + ); + } + + public function fromBasicEditingForm( + Properties $properties, + ClozeText $cloze_text, + ScoringIdentical $scoring_of_identical_responses, + bool $combinations_enabled + ): Properties { + $updated_properties = $properties + ->withScoringOfIdenticalResponses($scoring_of_identical_responses) + ->withCombinations( + $properties->getCombinations()->withCombinationsEnabled( + $combinations_enabled + ) + ); + + if ($updated_properties->getLegacyClozeText() !== '' + && $cloze_text->getRawRepresentation() === '') { + return $updated_properties; + } + + $updated_gaps = $cloze_text->updateGapsFromMarkdown( + $properties->getAnswerFormId(), + $properties->getGaps() + ); + + return $updated_properties + ->withClozeText( + $cloze_text->withIdsOfNewGapsInClozeText( + $updated_gaps->getIncompleteGaps() + ) + )->withGaps($updated_gaps); + } + + public function fromCarry( + Properties $properties, + ?string $carry + ): Properties { + if ($carry === null + || !is_array( + ($carry_array = json_decode($carry, true)) + )) { + return $properties; + } + + return $properties->withValuesFromCarry( + $this->cloze_text_factory, + $carry_array + ); + } + + private function retrieveFirstMatchingRowFromDBRecords( + string $answer_form_id, + array $vs + ): ?array { + foreach ($vs as $row) { + if ($row['answer_form_id'] === $answer_form_id) { + return $row; + } + } + return null; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php new file mode 100644 index 000000000000..96ce83eb3252 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOption.php @@ -0,0 +1,191 @@ +answer_option_id; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function withPosition( + int $position + ): self { + $clone = clone $this; + $clone->position = $position; + return $clone; + } + + public function getTextValue(): string + { + return $this->text_value; + } + + public function withTextValue( + string $text_value + ): self { + $clone = clone $this; + $clone->text_value = $text_value; + return $clone; + } + + public function getLowerLimit(): ?float + { + return $this->lower_limit; + } + + public function withLowerLimit( + float $lower_limit + ): self { + $clone = clone $this; + $clone->lower_limit = $lower_limit; + return $clone; + } + + public function getUpperLimit(): ?float + { + return $this->upper_limit; + } + + public function withUpperLimit( + ?float $upper_limit + ): self { + $clone = clone $this; + $clone->upper_limit = $upper_limit; + return $clone; + } + + public function getAvailablePoints(): float + { + return $this->available_points; + } + + public function withAvailablePoints( + float $available_points + ): self { + $clone = clone $this; + $clone->available_points = $available_points; + return $clone; + } + + public function toCarry(): array + { + $values = [ + self::FORM_KEY_ID => $this->getAnswerOptionId()->toString(), + self::FORM_KEY_POSITION => (string) $this->getPosition(), + self::FORM_KEY_TEXT_VALUE => $this->getTextValue() + ]; + + if ($this->getLowerLimit() !== null) { + $values[self::FORM_KEY_LOWER_LIMIT] = (string) $this->getLowerLimit(); + } + + if ($this->getUpperLimit() !== null) { + $values[self::FORM_KEY_UPPER_LIMIT] = (string) $this->getUpperLimit(); + } + + if ($this->getAvailablePoints() !== null) { + $values[self::FORM_KEY_AVAILABLE_POINTS] = (string) $this->getAvailablePoints(); + } + + return $values; + } + + public function buildReplace( + PersistenceFactory $persistence_factory, + ?Replace $replace, + array $columns + ): Replace { + if ($replace === null) { + return $persistence_factory->replace( + $columns, + $this->buildValuesForGapReplace($persistence_factory) + ); + } + + return $replace->withAdditionalValues( + $this->buildValuesForGapReplace($persistence_factory) + ); + } + + private function buildValuesForGapReplace( + PersistenceFactory $persistence_factory + ): array { + return [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_option_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_input_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->text_value + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->available_points + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->lower_limit + ), + $persistence_factory->value( + \ilDBConstants::T_FLOAT, + $this->upper_limit + ) + ]; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php new file mode 100644 index 000000000000..c673ec6291e2 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/AnswerOptions.php @@ -0,0 +1,330 @@ +answer_options_awarding_points = $this->buildAnswerOptionsAwardingPointsFromAnswerOptions($answer_options); + } + + public function isIncomplete(): bool + { + return $this->is_incomplete + || $this->answer_options === [] + || $this->answer_options_awarding_points === []; + } + + public function withIsIncomplete( + bool $is_incomplete + ): self { + $clone = clone $this; + $clone->is_incomplete = $is_incomplete; + return $clone; + } + + public function getAnswerOptionById( + Uuid $answer_option_id + ): ?AnswerOption { + return array_find( + $this->answer_options, + fn(AnswerOption $v): bool => $v->getAnswerOptionId()->toString() === $answer_option_id->toString() + ); + } + + public function getAnswerOptionForPositionOrNew( + int $position + ): AnswerOption { + return $this->answer_options[$position] + ?? $this->factory->getDefaultAnswerOptionForPosition( + $this->answer_input_id, + $position + ); + } + + public function getTagsArrayFromAnswerOptions(): array + { + return array_reduce( + $this->answer_options, + function (array $c, AnswerOption $v): array { + if ($v->getTextValue() === '') { + return $c; + } + $c[] = $v->getTextValue(); + return $c; + }, + [] + ); + } + + public function getAnswerOptionsAwardingPoints(): array + { + return $this->answer_options_awarding_points; + } + + public function withAnswerOptionsAwardingPoints( + array $options + ): self { + $clone = clone $this; + $clone->answer_options_awarding_points = array_reduce( + $options, + function (array $c, string $v): array { + $answer_option = $this->retrieveAnswerOptionByTextValue($v); + if ($answer_option !== null) { + $c[] = $answer_option; + } + return $c; + }, + [] + ); + return $clone; + } + + public function withAnswerOptions( + array $answer_options + ): self { + $clone = clone $this; + $clone->answer_options = $answer_options; + return $clone; + } + + public function withAnswerOptionsFromTags( + array $tags + ): self { + $clone = clone $this; + $position = 0; + $clone->answer_options = array_map( + function (string $v) use (&$position): AnswerOption { + return $this->buildAnswerOptionFromTag( + $position++, + $v + ); + }, + $tags + ); + return $clone; + } + + public function withAnswerOptionsWithAddedPointsFromForm( + Refinery $refinery, + array $values_from_form + ): self { + $clone = clone $this; + $clone->answer_options = array_map( + function (AnswerOption $v) use ($refinery, $values_from_form): AnswerOption { + $answer_option_id = $v->getAnswerOptionId()->toString(); + if (array_key_exists($answer_option_id, $values_from_form)) { + return $v->withAvailablePoints( + $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always(null) + ])->transform($values_from_form[$answer_option_id]) + ); + } + + return $v; + }, + $clone->answer_options + ); + $clone->answer_options_awarding_points = $clone + ->buildAnswerOptionsAwardingPointsFromAnswerOptions($clone->answer_options); + return $clone; + } + + public function buildArrayForSelectInput( + Transformation $shuffle_transformation + ): array { + return array_reduce( + $shuffle_transformation->transform($this->answer_options), + function (array $c, AnswerOption $v): array { + $c[$v->getAnswerOptionId()->toString()] = $v->getTextValue(); + return $c; + }, + [] + ); + } + + public function toCarry(): array + { + return [ + self::KEY_IS_INCOMPLETE => $this->is_incomplete ? 1 : 0, + self::KEY_ANSWER_OPIONS => array_map( + fn(AnswerOption $v): array => $v->toCarry(), + $this->answer_options + ), + self::KEY_ANSWER_OPTIONS_AWARDING_POINTS => array_map( + fn(AnswerOption $v): string => $v->getAnswerOptionId()->toString(), + $this->answer_options_awarding_points + ) + ]; + } + + public function withValuesFromCarry( + array $carry + ): self { + $clone = clone $this; + $clone->is_incomplete = $carry[self::KEY_IS_INCOMPLETE] === 1; + $clone->answer_options = array_map( + fn(array $vs): AnswerOption => $this->factory->buildAnswerOption( + $vs[AnswerOption::FORM_KEY_ID], + $this->answer_input_id, + (int) $vs[AnswerOption::FORM_KEY_POSITION], + $vs[AnswerOption::FORM_KEY_TEXT_VALUE], + $vs[AnswerOption::FORM_KEY_LOWER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_UPPER_LIMIT] ?? null, + $vs[AnswerOption::FORM_KEY_AVAILABLE_POINTS] ?? null + ), + $carry[self::KEY_ANSWER_OPIONS] ?? [] + ); + + $clone->answer_options_awarding_points = array_filter( + $clone->answer_options, + fn(AnswerOption $v): bool => in_array( + $v->getAnswerOptionId()->toString(), + $carry[self::KEY_ANSWER_OPTIONS_AWARDING_POINTS] ?? [] + ) + ); + return $clone; + } + + public function getEditPointsInputs( + FieldFactory $ff, + \Closure $build_label, + ?array $answer_options_awarding_points = null + ): array { + return array_reduce( + $answer_options_awarding_points ?? $this->answer_options, + function (array $c, AnswerOption $v) use ($ff, $build_label): array { + $c[$v->getAnswerOptionId()->toString()] = $ff->numeric($build_label($v)) + ->withStepSize(0.01) + ->withValue($v->getAvailablePoints()); + return $c; + }, + [] + ); + } + + public function buildReplace( + ?Replace $replace, + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Replace { + return array_reduce( + $this->answer_options, + fn(?Replace $c, AnswerOption $v): Replace => $v->buildReplace( + $persistence_factory, + $c, + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + TableTypes::AnswerOptions + ) + ), + $replace + ); + } + + public function buildDelete( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $answer_options_table_definition = TableTypes::AnswerOptions; + + return $persistence_factory->delete( + $answer_options_table_definition->getTable( + $persistence_factory, + $table_name_builder + ), + [ + $persistence_factory->where( + $persistence->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $answer_options_table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_input_id->toString() + ) + ) + ] + ); + } + + private function buildAnswerOptionsAwardingPointsFromAnswerOptions( + array $answer_options + ): array { + return array_filter( + $answer_options, + fn(AnswerOption $v): bool => $v->getAvailablePoints() > 0.0 + ); + } + + private function buildAnswerOptionFromTag( + int $position, + string $text_value + ): AnswerOption { + $answer_option = $this->retrieveAnswerOptionByTextValue($text_value) + ?? $this->factory->getDefaultAnswerOptionForPosition( + $this->answer_input_id, + $position + ); + + return $answer_option + ->withPosition($position) + ->withTextValue($text_value); + } + + private function retrieveAnswerOptionByTextValue( + string $value + ): ?AnswerOption { + $filtered_array = array_filter( + $this->answer_options, + fn(AnswerOption $v): bool => $v->getTextValue() === $value + ); + return array_shift($filtered_array) ?? null; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php new file mode 100644 index 000000000000..116107608504 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/AnswerOptions/Factory.php @@ -0,0 +1,138 @@ +uuid_factory->uuid4(), + $answer_input_id, + $position + ); + } + + public function buildAnswerOption( + string $answer_option_id, + Uuid $answer_input_id, + int $position, + string $text_value, + ?string $lower_limit, + ?string $upper_limit, + ?string $points + ): AnswerOption { + return new AnswerOption( + $this->uuid_factory->fromString($answer_option_id), + $answer_input_id, + $position, + $text_value, + $this->convertToFloatOrNull($lower_limit), + $this->convertToFloatOrNull($upper_limit), + $this->convertToFloatOrNull($points) + ); + } + + public function fromDatabase( + Query $query + ): array { + return $query->retrieveCurrentRecord( + TableTypes::AnswerOptions->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder(Definition::class) + ), + $query->getRefinery()->custom()->transformation( + function (array $vs): array { + $previous_answer_input_id = null; + $return_array = []; + $answer_options = []; + foreach ($vs as $v) { + if (array_key_exists($v['id'], $answer_options)) { + continue; + } + + if ($previous_answer_input_id !== null + && $v['answer_input_id'] !== $previous_answer_input_id) { + $return_array[$previous_answer_input_id] = new AnswerOptions( + $this, + $this->uuid_factory->fromString($previous_answer_input_id), + $answer_options + ); + $answer_options = []; + } + $previous_answer_input_id = $v['answer_input_id']; + $answer_options[$v['id']] = new AnswerOption( + $this->uuid_factory->fromString($v['id']), + $this->uuid_factory->fromString($v['answer_input_id']), + $v['position'], + $v['text_value'], + $v['lower_limit'], + $v['upper_limit'], + $v['points'] + ); + } + + $return_array[$v['answer_input_id']] = new AnswerOptions( + $this, + $this->uuid_factory->fromString($v['answer_input_id']), + $answer_options + ); + + return $return_array; + } + ) + ); + } + + private function convertToFloatOrNull( + ?string $value + ): ?float { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->float(), + $this->refinery->always(null) + ])->transform($value); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php new file mode 100644 index 000000000000..947c50361936 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Factory.php @@ -0,0 +1,153 @@ +available_gap_types[$type->getIdentifier()] = $type; + } + } + + public function getAvailableGapTypes(): array + { + return $this->available_gap_types; + } + + public function getAvailableGapTypesOptionsArray( + Language $lng + ): array { + return array_map( + fn(Type $v) => $lng->txt("{$v->getIdentifier()}_gap"), + $this->available_gap_types + ); + } + + public function getNewGap( + Uuid $answer_form_id, + int $position, + string $id = '' + ): Gap { + $answer_input_id = $id !== '' + ? $this->uuid_factory->fromString($id) + : $this->uuid_factory->uuid4(); + + return new Gap( + $answer_input_id, + $answer_form_id, + $position, + $this->answer_options_factory->getDefaultAnswerOptions($answer_input_id) + ); + } + + public function getEmptyGapsObject( + Uuid $answer_form_id, + ): Gaps { + return new Gaps( + $this->refinery, + $this, + $answer_form_id, + [] + ); + } + + public function getGapTypeByIdentifier( + string $identifier + ): Type { + if (!array_key_exists($identifier, $this->available_gap_types)) { + throw new \InvalidArgumentException('Gap type does not exist.'); + } + return $this->available_gap_types[$identifier]; + } + + public function fromDatabase( + Uuid $answer_form_id, + Query $query + ): Gaps { + $answer_options = $this->answer_options_factory->fromDatabase($query); + + return $query->retrieveCurrentRecord( + TableTypes::AnswerInputs->getTable( + $query->getPersistenceFactory(), + $query->getTableNameBuilder(Definition::class) + ), + $query->getRefinery()->custom()->transformation( + function (array $vs) use ($answer_form_id, $answer_options): Gaps { + $previous_answer_input_id = null; + $gaps = []; + foreach ($vs as $v) { + if ($v['answer_form_id'] !== $answer_form_id->toString() + || $previous_answer_input_id === $v['id']) { + continue; + } + $previous_answer_input_id = $v['id']; + $gaps[] = $this->buildGapFromDBValues($v, $answer_options); + } + return new Gaps( + $this->refinery, + $this, + $answer_form_id, + $gaps + ); + } + ) + ); + } + + private function buildGapFromDBValues( + array $values, + array $answer_options + ): Gap { + $answer_input_uuid = $this->uuid_factory->fromString($values['id']); + return new Gap( + $answer_input_uuid, + $this->uuid_factory->fromString($values['answer_form_id']), + $values['position'], + $answer_options[$values['id']], + $this->getGapTypeByIdentifier($values['gap_type']), + $values['max_chars'], + $values['step_size'], + $values['text_matching_method'] === null + ? null + : TextMatchingOptions::tryFrom($values['text_matching_method']), + $values['min_autocomplete'], + $values['shuffle_answer_options'] === 1 + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php new file mode 100644 index 000000000000..a27fc3e3bd58 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gap.php @@ -0,0 +1,411 @@ + $answer_options + */ + public function __construct( + private readonly Uuid $answer_input_id, + private readonly Uuid $answer_form_id, + private int $position, + private AnswerOptions $answer_options, + private ?Type $type = null, + private ?int $max_chars = null, + private ?float $step_size = null, + private ?TextMatchingOptions $text_matching_method = null, + private ?int $min_autocomplete = null, + private ?bool $shuffle_answer_options = null + ) { + } + + public function getAnswerInputId(): Uuid + { + return $this->answer_input_id; + } + + public function getPosition(): int + { + return $this->position; + } + + public function withPosition( + int $position + ): self { + $clone = clone $this; + $clone->position = $position; + return $clone; + } + + public function isUndefined(): bool + { + return $this->type === null; + } + + public function getType(): ?Type + { + return $this->type; + } + + public function withType( + Type $type + ): self { + $clone = clone $this; + $clone->type = $type; + return $clone; + } + + public function getMaxChars(): ?int + { + return $this->max_chars; + } + + public function withMaxChars( + ?int $max_chars + ): self { + $clone = clone $this; + $clone->max_chars = $max_chars; + return $clone; + } + + public function getStepSize(): ?float + { + return $this->step_size; + } + + public function withStepSize( + float $step_size + ): self { + $clone = clone $this; + $clone->step_size = $step_size; + return $clone; + } + + public function getTextMatchingMethod(): ?TextMatchingOptions + { + return $this->text_matching_method; + } + + public function withTextMatchingMethod( + TextMatchingOptions $matching_method + ): self { + $clone = clone $this; + $clone->text_matching_method = $matching_method; + return $clone; + } + + public function getMinAutocomplete(): ?int + { + return $this->min_autocomplete; + } + + public function withMinAutocomplete( + int $min_autocomplete + ): self { + $clone = clone $this; + $clone->min_autocomplete = $min_autocomplete; + return $clone; + } + + public function getShuffleAnswerOptions(): ?bool + { + return $this->shuffle_answer_options; + } + + public function withShuffleAnswerOptions( + bool $shuffle_answer_options + ): self { + $clone = clone $this; + $clone->shuffle_answer_options = $shuffle_answer_options; + return $clone; + } + + public function getAnswerOptions(): AnswerOptions + { + return $this->answer_options; + } + + public function withAnswerOptions( + AnswerOptions $answer_options + ): self { + $clone = clone $this; + $clone->answer_options = $answer_options; + return $clone; + } + + public function toCarry(): array + { + $inputs = [ + self::KEY_TYPE => $this->type?->getIdentifier() ?? '', + self::KEY_POSITION => $this->position, + self::KEY_ANSWER_OPTIONS => $this->answer_options->toCarry() + ]; + + if ($this->max_chars !== null) { + $inputs[self::KEY_MAX_CHARS] = (string) $this->getMaxChars(); + } + + if ($this->step_size !== null) { + $inputs[self::KEY_STEP_SIZE] = (string) $this->getStepSize(); + } + + if ($this->text_matching_method !== null) { + $inputs[self::KEY_TEXT_MATCHING_METHOD] = $this->getTextMatchingMethod()->value; + } + + if ($this->min_autocomplete !== null) { + $inputs[self::KEY_MIN_AUTOCOMPLETE] = (string) $this->getMinAutocomplete(); + } + + if ($this->shuffle_answer_options !== null) { + $inputs[self::KEY_SHUFFLE_ANSWER_OPTIONS] = $this->getShuffleAnswerOptions() ? '1' : '0'; + } + + return $inputs; + } + + public function withValuesFromCarry( + Refinery $refinery, + Factory $gaps_factory, + array $carry + ): self { + if ($carry === null) { + return $this; + } + + $clone = clone $this; + $clone->type = $carry[self::KEY_TYPE] === '' + ? $this->getType() + : $gaps_factory->getGapTypeByIdentifier($carry[self::KEY_TYPE]); + $clone->position = $carry[self::KEY_POSITION]; + + $clone->max_chars = $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMaxChars()) + ])->transform($carry[self::KEY_MAX_CHARS] ?? null); + + $clone->step_size = $refinery->byTrying([ + $refinery->kindlyTo()->float(), + $refinery->always($this->getStepSize()) + ])->transform($carry[self::KEY_STEP_SIZE] ?? null); + + $clone->text_matching_method = is_string($carry[self::KEY_TEXT_MATCHING_METHOD] ?? null) + ? TextMatchingOptions::tryFrom($carry[self::KEY_TEXT_MATCHING_METHOD]) + : $this->getTextMatchingMethod(); + + $clone->min_autocomplete = $refinery->byTrying([ + $refinery->kindlyTo()->int(), + $refinery->always($this->getMinAutocomplete()) + ])->transform($carry[self::KEY_MIN_AUTOCOMPLETE] ?? null); + + $clone->shuffle_answer_options = $refinery->byTrying([ + $refinery->kindlyTo()->bool(), + $refinery->always($this->getShuffleAnswerOptions()) + ])->transform($carry[self::KEY_SHUFFLE_ANSWER_OPTIONS] ?? null); + + $clone->answer_options = $this->answer_options + ->withValuesFromCarry($carry[self::KEY_ANSWER_OPTIONS]); + + return $clone; + } + + public function buildReplace( + ?Replace $replace, + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Replace { + if ($this->type === null) { + throw new \UnexpectedValueException( + 'A Gap without Type cannot be stored.' + ); + } + + $table_definition = TableTypes::AnswerInputs; + + if ($replace === null) { + return $persistence_factory->replace( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $this->buildValuesForGapReplace($persistence_factory) + ); + } + + return $replace->withAdditionalValues( + $this->buildValuesForGapReplace($persistence_factory) + ); + } + + public function getGapPlaceholder(): string + { + return "{{{$this->buildGapPlaceholderNameWithId()}}}"; + } + + public function buildShortenedGapName(): string + { + return self::GAP_PLACEHOLDER_NAME . '_' . $this->getShortenedAnswerInputId(); + } + + public function buildShortenedGapRepresentation(): string + { + return "[{$this->buildShortenedGapName()}]"; + } + + public function buildGapPlaceholderNameWithId(): string + { + return self::GAP_PLACEHOLDER_NAME . '_' . $this->getAnswerInputId()->toString(); + } + + public function buildParticipantViewLegacyInput(): string + { + return $this->type->getParticipantViewLegacyInput($this); + } + + public function getEditAnswerOptionsSection( + Language $lng, + FieldFactory $ff + ): Section { + $section = $ff->section( + $this->getType()->getEditAnswerOptionsInputs($this), + "{$this->buildShortenedGapName()} ({$lng->txt("{$this->getType()->getIdentifier()}_gap")})" + ); + + $edit_section_constraint = $this->getType()->getEditAnswerOptionsSectionConstraint(); + if ($edit_section_constraint !== null) { + $section = $section->withAdditionalTransformation($edit_section_constraint); + } + + + return $section->withAdditionalTransformation( + $this->getType()->getBuildGapTransformation($this) + ); + } + + public function getEditPointsSection( + Language $lng, + FieldFactory $ff + ): Section { + $type = $this->getType(); + $section = $ff->section( + $type->getEditPointsInputs($this->getAnswerOptions()), + "{$this->buildShortenedGapName()} ({$lng->txt("{$type->getIdentifier()}_gap")})" + ); + + $edit_section_constraint = $type->getEditPointsSectionConstraint(); + if ($edit_section_constraint !== null) { + $section = $section->withAdditionalTransformation($edit_section_constraint); + } + + + return $section->withAdditionalTransformation( + $type->getAddPointsTransformation($this) + ); + } + + public function toTableRow( + DataRowBuilder $row_builder, + Language $lng + ): DataRow { + $total_points = 0; + $answer_options_list = ''; + foreach ($this->answer_options->getAnswerOptionsAwardingPoints() as $option) { + $total_points += $option->getAvailablePoints(); + + $gap_text = $option->getTextValue(); + if ($gap_text === '') { + $gap_text = $option->getLowerlimit(); + } + + $answer_options_list .= "{$gap_text} ({$option->getAvailablePoints()})
"; + } + + return $row_builder->buildDataRow( + $this->answer_input_id->toString(), + [ + 'gap' => $this->buildShortenedGapName(), + 'type' => $lng->txt("{$this->type->getIdentifier()}_gap"), + 'answers_options_awarding_points' => $answer_options_list, + 'available_points' => $total_points + ] + ); + } + + private function buildValuesForGapReplace( + PersistenceFactory $persistence_factory + ): array { + return [ + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_input_id->toString()), + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->answer_form_id->toString()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->position), + $persistence_factory->value(\ilDBConstants::T_TEXT, $this->type->getIdentifier()), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->max_chars), + $persistence_factory->value(\ilDBConstants::T_FLOAT, $this->step_size), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->text_matching_method?->value), + $persistence_factory->value(\ilDBConstants::T_INTEGER, $this->min_autocomplete), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->shuffle_answer_options === null + ? null + : ($this->shuffle_answer_options ? 1 : 0) + ) + + ]; + } + + private function getShortenedAnswerInputId(): string + { + return mb_substr($this->answer_input_id->toString(), 0, 4); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php new file mode 100644 index 000000000000..837e154ba746 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Gaps.php @@ -0,0 +1,542 @@ + + */ + private array $gaps; + + public function __construct( + private readonly Refinery $refinery, + private readonly Factory $factory, + private Uuid $answer_form_id, + array $gaps + ) { + $this->gaps = array_reduce( + $gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function getGapById( + Uuid $gap_id + ): ?Gap { + return $this->gaps[$gap_id->toString()] ?? null; + } + + public function getGapByTagName( + string $tag_name + ): ?Gap { + return $this->gaps[$this->extractIdFromTagName($tag_name)] ?? null; + } + + public function getNumberOfGaps( + ): int { + return count($this->gaps); + } + + public function hasAtLeastOneGap(): bool + { + return $this->gaps !== []; + } + + public function withGap( + Gap $gap + ): self { + $clone = clone $this; + $clone->gaps[$gap->getAnswerInputId()->toString()] = $gap; + return $clone; + } + + public function withNewGap( + Uuid $answer_form_id, + int $position + ): self { + $new_gap = $this->factory->getNewGap($answer_form_id, $position); + $clone = clone $this; + $clone->gaps[$new_gap->getAnswerInputId()->toString()] = $new_gap; + return $clone; + } + + public function withAdditionalGapFromTagName( + Uuid $answer_form_id, + string $tag_name, + int $position + ): self { + $answer_input_id = $this->extractIdFromTagName($tag_name); + $clone = clone $this; + $clone->gaps[$answer_input_id] = $this->factory->getNewGap( + $answer_form_id, + $position, + $answer_input_id + ); + return $clone; + } + + public function withResetGaps(): self + { + if ($this->gaps === []) { + return $this; + } + + $clone = clone $this; + $clone->gaps = []; + return $clone; + } + + public function getIncompleteGaps(): array + { + return array_filter( + $this->gaps, + fn(Gap $v): bool => $v->getAnswerOptions()->isIncomplete() + ); + } + + public function withMarkedIncompleteGaps(): self + { + $clone = clone $this; + $clone->gaps = array_map( + fn(Gap $v): Gap => $v->getAnswerOptions()->isIncomplete() + ? $v->withAnswerOptions( + $v->getAnswerOptions()->withIsIncomplete(true) + ) : $v, + $clone->gaps + ); + return $clone; + } + + public function getRemovedGaps( + self $old_gaps + ): array { + return array_diff_key($old_gaps->gaps, $this->gaps); + } + + public function getAddedGaps( + self $old_gaps + ): array { + return array_diff_key($this->gaps, $old_gaps->gaps); + } + + public function getPlaceholderArrayForParticipantView(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildParticipantViewLegacyInput(); + return $c; + }, + [] + ); + } + + public function getPlaceholderArrayForEditFormPanel(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->buildGapPlaceholderNameWithId($v)] = $v->buildShortenedGapRepresentation(); + return $c; + }, + [] + ); + } + + public function buildGapsTypeInputs( + Language $lng, + FieldFactory $ff, + array $available_gap_types, + Properties $properties, + bool $is_in_creation_context, + array $selected_gaps + ): Section { + return $ff->section( + array_reduce( + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), + function (array $c, Gap $v) use ($ff, $available_gap_types): array { + $c[$v->getAnswerInputId()->toString()] = $ff->select( + $v->buildShortenedGapName(), + $available_gap_types + )->withRequired(true) + ->withValue($v->getType()?->getIdentifier()); + return $c; + }, + [] + ), + $lng->txt('select_gap_types') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap( + $c->gaps[$v]->withType( + $this->factory->getGapTypeByIdentifier($vs[$v]) + ) + ), + $this + ) + ) + ) + ); + } + + public function buildAnswerOptionsInputs( + Language $lng, + FieldFactory $ff, + Properties $properties, + bool $is_in_creation_context, + array $selected_gaps + ): Section { + return $ff->section( + array_reduce( + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), + function (array $c, Gap $v) use ($lng, $ff): array { + $c[$v->getAnswerInputId()->toString()] = $v->getEditAnswerOptionsSection( + $lng, + $ff + ); + return $c; + }, + [] + ), + $lng->txt('add_answer_options') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) + ) + ) + ); + } + + public function buildPointInputs( + Language $lng, + FieldFactory $ff, + Properties $properties, + bool $is_in_creation_context, + array $selected_gaps + ): Section { + return $ff->section( + array_reduce( + $this->retrieveGapsForInputs( + $is_in_creation_context, + $selected_gaps + ), + function (array $c, Gap $v) use ($lng, $ff): array { + $c[$v->getAnswerInputId()->toString()] = $v->getEditPointsSection( + $lng, + $ff + ); + return $c; + }, + [] + ), + $lng->txt('add_points') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(array $vs): Properties => $properties->withGaps( + array_reduce( + array_keys($vs), + fn(self $c, string $v): self => $c->withGap($vs[$v]), + $this + ) + ) + ) + ); + } + + public function buildGapsMultiSelect( + string $label, + FieldFactory $ff + ): MultiSelect { + return $ff->multiSelect( + $label, + array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->buildShortenedGapName(); + return $c; + }, + [] + ) + ); + } + + public function toCarry(): array + { + return array_reduce( + $this->gaps, + function (array $c, Gap $v): array { + $c[$v->getAnswerInputId()->toString()] = $v->toCarry(); + return $c; + }, + [] + ); + } + + public function withValuesFromCarry( + array $carry + ): self { + $clone = clone $this; + foreach ($carry as $answer_input_id => $gap_definition) { + if (!isset($clone->gaps[$answer_input_id])) { + $clone->gaps[$answer_input_id] = $this->factory->getNewGap( + $this->answer_form_id, + 0, + $answer_input_id + ); + } + + $clone->gaps[$answer_input_id] = $clone->gaps[$answer_input_id] + ->withValuesFromCarry( + $this->refinery, + $this->factory, + $gap_definition + ); + } + + return $clone; + } + + public function toTableRows( + DataRowBuilder $row_builder, + Language $lng + ): \Generator { + foreach ($this->orderGapsByPosition($this->gaps) as $gap) { + yield $gap->toTableRow( + $row_builder, + $lng + ); + } + } + + public function toStorage( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + [ + 'gaps' => $replace_for_gaps, + 'answer_options' => $replace_for_answer_options + ] = array_reduce( + $this->gaps, + fn(array $c, Gap $v): array => [ + 'gaps' => $v->buildReplace( + $c['gaps'], + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ), + 'answer_options' => $v->getAnswerOptions()->buildReplace( + $c['answer_options'], + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ], + [ + 'gaps' => null, + 'answer_options' => null + ] + ); + + return $manipulate->withAdditionalStatement( + $this->buildDeleteForRemovedGaps( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + )->withAdditionalStatement($replace_for_gaps) + ->withAdditionalStatement($replace_for_answer_options); + } + + public function toDelete( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + return array_reduce( + $this->gaps, + fn(Manipulate $c, Gap $v): Manipulate => $c->withAdditionalStatement( + $v->getAnswerOptions()->buildDelete( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ), + $manipulate->withAdditionalStatement( + $this->buildDeleteForDeletionOfAnswerForm( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ) + ); + } + + private function buildDeleteForRemovedGaps( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::AnswerInputs; + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), + [ + $persistence_factory->where( + $persistence->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ), + $persistence_factory->where( + $persistence->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + array_map( + fn(Gap $v): string => $v->getAnswerInputId()->toString(), + $this->gaps + ) + ), + Operator::In, + Junctor::Conjunction, + true + ) + ] + ); + } + + private function buildDeleteForDeletionOfAnswerForm( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::AnswerInputs; + + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), + [ + $persistence_factory->where( + $persistence->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + ) + ] + ); + } + + private function orderGapsByPosition( + array $gaps + ): array { + usort( + $gaps, + fn(Gap $a, Gap $b) => $a->getPosition() <=> $b->getPosition() + ); + + return $gaps; + } + + private function extractIdFromTagName( + string $tag_name + ): string { + return mb_substr($tag_name, mb_strlen(Gap::GAP_PLACEHOLDER_NAME) + 1); + } + + private function retrieveGapsForInputs( + bool $is_in_creation_context, + array $selected_gaps + ): array { + if ($is_in_creation_context) { + return $this->gaps; + } + + if ($selected_gaps === []) { + return $this->getIncompleteGaps(); + } + + return $this->filterGapsBySelected($selected_gaps); + } + + private function filterGapsBySelected( + array $selected_gaps + ): array { + return array_filter( + $this->gaps, + fn(string $k): bool => in_array($k, $selected_gaps), + ARRAY_FILTER_USE_KEY + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php new file mode 100644 index 000000000000..773e17b34c8b --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/LongMenu.php @@ -0,0 +1,192 @@ +getAnswerInputId()->toString(); + $gaptemplate = new \ilTemplate( + 'tpl.il_as_qpl_longmenu_question_text_gap.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $gaptemplate->setVariable( + 'KEY', + $answer_input_id + ); + + $this->global_tpl->addOnLoadCode('il.test.player.longmenu.init(' + . "document.querySelector('input[name=\"answer[{$answer_input_id}]\"]'), " + . "{$gap->getMinAutocomplete()}, " + . json_encode( + array_values( + $gap->getAnswerOptions()->buildArrayForSelectInput( + $this->refinery->random()->dontShuffle() + ) + ) + ) . ')'); + return $gaptemplate->get(); + } + + #[\Override] + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { + $ff = $this->ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'upload_answer_options' => $ff->file( + new UploadAnswerOptionsGUI(), + $this->lng->txt('upload_answer_options'), + $this->lng->txt('upload_answer_options_info') + )->withAcceptedMimeTypes(self::ACCEPTED_MIME_TYPES), + 'min_autocomplete' => $ff->numeric( + $this->lng->txt('min_autocomplete') + )->withRequired(true) + ->withValue($gap->getMinAutocomplete() ?? self::DEFAULT_MIN_AUTOCOMPLETE), + 'options_awarding_points' => $ff->tag( + $this->lng->txt('answer_options'), + $gap->getAnswerOptions()->getTagsArrayFromAnswerOptions() + ) + ->withRequired(true) + ->withValue( + array_values( + array_map( + fn(AnswerOption $v): string => $v->getTextValue(), + $gap->getAnswerOptions()->getAnswerOptionsAwardingPoints() + ) + ) + ) + ]; + } + + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + $values = array_merge( + $vs['answer_options'], + $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) + ); + + return $values !== [] && array_filter( + $vs['options_awarding_points'], + fn(string $v): bool => !in_array($v, $values) + ) === []; + }, + $this->lng->txt('error') + ); + } + + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue(), + $answer_options->getAnswerOptionsAwardingPoints() + ); + } + + #[\Override] + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + #[\Override] + public function getBuildGapTransformation( + Gap $gap + ): Transformation { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap + ->withMinAutocomplete($vs['min_autocomplete']) + ->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + array_merge( + $vs['answer_options'], + $this->retrieveAnswerOptionsArrayFromUpload($vs['upload_answer_options']) + ) + )->withAnswerOptionsAwardingPoints($vs['options_awarding_points']) + ) + ); + } + + private function retrieveAnswerOptionsArrayFromUpload( + ?array $upload_value + ): array { + if ($upload_value === null + || ($decoded_value = base64_decode($upload_value[0] ?? '')) === '') { + return []; + } + + return array_filter( + mb_split('\R', $decoded_value) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php new file mode 100644 index 000000000000..540a6f45b2f5 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Numeric.php @@ -0,0 +1,161 @@ +setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { + $answer_option = $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0); + + $ff = $this->ui_factory->input()->field(); + return [ + 'lower_limit' => $ff->numeric($this->lng->txt('lower_limit')) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withRequired(true) + ->withValue($answer_option->getLowerLimit()), + 'upper_limit' => $ff->numeric($this->lng->txt('upper_limit')) + ->withStepSize($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ->withValue($answer_option->getUpperLimit()), + 'step_size' => $ff->numeric($this->lng->txt('step_size')) + ->withStepSize(0.000001) + ->withRequired(true) + ->withValue($gap->getStepSize() ?? self::DEFAULT_STEP_SIZE) + ]; + } + + #[\Override] + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + fn(array $vs): bool => $vs['upper_limit'] === null + || $vs['upper_limit'] >= $vs['lower_limit'] + && $vs['upper_limit'] >= $vs['step_size'], + $this->lng->txt('upper_limit_bigger_than_lower') + ); + } + + #[\Override] + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { + $inputs = $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + function (AnswerOption $v): string { + if ($v->getUpperLimit() === null) { + return sprintf( + $this->lng->txt('equal'), + $v->getLowerLimit() + ); + } + + return sprintf( + $this->lng->txt('between'), + $v->getLowerLimit(), + $v->getUpperLimit() + ); + } + ); + return array_map( + fn(NumericInput $v): NumericInput => $v->withRequired(true), + $inputs + ); + } + + #[\Override] + public function getEditPointsSectionConstraint(): ?Constraint + { + return null; + } + + #[\Override] + public function getBuildGapTransformation( + Gap $gap + ): Transformation { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptions([ + $gap->getAnswerOptions()->getAnswerOptionForPositionOrNew(0) + ->withLowerLimit($vs['lower_limit']) + ->withUpperLimit($vs['upper_limit']) + ]) + )->withStepSize($vs['step_size']) + ); + } + + #[\Override] + public function getCombinationsSelectValues( + Gap $gap + ): array { + $values = []; + foreach (InRange::cases() as $in_range) { + $values[$in_range->value] = $in_range->getLabel($this->lng); + } + return $values; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php new file mode 100644 index 000000000000..bef4eeadf8f4 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Select.php @@ -0,0 +1,152 @@ +getShuffleAnswerOptions() + ? $this->refinery->random()->shuffleArray(new GivenSeed(4)) + : $this->refinery->random()->dontShuffle(); + + foreach ($gap->getAnswerOptions()->buildArrayForSelectInput($shuffler) as $key => $answer_option) { + $gaptemplate->setCurrentBlock('select_gap_option'); + $gaptemplate->setVariable( + 'SELECT_GAP_VALUE', + $key + ); + $gaptemplate->setVariable( + 'SELECT_GAP_TEXT', + \ilLegacyFormElementsUtil::prepareFormOutput($answer_option) + ); + $gaptemplate->parseCurrentBlock(); + } + + $gaptemplate->setVariable( + 'PLEASE_SELECT', + $this->lng->txt('please_select') + ); + + $gaptemplate->setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { + $ff = $this->ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withRequired(true) + ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'shuffle_answer_options' => $ff->checkbox( + $this->lng->txt('shuffle_answers') + )->withValue($gap?->getShuffleAnswerOptions() ?? self::DEFAULT_SHUFFLE_ANSWER_OPTIONS) + ]; + } + + #[\Override] + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return null; + } + + #[\Override] + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue() + ); + } + + #[\Override] + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + #[\Override] + public function getBuildGapTransformation( + Gap $gap + ): Transformation { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $vs['answer_options'] + ) + )->withShuffleAnswerOptions($vs['shuffle_answer_options']) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php new file mode 100644 index 000000000000..851b45ffc422 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Text.php @@ -0,0 +1,151 @@ +getMaxChars(); + if ($gap_size > 0) { + $gaptemplate->setCurrentBlock('size_and_maxlength'); + $gaptemplate->setVariable('TEXT_GAP_SIZE', $gap_size); + $gaptemplate->parseCurrentBlock(); + } + $gaptemplate->setVariable( + 'GAP_COUNTER', + $gap->getAnswerInputId()->toString() + ); + + return $gaptemplate->get(); + } + + #[\Override] + public function getEditAnswerOptionsInputs( + Gap $gap + ): array { + $ff = $this->ui_factory->input()->field(); + return [ + 'answer_options' => $ff->tag( + $this->lng->txt('answer_options'), + [] + )->withRequired(true) + ->withValue($gap->getAnswerOptions()->getTagsArrayFromAnswerOptions()), + 'matching_method' => $ff->select( + $this->lng->txt('matching_method'), + TextMatchingOptions::buildOptionsList($this->lng) + )->withRequired(true) + ->withValue($gap->getTextMatchingMethod()?->value ?? self::DEFAULT_TECT_MATCHING_METHOD->value), + 'max_chars' => $ff->numeric( + $this->lng->txt('max_chars'), + )->withValue($gap->getMaxChars()) + ]; + } + + #[\Override] + public function getEditAnswerOptionsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + fn(array $vs): bool => array_filter( + $vs['answer_options'], + fn(string $v): bool => in_array($v, $vs['answer_options']) + ) !== [], + $this->lng->txt('answer_options_must_be_unique') + ); + } + + #[\Override] + public function getEditPointsInputs( + AnswerOptions $answer_options + ): array { + return $answer_options->getEditPointsInputs( + $this->ui_factory->input()->field(), + fn(AnswerOption $v): string => $v->getTextValue() + ); + } + + #[\Override] + public function getEditPointsSectionConstraint(): ?Constraint + { + return $this->refinery->custom()->constraint( + function (array $vs): bool { + foreach ($vs as $v) { + if ($v > 0.0) { + return true; + } + } + return false; + }, + $this->lng->txt('at_least_one_gap_positiv_points') + ); + } + + #[\Override] + public function getBuildGapTransformation( + Gap $gap + ): Transformation { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withMaxChars($vs['max_chars']) + ->withTextMatchingMethod( + TextMatchingOptions::tryFrom($vs['matching_method']) + ?? self::DEFAULT_TECT_MATCHING_METHOD + )->withAnswerOptions( + $gap->getAnswerOptions()->withAnswerOptionsFromTags( + $vs['answer_options'] + ) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php new file mode 100644 index 000000000000..e9344e48b384 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/Type.php @@ -0,0 +1,75 @@ +getAnswerOptions()->buildArrayForSelectInput( + $this->refinery->random()->dontShuffle() + ); + } + + public function getAddPointsTransformation( + Gap $gap + ): Transformation { + return $this->refinery->custom()->transformation( + fn(array $vs): Gap => $gap->withAnswerOptions( + $gap->getAnswerOptions() + ->withAnswerOptionsWithAddedPointsFromForm($this->refinery, $vs) + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php new file mode 100755 index 000000000000..94c0f296d8a9 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Gaps/class.UploadAnswerOptionsGUI.php @@ -0,0 +1,104 @@ + + * @ilCtrl_isCalledBy ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\UploadAnswerOptionsGUI: ilObjQuestionsGUI + */ +class UploadAnswerOptionsGUI extends AbstractCtrlAwareUploadHandler +{ + #[\Override] + protected function getUploadResult(): HandlerResult + { + $this->upload->process(); + + $result_array = $this->upload->getResults(); + $result = end($result_array); + + if (!($result instanceof UploadResult) || !$result->isOK()) { + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_FAILED, + '', + $result->getStatus()->getMessage() + ); + } + + $content = base64_encode(file_get_contents($result->getPath())); + unlink($result->getPath()); + + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $content, + 'file upload OK' + ); + } + + #[\Override] + protected function getRemoveResult( + string $identifier + ): HandlerResult { + return new BasicHandlerResult( + $this->getFileIdentifierParameterName(), + HandlerResult::STATUS_OK, + $identifier, + 'We just don\'t do anything here.' + ); + } + + #[\Override] + public function getInfoResult( + string $identifier + ): ?FileInfoResult { + return new BasicFileInfoResult( + $this->getFileIdentifierParameterName(), + $identifier, + 'unknown', + 0, + 'unknown' + ); + } + + #[\Override] + /** + * @return \ILIAS\FileUpload\Handler\BasicFileInfoResult[] + */ + public function getInfoForExistingFiles( + array $file_ids + ): array { + $info_results = []; + foreach ($file_ids as $identifier) { + $info_results[] = $this->getInfoResult($identifier); + } + + return $info_results; + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php new file mode 100644 index 000000000000..f3cb647494bb --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Properties/Properties.php @@ -0,0 +1,461 @@ + $gaps + */ + public function __construct( + private readonly Uuid $answer_form_id, + private readonly Uuid $question_id, + private readonly Definition $definition, + private Text $cloze_text, + private readonly string $legacy_cloze_text, + private ScoringIdentical $scoring_identical, + private Gaps $gaps, + private Combinations $combinations + ) { + } + + #[\Override] + public function getAnswerFormId(): Uuid + { + return $this->answer_form_id; + } + + #[\Override] + public function getQuestionId(): Uuid + { + return $this->question_id; + } + + #[\Override] + public function getDefinition(): ?Definition + { + return $this->definition; + } + + #[\Override] + public function getTypeGenericProperties(): TypeGenericProperties + { + return new TypeGenericProperties( + $this->answer_form_id, + $this->question_id, + $this->definition, + null, + null, + null, + $this->cloze_text->getRawRepresentation(), + $this->legacy_cloze_text + ); + } + + public function getClozeText(): Text + { + return $this->cloze_text; + } + + public function withClozeText( + Text $cloze_text + ): self { + $clone = clone $this; + $clone->cloze_text = $cloze_text; + return $clone; + } + + public function getLegacyClozeText(): string + { + return $this->legacy_cloze_text; + } + + public function getClozeTextForPresentation(): string + { + return $this->cloze_text->getRawRepresentation() === '' + ? \ilRTE::_replaceMediaObjectImageSrc($this->legacy_cloze_text, 0) + : $this->cloze_text->getRenderedMarkdownForParticipantPresentation(); + } + + public function getScoringOfIdenticalResponses(): ScoringIdentical + { + return $this->scoring_identical; + } + + public function withScoringOfIdenticalResponses( + ScoringIdentical $scoring_identical + ): self { + $clone = clone $this; + $clone->scoring_identical = $scoring_identical; + return $clone; + } + + public function getCombinations(): Combinations + { + return $this->combinations; + } + + public function withCombinations( + Combinations $combinations + ): self { + $clone = clone $this; + $clone->combinations = $combinations; + $clone->updated_combinations = true; + return $clone; + } + + public function getGaps(): Gaps + { + return $this->gaps; + } + + public function withGaps( + Gaps $gaps + ): self { + $clone = clone $this; + $clone->gaps = $gaps; + return $clone; + } + + #[\Override] + public function getBasicPropertiesForListing( + Language $lng + ): array { + return [ + $lng->txt('cloze_text') => $this->cloze_text + ->getRenderedMarkdownForEditingPresentation( + $this->gaps, + $this->getLegacyClozeText() + ), + $lng->txt('score_identical') => $this->scoring_identical + ->getTranslatedOptionName($lng), + $lng->txt('gap_combinations') => $this->combinations->areCombinationsEnabled() + ? $lng->txt('enabled') + : $lng->txt('disabled') + ]; + } + + #[\Override] + public function getOverviewTable( + TableFactory $table_factory, + Language $lng, + ServerRequestInterface $request, + Environment $environment + ): DataTable { + return new OverviewTable( + $table_factory, + $lng, + $request, + $environment + )->getTable(); + } + + public function buildBasicEditingInputs( + Language $lng, + FieldFactory $ff, + Refinery $refinery, + Factory $properties_factory, + ClozeTextFactory $cloze_text_factory, + bool $add_legacy_cloze_text_to_input + ): Section { + $cloze_text_input = $this->cloze_text->getInput( + $lng, + $ff, + $cloze_text_factory, + $this->legacy_cloze_text === '' + || $this->legacy_cloze_text !== '' + && $this->cloze_text->getRawRepresentation() !== '' + ); + + if ($add_legacy_cloze_text_to_input) { + $cloze_text_input = $cloze_text_input->withValue( + strip_tags($this->legacy_cloze_text) + ); + } + + return $ff->section( + [ + self::KEY_CLOZE_TEXT => $cloze_text_input, + self::KEY_SCORING_IDENTICAL => ScoringIdentical::buildInput( + $lng, + $ff, + $refinery, + $this->scoring_identical + )->withValue($this->getScoringOfIdenticalResponses()->value), + self::KEY_ENABLE_COMBINATIONS => $ff->checkbox($lng->txt('cloze_enable_combinations')) + ->withValue($this->combinations->areCombinationsEnabled()) + ], + $lng->txt('set_basic_properties') + )->withAdditionalTransformation( + $refinery->custom()->transformation( + fn(array $vs): self => $properties_factory->fromBasicEditingForm( + $this, + $vs[self::KEY_CLOZE_TEXT], + $vs[self::KEY_SCORING_IDENTICAL], + $vs[self::KEY_ENABLE_COMBINATIONS] + ) + ) + ); + } + + public function toCarry(): string + { + return json_encode([ + self::KEY_CLOZE_TEXT => $this->cloze_text->getRawRepresentation(), + self::KEY_GAPS => $this->gaps->toCarry(), + self::KEY_SCORING_IDENTICAL => $this->scoring_identical->value, + self::KEY_ENABLE_COMBINATIONS => $this->combinations + ->areCombinationsEnabled() ? 1 : 0 + ]); + } + + public function withValuesFromCarry( + ClozeTextFactory $cloze_text_factory, + array $carry + ): self { + $clone = clone $this; + + $clone->cloze_text = $cloze_text_factory->buildFromTextString( + $carry[self::KEY_CLOZE_TEXT] + ); + $clone->gaps = $this->getGaps()->withValuesFromCarry( + $carry[self::KEY_GAPS] + ); + $clone->scoring_identical = ScoringIdentical::tryFrom( + $carry[self::KEY_SCORING_IDENTICAL] + ); + $clone->combinations = $this->combinations->withCombinationsEnabled( + $carry[self::KEY_ENABLE_COMBINATIONS] === 1 + ); + return $clone; + } + + #[\Override] + public function toStorage( + Manipulate $manipulate + ): Manipulate { + $persistence = $manipulate->getPersistenceForDefinitionClass( + $this->definition::class + ); + + $table_name_builder = $manipulate->getTableNameBuilder( + $this->definition::class + ); + + $answer_form_statement = $manipulate->getManipulationType() === ManipulationType::Create + ? $this->buildInsertAnswerFormStatement( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) : $this->buildUpdateAnswerFormStatement( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ); + + return $this->gaps->toStorage( + $this->addReplaceCombinationsStatements( + $manipulate, + $persistence, + $table_name_builder + )->withAdditionalStatement( + $answer_form_statement + ), + $persistence, + $table_name_builder + ); + } + + #[\Override] + public function toDelete( + Manipulate $manipulate + ): Manipulate { + $persistence = $manipulate->getPersistenceForDefinitionClass( + $this->definition::class + ); + + $table_name_builder = $manipulate->getTableNameBuilder( + $this->definition::class + ); + + return $this->gaps->toDelete( + $manipulate->withAdditionalStatement( + $this->buildDeleteAnswerFormStatement( + $persistence, + $manipulate->getPersistenceFactory(), + $table_name_builder + ) + ), + $persistence, + $table_name_builder + ); + } + + private function buildInsertAnswerFormStatement( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Insert { + $table_definition = TableTypes::TypeSpecificAnswerForms; + return $persistence_factory->insert( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->scoring_identical->value + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->combinations->areCombinationsEnabled() ? 1 : 0 + ) + ] + ); + } + + private function buildUpdateAnswerFormStatement( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Update { + $table_definition = TableTypes::TypeSpecificAnswerForms; + return $persistence_factory->update( + $persistence->getColumns( + $persistence_factory, + $table_name_builder, + $table_definition, + '', + ['answer_form_id'] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->scoring_identical->value + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->combinations->areCombinationsEnabled() ? 1 : 0 + ) + ], + [ + $persistence_factory->where( + $persistence->getIdColumn( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] + ); + } + + private function addReplaceCombinationsStatements( + Manipulate $manipulate, + Persistence $persistence, + TableNameBuilder $table_name_builder + ): Manipulate { + if (!$this->combinations->areCombinationsEnabled() + || !$this->updated_combinations) { + return $manipulate; + } + + return $this->combinations->toStorage( + $manipulate, + $persistence, + $table_name_builder + ); + } + + private function buildDeleteAnswerFormStatement( + Persistence $persistence, + PersistenceFactory $persistence_factory, + TableNameBuilder $table_name_builder + ): Delete { + $table_definition = TableTypes::TypeSpecificAnswerForms; + + return $persistence_factory->delete( + $table_definition->getTable( + $persistence_factory, + $table_name_builder + ), + [ + $persistence_factory->where( + $persistence->getForeignKeyColumn( + $persistence_factory, + $table_name_builder, + $table_definition + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->answer_form_id->toString() + ) + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php new file mode 100644 index 000000000000..ffa5b42c4554 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Edit.php @@ -0,0 +1,344 @@ +getStep(); + + return match($step) { + '' => $this->startEditing($environment), + self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + $environment + ), + default => $this->forwardCmdToEditGaps( + $environment, + $step + ) + }; + } + + #[\Override] + public function edit( + Environment $environment + ): EditOverview|EditForm|Properties { + $step = $environment->getStep(); + + $combinations = $environment->getAnswerFormProperties()->getCombinations(); + if ($combinations->areCombinationsEnabled()) { + $combinations->getEditView( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http + )->addCombinationsSubTab($environment); + } + + if ($step === '') { + return $environment->getPresentationFactory()->getEditOverview( + $environment, + $environment->withStepParameter(self::STEP_EDIT_BASIC_PROPERTIES) + ->getUrlBuilder() + ); + } + + $environment->setEditAnswerFormBackTarget(); + + return match ($step) { + self::STEP_EDIT_BASIC_PROPERTIES => $this->startEditing($environment), + self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES => + $this->addLegacyTextToBasicProperties($environment), + self::STEP_CONFIRMED_GAP_REMOVAL, + self::STEP_PROCESS_BASIC_PROPERTIES => $this->processBasicEditingForm( + $environment->withPreservedTableRowIdsParameter() + ), + default => $this->forwardCmdToEditGaps( + $environment->withPreservedTableRowIdsParameter(), + $step + ) + }; + } + + #[\Override] + public function other( + Environment $environment + ): Async|Renderable|Properties { + return $environment + ->getAnswerFormProperties() + ->getCombinations()->getEditView( + $this->ui_factory, + $this->toolbar, + $this->refinery, + $this->lng, + $this->http + )->show($environment); + } + + #[\Override] + public function getFinishEditingUrl( + Environment $environment + ): URLBuilder { + return $environment->getUrlBuilder(); + } + + private function startEditing( + Environment $environment + ): EditForm { + $input_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false + ); + $input_builder->resetCarry(); + + return $this->buildBasicEditingForm( + $environment, + $input_builder, + false + ); + } + + private function forwardCmdToEditGaps( + Environment $environment, + string $step + ): EditForm|Properties { + $processed_form = $this->edit_gaps->call($environment, $step); + if (is_string($processed_form)) { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false, + $processed_form + ); + + $inputs_builder->persistCarry(); + + return $this->buildBasicEditingForm( + $environment, + $inputs_builder, + false + ); + } + + return $processed_form; + } + + private function buildBasicEditingForm( + Environment $environment, + InputsBuilderSession $inputs_builder, + bool $add_legacy_cloze_text_to_input + ): EditForm { + $editing_form = $this->buildEditFormForBasicInputs( + $environment, + $inputs_builder + ); + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ + $properties = $environment->getAnswerFormProperties(); + if (!$add_legacy_cloze_text_to_input + && $properties->getLegacyClozeText() !== '' + && $properties->getClozeText()->getRawRepresentation() === '') { + return $editing_form->withInsertLegacyTextsButton( + $environment->withStepParameter( + self::STEP_ADD_LEGACY_TEXT_BASIC_PROPERTIES + )->getUrlBuilder() + ); + } + + return $editing_form; + } + + private function addLegacyTextToBasicProperties( + Environment $environment + ): EditForm { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + true + ); + + $inputs_builder->persistCarry(); + + return $this->buildBasicEditingForm( + $environment, + $inputs_builder, + true + ); + } + + private function processBasicEditingForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder = $this->buildInputsBuilderForBasicInputs( + $environment, + false, + ); + + $form = $this->buildBasicEditingForm( + $environment, + $inputs_builder, + false + )->withRequest($this->http->request()); + + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $data */ + $data = $form->getData(); + if ($data === null) { + $inputs_builder->persistCarry(); + return $form; + } + + $new_gaps = $data->getGaps(); + $old_gaps = $environment->getAnswerFormProperties()->getGaps(); + + if ($environment->getStep() !== self::STEP_CONFIRMED_GAP_REMOVAL) { + $removed_gaps = $new_gaps->getRemovedGaps($old_gaps); + if ($removed_gaps !== []) { + return $form->withConfirmation( + $this->buildRemovedGapsConfirmation( + $environment, + $removed_gaps + ) + ); + } + } + + if ($new_gaps->getAddedGaps($old_gaps) === []) { + return $data; + } + + return $this->edit_gaps->call( + $environment->withAnswerFormProperties( + $data->withGaps( + $data->getGaps()->withMarkedIncompleteGaps() + ) + ) + ); + } + + private function buildEditFormForBasicInputs( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $environment + ->withStepParameter(self::STEP_PROCESS_BASIC_PROPERTIES) + ->getUrlBuilder(), + null, + false + ); + } + + private function buildInputsBuilderForBasicInputs( + Environment $environment, + bool $add_legacy_cloze_text_to_input, + ?string $carry = null + ): InputsBuilderSession { + $inputs_builder = $environment->getPresentationFactory() + ->getSessionBasedInputsBuilder( + $environment->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + fn(?string $carry): Section => $this->properties_factory + ->fromCarry( + $environment->getAnswerFormProperties(), + $carry + )->buildBasicEditingInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->refinery, + $this->properties_factory, + $this->cloze_text_factory, + $add_legacy_cloze_text_to_input + ) + ) + ); + + if ($carry === null) { + return $inputs_builder; + } + + return $inputs_builder->withCarry($carry); + } + + /** + * @param array<\ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Gaps\Gap> $removed_gaps + */ + private function buildRemovedGapsConfirmation( + Environment $environment, + array $removed_gaps + ): InterruptiveModal { + return $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('confirm_remove_gaps'), + $environment->withStepParameter( + self::STEP_CONFIRMED_GAP_REMOVAL + )->getUrlBuilder()->buildURI()->__toString() + )->withAffectedItems( + array_map( + fn(Gap $v): InterruptiveItem => $this->ui_factory->modal() + ->interruptiveItem()->standard( + $v->getAnswerInputId()->toString(), + $v->buildShortenedGapName() + ), + $removed_gaps + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php new file mode 100644 index 000000000000..0f27a7ef3d55 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/EditGaps.php @@ -0,0 +1,550 @@ +step = $step_array[0]; + $this->start_step = $this->determineStartStepFromStep( + $step_array[1] ?? null + ); + + return match ($this->step) { + self::STEP_SET_GAP_TYPES, + self::STEP_JUMP_TO_SET_GAP_TYPES + => $this->buildGapTypesFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + => $this->backToEditBasicProperties( + $environment + ), + self::STEP_BACK_TO_SET_GAP_TYPES + => $this->backToGapTypesForm( + $environment + ), + self::STEP_SET_ANSWER_OPTIONS + => $this->forwardToAnswerOptionsForm( + $environment + ), + self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + => $this->buildAnswerOptionsFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_BACK_TO_SET_ANSWER_OPTIONS + => $this->backToSetAnswerOptionsForm( + $environment + ), + self::STEP_ASSIGN_POINTS + => $this->forwardToAssignPointsForm( + $environment + ), + self::STEP_JUMP_TO_ASSIGN_POINTS + => $this->buildAssignPointsFormWithCarry( + $environment, + $environment->getAnswerFormProperties() + ), + self::STEP_SAVE + => $this->processAssignPointsForm( + $environment + ) + }; + } + + private function backToEditBasicProperties( + Environment $environment + ): EditForm|string { + $processed_form = $this->processGapTypesForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $processed_form->toCarry(); + } + + private function backToGapTypesForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAnswerOptionsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildGapTypesFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildGapTypesFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder = $this->buildInputsBuilderForTypesForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder->persistCarry(); + + return $this->buildGapTypesForm( + $environment, + $inputs_builder + ); + } + + private function buildGapTypesForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + /** @var \ILIAS\Questions\AnswerFormTypes\Cloze\Properties\Properties $properties */ + $properties = $environment->getAnswerFormProperties(); + + $inputs_builder->persistCarry(); + + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_SET_ANSWER_OPTIONS + ), + $this->step === self::STEP_JUMP_TO_SET_GAP_TYPES + || $this->step === $this->start_step + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_EDIT_BASIC_PROPERTIES + ), + false + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processGapTypesForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_types = $this->buildInputsBuilderForTypesForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $properties = $inputs_builder_for_types->retrieveCarry( + $this->buildRetrievePropertiesTransformation($environment) + ); + + $form = $this->buildGapTypesForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder_for_types + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_types->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForTypesForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps()->buildGapsTypeInputs( + $this->lng, + $this->ui_factory->input()->field(), + $this->gap_factory->getAvailableGapTypesOptionsArray($this->lng), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function forwardToAnswerOptionsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processGapTypesForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAnswerOptionsFormWithCarry( + $environment, + $processed_form + ); + } + + private function backToSetAnswerOptionsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAssignPointsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAnswerOptionsFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildAnswerOptionsFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder = $this->buildInputsBuilderForAnswerOptionsForm( + $environment->getPresentationFactory(), + $properties, + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder->persistCarry(); + + return $this->buildAnswerOptionsForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder + ); + } + + private function buildAnswerOptionsForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_ASSIGN_POINTS + ), + $this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS + || $this->step === $this->start_step + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_SET_GAP_TYPES + ), + false + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processAnswerOptionsForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_options = $this->buildInputsBuilderForAnswerOptionsForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $form = $this->buildAnswerOptionsForm( + $environment, + $inputs_builder_for_options + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_options->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForAnswerOptionsForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps() + ->buildAnswerOptionsInputs( + $this->lng, + $this->ui_factory->input()->field(), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function forwardToAssignPointsForm( + Environment $environment + ): EditForm { + $processed_form = $this->processAnswerOptionsForm($environment); + if ($processed_form instanceof EditForm) { + return $processed_form; + } + + return $this->buildAssignPointsFormWithCarry( + $environment, + $processed_form + ); + } + + private function buildAssignPointsFormWithCarry( + Environment $environment, + Properties $properties + ): EditForm { + $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( + $environment->getPresentationFactory(), + $properties, + $environment->isInCreationContext(), + $environment->getTableRowIds() + )->withCarry( + $properties->toCarry() + ); + + $inputs_builder_for_points->persistCarry(); + + return $this->buildAssignPointsForm( + $environment->withAnswerFormProperties($properties), + $inputs_builder_for_points + ); + } + + private function buildAssignPointsForm( + Environment $environment, + InputsBuilderSession $inputs_builder + ): EditForm { + $properties = $environment->getAnswerFormProperties(); + return $environment->getPresentationFactory()->getEditForm( + $inputs_builder, + $this->buildPostTarget( + $environment, + self::STEP_SAVE + ), + $this->step === self::STEP_JUMP_TO_ASSIGN_POINTS + ? null + : $this->buildPostTarget( + $environment, + self::STEP_BACK_TO_SET_ANSWER_OPTIONS + ), + true + )->withContentBeforeForm( + $properties->getClozeText()->buildPanelForEditing( + $this->ui_factory, + $this->lng, + $properties->getGaps(), + $properties->getLegacyClozeText() + ) + ); + } + + private function processAssignPointsForm( + Environment $environment + ): EditForm|Properties { + $inputs_builder_for_points = $this->buildInputsBuilderForPointsForm( + $environment->getPresentationFactory(), + $environment->getAnswerFormProperties(), + $environment->isInCreationContext(), + $environment->getTableRowIds() + ); + + $form = $this->buildAssignPointsForm( + $environment, + $inputs_builder_for_points + )->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + $inputs_builder_for_points->persistCarry(); + return $form; + } + + return $data; + } + + private function buildInputsBuilderForPointsForm( + PresentationFactory $presentation_factory, + Properties $properties, + bool $is_in_creation_context, + array $table_row_ids + ): InputsBuilderSession { + return $presentation_factory->getSessionBasedInputsBuilder( + $properties->getAnswerFormId()->toString(), + $this->refinery->custom()->transformation( + function (?string $carry) use ( + $properties, + $is_in_creation_context, + $table_row_ids + ): Section { + $properties_from_carry = $this->properties_factory + ->fromCarry( + $properties, + $carry + ); + return $properties_from_carry->getGaps() + ->buildPointInputs( + $this->lng, + $this->ui_factory->input()->field(), + $properties_from_carry, + $is_in_creation_context, + $table_row_ids + ); + } + ) + ); + } + + private function buildPostTarget( + Environment $environment, + string $next_step + ): URLBuilder { + if ($this->start_step !== null) { + $next_step = "{$next_step}_{$this->start_step}"; + } + + return $environment->withStepParameter($next_step)->getUrlBuilder(); + } + + private function determineStartStepFromStep( + ?string $start_step_from_get + ): ?string { + if ($start_step_from_get !== null) { + return $start_step_from_get; + } + + if ($this->step === self::STEP_JUMP_TO_SET_GAP_TYPES) { + return self::STEP_BACK_TO_SET_GAP_TYPES; + } + + if ($this->step === self::STEP_JUMP_TO_SET_ANSWER_OPTIONS) { + return self::STEP_BACK_TO_SET_ANSWER_OPTIONS; + } + + return null; + } + + private function buildRetrievePropertiesTransformation( + Environment $environment + ): CustomTransformation { + return $this->refinery->custom()->transformation( + fn(?string $carry): Properties => $this->properties_factory + ->fromCarry( + $environment->getAnswerFormProperties(), + $carry + ) + ); + } +} diff --git a/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php new file mode 100644 index 000000000000..77579cd67a73 --- /dev/null +++ b/components/ILIAS/Questions/src/AnswerFormTypes/Cloze/Views/Participant.php @@ -0,0 +1,54 @@ +global_tpl->addJavaScript('assets/js/ParticipantViewLongMenu.js'); + return $this->mustache_engine->render( + $properties->getClozeTextForPresentation(), + $properties->getGaps()->getPlaceholderArrayForParticipantView() + ); + } +} diff --git a/components/ILIAS/Questions/src/Collector.php b/components/ILIAS/Questions/src/Collector.php new file mode 100644 index 000000000000..d3ecf6764511 --- /dev/null +++ b/components/ILIAS/Questions/src/Collector.php @@ -0,0 +1,63 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function getQuestionsForId( + Uuid $id + ): ?Question { + return $this->repository->getForQuestionId($id); + } + + /** + * Use with Care: This is going to be freakishly expensive, if you ask + * for a lot of questions as the query will contain a huge amount of joins! + * + * @param list<\ILIAS\Data\Uuid> $ids + * @return \Generator + */ + public function getQuestionsForIds( + array $ids + ): \Generator { + yield from $this->repository->getForQuestionIds($ids); + } +} diff --git a/components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php b/components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php new file mode 100644 index 000000000000..c4c76919e765 --- /dev/null +++ b/components/ILIAS/Questions/src/Definitions/TextMatchingOptions.php @@ -0,0 +1,56 @@ + $lng->txt("cloze_textgap_{$this->value}"), + default => sprintf($lng->txt('cloze_textgap_levenshtein_of'), $this->value) + }; + } + + public static function buildOptionsList( + Language $lng + ): array { + return array_reduce( + self::cases(), + function (array $c, self $v) use ($lng): array { + $c[$v->value] = $v->getLabel($lng); + return $c; + }, + [] + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Column.php b/components/ILIAS/Questions/src/Persistence/Column.php new file mode 100644 index 000000000000..63a1aa110f31 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Column.php @@ -0,0 +1,50 @@ +table->getName(); + } + + public function getColumnAlias(): string + { + return "{$this->table->getName()}_{$this->identifier}"; + } + + public function getColumnString(): string + { + return "{$this->table->getName()}.{$this->identifier}"; + } + + public function getAliasedColumnString(): string + { + return "{$this->table->getName()}.{$this->identifier} {$this->getColumnAlias()}"; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/CoreTables.php b/components/ILIAS/Questions/src/Persistence/CoreTables.php new file mode 100644 index 000000000000..d181857711ce --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/CoreTables.php @@ -0,0 +1,143 @@ +table($this); + } + + public function getColumns( + Factory $persistence_factory, + array $columns_to_skip = [] + ): array { + $table = $this->getTable($persistence_factory); + $column_identifiers = match($this) { + self::Questions => self::QUESTION_TABLE_COLUMNS, + self::AnswerForms => self::ANSWER_FORM_TABLE_COLUMNS, + self::Linking => self::LINKING_TABLE_COLUMNS, + self::MigrationsTable => self::MIGRATIONS_TABLE_COLUMNS + }; + return array_map( + fn(string $v): Column => $persistence_factory->column($table, $v), + array_values( + array_filter( + $column_identifiers, + fn(string $v) => !in_array($v, $columns_to_skip) + ) + ) + ); + } + + public function getIdColumn( + Factory $persistence_factory + ): Column { + return match($this) { + self::Questions => $persistence_factory->column( + $this->getTable($persistence_factory), + self::QUESTION_TABLE_ID_COLUMN + ), + self::AnswerForms => $persistence_factory->column( + $this->getTable($persistence_factory), + self::ANSWER_FORM_TABLE_ID_COLUMN + ), + self::Linking => $persistence_factory->column( + $this->getTable($persistence_factory), + self::LINKING_TABLE_ID_COLUMN + ), + self::MigrationsTable => $persistence_factory->column( + $this->getTable($persistence_factory), + self::MIGRATIONS_TABLE_ID_COLUMN + ) + }; + } + + public function getForeignKeyColumn( + Factory $persistence_factory + ): ?Column { + return match($this) { + self::AnswerForms => $persistence_factory->column( + $this->getTable($persistence_factory), + self::ANSWER_FORM_TABLE_FOREIGN_KEY_COLUMN + ), + self::Linking => $persistence_factory->column( + $this->getTable($persistence_factory), + self::LINKING_TABLE_FOREIGN_KEY_COLUMN + ), + self::MigrationsTable => $persistence_factory->column( + $this->getTable($persistence_factory), + self::MIGRATIONS_TABLE_FOREIGN_KEY_COLUMN + ), + default => null + }; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Delete.php b/components/ILIAS/Questions/src/Persistence/Delete.php new file mode 100644 index 000000000000..7abec2bc259e --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Delete.php @@ -0,0 +1,73 @@ + $where + */ + public function __construct( + private readonly Table $table, + private readonly array $where + ) { + } + + public function getTableToLock(): string + { + return $this->table->getName(); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { + return "DELETE FROM {$this->table->getName()}" . PHP_EOL + . $this->buildWhereString($db); + } + + private function buildWhereString( + \ilDBInterface $db + ): string { + $values = []; + return sprintf( + array_reduce( + $this->where, + function (?string $c, Where $v) use ($db, &$values): string { + $quoted_value = $v->getRight()->getQuotedValue($db); + if (is_array($quoted_value)) { + $values = array_merge( + $values, + array_values($quoted_value) + ); + } else { + $values[] = $quoted_value; + } + + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()->value} {$v->toSql()}" . PHP_EOL; + } + ) ?? '', + ...$values + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Factory.php b/components/ILIAS/Questions/src/Persistence/Factory.php new file mode 100644 index 000000000000..6ba8487d645f --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Factory.php @@ -0,0 +1,125 @@ + $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function insert( + array $columns, + array $values + ): Insert { + return new Insert($columns, $values); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function replace( + array $columns, + array $values + ): Replace { + return new Replace($columns, $values); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ + public function update( + array $columns, + array $values, + array $where + ): Update { + return new Update($columns, $values, $where); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ + public function delete( + Table $table, + array $where + ): Delete { + return new Delete($table, $where); + } + + public function table( + CoreTables|TableTypes $table_definition, + ?TableNameBuilder $table_name_builder = null, + string $table_identifier = '' + ): Table { + return new Table($table_definition, $table_name_builder, $table_identifier); + } + + public function column( + Table $table, + string $identifier + ): Column { + return new Column($table, $identifier); + } + + /** + * @param array<\ILIAS\Questions\Persistence\Column> $columns + */ + public function select( + array $columns + ): Select { + return new Select($columns); + } + + public function join( + Column $left, + Column $right, + JoinType $type = JoinType::Inner + ): Join { + return new Join($left, $right, $type); + } + + public function where( + Column $left, + Value $right, + Operator $comparison = Operator::Equal, + Junctor $junctor = Junctor::Conjunction, + bool $negate = false + ): Where { + return new Where($left, $right, $comparison, $junctor, $negate); + } + + public function order( + Column $column, + OrderDirection $direction = OrderDirection::Asc + ): Order { + return new Order($column, $direction); + } + + public function value( + string $type, + null|string|int|float|array $value + ): Value { + return new Value($type, $value); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Insert.php b/components/ILIAS/Questions/src/Persistence/Insert.php new file mode 100644 index 000000000000..bccf22ee793d --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Insert.php @@ -0,0 +1,114 @@ + $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function __construct( + protected readonly array $columns, + array $values + ) { + if ($columns === [] || count($columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be at least one Column and the same amount of Values as there are Columns." + ); + } + + $table_name = $columns[0]->getTableName(); + foreach ($columns as $column) { + if ($column->getTableName() !== $table_name) { + throw new \InvalidArgumentException( + "All Columns MUST belong to the same Table." + ); + } + } + + $this->value_sets[] = $values; + } + + /** + * @param array<\ILIAS\Questions\Persistence\Value> $values + */ + public function withAdditionalValues( + array $values + ): self { + if (count($values) !== count($this->columns)) { + throw new \InvalidArgumentException( + "There MUST be the same amount of Values as there are Columns." + ); + } + + $clone = clone $this; + $clone->value_sets[] = $values; + return $clone; + } + + public function getTableToLock(): string + { + return $this->columns[0]->getTableName(); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { + return "INSERT INTO {$this->columns[0]->getTableName()}" . PHP_EOL + . $this->buildColumnsString() . PHP_EOL + . $this->buildValuesString($db); + } + + protected function buildColumnsString(): string + { + return '(' + . implode( + ', ', + array_map( + fn(Column $v): string => $v->getColumnString(), + $this->columns + ) + ) . ')'; + } + + protected function buildValuesString( + \ilDBInterface $db + ): string { + $return = []; + foreach ($this->value_sets as $values) { + $return[] = '(' . implode( + ', ', + array_map( + fn(Value $v): string => $v->getQuotedValue($db), + $values + ) + ) . ')'; + } + + return 'VALUES ' . implode( + ',' . PHP_EOL, + $return + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Join.php b/components/ILIAS/Questions/src/Persistence/Join.php new file mode 100644 index 000000000000..6ef4e4d941fd --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Join.php @@ -0,0 +1,52 @@ +type->value} JOIN {$this->right->getTableName()} " + . "ON {$this->left->getColumnString()} = {$this->right->getColumnString()}"; + } + + public function getLeft(): Column + { + return $this->left; + } + + public function getRight(): Column + { + return $this->right; + } + + public function getType(): JoinType + { + return $this->type; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/JoinType.php b/components/ILIAS/Questions/src/Persistence/JoinType.php new file mode 100644 index 000000000000..3a7d11732407 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/JoinType.php @@ -0,0 +1,27 @@ +persistence_factory; + } + + public function getManipulationType(): ManipulationType + { + return $this->type; + } + + public function getPersistenceForDefinitionClass( + string $definition_class + ): Persistence { + return $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence(); + } + + public function getTableNameBuilder( + string $definition_class + ): TableNameBuilder { + return new TableNameBuilder( + $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence() + ->getTableNameSpace() + ); + } + + public function withAdditionalStatement( + Insert|Update|Replace|Delete $statement + ): self { + $clone = clone $this; + $clone->statements[] = $statement; + return $clone; + } + + public function run(): void + { + if ($this->statements === []) { + return; + } + + $atom_query = $this->db->buildAtomQuery(); + + $manipulates = []; + $locked_tables = []; + foreach ($this->statements as $statement) { + $table_to_lock = $statement->getTableToLock(); + if (!in_array($table_to_lock, $locked_tables)) { + $atom_query->addTableLock($table_to_lock); + $locked_tables[] = $table_to_lock; + } + $manipulates[] = $statement->toManipulateString($this->db); + } + $atom_query->addQueryCallable( + function (\ilDBInterface $db) use ($manipulates): void { + foreach ($manipulates as $manipulate) { + $db->manipulate($manipulate); + } + } + ); + $atom_query->run(); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/ManipulationType.php b/components/ILIAS/Questions/src/Persistence/ManipulationType.php new file mode 100644 index 000000000000..069a58b51d5f --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/ManipulationType.php @@ -0,0 +1,28 @@ +'; + case Greater = '>'; + case Less = '<'; + case GreaterOrEqual = '>='; + case LessOrEqual = '<='; + case In = 'IN'; + case Like = 'LIKE'; + case Between = 'BETWEEN'; + + public function toSql( + Column $left, + int $nr_of_values + ): string { + $placeholders = '%s'; + for ($i = 1; $i < $nr_of_values; $i++) { + $placeholders .= ', %s'; + } + + return match($this) { + self::In => "{$left->getColumnString()} {$this->value} ({$placeholders})", + self::Between => "{$left->getColumnString()} {$this->value} %s AND %s", + default => "{$left->getColumnString()} {$this->value} %s" + }; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Order.php b/components/ILIAS/Questions/src/Persistence/Order.php new file mode 100644 index 000000000000..b7d73975448e --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Order.php @@ -0,0 +1,35 @@ +column->getColumnString()} {$this->direction->value}"; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/OrderDirection.php b/components/ILIAS/Questions/src/Persistence/OrderDirection.php new file mode 100644 index 000000000000..072b3a6acd97 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/OrderDirection.php @@ -0,0 +1,27 @@ +getIdColumn( + $this->persistence_factory + ); + + $this->select[] = $this->persistence_factory->select( + $questions_linking_table_definition->getColumns( + $this->persistence_factory + ) + ); + + $this->select[] = $this->persistence_factory->select( + $questions_table_definition->getColumns( + $this->persistence_factory + ) + ); + + $this->select[] = $this->persistence_factory->select( + $answer_form_table_definition->getColumns( + $this->persistence_factory + ) + ); + + $this->joins[] = $this->persistence_factory->join( + $questions_linking_table_definition->getIdColumn( + $this->persistence_factory + ), + $questions_table_definition->getIdColumn( + $this->persistence_factory + ), + JoinType::Inner + ); + + $this->joins[] = $this->persistence_factory->join( + $questions_id_column, + $answer_form_table_definition->getForeignKeyColumn( + $this->persistence_factory + ), + JoinType::Left + ); + + $this->order[] = $this->persistence_factory->order( + $questions_id_column + ); + + $this->order[] = $this->persistence_factory->order( + $answer_form_table_definition->getIdColumn( + $this->persistence_factory + ) + ); + } + + public function getPersistenceFactory(): Factory + { + return $this->persistence_factory; + } + + public function getPersistenceForDefinitionClass( + string $definition_class + ): Persistence { + return $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence(); + } + + public function getTableNameBuilder( + string $definition_class + ): TableNameBuilder { + return new TableNameBuilder( + $this->answer_form_factory + ->getDefinitionForClass($definition_class) + ->getPersistence() + ->getTableNameSpace() + ); + } + + public function getRefinery(): Refinery + { + return $this->refinery; + } + + public function withAdditionalSelect( + Select $select + ): self { + $clone = clone $this; + $clone->select[] = $select; + return $clone; + } + + public function withAdditionalJoin( + Join $join + ): self { + $clone = clone $this; + $clone->joins[] = $join; + return $clone; + } + + public function withAdditionalWhere( + Where $where + ): self { + $clone = clone $this; + $clone->where[] = $where; + return $clone; + } + + public function withAdditionalOrder( + Order $order + ): self { + $clone = clone $this; + $clone->order[] = $order; + return $clone; + } + + public function withRange( + Range $range + ): self { + $clone = clone $this; + $clone->range = $range; + return $clone; + } + + public function loadNextRecord(): \Generator + { + $alias = CoreTables::Questions->getIdColumn( + $this->persistence_factory + )->getColumnAlias(); + + $result = $this->toSql(); + + $this->current_record = [$this->db->fetchAssoc($result)]; + if ($this->current_record[0] === null) { + return null; + } + + while (($db_record = $this->db->fetchAssoc($result)) !== null) { + if ($db_record[$alias] === $this->current_record[0][$alias]) { + $this->current_record[] = $db_record; + continue; + } + yield $this; + $this->current_record = [$db_record]; + } + yield $this; + } + + public function retrieveCurrentRecord( + Table $table, + Transformation $transformation + ): mixed { + $table_name = $table->getName(); + $filtered_record = []; + foreach ($this->current_record as $data_set) { + $filtered_dataset = $this->filterDataSetByTable($table_name, $data_set); + if (array_filter($filtered_dataset) !== []) { + $filtered_record[] = $filtered_dataset; + } + } + + return $transformation->transform($filtered_record); + } + + private function toSql(): \ilDBStatement + { + return $this->db->queryF( + 'SELECT ' . implode( + ', ', + array_reduce( + $this->select, + static fn(array $c, Select $v): array => [...$c, ...$v->toColumnsArray()], + [] + ) + ) . ' FROM ' . CoreTables::Linking->value + . array_reduce( + $this->joins, + static fn(string $c, Join $v): string => $c . PHP_EOL . $v->toSql(), + '' + ) . PHP_EOL + . $this->buildWhereString() + . 'ORDER BY ' . implode( + ', ', + array_reduce( + $this->order, + static function (array $c, Order $v): array { + $c[] = $v->toSql(); + return $c; + }, + [] + ) + ) . PHP_EOL + . ($this->range !== null ? "LIMIT {$this->range->getStart()}, {$this->range->getLength()}" : ''), + $this->binding_types, + $this->binding_values + ); + } + + private function buildWhereString(): string + { + return array_reduce( + $this->where, + function (?string $c, Where $v): string { + $this->addValueToBinding($v->getRight()); + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()} {$v->toSql()}" . PHP_EOL; + } + ) ?? ''; + } + + private function addValueToBinding( + Value $value + ): void { + if (!is_array($value->getValue())) { + $this->binding_types[] = $value->getType(); + $this->binding_values[] = $value->getValue(); + return; + } + + foreach ($value->getValue() as $v) { + $this->binding_types[] = $value->getType(); + $this->binding_values[] = $v; + } + } + + public function filterDataSetByTable( + string $table_name, + array $data_set + ): array { + return array_reduce( + array_keys($data_set), + function (array $c, string $v) use ($table_name, $data_set): array { + if (str_starts_with($v, $table_name)) { + $c[mb_substr($v, mb_strlen($table_name) + 1)] = $data_set[$v]; + } + return $c; + }, + [] + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Replace.php b/components/ILIAS/Questions/src/Persistence/Replace.php new file mode 100644 index 000000000000..9b34db9f6d8c --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Replace.php @@ -0,0 +1,33 @@ +columns[0]->getTableName()}" . PHP_EOL + . $this->buildColumnsString() . PHP_EOL + . $this->buildValuesString($db); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Repository.php b/components/ILIAS/Questions/src/Persistence/Repository.php new file mode 100644 index 000000000000..f395ab96b436 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Repository.php @@ -0,0 +1,432 @@ +buildAvailableUuid(), + $parent_obj_id + ); + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getQuestionDataOnlyForAllQuestions(): \Generator + { + foreach ($query = new Query( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + $this->refinery + )->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + [] + ); + } + } + + /** + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getQuestionDataOnlyForQuestionIds( + array $question_ids + ): \Generator { + foreach ((new Query( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + $this->refinery + )->withAdditionalWhere( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + array_map( + fn(Uuid $v): string => $v->toString(), + $question_ids + ) + ), + Operator::In + ) + ))->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + [] + ); + } + } + + public function getForQuestionId( + Uuid $question_id + ): ?QuestionImplementation { + return $this->getForBaseQuery( + (new Query( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + $this->refinery + ))->withAdditionalWhere( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $question_id->toString() + ), + Operator::Equal + ) + ), + [$question_id] + )->current(); + } + + /** + * + * @param list<\ILIAS\Data\Uuid> $question_ids + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + public function getForQuestionIds( + array $question_ids + ): \Generator { + yield from $this->getForBaseQuery( + (new Query( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + $this->refinery + ))->withAdditionalWhere( + $this->persistence_factory->where( + CoreTables::Questions->getIdColumn( + $this->persistence_factory + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + array_map( + fn(Uuid $v): string => $v->toString(), + $question_ids + ) + ), + Operator::In + ) + ), + $question_ids + ); + } + + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ + public function create( + array $questions + ): void { + $this->store( + array_map( + fn(QuestionImplementation $v): QuestionImplementation => $v + ->withPageId($this->buildQuestionPage($v->getParentObjId())), + $questions + ), + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Create + ) + ); + } + + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ + public function update( + array $questions + ): void { + $this->store( + $questions, + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Update + ) + ); + } + + public function delete( + array $questions + ): void { + array_reduce( + $questions, + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toDelete($c), + new Manipulate( + $this->db, + $this->persistence_factory, + $this->answer_form_factory, + ManipulationType::Delete + ) + )->run(); + + foreach ($questions as $question) { + (new \QstsQuestionPage($question->getPageId()))->delete(); + } + } + + /** + * @param array<\ILIAS\Data\Uuid> $question_ids + * @return \Generator<\ILIAS\Questions\Question\QuestionImplementation> + */ + private function getForBaseQuery( + Query $query, + array $question_ids + ): \Generator { + $query_with_answer_forms = array_reduce( + $this->getAnswerFormTypesForQuestionIds($question_ids), + fn(Query $c, AnswerFormDefinition $v) => $v->getPersistence()->completeQuery( + $c, + CoreTables::AnswerForms->getIdColumn( + $this->persistence_factory + ) + ), + $query + ); + + foreach ($query_with_answer_forms->loadNextRecord() as $query_with_record) { + yield $this->retrieveQuestionFromQuery( + $query_with_record, + $this->retrieveAnswerFormsFromQuery($query_with_record) + ); + } + } + + /** + * $param array<\ILIAS\Questions\AnswerForms\Properties> $answer_forms + */ + private function retrieveQuestionFromQuery( + Query $query, + array $answer_forms + ): QuestionImplementation { + $linking_info = $query->retrieveCurrentRecord( + CoreTables::Linking->getTable( + $query->getPersistenceFactory() + ), + $this->refinery->identity() + ); + + $question = $query->retrieveCurrentRecord( + CoreTables::Questions->getTable( + $query->getPersistenceFactory() + ), + $this->refinery->custom()->transformation( + fn(array $vs): QuestionImplementation => new QuestionImplementation( + $this->uuid_factory->fromString($vs[0]['id']), + $linking_info[0]['obj_id'], + $linking_info[0]['position'], + $vs[0]['page_id'], + $vs[0]['title'], + $vs[0]['author'], + Lifecycle::from($vs[0]['lifecycle']), + $vs[0]['remarks'], + $vs[0]['original_id'] === null + ? null + : $this->uuid_factory->fromString($vs[0]['original_id']), + new \DateTimeImmutable('@' . $vs[0]['last_update'], new \DateTimeZone('UTC')), + new \DateTimeImmutable('@' . $vs[0]['created'], new \DateTimeZone('UTC')), + $answer_forms + ) + ) + ); + + if ($answer_forms === [] || $question->getPageId() !== 0) { + return $question; + } + + return $this->migrateQuestionPage($question); + } + + /** + * @return array<\ILIAS\Questions\AnswerForms\Properties> + */ + private function retrieveAnswerFormsFromQuery( + Query $query + ): array { + return $query->retrieveCurrentRecord( + CoreTables::AnswerForms->getTable( + $query->getPersistenceFactory() + ), + $this->refinery->custom()->transformation( + function (array $vs) use ($query): array { + if (count($vs) === 1 && $vs[0]['type'] === null) { + return []; + } + + $answer_forms = []; + $previous_answer_form_id = null; + foreach ($vs as $data_set) { + if ($data_set['id'] === $previous_answer_form_id) { + continue; + } + $previous_answer_form_id = $data_set['id']; + $definition = $this->answer_form_factory + ->getDefinitionForClass($data_set['type']); + $answer_forms[] = $definition->buildProperties( + $this->answer_form_factory->buildTypeGenericPropertiesFromDatabase($data_set), + $query + ); + } + return $answer_forms; + } + ) + ); + } + + /** + * @param array<\ILIAS\Data\Uuid> $question_ids + * @return array<\ILIAS\Questions\AnswerForm\Definition> + */ + private function getAnswerFormTypesForQuestionIds( + array $question_ids + ): array { + $query = $this->db->query( + 'SELECT DISTINCT type FROM ' . CoreTables::AnswerForms->value . PHP_EOL + . "WHERE {$this->db->in( + 'question_id', + $question_ids, + false, + \ilDBConstants::T_TEXT + )}" + ); + $answer_form_types = []; + while (($type_class = $this->db->fetchObject($query)?->type) !== null) { + $answer_form_types[] = $this->answer_form_factory->getDefinitionForClass($type_class); + } + return $answer_form_types; + } + + /** + * @param array<\ILIAS\Questions\Question\QuestionImplementation> $questions + */ + private function store( + array $questions, + Manipulate $manipulate + ): void { + array_reduce( + $questions, + fn(Manipulate $c, QuestionImplementation $v): Manipulate => $v->toStorage($c), + $manipulate + )->run(); + } + + private function buildAvailableUuid(): Uuid + { + do { + $uuid = $this->uuid_factory->uuid4(); + if ($this->checkAvailabilityOfId($uuid)) { + return $uuid; + } + } while (true); + } + + private function checkAvailabilityOfId( + Uuid $uuid + ): bool { + return $this->db->fetchObject( + $this->db->query( + 'SELECT COUNT(*) as cnt FROM ' . CoreTables::Questions->value + . " WHERE id='{$uuid->toString()}'" + ) + )->cnt === 0; + } + + private function buildQuestionPage( + int $parent_obj_id + ): int { + $page = new \QstsQuestionPage(); + $page->setId($this->getNextAvailableQuestionPageId()); + $page->setParentId($parent_obj_id); + $page->createFromXML(); + return $page->getId(); + } + + private function getNextAvailableQuestionPageId(): int + { + + $last_id = $this->db->fetchObject( + $this->db->query( + 'SELECT MAX(page_id) AS last FROM page_object ' + . 'WHERE parent_type = "qsts"' + ) + )->last; + if ($last_id === null) { + return 1; + } + + return $last_id + 1; + } + + private function migrateQuestionPage( + QuestionImplementation $question + ): QuestionImplementation { + $old_page_id = $this->db->fetchObject( + $this->db->query( + 'SELECT old_question_id FROM ' . CoreTables::MigrationsTable->value . PHP_EOL + . "WHERE new_question_id = {$this->db->quote($question->getId(), \ilDBConstants::T_TEXT)}" + ) + )->old_question_id; + + $new_page_id = $this->getNextAvailableQuestionPageId(); + $old_qsts_page = new \ilAssQuestionPage($old_page_id); + $old_qsts_page->setQuestion($question); + $old_qsts_page->copyToAnswerForm($new_page_id, $question); + + $new_question = $question->withPageId($new_page_id); + + $this->update([$new_question]); + + return $new_question; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Select.php b/components/ILIAS/Questions/src/Persistence/Select.php new file mode 100644 index 000000000000..ef4c4ad50258 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Select.php @@ -0,0 +1,40 @@ + $columns + */ +class Select +{ + public function __construct( + private readonly array $columns + ) { + } + + public function toColumnsArray(): array + { + return array_map( + fn(Column $v): string => $v->getAliasedColumnString(), + $this->columns + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Storable.php b/components/ILIAS/Questions/src/Persistence/Storable.php new file mode 100644 index 000000000000..82290134d3e6 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Storable.php @@ -0,0 +1,32 @@ +table_definition instanceof CoreTables) { + return $this->table_definition->value; + } + + return $this->table_name_builder->getTableNameFor( + $this->table_definition, + $this->table_identifier + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php new file mode 100644 index 000000000000..7d007559198f --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/TableNameBuilder.php @@ -0,0 +1,50 @@ +type_specific_part = $table_name_space->getTypeSpecificTableNamePart(); + } + + public function getTableNameFor( + TableTypes $type, + string $identifier = '' + ): string { + if ($type === TableTypes::Additional && $identifier === '') { + throw \InvalidArgumentException( + 'Identifier cannot be empty for type ' . TableTypes::Additional->name . '.' + ); + } + return match ($type) { + TableTypes::TypeSpecificAnswerForms => "qsts_answer_forms_{$this->type_specific_part}", + TableTypes::AnswerInputs => "qsts_answer_inputs_{$this->type_specific_part}", + TableTypes::AnswerOptions => "qsts_answer_options_{$this->type_specific_part}", + TableTypes::Responses => "qsts_responses_{$this->type_specific_part}", + TableTypes::Additional => "qsts_{$this->type_specific_part}_{$identifier}" + }; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/TableNameSpace.php b/components/ILIAS/Questions/src/Persistence/TableNameSpace.php new file mode 100644 index 000000000000..2c8d735bfd20 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/TableNameSpace.php @@ -0,0 +1,44 @@ + 4 || mb_strlen($answer_form_id) > 8) { + throw new \InvalidArgumentException( + '$vendor cannot be longer than 4, $answer_form_id can be longer then 8 characters.' + ); + } + } + + public function getTypeSpecificTableNamePart(): string + { + return "{$this->vendor}_{$this->answer_form_id}"; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php new file mode 100644 index 000000000000..aa7815dabc80 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/TableNameSpaceCore.php @@ -0,0 +1,35 @@ +answer_form_id; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/TableTypes.php b/components/ILIAS/Questions/src/Persistence/TableTypes.php new file mode 100644 index 000000000000..7caed2f133de --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/TableTypes.php @@ -0,0 +1,48 @@ + $persistence_factory->table( + $this, + $table_name_builder, + $table_identifier + ), + default => $persistence_factory->table( + $this, + $table_name_builder + ) + }; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Update.php b/components/ILIAS/Questions/src/Persistence/Update.php new file mode 100644 index 000000000000..0631f6099b59 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Update.php @@ -0,0 +1,106 @@ + $columns + * @param array<\ILIAS\Questions\Persistence\Value> $values + * @param array<\ILIAS\Questions\Persistence\Where> $where + */ + public function __construct( + private readonly array $columns, + private readonly array $values, + private readonly array $where + ) { + if ($columns === [] || count($columns) !== count($values)) { + throw new \InvalidArgumentException( + "There MUST be at least one Column and the same amount of Values as there are Columns." + ); + } + + $table_name = $columns[0]->getTableName(); + foreach ($columns as $column) { + if ($column->getTableName() !== $table_name) { + throw new \InvalidArgumentException( + "All Columns MUST belong to the same Table." + ); + } + } + } + + public function getTableToLock(): string + { + return $this->columns[0]->getTableName(); + } + + public function toManipulateString( + \ilDBInterface $db + ): string { + return "UPDATE {$this->columns[0]->getTableName()}" . PHP_EOL + . $this->buildSetterString($db) . PHP_EOL + . $this->buildWhereString($db); + } + + private function buildSetterString( + \ilDBInterface $db + ): string { + return trim( + array_reduce( + array_keys($this->columns), + fn(string $c, int $v): string => $c + . "{$this->columns[$v]->getColumnString()} = {$this->values[$v]->getQuotedValue($db)},", + 'SET ' + ), + ',' + ); + } + + private function buildWhereString( + \ilDBInterface $db + ): string { + $values = []; + return sprintf( + array_reduce( + $this->where, + function (?string $c, Where $v) use ($db, &$values): string { + $quoted_value = $v->getRight()->getQuotedValue($db); + if (is_array($quoted_value)) { + $values = array_merge( + $values, + array_values($quoted_value) + ); + } else { + $values[] = $quoted_value; + } + + if ($c === null) { + return "WHERE {$v->toSql()}" . PHP_EOL; + } + + return "{$c}{$v->getLogicalOperator()->value} {$v->toSql()}" . PHP_EOL; + } + ) ?? '', + ...$values + ); + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Value.php b/components/ILIAS/Questions/src/Persistence/Value.php new file mode 100644 index 000000000000..bb986140fd4c --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Value.php @@ -0,0 +1,68 @@ +type; + } + + public function getValue(): string|int|array + { + return $this->value; + } + + public function getQuotedValue( + \ilDBInterface $db + ): array|string { + if (!is_array($this->value)) { + return $db->quote( + $this->value, + $this->type + ); + } + + return array_map( + fn(mixed $v): string => $db->quote($v, $this->type), + $this->value + ); + } + + public function getNumberOfElements(): int + { + if (is_array($this->value)) { + return count($this->value); + } + + return 1; + } +} diff --git a/components/ILIAS/Questions/src/Persistence/Where.php b/components/ILIAS/Questions/src/Persistence/Where.php new file mode 100644 index 000000000000..c0714bb73224 --- /dev/null +++ b/components/ILIAS/Questions/src/Persistence/Where.php @@ -0,0 +1,52 @@ +negate ? 'NOT ' : '') + . $this->comparison->toSql( + $this->left, + $this->right->getNumberOfElements() + ); + } + + public function getRight(): Value + { + return $this->right; + } + + public function getLogicalOperator(): Junctor + { + return $this->junctor; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php new file mode 100644 index 000000000000..f7ca340fb0d8 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Definitions/Editability.php @@ -0,0 +1,28 @@ +acquireURLBuilderAndParameters($base_uri); + } + + #[\Override] + public function setEditAnswerFormBackTarget(): void + { + $this->tabs_gui->clearTargets(); + $this->tabs_gui->setBackTarget( + $this->lng->txt('cancel'), + $this->buildEditAnswerFormBackUrl()->buildURI()->__toString() + ); + } + + #[\Override] + public function addEditAnswerFormSubTab( + string $step, + string $language_variable + ): void { + $this->tabs_gui->addSubTab( + $step, + $this->lng->txt($language_variable), + $this->withStepParameter($step) + ->withActionParameter(Edit::ACTION_OTHER_ANSWER_FORM) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + #[\Override] + public function activateEditAnswerFormSubTab( + string $step + ): void { + $this->tabs_gui->activateSubTab($step); + } + + #[\Override] + public function getPresentationFactory(): Factory + { + return $this->presentation_factory; + } + + #[\Override] + public function getUrlBuilder(): URLBuilder + { + return $this->url_builder; + } + + #[\Override] + public function withStepParameter( + string $step + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->step_token, $step); + return $clone; + } + + #[\Override] + public function withDefaultStep(): self + { + $clone = clone $this; + $clone->default_step = true; + return $clone; + } + + #[\Override] + public function getStep(): string + { + return $this->default_step + ? '' + : $this->retrieveStringValueForToken($this->step_token, self::TOKEN_STRING_STEP); + } + + #[\Override] + public function getEditability(): Editability + { + return $this->editability; + } + + #[\Override] + public function isInCreationContext(): bool + { + return $this->is_in_creation_context; + } + + #[\Override] + public function getAnswerFormId(): ?Uuid + { + if ($this->answer_form_properties !== null) { + return $this->answer_form_properties->getAnswerFormId(); + } + + $answer_form_id_token = $this->answer_form_id_token; + if ($answer_form_id_token === null) { + [,$answer_form_id_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ANSWER_FORM_ID + ); + } + return $this->http->wrapper()->query()->retrieve( + $answer_form_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + $this->buildRetrieveUuidClosure() + ), + $this->refinery->always(null) + ]) + ); + } + + #[\Override] + public function getAnswerFormProperties(): ?Properties + { + return $this->answer_form_properties; + } + + #[\Override] + public function withAnswerFormProperties( + Properties $properties + ): self { + $clone = clone $this; + $clone->answer_form_properties = $properties; + return $clone; + } + + #[\Override] + public function getTableRowIdToken(): URLBuilderToken + { + return $this->table_row_token; + } + + /** + * @return list + */ + #[\Override] + public function getTableRowIds(): array + { + if ($this->table_row_ids !== null) { + return $this->table_row_ids; + } + + return $this->table_row_ids = $this->http->wrapper()->query()->retrieve( + $this->table_row_token->getName(), + $this->refinery->byTrying([ + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + fn($v): string => $v !== '' + ? $this->refinery->kindlyTo()->string()->transform($v) + : throw new \UnexpectedValueException() + ) + ), + $this->refinery->always([]) + ]) + ); + } + + #[\Override] + public function withPreservedTableRowIdsParameter(): self + { + $clone = clone $this; + $clone->table_row_ids = $clone->getTableRowIds(); + $clone->url_builder = $this->url_builder + ->withParameter($this->table_row_token, $clone->table_row_ids); + return $clone; + } + + public function withIsInCreationContext( + bool $is_in_creation_context + ): self { + $clone = clone $this; + $clone->is_in_creation_context = $is_in_creation_context; + return $clone; + } + + public function getObjId(): int + { + return $this->obj_id; + } + + public function getAction(): string + { + return $this->retrieveStringValueForToken($this->action_token); + } + + public function withActionParameter( + string $action + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->action_token, $action); + return $clone; + } + + public function withQuestionIdParameter( + Uuid $question_id + ): self { + $clone = clone $this; + $clone->url_builder = $this->url_builder + ->withParameter($this->question_id_token, $question_id->toString()); + return $clone; + } + + public function withAnswerFormTypeHashParameter( + string $type_hash + ): self { + $clone = clone $this; + [ + $clone->url_builder, + $clone->type_hash_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + + $clone->url_builder = $clone->url_builder + ->withParameter($clone->type_hash_token, $type_hash); + return $clone; + } + + public function withAnswerFormIdParameter( + Uuid $answer_form_id + ): self { + $clone = clone $this; + [ + $clone->url_builder, + $clone->answer_form_id_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ANSWER_FORM_ID + ); + + $clone->url_builder = $clone->url_builder->withParameter( + $clone->answer_form_id_token, + $answer_form_id->toString() + ); + return $clone; + } + + public function withCreateModeParameter(): self + { + $clone = clone $this; + + [ + $clone->url_builder, + $clone->create_mode_token + ] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + + $clone->url_builder = $clone->url_builder + ->withParameter($clone->create_mode_token, '1'); + return $clone; + } + + public function getQuestionId(): ?Uuid + { + return $this->http->wrapper()->query()->retrieve( + $this->question_id_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + $this->buildRetrieveUuidClosure() + ), + $this->refinery->always(null) + ]) + ); + } + + /** + * This function will either return the QuestionIds from the $_GET parameter + * for row ids OR from an InterruptiveItems $_POST value. + * @return array<\ILIAS\Data\UUID\Uuid>|string|null + */ + public function getQuestionIds(): array|string|null + { + return $this->http->wrapper()->query()->retrieve( + $this->table_row_token->getName(), + $this->refinery->byTrying([ + $this->refinery->custom()->transformation( + fn($v): string => $v === ['ALL_OBJECTS'] + ? 'ALL_OBJECTS' + : throw new \UnexpectedValueException() + ), + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + $this->buildRetrieveUuidClosure() + ) + ), + $this->refinery->always(null) + ]) + ) ?? $this->http->wrapper()->post()->retrieve( + self::INTERRUPTIVE_ITEMS_KEY, + $this->refinery->kindlyTo()->listOf( + $this->refinery->custom()->transformation( + $this->buildRetrieveUuidClosure() + ) + ), + $this->refinery->always(null) + ); + } + + public function getTypeClassHash(): string + { + $type_hash_token = $this->type_hash_token; + if ($type_hash_token === null) { + [,$type_hash_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_TYPE_HASH + ); + } + return $this->retrieveStringValueForToken($type_hash_token); + } + + public function isCreateModeSimple(): bool + { + $create_mode_token = $this->create_mode_token; + if ($create_mode_token === null) { + [, $create_mode_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + } + + return $this->http->wrapper()->query()->has( + $create_mode_token->getName() + ); + } + + public function setEditAnswerFormTabs( + string $cmd_feedback, + string $cmd_content_for_repetition + ): void { + $this->tabs_gui->addTab( + self::TAB_ID_ANSWER_FORM, + $this->lng->txt('answer_form'), + $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + + $this->tabs_gui->addTab( + $cmd_feedback, + $this->lng->txt('feedback'), + $this->withActionParameter($cmd_feedback) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + + $this->tabs_gui->addTab( + $cmd_content_for_repetition, + $this->lng->txt('suggested_solution'), + $this->withActionParameter($cmd_content_for_repetition) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + + $this->tabs_gui->addSubTab( + self::TAB_ID_ANSWER_FORM, + $this->lng->txt('overview'), + $this->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + + $this->tabs_gui->activateTab(self::TAB_ID_ANSWER_FORM); + $this->tabs_gui->activateSubTab(self::TAB_ID_ANSWER_FORM); + } + + public function preserveParametersForPageEditorCmds(): void + { + $this->setQuestionIdParamterForPageEditorCmds($this->getQuestionId()); + } + + public function setParamtersForSimpleCreateCmd( + Uuid $question_id + ): void { + $this->setQuestionIdParamterForPageEditorCmds($question_id); + + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + self::PARAMETER_STRING_HIER_ID, + '1' + ); + + [, $create_mode_token] = $this->url_builder->acquireParameter( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_MODE + ); + + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $create_mode_token->getName(), + '1' + ); + } + + public function isCreateAndNewAction(): bool + { + return $this->http->wrapper()->query()->has( + $this->buildURLBuilderTokenForCreateAndNew()->getName() + ); + } + + public function buildURLBuilderTokenForCreateAndNew(): URLBuilderToken + { + return new URLBuilderToken( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_CREATE_AND_NEW + ); + } + + private function setQuestionIdParamterForPageEditorCmds( + Uuid $question_id + ): void { + $this->ctrl->setParameterByClass( + \QstsQuestionPageGUI::class, + $this->question_id_token->getName(), + $question_id->toString() + ); + } + + private function buildEditAnswerFormBackUrl(): URLBuilder + { + if (!$this->is_in_creation_context) { + return $this->withDefaultStep()->getUrlBuilder(); + } + + if (!$this->isCreateModeSimple()) { + return new URLBuilder( + new URI( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass( + \QstsQuestionPageGUI::class, + 'edit' + ) + ) + ); + } + + return $this->url_builder->withParameter( + $this->action_token, + Edit::ACTION_DELETE_QUESTIONS + )->withParameter( + $this->step_token, + Edit::ACTION_DELETE_QUESTIONS + )->withParameter( + $this->table_row_token, + [$this->getQuestionId()->toString()] + ); + } + + private function acquireURLBuilderAndParameters( + URI $base_uri + ): void { + [ + $this->url_builder, + $this->action_token, + $this->step_token, + $this->question_id_token, + $this->table_row_token + ] = (new URLBuilder($base_uri)) + ->acquireParameters( + self::QUERY_PARAMETER_NAME_SPACE, + self::TOKEN_STRING_ACTION, + self::TOKEN_STRING_STEP, + self::TOKEN_STRING_QUESTION_ID, + self::TOKEN_STRING_TABLE_ROW_ID + ); + } + + private function retrieveStringValueForToken( + URLBuilderToken $token + ): string { + return $this->http->wrapper()->query()->retrieve( + $token->getName(), + $this->buildStringTrafo() + ); + } + + private function buildStringTrafo(): Transformation + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always('') + ]); + } + + private function buildRetrieveUuidClosure(): \Closure + { + return fn($v): Uuid => is_string($v) + ? $this->uuid_factory->fromString($v) + : throw new \UnexpectedValueException(); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Async.php b/components/ILIAS/Questions/src/Presentation/Layout/Async.php new file mode 100644 index 000000000000..f1de349ba47c --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Async.php @@ -0,0 +1,53 @@ +http->saveResponse( + $this->http->response()->withBody( + Streams::ofString( + $ui_renderer->renderAsync($this->content) + ) + ) + ); + $this->http->sendResponse(); + $this->http->close(); + } + + +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php new file mode 100644 index 000000000000..469b158eaaf0 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditForm.php @@ -0,0 +1,210 @@ +form = $this->buildForm( + $inputs, + $default_form_action, + $back_form_action, + $is_final_step + ); + } + + #[\Override] + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); + } + + public function isFinalStep(): bool + { + return $this->is_final_step; + } + + public function withContentBeforeForm( + StandardPanel $content + ): self { + $clone = clone $this; + $clone->content_before_form = $content; + return $clone; + } + + public function withContentAfterForm( + StandardPanel $content + ): self { + $clone = clone $this; + $clone->content_after_form = $content; + return $clone; + } + + public function withConfirmation( + InterruptiveModal $confirmation_modal + ): self { + $clone = clone $this; + $clone->confirmation = $confirmation_modal; + return $clone; + } + + public function withInsertLegacyTextsButton( + URLBuilder $target_builder + ): self { + $clone = clone $this; + $clone->insert_legacy_text_button = $this->ui_factory->messageBox()->info( + $this->lng->txt('insert_legacy_texts_info') + )->withButtons([ + $this->ui_factory->button()->standard( + $this->lng->txt('insert_legacy_texts'), + $target_builder->buildURI()->__toString() + ) + ]); + return $clone; + } + + public function withRequest( + ServerRequestInterface $request + ): self { + $clone = clone $this; + $clone->form = $clone->form->withRequest($request); + return $clone; + } + + public function getData(): mixed + { + $data = $this->form->getData(); + return $data[self::MAIN_SECTION_NAME] ?? null; + } + + public function withAdditionalAction( + URLBuilderToken $parameter_token, + string $parameter_value, + string $label + ): self { + $clone = clone $this; + $clone->form = $this->form->withAdditionalFormAction( + $this->default_form_action->buildURI()->withParameter( + $parameter_token->getName(), + $parameter_value + )->__toString(), + $label + ); + return $clone; + } + + private function buildContent(): array + { + $content = []; + + if ($this->content_before_form !== null) { + $content[] = $this->content_before_form; + } + + if ($this->confirmation !== null) { + $content[] = $this->confirmation->withOnLoad( + $this->confirmation->getShowSignal() + )->withAdditionalOnLoadCode( + function ($id) { + return "var button = {$id}.querySelector('input[type=\"submit\"]'); " + . "button.addEventListener('click', (e) => {e.preventDefault();" + . 'const form = button.closest("dialog").nextElementSibling;' + . "form.action = '{$this->confirmation->getFormAction()}';" + . 'form.submit();});'; + } + ); + } + + if ($this->insert_legacy_text_button !== null) { + $content[] = $this->insert_legacy_text_button; + } + + $content[] = $this->form; + + if ($this->content_after_form !== null) { + $content[] = $this->content_after_form; + } + + return $content; + } + + private function buildForm( + Input|InputsBuilder $inputs, + URLBuilder $default_form_action, + ?URLBuilder $back_form_action, + bool $is_final_step + ): StandardForm { + if ($inputs instanceof InputsBuilder) { + $inputs = $inputs->getInputs(); + } + $form = $this->ui_factory->input()->container()->form()->standard( + $default_form_action->buildURI()->__toString(), + [ + self::MAIN_SECTION_NAME => $inputs + ] + ); + + if ($back_form_action !== null) { + $form = $form->withAdditionalFormAction( + $back_form_action->buildURI()->__toString(), + $this->lng->txt('previous') + ); + } + + $submit_action_label = 'next'; + if ($is_final_step) { + $submit_action_label = 'save'; + } + + return $form->withSubmitLabel( + $this->lng->txt($submit_action_label) + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php new file mode 100644 index 000000000000..95651e4c55c5 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/EditOverview.php @@ -0,0 +1,84 @@ +render($this->buildContent()); + } + + private function buildContent(): array + { + return [ + $this->buildBasicAnswerFormPanel(), + $this->environment->getAnswerFormProperties()->getOverviewTable( + $this->ui_factory->table(), + $this->lng, + $this->request, + $this->environment + ) + ]; + } + + private function buildBasicAnswerFormPanel(): StandardPanel + { + $content = [ + $this->ui_factory->listing()->descriptive( + $this->environment->getAnswerFormProperties()->getBasicPropertiesForListing($this->lng) + ) + ]; + + if ($this->environment->getEditability() === Editability::Full) { + $content[] = $this->ui_factory->button()->standard( + $this->lng->txt('edit_basic_answer_form_properties'), + $this->target_to_edit_basic_answer_form_properties + ->buildURI() + ->__toString() + ); + } + + return $this->ui_factory->panel()->standard( + $this->lng->txt('basic_answer_form_properites'), + $content + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Factory.php b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php new file mode 100644 index 000000000000..4857dcc5e158 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Factory.php @@ -0,0 +1,90 @@ +ui_factory, + $this->lng, + $this->http->request(), + $environment, + $target_to_edit_basic_answer_form_properties + ); + } + + public function getEditForm( + Input|InputsBuilder $main_section_inputs, + URLBuilder $default_form_action, + ?URLBuilder $back_form_action, + bool $is_final_step + ): EditForm { + return new EditForm( + $this->ui_factory, + $this->lng, + $main_section_inputs, + $default_form_action, + $back_form_action, + $is_final_step + ); + } + + public function getAsync( + InterruptiveModal|RoundTripModal|MessageBox $content + ): Async { + return new Async( + $this->http, + $content + ); + } + + public function getSessionBasedInputsBuilder( + string $storage_key, + Transformation $to_inputs + ): InputsBuilderSession { + return new InputsBuilderSession( + $storage_key, + $to_inputs + ); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php new file mode 100755 index 000000000000..590231b8a9c0 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/GlobalScreen/LayoutProvider.php @@ -0,0 +1,165 @@ +context_collection->main(); + } + + protected function isModeEnabled( + CalledContexts $called_contexts + ): bool { + return $called_contexts->current()->getAdditionalData() + ->is(self::MODE_ENABLED, true); + } + + #[\Override] + public function getBreadCrumbsModification( + CalledContexts $called_contexts + ): ?BreadCrumbsModification { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $this->globalScreen()->layout()->factory()->breadcrumbs() + ->withModification( + function (?Breadcrumbs $current): ?Breadcrumbs { + return null; + } + )->withPriority(self::MODIFICATION_PRIORITY); + } + + #[\Override] + public function getMainBarModification( + CalledContexts $called_contexts + ): ?MainBarModification { + $mainbar = $this->globalScreen()->layout()->factory()->mainbar(); + + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $mainbar + ->withModification( + $this->buildMainBarModification($called_contexts) + )->withPriority(self::MODIFICATION_PRIORITY); + } + + #[\Override] + public function getMetaBarModification( + CalledContexts $called_contexts + ): ?MetaBarModification { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + return $this->globalScreen()->layout()->factory()->metabar() + ->withModification( + function (?MetaBar $current): ?MetaBar { + return null; + } + )->withPriority(self::MODIFICATION_PRIORITY); + } + + #[\Override] + public function getPageBuilderDecorator( + CalledContexts $called_contexts + ): ?PageBuilderModification { + if (!$this->isModeEnabled($called_contexts)) { + return null; + } + + $mode_info = $this->dic['ui.factory']->mainControls()->modeInfo( + $this->dic->language()->txt('edit_questions'), + $called_contexts->current()->getAdditionalData()->get(self::URL_CLOSE_MODE_INFO) + ); + + return $this->factory->page() + ->withLowPriority() + ->withModification( + static function (PagePartProvider $parts) use ($mode_info): Page { + $p = new StandardPageBuilder(); + $page = $p->build($parts); + return $page->withModeInfo($mode_info); + } + ); + } + + private function buildMainbarModification( + CalledContexts $called_contexts + ): \Closure { + return function (?MainBar $mainbar) use ($called_contexts): ?MainBar { + if ($mainbar === null) { + return null; + } + + $tools = $mainbar->getToolEntries(); + $new_mainbar = array_reduce( + array_keys($tools), + static fn(MainBar $c, string $v): MainBar => $c->withAdditionalToolEntry($v, $tools[$v]), + $mainbar + ->withClearedEntries() + ->withAdditionalEntry( + 'create_question', + $this->dic['ui.factory']->button()->bulky( + $this->dic['ui.factory']->symbol()->icon()->standard('', '')->withAbbreviation('CQ'), + $this->dic['lng']->txt('create_question'), + $called_contexts->current()->getAdditionalData()->get(self::URL_CREATE_QUESTION)->__toString() + ) + ) + ); + + if ($called_contexts->current()->getAdditionalData()->exists(self::QUESTIONLIST_ENTRY)) { + return $new_mainbar->withAdditionalEntry( + 'question_list', + $called_contexts->current()->getAdditionalData()->get(self::QUESTIONLIST_ENTRY) + ); + } + + return $new_mainbar; + }; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php new file mode 100644 index 000000000000..10bcd3ec1389 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/InputsBuilder.php @@ -0,0 +1,28 @@ +carry = $carry; + return $clone; + } + + public function persistCarry(): void + { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + \ilSession::set($this->storage_key, $this->carry); + } + + public function resetCarry(): void + { + \ilSession::clear($this->storage_key); + } + + public function retrieveCarry( + Transformation $transformation + ): mixed { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + + return $transformation->transform($this->carry); + } + + public function getInputs(): Section + { + if ($this->carry === null) { + $this->loadCarryFromSessionAndClear(); + } + return $this->to_inputs->transform($this->carry); + } + + private function loadCarryFromSessionAndClear(): void + { + $this->carry = \ilSession::get($this->storage_key); + \ilSession::clear($this->storage_key); + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php new file mode 100644 index 000000000000..b2f5bde3687c --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/QuestionsTable.php @@ -0,0 +1,153 @@ +loadLanguageModule('qpl'); + } + + public function render( + UIRenderer $ui_renderer + ): string { + return $ui_renderer->render($this->buildContent()); + } + + #[\Override] + public function getRows( + DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): \Generator { + $environment_with_action = $this->environment->withActionParameter( + Edit::ACTION_EDIT_QUESTION + ); + foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { + yield $question->toTableRow( + $row_builder, + $this->ui_factory, + $environment_with_action + ); + } + } + + #[\Override] + public function getTotalRowCount( + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters + ): ?int { + return 0; + } + + private function buildContent(): array + { + return [ + $this->buildFilter($this->environment->getUrlBuilder()->buildURI()->__toString()), + $this->ui_factory->table()->data( + $this, + $this->lng->txt('questions'), + $this->getColums(), + )->withActions( + $this->getActions() + )->withRange(new Range(0, 20)) + ->withRequest($this->request) + ]; + } + + private function buildFilter( + string $action + ): Filter { + $question_type_options = [ + '' => $this->lng->txt('filter_all_question_types') + ]; + + $field_factory = $this->ui_factory->input()->field(); + $filter_inputs = [ + 'title' => $field_factory->text($this->lng->txt('title')), + 'contains_type' => $field_factory->select( + $this->lng->txt('contains_type'), + $question_type_options + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + ), + ]; + + $active = array_fill(0, count($filter_inputs), true); + + $filter = $this->ui_service->filter()->standard( + 'question_table_filter_id', + $action, + $filter_inputs, + $active, + true, + true + ); + return $filter; + } + + private function getColums(): array + { + $f = $this->ui_factory->table()->column(); + + return [ + 'title' => $f->link($this->lng->txt('title')), + 'type' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true), + ]; + } + + private function getActions(): array + { + return [ + 'delete' => $this->ui_factory->table()->action()->standard( + $this->lng->txt('delete'), + $this->environment->withActionParameter(Edit::ACTION_DELETE_QUESTIONS) + ->getUrlBuilder(), + $this->environment->getTableRowIdToken() + )->withAsync(true) + ]; + } +} diff --git a/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php new file mode 100644 index 000000000000..522b6d0def13 --- /dev/null +++ b/components/ILIAS/Questions/src/Presentation/Layout/Renderable.php @@ -0,0 +1,30 @@ +checkCapabilities($capability_class_names); + $clone = clone $this; + $clone->required_capabilities = $capability_class_names; + return $clone; + } + + public function withEditable( + Editability $editability + ): self { + $clone = clone $this; + $clone->editability = $editability; + return $clone; + } + + public function withOrderingEnabled( + bool $enable + ): self { + $clone = clone $this; + $clone->ordering_enabled = $enable; + return $clone; + } + + public function show( + \ilToolbarGUI $toolbar, + URI $base_uri, + int $obj_id, + int $ref_id + ): Async|QuestionsTable|EditForm { + $this->content_style->gui()->addCss( + $this->global_tpl, + $ref_id + ); + + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + ); + + return match($environment->getAction()) { + self::ACTION_CREATE_QUESTION => $this->createQuestion( + $environment->withIsInCreationContext(true) + ), + self::ACTION_EDIT_QUESTION => $this->editQuestion($environment), + self::ACTION_DELETE_QUESTIONS => $this->deleteQuestions($environment), + default => $this->showTable($toolbar, $environment) + }; + } + + public function forwardPageCmds( + \ilGlobalTemplateInterface $tpl, + URI $base_uri, + int $obj_id, + int $ref_id + ): void { + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + ); + + if ($this->ctrl->getCmd() === 'insert' + && $environment->getAction() === self::ACTION_DELETE_QUESTIONS) { + $this->deleteQuestions($environment); + return; + } + + $this->initializeEditMode($environment); + $environment->preserveParametersForPageEditorCmds(); + + $this->content_style->gui()->addCss( + $tpl, + $ref_id + ); + + $tpl->setContent( + $this->ctrl->forwardCommand( + new \QstsQuestionPageGUI( + $this->questions_repository->getForQuestionId( + $environment->getQuestionId() + ), + $obj_id + )->withReturnURI( + $environment + ->withActionParameter(self::ACTION_EDIT_QUESTION) + ->withQuestionIdParameter($environment->getQuestionId()) + ->getUrlBuilder() + ->buildURI() + ) + ) + ); + } + + public function createAnswerForm( + URI $base_uri, + int $obj_id, + QuestionImplementation $question, + \ilPCAnswerForm $content_object + ): EditForm { + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + )->withIsInCreationContext(true) + ->withActionParameter(self::ACTION_CREATE_ANSWER_FORM) + ->withQuestionIdParameter($question->getId()); + + + $environment->setEditAnswerFormBackTarget(); + + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $environment = $environment->withCreateModeParameter(); + } + + $answer_form_type_class_hash = $environment->getTypeClassHash(); + + if ($answer_form_type_class_hash !== '') { + $type = $this->answer_form_factory + ->buildTypeDefinitionFromSelectValue($answer_form_type_class_hash); + + return $this->forwardCreateAnswerFormCmd( + $environment->withAnswerFormProperties( + $type->buildProperties( + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId(), + $type, + $environment->getAnswerFormId(), + ), + null + ) + )->withAnswerFormTypeHashParameter($answer_form_type_class_hash), + $question, + $content_object, + $type->getEditView() + ); + } + + return match($environment->getAction()) { + self::ACTION_CREATE_ANSWER_FORM => $this->processCreateAnswerForm( + $environment, + $question, + $content_object + ), + default => $this->buildCreateAnswerForm($environment) + }; + } + + public function editAnswerForm( + URI $base_uri, + int $obj_id, + QuestionImplementation $question, + AnswerFormProperties $answer_form_properties, + Definition $type + ): Async|Renderable { + $environment = $this->buildEnvironment( + $base_uri, + $obj_id + )->withAnswerFormProperties($answer_form_properties) + ->withQuestionIdParameter($question->getId()); + + $environment->setEditAnswerFormTabs( + self::ACTION_EDIT_FEEDBACK, + self::ACTION_EDIT_CONTENT_FOR_REPETITION + ); + + $edit_view = $type->getEditView(); + + if ($environment->getAction() === self::ACTION_OTHER_ANSWER_FORM) { + $environment = $environment->withActionParameter(self::ACTION_OTHER_ANSWER_FORM); + $next = $edit_view->other($environment); + } else { + $next = $edit_view->edit($environment); + } + + if (!($next instanceof AnswerFormProperties)) { + return $next; + } + + $this->questions_repository->update( + [$question->withAnswerFormProperties($next)] + ); + + $this->ctrl->redirectToURL( + $edit_view->getFinishEditingUrl($environment)->buildURI()->__toString() + ); + } + + private function createQuestion( + EnvironmentImplementation $environment + ): EditForm { + $this->initializeEditMode($environment); + $this->tabs_gui->setBackTarget( + $this->lng->txt('cancel'), + $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + + $create = $this->questions_repository->getNew( + $environment->getObjId() + )->getEditView( + $this->lng, + $this->configuration_repository, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl + )->create( + $environment->withActionParameter(self::ACTION_CREATE_QUESTION) + ); + + if ($create instanceof EditForm) { + return $create; + } + + $this->questions_repository->create([$create]); + $this->ctrl->redirectToURL( + $this->buildAfterQuestionCreationRedirectUri( + $environment, + $create->getCreateMode(), + $create->getId() + ) + ); + + } + + private function editQuestion( + EnvironmentImplementation $environment + ): EditForm { + $this->initializeEditMode($environment); + $this->tabs_gui->setBackTarget( + $this->lng->txt('back'), + $environment->withDefaultStep()->getUrlBuilder()->buildURI()->__toString() + ); + + $question_id = $environment->getQuestionId(); + $question = $this->questions_repository->getForQuestionId($question_id); + $environment_with_question_parameter = $environment + ->withQuestionIdParameter($question_id); + + $edit = $question->getEditView( + $this->lng, + $this->configuration_repository, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl + )->edit( + $environment_with_question_parameter + ->withActionParameter(self::ACTION_EDIT_QUESTION), + $question->getParticipantView() + ); + + if ($edit instanceof EditForm) { + return $edit; + } + + $this->questions_repository->update([$edit]); + return $this->buildEditStartView( + $environment_with_question_parameter + ->withDefaultStep() + ->withActionParameter(self::ACTION_EDIT_QUESTION), + $edit + ); + } + + private function deleteQuestions( + EnvironmentImplementation $environment + ): Async { + $question_ids = $environment->getQuestionIds(); + + if ($question_ids === null) { + return $environment->getPresentationFactory()->getAsync( + $this->ui_factory->messageBox()->failure( + $this->lng->txt('msg_no_questions_selected') + ) + ); + } + + if ($environment->getStep() === self::ACTION_DELETE_QUESTIONS) { + $this->deleteSelectedQuestions($question_ids); + $this->ctrl->redirectToURL( + $environment->getUrlBuilder()->buildURI()->__toString() + ); + } + + return $environment->getPresentationFactory()->getAsync( + $this->ui_factory->modal()->interruptive( + $this->lng->txt('confirm'), + $this->lng->txt('qpl_confirm_delete_questions'), + $environment->withActionParameter( + self::ACTION_DELETE_QUESTIONS + )->withStepParameter( + self::ACTION_DELETE_QUESTIONS + )->getUrlBuilder()->buildURI()->__toString() + )->withAffectedItems( + $this->buildAffectedItems($question_ids) + ) + ); + } + + private function showTable( + \ilToolbarGUI $toolbar, + EnvironmentImplementation $environment + ): QuestionsTable { + $toolbar->addComponent( + $this->ui_factory->button()->primary( + $this->lng->txt('create'), + $environment->withActionParameter(self::ACTION_CREATE_QUESTION) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ) + ); + + return new QuestionsTable( + $this->ui_factory, + $this->ui_services, + $this->lng, + $this->http->request(), + $this->answer_form_factory, + $this->questions_repository, + $environment + ); + } + + private function processCreateAnswerForm( + EnvironmentImplementation $environment, + QuestionImplementation $question, + \ilPCAnswerForm $content_object + ): EditForm { + $form = $this->buildCreateAnswerForm($environment)->withRequest($this->http->request()); + + $data = $form->getData(); + if ($data === null) { + return $form; + } + + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $question_page = new \QstsQuestionPage($question->getPageId()); + $question_page->setQuestion($question); + $question_page->addQuestionText($data['question_text']); + } + + $type_definition = $data['type']; + return $this->forwardCreateAnswerFormCmd( + $environment->withAnswerFormProperties( + $type_definition->buildProperties( + $this->answer_form_factory->getDefaultTypeGenericProperties( + $question->getId(), + $type_definition + ), + null + ) + )->withAnswerFormTypeHashParameter( + $this->answer_form_factory->getHashedClass($type_definition::class) + ), + $question, + $content_object, + $type_definition->getEditView() + ); + } + + private function forwardCreateAnswerFormCmd( + EnvironmentImplementation $environment, + QuestionImplementation $question, + \ilPCAnswerForm $content_object, + AnswerFormEditView $answer_form_edit_view + ): ?EditForm { + $create = $answer_form_edit_view->create( + $environment->withAnswerFormIdParameter( + $environment->getAnswerFormId() + ) + ); + + if ($create instanceof EditForm) { + return $create->isFinalStep() + ? $create->withAdditionalAction( + $environment->buildURLBuilderTokenForCreateAndNew(), + '1', + $this->lng->txt('create_and_new') + ) : $create; + } + + $this->questions_repository->create( + [$question->withAnswerFormProperties($create)] + ); + + $content_object->create($create->getAnswerFormId()); + $content_object->getPage()->update(); + + $this->ctrl->redirectToURL( + $this->buildAfterAnswerFormCreationRedirectUri($environment) + ); + } + + private function initializeEditMode( + EnvironmentImplementation $environment + ): void { + $this->tabs_gui->clearTargets(); + + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::MODE_ENABLED, + true + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::QUESTIONLIST_ENTRY, + $this->buildQuestionListSlate($environment) + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CLOSE_MODE_INFO, + $environment->getUrlBuilder()->buildURI() + ); + $this->global_screen->tool()->context()->current()->addAdditionalData( + LayoutProvider::URL_CREATE_QUESTION, + $environment + ->withActionParameter(self::ACTION_CREATE_QUESTION) + ->getUrlBuilder() + ->buildURI() + ); + } + + private function buildQuestionListSlate( + EnvironmentImplementation $environment + ): LegacySlate { + return $this->ui_factory->mainControls()->slate()->legacy( + $this->lng->txt('mainbar_button_label_questionlist'), + $this->ui_factory->symbol()->icon()->standard('', '')->withAbbreviation('QL'), + $this->ui_factory->legacy()->content( + $this->ui_renderer->render( + $this->ui_factory->panel()->secondary()->listing( + $this->lng->txt('mainbar_button_label_questionlist'), + [ + $this->buildItemGroupForQuestionListSlate($environment) + ] + ) + ) + ) + ); + } + + private function buildItemGroupForQuestionListSlate( + EnvironmentImplementation $environment + ): ItemGroup { + return $this->ui_factory->item()->group( + '', + $this->builEditLinksForQuestionListSlate($environment) + ); + } + + private function builEditLinksForQuestionListSlate( + Environment $environment + ): array { + $links = []; + foreach ($this->questions_repository->getQuestionDataOnlyForAllQuestions() as $question) { + $links[] = $this->ui_factory->item()->standard( + $question->toEditLink( + $this->ui_factory->link(), + $environment->withActionParameter(self::ACTION_EDIT_QUESTION) + ) + ); + } + return $links; + } + + private function buildEditStartView( + EnvironmentImplementation $environment, + QuestionImplementation $question + ): EditForm { + return $question->getEditView( + $this->lng, + $this->configuration_repository, + $this->current_user, + $this->ui_factory, + $this->refinery, + $this->http->request(), + $this->ctrl + )->edit( + $environment, + $question->getParticipantView() + ); + } + + private function buildCreateAnswerForm( + EnvironmentImplementation $environment + ): EditForm { + $if = $this->ui_factory->input(); + + $inputs = []; + if ($this->configuration_repository->isCreateModeSimple($environment)) { + $inputs['question_text'] = $if->field()->textarea( + $this->lng->txt('question_text') + )->withRequired(true); + } + + return $environment->getPresentationFactory()->getEditForm( + $if->field()->section( + $inputs + [ + 'type' => $if->field()->select( + $this->lng->txt('select_answer_form_type'), + $this->answer_form_factory->getAnswerFormTypesArrayForSelect($this->lng) + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): ?Definition => $this->answer_form_factory + ->buildTypeDefinitionFromSelectValue($v) + ) + ) + ], + $this->lng->txt('create_answer_form') + ), + $environment->getUrlBuilder(), + null, + false + ); + } + + /** + * + * @param string|array<\ILIAS\Data\UUID\Uuid> $question_ids + * @return array<\ILIAS\UI\Component\Modal\InterruptiveItem\Standard> + */ + private function buildAffectedItems( + string|array $question_ids + ): array { + $questions = $question_ids === 'ALL_OBJECTS' + ? $this->questions_repository->getQuestionDataOnlyForAllQuestions() + : $this->questions_repository->getQuestionDataOnlyForQuestionIds($question_ids); + $affected_items = []; + foreach ($questions as $question) { + $affected_items[] = $this->ui_factory->modal()->interruptiveItem()->standard( + $question->getId()->toString(), + $question->getTitle() + ); + } + return $affected_items; + } + + private function checkCapabilities( + array $capabilities + ): void { + foreach ($capabilities as $capability) { + if (!$this->questions_repository->capabilityExists($capability)) { + throw new \InvalidArgumentException( + 'All provided capabilities must implement ' + . 'ILIAS\Questions\AnswerForm\Capabilities\Capability.' + ); + } + } + } + + private function deleteSelectedQuestions( + array $question_ids + ): void { + $questions_to_delete = []; + foreach ($this->questions_repository->getForQuestionIds($question_ids) as $question) { + if (count($questions_to_delete) < 100) { + $questions_to_delete[] = $question; + continue; + } + + $this->questions_repository->delete($questions_to_delete); + $questions_to_delete = []; + } + + $this->questions_repository->delete($questions_to_delete); + } + + private function buildEnvironment( + URI $base_uri, + int $obj_id + ): EnvironmentImplementation { + return new EnvironmentImplementation( + $this->ctrl, + $this->http, + $this->refinery, + $this->lng, + $this->tabs_gui, + $this->uuid_factory, + $this->definitions_factory, + $this->editability, + $base_uri, + $obj_id + ); + } + + private function buildAfterQuestionCreationRedirectUri( + EnvironmentImplementation $environment, + CreateModes $create_mode, + Uuid $question_uuid + ): string { + if ($create_mode !== CreateModes::Simple) { + return $environment + ->withDefaultStep() + ->withActionParameter(self::ACTION_EDIT_QUESTION) + ->withQuestionIdParameter($question_uuid) + ->getUrlBuilder() + ->buildURI() + ->__toString(); + } + + $environment->setParamtersForSimpleCreateCmd($question_uuid); + + return $this->ctrl->getLinkTargetByClass( + [ + \QstsQuestionPageGUI::class, + \ilPageEditorGUI::class, + \ilPCAnswerFormGUI::class + ], + 'insert' + ); + } + + private function buildAfterAnswerFormCreationRedirectUri( + EnvironmentImplementation $environment, + ): string { + if (!$this->configuration_repository->isCreateModeSimple($environment)) { + return $this->ctrl->getLinkTargetByClass( + \QstsQuestionPageGUI::class, + 'edit' + ); + } + + $additonal_data = $this->global_screen + ->tool() + ->context() + ->current() + ->getAdditionalData(); + + if ($environment->isCreateAndNewAction()) { + return $additonal_data + ->get(LayoutProvider::URL_CREATE_QUESTION) + ->__toString(); + } + + return $additonal_data + ->get(LayoutProvider::URL_CLOSE_MODE_INFO) + ->__toString(); + } +} diff --git a/components/ILIAS/Questions/src/PublicInterface.php b/components/ILIAS/Questions/src/PublicInterface.php new file mode 100644 index 000000000000..85370e839b29 --- /dev/null +++ b/components/ILIAS/Questions/src/PublicInterface.php @@ -0,0 +1,43 @@ +collector; + } + + public function getEditPresentation(): Edit + { + return $this->edit_presentation; + } +} diff --git a/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php b/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php new file mode 100644 index 000000000000..43184f62a9f7 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Definitions/Lifecycle.php @@ -0,0 +1,31 @@ +answer_forms = array_reduce( + $answer_forms, + function (array $c, AnswerFormProperties $v): array { + $c[$v->getAnswerFormId()->toString()] = $v; + return $c; + }, + [] + ); + } + + public function getId(): ?Uuid + { + return $this->id; + } + + public function getParentObjId(): int + { + return $this->parent_obj_id; + } + + public function withParentObjId( + int $parent_obj_id + ): self { + $clone = clone $this; + $clone->parent_obj_id = $parent_obj_id; + $clone->linking_information_updated = true; + return $clone; + } + + public function withPosition( + int $position + ): self { + $clone = clone $this; + $clone->position = $position; + $clone->linking_information_updated = true; + return $clone; + } + + public function getPageId(): ?int + { + return $this->page_id; + } + + public function withPageId( + int $page_id + ): self { + $clone = clone $this; + $clone->page_id = $page_id; + $clone->page_id_updated = true; + return $clone; + } + + public function getTitle(): string + { + return $this->title; + } + + public function withTitle( + string $title + ): self { + $clone = clone $this; + $clone->title = $title; + $clone->self_updated = true; + return $clone; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function withAuthor( + string $author + ): self { + $clone = clone $this; + $clone->author = $author; + $clone->self_updated = true; + return $clone; + } + + public function getLifecycle(): Lifecycle + { + return $this->lifecycle; + } + + public function withLifecycle( + Lifecycle $lifecycle + ): self { + $clone = clone $this; + $clone->lifecycle = $lifecycle; + $clone->self_updated = true; + return $clone; + } + + public function getRemarks(): string + { + return $this->remarks; + } + + public function withRemarks( + string $remarks + ): self { + $clone = clone $this; + $clone->remarks = $remarks; + $clone->self_updated = true; + return $clone; + } + + public function getOriginalId(): ?Uuid + { + return $this->original_id; + } + + public function withOriginalId( + Uuid $original_id + ): self { + $clone = clone $this; + $clone->original_id = $original_id; + $clone->self_updated = true; + return $clone; + } + + public function getLastUpdate(): ?\DateTimeImmutable + { + return $this->last_update; + } + + public function getCreated(): ?\DateTimeImmutable + { + return $this->created; + } + + public function getAnswerFormProperties(): array + { + return $this->answer_forms; + } + + public function getAnswerFormPropertiesByIdString( + string $form_id + ): ?AnswerFormProperties { + return $this->answer_forms[$form_id] ?? null; + } + + public function withAnswerFormProperties( + AnswerFormProperties $answer_form + ): self { + $clone = clone $this; + $clone->answer_forms[$answer_form->getAnswerFormId()->toString()] = $answer_form; + $clone->updated_answer_forms[] = $answer_form; + return $clone; + } + + public function withoutDeletedAnswerForms( + array $found_answer_form_ids + ): self { + $clone = clone $this; + foreach (array_keys($this->answer_forms) as $answer_form_id) { + if (!in_array($answer_form_id, $found_answer_form_ids)) { + $clone->deleted_answer_forms[] = $clone->answer_forms[$answer_form_id]; + unset($clone->answer_forms[$answer_form_id]); + } + } + + return $clone; + } + + /** + * Checks whether the question is a clone of another question or not + */ + public function isClone(): bool + { + return $this->original_id !== null; + } + + public function getCreateMode(): ?CreateModes + { + return $this->create_mode; + } + + public function withCreateMode( + CreateModes $create_mode + ): self { + $clone = clone $this; + $clone->create_mode = $create_mode; + return $clone; + } + + public function getEditView( + Language $lng, + ConfigurationRepository $configuration_repository, + \ilObjUser $current_user, + UIFactory $ui_factory, + Refinery $refinery, + RequestInterface $request, + \ilCtrl $ctrl + ): Views\Edit { + return new Views\Edit( + $lng, + $configuration_repository, + $current_user, + $ui_factory, + $refinery, + $request, + $ctrl, + $this + ); + } + + #[\Override] + public function getParticipantView(): Views\Participant + { + return new Views\Participant( + $this + ); + } + + public function toEditLink( + LinkFactory $link_factory, + EnvironmentImplementation $environment + ): StandardLink { + return $link_factory->standard( + $this->title, + $environment->withQuestionIdParameter($this->id) + ->getUrlBuilder() + ->buildURI() + ->__toString() + ); + } + + public function toTableRow( + DataRowBuilder $row_builder, + UIFactory $ui_factory, + EnvironmentImplementation $environment + ): DataRow { + return $row_builder->buildDataRow( + $this->id->toString(), + [ + 'title' => $ui_factory->link()->standard( + $this->title, + $environment->withQuestionIdParameter( + $this->id + )->getUrlBuilder() + ->buildURI() + ->__toString() + ) + ] + ); + } + + public function toStorage( + Manipulate $manipulate + ): Manipulate { + return $manipulate->getManipulationType() === ManipulationType::Create + ? $this->addInsertStatementsToManipulation($manipulate) + : $this->addUpdateStatementsToManipulation($manipulate); + } + + public function toDelete( + Manipulate $manipulate + ): Manipulate { + return $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate->withAdditionalStatement( + $this->buildDeleteQuestionStatement( + $manipulate->getPersistenceFactory() + ) + )->withAdditionalStatement( + $this->buildDeleteLinkingStatement( + $manipulate->getPersistenceFactory() + ) + )->withAdditionalStatement( + $this->buildDeleteMigrationStatement( + $manipulate->getPersistenceFactory() + ) + ), + $this->answer_forms + ); + } + + private function addInsertStatementsToManipulation( + Manipulate $manipulate + ): Manipulate { + if ($this->created === null) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildInsertLinkingStatement( + $manipulate->getPersistenceFactory() + ) + )->withAdditionalStatement( + $this->buildInsertQuestionStatement( + $manipulate->getPersistenceFactory() + ) + ); + } + + if ($this->updated_answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + if ($this->answer_forms !== []) { + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->answer_forms + ); + } + + return $manipulate; + } + + private function addUpdateStatementsToManipulation( + Manipulate $manipulate + ): Manipulate { + if ($this->linking_information_updated) { + $manipulate = $manipulate + ->withAdditionalStatement( + $this->buildUpdateLinkingStatement() + ); + } + + if ($this->self_updated) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdateQuestionStatement( + $manipulate->getPersistenceFactory() + ) + ); + } + + if ($this->page_id) { + $manipulate = $manipulate->withAdditionalStatement( + $this->buildUpdatePageIdStatement( + $manipulate->getPersistenceFactory() + ) + ); + } + + if ($this->deleted_answer_forms !== []) { + $manipulate = $this->addDeleteAnswerFormsStatementsToManipulate( + $manipulate, + $this->deleted_answer_forms + ); + } + + return $this->addAnswerFormStatementsToManipulate( + $manipulate, + $this->updated_answer_forms + ); + } + + private function addAnswerFormStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms + ): Manipulate { + return array_reduce( + $answer_forms, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toStorage( + $v->getTypeGenericProperties()->toStorage($c) + ), + $manipulate + ); + } + + private function addDeleteAnswerFormsStatementsToManipulate( + Manipulate $manipulate, + array $answer_forms_to_delete + ): Manipulate { + return array_reduce( + $answer_forms_to_delete, + fn(Manipulate $c, AnswerFormProperties $v): Manipulate => $v->toDelete( + $v->getTypeGenericProperties()->toDelete($c) + ), + $manipulate + ); + } + + + + private function buildInsertLinkingStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::Linking->getColumns( + $persistence_factory + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) + ] + ); + } + + private function buildInsertQuestionStatement( + PersistenceFactory $persistence_factory + ): Insert { + return $persistence_factory->insert( + CoreTables::Questions->getColumns( + $persistence_factory + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ] + ); + } + + private function buildUpdateLinkingStatement( + PersistenceFactory $persistence_factory + ): Update { + $linking_table_definition = CoreTables::Linking; + return $persistence_factory->update( + $linking_table_definition->getColumns( + $persistence_factory, + [CoreTables::LINKING_TABLE_ID_COLUMN] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->parent_obj_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + $this->position + ) + ], + [ + $persistence_factory->where( + $linking_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildUpdateQuestionStatement( + PersistenceFactory $persistence_factory + ): Update { + $questions_table_definition = CoreTables::Questions; + return $persistence_factory->update( + $questions_table_definition->getColumns( + $persistence_factory, + [ + CoreTables::ANSWER_FORM_TABLE_ID_COLUMN, + 'page_id', + 'created' + ] + ), + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->title + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->author + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->lifecycle->value + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->remarks + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->original_id?->toString() + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ], + [ + $persistence_factory->where( + $questions_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildDeleteQuestionStatement( + PersistenceFactory $persistence_factory + ): Delete { + $table_definition = CoreTables::Questions; + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), + [ + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + private function buildDeleteLinkingStatement( + PersistenceFactory $persistence_factory + ): Delete { + $table_definition = CoreTables::Linking; + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), + [ + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-86: This we only need while the migrations exist, after + * this MUST go! + */ + private function buildDeleteMigrationStatement( + PersistenceFactory $persistence_factory + ): Delete { + $table_definition = CoreTables::MigrationsTable; + return $persistence_factory->delete( + $table_definition->getTable($persistence_factory), + [ + $persistence_factory->where( + $table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } + + /** + * @todo skergomard, 2026-01-26: This we only need while the migrations exist, after + * this a question MUST never change the page assigned to it after its creation! + */ + private function buildUpdatePageIdStatement( + PersistenceFactory $persistence_factory + ): Update { + $questions_table_definition = CoreTables::Questions; + return $persistence_factory->update( + [ + $persistence_factory->column( + $questions_table_definition->getTable($persistence_factory), + 'page_id' + ), + $persistence_factory->column( + $questions_table_definition->getTable($persistence_factory), + 'last_update' + ) + ], + [ + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->page_id + ), + $persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ) + ], + [ + $persistence_factory->where( + $questions_table_definition->getIdColumn( + $persistence_factory + ), + $persistence_factory->value( + \ilDBConstants::T_TEXT, + $this->id->toString() + ) + ) + ] + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Response.php b/components/ILIAS/Questions/src/Question/Response.php new file mode 100644 index 000000000000..3bba6d1e8643 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Response.php @@ -0,0 +1,25 @@ +getStep()) { + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesCreateForm( + $environment + ), + default => $this->buildBasicPropertiesCreateForm($environment) + }; + } + + public function edit( + EnvironmentImplementation $environment, + Participant $participant_view + ): EditForm|Question { + return match ($environment->getStep()) { + self::CMD_SAVE_QUESTION => $this->processBasicPropertiesEditingForm( + $environment + ), + default => $this->buildBasicPropertiesEditingForm($environment) + ->withContentAfterForm( + $this->buildPreviewPanel($environment, $participant_view) + ) + }; + } + + private function buildBasicPropertiesCreateForm( + EnvironmentImplementation $environment + ): EditForm { + $ff = $this->ui_factory->input()->field(); + + $inputs = [ + 'question' => $ff->group( + $this->buildBasicPropertiesInputs() + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() + ) + ]; + + if ($this->configuration_repository->isCreateModeChangeableByUser()) { + $inputs['create_mode'] = $this->configuration_repository->getInputForCreateMode( + $ff, + $this->lng, + $this->refinery + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn(string $v): CreateModes => CreateModes::tryFrom($v) + ?? $this->configuration_repository->getGlobalCreateMode() + ) + ); + } + + return $environment->getPresentationFactory()->getEditForm( + $ff->section( + $inputs, + $this->lng->txt('edit_basic_form_properties') + ), + $environment + ->withStepParameter(self::CMD_SAVE_QUESTION) + ->getUrlBuilder(), + null, + false + ); + } + + private function processBasicPropertiesCreateForm( + EnvironmentImplementation $environment + ): EditForm|Question { + $form = $this->buildBasicPropertiesCreateForm( + $environment + )->withRequest($this->request); + + $data = $form->getData(); + return $data === null + ? $form + : $data['question']->withCreateMode($data['create_mode']); + } + + private function buildBasicPropertiesEditingForm( + EnvironmentImplementation $environment + ): EditForm { + return $environment->getPresentationFactory()->getEditForm( + $this->ui_factory->input()->field()->section( + $this->buildBasicPropertiesInputs(), + $this->lng->txt('edit_basic_form_properties') + )->withAdditionalTransformation( + $this->buildAddBasicPropertiesToQuestionTrafo() + )->withValue( + $this->buildBasicPropertiesBasicValuesArray() + ), + $environment + ->withStepParameter(self::CMD_SAVE_QUESTION) + ->getUrlBuilder(), + null, + true + ); + } + + private function processBasicPropertiesEditingForm( + EnvironmentImplementation $environment + ): EditForm|Question { + $form = $this->buildBasicPropertiesEditingForm( + $environment + )->withRequest($this->request); + + $data = $form->getData(); + return $data === null + ? $form + : $data; + } + + private function buildBasicPropertiesInputs(): array + { + $ff = $this->ui_factory->input()->field(); + + return [ + 'title' => $ff->text($this->lng->txt('title')) + ->withRequired(true), + 'author' => $ff->text($this->lng->txt('author')), + 'lifecycle' => $ff->select( + $this->lng->txt('qst_lifecycle'), + array_reduce( + Lifecycle::cases(), + function (array $c, Lifecycle $v): array { + $c[$v->value] = $this->lng->txt("qst_lifecycle_{$v->value}"); + return $c; + }, + [] + ) + )->withRequired(true), + 'remarks' => $ff->textarea($this->lng->txt('qst_remarks')) + ]; + } + + private function buildBasicPropertiesBasicValuesArray(): array + { + return [ + 'title' => $this->question->getTitle(), + 'author' => $this->question->getAuthor() !== '' + ? $this->question->getAuthor() + : $this->current_user->getFullname(), + 'lifecycle' => $this->question->getLifecycle()->value, + 'remarks' => $this->question->getRemarks() + ]; + } + + private function buildAddBasicPropertiesToQuestionTrafo(): Transformation + { + return $this->refinery->custom()->transformation( + function (array $vs): QuestionImplementation { + $question = $this->question + ->withTitle($vs['title']) + ->withAuthor($vs['author']) + ->withRemarks($vs['remarks']); + + $lifecycle = Lifecycle::tryFrom($vs['lifecycle']); + if ($lifecycle !== null) { + return $question->withLifecycle($lifecycle); + } + + return $question; + } + ); + } + + private function buildPreviewPanel( + EnvironmentImplementation $environment, + Participant $participant_view + ): StandardPanel { + $environment->preserveParametersForPageEditorCmds(); + return $this->ui_factory->panel()->standard( + $this->lng->txt('preview'), + $this->ui_factory->legacy()->content( + $participant_view->get($environment->getObjId()) + ) + )->withActions( + $this->ui_factory->dropdown()->standard([ + $this->ui_factory->link()->standard( + $this->lng->txt('edit'), + $this->ctrl->getLinkTargetByClass(\QstsQuestionPageGUI::class, 'edit') + ) + ]) + ); + } +} diff --git a/components/ILIAS/Questions/src/Question/Views/Participant.php b/components/ILIAS/Questions/src/Question/Views/Participant.php new file mode 100644 index 000000000000..f36881863239 --- /dev/null +++ b/components/ILIAS/Questions/src/Question/Views/Participant.php @@ -0,0 +1,113 @@ +question->getAnswerFormProperties() as $form) { + if (!$form->getType()->isAsyncPresentationAvailable()) { + throw \Exception('This QuestionType has no async presentation.'); + } + } + $clone = clone $this; + $clone->async = $async; + return $clone; + } + + public function withIsInteractive( + bool $interactive + ): self { + $clone = clone $this; + $clone->interactive = $interactive; + return $clone; + } + + public function withShowMarks( + bool $show_marks + ): self { + foreach ($this->question->getAnswerFormProperties() as $form) { + if (!$form->getType()->isMarkable()) { + throw \Exception('This QuestionType cannot be marked.'); + } + } + + $clone = clone $this; + $clone->show_marks = $show_marks; + return $clone; + } + + public function withShowCorrectSolution( + bool $show_correct_solution + ): self { + foreach ($this->question->getAnswerFormProperties() as $form) { + if (!$form->getType()->isMarkable()) { + throw \Exception('This QuestionType cannot be marked.'); + } + } + + $clone = clone $this; + $clone->show_correct_solution = $show_correct_solution; + return $clone; + } + + public function get( + int $obj_id + ): string { + $tpl = new \ilTemplate( + 'tpl.qpl_question_preview.html', + true, + true, + 'components/ILIAS/TestQuestionPool' + ); + + $tpl->setVariable( + 'PREVIEW_FORMACTION', + '' + ); + + $question_page = new \QstsQuestionPageGUI( + $this->question, + $obj_id + ); + $question_page->setPresentationTitle($this->question->getTitle()); + + $tpl->setVariable( + 'QUESTION_OUTPUT', + $question_page->presentation() + ); + return $tpl->get(); + } +} diff --git a/components/ILIAS/Questions/src/Response/Repository.php b/components/ILIAS/Questions/src/Response/Repository.php new file mode 100644 index 000000000000..01e2ff350f1b --- /dev/null +++ b/components/ILIAS/Questions/src/Response/Repository.php @@ -0,0 +1,37 @@ +persistence_factory, + $this->answer_form_migrations + ) + ]; + } + + #[\Override] + public function getBuildObjective(): Objective + { + return new NullObjective(); + } + + #[\Override] + public function getInstallObjective( + ?Config $config = null + ): Objective { + return new NullObjective(); + } + + + #[\Override] + public function getNamedObjectives( + ?Config $config = null + ): array { + return new NullObjective(); + } +} diff --git a/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php new file mode 100644 index 000000000000..2ca3f8714441 --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/ClozeQuestionTables.php @@ -0,0 +1,291 @@ +db = $db; + } + + public function step_1(): void + { + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::TypeSpecificAnswerForms); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'scoring_identical_responses' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => true + ], + 'combinations_enabled' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addPrimaryKey($table_name, ['answer_form_id']); + } + } + + public function step_2(): void + { + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerInputs); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => true + ], + 'gap_type' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => true + ], + 'max_chars' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'step_size' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'text_matching_method' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 32, + 'notnull' => false + ], + 'min_autocomplete' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'shuffle_answer_options' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addIndex($table_name, ['answer_form_id'], 'af'); + } + } + + public function step_3(): void + { + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::AnswerOptions); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_input_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => true + ], + 'text_value' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => false + ], + 'lower_limit' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'upper_limit' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_input_id'])) { + $this->db->addIndex($table_name, ['answer_input_id'], 'ai'); + } + } + + public function step_4(): void + { + $table_name = $this->table_name_builder->getTableNameFor( + TableTypes::Additional, + Persistence::COMBINATION_TABLE_IDENTIFIER + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_form_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_form_id'])) { + $this->db->addIndex($table_name, ['answer_form_id'], 'ai'); + } + } + + public function step_5(): void + { + $table_name = $this->table_name_builder->getTableNameFor( + TableTypes::Additional, + Persistence::COMBINATION_TO_ANSWER_OPTIONS_TABLE_IDENTIFIER + ); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'combination_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'gap_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_option_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'in_range' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 16, + 'notnull' => false + ] + ]); + } + + if (!$this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id'])) { + $this->db->addPrimaryKey($table_name, ['combination_id', 'gap_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['combination_id'])) { + $this->db->addIndex($table_name, ['combination_id'], 'ci'); + } + } + + public function step_6(): void + { + $table_name = $this->table_name_builder->getTableNameFor(TableTypes::Responses); + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'answer_input_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'selected_answer_option' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => false + ], + 'text' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['answer_input_id'])) { + $this->db->addIndex($table_name, ['answer_input_id'], 'ai'); + } + } +} diff --git a/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php new file mode 100644 index 000000000000..57775096067b --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/OverarchingQuestionTables.php @@ -0,0 +1,244 @@ +db = $db; + } + + public function step_1(): void + { + $table_name = CoreTables::Questions->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'page_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => true + ], + 'title' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 512, + 'notnull' => true + ], + 'author' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 512, + 'notnull' => false + ], + 'lifecycle' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 16, + 'notnull' => true + ], + 'remarks' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => false + ], + 'original_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => false + ], + 'last_update' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], + 'created' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 8, + 'notnull' => true + ], + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + } + + public function step_2(): void + { + $table_name = CoreTables::AnswerForms->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'type' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 4000, + 'notnull' => true + ], + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'available_points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ], + 'image_size' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ], + 'shuffle_answer_options' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => false + ], + 'additional_text' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ], + 'additional_text_legacy' => [ + 'type' => \ilDBConstants::T_CLOB, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['question_id'])) { + $this->db->addIndex($table_name, ['question_id'], 'q'); + } + } + + public function step_3(): void + { + $table_name = CoreTables::Responses->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'reached_points' => [ + 'type' => \ilDBConstants::T_FLOAT, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['id'])) { + $this->db->addPrimaryKey($table_name, ['id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['question_id'])) { + $this->db->addIndex($table_name, ['question_id'], 'q'); + } + } + + public function step_4(): void + { + $table_name = CoreTables::Linking->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'obj_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => true + ], + 'position' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 2, + 'notnull' => false + ] + ]); + } + + if (!$this->db->primaryExistsByFields($table_name, ['question_id'])) { + $this->db->addPrimaryKey($table_name, ['question_id']); + } + + if (!$this->db->indexExistsByFields($table_name, ['obj_id'])) { + $this->db->addIndex($table_name, ['obj_id'], 'o'); + } + } + + public function step_5(): void + { + $table_name = CoreTables::MigrationsTable->value; + if (!$this->db->tableExists($table_name)) { + $this->db->createTable($table_name, [ + 'new_question_id' => [ + 'type' => \ilDBConstants::T_TEXT, + 'length' => 64, + 'notnull' => true + ], + 'old_question_id' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 4, + 'notnull' => false + ], + 'success' => [ + 'type' => \ilDBConstants::T_INTEGER, + 'length' => 1, + 'notnull' => true + ] + ]); + } + + if (!$this->db->primaryExistsByFields( + $table_name, + ['old_question_id'] + )) { + $this->db->addPrimaryKey( + $table_name, + ['old_question_id'] + ); + } + } +} diff --git a/components/ILIAS/Questions/src/Setup/QuestionsMigration.php b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php new file mode 100644 index 000000000000..6f9e9a9265ee --- /dev/null +++ b/components/ILIAS/Questions/src/Setup/QuestionsMigration.php @@ -0,0 +1,423 @@ +answer_form_migrations = array_reduce( + $answer_form_migrations, + function (array $c, AnswerFormMigration $v): array { + $c[$v->getOldQuestionIdentifier()] = $v; + return $c; + }, + [] + ); + } + + #[\Override] + public function getLabel(): string + { + return 'Migrate questions to Questions Component.'; + } + + #[\Override] + public function getDefaultAmountOfStepsPerRun(): int + { + return 100; + } + + #[\Override] + public function getPreconditions(Environment $environment): array + { + return [new \ilDatabaseInitializedObjective()]; + } + + #[\Override] + public function prepare(Environment $environment): void + { + $this->db = $environment->getResource(Setup\Environment::RESOURCE_DATABASE); + $this->io = $environment->getResource(Setup\Environment::RESOURCE_ADMIN_INTERACTION); + $this->uuid_factory = new UuidFactory(); + } + + #[\Override] + public function step( + Environment $environment + ): void { + $db_values = $this->fetchValidRecord(); + + if ($db_values === null) { + return; + } + + if ($db_values->obj_fi === 0) { + $db_values->obj_fi = $this->getObjIdFromLearningModulMapping($db_values->question_id); + } + + if ($db_values->obj_fi === null) { + $this->io->error( + "The question with the id {$db_values->question_id} could not be " + . "migrated as it doesn't belong to any object." + ); + return; + } + + /** @var \ILIAS\Questions\AnswerForm\Migration\Migration $answer_form_migration */ + $answer_form_migration = $this->answer_form_migrations[$db_values->type_tag]; + + $new_question_id = $this->uuid_factory->uuid4(); + + $migration_insert = $answer_form_migration->completeMigrationInsert( + $environment, + $this->buildMigrationInsert( + $answer_form_migration, + [ + $this->buildInsertLinkingStatement( + $new_question_id, + $db_values->obj_fi, + $db_values->sequence + ), + $this->buildInsertQuestionStatement( + $new_question_id, + $db_values->title, + $db_values->author, + Lifecycle::tryFrom($db_values->lifecycle), + $db_values->description, + $db_values->original_id, + $db_values->created + ), + $this->buildInsertMigrationStatement( + $db_values->question_id, + $new_question_id + ) + ], + $new_question_id, + $db_values + ) + ); + + if ($migration_insert === null) { + $this->db->manipulate( + $this->buildInsertMigrationStatement( + $db_values->question_id, + null + )->toManipulateString($this->db) + ); + $this->io->inform( + "{$db_values->question_id} could not be migrated due to missing question data." + ); + return; + } + + $migration_insert->run(); + $this->io->inform("{$new_question_id->toString()} successfully migrated."); + } + + #[\Override] + public function getRemainingAmountOfSteps(): int + { + $query = $this->db->query( + 'SELECT COUNT(question_id) cnt FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL + . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'WHERE t.type_tag IN (' + . implode( + ', ', + array_map( + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + $this->answer_form_migrations + ) + ) . ')' . PHP_EOL + . 'AND q.complete = 1' . PHP_EOL + . 'AND m.old_question_id IS NULL' + ); + return $this->db->fetchObject( + $query + )->cnt; + } + + private function fetchValidRecord(): ?\stdClass + { + $query = $this->db->query( + 'SELECT q.*, t.type_tag, s.sequence FROM ' . self::OLD_QUESTIONS_TABLE . ' q' . PHP_EOL + . 'JOIN ' . self::OLD_QUESTION_TYPE_TABLE . ' t ON q.question_type_fi = t.question_type_id' . PHP_EOL + . 'LEFT JOIN ' . CoreTables::MigrationsTable->value . ' m ON q.question_id = m.old_question_id' . PHP_EOL + . 'LEFT JOIN ' . self::TEST_QUESTIONS_SEQUENCE_TABLE . ' s ON q.question_id = s.question_fi' . PHP_EOL + . 'WHERE t.type_tag IN (' + . implode( + ', ', + array_map( + fn(AnswerFormMigration $v): string => "'{$v->getOldQuestionIdentifier()}'", + $this->answer_form_migrations + ) + ) . ')' . PHP_EOL + . 'AND q.complete = 1' . PHP_EOL + . 'AND m.old_question_id IS NULL' + ); + + do { + $db_values = $this->db->fetchObject($query); + if ($db_values === null) { + return null; + } + } while (!$this->areDbValuesValid($db_values)); + + $db_values->original_id = $this->cleanupAndMigrateOriginalId($db_values->original_id); + return $db_values; + } + + private function areDbValuesValid( + \stdClass $db_values + ): bool { + if ($db_values->original_id === null) { + return true; + } + + if ($this->allready_migrated_questions === null) { + $this->loadAlreadyMigratedQuestions(); + } + + if (isset($this->allready_migrated_questions[$db_values->original_id])) { + return true; + } + + return false; + } + + private function cleanupAndMigrateOriginalId( + ?int $original_id + ): ?Uuid { + if ($original_id === null + || in_array($original_id, $this->allready_migrated_questions_in_qpls)) { + return null; + } + return $this->uuid_factory->fromString( + $this->allready_migrated_questions[$original_id] + ); + } + + private function loadAlreadyMigratedQuestions(): void + { + $query = $this->db->query( + 'SELECT m.*, o.type FROM ' . CoreTables::MigrationsTable->value . ' m' . PHP_EOL + . 'JOIN ' . CoreTables::Linking->value . ' l' . PHP_EOL + . 'ON m.new_question_id = l.question_id' . PHP_EOL + . 'JOIN object_data o ON l.obj_id = o.obj_id' . PHP_EOL + ); + + $this->allready_migrated_questions = []; + $this->allready_migrated_questions_in_qpls = []; + while (($row = $this->db->fetchObject($query)) !== null) { + $this->allready_migrated_questions[$row->old_question_id] = $row->new_question_id; + if ($row->type === 'qpl') { + $this->allready_migrated_questions_in_qpls[] = $row->new_question_id; + } + } + } + + private function getObjIdFromLearningModulMapping( + int $question_id + ): ?int { + if ($this->question_to_learning_module_mapping === null) { + $this->loadQuestionsToLearningModuleMapping(); + } + + return $this->question_to_learning_module_mapping[$question_id] ?? null; + } + + private function loadQuestionsToLearningModuleMapping(): void + { + + $query = $this->db->query( + 'SELECT question_id, obj_id FROM page_question pq' . PHP_EOL + . 'JOIN page_object po ON pq.page_id = po.page_id' . PHP_EOL + . 'AND pq.page_parent_type = po.parent_type' . PHP_EOL + . 'JOIN object_data o ON po.parent_id = o.obj_id' . PHP_EOL + . 'WHERE page_parent_type = "lm"' + ); + + $this->question_to_learning_module_mapping = []; + while (($row = $this->db->fetchObject($query)) !== null) { + $this->question_to_learning_module_mapping[$row->question_id] = $row->obj_id; + } + } + + private function buildInsertLinkingStatement( + Uuid $new_question_id, + int $obj_id, + ?int $position + ): Insert { + return $this->persistence_factory->insert( + CoreTables::Linking->getColumns( + $this->persistence_factory + ), + [ + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $new_question_id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $obj_id + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $position + ) + ] + ); + } + + private function buildInsertQuestionStatement( + Uuid $id, + string $title, + string $author, + Lifecycle $lifecycle, + string $remarks, + ?Uuid $original_id, + int $create_date + ): Insert { + return $this->persistence_factory->insert( + CoreTables::Questions->getColumns( + $this->persistence_factory + ), + [ + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $id->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + 0 + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $title + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $author + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $lifecycle->value + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $remarks + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $original_id?->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + time() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $create_date + ) + ] + ); + } + + private function buildInsertMigrationStatement( + int $old_question_id, + ?Uuid $new_question_id + ): Insert { + return $this->persistence_factory->insert( + CoreTables::MigrationsTable->getColumns( + $this->persistence_factory + ), + [ + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $old_question_id + ), + $this->persistence_factory->value( + \ilDBConstants::T_TEXT, + $new_question_id?->toString() + ), + $this->persistence_factory->value( + \ilDBConstants::T_INTEGER, + $new_question_id === null + ? '0' + : '1' + ) + ] + ); + } + + private function buildMigrationInsert( + AnswerFormMigration $answer_form_migration, + array $question_inserts, + Uuid $new_question_id, + \stdClass $db_values + ): AnswerFormMigrationInsert { + return new AnswerFormMigrationInsert( + $this->db, + $this->io, + $this->uuid_factory, + $this->persistence_factory, + new TableNameBuilder( + $answer_form_migration->getTableNameSpace() + ), + $question_inserts, + $db_values->question_id, + $new_question_id, + $this->uuid_factory->uuid4(), + $answer_form_migration->getDefinitionClass(), + $db_values->add_cont_edit_mode === \assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE + ); + } +} diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php b/components/ILIAS/Questions/src/Units/Category.php similarity index 55% rename from components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php rename to components/ILIAS/Questions/src/Units/Category.php index 963c0d7637e5..30c808f621df 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php +++ b/components/ILIAS/Questions/src/Units/Category.php @@ -18,26 +18,27 @@ declare(strict_types=1); -/** - * Formula Question Unit Category - * @author Helmut Schottmüller - * @ingroup components\ILIASTestQuestionPool - */ -class assFormulaQuestionUnitCategory +namespace ILIAS\Questions\Units; + +use ILIAS\Language\Language; + +class Category { private int $id = 0; private string $category = ''; private int $question_fi = 0; - public function initFormArray(array $data): void - { + public function initFormArray( + array $data + ): void { $this->id = (int) $data['category_id']; $this->category = $data['category']; $this->question_fi = (int) $data['question_fi']; } - public function setId(int $id): void - { + public function setId( + int $id + ): void { $this->id = $id; } @@ -46,8 +47,9 @@ public function getId(): int return $this->id; } - public function setCategory(string $category): void - { + public function setCategory( + string $category + ): void { $this->category = $category; } @@ -56,13 +58,9 @@ public function getCategory(): string return $this->category; } - public function getSanitizedCategory(): string - { - return $this->sanitizeString($this->getCategory()); - } - - public function setQuestionFi(int $question_fi): void - { + public function setQuestionFi( + int $question_fi + ): void { $this->question_fi = $question_fi; } @@ -71,19 +69,14 @@ public function getQuestionFi(): int return $this->question_fi; } - public function getDisplayString(): string - { - global $DIC; - + public function getDisplayString( + Language $lng + ): string { $category = $this->getCategory(); - $txt = $DIC->language()->txt("qpl_qst_formulaquestion_{$category}"); - return strcmp("-qpl_qst_formulaquestion_{$category}-", $txt) !== 0 - ? $this->sanitizeString($txt) - : $this->getSanitizedCategory(); - } + if (strcmp('-qpl_qst_formulaquestion_' . $category . '-', $lng->txt('qpl_qst_formulaquestion_' . $category)) !== 0) { + $category = $lng->txt('qpl_qst_formulaquestion_' . $category); + } - private function sanitizeString(string $string): string - { - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); + return $category; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php b/components/ILIAS/Questions/src/Units/ConfigurationGUI.php similarity index 77% rename from components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php rename to components/ILIAS/Questions/src/Units/ConfigurationGUI.php index 2fdc9bc9fa5c..6e4694b0eabf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationGUI.php +++ b/components/ILIAS/Questions/src/Units/ConfigurationGUI.php @@ -18,33 +18,33 @@ declare(strict_types=1); +namespace ILIAS\Questions\Units; + use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; +use ILIAS\Language\Language; -/** - * Class ilUnitConfigurationGUI - */ -abstract class ilUnitConfigurationGUI +abstract class ConfigurationGUI { protected RequestDataCollector $request; - protected ?ilPropertyFormGUI $unit_cat_form = null; - protected ?ilPropertyFormGUI $unit_form = null; - protected ilGlobalTemplateInterface $tpl; - protected ilLanguage $lng; - protected ilCtrlInterface $ctrl; + protected ?\ilPropertyFormGUI $unit_cat_form = null; + protected ?\ilPropertyFormGUI $unit_form = null; public function __construct( - protected ilUnitConfigurationRepository $repository + protected readonly Repository $repository, + protected readonly Language $lng, + protected readonly \ilCtrl $ctrl, + protected readonly \ilRbacSystem $rbac_system, + protected readonly \ilGlobalTemplateInterface $tpl, + protected readonly \ilToolbarGUI $toolbar, + protected readonly \ilTabsGUI $tabs, + protected readonly \ilHelpGUI $help ) { - global $DIC; - $this->lng = $DIC->language(); - $this->ctrl = $DIC->ctrl(); - $this->tpl = $DIC->ui()->mainTemplate(); - $local_dic = QuestionPoolDIC::dic(); $this->request = $local_dic['request_data_collector']; $this->lng->loadLanguageModule('assessment'); + $this->tabs->activateTab('units'); } abstract protected function getDefaultCommand(): string; @@ -55,12 +55,16 @@ abstract public function isCRUDContext(): bool; abstract public function getUniqueId(): string; - abstract protected function showUnitCategories(array $categories): void; + abstract protected function showUnitCategories( + array $categories + ): void; - protected function getCategoryById(int $id, bool $for_CRUD = true): assFormulaQuestionUnitCategory - { + protected function getCategoryById( + int $id, + bool $for_CRUD = true + ): Category { $category = $this->repository->getUnitCategoryById($id); - if ($for_CRUD && $category->getQuestionFi() !== $this->repository->getConsumerId()) { + if ($for_CRUD && $category->getQuestionFi() !== $this->request->getQuestionId()) { $this->tpl->setOnScreenMessage('failure', $this->lng->txt('change_adm_categories_not_allowed'), true); $this->ctrl->redirect($this, $this->getDefaultCommand()); } @@ -72,18 +76,24 @@ protected function handleSubtabs(): void { } - protected function checkPermissions(string $cmd): void - { + protected function checkPermissions( + string $cmd + ): void { } public function executeCommand(): void { $cmd = $this->ctrl->getCmd($this->getDefaultCommand()); $this->checkPermissions($cmd); - match ($cmd) { - 'confirmImportGlobalCategories' => $this->$cmd($this->request->getUnitCategoryIds()), - default => $this->$cmd(), - }; + switch ($cmd) { + case 'confirmImportGlobalCategories': + $category_ids = $this->request->raw('category_ids'); + $this->$cmd($category_ids); + break; + default: + $this->$cmd(); + break; + } $this->handleSubtabs(); } @@ -98,13 +108,9 @@ protected function confirmDeleteUnit(): void $this->confirmDeleteUnits([$this->request->int('unit_id')]); } - /** - * @param int[]|null $unit_ids - * @return void - * @throws ilCtrlException - */ - protected function confirmDeleteUnits(?array $unit_ids = null): void - { + protected function confirmDeleteUnits( + ?array $unit_ids = null + ): void { if (!$this->isCRUDContext()) { $this->showUnitsOfCategory(); return; @@ -117,7 +123,7 @@ protected function confirmDeleteUnits(?array $unit_ids = null): void } $this->ctrl->setParameter($this, 'category_id', $this->request->int('category_id')); - $confirmation = new ilConfirmationGUI(); + $confirmation = new \ilConfirmationGUI(); $confirmation->setFormAction($this->ctrl->getFormAction($this, 'deleteUnits')); $confirmation->setConfirm($this->lng->txt('confirm'), 'deleteUnits'); $confirmation->setCancel($this->lng->txt('cancel'), 'showUnitsOfCategory'); @@ -131,14 +137,18 @@ protected function confirmDeleteUnits(?array $unit_ids = null): void continue; } - if ($check_result = $this->repository->checkDeleteUnit($unit->getId())) { - $errors[] = $unit->getDisplayString() . ' - ' . $check_result; + if (($check_result = $this->repository->checkDeleteUnit($unit->getId()))) { + $errors[] = $unit->getDisplayString($this->lng) . ' - ' . $check_result; continue; } - $confirmation->addItem('unit_ids[]', (string) $unit->getId(), $unit->getDisplayString()); + $confirmation->addItem( + 'unit_ids[]', + (string) $unit->getId(), + $unit->getDisplayString($this->lng) + ); ++$num_to_confirm; - } catch (ilException $e) { + } catch (\ilException $e) { continue; } } @@ -199,12 +209,12 @@ public function deleteUnits(): void $check_result = $this->repository->deleteUnit($unit->getId()); if (!is_null($check_result)) { - $errors[] = $unit->getDisplayString() . ' - ' . $check_result; + $errors[] = $unit->getDisplayString($this->lng) . ' - ' . $check_result; continue; } ++$num_deleted; - } catch (ilException $e) { + } catch (\ilException $e) { continue; } } @@ -254,8 +264,11 @@ protected function saveOrder(): void $sequences = $this->request->raw('sequence'); foreach ($sequences as $id => $sequence) { $sorting_value = str_replace(',', '.', $sequence); - $sorting_value = (int) $sorting_value * 100; - $this->repository->saveUnitOrder((int) $id, $sorting_value); + $this->repository->saveUnitOrder( + $this->request->getQuestionId(), + (int) $id, + (int) $sorting_value * 100 + ); } $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully')); @@ -283,7 +296,10 @@ protected function saveUnit(): void $unit->setFactor((float) $this->unit_form->getInput('factor')); $unit->setBaseUnit((int) $this->unit_form->getInput('base_unit') !== $unit->getId() ? (int) $this->unit_form->getInput('base_unit') : 0); $unit->setCategory($category->getId()); - $this->repository->saveUnit($unit); + $this->repository->saveUnit( + $this->request->getQuestionId(), + $unit + ); $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully')); $this->showUnitsOfCategory(); @@ -325,17 +341,24 @@ protected function addUnit(): void $category = $this->getCategoryById($this->request->int('category_id')); $this->initUnitForm($category); + $question_id = $this->request->getQuestionId(); if ($this->unit_form->checkInput()) { - $unit = new assFormulaQuestionUnit(); + $unit = new Unit(); $unit->setUnit($this->unit_form->getInput('unit_title')); $unit->setCategory($category->getId()); - $this->repository->createNewUnit($unit); + $this->repository->createNewUnit( + $question_id, + $unit + ); $unit->setBaseUnit((int) $this->unit_form->getInput('base_unit')); $unit->setFactor((float) $this->unit_form->getInput('factor')); - $this->repository->saveUnit($unit); + $this->repository->saveUnit( + $question_id, + $unit + ); $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully')); $this->showUnitsOfCategory(); @@ -366,54 +389,57 @@ protected function showUnitCreationForm(): void } protected function initUnitForm( - ?assFormulaQuestionUnitCategory $category = null, - ?assFormulaQuestionUnit $unit = null - ): ilPropertyFormGUI { - if ($this->unit_form instanceof ilPropertyFormGUI) { + ?Category $category = null, + ?Unit $unit = null + ): \ilPropertyFormGUI { + if ($this->unit_form instanceof \ilPropertyFormGUI) { return $this->unit_form; } $unit_in_use = false; - if ($unit instanceof assFormulaQuestionUnit && $this->repository->isUnitInUse($unit->getId())) { + if ($unit instanceof Unit && $this->repository->isUnitInUse($unit->getId())) { $unit_in_use = true; } - $this->unit_form = new ilPropertyFormGUI(); + $this->unit_form = new \ilPropertyFormGUI(); - $title = new ilTextInputGUI($this->lng->txt('unit'), 'unit_title'); + $title = new \ilTextInputGUI($this->lng->txt('unit'), 'unit_title'); $title->setDisabled($unit_in_use); $title->setRequired(true); $this->unit_form->addItem($title); - $baseunit = new ilSelectInputGUI($this->lng->txt('baseunit'), 'base_unit'); - $items = $this->repository->getCategorizedUnits(); + $baseunit = new \ilSelectInputGUI($this->lng->txt('baseunit'), 'base_unit'); + $items = $this->repository->getCategorizedUnits( + $this->request->getQuestionId() + ); $options = []; $category_name = ''; $new_category = false; foreach ($items as $item) { if ( - $unit instanceof assFormulaQuestionUnit && + $unit instanceof Unit && $unit->getId() === $item->getId() ) { continue; } - if ($item instanceof assFormulaQuestionUnitCategory) { - if ($category_name !== $item->getDisplayString()) { + if ($item instanceof Category) { + if ($category_name !== $item->getDisplayString($this->lng)) { $new_category = true; - $category_name = $item->getDisplayString(); + $category_name = $item->getDisplayString($this->lng); } continue; } - $options[$item->getId()] = $item->getDisplayString() . ($new_category ? ' (' . $category_name . ')' : ''); + $options[$item->getId()] = $item->getDisplayString($this->lng) + . ($new_category ? ' (' . $category_name . ')' : ''); $new_category = false; } $baseunit->setDisabled($unit_in_use); $baseunit->setOptions([0 => $this->lng->txt('no_selection')] + $options); $this->unit_form->addItem($baseunit); - $factor = new ilNumberInputGUI($this->lng->txt('factor'), 'factor'); + $factor = new \ilNumberInputGUI($this->lng->txt('factor'), 'factor'); $factor->setRequired(true); $factor->setSize(3); $factor->setMinValue(0); @@ -436,8 +462,8 @@ protected function initUnitForm( } $this->unit_form->setTitle(sprintf( $this->lng->txt('un_sel_cat_sel_unit'), - $category->getDisplayString(), - $unit->getDisplayString() + $category->getDisplayString($this->lng), + $unit->getDisplayString($this->lng) )); } $this->ctrl->clearParameterByClass(get_class($this), 'category_id'); @@ -448,37 +474,32 @@ protected function initUnitForm( protected function showUnitsOfCategory(): void { - global $DIC; - - $ilToolbar = $DIC->toolbar(); - $category = $this->getCategoryById($this->request->int('category_id'), false); $this->tpl->addJavaScript("assets/js/Basic.js"); $this->tpl->addJavaScript("assets/js/Form.js"); $this->lng->loadLanguageModule('form'); - $ilToolbar->addButton( + $this->toolbar->addButton( $this->lng->txt('back'), $this->ctrl->getLinkTarget($this, $this->getUnitCategoryOverviewCommand()) ); if ($this->isCRUDContext()) { $this->ctrl->setParameterByClass(get_class($this), 'category_id', $category->getId()); - $ilToolbar->addButton( + $this->toolbar->addButton( $this->lng->txt('un_add_unit'), $this->ctrl->getLinkTarget($this, 'showUnitCreationForm') ); $this->ctrl->clearParameterByClass(get_class($this), 'category_id'); } - $table = new ilUnitTableGUI($this, 'showUnitsOfCategory', $category); + $table = new \ilUnitTableGUI($this, 'showUnitsOfCategory', $category); $units = $this->repository->loadUnitsForCategory($category->getId()); $data = []; foreach ($units as $unit) { - /** @var assFormulaQuestionUnit $unit */ $data[] = [ 'unit_id' => $unit->getId(), - 'unit' => $unit->getSanitizedUnit(), - 'baseunit' => $unit->getSanitizedBaseunitTitle(), + 'unit' => $unit->getUnit(), + 'baseunit' => $unit->getBaseunitTitle(), 'baseunit_id' => $unit->getBaseUnit(), 'factor' => $unit->getFactor(), 'sequence' => $unit->getSequence(), @@ -492,17 +513,18 @@ protected function showUnitsOfCategory(): void protected function showGlobalUnitCategories(): void { $categories = array_filter( - $this->repository->getAllUnitCategories(), - static function (assFormulaQuestionUnitCategory $category): bool { + $this->repository->getAllUnitCategories( + $this->request->getQuestionId() + ), + static function (Category $category): bool { return !$category->getQuestionFi() ? true : false; } ); $data = []; foreach ($categories as $category) { - /** @var assFormulaQuestionUnitCategory $category */ $data[] = [ 'category_id' => $category->getId(), - 'category' => $category->getDisplayString() + 'category' => $category->getDisplayString($this->lng) ]; } @@ -522,10 +544,11 @@ protected function confirmDeleteCategory(): void /** * @param int[]|null $category_ids * @return void - * @throws ilCtrlException + * @throws \ilCtrlException */ - protected function confirmDeleteCategories(?array $category_ids = null): void - { + protected function confirmDeleteCategories( + ?array $category_ids = null + ): void { if (!$this->isCRUDContext()) { $this->{$this->getDefaultCommand()}(); return; @@ -537,7 +560,7 @@ protected function confirmDeleteCategories(?array $category_ids = null): void return; } - $confirmation = new ilConfirmationGUI(); + $confirmation = new \ilConfirmationGUI(); $confirmation->setFormAction($this->ctrl->getFormAction($this, 'deleteCategories')); $confirmation->setConfirm($this->lng->txt('confirm'), 'deleteCategories'); $confirmation->setCancel($this->lng->txt('cancel'), $this->getUnitCategoryOverviewCommand()); @@ -547,22 +570,31 @@ protected function confirmDeleteCategories(?array $category_ids = null): void foreach ($category_ids as $category_id) { try { $category = $this->repository->getUnitCategoryById($category_id); - } catch (ilException $e) { + } catch (\ilException $e) { continue; } - if (!$this->repository->isCRUDAllowed($category_id)) { - $errors[] = $category->getDisplayString() . ' - ' . $this->lng->txt('change_adm_categories_not_allowed'); + if (!$this->repository->isCRUDAllowed( + $this->request->getQuestionId(), + $category_id + )) { + $errors[] = $category->getDisplayString($this->lng) + . ' - ' . $this->lng->txt('change_adm_categories_not_allowed'); continue; } $possible_error = $this->repository->checkDeleteCategory($category_id); if (is_string($possible_error) && $possible_error !== '') { - $errors[] = $category->getDisplayString() . ' - ' . $possible_error; + $errors[] = $category->getDisplayString($this->lng) + . ' - ' . $possible_error; continue; } - $confirmation->addItem('category_ids[]', (string) $category->getId(), $category->getDisplayString()); + $confirmation->addItem( + 'category_ids[]', + (string) $category->getId(), + $category->getDisplayString($this->lng) + ); ++$num_to_confirm; } @@ -616,18 +648,23 @@ protected function deleteCategories(): void foreach ($category_ids as $category_id) { try { $category = $this->repository->getUnitCategoryById($category_id); - } catch (ilException $e) { + } catch (\ilException $e) { continue; } - if (!$this->repository->isCRUDAllowed($category_id)) { - $errors[] = $category->getDisplayString() . ' - ' . $this->lng->txt('change_adm_categories_not_allowed'); + if (!$this->repository->isCRUDAllowed( + $this->request->getQuestionId(), + $category_id + )) { + $errors[] = $category->getDisplayString($this->lng) + . ' - ' . $this->lng->txt('change_adm_categories_not_allowed'); continue; } $possible_error = $this->repository->deleteCategory($category_id); if (is_string($possible_error) && $possible_error !== '') { - $errors[] = $category->getDisplayString() . ' - ' . $possible_error; + $errors[] = $category->getDisplayString($this->lng) + . ' - ' . $possible_error; continue; } @@ -664,15 +701,16 @@ protected function deleteCategories(): void $this->{$this->getUnitCategoryOverviewCommand()}(); } - protected function initUnitCategoryForm(?assFormulaQuestionUnitCategory $cat = null): ilPropertyFormGUI - { - if ($this->unit_cat_form instanceof ilPropertyFormGUI) { + protected function initUnitCategoryForm( + ?Category $cat = null + ): \ilPropertyFormGUI { + if ($this->unit_cat_form instanceof \ilPropertyFormGUI) { return $this->unit_cat_form; } - $this->unit_cat_form = new ilPropertyFormGUI(); + $this->unit_cat_form = new \ilPropertyFormGUI(); - $title = new ilTextInputGUI($this->lng->txt('title'), 'category_name'); + $title = new \ilTextInputGUI($this->lng->txt('title'), 'category_name'); $title->setRequired(true); $this->unit_cat_form->addItem($title); @@ -684,10 +722,18 @@ protected function initUnitCategoryForm(?assFormulaQuestionUnitCategory $cat = n $this->ctrl->setParameter($this, 'category_id', $cat->getId()); $this->unit_cat_form->addCommandButton('saveCategory', $this->lng->txt('save')); $this->unit_cat_form->setFormAction($this->ctrl->getFormAction($this, 'saveCategory')); - $this->unit_cat_form->setTitle(sprintf($this->lng->txt('selected_category'), $cat->getDisplayString())); + $this->unit_cat_form->setTitle( + sprintf( + $this->lng->txt('selected_category'), + $cat->getDisplayString($this->lng) + ) + ); } - $this->unit_cat_form->addCommandButton($this->getUnitCategoryOverviewCommand(), $this->lng->txt('cancel')); + $this->unit_cat_form->addCommandButton( + $this->getUnitCategoryOverviewCommand(), + $this->lng->txt('cancel') + ); return $this->unit_cat_form; } @@ -701,14 +747,15 @@ protected function addCategory(): void $this->initUnitCategoryForm(); if ($this->unit_cat_form->checkInput()) { try { - $category = new assFormulaQuestionUnitCategory(); + $category = new Category(); $category->setCategory($this->unit_cat_form->getInput('category_name')); + $category->setQuestionFi($this->request->getQuestionId()); $this->repository->saveNewUnitCategory($category); $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully')); $this->{$this->getUnitCategoryOverviewCommand()}(); return; - } catch (ilException $e) { + } catch (\ilException $e) { $this->unit_cat_form->getItemByPostVar('category_name')->setAlert($this->lng->txt($e->getMessage())); $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_input_not_valid')); } @@ -749,7 +796,7 @@ protected function saveCategory(): void $this->{$this->getUnitCategoryOverviewCommand()}(); return; - } catch (ilException $e) { + } catch (\ilException $e) { $this->unit_cat_form->getItemByPostVar('category_name')->setAlert($this->lng->txt($e->getMessage())); $this->tpl->setOnScreenMessage('failure', $this->lng->txt('form_input_not_valid')); } diff --git a/components/ILIAS/Questions/src/Units/Repository.php b/components/ILIAS/Questions/src/Units/Repository.php new file mode 100755 index 000000000000..c87b4cd0483b --- /dev/null +++ b/components/ILIAS/Questions/src/Units/Repository.php @@ -0,0 +1,859 @@ + $units */ + private array $units = []; + /** @var list<\ILIAS\Questions\Units\Unit|\ILIAS\Questions\Units\Category> $categorized_units */ + private array $categorized_units = []; + + public function __construct( + private readonly Language $lng, + private readonly \ilDBInterface $db + ) { + } + + public function isCRUDAllowed( + int $question_id, + int $category_id + ): bool { + $res = $this->db->queryF( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE category_id = %s', + [\ilDBConstants::T_INTEGER], + [$category_id] + ); + $row = $this->db->fetchAssoc($res); + return isset($row['question_fi']) && (int) $row['question_fi'] === $question_id; + } + + public function copyCategory( + int $question_fi, + int $category_id, + ?string $category_name = null + ): int { + $res = $this->db->queryF( + 'SELECT category FROM ' . self::CATEGORY_TABLE . ' WHERE category_id = %s', + [\ilDBConstants::T_INTEGER], + [$category_id] + ); + $row = $this->db->fetchAssoc($res); + + if (null === $category_name) { + $category_name = $row['category']; + } + + $next_id = $this->db->nextId(self::CATEGORY_TABLE); + $this->db->insert( + self::CATEGORY_TABLE, + [ + 'category_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'category' => [\ilDBConstants::T_TEXT, $category_name], + 'question_fi' => [\ilDBConstants::T_INTEGER, (int) $question_fi] + ] + ); + + return $next_id; + } + + public function copyUnitsByCategories( + int $question_id, + int $from_category_id, + int $to_category_id, + ): void { + $res = $this->db->queryF( + 'SELECT * FROM ' . self::UNIT_TABLE . ' WHERE category_fi = %s', + [\ilDBConstants::T_INTEGER], + [$from_category_id] + ); + $i = 0; + $units = []; + while (($row = $this->db->fetchAssoc($res)) !== null) { + $next_id = $this->db->nextId(self::UNIT_TABLE); + + $units[$i]['old_unit_id'] = $row['unit_id']; + $units[$i]['new_unit_id'] = $next_id; + + $this->db->insert( + self::UNIT_TABLE, + [ + 'unit_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'unit' => [\ilDBConstants::T_TEXT, $row['unit']], + 'factor' => [\ilDBConstants::T_FLOAT, $row['factor']], + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, (int) $row['baseunit_fi']], + 'category_fi' => [\ilDBConstants::T_INTEGER, (int) $to_category_id], + 'sequence' => [\ilDBConstants::T_INTEGER, (int) $row['sequence']], + 'question_fi' => [\ilDBConstants::T_INTEGER, (int) $question_id] + ] + ); + $i++; + } + + foreach ($units as $unit) { + //update unit : baseunit_fi + $this->db->update( + self::UNIT_TABLE, + [ + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, $unit['old_unit_id']], + 'category_fi' => [\ilDBConstants::T_INTEGER, $to_category_id] + ] + ); + + //update var : unit_fi + $this->db->update( + self::VARIABLES_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $question_id] + ] + ); + + //update res : unit_fi + $this->db->update( + self::RESULTS_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $question_id] + ] + ); + + //update res_unit : unit_fi + $this->db->update( + self::RESULT_UNITS_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $question_id] + ] + ); + } + } + + public function getCategoryUnitCount( + int $id + ): int { + $row = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(category_id) FROM ' . self::UNIT_TABLE . ' WHERE category_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + + return $row->cnt; + } + + public function isUnitInUse( + int $id + ): bool { + $use_in_result_units = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(result_unit_id) cnt FROM ' . self::RESULT_UNITS_TABLE . ' WHERE unit_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + + if ($use_in_result_units->cnt > 0) { + return true; + } + + $use_in_vars = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(variable_id) cnt FROM ' . self::VARIABLES_TABLE . ' WHERE unit_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + if ($use_in_vars->cnt > 0) { + return true; + } + + $use_in_results = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(result_id) cnt FROM ' . self::RESULTS_TABLE . ' WHERE unit_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + if ($use_in_results->cnt > 0) { + return true; + } + + return false; + } + + public function checkDeleteCategory( + int $id + ): ?string { + $res = $this->db->queryF( + 'SELECT unit_id FROM ' . self::UNIT_TABLE . ' WHERE category_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ); + + while ($row = $this->db->fetchAssoc($res)) { + $unit_res = $this->checkDeleteUnit((int) $row['unit_id'], $id); + if ($unit_res !== null) { + return $unit_res; + } + } + + return null; + } + + public function deleteUnit( + int $id + ): ?string { + $res = $this->checkDeleteUnit($id); + if ($res !== null) { + return $res; + } + + $affected_rows = $this->db->manipulateF( + 'DELETE FROM ' . self::UNIT_TABLE . ' WHERE unit_id = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ); + + if ($affected_rows > 0) { + $this->clearUnits(); + } + + return null; + } + + protected function loadUnits(): void + { + $result = $this->db->query( + 'SELECT units.*, ' . self::CATEGORY_TABLE . '.category, baseunits.unit baseunit_title' . PHP_EOL + . 'FROM ' . self::UNIT_TABLE . ' units' . PHP_EOL + . 'INNER JOIN ' . self::CATEGORY_TABLE . ' ON ' . self::CATEGORY_TABLE . '.category_id = units.category_fi' . PHP_EOL + . 'LEFT JOIN ' . self::UNIT_TABLE . ' baseunits ON baseunits.unit_id = units.baseunit_fi' . PHP_EOL + . 'ORDER BY ' . self::CATEGORY_TABLE . '.category, units.sequence' + ); + + while ($row = $this->db->fetchAssoc($result)) { + $unit = new Unit(); + $unit->initFormArray($row); + $this->addUnit($unit); + } + } + + /** + * @return list<\ILIAS\Questions\Units\Unit|\ILIAS\Questions\Units\Category> + */ + public function getCategorizedUnits( + int $question_id + ): array { + if (count($this->categorized_units) === 0) { + $result = $this->db->queryF( + 'SELECT units.*, ' . self::CATEGORY_TABLE . '.category, ' . self::CATEGORY_TABLE . '.question_fi, baseunits.unit baseunit_title' . PHP_EOL + . 'FROM ' . self::UNIT_TABLE . ' units' . PHP_EOL + . 'INNER JOIN ' . self::CATEGORY_TABLE . ' ON ' . self::CATEGORY_TABLE . '.category_id = units.category_fi' . PHP_EOL + . 'LEFT JOIN ' . self::UNIT_TABLE . ' baseunits ON baseunits.unit_id = units.baseunit_fi' . PHP_EOL + . 'WHERE units.question_fi = %s' . PHP_EOL + . 'ORDER BY ' . self::CATEGORY_TABLE . '.category, units.sequence' . PHP_EOL, + [\ilDBConstants::T_INTEGER], + [$question_id] + ); + + $category = 0; + while (($row = $this->db->fetchAssoc($result)) !== null) { + $unit = new Unit(); + $unit->initFormArray($row); + + if ($category !== $unit->getCategory()) { + $cat = new Category(); + $cat->initFormArray([ + 'category_id' => (int) $row['category_fi'], + 'category' => $row['category'], + 'question_fi' => (int) $row['question_fi'], + ]); + $this->categorized_units[] = $cat; + $category = $unit->getCategory(); + } + + $this->categorized_units[] = $unit; + } + } + + return $this->categorized_units; + } + + protected function clearUnits(): void + { + $this->units = []; + } + + protected function addUnit( + Unit $unit + ): void { + $this->units[$unit->getId()] = $unit; + } + + /** + * @return list<\ILIAS\Questions\Units\Unit> + */ + public function getUnits(): array + { + if ($this->units === []) { + $this->loadUnits(); + } + return $this->units; + } + + /** + * @return list<\ILIAS\Questions\Units\Unit> + */ + public function loadUnitsForCategory( + int $category + ): array { + $units = []; + $result = $this->db->queryF( + 'SELECT units.*, baseunits.unit baseunit_title, ' . self::CATEGORY_TABLE . '.category' . PHP_EOL + . 'FROM ' . self::UNIT_TABLE . ' units' . PHP_EOL + . 'INNER JOIN ' . self::CATEGORY_TABLE . ' ON ' . self::CATEGORY_TABLE . '.category_id = units.category_fi' . PHP_EOL + . 'LEFT JOIN ' . self::UNIT_TABLE . ' baseunits ON baseunits.unit_id = units.baseunit_fi' . PHP_EOL + . 'WHERE ' . self::CATEGORY_TABLE . '.category_id = %s' . PHP_EOL + . 'ORDER BY units.sequence', + [\ilDBConstants::T_INTEGER], + [$category] + ); + + while (($row = $this->db->fetchAssoc($result)) !== null) { + $unit = new Unit(); + $unit->initFormArray($row); + $units[] = $unit; + } + + return $units; + } + + public function getUnit( + int $id + ): ?Unit { + if ($this->units === []) { + $this->loadUnits(); + } + + if (array_key_exists($id, $this->units)) { + return $this->units[$id]; + } + + // Maybe this is a new unit, reload $this->units + $this->loadUnits(); + + return $this->units[$id] ?? null; + } + + /** + * @return array + */ + public function getUnitCategories(): array + { + $categories = []; + $result = $this->db->queryF( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE question_fi > %s ORDER BY category', + [\ilDBConstants::T_INTEGER], + [0] + ); + + while (($row = $this->db->fetchAssoc($result)) !== null) { + $value = $this->lng->txt($row['category']) === "-qpl_qst_formulaquestion_{$row['category']}-" + ? $row['category'] + : $this->lng->txt($row['category']); + + if (trim($row['category']) !== '') { + $cat = [ + 'value' => (int) $row['category_id'], + 'text' => $value, + 'qst_id' => (int) $row['question_fi'] + ]; + $categories[(int) $row['category_id']] = $cat; + } + } + + return $categories; + } + + /** + * @return array + */ + public function getAdminUnitCategories(): array + { + $categories = []; + + $result = $this->db->queryF( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE question_fi = %s ORDER BY category', + [\ilDBConstants::T_INTEGER], + [0] + ); + + while (($row = $this->db->fetchAssoc($result)) !== null) { + $value = $this->lng->txt($row['category']) === "-qpl_qst_formulaquestion_{$row['category']}-'" + ? $row['category'] + : $this->lng->txt($row['category']); + + if (trim($row['category']) !== '') { + $cat = [ + 'value' => (int) $row['category_id'], + 'text' => $value, + 'qst_id' => (int) $row['question_fi'] + ]; + $categories[(int) $row['category_id']] = $cat; + } + } + + return $categories; + } + + public function saveUnitOrder( + int $question_id, + int $unit_id, + int $sequence + ): void { + $this->db->manipulateF( + 'UPDATE ' . self::UNIT_TABLE . ' SET sequence = %s WHERE unit_id = %s AND question_fi = %s', + [ + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $sequence, + $unit_id, + $question_id + ] + ); + } + + public function checkDeleteUnit( + int $id, + ?int $category_id = null + ): ?string { + $use_in_vars = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(variable_id) cnt FROM ' . self::VARIABLES_TABLE . ' WHERE unit_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + if ($use_in_vars->cnt > 0) { + return $this->lng->txt('err_unit_in_variables'); + } + + $use_in_results = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(result_id) cnt FROM ' . self::RESULTS_TABLE . ' WHERE unit_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ) + ); + if ($use_in_results->cnt > 0) { + return $this->lng->txt('err_unit_in_results'); + } + + $additional_where = 'unit_id != %s'; + $values_array = [$id, $id]; + if ($category_id !== null) { + $additional_where = 'category_fi != %s'; + $values_array = [$id, $category_id]; + } + + $use_as_base_unit = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(unit_id) cnt FROM ' . self::UNIT_TABLE . '' . PHP_EOL + . "WHERE baseunit_fi = %s AND {$additional_where}", + [\ilDBConstants::T_INTEGER, \ilDBConstants::T_INTEGER], + $values_array + ) + ); + + if ($use_as_base_unit->cnt > 0) { + return $this->lng->txt('err_unit_is_baseunit'); + } + + return null; + } + + public function getUnitCategoryById( + int $id + ): Category { + $res = $this->db->query( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE category_id = ' + . $this->db->quote($id, \ilDBConstants::T_INTEGER) + ); + + if ($this->db->numRows($res) === 0) { + throw new \ilException('un_category_not_exist'); + } + + $category = new Category(); + $category->initFormArray($this->db->fetchAssoc($res)); + return $category; + } + + public function saveCategory( + Category $category + ): void { + $row = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(category_id) cnt FROM ' . self::CATEGORY_TABLE . '' . PHP_EOL + . 'WHERE category = %s AND question_fi = %s AND category_id != %s', + [ + \ilDBConstants::T_TEXT, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $category->getCategory(), + $category->getQuestionFi(), + $category->getId() + ] + ) + ); + if ($row->cnt > 0) { + throw new \ilException('err_wrong_categoryname'); + } + + $this->db->manipulateF( + 'UPDATE ' . self::CATEGORY_TABLE . ' SET category = %s WHERE question_fi = %s AND category_id = %s', + [ + \ilDBConstants::T_TEXT, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $category->getCategory(), + $category->getQuestionFi(), + $category->getId() + ] + ); + } + + public function saveNewUnitCategory( + Category $category + ): void { + $row = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(category_id) cnt FROM ' . self::CATEGORY_TABLE . ' WHERE category = %s AND question_fi = %s', + [ + \ilDBConstants::T_TEXT, + \ilDBConstants::T_INTEGER + ], + [ + $category->getCategory(), + $category->getQuestionFi() + ] + ) + ); + if ($row->cnt > 0) { + throw new \ilException('err_wrong_categoryname'); + } + + $next_id = $this->db->nextId(self::CATEGORY_TABLE); + $this->db->manipulateF( + 'INSERT INTO ' . self::CATEGORY_TABLE . ' (category_id, category, question_fi) VALUES (%s, %s, %s)', + [ + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_TEXT, + \ilDBConstants::T_INTEGER + ], + [ + $next_id, + $category->getCategory(), + $category->getQuestionFi() + ] + ); + $category->setId($next_id); + } + + /** + * @return list<\ILIAS\Questions\Units\Category> + */ + public function getAllUnitCategories( + int $question_id + ): array { + $categories = []; + $result = $this->db->queryF( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE question_fi = %s OR question_fi = %s ORDER BY category', + [ + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $question_id, + 0 + ] + ); + + while ($row = $this->db->fetchAssoc($result)) { + $category = new Category(); + $category->initFormArray($row); + $categories[] = $category; + } + + return $categories; + } + + public function deleteCategory( + int $id + ): ?string { + if ($this->checkDeleteCategory($id) !== null) { + return $this->lng->txt('err_category_in_use'); + } + + $res = $this->db->queryF( + 'SELECT * FROM ' . self::UNIT_TABLE . ' WHERE category_fi = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ); + while (($row = $this->db->fetchAssoc($res)) !== null) { + $this->deleteUnit((int) $row['unit_id']); + } + + $ar = $this->db->manipulateF( + 'DELETE FROM ' . self::CATEGORY_TABLE . ' WHERE category_id = %s', + [\ilDBConstants::T_INTEGER], + [$id] + ); + + if ($ar > 0) { + $this->clearUnits(); + } + + return null; + } + + public function createNewUnit( + int $question_id, + Unit $unit + ): void { + $next_id = $this->db->nextId(self::UNIT_TABLE); + $this->db->manipulateF( + 'INSERT INTO ' . self::UNIT_TABLE . ' (unit_id, unit, factor, baseunit_fi, category_fi, sequence, question_fi)' . PHP_EOL + . 'VALUES (%s, %s, %s, %s, %s, %s, %s)', + [ + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_TEXT, + \ilDBConstants::T_FLOAT, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $next_id, + $unit->getUnit(), + 1, + 0, + $unit->getCategory(), + 0, + $question_id + ] + ); + $unit->setId($next_id); + $unit->setFactor(1.0); + $unit->setBaseUnit(0); + $unit->setSequence(0); + + $this->clearUnits(); + } + + public function saveUnit( + int $question_id, + Unit $unit + ): void { + $row = $this->db->fetchObject( + $this->db->queryF( + 'SELECT COUNT(unit_id) cnt FROM ' . self::UNIT_TABLE . ' WHERE unit_id = %s', + [\ilDBConstants::T_INTEGER], + [$unit->getId()] + ) + ); + if ($row->cnt === 0) { + return; + } + + if ($unit->getBaseUnit() === 0 || $unit->getBaseUnit() === $unit->getId()) { + $unit->setFactor(1); + } + + $ar = $this->db->manipulateF( + 'UPDATE ' . self::UNIT_TABLE . '' . PHP_EOL + . 'SET unit = %s, factor = %s, baseunit_fi = %s, category_fi = %s, sequence = %s' . PHP_EOL + . 'WHERE unit_id = %s AND question_fi = %s', + [ + \ilDBConstants::T_TEXT, + \ilDBConstants::T_FLOAT, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER, + \ilDBConstants::T_INTEGER + ], + [ + $unit->getUnit(), $unit->getFactor(), (int) $unit->getBaseUnit(), + $unit->getCategory(), + $unit->getSequence(), + $unit->getId(), + $question_id + ] + ); + if ($ar > 0) { + $this->clearUnits(); + } + } + + public function cloneUnits( + int $from_consumer_id, + int $to_consumer_id + ): void { + $category_mapping = []; + + $res = $this->db->queryF( + 'SELECT * FROM ' . self::CATEGORY_TABLE . ' WHERE question_fi = %s', + [\ilDBConstants::T_INTEGER], + [$from_consumer_id] + ); + while ($row = $this->db->fetchAssoc($res)) { + $new_category_id = $this->copyCategory((int) $row['category_id'], $to_consumer_id); + $category_mapping[$row['category_id']] = $new_category_id; + } + + foreach ($category_mapping as $old_category_id => $new_category_id) { + $res = $this->db->queryF( + 'SELECT * FROM ' . self::UNIT_TABLE . ' WHERE category_fi = %s', + [\ilDBConstants::T_INTEGER], + [$old_category_id] + ); + + $i = 0; + $units = []; + while ($row = $this->db->fetchAssoc($res)) { + $next_id = $this->db->nextId(self::UNIT_TABLE); + + $units[$i]['old_unit_id'] = $row['unit_id']; + $units[$i]['new_unit_id'] = $next_id; + + $this->db->insert( + self::UNIT_TABLE, + [ + 'unit_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'unit' => [\ilDBConstants::T_TEXT, $row['unit']], + 'factor' => [\ilDBConstants::T_FLOAT, $row['factor']], + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, (int) $row['baseunit_fi']], + 'category_fi' => [\ilDBConstants::T_INTEGER, (int) $new_category_id], + 'sequence' => [\ilDBConstants::T_INTEGER, (int) $row['sequence']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $to_consumer_id] + ] + ); + $i++; + } + + foreach ($units as $unit) { + //update unit : baseunit_fi + $this->db->update( + self::UNIT_TABLE, + [ + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'baseunit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $to_consumer_id] + ] + ); + + //update var : unit_fi + $this->db->update( + self::VARIABLES_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $to_consumer_id] + ] + ); + + //update res : unit_fi + $this->db->update( + self::RESULTS_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $to_consumer_id] + ] + ); + + //update res_unit : unit_fi + $this->db->update( + self::RESULT_UNITS_TABLE, + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['new_unit_id']] + ], + [ + 'unit_fi' => [\ilDBConstants::T_INTEGER, (int) $unit['old_unit_id']], + 'question_fi' => [\ilDBConstants::T_INTEGER, $to_consumer_id] + ] + ); + } + } + } + + + public function lookupUnitFactor( + int $a_unit_id + ): float { + $res = $this->db->fetchObject( + $this->db->queryF( + 'SELECT factor FROM il_qpl_qst_fq_unit WHERE unit_id = %s', + [\ilDBConstants::T_INTEGER], + [$a_unit_id] + ) + ); + + return (float) $row->factor; + } +} diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/Questions/src/Units/Unit.php similarity index 65% rename from components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php rename to components/ILIAS/Questions/src/Units/Unit.php index 959c8362192a..68e29611de53 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/Questions/src/Units/Unit.php @@ -18,12 +18,11 @@ declare(strict_types=1); -/** - * Formula Question Unit - * @author Helmut Schottmüller - * @ingroup components\ILIASTestQuestionPool - */ -class assFormulaQuestionUnit +namespace ILIAS\Questions\Units; + +use ILIAS\Language\Language; + +class Unit { private int $id = 0; private string $unit = ''; @@ -40,7 +39,7 @@ public function initFormArray(array $data): void $this->factor = (float) $data['factor']; $this->baseunit = (int) $data['baseunit_fi']; $this->baseunit_title = $data['baseunit_title'] ?? null; - $this->category = (int) $data['category_fi']; + $this->category = (int) $data['category']; $this->sequence = (int) $data['sequence']; } @@ -64,11 +63,6 @@ public function getUnit(): string return $this->unit; } - public function getSanitizedUnit(): string - { - return htmlspecialchars($this->getUnit(), ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); - } - public function setSequence(int $sequence): void { $this->sequence = $sequence; @@ -113,11 +107,6 @@ public function getBaseunitTitle(): ?string return $this->baseunit_title; } - public function getSanitizedBaseunitTitle(): ?string - { - return $this->sanitizeString($this->getBaseunitTitle() ?? ''); - } - public function setCategory(int $category): void { $this->category = $category; @@ -128,35 +117,14 @@ public function getCategory(): int return $this->category; } - public function getDisplayString(): string - { - global $DIC; - + public function getDisplayString( + Language $lng + ): string { $unit = $this->getUnit(); - $txt = $DIC->language()->txt("qpl_qst_formulaquestion_{$unit}"); - return strcmp("-qpl_qst_formulaquestion_{$unit}-", $txt) !== 0 - ? $this->sanitizeString($txt) - : $this->getSanitizedUnit(); - } - - public static function lookupUnitFactor(int $a_unit_id): float - { - global $DIC; - $ilDB = $DIC['ilDB']; - - $res = $ilDB->queryF( - 'SELECT factor FROM il_qpl_qst_fq_unit WHERE unit_id = %s', - ['integer'], - [$a_unit_id] - ); - - $row = $ilDB->fetchAssoc($res); - - return (float) $row['factor']; - } + if ($lng->txt("qpl_qst_formulaquestion_{$unit}") !== "-qpl_qst_formulaquestion_{$unit}-") { + return $lng->txt("qpl_qst_formulaquestion_{$unit}"); + } - private function sanitizeString(string $string): string - { - return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); + return $unit; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilGlobalUnitConfigurationGUI.php b/components/ILIAS/Questions/src/Units/class.GlobalConfigurationGUI.php similarity index 67% rename from components/ILIAS/TestQuestionPool/classes/class.ilGlobalUnitConfigurationGUI.php rename to components/ILIAS/Questions/src/Units/class.GlobalConfigurationGUI.php index b979ac2e2a63..cf1cc22235ea 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilGlobalUnitConfigurationGUI.php +++ b/components/ILIAS/Questions/src/Units/class.GlobalConfigurationGUI.php @@ -18,10 +18,9 @@ declare(strict_types=1); -/** - * Class ilGlobalUnitConfigurationGUI - */ -class ilGlobalUnitConfigurationGUI extends ilUnitConfigurationGUI +namespace ILIAS\Questions\Units; + +class GlobalConfigurationGUI extends ConfigurationGUI { public const REQUEST_PARAM_SUB_CONTEXT = 'context'; @@ -42,26 +41,29 @@ public function isCRUDContext(): bool public function getUniqueId(): string { - return $this->repository->getConsumerId() . '_global'; + return $this->request->getQuestionId() . '_global'; } + #[\Override] protected function showGlobalUnitCategories(): void { - global $DIC; - - $ilToolbar = $DIC->toolbar(); - $rbacsystem = $DIC->rbac()->system(); - - if ($rbacsystem->checkAccess('write', $this->request->getRefId())) { - $ilToolbar->addButton($this->lng->txt('un_add_category'), $this->ctrl->getLinkTarget($this, 'showUnitCategoryCreationForm')); + if ($this->rbac_system->checkAccess('write', $this->request->getRefId())) { + $this->toolbar->addButton( + $this->lng->txt('un_add_category'), + $this->ctrl->getLinkTargetByClass( + self::class, + 'showUnitCategoryCreationForm' + ) + ); } parent::showGlobalUnitCategories(); } + #[\Override] protected function showUnitCategories(array $categories): void { - $table = new ilGlobalUnitCategoryTableGUI($this, $this->getUnitCategoryOverviewCommand()); + $table = new \ilGlobalUnitCategoryTableGUI($this, $this->getUnitCategoryOverviewCommand()); $table->setData($categories); $this->tpl->setContent($table->getHTML()); diff --git a/components/ILIAS/Questions/src/Units/class.LocalConfigurationGUI.php b/components/ILIAS/Questions/src/Units/class.LocalConfigurationGUI.php new file mode 100755 index 000000000000..7a634dd02eec --- /dev/null +++ b/components/ILIAS/Questions/src/Units/class.LocalConfigurationGUI.php @@ -0,0 +1,214 @@ +isCRUDContext()) { + return 'showLocalUnitCategories'; + } + + return 'showGlobalUnitCategories'; + } + + #[\Override] + public function isCRUDContext(): bool + { + if (!$this->request->isset(self::REQUEST_PARAM_SUB_CONTEXT_ID) || + $this->request->int(self::REQUEST_PARAM_SUB_CONTEXT_ID) === $this->request->getQuestionId()) { + return true; + } + + return false; + } + + #[\Override] + public function getUniqueId(): string + { + $id = $this->request->getQuestionId(); + if ($this->isCRUDContext()) { + $id .= '_local'; + } else { + $id .= '_global'; + } + + return $id; + } + + #[\Override] + public function executeCommand(): void + { + $this->ctrl->saveParameter($this, self::REQUEST_PARAM_SUB_CONTEXT_ID); + $this->help->setScreenIdComponent('qpl'); + + parent::executeCommand(); + } + + #[\Override] + protected function handleSubtabs(): void + { + $this->ctrl->setParameterByClass( + self::class, + self::REQUEST_PARAM_SUB_CONTEXT_ID, + $this->request->getQuestionId() + ); + + $this->tabs->addSubTab( + 'view_unit_ctx_local', + $this->lng->txt('un_local_units'), + $this->ctrl->getLinkTargetByClass( + self::class, + 'showLocalUnitCategories' + ) + ); + + $this->ctrl->setParameterByClass( + self::class, + self::REQUEST_PARAM_SUB_CONTEXT_ID, + 0 + ); + + $this->tabs->addSubTab( + 'view_unit_ctx_global', + $this->lng->txt('un_global_units'), + $this->ctrl->getLinkTargetByClass( + self::class, + 'showGlobalUnitCategories' + ) + ); + + $this->ctrl->setParameterByClass( + self::class, + self::REQUEST_PARAM_SUB_CONTEXT_ID, + '' + ); + + if ($this->isCRUDContext()) { + $this->tabs->activateSubTab('view_unit_ctx_local'); + } else { + $this->tabs->activateSubTab('view_unit_ctx_global'); + } + } + + protected function showLocalUnitCategories(): void + { + $this->toolbar->addButton( + $this->lng->txt('un_add_category'), + $this->ctrl->getLinkTargetByClass( + self::class, + 'showUnitCategoryCreationForm' + ) + ); + + $question_id = $this->request->getQuestionId(); + $this->showUnitCategories( + array_map( + fn(Category $v): array => [ + 'category_id' => $v->getId(), + 'category' => $v->getDisplayString($this->lng) + ], + array_filter( + $this->repository->getAllUnitCategories($question_id), + fn(Category $category): bool => $category->getQuestionFi() === $question_id + ) + ) + ); + } + + /** + * @param array $categories + */ + #[\Override] + protected function showUnitCategories( + array $categories + ): void { + $table = new \ilLocalUnitCategoryTableGUI($this, $this->getUnitCategoryOverviewCommand()); + $table->setData($categories); + + $this->tpl->setContent($table->getHTML()); + } + + protected function confirmImportGlobalCategory(): void + { + if (!$this->request->isset('category_id')) { + $this->showGlobalUnitCategories(); + return; + } + $this->confirmImportGlobalCategories([$this->request->raw('category_id')]); + } + + protected function confirmImportGlobalCategories( + array $category_ids + ): void { + // @todo: Confirmation Currently not implemented, so forward to import + $this->importGlobalCategories($category_ids); + } + + protected function importGlobalCategories( + array $category_ids + ): void { + if ($this->isCRUDContext()) { + $this->{$this->getDefaultCommand()}(); + return; + } + + $i = 0; + foreach ($category_ids as $category_id) { + try { + $category = $this->repository->getUnitCategoryById((int) $category_id); + } catch (\ilException $e) { + continue; + } + + // Copy admin-category to custom-category (with question_fi) + $new_cat_id = $this->repository->copyCategory( + $this->request->getQuestionId(), + $category->getId() + ); + + // Copy units to custom_category + $this->repository->copyUnitsByCategories( + $this->request->getQuestionId(), + $category->getId(), + $new_cat_id + ); + ++$i; + } + + if ($i) { + $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'), true); + } + + $this->ctrl->setParameter($this, 'question_fi', 0); + $this->ctrl->redirect($this, 'showLocalUnitCategories'); + } +} diff --git a/components/ILIAS/Questions/src/UserSettings/CreateMode.php b/components/ILIAS/Questions/src/UserSettings/CreateMode.php new file mode 100644 index 000000000000..d21d22e745f1 --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/CreateMode.php @@ -0,0 +1,173 @@ +settings = new \ilSetting('questions'); + } + #[\Override] + public function getIdentifier(): string + { + return 'question_create_mode'; + } + + #[\Override] + public function isAvailable(): bool + { + return true; + } + + #[\Override] + public function getLabel(Language $lng): string + { + return $lng->txt('question_create_mode'); + } + + #[\Override] + public function getSettingsPage(): AvailablePages + { + return AvailablePages::MainSettings; + } + + #[\Override] + public function getSection(): AvailableSections + { + return AvailableSections::Additional; + } + + #[\Override] + public function getInput( + FieldFactory $field_factory, + Language $lng, + Refinery $refinery, + \ilSetting $settings, + ?\ilObjUser $user = null + ): Input { + $lng->loadLanguageModule('questions'); + return array_reduce( + CreateModes::cases(), + fn(Radio $c, CreateModes $v): Radio => $c->withOption( + $v->value, + $v->getLabelForInput($lng), + $v->getBylineForInput($lng) + ), + $field_factory->radio( + $lng->txt('create_mode') + ) + )->withValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) + ); + } + + #[\Override] + public function getLegacyInput( + Language $lng, + \ilSetting $settings, + ?\ilObjUser $user = null + ): \ilFormPropertyGUI { + $lng->loadLanguageModule('questions'); + $input = new \ilRadioGroupInputGUI($lng->txt('create_mode')); + $input->setOptions( + array_map( + fn(CreateModes $v): \ilRadioOption => new \ilRadioOption( + $v->getLabelForInput($lng), + $v->value, + $v->getBylineForInput($lng) + ), + CreateModes::cases() + ) + ); + $input->setValue( + $user !== null + ? $this->retrieveValueFromUser($user) + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) + ); + return $input; + } + + #[\Override] + public function getDefaultValueForDisplay( + Language $lng, + \ilSetting $settings + ): string { + return CreateModes::getDefaultMode()->getLabelForInput($lng); + } + + #[\Override] + public function hasUserPersonalizedSetting( + \ilSetting $settings, + \ilObjUser $user + ): bool { + return $this->retrieveValueFromUser($user) + !== $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ); + } + + #[\Override] + public function persistUserInput( + \ilObjUser $user, + mixed $input + ): \ilObjUser { + $user->setPref( + 'question_create_mode', + $input !== null + ? $input + : $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ) + ); + return $user; + } + + #[\Override] + public function retrieveValueFromUser(\ilObjUser $user): string + { + return $user->getPref('question_create_mode') + ?? $this->settings->get( + 'default_create_mode', + CreateModes::getDefaultMode()->value + ); + } +} diff --git a/components/ILIAS/Questions/src/UserSettings/CreateModes.php b/components/ILIAS/Questions/src/UserSettings/CreateModes.php new file mode 100644 index 000000000000..ee6e8c97169f --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/CreateModes.php @@ -0,0 +1,46 @@ +txt("create_mode_{$this->value}"); + } + + public function getBylineForInput( + Language $lng + ): string { + return $lng->txt("byline_create_mode_{$this->value}"); + } + + public static function getDefaultMode(): self + { + return self::Simple; + } +} diff --git a/components/ILIAS/Questions/src/UserSettings/Settings.php b/components/ILIAS/Questions/src/UserSettings/Settings.php new file mode 100644 index 000000000000..959f42df91b3 --- /dev/null +++ b/components/ILIAS/Questions/src/UserSettings/Settings.php @@ -0,0 +1,34 @@ +values[$key] = $value; $field = $this->ui->factory()->input()->field()->radio($title, $description); - if (!is_null($value)) { - $field = $field->withOption($value, ""); // dummy to prevent exception, will be overwritten by radioOption - $field = $field->withValue($value); - } $this->addField( $key, $field @@ -425,6 +421,9 @@ public function radioOption(string $value, string $title, string $description = { if ($field = $this->getLastField()) { $field = $field->withOption($value, $title, $description); + if (($this->values[$this->last_key] ?? null) === $value) { + $field = $field->withValue($value); + } $this->replaceLastField($field); } return $this; @@ -663,6 +662,10 @@ protected function replaceLastField(FormInput $field): void if ($this->last_key !== "") { $this->fields[$this->last_key] = $field; } + // also replace the field in current optional, if it's in it + if (!is_null($this->current_optional) && isset($this->current_optional["fields"][$this->last_key])) { + $this->current_optional["fields"][$this->last_key] = $field; + } } public function getForm(): Form\Standard diff --git a/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php b/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php index 4ebe3b780a08..f1e68d09e6b8 100755 --- a/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php +++ b/components/ILIAS/Repository/Service/IRSS/CollectionWrapperGUI.php @@ -52,7 +52,8 @@ public function getResourceCollectionGUI( Mode::DATA_TABLE, 100, $write, - $write + $write, + true ) ); } diff --git a/components/ILIAS/Repository/Service/IRSS/IRSSWrapper.php b/components/ILIAS/Repository/Service/IRSS/IRSSWrapper.php index df4777312a95..5401bc3beffe 100755 --- a/components/ILIAS/Repository/Service/IRSS/IRSSWrapper.php +++ b/components/ILIAS/Repository/Service/IRSS/IRSSWrapper.php @@ -451,7 +451,13 @@ public function hasContainerEntry( string $rid, string $entry ): bool { + if ($rid === "") { + return false; + } $zip_path = $this->stream($rid)?->getMetadata("uri"); + if (is_null($zip_path)) { + return false; + } try { $stream = Streams::ofFileInsideZIP( $zip_path, diff --git a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php index 6d4495a57ef5..c42e5d20d4cf 100755 --- a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php +++ b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php @@ -156,9 +156,10 @@ public function singleRedirectAction( string $title, array $class_path, string $cmd = "", - string $id_param = "" + string $id_param = "", + bool $async = false ): self { - $this->addAction(self::SINGLE, $action, $title); + $this->addAction(self::SINGLE, $action, $title, $async); $act = $this->actions[$this->last_action_key] ?? false; if ($act && $act["type"] === self::SINGLE) { $act["redirect_class_path"] = $class_path; diff --git a/components/ILIAS/Repository/classes/class.ilRepositoryGUI.php b/components/ILIAS/Repository/classes/class.ilRepositoryGUI.php index 8fdc66ff78c8..b69c69f693a1 100755 --- a/components/ILIAS/Repository/classes/class.ilRepositoryGUI.php +++ b/components/ILIAS/Repository/classes/class.ilRepositoryGUI.php @@ -134,14 +134,13 @@ public function executeCommand(): void ) { $this->ctrl->redirectToURL('./login.php?cmd=force_login'); } - $this->tool_context->claim()->repository(); // check creation mode // determined by "new_type" parameter $new_type = $this->request->getNewType(); - if ($new_type !== "" && $new_type !== "sty") { + if ($new_type !== "" && $new_type !== "sty" && $new_type !== "tax") { $this->creation_mode = true; $ilHelp->setScreenIdComponent($new_type); $ilHelp->setDefaultScreenId(ilHelpGUI::ID_PART_SCREEN, "create"); @@ -194,7 +193,6 @@ public function executeCommand(): void if ($cmd === "showRepTree") { $next_class = ""; } - switch ($next_class) { // forward asynchronous file uploads to the upload handler. // possible via dropzones in list guis or global template @@ -223,7 +221,6 @@ public function executeCommand(): void } $this->gui_obj->setCreationMode($this->creation_mode); $this->ctrl->setReturn($this, "return"); - $this->show(); } else { // $cmd = (string) $this->ctrl->getCmd(""); diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php b/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php index f3eb86f091db..fe8d979cad08 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/Configuration.php @@ -36,6 +36,7 @@ public function __construct( private int $items_per_page = 100, private bool $user_can_upload = false, private bool $user_can_administrate = false, + private bool $append_duplicate_filenames_as_revision = false ) { } @@ -78,4 +79,9 @@ public function canUserAdministrate(): bool { return $this->user_can_administrate; } + + public function preventDuplicates(): bool + { + return $this->append_duplicate_filenames_as_revision; + } } diff --git a/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php b/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php index 28c7ea73a4f0..ddc3cdb2144a 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/View/Request.php @@ -197,4 +197,9 @@ public function canUserAdministrate(): bool { return $this->view_configuration->canUserAdministrate(); } + + public function preventDuplicates(): bool + { + return $this->view_configuration->preventDuplicates(); + } } diff --git a/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php b/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php index f6411c3bdd38..3d5afb0d39a1 100755 --- a/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php +++ b/components/ILIAS/ResourceStorage/classes/Collections/class.ilResourceCollectionGUI.php @@ -229,7 +229,22 @@ public function upload(): void if (!$result->isOK()) { continue; } - $rid = $this->irss->manage()->upload( + // if activated, prevent duplicate files by checking filenames. in thjis case a new revision gets appended + if ($this->view_request->preventDuplicates()) { + $existing_rid = $this->irss->collection()->findIdentificationByNameIn( + $this->view_request->getCollection(), + $result->getName() + ); + if ($existing_rid !== null) { + $this->irss->manage()->appendNewRevision( + $existing_rid, + $upload_result, + $this->view_configuration->getStakeholder() + ); + } + } + + $rid = $existing_rid ?? $this->irss->manage()->upload( $result, $this->view_configuration->getStakeholder() ); diff --git a/components/ILIAS/ResourceStorage/src/Collection/Collections.php b/components/ILIAS/ResourceStorage/src/Collection/Collections.php index 7a4ea78091ca..391cd0ed8ee0 100755 --- a/components/ILIAS/ResourceStorage/src/Collection/Collections.php +++ b/components/ILIAS/ResourceStorage/src/Collection/Collections.php @@ -48,7 +48,7 @@ public function __construct(private ResourceBuilder $resource_builder, private C /** * @param string|null $collection_identification an existing collection identification or null for a new - * @param int|null $owner if this colletion is owned by a users, you must prvide it's owner ID + * @param int|null $owner if this colletion is owned by a users, you must prvide it's owner ID */ public function id( ?string $collection_identification = null, @@ -116,6 +116,17 @@ public function get( return $this->cache[$rcid] = $collection; } + public function findIdentificationByNameIn(ResourceCollection $collection, string $name): ?ResourceIdentification + { + foreach ($collection->getResourceIdentifications() as $identification) { + $resource = $this->resource_builder->get($identification); + if ($resource->getCurrentRevisionIncludingDraft()->getTitle() === $name) { + return $identification; + } + } + return null; + } + public function store(ResourceCollection $collection): bool { $this->cache[$collection->getIdentification()->serialize()] = $collection; diff --git a/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php b/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php index 7ad59282fe7e..81afe48f9496 100755 --- a/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php +++ b/components/ILIAS/Saml/classes/class.ilAuthProviderSaml.php @@ -25,6 +25,7 @@ class ilAuthProviderSaml extends ilAuthProvider implements ilAuthProviderAccount private const string LOG_COMPONENT = 'auth'; private const string ERR_WRONG_LOGIN = 'err_wrong_login'; + private const string ERR_PROVIDER_INACTIVE = 'auth_saml_idp_deactivated_auth_failed'; private const string SESSION_TMP_ATTRIBUTES = 'tmp_attributes'; private const string SESSION_TMP_RETURN_TO = 'tmp_return_to'; @@ -83,6 +84,16 @@ private function determineUidFromAttributes(): void public function doAuthentication(ilAuthStatus $status): bool { + if (!$this->idp->isActive()) { + $this->getLogger()->info( + 'SAML IdP with id {idp_id} is not active.', + ['idp_id' => $this->idp->getIdpId()] + ); + $status->setStatus(ilAuthStatus::STATUS_AUTHENTICATION_FAILED); + $status->setTranslatedReason($this->lng->txt(self::ERR_PROVIDER_INACTIVE)); + return false; + } + if ([] === $this->attributes) { $this->getLogger()->warning('Could not parse any attributes from SAML response.'); $this->handleAuthenticationFail($status, self::ERR_WRONG_LOGIN); diff --git a/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php b/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php index 073295e1523c..f83c4081fe9b 100755 --- a/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php +++ b/components/ILIAS/ScormAicc/classes/class.ilObjSAHSLearningModuleGUI.php @@ -429,7 +429,7 @@ public function uploadObject(): void $newObj->getDataDirectory(), false, false, - true + false ); } ilFileUtils::renameExecutables($newObj->getDataDirectory()); diff --git a/components/ILIAS/StaticURL/src/Response/Factory.php b/components/ILIAS/StaticURL/src/Response/Factory.php index 67efbae162d5..4b4e9ccd1ca5 100755 --- a/components/ILIAS/StaticURL/src/Response/Factory.php +++ b/components/ILIAS/StaticURL/src/Response/Factory.php @@ -40,9 +40,6 @@ public function loginFirst(): MaybeCanHandlerAfterLogin|CannotReach if ($this->context->isUserLoggedIn()) { return new CannotReach(); } - if (!$this->context->isUserLoggedIn() && !$this->context->isPublicSectionActive()) { - return new CannotReach(); - } return new MaybeCanHandlerAfterLogin(); } diff --git a/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php b/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php index 633f2e10afba..5ba15dec9453 100755 --- a/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php +++ b/components/ILIAS/Survey/Settings/class.SettingsFormGUI.php @@ -375,7 +375,7 @@ public function withAccess( // anonymization if ($feature_config->supportsAccessCodes()) { - $codes = new \ilCheckboxInputGUI($lng->txt("survey_access_codes"), "acc_codes"); + $codes = new \ilCheckboxInputGUI($lng->txt("survey_access_code"), "acc_codes"); $codes->setInfo($lng->txt("survey_access_codes_info")); $codes->setChecked(!$survey->isAccessibleWithoutCode()); $form->addItem($codes); diff --git a/components/ILIAS/Taxonomy/GlobalScreen/classes/class.ilTaxonomyGSToolProvider.php b/components/ILIAS/Taxonomy/GlobalScreen/classes/class.ilTaxonomyGSToolProvider.php index 2cd06d51fded..e6bc46238de0 100755 --- a/components/ILIAS/Taxonomy/GlobalScreen/classes/class.ilTaxonomyGSToolProvider.php +++ b/components/ILIAS/Taxonomy/GlobalScreen/classes/class.ilTaxonomyGSToolProvider.php @@ -91,7 +91,9 @@ private function getEditTree(array $gui_path, int $tax_id, string $cmd, string $ $gui = $this->dic->taxonomy()->internal()->gui(); $params = $gui->http()->request()->getQueryParams(); $current_tax_node = (int) ($params["tax_node"] ?? null); - $tax_exp->setPathOpen($current_tax_node); + if ($current_tax_node > 0) { + $tax_exp->setPathOpen($current_tax_node); + } return $tax_exp->getHTML(); diff --git a/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php b/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php index ccdb44413a7b..a57d70ae78bc 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestFolderGUI.php @@ -22,13 +22,15 @@ use ILIAS\Test\RequestDataCollector; use ILIAS\Test\Logging\TestLogViewer; use ILIAS\Test\Logging\LogTable; +use ILIAS\Questions\Units\GlobalConfigurationGUI; +use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Data\Factory as DataFactory; use ILIAS\UI\URLBuilder; use ILIAS\UI\Component\Input\Container\Form\Form; /** * @author Helmut Schottmüller - * @ilCtrl_Calls ilObjTestFolderGUI: ilPermissionGUI, ilGlobalUnitConfigurationGUI + * @ilCtrl_Calls ilObjTestFolderGUI: ilPermissionGUI, ILIAS\Questions\Units\GlobalConfigurationGUI */ class ilObjTestFolderGUI extends ilObjectGUI { @@ -36,6 +38,8 @@ class ilObjTestFolderGUI extends ilObjectGUI private RequestDataCollector $testrequest; private TestLogViewer $log_viewer; + private ilDBInterface $db; + private ilHelpGUI $help; private DataFactory $data_factory; @@ -47,6 +51,8 @@ public function __construct( ) { global $DIC; $rbacsystem = $DIC['rbacsystem']; + $this->db = $DIC['ilDB']; + $this->help = $DIC['ilHelp']; $this->data_factory = new DataFactory(); $local_dic = TestDIC::dic(); @@ -82,15 +88,25 @@ public function executeCommand(): void $perm_gui = new \ilPermissionGUI($this); $this->ctrl->forwardCommand($perm_gui); break; - case 'ilglobalunitconfigurationgui': + case strtolower(GlobalConfigurationGUI::class): if (!$this->rbac_system->checkAccess('read', $this->getTestFolder()->getRefId())) { $this->ilias->raiseError($this->lng->txt('permission_denied'), $this->ilias->error_obj->WARNING); } $this->tabs_gui->setTabActive('units'); - $gui = new \ilGlobalUnitConfigurationGUI( - new \ilUnitConfigurationRepository(0) + $gui = new GlobalConfigurationGUI( + new UnitsRepository( + $this->lng, + $this->db + ), + $this->lng, + $this->ctrl, + $this->rbac_system, + $this->tpl, + $this->toolbar, + $this->tabs_gui, + $this->help ); $this->ctrl->forwardCommand($gui); break; @@ -292,7 +308,7 @@ protected function getTabs(): void $this->tabs_gui->addTarget( 'units', - $this->ctrl->getLinkTargetByClass('ilGlobalUnitConfigurationGUI', ''), + $this->ctrl->getLinkTargetByClass(GlobalConfigurationGUI::class, ''), '', 'ilglobalunitconfigurationgui' ); diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 7a59ac284581..e076ac66bacb 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -64,6 +64,8 @@ use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; +use ILIAS\Questions\Units\LocalConfigurationGUI as LocalUnitsConfigurationGUI; +use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Data\Factory as DataFactory; use ILIAS\UI\Component\Modal\Modal; use ILIAS\UI\URLBuilder; @@ -105,12 +107,12 @@ * @ilCtrl_Calls ilObjTestGUI: assOrderingQuestionGUI, assImagemapQuestionGUI, assNumericGUI, assErrorTextGUI * @ilCtrl_Calls ilObjTestGUI: assTextSubsetGUI, assOrderingHorizontalGUI * @ilCtrl_Calls ilObjTestGUI: assSingleChoiceGUI, assFileUploadGUI, assTextQuestionGUI - * @ilCtrl_Calls ilObjTestGUI: assKprimChoiceGUI, assLongMenuGUI + * @ilCtrl_Calls ilObjTestGUI: assKprimChoiceGUI, assLongMenuGUI, assFormulaQuestionGUI * @ilCtrl_Calls ilObjTestGUI: ilEditClipboardGUI * @ilCtrl_Calls ilObjTestGUI: ILIAS\Test\Settings\MainSettings\SettingsMainGUI, ILIAS\Test\Settings\ScoreReporting\SettingsScoringGUI * @ilCtrl_Calls ilObjTestGUI: ilCommonActionDispatcherGUI * @ilCtrl_Calls ilObjTestGUI: ilTestFixedQuestionSetConfigGUI, ilTestRandomQuestionSetConfigGUI - * @ilCtrl_Calls ilObjTestGUI: ilAssQuestionFeedbackEditingGUI, ilLocalUnitConfigurationGUI, assFormulaQuestionGUI + * @ilCtrl_Calls ilObjTestGUI: ilAssQuestionFeedbackEditingGUI, ILIAS\Questions\Units\LocalConfigurationGUI * @ilCtrl_Calls ilObjTestGUI: ilTestPassDetailsOverviewTableGUI * @ilCtrl_Calls ilObjTestGUI: ilTestCorrectionsGUI * @ilCtrl_Calls ilObjTestGUI: ilTestSettingsChangeConfirmationGUI @@ -174,6 +176,7 @@ class ilObjTestGUI extends ilObjectGUI implements ilCtrlBaseClassInterface, ilDe protected MarkSchemaFactory $mark_schema_factory; protected AdditionalInformationGenerator $additional_information_generator; protected PersonalSettingsExporter $personal_settings_exporter; + protected readonly UnitsRepository $units_repository; protected ?QuestionsTableQuery $table_query = null; protected ?QuestionsTableActions $table_actions = null; protected DataFactory $data_factory; @@ -234,6 +237,7 @@ public function __construct() $this->mark_schema_factory = $local_dic['marks.factory']; $this->additional_information_generator = $local_dic['logging.information_generator']; $this->personal_settings_exporter = $local_dic['settings.personal_templates.exporter']; + $this->units_repository = $local_dic['units.repository']; $ref_id = 0; if ($this->testrequest->hasRefId() && is_numeric($this->testrequest->getRefId())) { @@ -249,6 +253,7 @@ public function __construct() $this->ctrl->saveParameter($this, ['ref_id', 'test_ref_id']); $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $this->objective_oriented_container = new ilTestObjectiveOrientedContainer(); @@ -845,8 +850,8 @@ public function executeCommand(): void $this->ctrl->forwardCommand($pg_gui); break; - case 'illocalunitconfigurationgui': - if ((!$this->access->checkAccess("write", "", $this->testrequest->getRefId()))) { + case strtolower(LocalUnitsConfigurationGUI::class): + if ((!$this->access->checkAccess('write', '', $this->testrequest->getRefId()))) { $this->redirectAfterMissingWrite(); } $this->prepareSubGuiOutput(); @@ -857,10 +862,19 @@ public function executeCommand(): void $question->setObjId($this->getTestObject()->getId()); $question_gui->setObject($question); $question_gui->setQuestionTabs(); - $gui = new ilLocalUnitConfigurationGUI( - new ilUnitConfigurationRepository($this->testrequest->getQuestionId()) + + $this->ctrl->forwardCommand( + new LocalUnitsConfigurationGUI( + $this->units_repository, + $this->lng, + $this->ctrl, + $this->rbac_system, + $this->tpl, + $this->toolbar, + $this->tabs_gui, + $this->help + ) ); - $this->ctrl->forwardCommand($gui); break; case "ilcommonactiondispatchergui": @@ -1052,7 +1066,7 @@ protected function forwardCommandToQuestionPreview( $gui->setPrimaryCmd( $this->lng->txt('edit_question'), $this->ctrl->getLinkTargetByClass( - get_class($question_gui), + $question_gui::class, 'editQuestion' ) ); diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 3aca83d2cc2e..8f168765fd0b 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -20,7 +20,6 @@ namespace ILIAS\Test; -use ILIAS\LegalDocuments\ConsumerToolbox\Setting; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\Scoring\Marks\MarkSchemaFactory; @@ -52,6 +51,7 @@ use ILIAS\Test\Results\Toplist\TestTopListRepository; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; +use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Data\Factory as DataFactory; use ILIAS\DI\Container as ILIASContainer; use Pimple\Container as PimpleContainer; @@ -256,6 +256,12 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['participant.repository'] = static fn($c): ParticipantRepository => new ParticipantRepository($DIC['ilDB']); + $dic['units.repository'] = static fn($c): UnitsRepository => + new UnitsRepository( + $DIC['lng'], + $DIC['ilDB'] + ); + $dic['gui.factory'] = static fn($c): GUIFactory => new GUIFactory($DIC, $c); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index f47d721ccda8..fb96dbfc89c6 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -19,7 +19,9 @@ declare(strict_types=1); use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; +use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Questions\Units\Repository as UnitsRepository; /** * Class for single choice questions @@ -33,7 +35,7 @@ class assFormulaQuestion extends assQuestion implements iQuestionCondition, Ques private array $variables; private array $results; private array $resultunits; - private ilUnitConfigurationRepository $unitrepository; + private UnitsRepository $unitrepository; protected PassPresentedVariablesRepo $pass_presented_variables_repo; public function __construct( @@ -47,7 +49,7 @@ public function __construct( $this->variables = []; $this->results = []; $this->resultunits = []; - $this->unitrepository = new ilUnitConfigurationRepository(0); + $this->unitrepository = QuestionPoolDIC::dic()['units.repository']; $this->pass_presented_variables_repo = new PassPresentedVariablesRepo($this->db); } @@ -705,8 +707,6 @@ public function loadFromDb(int $question_id): void } catch (ilTestQuestionPoolException $e) { } - $this->unitrepository = new ilUnitConfigurationRepository($question_id); - $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data["question_text"], 1)); // load variables @@ -1118,7 +1118,7 @@ public function getBestSolution(array $solutions): array isset($available_units[$result_name]) && in_array($user_solution[$result_name]['unit'] ?? null, $available_units[$result_name]) ) { - $unit_factor = assFormulaQuestionUnit::lookupUnitFactor($user_solution[$result_name]['unit']); + $unit_factor = $this->unitrepository->lookupUnitFactor($user_solution[$result_name]['unit']); } try { @@ -1147,15 +1147,9 @@ public function getBestSolution(array $solutions): array public function setId(int $id = -1): void { parent::setId($id); - $this->unitrepository->setConsumerId($this->getId()); - } - - public function setUnitrepository(\ilUnitConfigurationRepository $unitrepository): void - { - $this->unitrepository = $unitrepository; } - public function getUnitrepository(): ilUnitConfigurationRepository + public function getUnitrepository(): UnitsRepository { return $this->unitrepository; } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php index d56216e7f4ad..cf7a895bf786 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php @@ -16,6 +16,7 @@ * *********************************************************************/ +use ILIAS\Questions\Units\LocalConfigurationGUI as LocalUnitsConfigurationGUI; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; @@ -56,8 +57,17 @@ public function __construct($id = -1) protected function setQuestionSpecificTabs(ilTabsGUI $ilTabs): void { - $this->ctrl->setParameterByClass(ilLocalUnitConfigurationGUI::class, 'q_id', $this->object->getId()); - $ilTabs->addTarget('units', $this->ctrl->getLinkTargetByClass(ilLocalUnitConfigurationGUI::class, ''), '', 'illocalunitconfigurationgui'); + $this->ctrl->setParameterByClass( + LocalUnitsConfigurationGUI::class, + 'q_id', + $this->object->getId() + ); + + $ilTabs->addTab( + 'units', + $this->lng->txt('units'), + $this->ctrl->getLinkTargetByClass(LocalUnitsConfigurationGUI::class, ''), + ); } public function suggestRange(): void @@ -221,7 +231,9 @@ public function editQuestion( $question->setInfo($this->lng->txt('fq_question_desc')); $variables = $this->object->getVariables(); - $categorized_units = $this->object->getUnitrepository()->getCategorizedUnits(); + $categorized_units = $this->object->getUnitrepository()->getCategorizedUnits( + $this->object->getId() + ); $result_units = $this->object->getAllResultUnits(); $unit_options = []; @@ -232,13 +244,14 @@ public function editQuestion( * @var $item assFormulaQuestionUnitCategory|assFormulaQuestionUnit */ if ($item instanceof assFormulaQuestionUnitCategory) { - if ($category_name != $item->getDisplayString()) { + if ($category_name !== $item->getDisplayString($this->lng)) { $new_category = true; - $category_name = $item->getDisplayString(); + $category_name = $item->getDisplayString($this->lng); } continue; } - $unit_options[$item->getId()] = $item->getDisplayString() . ($new_category ? ' (' . $category_name . ')' : ''); + $unit_options[$item->getId()] = $item->getDisplayString($this->lng) + . ($new_category ? ' (' . $category_name . ')' : ''); $new_category = false; } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php index ba7b5c1ef0c2..8f3a80954dea 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssQuestionPage.php @@ -16,17 +16,15 @@ * *********************************************************************/ -/** - * Question page object - * - * @author Alex Killing - * - * @version $Id$ - * - * @ingroup components\ILIASTestQuestionPool - */ +declare(strict_types=1); + +use ILIAS\Questions\Question\QuestionImplementation; +use ILIAS\Data\UUID\Uuid; + class ilAssQuestionPage extends ilPageObject { + private readonly QuestionImplementation $question; + /** * Get parent type * @return string parent type @@ -35,4 +33,93 @@ public function getParentType(): string { return "qpl"; } + + public function setQuestion( + QuestionImplementation $question + ): void { + $this->question = $question; + } + + public function copyToAnswerForm( + int $new_id, + QuestionImplementation $question + ): void { + $this->buildDom(); + $this->migrateQuestionElementToAnswerForm(); + + $new_page_object = new QstsQuestionPage(); + $new_page_object->setParentId($this->getParentId()); + $new_page_object->setId($new_id); + $new_page_object->setXMLContent($this->copyXMLContent(false, $this->getParentId())); + $new_page_object->setActive($this->getActive()); + $new_page_object->setActivationStart($this->getActivationStart()); + $new_page_object->setActivationEnd($this->getActivationEnd()); + $new_page_object->setQuestion($question); + $new_page_object->create(false); + } + + private function migrateQuestionElementToAnswerForm(): void + { + global $DIC; + $dom_util = $DIC->copage()->internal()->domain()->domUtil(); + + /** @var \ILIAS\Questions\AnswerForm\Properties $answer_form_properties */ + $answer_form_properties = $this->question->getAnswerFormProperties(); + + $dom_util->path($this->getDomDoc(), '//Question') + ->item(0)->parentNode->replaceWith( + $this->buildLegacyAnswerFormTextNode(), + $this->buildAnswerFormNode( + reset($answer_form_properties)->getAnswerFormId() + ) + ); + $this->xml = $this->getXMLFromDom(); + } + + private function buildLegacyAnswerFormTextNode(): DOMNode + { + $legacy_answer_form_text_node = new ilPCLegacyAnswerFormText($this); + $legacy_answer_form_text_node->createPageContentNode(); + $legacy_answer_form_text_node->writePCId($this->generatePCId()); + $legacy_answer_form_text_node->create( + $this->retrieveLegacyPageElementContent() + ); + + return $legacy_answer_form_text_node->getDomNode(); + } + + private function buildAnswerFormNode( + Uuid $answer_form_id + ): DOMNode { + $answer_form_node = new ilPCAnswerForm($this); + $answer_form_node->createPageContentNode(); + $answer_form_node->writePCId($this->generatePCId()); + $answer_form_node->create($answer_form_id); + + return $answer_form_node->getDomNode(); + } + + private function retrieveLegacyPageElementContent(): string + { + $question_info = $this->db->fetchObject( + $this->db->query( + "SELECT add_cont_edit_mode, question_text FROM qpl_questions WHERE question_id = {$this->id}" + ) + ); + + $purified_content = ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution') + ->purify($question_info->question_text); + + if ($question_info->add_cont_edit_mode === assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE + || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') { + $purified_content = nl2br($purified_content); + } + return base64_encode( + ilLegacyFormElementsUtil::prepareTextareaOutput( + $purified_content, + true, + true + ) + ); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilLocalUnitConfigurationGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilLocalUnitConfigurationGUI.php deleted file mode 100755 index 5333b2657708..000000000000 --- a/components/ILIAS/TestQuestionPool/classes/class.ilLocalUnitConfigurationGUI.php +++ /dev/null @@ -1,181 +0,0 @@ -isCRUDContext()) { - return 'showLocalUnitCategories'; - } - - return 'showGlobalUnitCategories'; - } - - public function isCRUDContext(): bool - { - if (!$this->request->isset(self::REQUEST_PARAM_SUB_CONTEXT_ID) || - $this->request->raw(self::REQUEST_PARAM_SUB_CONTEXT_ID) == $this->repository->getConsumerId()) { - return true; - } - - return false; - } - - public function getUniqueId(): string - { - $id = $this->repository->getConsumerId(); - if ($this->isCRUDContext()) { - $id .= '_local'; - } else { - $id .= '_global'; - } - - return $id; - } - - public function executeCommand(): void - { - global $DIC; - - /** @var ilHelpGUI $ilHelp */ - $ilHelp = $DIC['ilHelp']; - - $this->ctrl->saveParameter($this, self::REQUEST_PARAM_SUB_CONTEXT_ID); - - $ilHelp->setScreenIdComponent('qpl'); - parent::executeCommand(); - } - - protected function handleSubtabs(): void - { - global $DIC; - - $ilTabs = $DIC->tabs(); - - $this->ctrl->setParameter($this, self::REQUEST_PARAM_SUB_CONTEXT_ID, $this->repository->getConsumerId()); - $ilTabs->addSubTab('view_unit_ctx_local', $this->lng->txt('un_local_units'), $this->ctrl->getLinkTarget($this, 'showLocalUnitCategories')); - $this->ctrl->setParameter($this, self::REQUEST_PARAM_SUB_CONTEXT_ID, 0); - $ilTabs->addSubTab('view_unit_ctx_global', $this->lng->txt('un_global_units'), $this->ctrl->getLinkTarget($this, 'showGlobalUnitCategories')); - $this->ctrl->setParameter($this, self::REQUEST_PARAM_SUB_CONTEXT_ID, ''); - - if ($this->isCRUDContext()) { - $ilTabs->activateSubTab('view_unit_ctx_local'); - } else { - $ilTabs->activateSubTab('view_unit_ctx_global'); - } - } - - protected function showLocalUnitCategories(): void - { - global $DIC; - - $ilToolbar = $DIC->toolbar(); - - $ilToolbar->addButton($this->lng->txt('un_add_category'), $this->ctrl->getLinkTarget($this, 'showUnitCategoryCreationForm')); - - $repo = $this->repository; - $categories = array_filter( - $this->repository->getAllUnitCategories(), - static function (assFormulaQuestionUnitCategory $category) use ($repo): bool { - return $category->getQuestionFi() === $repo->getConsumerId(); - } - ); - $data = []; - foreach ($categories as $category) { - /** @var assFormulaQuestionUnitCategory $category */ - $data[] = [ - 'category_id' => $category->getId(), - 'category' => $category->getDisplayString() - ]; - } - - $this->showUnitCategories($data); - } - - /** - * @param array $categories - */ - protected function showUnitCategories(array $categories): void - { - $table = new ilLocalUnitCategoryTableGUI($this, $this->getUnitCategoryOverviewCommand()); - $table->setData($categories); - - $this->tpl->setContent($table->getHTML()); - } - - protected function confirmImportGlobalCategory(): void - { - if (!$this->request->isset('category_id')) { - $this->showGlobalUnitCategories(); - return; - } - $this->confirmImportGlobalCategories([$this->request->raw('category_id')]); - } - - protected function confirmImportGlobalCategories(array $category_ids): void - { - // @todo: Confirmation Currently not implemented, so forward to import - $category_ids === [] - ? $this->showGlobalUnitCategories() - : $this->importGlobalCategories($category_ids); - } - - protected function importGlobalCategories(array $category_ids): void - { - if ($this->isCRUDContext()) { - $this->{$this->getDefaultCommand()}(); - return; - } - - $i = 0; - foreach ($category_ids as $category_id) { - try { - $category = $this->repository->getUnitCategoryById((int) $category_id); - } catch (ilException $e) { - continue; - } - - // Copy admin-category to custom-category (with question_fi) - $new_cat_id = $this->repository->copyCategory($category->getId(), $this->repository->getConsumerId()); - - // Copy units to custom_category - $this->repository->copyUnitsByCategories($category->getId(), $new_cat_id, $this->repository->getConsumerId()); - ++$i; - } - - if ($i) { - $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'), true); - $this->ctrl->setParameter($this, 'question_fi', $this->request->getQuestionId()); - } - - $this->ctrl->redirect($this, 'showLocalUnitCategories'); - } -} diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 98195c86b9e0..a0a78d0396fd 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -24,6 +24,8 @@ use ILIAS\TestQuestionPool\Questions\Presentation\QuestionTable; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\Test\Settings\GlobalSettings\GlobalTestSettings; +use ILIAS\Questions\Units\LocalConfigurationGUI as LocalUnitsConfigurationGUI; +use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Taxonomy\Service; use ILIAS\UI\Component\Input\Container\Form\Form; use ILIAS\UI\Component\Input\Field\Select; @@ -54,7 +56,7 @@ * @ilCtrl_Calls ilObjQuestionPoolGUI: assNumericGUI, assTextSubsetGUI, assSingleChoiceGUI, ilPropertyFormGUI * @ilCtrl_Calls ilObjQuestionPoolGUI: assTextQuestionGUI, ilObjectMetaDataGUI, ilPermissionGUI, ilObjectCopyGUI * @ilCtrl_Calls ilObjQuestionPoolGUI: ilExportGUI, ilInfoScreenGUI, ilTaxonomySettingsGUI, ilCommonActionDispatcherGUI - * @ilCtrl_Calls ilObjQuestionPoolGUI: ilAssQuestionFeedbackEditingGUI, ilLocalUnitConfigurationGUI + * @ilCtrl_Calls ilObjQuestionPoolGUI: ilAssQuestionFeedbackEditingGUI, ILIAS\Questions\Units\LocalConfigurationGUI * @ilCtrl_Calls ilObjQuestionPoolGUI: ilObjQuestionPoolSettingsGeneralGUI, assFormulaQuestionGUI * @ilCtrl_Calls ilObjQuestionPoolGUI: ilAssQuestionPreviewGUI * @ilCtrl_Calls ilObjQuestionPoolGUI: assKprimChoiceGUI, assLongMenuGUI @@ -95,6 +97,7 @@ class ilObjQuestionPoolGUI extends ilObjectGUI implements ilCtrlBaseClassInterfa protected RequestDataCollector $request_data_collector; protected GeneralQuestionPropertiesRepository $questionrepository; protected GlobalTestSettings $global_test_settings; + protected UnitsRepository $units_repository; public function __construct() { @@ -121,6 +124,7 @@ public function __construct() $this->request_data_collector = $local_dic['request_data_collector']; $this->questionrepository = $local_dic['question.general_properties.repository']; $this->global_test_settings = $local_dic['global_test_settings']; + $this->units_repository = $local_dic['units.repository']; parent::__construct('', $this->request_data_collector->getRefId(), true, false); @@ -136,6 +140,7 @@ public function __construct() $this->ctrl->saveParameterByClass('ilobjquestionpoolgui', 'consumer_context'); $this->lng->loadLanguageModule('assessment'); + $this->lng->loadLanguageModule('qsts'); $here_uri = $this->data_factory->uri($this->request->getUri()->__toString()); $url_builder = new URLBuilder($here_uri); @@ -336,7 +341,7 @@ public function executeCommand(): void $this->infoScreenForward(); break; - case 'illocalunitconfigurationgui': + case strtolower(LocalUnitsConfigurationGUI::class): if (!$this->access->checkAccess('write', '', $this->object->getRefId())) { $this->error->raiseError($this->lng->txt('permission_denied'), $this->error->WARNING); } @@ -350,8 +355,15 @@ public function executeCommand(): void $question_gui->setQuestionTabs(); $this->ctrl->setReturn($this, self::DEFAULT_CMD); - $gui = new ilLocalUnitConfigurationGUI( - new ilUnitConfigurationRepository($this->request_data_collector->getQuestionId()) + $gui = new LocalUnitsConfigurationGUI( + $this->units_repository, + $this->lng, + $this->ctrl, + $this->rbac_system, + $this->tpl, + $this->toolbar, + $this->tabs_gui, + $this->help ); $this->ctrl->forwardCommand($gui); break; diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationRepository.php b/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationRepository.php deleted file mode 100755 index 3f8ddeb4a8f8..000000000000 --- a/components/ILIAS/TestQuestionPool/classes/class.ilUnitConfigurationRepository.php +++ /dev/null @@ -1,739 +0,0 @@ -language(); - - $this->db = $DIC->database(); - $this->consumer_id = $consumer_id; - $this->lng = $lng; - } - - public function setConsumerId(int $consumer_id): void - { - $this->consumer_id = $consumer_id; - } - - public function getConsumerId(): int - { - return $this->consumer_id; - } - - public function isCRUDAllowed(int $category_id): bool - { - $res = $this->db->queryF( - 'SELECT * FROM il_qpl_qst_fq_ucat WHERE category_id = %s', - ['integer'], - [$category_id] - ); - $row = $this->db->fetchAssoc($res); - return isset($row['question_fi']) && (int) $row['question_fi'] === $this->getConsumerId(); - } - - public function copyCategory(int $category_id, int $question_fi, ?string $category_name = null): int - { - $res = $this->db->queryF( - 'SELECT category FROM il_qpl_qst_fq_ucat WHERE category_id = %s', - ['integer'], - [$category_id] - ); - $row = $this->db->fetchAssoc($res); - - if (null === $category_name) { - $category_name = $row['category']; - } - - $next_id = $this->db->nextId('il_qpl_qst_fq_ucat'); - $this->db->insert( - 'il_qpl_qst_fq_ucat', - [ - 'category_id' => ['integer', $next_id], - 'category' => ['text', $category_name], - 'question_fi' => ['integer', (int) $question_fi] - ] - ); - - return $next_id; - } - - public function copyUnitsByCategories(int $from_category_id, int $to_category_id, int $qustion_fi): void - { - $res = $this->db->queryF( - 'SELECT * FROM il_qpl_qst_fq_unit WHERE category_fi = %s', - ['integer'], - [$from_category_id] - ); - $i = 0; - $units = []; - while ($row = $this->db->fetchAssoc($res)) { - $next_id = $this->db->nextId('il_qpl_qst_fq_unit'); - - $units[$i]['old_unit_id'] = $row['unit_id']; - $units[$i]['new_unit_id'] = $next_id; - - $this->db->insert( - 'il_qpl_qst_fq_unit', - [ - 'unit_id' => ['integer', $next_id], - 'unit' => ['text', $row['unit']], - 'factor' => ['float', $row['factor']], - 'baseunit_fi' => ['integer', (int) $row['baseunit_fi']], - 'category_fi' => ['integer', (int) $to_category_id], - 'sequence' => ['integer', (int) $row['sequence']], - 'question_fi' => ['integer', (int) $qustion_fi] - ] - ); - $i++; - } - - foreach ($units as $unit) { - //update unit : baseunit_fi - $this->db->update( - 'il_qpl_qst_fq_unit', - ['baseunit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'baseunit_fi' => ['integer', $unit['old_unit_id']], - 'category_fi' => ['integer', $to_category_id] - ] - ); - - //update var : unit_fi - $this->db->update( - 'il_qpl_qst_fq_var', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', $unit['old_unit_id']], - 'question_fi' => ['integer', $qustion_fi] - ] - ); - - //update res : unit_fi - $this->db->update( - 'il_qpl_qst_fq_res', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', $unit['old_unit_id']], - 'question_fi' => ['integer', $qustion_fi] - ] - ); - - //update res_unit : unit_fi - $this->db->update( - 'il_qpl_qst_fq_res_unit', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', $unit['old_unit_id']], - 'question_fi' => ['integer', $qustion_fi] - ] - ); - } - } - - public function getCategoryUnitCount(int $id): int - { - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_unit WHERE category_fi = %s", - ['integer'], - [$id] - ); - - return $this->db->numRows($result); - } - - public function isUnitInUse(int $id): bool - { - $result_1 = $this->db->queryF( - "SELECT unit_fi FROM il_qpl_qst_fq_res_unit WHERE unit_fi = %s", - ['integer'], - [$id] - ); - - $result_2 = $this->db->queryF( - "SELECT unit_fi FROM il_qpl_qst_fq_var WHERE unit_fi = %s", - ['integer'], - [$id] - ); - $result_3 = $this->db->queryF( - "SELECT unit_fi FROM il_qpl_qst_fq_res WHERE unit_fi = %s", - ['integer'], - [$id] - ); - - $cnt_1 = $this->db->numRows($result_1); - $cnt_2 = $this->db->numRows($result_2); - $cnt_3 = $this->db->numRows($result_3); - - return $cnt_1 > 0 || $cnt_2 > 0 || $cnt_3 > 0; - } - - public function checkDeleteCategory(int $id): ?string - { - $res = $this->db->queryF( - 'SELECT unit_id FROM il_qpl_qst_fq_unit WHERE category_fi = %s', - ['integer'], - [$id] - ); - - if ($this->db->numRows($res)) { - while ($row = $this->db->fetchAssoc($res)) { - $unit_res = $this->checkDeleteUnit((int) $row['unit_id'], $id); - if (!is_null($unit_res)) { - return $unit_res; - } - } - } - - return null; - } - - public function deleteUnit(int $id): ?string - { - $res = $this->checkDeleteUnit($id); - if (!is_null($res)) { - return $res; - } - - $affectedRows = $this->db->manipulateF( - "DELETE FROM il_qpl_qst_fq_unit WHERE unit_id = %s", - ['integer'], - [$id] - ); - - if ($affectedRows > 0) { - $this->clearUnits(); - } - - return null; - } - - protected function loadUnits(): void - { - $result = $this->db->query( - " - SELECT units.*, il_qpl_qst_fq_ucat.category, baseunits.unit baseunit_title - FROM il_qpl_qst_fq_unit units - INNER JOIN il_qpl_qst_fq_ucat ON il_qpl_qst_fq_ucat.category_id = units.category_fi - LEFT JOIN il_qpl_qst_fq_unit baseunits ON baseunits.unit_id = units.baseunit_fi - ORDER BY il_qpl_qst_fq_ucat.category, units.sequence" - ); - - if ($this->db->numRows($result)) { - while ($row = $this->db->fetchAssoc($result)) { - $unit = new assFormulaQuestionUnit(); - $unit->initFormArray($row); - $this->addUnit($unit); - } - } - } - - /** - * @return assFormulaQuestionUnit[]|assFormulaQuestionUnitCategory[] - */ - public function getCategorizedUnits(): array - { - if (count($this->categorizedUnits) === 0) { - $result = $this->db->queryF( - " - SELECT units.*, il_qpl_qst_fq_ucat.category, il_qpl_qst_fq_ucat.question_fi, baseunits.unit baseunit_title - FROM il_qpl_qst_fq_unit units - INNER JOIN il_qpl_qst_fq_ucat ON il_qpl_qst_fq_ucat.category_id = units.category_fi - LEFT JOIN il_qpl_qst_fq_unit baseunits ON baseunits.unit_id = units.baseunit_fi - WHERE units.question_fi = %s - ORDER BY il_qpl_qst_fq_ucat.category, units.sequence", - ['integer'], - [$this->getConsumerId()] - ); - - if ($this->db->numRows($result) > 0) { - $category = 0; - while ($row = $this->db->fetchAssoc($result)) { - $unit = new assFormulaQuestionUnit(); - $unit->initFormArray($row); - - if ($category !== $unit->getCategory()) { - $cat = new assFormulaQuestionUnitCategory(); - $cat->initFormArray([ - 'category_id' => (int) $row['category_fi'], - 'category' => $row['category'], - 'question_fi' => (int) $row['question_fi'], - ]); - $this->categorizedUnits[] = $cat; - $category = $unit->getCategory(); - } - - $this->categorizedUnits[] = $unit; - } - } - } - - return $this->categorizedUnits; - } - - protected function clearUnits(): void - { - $this->units = []; - } - - protected function addUnit(assFormulaQuestionUnit $unit): void - { - $this->units[$unit->getId()] = $unit; - } - - /** - * @return assFormulaQuestionUnit[] - */ - public function getUnits(): array - { - if (count($this->units) === 0) { - $this->loadUnits(); - } - return $this->units; - } - - /** - * @param int $category - * @return assFormulaQuestionUnit[] - */ - public function loadUnitsForCategory(int $category): array - { - global $DIC; - $ilDB = $DIC['ilDB']; - - $units = []; - $result = $ilDB->queryF( - "SELECT units.*, baseunits.unit baseunit_title, il_qpl_qst_fq_ucat.category - FROM il_qpl_qst_fq_unit units - INNER JOIN il_qpl_qst_fq_ucat ON il_qpl_qst_fq_ucat.category_id = units.category_fi - LEFT JOIN il_qpl_qst_fq_unit baseunits ON baseunits.unit_id = units.baseunit_fi - WHERE il_qpl_qst_fq_ucat.category_id = %s - ORDER BY units.sequence", - ['integer'], - [$category] - ); - - if ($result->numRows() > 0) { - while ($row = $ilDB->fetchAssoc($result)) { - $unit = new assFormulaQuestionUnit(); - $unit->initFormArray($row); - $units[] = $unit; - } - } - - return $units; - } - - /** - * @param int $id - * @return assFormulaQuestionUnit|null - */ - public function getUnit(int $id): ?assFormulaQuestionUnit - { - if (count($this->units) === 0) { - $this->loadUnits(); - } - - if (array_key_exists($id, $this->units)) { - return $this->units[$id]; - } - - // Maybe this is a new unit, reload $this->units - - $this->loadUnits(); - - return $this->units[$id] ?? null; - } - - /** - * @return array - */ - public function getUnitCategories(): array - { - $categories = []; - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_ucat WHERE question_fi > %s ORDER BY category", - ['integer'], - [0] - ); - - if ($this->db->numRows($result)) { - while ($row = $this->db->fetchAssoc($result)) { - $value = strcmp('-qpl_qst_formulaquestion_' . $row['category'] . '-', $this->lng->txt($row['category'])) === 0 - ? $row['category'] - : $this->lng->txt($row['category']); - - if (trim($row['category']) !== '') { - $cat = [ - 'value' => (int) $row['category_id'], - 'text' => $value, - 'qst_id' => (int) $row['question_fi'] - ]; - $categories[(int) $row['category_id']] = $cat; - } - } - } - - return $categories; - } - - /** - * @return array - */ - public function getAdminUnitCategories(): array - { - $categories = []; - - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_ucat WHERE question_fi = %s ORDER BY category", - ['integer'], - [0] - ); - - if ($result = $this->db->numRows($result)) { - while ($row = $this->db->fetchAssoc($result)) { - $value = strcmp('-qpl_qst_formulaquestion_' . $row['category'] . '-', $this->lng->txt($row['category'])) === 0 - ? $row['category'] - : $this->lng->txt($row['category']); - - if (trim($row['category']) !== '') { - $cat = [ - 'value' => (int) $row['category_id'], - 'text' => $value, - 'qst_id' => (int) $row['question_fi'] - ]; - $categories[(int) $row['category_id']] = $cat; - } - } - } - - return $categories; - } - - public function saveUnitOrder(int $unit_id, int $sequence): void - { - $this->db->manipulateF( - 'UPDATE il_qpl_qst_fq_unit SET sequence = %s WHERE unit_id = %s AND question_fi = %s', - ['integer', 'integer', 'integer'], - [$sequence, $unit_id, $this->getConsumerId()] - ); - } - - public function checkDeleteUnit(int $id, ?int $category_id = null): ?string - { - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_var WHERE unit_fi = %s", - ['integer'], - [$id] - ); - if ($this->db->numRows($result) > 0) { - return $this->lng->txt("err_unit_in_variables"); - } - - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_res WHERE unit_fi = %s", - ['integer'], - [$id] - ); - if ($this->db->numRows($result) > 0) { - return $this->lng->txt("err_unit_in_results"); - } - - if (!is_null($category_id)) { - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_unit WHERE baseunit_fi = %s AND category_fi != %s", - ['integer', 'integer', 'integer'], - [$id, $id, $category_id] - ); - } else { - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_unit WHERE baseunit_fi = %s AND unit_id != %s", - ['integer', 'integer'], - [$id, $id] - ); - } - - if ($this->db->numRows($result) > 0) { - return $this->lng->txt("err_unit_is_baseunit"); - } - - return null; - } - - public function getUnitCategoryById(int $id): assFormulaQuestionUnitCategory - { - $query = 'SELECT * FROM il_qpl_qst_fq_ucat WHERE category_id = ' . $this->db->quote($id, 'integer'); - $res = $this->db->query($query); - if (!$this->db->numRows($res)) { - throw new ilException('un_category_not_exist'); - } - - $row = $this->db->fetchAssoc($res); - $category = new assFormulaQuestionUnitCategory(); - $category->initFormArray($row); - return $category; - } - - public function saveCategory(assFormulaQuestionUnitCategory $category): void - { - $res = $this->db->queryF( - 'SELECT * FROM il_qpl_qst_fq_ucat WHERE category = %s AND question_fi = %s AND category_id != %s', - ['text', 'integer', 'integer'], - [$category->getCategory(), $this->getConsumerId(), $category->getId()] - ); - if ($this->db->numRows($res)) { - throw new ilException('err_wrong_categoryname'); - } - - $this->db->manipulateF( - 'UPDATE il_qpl_qst_fq_ucat SET category = %s WHERE question_fi = %s AND category_id = %s', - ['text', 'integer', 'integer'], - [$category->getCategory(), $this->getConsumerId(), $category->getId()] - ); - } - - public function saveNewUnitCategory(assFormulaQuestionUnitCategory $category): void - { - $res = $this->db->queryF( - 'SELECT category FROM il_qpl_qst_fq_ucat WHERE category = %s AND question_fi = %s', - ['text', 'integer'], - [$category->getCategory(), $this->getConsumerId()] - ); - if ($this->db->numRows($res)) { - throw new ilException('err_wrong_categoryname'); - } - - $next_id = $this->db->nextId('il_qpl_qst_fq_ucat'); - $this->db->manipulateF( - "INSERT INTO il_qpl_qst_fq_ucat (category_id, category, question_fi) VALUES (%s, %s, %s)", - ['integer', 'text', 'integer'], - [ - $next_id, - $category->getCategory(), - $this->getConsumerId() - ] - ); - $category->setId($next_id); - } - - /** - * @return assFormulaQuestionUnitCategory[] - */ - public function getAllUnitCategories(): array - { - $categories = []; - $result = $this->db->queryF( - "SELECT * FROM il_qpl_qst_fq_ucat WHERE question_fi = %s OR question_fi = %s ORDER BY category", - ['integer', 'integer'], - [$this->getConsumerId(), 0] - ); - - if ($result->numRows() > 0) { - while ($row = $this->db->fetchAssoc($result)) { - $category = new assFormulaQuestionUnitCategory(); - $category->initFormArray($row); - $categories[] = $category; - } - } - return $categories; - } - - public function deleteCategory(int $id): ?string - { - $res = $this->checkDeleteCategory($id); - if (!is_null($res)) { - return $this->lng->txt('err_category_in_use'); - } - - $res = $this->db->queryF( - 'SELECT * FROM il_qpl_qst_fq_unit WHERE category_fi = %s', - ['integer'], - [$id] - ); - while ($row = $this->db->fetchAssoc($res)) { - $this->deleteUnit((int) $row['unit_id']); - } - - $ar = $this->db->manipulateF( - 'DELETE FROM il_qpl_qst_fq_ucat WHERE category_id = %s', - ['integer'], - [$id] - ); - - if ($ar > 0) { - $this->clearUnits(); - } - - return null; - } - - public function createNewUnit(assFormulaQuestionUnit $unit): void - { - $next_id = $this->db->nextId('il_qpl_qst_fq_unit'); - $this->db->manipulateF( - 'INSERT INTO il_qpl_qst_fq_unit (unit_id, unit, factor, baseunit_fi, category_fi, sequence, question_fi) VALUES (%s, %s, %s, %s, %s, %s, %s)', - ['integer', 'text', 'float', 'integer', 'integer', 'integer', 'integer'], - [ - $next_id, - $unit->getUnit(), - 1, - 0, - $unit->getCategory(), - 0, - $this->getConsumerId() - ] - ); - $unit->setId($next_id); - $unit->setFactor(1.0); - $unit->setBaseUnit(0); - $unit->setSequence(0); - - $this->clearUnits(); - } - - public function saveUnit(assFormulaQuestionUnit $unit): void - { - $res = $this->db->queryF( - 'SELECT unit_id FROM il_qpl_qst_fq_unit WHERE unit_id = %s', - ['integer'], - [$unit->getId()] - ); - if ($this->db->numRows($res)) { - $row = $this->db->fetchAssoc($res); - - if ($unit->getBaseUnit() === 0 || $unit->getBaseUnit() === $unit->getId()) { - $unit->setFactor(1); - } - - $ar = $this->db->manipulateF( - 'UPDATE il_qpl_qst_fq_unit SET unit = %s, factor = %s, baseunit_fi = %s, category_fi = %s, sequence = %s WHERE unit_id = %s AND question_fi = %s', - ['text', 'float', 'integer', 'integer', 'integer', 'integer', 'integer'], - [ - $unit->getUnit(), $unit->getFactor(), (int) $unit->getBaseUnit(), - $unit->getCategory(), - $unit->getSequence(), - $unit->getId(), - $this->getConsumerId() - ] - ); - if ($ar > 0) { - $this->clearUnits(); - } - } - } - - public function cloneUnits(int $from_consumer_id, int $to_consumer_id): void - { - $category_mapping = []; - - $res = $this->db->queryF("SELECT * FROM il_qpl_qst_fq_ucat WHERE question_fi = %s", ['integer'], [$from_consumer_id]); - while ($row = $this->db->fetchAssoc($res)) { - $new_category_id = $this->copyCategory((int) $row['category_id'], $to_consumer_id); - $category_mapping[$row['category_id']] = $new_category_id; - } - - foreach ($category_mapping as $old_category_id => $new_category_id) { - $res = $this->db->queryF( - 'SELECT * FROM il_qpl_qst_fq_unit WHERE category_fi = %s', - ['integer'], - [$old_category_id] - ); - - $i = 0; - $units = []; - while ($row = $this->db->fetchAssoc($res)) { - $next_id = $this->db->nextId('il_qpl_qst_fq_unit'); - - $units[$i]['old_unit_id'] = $row['unit_id']; - $units[$i]['new_unit_id'] = $next_id; - - $this->db->insert( - 'il_qpl_qst_fq_unit', - [ - 'unit_id' => ['integer', $next_id], - 'unit' => ['text', $row['unit']], - 'factor' => ['float', $row['factor']], - 'baseunit_fi' => ['integer', (int) $row['baseunit_fi']], - 'category_fi' => ['integer', (int) $new_category_id], - 'sequence' => ['integer', (int) $row['sequence']], - 'question_fi' => ['integer', $to_consumer_id] - ] - ); - $i++; - } - - foreach ($units as $unit) { - //update unit : baseunit_fi - $this->db->update( - 'il_qpl_qst_fq_unit', - ['baseunit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'baseunit_fi' => ['integer', (int) $unit['old_unit_id']], - 'question_fi' => ['integer', $to_consumer_id] - ] - ); - - //update var : unit_fi - $this->db->update( - 'il_qpl_qst_fq_var', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', (int) $unit['old_unit_id']], - 'question_fi' => ['integer', $to_consumer_id] - ] - ); - - //update res : unit_fi - $this->db->update( - 'il_qpl_qst_fq_res', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', (int) $unit['old_unit_id']], - 'question_fi' => ['integer', $to_consumer_id] - ] - ); - - //update res_unit : unit_fi - $this->db->update( - 'il_qpl_qst_fq_res_unit', - ['unit_fi' => ['integer', (int) $unit['new_unit_id']]], - [ - 'unit_fi' => ['integer', (int) $unit['old_unit_id']], - 'question_fi' => ['integer', $to_consumer_id] - ] - ); - } - } - } -} diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php index 5fb30a0a6003..96da2fea617a 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php @@ -130,7 +130,6 @@ public function fromXML( private function importUnitsAndUnitCategories(ilQTIItem $item): void { - /** @var ilUnitConfigurationRepository $unit_repository */ $unit_repository = $this->object->getUnitrepository(); foreach ($item->getUnitCategoryObjets() as $unit_category) { $old_category_id = $unit_category->getId(); diff --git a/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitCategoryTableGUI.php b/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitCategoryTableGUI.php index b2641a7afd20..7f187b4db19f 100755 --- a/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitCategoryTableGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitCategoryTableGUI.php @@ -14,10 +14,10 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning * - *********************************************************************/ - + * ******************************************************************* */ declare(strict_types=1); +use ILIAS\Questions\Units\ConfigurationGUI; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; @@ -29,16 +29,13 @@ abstract class ilUnitCategoryTableGUI extends ilTable2GUI { private \ILIAS\UI\Factory $ui_factory; private \ILIAS\UI\Renderer $ui_renderer; - private RequestDataCollector $request; private \ILIAS\Refinery\Factory $refinery; - /** - * @param ilUnitConfigurationGUI $controller - * @param string $cmd - */ - public function __construct(ilUnitConfigurationGUI $controller, $cmd) - { + public function __construct( + ConfigurationGUI $controller, + $cmd + ) { /** * @var $ilCtrl ilCtrl */ @@ -70,17 +67,29 @@ public function __construct(ilUnitConfigurationGUI $controller, $cmd) } if ($hasAccess) { if ($this->getParentObject()->isCRUDContext()) { - $this->addMultiCommand('confirmDeleteCategories', $this->lng->txt('delete')); + $this->addMultiCommand( + 'confirmDeleteCategories', + $this->lng->txt('delete') + ); } else { - $this->addMultiCommand('confirmImportGlobalCategories', $this->lng->txt('import')); + $this->addMultiCommand( + 'confirmImportGlobalCategories', + $this->lng->txt('import') + ); } } $this->populateTitle(); - $this->setFormAction($ilCtrl->getFormAction($this->getParentObject(), $cmd)); + $this->setFormAction($ilCtrl->getFormAction( + $this->getParentObject(), + $cmd + )); $this->setSelectAllCheckbox('category_ids[]'); - $this->setRowTemplate('tpl.unit_category_row.html', 'components/ILIAS/TestQuestionPool'); + $this->setRowTemplate( + 'tpl.unit_category_row.html', + 'components/ILIAS/TestQuestionPool' + ); } abstract protected function populateTitle(): void; @@ -92,29 +101,72 @@ public function fillRow(array $row): void { global $DIC; - $row['chb'] = ilLegacyFormElementsUtil::formCheckbox(false, 'category_ids[]', (string) $row['category_id']); + $row['chb'] = ilLegacyFormElementsUtil::formCheckbox( + false, + 'category_ids[]', + (string) $row['category_id'] + ); $actions = []; - $this->ctrl->setParameter($this->getParentObject(), 'category_id', $row['category_id']); - $actions[] = $this->ui_factory->link()->standard($this->lng->txt('un_show_units'), $this->ctrl->getLinkTarget($this->getParentObject(), 'showUnitsOfCategory')); + $this->ctrl->setParameter( + $this->getParentObject(), + 'category_id', + $row['category_id'] + ); + $actions[] = $this->ui_factory->link()->standard( + $this->lng->txt('un_show_units'), + $this->ctrl->getLinkTarget( + $this->getParentObject(), + 'showUnitsOfCategory' + ) + ); $ref_id = $this->request->getRefId(); $type = ilObject::_lookupType($ref_id, true); if ($type === 'assf') { $hasAccess = $DIC->rbac()->system()->checkAccess('edit', $ref_id); } else { - $hasAccess = $DIC->access()->checkAccess('edit', 'showUnitCategoryModificationForm', $ref_id) && - $DIC->access()->checkAccess('edit', 'confirmDeleteCategory', $ref_id); + $hasAccess = $DIC->access()->checkAccess( + 'edit', + 'showUnitCategoryModificationForm', + $ref_id + ) && + $DIC->access()->checkAccess( + 'edit', + 'confirmDeleteCategory', + $ref_id + ); } if ($this->getParentObject()->isCRUDContext()) { if ($hasAccess) { - $actions[] = $this->ui_factory->link()->standard($this->lng->txt('edit'), $this->ctrl->getLinkTarget($this->getParentObject(), 'showUnitCategoryModificationForm')); - $actions[] = $this->ui_factory->link()->standard($this->lng->txt('delete'), $this->ctrl->getLinkTarget($this->getParentObject(), 'confirmDeleteCategory')); + $actions[] = $this->ui_factory->link()->standard( + $this->lng->txt('edit'), + $this->ctrl->getLinkTarget( + $this->getParentObject(), + 'showUnitCategoryModificationForm' + ) + ); + $actions[] = $this->ui_factory->link()->standard( + $this->lng->txt('delete'), + $this->ctrl->getLinkTarget( + $this->getParentObject(), + 'confirmDeleteCategory' + ) + ); } } else { - $actions[] = $this->ui_factory->link()->standard($this->lng->txt('import'), $this->ctrl->getLinkTarget($this->getParentObject(), 'confirmImportGlobalCategory')); + $actions[] = $this->ui_factory->link()->standard( + $this->lng->txt('import'), + $this->ctrl->getLinkTarget( + $this->getParentObject(), + 'confirmImportGlobalCategory' + ) + ); } - $row['title_href'] = $this->ctrl->getLinkTarget($this->getParentObject(), 'showUnitsOfCategory'); + $row['title_href'] = $this->ctrl->getLinkTarget( + $this->getParentObject(), + 'showUnitsOfCategory' + ); $this->ctrl->setParameter($this->getParentObject(), 'category_id', ''); $dropdown = $this->ui_factory->dropdown()->standard($actions)->withLabel($this->lng->txt('actions')); $row['actions'] = $this->ui_renderer->render($dropdown); diff --git a/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitTableGUI.php b/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitTableGUI.php index c394bce4bd4c..e544df1e6dc7 100755 --- a/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitTableGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/tables/class.ilUnitTableGUI.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\Questions\Units\Category; +use ILIAS\Questions\Units\ConfigurationGUI; + /** * Class ilUnitTableGUI */ @@ -28,13 +31,11 @@ class ilUnitTableGUI extends ilTable2GUI private \ILIAS\UI\Factory $ui_factory; private \ILIAS\UI\Renderer $ui_renderer; - /** - * @param ilUnitConfigurationGUI $controller - * @param string $default_cmd - * @param assFormulaQuestionUnitCategory $category - */ - public function __construct(ilUnitConfigurationGUI $controller, $default_cmd, assFormulaQuestionUnitCategory $category) - { + public function __construct( + ConfigurationGUI $controller, + string $default_cmd, + Category $category + ) { /** * @var $ilCtrl ilCtrl * @var $lng ilLanguage @@ -58,7 +59,12 @@ public function __construct(ilUnitConfigurationGUI $controller, $default_cmd, as $this->addCommandButton('saveOrder', $this->lng->txt('un_save_order')); } - $this->setTitle(sprintf($this->lng->txt('un_units_of_category_x'), $category->getDisplayString())); + $this->setTitle( + sprintf( + $this->lng->txt('un_units_of_category_x'), + $category->getDisplayString($this->lng) + ) + ); $this->addColumn($this->lng->txt('un_sequence'), ''); $this->addColumn($this->lng->txt('unit'), ''); diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index f2cce4c0a366..076303cabad8 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -28,6 +28,7 @@ use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Settings\GlobalSettings\Repository as GlobalTestSettingsRepository; use ILIAS\Test\Settings\GlobalSettings\GlobalTestSettings; +use ILIAS\Questions\Units\Repository as UnitsRepository; class QuestionPoolDIC extends PimpleContainer { @@ -65,7 +66,15 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['participant_repository'] = static fn($c): ParticipantRepository => new ParticipantRepository($DIC['ilDB']); $dic['global_test_settings'] = static fn($c): GlobalTestSettings => - (new GlobalTestSettingsRepository($DIC['ilSetting'], new \ilSetting('assessment')))->getGlobalSettings(); + (new GlobalTestSettingsRepository( + $DIC['ilSetting'], + new \ilSetting('assessment') + ))->getGlobalSettings(); + $dic['units.repository'] = static fn($c): UnitsRepository => + new UnitsRepository( + $DIC['lng'], + $DIC['ilDB'] + ); return $dic; } diff --git a/components/ILIAS/Tracking/classes/class.ilLPProgressBlockGUI.php b/components/ILIAS/Tracking/classes/class.ilLPProgressBlockGUI.php index 4cdbcafa6542..80894e93a3c9 100644 --- a/components/ILIAS/Tracking/classes/class.ilLPProgressBlockGUI.php +++ b/components/ILIAS/Tracking/classes/class.ilLPProgressBlockGUI.php @@ -43,6 +43,7 @@ public function __construct() $this->setBlockId('lpprogress_' . $this->ctrl->getContextObjId()); $this->setTitle($this->lng->txt('trac_progress_block_title')); $this->setPresentation(self::PRES_SEC_LEG); + $this->setActions(); } public function getBlockType(): string @@ -81,4 +82,29 @@ protected function getLegacyContent(): string $mode_and_status ]); } + + protected function setActions(): void + { + $read_only_allowed = true; + if ($this->supportsMembers($this->requested_ref_id)) { + $read_only_allowed = ilParticipants::_isParticipant($this->requested_ref_id, $this->user->getId()); + } + if (!ilLearningProgressAccess::checkAccess($this->requested_ref_id, $read_only_allowed)) { + return; + } + $this->ctrl->setParameterByClass(ilLearningProgressGUI::class, 'ref_id', $this->requested_ref_id); + $link = $this->ctrl->getLinkTargetByClass(ilLearningProgressGUI::class); + $this->ctrl->clearParameterByClass(ilLearningProgressGUI::class, 'ref_id'); + $this->addBlockCommand($link, $this->lng->txt('trac_progress_block_details')); + } + + protected function supportsMembers(int $ref_id): bool + { + try { + ilParticipants::getInstance($ref_id); + return true; + } catch (Exception) { + return false; + } + } } diff --git a/components/ILIAS/Tracking/classes/status/class.ilLPStatusTestPassed.php b/components/ILIAS/Tracking/classes/status/class.ilLPStatusTestPassed.php index 5e26a7c0b6ba..f8d991e89de1 100755 --- a/components/ILIAS/Tracking/classes/status/class.ilLPStatusTestPassed.php +++ b/components/ILIAS/Tracking/classes/status/class.ilLPStatusTestPassed.php @@ -19,6 +19,7 @@ declare(strict_types=0); use ILIAS\Test\Results\Data\Repository; +use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\TestDIC; /** @@ -71,9 +72,27 @@ private static function getUserIdsByResultArrayStatus( public static function _getStatusInfo(int $a_obj_id): array { - /** @var Repository $test_result_repository */ - $test_result_repository = TestDIC::dic()['results.data.repository']; - $status_info['results'] = $test_result_repository->getPassedParticipants($a_obj_id); + /** @var ParticipantRepository $participant_repository */ + $participant_repository = TestDIC::dic()['participant.repository']; + $test_id = ilObjTestAccess::_getTestIDFromObjectID($a_obj_id); + + $lp_status = new self($a_obj_id); + $results = []; + + foreach ($participant_repository->getParticipants($test_id) as $participant) { + $user_id = $participant->getUserId(); + $status = $lp_status->determineStatus($a_obj_id, $user_id); + + $results[] = [ + 'user_id' => $user_id, + 'passed' => ($status === self::LP_STATUS_COMPLETED_NUM), + 'failed' => ($status === self::LP_STATUS_FAILED_NUM), + 'in_progress' => ($status === self::LP_STATUS_IN_PROGRESS_NUM), + 'not_attempted' => ($status === self::LP_STATUS_NOT_ATTEMPTED_NUM) + ]; + } + + $status_info['results'] = $results; return $status_info; } diff --git a/components/ILIAS/UI/resources/images/standard/icon_qsts.svg b/components/ILIAS/UI/resources/images/standard/icon_qsts.svg new file mode 100755 index 000000000000..a47443e9b27c --- /dev/null +++ b/components/ILIAS/UI/resources/images/standard/icon_qsts.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js b/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js index 272e61d1a4b0..1c349d92a953 100644 --- a/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js +++ b/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js @@ -801,7 +801,10 @@ var persistence = function() { element.attr('aria-hidden', false); //https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html - element.attr('role', 'region'); + var currentRole = element.attr('role'); + if (!currentRole || currentRole === 'region') { + element.attr('role', 'region'); + } if(isInView && !thrown) { element.trigger('in_view'); //this is most important for async loading of slates, //it triggers the GlobalScreen-Service. @@ -814,8 +817,11 @@ var persistence = function() { additional_disengage: function(){ var entry_id = dom_ref_to_element[this.html_id]; thrown_for[entry_id] = false; - this.getElement().attr('aria-hidden', true); - this.getElement().removeAttr('role', 'region'); + var element = this.getElement(); + element.attr('aria-hidden', true); + if (element.attr('role') === 'region') { + element.removeAttr('role'); + } } }), remover: Object.assign({}, dom_element, { diff --git a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js index da537876f190..0cc38a685ee8 100755 --- a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js +++ b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js @@ -95,7 +95,10 @@ element.attr('aria-hidden', false); //https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html - element.attr('role', 'region'); + var currentRole = element.attr('role'); + if (!currentRole || currentRole === 'region') { + element.attr('role', 'region'); + } if(isInView && !thrown) { element.trigger('in_view'); //this is most important for async loading of slates, //it triggers the GlobalScreen-Service. @@ -108,8 +111,11 @@ additional_disengage: function(){ var entry_id = dom_ref_to_element[this.html_id]; thrown_for[entry_id] = false; - this.getElement().attr('aria-hidden', true); - this.getElement().removeAttr('role', 'region'); + var element = this.getElement(); + element.attr('aria-hidden', true); + if (element.attr('role') === 'region') { + element.removeAttr('role'); + } } }), remover: Object.assign({}, dom_element, { diff --git a/components/ILIAS/UI/resources/js/MainControls/system_info.js b/components/ILIAS/UI/resources/js/MainControls/system_info.js index dfae4a6f2a38..b12c8e0cb79f 100755 --- a/components/ILIAS/UI/resources/js/MainControls/system_info.js +++ b/components/ILIAS/UI/resources/js/MainControls/system_info.js @@ -1,3 +1,18 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + il = il || {}; il.UI = il.UI || {}; il.UI.maincontrols = il.UI.maincontrols || {}; @@ -19,7 +34,7 @@ il.UI.maincontrols = il.UI.maincontrols || {}; maybeShowMoreButton(item, more_button); $(window).resize(() => { if (!calculating) { - maybeShowMoreButton(item); + maybeShowMoreButton(item, more_button); } }); }; diff --git a/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php b/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php index f27efd55ac11..c405aacc3452 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php +++ b/components/ILIAS/UI/src/Component/Symbol/Icon/Standard.php @@ -184,7 +184,7 @@ interface Standard extends Icon public const GCON = 'gcon'; //Group Conversaion public const FILS = 'fils'; //File System Service public const TALA = 'tala'; //Employee Talk Template Admin - public const QST = 'ques'; //Question + public const QSTS = 'qsts'; //Question Component public const GSFO = 'gsfo'; //Footer Administration public const STUS = 'stus'; //Shortlink public const ADMA = 'adma'; //Administration - General Settings diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/Renderer.php index 5871981dfe04..0f3a5194229e 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/Renderer.php @@ -52,11 +52,6 @@ protected function renderStandard(Form\Standard $component, RendererInterface $d $additional_form_actions = $component->getAdditionalFormActions(); foreach ($additional_form_actions as $action => $label) { - $tpl->setCurrentBlock('with_additional_form_action_top'); - $tpl->setVariable('ACTION_TOP', $action); - $tpl->setVariable('ACTION_LABEL_TOP', $label); - $tpl->parseCurrentBlock(); - $tpl->setCurrentBlock('with_additional_form_action_bottom'); $tpl->setVariable('ACTION_BOTTOM', $action); $tpl->setVariable('ACTION_LABEL_BOTTOM', $label); @@ -74,7 +69,6 @@ protected function renderStandard(Form\Standard $component, RendererInterface $d "" ); - $tpl->setVariable("BUTTONS_TOP", $default_renderer->render($main_submit_button)); $tpl->setVariable("BUTTONS_BOTTOM", $default_renderer->render($main_submit_button)); $tpl->setVariable("INPUTS", $default_renderer->render($component->getInputGroup())); diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/GlyphRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/GlyphRendererFactory.php index 417c37e3549f..0b8c11008f5b 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/GlyphRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/GlyphRendererFactory.php @@ -27,8 +27,10 @@ class GlyphRendererFactory extends Render\DefaultRendererFactory { /** - * components which render glyphs inside an HTML - - {BUTTONS_TOP} -
* {TXT_REQUIRED_TOP} diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php index 99f20a3d5fbc..04ce9e3ae1d7 100755 --- a/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Filter/StandardFilterTest.php @@ -184,14 +184,14 @@ public function testRenderActivatedCollapsed(): void @@ -322,14 +322,14 @@ public function testRenderDeactivatedCollapsed(): void @@ -460,14 +460,14 @@ public function testRenderActivatedExpanded(): void @@ -598,14 +598,14 @@ public function testRenderDeactivatedExpanded(): void diff --git a/components/ILIAS/UI/tests/Component/Input/Container/Form/StandardFormTest.php b/components/ILIAS/UI/tests/Component/Input/Container/Form/StandardFormTest.php index fc8bc1fa0b6e..f9db51cac6af 100755 --- a/components/ILIAS/UI/tests/Component/Input/Container/Form/StandardFormTest.php +++ b/components/ILIAS/UI/tests/Component/Input/Container/Form/StandardFormTest.php @@ -126,12 +126,11 @@ public function testRender(): void ]); $r = $this->getDefaultRenderer(); - $html = $this->getDefaultRenderer()->render($form); + $html = $this->brutallyTrimHTML($this->getDefaultRenderer()->render($form)); $expected = $this->brutallyTrimHTML('
-
' . $this->getTextFieldHtml() . '