aboutsummaryrefslogtreecommitdiff
path: root/lib/admissionrules/conditionaladmission/ConditionalAdmission.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/admissionrules/conditionaladmission/ConditionalAdmission.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/admissionrules/conditionaladmission/ConditionalAdmission.php')
-rw-r--r--lib/admissionrules/conditionaladmission/ConditionalAdmission.php563
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;
+ }
+}