aboutsummaryrefslogtreecommitdiff
path: root/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/admissionrules/preferentialadmission/PreferentialAdmission.php')
-rw-r--r--lib/admissionrules/preferentialadmission/PreferentialAdmission.php537
1 files changed, 537 insertions, 0 deletions
diff --git a/lib/admissionrules/preferentialadmission/PreferentialAdmission.php b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
new file mode 100644
index 0000000..2f57a44
--- /dev/null
+++ b/lib/admissionrules/preferentialadmission/PreferentialAdmission.php
@@ -0,0 +1,537 @@
+<?php
+
+/**
+ * PreferentialAdmission.php
+ *
+ * An admission rule that favors selected courses of study or semesters of study.
+ *
+ * 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 PreferentialAdmission extends AdmissionRule
+{
+ // --- ATTRIBUTES ---
+
+ /**
+ * Stores IDs of userlists generated for representing the selected
+ * conditions. These lists are created on seat distribution in the course
+ * set and are deleted immediately after.
+ */
+ public $userlists = [];
+
+ /**
+ * Conditions for selecting the favored people in seat distribution.
+ */
+ public $conditions = [];
+
+ /**
+ * Should higher semesters of study be favored?
+ */
+ public $favorSemester = false;
+
+ /**
+ * If semesters are favored, which bonus difference shall be set between
+ * each semester of study?
+ */
+ public $bonus_difference = 100;
+
+ /**
+ * The courseset this rule belongs to.
+ */
+ public $courseset = null;
+
+ // --- OPERATIONS ---
+
+ /**
+ * Standard constructor.
+ *
+ * @param String ruleId If this rule has been saved previously, it
+ * will be loaded from database.
+ * @return AdmissionRule the current object (this).
+ */
+ public function __construct($ruleId='')
+ {
+ $this->id = $ruleId;
+ if ($ruleId) {
+ $this->load();
+ } else {
+ $this->id = $this->generateId('prefadmissions');
+ }
+ }
+
+ /**
+ * Adds a new UserFilter to this rule.
+ *
+ * @param UserFilter condition
+ * @return PreferentialAdmission
+ */
+ public function addCondition($condition)
+ {
+ $this->conditions[$condition->getId()] = $condition;
+ return $this;
+ }
+
+ /**
+ * Hook that can be called after the seat distribution on the courseset
+ * has completed. User lists that were generated before are removed now.
+ */
+ public function afterSeatDistribution($courseset)
+ {
+ foreach ($this->userlists as $id) {
+ $current = new AdmissionUserList($id);
+ $courseset->removeUserList($id);
+ $current->delete();
+ }
+ }
+
+ /**
+ * Hook that can be called when the seat distribution on the courseset
+ * starts. This type of admission rule gets all users that fulfill the
+ * specified conditions and generates user lists with modified chances
+ * in seat distribution.
+ *
+ * @param CourseSet The courseset this rule belongs to.
+ */
+ public function beforeSeatDistribution($courseset)
+ {
+ $this->courseset = $courseset;
+ /*
+ * First, we need to calculate the maximum of persons applying
+ * for a single course as that number will influence the numbers
+ * to set for preferation.
+ */
+ $this->bonus_difference = DBManager::get()->fetchColumn("SELECT MAX(users) FROM (
+ SELECT `priority`, COUNT(DISTINCT `user_id`) AS users
+ FROM `priorities`
+ WHERE `set_id` = ?
+ GROUP BY `priority`
+ ) t", [$courseset->getId()]);
+ $users = $this->getAffectedUsers();
+
+ // No study semester variation, just put all users together.
+ if (!$this->favorSemester) {
+
+ $userlist = new AdmissionUserList();
+ $userlist->setUsers($users)->setFactor($this->bonus_difference + 1)->store();
+ $this->userlists[] = $userlist->getId();
+ $courseset->addUserList($userlist->getId());
+
+ // Study semesters need to be considered for differentiation...
+ } else {
+ /*
+ * Build data grouped by semester of study for users affected
+ * by given conditions.
+ */
+ if ($this->conditions) {
+ $grouped = $this->getSemesterGroups($users, true);
+
+ /*
+ * Build data grouped by semester of study for all users
+ * (excluding all users affected by given conditions).
+ */
+ $rest = $this->getSemesterGroups(
+ array_keys(AdmissionPriority::getPriorities($courseset->getId())),
+ false, $users);
+
+ /*
+ * Now set bonus factors to higher semesters. We are processing
+ * users not affected by conditions first so that we get the
+ * maximum bonus these users get and can build on top of that
+ * for users affected by conditions.
+ */
+ $maxbonus = $this->setSemesterBonus($courseset, $rest);
+
+ /*
+ * Finally, set bonuses for the users affected by conditions.
+ */
+ $endbonus = $this->setSemesterBonus($courseset, $grouped, $maxbonus + 1);
+ /*
+ * No conditions given, just group all users
+ * by their semester of study.
+ */
+ } else {
+ // Build list of users by semester of study.
+ $grouped = $this->getSemesterGroups(
+ array_keys(AdmissionPriority::getPriorities($courseset->getId())),
+ false);
+
+ // Assign corresponding bonus to users.
+ $maxbonus = $this->setSemesterBonus($courseset, $grouped);
+ }
+ }
+ }
+
+ /**
+ * Deletes the admission rule and all associated data.
+ */
+ public function delete()
+ {
+ parent::delete();
+ // Delete rule data.
+ $stmt = DBManager::get()->prepare("DELETE FROM `prefadmissions`
+ WHERE `rule_id`=?");
+ $stmt->execute([$this->id]);
+ // Delete all associated conditions...
+ foreach ($this->conditions as $condition) {
+ $condition->delete();
+ }
+ // ... and their connection to this rule.
+ $stmt = DBManager::get()->prepare("DELETE FROM `prefadmission_condition`
+ 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 = [];
+ if ($this->conditions) {
+ // Get users from all specified conditions.
+ foreach ($this->conditions as $condition) {
+ $users = array_unique(array_merge($users, $condition->getUsers()));
+ }
+ } else {
+ $users = array_keys(AdmissionPriority::getPriorities($this->courseset->getId()));
+ }
+ return $users;
+ }
+
+ /**
+ * Gets all defined conditions.
+ *
+ * @return Array
+ */
+ public function getConditions()
+ {
+ return $this->conditions;
+ }
+
+ /**
+ * Gets some text that describes what this AdmissionRule (or respective
+ * subclass) does.
+ */
+ public static function getDescription()
+ {
+ return _('Sie können hier festlegen, dass bestimmte Studiengänge, '.
+ 'Fachsemester etc. bei der Platzverteilung zu Veranstaltungen '.
+ 'bevorzugt behandelt werden sollen.');
+ }
+
+ /**
+ * Returns whether higher semesters of study should be favored.
+ *
+ * @return bool
+ */
+ public function getFavorSemester()
+ {
+ return $this->favorSemester;
+ }
+
+ /**
+ * Return this rule's name.
+ */
+ public static function getName()
+ {
+ return _('Bevorzugte Anmeldung');
+ }
+
+ /**
+ * Gets the semesters of study for the given users. If conditions are
+ * set and should be considered, only the semesters of study belonging
+ * to the given conditions are set.
+ *
+ * @param $users user IDs to process
+ * @param $considerConditions should only the semesters of study belonging
+ * to given conditions be considered?
+ * @param array $exclude user IDs to exclude
+ * @return array Users with their maximal semester of study.
+ */
+ public function getSemesterGroups($users, $considerConditions, $exclude = [])
+ {
+ /*
+ * Get all selected condition values so that the study semester
+ * can be matched against that data; we don't want some "general"
+ * value for a user's study semester, but the one that is assigned
+ * to a given subject and degree.
+ */
+ $queryParts = [];
+ $values = [$users];
+ if ($exclude) {
+ $values[] = $exclude;
+ }
+ if ($considerConditions) {
+ foreach ($this->conditions as $condition) {
+ $queryPart = "";
+ // Search for subject and degree entries.
+ foreach ($condition->getFields() as $field) {
+ switch (get_class($field)) {
+ case 'DegreeCondition':
+ if ($queryPart) {
+ $queryPart .= " AND ";
+ }
+ $queryPart .= "`abschluss_id`".$field->getCompareOperator()."?";
+ $values[] = $field->getValue() ?: '';
+ break;
+ case 'SubjectCondition':
+ if ($queryPart) {
+ $queryPart .= " AND ";
+ }
+ $queryPart .= "`fach_id`".$field->getCompareOperator()."?";
+ $values[] = $field->getValue() ?: '';
+ break;
+ case 'SemesterOfStudyCondition':
+ if ($queryPart) {
+ $queryPart .= " AND ";
+ }
+ $queryPart .= "`semester`".$field->getCompareOperator()."?";
+ $values[] = $field->getValue() ?: '';
+ break;
+ default:
+ break;
+ }
+ }
+ if ($queryPart) {
+ $queryParts[] = $queryPart;
+ }
+ }
+ }
+ // Build SQL query with affected users and selected subjects and degrees.
+ $query = "SELECT `user_id`, MAX(`semester`) AS semester
+ FROM `user_studiengang`
+ WHERE `user_id` IN (?)";
+ if ($exclude) {
+ $query .= " AND `user_id` NOT IN (?)";
+ }
+ if ($queryParts) {
+ $query .= " AND ((".implode(") OR (", $queryParts)."))";
+ }
+ $query .= " GROUP BY `user_id` ORDER BY `semester`, `user_id`";
+ $groups = [];
+ foreach (DBManager::get()->fetchAll($query, $values) as $entry) {
+ if (intval($entry['semester'])) {
+ $groups[intval($entry['semester'])][] = $entry['user_id'];
+ }
+ }
+ ksort($groups);
+ return $groups;
+ }
+
+ /**
+ * Gets the template that provides a configuration GUI for this rule.
+ *
+ * @return String
+ */
+ public function getTemplate()
+ {
+ $factory = new Flexi\Factory(__DIR__.'/templates/');
+ // Now open specific template for this rule and insert base template.
+ $tpl = $factory->open('configure');
+ $tpl->set_attribute('rule', $this);
+ return $tpl->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 `prefadmissions` WHERE `rule_id`=? LIMIT 1");
+ $stmt->execute([$this->id]);
+ if ($current = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $this->favorSemester = $current['favor_semester'];
+ // Retrieve conditions.
+ $stmt = DBManager::get()->prepare("SELECT *
+ FROM `prefadmission_condition` WHERE `rule_id`=?");
+ $stmt->execute([$this->id]);
+ $conditions = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($conditions as $condition) {
+ $currentCondition = new UserFilter($condition['condition_id']);
+ $this->conditions[$condition['condition_id']] = $currentCondition;
+ }
+ }
+ }
+
+ /**
+ * Removes the condition with the given ID from the rule.
+ *
+ * @param String conditionId
+ * @return PreferentialAdmission
+ */
+ public function removeCondition($conditionId)
+ {
+ $this->conditions[$conditionId]->delete();
+ unset($this->conditions[$conditionId]);
+ return $this;
+ }
+
+ /**
+ * Admission is open for everyone. On seat distribution, the rule conditions
+ * will be used to generate user lists with the specified chance.
+ *
+ * @param String $userId
+ * @param String $courseId
+ * @return Array Is the user allowed to register or are there any errors?
+ */
+ public function ruleApplies($userId, $courseId)
+ {
+ return [];
+ }
+
+ /**
+ * 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->favorSemester = (bool) $data['favor_semester'];
+ $this->conditions = [];
+ if ($data['conditions']) {
+ foreach ($data['conditions'] as $condition) {
+ $this->addCondition(ObjectBuilder::build($condition, 'UserFilter'));
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * New setting for favoring higher semesters of study.
+ *
+ * @param bool $newFavorSemester
+ * @return PreferentialAdmission
+ */
+ public function setFavorSemester($newFavorSemester) {
+ $this->favorSemester = $newFavorSemester;
+ return $this;
+ }
+
+ /**
+ * Create user lists and set bonus corresponding to
+ * the maximal available semester of study for given users.
+ *
+ * @param $courseset CourseSet to add user lists to
+ * @param $grouped associative array of users in the form
+ * <semester> => array(<user_id1>, <user_id2, ...))
+ * @param $baseBonus basic bonus to start with, defaults to 0.
+ */
+ public function setSemesterBonus($courseset, $grouped, $baseBonus = 1)
+ {
+ // Create user lists from each semester group.
+ $bonus = $baseBonus;
+ foreach ($grouped as $semester => $members) {
+ $userlist = new AdmissionUserList();
+ $userlist->setUsers($members);
+ $userlist->setFactor($bonus);
+ $userlist->store();
+ $bonus = $bonus + ($this->bonus_difference + 1);
+ $courseset->addUserList($userlist->getId());
+ $this->userlists[] = $userlist->getId();
+ }
+ return $bonus;
+ }
+
+ /**
+ * Helper function for storing data to DB.
+ */
+ public function store()
+ {
+ // Store rule data.
+ $stmt = DBManager::get()->prepare("INSERT INTO `prefadmissions`
+ (`rule_id`, `favor_semester`, `mkdate`, `chdate`)
+ VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE
+ `favor_semester`=VALUES(`favor_semester`),
+ `chdate`=VALUES(`chdate`)");
+ $stmt->execute([$this->id, $this->favorSemester, time(), time()]);
+ // Delete removed conditions from DB.
+ $entries = DBManager::get()->fetchAll("SELECT `condition_id` FROM
+ `prefadmission_condition` WHERE `rule_id`=? AND `condition_id` NOT IN (?)",
+ [$this->id, array_keys($this->conditions)]);
+ foreach ($entries as $entry) {
+ $current = new UserFilter($entry['condition_id']);
+ $current->delete();
+ }
+ DBManager::get()->execute("DELETE FROM `prefadmission_condition`
+ WHERE `rule_id`=? AND `condition_id` NOT IN (?)", [$this->id, array_keys($this->conditions)]);
+ // Store all conditions.
+ $queries = [];
+ $parameters = [];
+ if ($this->conditions) {
+ foreach ($this->conditions as $condition) {
+ // Store each condition...
+ $condition->store();
+ $queries[] = "(?, ?, ?)";
+ $parameters[] = $this->id;
+ $parameters[] = $condition->getId();
+ $parameters[] = time();
+ }
+ // Store all assignments between rule and condition.
+ $stmt = DBManager::get()->execute("INSERT INTO `prefadmission_condition`
+ (`rule_id`, `condition_id`, `mkdate`)
+ VALUES ".implode(",", $queries)." ON DUPLICATE KEY UPDATE
+ `condition_id`=VALUES(`condition_id`)", $parameters);
+ }
+ 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 Request data
+ * @return Array Error messages.
+ */
+ public function validate($data)
+ {
+ $errors = parent::validate($data);
+ if (!$data['conditions'] && !$data['favor_semester']) {
+ $errors[] = _('Es muss mindestens eine Auswahlbedingung angegeben werden.');
+ }
+ return $errors;
+ }
+
+ public function __clone()
+ {
+ $this->id = md5(uniqid(get_class($this)));
+ $this->courseSetId = null;
+ $cloned_conditions = [];
+ foreach ($this->conditions as $condition) {
+ $dolly = clone $condition;
+ $cloned_conditions[$dolly->id] = $dolly;
+ }
+ $this->conditions = $cloned_conditions;
+ }
+
+} /* end of class PreferentialAdmission */