diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/admissionrules/conditionaladmission/ConditionalAdmission.php | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/admissionrules/conditionaladmission/ConditionalAdmission.php')
| -rw-r--r-- | lib/admissionrules/conditionaladmission/ConditionalAdmission.php | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/lib/admissionrules/conditionaladmission/ConditionalAdmission.php b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php new file mode 100644 index 0000000..640e800 --- /dev/null +++ b/lib/admissionrules/conditionaladmission/ConditionalAdmission.php @@ -0,0 +1,563 @@ +<?php + +/** + * ConditionalAdmission.php + * + * An admission rule that specifies conditions for course admission, like + * degree, study course or semester. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author Thomas Hackl <thomas.hackl@uni-passau.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + +class ConditionalAdmission extends AdmissionRule +{ + // --- ATTRIBUTES --- + + /** + * All conditions that must be fulfilled for successful admission. + */ + public $conditions = []; + + /** + * Grouped conditions that must be fulfilled for successful admission. + */ + public $conditiongroups = []; + + /** + * Conditions that must be fulfilled for successful admission. + */ + public $ungrouped_conditions = []; + + /** + * Quota for grouped conditions + */ + public $quota = []; + + /** + * Are condition groups allowed? + */ + public $conditiongroups_allowed = null; + + /** + * courseset siblings of this rule + */ + public $siblings = []; + + // --- OPERATIONS --- + + /** + * Standard constructor. + * + * @param String $ruleId If this rule has been saved previously, it + * will be loaded from database. + * @return ConditionalAdmission the current object (this). + */ + public function __construct($ruleId='', $courseSetId = '') + { + parent::__construct($ruleId, $courseSetId); + $this->default_message = _('Sie erfüllen nicht die Bedingung: %s'); + if ($ruleId) { + $this->load(); + } else { + $this->id = $this->generateId('conditionaladmissions'); + } + return $this; + } + + /** + * Adds a new UserFilter to this rule. + * + * @param UserFilter $condition + * @param String $group + * @param Int $quota + * @return ConditionalAdmission + */ + public function addCondition($condition, $group = '', $quota = 0) + { + if ($group) { + $this->conditiongroups[$group][$condition->getId()] = $condition; + $this->quota[$group] = $quota; + } else { + $this->ungrouped_conditions[$condition->getId()] = $condition; + } + return $this; + } + + /** + * Deletes the admission rule and all associated data. + */ + public function delete() + { + parent::delete(); + // Delete rule data. + $stmt = DBManager::get()->prepare("DELETE FROM `conditionaladmissions` + WHERE `rule_id`=?"); + $stmt->execute([$this->id]); + // Delete all associated conditions... + foreach ($this->ungrouped_conditions as $condition) { + $condition->delete(); + } + foreach ($this->conditiongroups as $conditions) { + foreach ($conditions as $condition) { + $condition->delete(); + } + } + // ... and their connection to this rule. + $stmt = DBManager::get()->prepare("DELETE `admission_condition`, `admission_conditiongroup` FROM `admission_condition` + LEFT JOIN `admission_conditiongroup` USING( `conditiongroup_id`) + WHERE `rule_id`=?"); + $stmt->execute([$this->id]); + } + + /** + * Gets all users that are matched by thís rule. + * + * @return Array An array containing IDs of users who are matched by + * this rule. + */ + public function getAffectedUsers() + { + $users = []; + foreach ($this->ungrouped_conditions as $condition) { + $users = array_intersect($users, $condition->getAffectedUsers()); + } + foreach ($this->conditiongroups as $conditions) { + foreach ($conditions as $condition) { + $users = array_intersect($users, $condition->getAffectedUsers()); + } + } + return $users; + } + + /** + * Gets all defined conditions. + * + * @return Array + */ + public function getConditions() + { + return $this->conditions; + } + + /** + * Gets all grouped conditiongroups. + * + * @return Array + */ + public function getConditionGroups() + { + return $this->conditiongroups; + } + + /** + * Gets all grouped conditiongroups. + * + * @return Array + */ + public function getUngroupedConditions() + { + return $this->ungrouped_conditions; + } + + /** + * Gets quota for given conditiongroup. + * + * @return Array + */ + public function getQuota($group_id) + { + return $this->quota[$group_id]; + } + + /** + * Gets some text that describes what this AdmissionRule (or respective + * subclass) does. + */ + public static function getDescription() + { + return _('Über eine Menge von Bedingungen kann festgelegt werden, '. + 'wer zur Anmeldung zu den Veranstaltungen des Anmeldesets '. + 'zugelassen ist. Es muss nur eine der Bedingungen erfüllt sein, '. + 'innerhalb einer Bedingung müssen aber alle Datenfelder '. + 'zutreffen.'); + } + + /** + * Return this rule's name. + */ + public static function getName() + { + return _('Bedingte Anmeldung'); + } + + /** + * Gets the template that provides a configuration GUI for this rule. + * + * @return String + */ + public function getTemplate() + { + // Open generic admission rule template. + $tpl = $GLOBALS['template_factory']->open('admission/rules/configure'); + $tpl->set_attribute('rule', $this); + $factory = new Flexi\Factory(__DIR__ . '/templates/'); + // Now open specific template for this rule and insert base template. + $tpl2 = $factory->open('configure'); + $tpl2->set_attribute('rule', $this); + $tpl2->set_attribute('tpl', $tpl->render()); + return $tpl2->render(); + } + + /** + * Helper function for loading data from DB. Generic AdmissionRule data is + * loaded with the parent load() method. + */ + public function load() + { + // Load basic data. + $stmt = DBManager::get()->prepare("SELECT * + FROM `conditionaladmissions` WHERE `rule_id`=? LIMIT 1"); + $stmt->execute([$this->id]); + if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { + $this->message = $current['message']; + $this->startTime = $current['start_time']; + $this->endTime = $current['end_time']; + // Retrieve conditions. + $stmt = DBManager::get()->prepare("SELECT * + FROM `admission_condition` LEFT JOIN `admission_conditiongroup` USING (`conditiongroup_id`) WHERE `rule_id`=?"); + $stmt->execute([$this->id]); + $conditions = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($conditions as $condition) { + $currentCondition = new UserFilter($condition['filter_id']); + if ($condition['conditiongroup_id']) { + $this->conditiongroups[$condition['conditiongroup_id']][$condition['filter_id']] = $currentCondition; + $this->quota[$condition['conditiongroup_id']] = $condition['quota']; + } else { + $this->ungrouped_conditions[$condition['filter_id']] = $currentCondition; + } + } + } + } + + /** + * Checks if condition groups are allowed. + * + * @return Boolean + */ + public function conditiongroupsAllowed() + { + if ($this->conditiongroups_allowed === null) { + foreach ($this->getSiblings() as $rule) { + if (get_class($rule) == 'ParticipantRestrictedAdmission') { + if ($rule->getDistributionTime() > time()) { + $this->conditiongroups_allowed = true; + } + } + } + } + return $this->conditiongroups_allowed; + } + + /** + * Removes condition groups and sets all conditions as ungrouped. + * + */ + public function removeConditionGroups() + { + foreach ($this->conditiongroups as $conditiongroup_id => $conditions) { + foreach ($conditions as $condition_id => $condition) { + $this->ungrouped_conditions[$condition_id] = $condition; + } + } + $this->conditiongroups = []; + } + + /** + * Removes the condition with the given ID from the rule. + * + * @param String $conditionId + * @return ConditionalAdmission + */ + public function removeCondition($conditionId) + { + if (isset($this->ungrouped_conditions[$conditionId])) { + $this->ungrouped_conditions[$conditionId]->delete(); + unset($this->ungrouped_conditions[$conditionId]); + } else { + foreach ($this->conditiongroups as $conditiongroup_id => $conditions) { + if (isset($conditions[$conditionId])) { + $this->conditiongroups[$conditiongroup_id]->conditions[$conditionId]->delete(); + unset($this->conditiongroups[$conditiongroup_id]->conditions[$conditionId]); + } + } + } + return $this; + } + + /** + * Checks whether the given user fulfills the configured + * admission conditions. Only one of the conditions needs to be + * fulfilled (logical operator OR). The fields in a condition are + * in conjunction (logical operator AND). + * + * @param String $userId + * @param String $courseId + * @return Array Array with conditions that have failed. If array + * is empty, everything's all right. + */ + public function ruleApplies($userId, $courseId) + { + $failed = []; + // Check for rule validity time frame. + if ($this->checkTimeFrame()) { + // Check all configured conditions. + foreach ($this->ungrouped_conditions as $condition) { + if (!$condition->isFulfilled($userId)) { + $failed[] = $this->getMessage($condition->toString()); + } else { + $failed = []; + break; + } + } + foreach ($this->conditiongroups as $conditions) { + foreach ($conditions as $condition) { + if (!$condition->isFulfilled($userId)) { + $failed[] = $this->getMessage($condition->toString()); + } else { + $failed = []; + break 2; + } + } + } + } + return $failed; + } + + /** + * Uses the given data to fill the object values. This can be used + * as a generic function for storing data if the concrete rule type + * isn't known in advance. + * + * @param Array $data + * @return AdmissionRule This object. + */ + public function setAllData($data) + { + UserFilterField::getAvailableFilterFields(); + parent::setAllData($data); + $this->conditions = []; + $this->ungrouped_conditions = []; + $this->conditiongroups = []; + $this->quota = []; + foreach ($data['conditions'] as $ser_con) { + $condition = ObjectBuilder::build($ser_con, 'UserFilter'); + $this->addCondition($condition, $data['conditiongroup_'.$condition->getId()], $data['quota_'.$data['conditiongroup_'.$condition->getId()]] ?? 0); + } + foreach ($this->getConditiongroups() as $conditiongroup_id => $conditions) { + if (mb_strlen($conditiongroup_id) < 32) { + $group = md5(uniqid('conditiongroups' . microtime(), true)); + + $this->conditiongroups[$group] = $this->conditiongroups[$conditiongroup_id]; + unset($this->conditiongroups[$conditiongroup_id]); + + $this->quota[$group] = $this->quota[$conditiongroup_id]; + unset($this->quota[$conditiongroup_id]); + } + } + if (count($this->getConditiongroups()) && $data['conditiongroups_allowed']) { + $this->conditiongroups_allowed = true; + } + + return $this; + } + + /** + * Helper function for storing data to DB. + */ + public function store() + { + // Store rule data. + $stmt = DBManager::get()->prepare("INSERT INTO `conditionaladmissions` + (`rule_id`, `message`, `start_time`, `end_time`, `mkdate`, `chdate`) + VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE + `message`=VALUES(`message`), + `start_time`=VALUES(`start_time`), + `end_time`=VALUES(`end_time`), + `chdate`=VALUES(`chdate`)"); + $stmt->execute([$this->id, $this->message, (int)$this->startTime, + (int)$this->endTime, time(), time()]); + // prepare condition data. + $keys = array_keys($this->ungrouped_conditions); + foreach ($this->conditiongroups as $conditiongroup_id => $conditions) { + $keys = array_merge($keys, array_keys($conditions)); + } + + // Delete removed conditions from DB. + $stmt = DBManager::get()->prepare("SELECT `filter_id`, `conditiongroup_id` FROM + `admission_condition` WHERE `rule_id`=? AND `filter_id` NOT IN ('". + implode("', '", $keys)."')"); + $stmt->execute([$this->id]); + $groups = []; + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $entry) { + $current = new UserFilter($entry['filter_id']); + $current->delete(); + $groups[] = $entry['conditiongroup_id']; + } + DBManager::get()->exec("DELETE FROM `admission_condition` + WHERE `rule_id`='".$this->id."' AND `filter_id` NOT IN ('". + implode("', '", $keys)."')"); + // Store all conditions. + $queries = []; + $parameters = []; + $groupqueries = []; + $groupparameters = []; + foreach ($this->ungrouped_conditions as $condition) { + // Store each ungrouped condition... + $condition->store(); + $queries[] = "(?, ?, ?, ?)"; + $parameters[] = $this->id; + $parameters[] = $condition->getId(); + $parameters[] = ''; + $parameters[] = time(); + } + + foreach ($this->conditiongroups as $conditiongroup_id => $conditions) { + $groupqueries[] = "(?, ?)"; + $groupparameters[] = $conditiongroup_id; + $groupparameters[] = $this->quota[$conditiongroup_id]; + foreach ($conditions as $condition) { + // Store each group of conditions... + $condition->store(); + $queries[] = "(?, ?, ?, ?)"; + $parameters[] = $this->id; + $parameters[] = $condition->getId(); + $parameters[] = $conditiongroup_id; + $parameters[] = time(); + } + } + + // Store all assignments between rule and condition. + if (count($queries) > 0) { + $stmt = DBManager::get()->prepare("INSERT INTO `admission_condition` + (`rule_id`, `filter_id`, `conditiongroup_id`, `mkdate`) + VALUES " . implode(',', $queries) . " ON DUPLICATE KEY UPDATE + `filter_id`=VALUES(`filter_id`), `conditiongroup_id`=VALUES(`conditiongroup_id`)"); + $stmt->execute($parameters); + } + + // Store all assignments between condition and conditiongroup. + if (count($groupqueries) > 0) { + $stmt = DBManager::get()->prepare("INSERT INTO `admission_conditiongroup` + (`conditiongroup_id`, `quota`) + VALUES " . implode(',', $groupqueries) . " ON DUPLICATE KEY UPDATE + `quota`=VALUES(`quota`)"); + $stmt->execute($groupparameters); + } + + return $this; + } + + /** + * A textual description of the current rule. + * + * @return String + */ + public function toString() + { + $factory = new Flexi\Factory(__DIR__ . '/templates/'); + $tpl = $factory->open('info'); + $tpl->set_attribute('rule', $this); + return $tpl->render(); + } + + /** + * Validates if the given request data is sufficient to configure this rule + * (e.g. if required values are present). + * + * @param Array $data Request data + * @return Array Error messages. + */ + public function validate($data) + { + $errors = parent::validate($data); + if (!$data['conditions'] || !is_array($data['conditions'])) { + $errors[] = _('Es muss mindestens eine Auswahlbedingung angegeben werden.'); + return $errors; + } + $quota = []; + $quota_total = 0; + $grouped = 0; + $ungrouped = 0; + $no_quota = 0; + foreach ($data['conditions'] as $ser_con) { + $condition = ObjectBuilder::build($ser_con, 'UserFilter'); + if ($data['conditiongroup_'.$condition->getId()]) { + $grouped++; + } else { + $ungrouped++; + } + $quota[$data['conditiongroup_' . $condition->getId()]] = $data['quota_' . $data['conditiongroup_' . $condition->getId()]] ?? 0; + if (!$quota[$data['conditiongroup_' . $condition->getId()]]) { + $no_quota++; + } + } + foreach ($quota as $part) { + $quota_total += $part; + } + if ($grouped && $ungrouped) { + $errors[] = sprintf(_('Es müssen entweder alle Bedingungen Teil einer Gruppe sein, oder keine. %s Bedingungen sind keiner Gruppe zugeordnet.'), $ungrouped); + } elseif ($grouped && $quota_total !== 100) { + $errors[] = _('Die Gesamtsumme der Kontingente muss 100 Prozent betragen.'); + } elseif ($grouped && $no_quota) { + $errors[] = sprintf(_('Für %s Gruppen muss noch ein Kontingent festgelegt werden.'), $no_quota); + } + return $errors; + } + + public function getMessage($condition = null) + { + $message = parent::getMessage(); + if ($condition) { + return sprintf($message, $condition); + } else { + return $message; + } + } + + public function __clone() + { + $this->id = md5(uniqid(get_class($this))); + $this->courseSetId = null; + $cloned_conditions = []; + foreach ($this->ungrouped_conditions as $condition) { + $dolly = clone $condition; + $cloned_conditions[$dolly->id] = $dolly; + } + $this->ungrouped_conditions = $cloned_conditions; + $cloned_conditiongroups = []; + $cloned_quota = []; + foreach ($this->conditiongroups as $conditiongroup_id => $conditions) { + $cloned_conditiongroup_id = md5(uniqid($conditiongroup_id)); + $cloned_quota[$cloned_conditiongroup_id] = $this->quota[$conditiongroup_id]; + foreach ($conditions as $condition) { + $dolly = clone $condition; + $cloned_conditiongroups[$cloned_conditiongroup_id][$dolly->id] = $dolly; + } + } + $this->conditiongroups = $cloned_conditiongroups; + $this->quota = $cloned_quota; + } + + public function setSiblings($siblings = []) + { + parent::setSiblings($siblings); + $this->conditiongroups_allowed = null; + } +} |
