* @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 (bool) $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; } /** * 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]); $current = $stmt->fetchOne(); if ($current) { $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; } } else { $this->id = $this->generateId('prefadmissions'); } } /** * 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) { parent::setAllData($data); $this->favorSemester = (bool) $data['favor-semester']; $this->conditions = []; if ($data['conditions']) { foreach ($data['conditions'] as $con) { $condition = new UserFilter(); foreach ($con['attributes']['fields'] as $field) { $classname = $field['attributes']['type']; $obj = new $classname(); $obj->setCompareOperator($field['attributes']['compare-operator']); $obj->setValue($field['attributes']['value']); $condition->addField($obj); } $this->addCondition($condition); } } 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 * => array(, $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) { $condition->setRange(CourseSet::class, $this->courseSetId); // 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; } /** * Get fields and settings defining this admission rule as array. */ public function getPayload(): array { // Build everything as plain array. $conditions = []; foreach ($this->getConditions() as $one) { $fields = []; foreach ($one->getFields() as $field) { $fields[] = [ 'attributes' => [ 'type' => get_class($field), 'id' => $field->getId(), 'compare-operator' => $field->getCompareOperator(), 'value' => $field->getValue() ] ]; } $conditions[] = [ 'attributes' => [ 'text' => $one->toString(), 'fields' => $fields ] ]; } return array_merge( parent::getPayload(), [ 'conditions' => $conditions, 'favor-semester' => $this->getFavorSemester() ] ); } } /* end of class PreferentialAdmission */