* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP */ class CourseSet implements UserFilterRange { // --- ATTRIBUTES --- /** * Admission rules that are applied to the courses belonging to this set. */ protected $admissionRules = []; /** * Seat distribution algorithm. */ protected $algorithm = null; /** * IDs of courses that are aggregated into this set. The array is in the * form ($courseId1 => true, $courseId2 => true). */ protected $courses = []; /** * Has the seat distribution algorithm already been executed? */ protected $hasAlgorithmRun = false; /** * Unique identifier for this set. */ protected $id = ''; /** * Some extensive descriptional text for informing confused students. */ protected $infoText = ''; /** * Which Stud.IP institute does the course set belong to? */ protected $institutes = []; /** * Some display name for this course set. */ protected $name = ''; /** * Is the course set only visible for the creator? */ protected $private = false; /** * Who owns this course set? */ protected $user_id = false; /** * When was the course set changed? */ protected $chdate = null; /* * Lists of users who are treated differently on seat distribution */ protected $userlists = []; // --- OPERATIONS --- public function __construct($setId='') { $this->id = $setId; $this->name = _("Anmeldeset"); $this->algorithm = new RandomAlgorithm(); // Autoload admission rules. AdmissionRule::getAvailableAdmissionRules(); UserFilterField::getAvailableFilterFields(); if ($setId) { $this->load(); } } /** * Adds the given admission rule to the list of rules for the course set. * * @param AdmissionRule rule * @return CourseSet */ public function addAdmissionRule($rule) { $this->admissionRules[$rule->getId()] = $rule; return $this; } /** * Adds the course with the given ID to the course set. * * @param String courseId * @return CourseSet */ public function addCourse($courseId) { $this->courses[$courseId] = true; return $this; } /** * Adds a new institute ID. * * @param String newId * @return CourseSet */ public function addInstitute($newId) { $this->institutes[$newId] = true; return $this; } /** * Adds several institute IDs to the existing institute assignments. * * @param Array newIds * @return CourseSet */ public function addInstitutes($newIds) { foreach ($newIds as $newId) { $this->addInstitute($newId); } return $this; } /** * Adds a UserList to the course set. The list contains several users and a * factor that changes seat distribution chances for these users; * * @param String listId * @return CourseSet */ public function addUserList($listId) { $this->userlists[$listId] = true; return $this; } /** * Is the given user allowed to register as participant in the given * course according to the rules of this course set? * * @param String userId * @param String courseId * @return Array Optional error messages from rules if something went wrong. */ public function checkAdmission($userId, $courseId) { $errors = []; foreach ($this->admissionRules as $rule) { // All rules must be fulfilled. $ruleCheck = $rule->ruleApplies($userId, $courseId); if ($ruleCheck) { $errors = array_merge($errors, $ruleCheck); } } return $errors; } /** * Removes all admission rules at once. * * @return CourseSet */ public function clearAdmissionRules() { $this->admissionRules = []; return $this; } /** * Deletes the course set and all associated data. */ public function delete() { NotificationCenter::postNotification('CourseSetWillDelete', $this->id, $GLOBALS['user']->id); // Delete institute associations. $stmt = DBManager::get()->prepare("DELETE FROM `courseset_institute` WHERE `set_id`=?"); $stmt->execute([$this->id]); // Delete course associations. $stmt = DBManager::get()->prepare("DELETE FROM `seminar_courseset` WHERE `set_id`=?"); $stmt->execute([$this->id]); // Delete all rules... foreach ($this->admissionRules as $rule) { $rule->delete(); } // ... and their association to the current course set. $stmt = DBManager::get()->prepare("DELETE FROM `courseset_rule` WHERE `set_id`=?"); $stmt->execute([$this->id]); // Delete associations to user lists. $stmt = DBManager::get()->prepare("DELETE FROM `courseset_factorlist` WHERE `set_id`=?"); $stmt->execute([$this->id]); // Delete course set data. $stmt = DBManager::get()->prepare("DELETE FROM `coursesets` WHERE `set_id`=?"); $stmt->execute([$this->id]); /* * Delete waiting lists */ foreach ($this->courses as $id => $assigned) { AdmissionApplication::deleteBySQL("status='awaiting' AND seminar_id=?", [$id]); } //Delete priorities AdmissionPriority::unsetAllPriorities($this->getId()); NotificationCenter::postNotification('CourseSetDidDelete', $this->id, $GLOBALS['user']->id); } /** * Starts the seat distribution algorithm. */ public function distributeSeats() { if ($this->algorithm) { // Call pre-distribution hooks on all assigned rules. foreach ($this->admissionRules as &$rule) { $rule->beforeSeatDistribution($this); } $this->algorithm->run($this); // Mark as "seats distributed". $this->setAlgorithmRun(true); // Call post-distribution hooks on all assigned rules. foreach ($this->admissionRules as &$rule) { $rule->afterSeatDistribution($this); } AdmissionPriority::unsetAllPriorities($this->getId()); } } public function setAlgorithmRun($state) { NotificationCenter::postNotification('CourseSetAlgorithmWillStart', $state, $this->getId()); $this->hasAlgorithmRun = (bool)$state; $db = DBManager::get(); $ok = $db->execute("UPDATE coursesets SET algorithm_run = ? WHERE set_id = ?", [$this->hasAlgorithmRun, $this->getId()]); if ($ok) { NotificationCenter::postNotification('CourseSetAlgorithmDidStart', $state, $this->getId()); } return $ok; } /** * returns true if the set allows only a limited number of places * * @return boolean */ public function isSeatDistributionEnabled() { return $this->getSeatDistributionTime() !== null; } /** * returns timestamp of distribution time or null if no distribution time available * may return 0 if first-come-first-serve is enabled * * @return integer|null timestamp of distribution */ public function getSeatDistributionTime() { $pr_admission = $this->getAdmissionRule('ParticipantRestrictedAdmission'); if ($pr_admission) { return $pr_admission->getDistributionTime(); } } /** * Get all admission rules belonging to the course set. * * @return AdmissionRule[] */ public function getAdmissionRules() { return $this->admissionRules; } public function getAdmissionRule($class_name) { $result = array_filter($this->getAdmissionRules(), function($r) use ($class_name) { return $r instanceof $class_name;} ); return array_pop($result); } /** * check if course set has given admission rule * * @param string $rule name of AdmissionRule class * @return boolean */ public function hasAdmissionRule($rule) { return is_object($this->getAdmissionRule($rule)); } /** * Get the currently used distribution algorithm. * * @return AdmissionAlgorithm */ public function getAlgorithm() { return $this->algorithm; } /** * How many users will be allowed to register according to the defined * rules? This can help in estimating whether the combination of the * defined rules makes sense. * * @return int */ public function getAllowedUserCount() { $users = []; foreach ($this->admissionRules as $rule) { $users = array_merge($users, $rule->getAffectedUsers()); } return sizeof($users); } /** * Gets the course IDs belonging to the course set. * * @return Array */ public function getCourses() { return array_keys($this->courses); } /** * Gets all courses belonging to the given course set ID. * * @param String $courseSetId * @return Array */ public static function getCoursesByCourseSetId($courseSetId) { $query = "SELECT `seminar_id` FROM `seminar_courseset` WHERE `set_id` = ?"; $stmt = DBManager::get()->prepare($query); $stmt->execute([$courseSetId]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Gets all course sets belonging to the given institute ID. * * @param String $instituteId * @return Array */ public static function getCoursesetsByInstituteId($instituteId, $filter = []) { $query = "SELECT DISTINCT ci.* FROM `courseset_institute` ci JOIN `coursesets` c ON (ci.`set_id`=c.`set_id`) LEFT JOIN courseset_rule cr ON cr.set_id=ci.set_id LEFT JOIN seminar_courseset sc ON c.set_id = sc.set_id LEFT JOIN seminare s ON s.seminar_id = sc.seminar_id LEFT JOIN `semester_courses` ON s.`seminar_id` = `semester_courses`.`course_id` WHERE ci.`institute_id`=?"; $parameters = [$instituteId]; if (!$GLOBALS['perm']->have_perm('admin')) { $query .= " AND (c.`private`=0 OR c.`user_id`=?)"; $parameters[] = $GLOBALS['user']->id; } if (!empty($filter['course_set_name'])) { $query .= " AND c.name LIKE ?"; $parameters[] = $filter['course_set_name'] . '%'; } if (!empty($filter['rule_types']) && is_array($filter['rule_types']) && count($filter['rule_types'])) { $query .= " AND cr.type IN (?)"; $parameters[] = $filter['rule_types']; } if (!empty($filter['semester_id'])) { $query .= " AND `semester_courses`.`semester_id` = ?"; $parameters[] = $filter['semester_id']; } if (!empty($filter['course_set_chdate'])) { $query .= " AND c.chdate > ?"; $parameters[] = $filter['course_set_chdate']; } $query .= " ORDER BY c.name"; $stmt = DBManager::get()->prepare($query); $stmt->execute($parameters); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Gets all global course sets * * @param array $filter * @return Array */ public static function getGlobalCoursesets($filter = []) { $query = "SELECT DISTINCT c.set_id FROM coursesets c LEFT JOIN courseset_institute ci ON ci.`set_id`=c.`set_id` LEFT JOIN courseset_rule cr ON cr.set_id=ci.set_id LEFT JOIN seminar_courseset sc ON c.set_id = sc.set_id LEFT JOIN seminare s ON s.seminar_id = sc.seminar_id "; if (!empty($filter['semester_id'])) { $query .= "JOIN `semester_courses` ON s.`seminar_id` = `semester_courses`.`course_id` "; } $query .= "WHERE ci.institute_id IS NULL "; $parameters = []; $query .= " AND (c.`private`=0 OR c.`user_id`=?)"; $parameters[] = $GLOBALS['user']->id; if (!empty($filter['course_set_name'])) { $query .= " AND c.name LIKE ?"; $parameters[] = $filter['course_set_name'] . '%'; } if (!empty($filter['rule_types']) && is_array($filter['rule_types']) && count($filter['rule_types'])) { $query .= " AND cr.type IN (?)"; $parameters[] = $filter['rule_types']; } if (!empty($filter['semester_id'])) { $query .= " AND `semester_courses`.`semester_id` = ?"; $parameters[] = $filter['semester_id']; } if (!empty($filter['course_set_chdate'])) { $query .= " AND c.chdate > ?"; $parameters[] = $filter['course_set_chdate']; } $query .= " ORDER BY c.name"; $stmt = DBManager::get()->prepare($query); $stmt->execute($parameters); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * Get the identifier of the course set. * * @return String */ public function getId() { return $this->id; } /** * Get the course set's info text. * * @return String */ public function getInfoText() { return $this->infoText; } /** * Which institutes does the rule belong to? * * @return Array */ public function getInstituteIds() { return $this->institutes; } public function isGlobal() { return count($this->institutes) == 0; } /** * Gets this course set's display name. */ public function getName() { return $this->name; } /** * Returns the date of the last change. * @return int */ public function getChdate() { return $this->chdate; } /** * Retrieves the priorities given to the courses in this set. * * @return Array */ public function getPriorities() { return AdmissionPriority::getPriorities($this->id); } public function getNumApplicants() { return AdmissionPriority::getPrioritiesCount($this->id); } /** * Is the current courseset private? * * @return bool */ public function getPrivate() { return $this->private; } /** * returns latest semester id from assigned courses * if no courses are assigned current semester * * @return string id of semester */ public function getSemester() { $db = DBManager::get(); $data = $db->fetchOne(" SELECT semester_data.* FROM semester_data INNER JOIN semester_courses ON (semester_data.semester_id = semester_courses.semester_id) WHERE semester_courses.course_id IN (?) ORDER BY semester_data.ende DESC LIMIT 1 ", [$this->getCourses()]); if ($data) { $semester = Semester::buildExisting($data); } else { $semester = $_SESSION['_default_sem'] ? Semester::find($_SESSION['_default_sem']) : Semester::findCurrent(); } return $semester->id; } /** * Gets the owner of this course set. */ public function getUserId() { return $this->user_id; } public function setUserId($user_id) { $this->user_id = $user_id; return $this; } /** * Gets the course sets the given course belongs to. * * @param string $courseId * @return CourseSet */ public static function getSetForCourse($courseId) { $stmt = DBManager::get()->prepare("SELECT `set_id` FROM `seminar_courseset` WHERE `seminar_id`=?"); $stmt->execute([$courseId]); $set_id = $stmt->fetchColumn(); if ($set_id) { return new CourseSet($set_id); } return null; } /** * Gets the course sets the given rule belongs to. * * @param String $rule_id * @return CourseSet */ public static function getSetForRule($rule_id) { $set_id = DBManager::get()->fetchColumn("SELECT `set_id` FROM `courseset_rule` WHERE `rule_id`=?", [$rule_id]); if ($set_id) { return new CourseSet($set_id); } return null; } /** * Retrieves the lists of users that are considered specially in * seat distribution. * * @return Array */ public function getUserLists() { return array_keys($this->userlists); } public function getUserFactorList() { $factored_users = []; foreach ($this->getUserLists() as $ul_id) { $user_list = new AdmissionUserList($ul_id); // Iterate through user list. foreach ($user_list->getUsers() as $user => $assigned) { switch ($user_list->getFactor()) { // Maximum factor, just set it and stop further processing of user lists. case PHP_INT_MAX: $factored_users[$user] = PHP_INT_MAX; break; // Backlist, set malus and stop further processing of user lists. case 0: $factored_users[$user] = 0; break; default: // Add up current bonus if it isn't already at 0 or PHP_INT_MAX. if ($factored_users[$user] !== 0 && $factored_users[$user] != PHP_INT_MAX) { $factored_users[$user] = isset($factored_users[$user]) ? $factored_users[$user] + $user_list->getFactor() : $user_list->getFactor(); } } } } return $factored_users; } /** * Evaluates whether the seat distribution algorithm has already been * executed on this course set. * * @return boolean True if algorithm has already run, otherwise false. */ public function hasAlgorithmRun() { return $this->hasAlgorithmRun; } /** * Helper function for loading data from DB. */ public function load() { // Load basic data. $stmt = DBManager::get()->prepare( "SELECT * FROM `coursesets` WHERE set_id=? LIMIT 1"); $stmt->execute([$this->id]); if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->name = $data['name']; $this->infoText = $data['infotext']; $this->hasAlgorithmRun = (bool)$data['algorithm_run']; if ($data['algorithm']) { if (class_exists($data['algorithm'])) { $this->algorithm = new $data['algorithm'](); } } $this->private = (bool) $data['private']; $this->user_id = $data['user_id']; $this->chdate = $data['chdate']; } // Load institute assigments. $stmt = DBManager::get()->prepare(" SELECT courseset_institute.institute_id FROM `courseset_institute` INNER JOIN Institute ON (courseset_institute.institute_id = Institute.Institut_id) WHERE courseset_institute.set_id = ? ORDER BY Institute.Name ASC "); $stmt->execute([$this->id]); $this->institutes = []; while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->institutes[$data['institute_id']] = true; } // Load courses. $stmt = DBManager::get()->prepare( "SELECT seminar_id FROM `seminar_courseset` WHERE set_id=?"); $stmt->execute([$this->id]); $this->courses = []; while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->courses[$data['seminar_id']] = true; } // Load admission rules. $stmt = DBManager::get()->prepare( "SELECT * FROM `courseset_rule` WHERE set_id=?"); $stmt->execute([$this->id]); $this->admissionRules = []; while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { if (class_exists($data['type'])) { $this->admissionRules[$data['rule_id']] = new $data['type']($data['rule_id'], $this->id); } } // Load assigned user lists. $stmt = DBManager::get()->prepare("SELECT `factorlist_id` FROM `courseset_factorlist` WHERE `set_id`=?"); $stmt->execute([$this->id]); $this->userlists = []; while ($current = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->userlists[$current['factorlist_id']] = true; } return $this; } /** * Removes the course with the given ID from the set. * * @param String courseId * @return CourseSet */ public function removeCourse($courseId) { unset($this->courses[$courseId]); return $this; } /** * Removes the rule with the given ID from the set. * * @param String ruleId * @return CourseSet */ public function removeAdmissionRule($ruleId) { unset($this->admissionRules[$ruleId]); return $this; } /** * Removes the institute with the given ID from the set. * * @param String instituteId * @return CourseSet */ public function removeInstitute($instituteId) { unset($this->institutes[$instituteId]); return $this; } /** * Removes the user list with the given ID from the set. * * @param String listId * @return CourseSet */ public function removeUserlist($listId) { unset($this->userlists[$listId]); return $this; } /** * Adds several admission rules after clearing the existing rule * assignments. * * @param Array newIds * @return CourseSet */ public function setAdmissionRules($newRules) { $this->admissionRules = []; foreach ($newRules as $newRule) { $rule = ObjectBuilder::build($newRule, 'AdmissionRule'); $this->addAdmissionRule($rule); } return $this; } /** * Sets a seat distribution algorithm for this course set. This will only * have an effect in conjunction with a TimedAdmission, as the algorithm * needs a defined point in time where it will start. * * @param String newAlgorithm * @return CourseSet */ public function setAlgorithm($newAlgorithm) { try { $this->algorithm = new $newAlgorithm(); } catch (Exception $e) { } return $this; } /** * Adds several course IDs after clearing the existing course * assignments. * * @param Array newIds * @return CourseSet */ public function setCourses($newIds) { $this->courses = array_fill_keys($newIds, true); return $this; } /** * Adds several institute IDs after clearing the existing institute * assignments. * * @param Array newIds * @return CourseSet */ public function setInstitutes($newIds) { $this->institutes = []; $this->addInstitutes($newIds); return $this; } /** * Set the course set's info text. * * @return CourseSet */ public function setInfoText($newText) { $this->infoText = $newText; return $this; } /* Sets a new name for this course set. * * @param String newName * @return CourseSet */ public function setName($newName) { $this->name = $newName; return $this; } /** * Set a new value for courseset privacy. * * @param bool $newPrivate * @return CourseSet */ public function setPrivate($newPrivate) { $this->private = $newPrivate; return $this; } /** * Adds several user list IDs after clearing the existing user list * assignments. * * @param Array newIds * @return CourseSet */ public function setUserlists($newIds) { $this->userlists = []; foreach ($newIds as $newId) { $this->addUserlist($newId); } return $this; } public function store() { // Generate new ID if course set doesn't exist in DB yet. if (!$this->id) { do { $newid = md5(uniqid(get_class($this), true)); $db = DBManager::get()->query("SELECT `set_id` FROM `coursesets` WHERE `set_id`='$newid'"); } while ($db->fetch()); $this->id = $newid; } if (!$this->user_id) { $this->user_id = $GLOBALS['user']->id; } if ($this->isSeatDistributionEnabled()) { if (!$this->getAlgorithm()) { $algorithm = new RandomAlgorithm(); $this->setAlgorithm($algorithm); } if (!$this->getSeatDistributionTime()) { $this->setAlgorithmRun(true); //Delete priorities AdmissionPriority::unsetAllPriorities($this->getId()); } if ($this->getSeatDistributionTime() > time()) { $this->setAlgorithmRun(false); } } // Store basic data. $stmt = DBManager::get()->prepare("INSERT INTO `coursesets` (`set_id`, `user_id`, `name`, `infotext`, `algorithm`, `algorithm_run`, `private`, `mkdate`, `chdate`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`), `infotext`=VALUES(`infotext`), `algorithm`=VALUES(`algorithm`), `algorithm_run`=VALUES(`algorithm_run`), `private`=VALUES(`private`), `chdate`=VALUES(`chdate`)"); $stmt->execute([$this->id, $this->user_id, $this->name, $this->infoText, get_class($this->algorithm), $this->hasAlgorithmRun(), intval($this->private), time(), time()]); // Delete removed institute assignments from database. DBManager::get()->exec("DELETE FROM `courseset_institute` WHERE `set_id`='".$this->id."' AND `institute_id` NOT IN ('". implode("', '", array_keys($this->institutes))."')"); // Store associated institute IDs. foreach ($this->institutes as $institute => $associated) { $stmt = DBManager::get()->prepare("INSERT IGNORE INTO `courseset_institute` (`set_id`, `institute_id`, `mkdate`) VALUES (?, ?, ?)"); $stmt->execute([$this->id, $institute, time()]); } // log removed course assignments. DBManager::get()->fetchAll("SELECT seminar_id,set_id FROM `seminar_courseset` WHERE `set_id` = ? AND `seminar_id` NOT IN (?)", [$this->id, count($this->courses) ? array_keys($this->courses) : ''], function ($row) { StudipLog::log('SEM_CHANGED_ACCESS', $row['seminar_id'], null, 'Entfernung von Anmeldeset', sprintf('Anmeldeset: %s', $row['set_id'])); //Delete priorities AdmissionPriority::unsetAllPrioritiesForCourse($row['seminar_id']); Course::buildExisting(['seminar_id' => $row['seminar_id']])->triggerChdate(); }); //removed course assignments DBManager::get()->execute("DELETE FROM `seminar_courseset` WHERE `set_id` = ? AND `seminar_id` NOT IN (?)", [$this->id, count($this->courses) ? array_keys($this->courses) : '']); //log removing other associations DBManager::get()->execute("SELECT seminar_id,set_id FROM `seminar_courseset` WHERE `set_id` <> ? AND `seminar_id` IN (?)", [$this->id, array_keys($this->courses)], function ($row) { StudipLog::log('SEM_CHANGED_ACCESS', $row['seminar_id'], null, 'Entfernung von Anmeldeset', sprintf('Anmeldeset: %s', $row['set_id'])); //Delete priorities AdmissionPriority::unsetAllPrioritiesForCourse($row['seminar_id']); Course::buildExisting(['seminar_id' => $row['seminar_id']])->triggerChdate(); }); //Delete other associations, only one set possible DBManager::get()->execute("DELETE FROM `seminar_courseset` WHERE `set_id` <> ? AND `seminar_id` IN (?)", [$this->id, array_keys($this->courses)]); // Store associated course IDs. foreach ($this->courses as $course => $associated) { $stmt = DBManager::get()->prepare("INSERT IGNORE INTO `seminar_courseset` (`set_id`, `seminar_id`, `mkdate`) VALUES (?, ?, ?)"); $stmt->execute([$this->id, $course, time()]); if ($stmt->rowCount()) { StudipLog::log('SEM_CHANGED_ACCESS', $course, null, 'Zuordnung zu Anmeldeset', sprintf('Anmeldeset: %s', $this->id)); Course::buildExisting(['seminar_id' => $course])->triggerChdate(); } } // Delete removed user list assignments from database. DBManager::get()->exec("DELETE FROM `courseset_factorlist` WHERE `set_id`='".$this->id."' AND `factorlist_id` NOT IN ('". implode("', '", array_keys($this->userlists))."')"); // Store associated user list IDs. foreach ($this->userlists as $list => $associated) { $stmt = DBManager::get()->prepare("INSERT IGNORE INTO `courseset_factorlist` (`set_id`, `factorlist_id`, `mkdate`) VALUES (?, ?, ?)"); $stmt->execute([$this->id, $list, time()]); } // Delete removed admission rules from database. $stmt = DBManager::get()->query("SELECT `rule_id`, `type` FROM `courseset_rule` WHERE `set_id`='".$this->id."' AND `rule_id` NOT IN ('". implode("', '", array_keys($this->admissionRules))."')"); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($data as $ruleData) { $rule = new $ruleData['type']($ruleData['rule_id']); $rule->delete(); } // Store all rules. foreach ($this->admissionRules as $rule) { $rule->courseSetId = $this->id; // Store each rule... $rule->store(); // ... and its connection to the current course set. $stmt = DBManager::get()->prepare("INSERT INTO `courseset_rule` (`set_id`, `rule_id`, `type`, `mkdate`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE `type`=VALUES(`type`)"); $stmt->execute([$this->id, $rule->getId(), get_class($rule), time()]); } //fix free access courses if (count($this->courses)) { DBManager::get()->execute("UPDATE seminare SET Lesezugriff=1,Schreibzugriff=1 WHERE seminar_id IN(?)", [array_keys($this->courses)]); } //create general log $this->log_store(); } /** * Generates a general log entry if the CourseSet rules were changed. */ private function log_store() { $text = ''; $rule_counter = 1; foreach ($this->getAdmissionRules() as $rule) { $rule_text = strip_tags(kill_format($rule->toString())); $semicolon = ($rule_counter < count($this->getAdmissionRules()) ? '; ' : ''); $text .= '#' . $rule_counter . ' => ' . $rule_text . $semicolon; $rule_counter++; } $courses = $this->getCourses(); foreach ($courses as $course_id) { StudipLog::log( 'SEM_CHANGED_ACCESS', $course_id, NULL, $text, sprintf('Anmeldeset: %s (%s)', strip_tags(kill_format(($this->name))), $this->id) ); } } /** * A textual description of the current rule. * * @param bool short Show only short info without overview of assigned * courses and institutes. * @return String */ public function toString($short=false) { $tpl = $GLOBALS['template_factory']->open('admission/courseset/info'); $tpl->set_attribute('courseset', $this); $tpl->set_attribute('is_limited', false); $institutes = []; if (!$short) { $institutes = Institute::findAndMapMany(function($i) {return $i->name;}, array_keys($this->institutes), 'ORDER BY Name'); $tpl->set_attribute('institutes', $institutes); } if (!$short || $this->hasAdmissionRule('LimitedAdmission')) { $courses = Course::findAndMapBySQL( function($c) { return [ 'id' => $c->id, 'name' => $c->getFullName('number-name-semester'), 'visible' => $c->visible ]; }, "LEFT JOIN `semester_courses` ON `seminare`.`seminar_id` = `semester_courses`.`course_id` LEFT JOIN `semester_data` USING (`semester_id`) WHERE `seminare`.`seminar_id` IN ( :course_ids ) ORDER BY `semester_data`.`beginn`, `VeranstaltungsNummer`, `Name`", [ 'course_ids' => array_keys($this->courses) ] ); if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) { $courses = array_filter($courses, function ($c) { return $c['visible']; } ); } $tpl->set_attribute('is_limited', $this->hasAdmissionRule('LimitedAdmission')); $tpl->set_attribute('courses', $courses); } $tpl->set_attribute('short', $short); return $tpl->render(); } public function __toString() { return $this->toString(); } /** * is user with given user id allowed to assign/unassign given course to courseset * * @param string $user_id * @param string $course_id * @return boolean */ public function isUserAllowedToAssignCourse($user_id, $course_id) { global $perm; $is_dozent = $perm->have_studip_perm('tutor', $course_id, $user_id); $is_admin = $perm->have_perm('admin', $user_id) || ($perm->have_perm('dozent', $user_id) && Config::get()->ALLOW_DOZENT_COURSESET_ADMIN); $is_private = $this->getPrivate(); $is_my_own = $this->getUserId() == $user_id; $is_correct_institute = $this->isGlobal() || isset($this->institutes[Course::find($course_id)->institut_id]); return $is_dozent && $is_correct_institute && ($is_my_own || $is_admin || !$is_private || $this->isGlobal()) || $perm->have_perm('root', $user_id); } /** * is user with given user id allowed to edit or delete the courseset * * @param string $user_id * @return boolean */ public function isUserAllowedToEdit($user_id) { global $perm; if ($this->getUserId() == '') { return false; } if ($perm->have_perm('root', $user_id) || $this->getUserId() == $user_id) { return true; } if (count($this->institutes) == 0 && count($this->courses) == 1 && $perm->have_studip_perm('tutor', current($this->getCourses()), $user_id)) { return true; } if ($perm->have_perm('admin', $user_id) || ($perm->have_perm('dozent', $user_id) && Config::get()->ALLOW_DOZENT_COURSESET_ADMIN)) { foreach (array_keys($this->getInstituteIds()) as $one) { if ($perm->have_studip_perm('dozent', $one, $user_id)) { return true; } } } return false; } /** * checks if given rule is allowed to be added to current set of rules * * @param AdmissionRule|string $admission_rule * @return boolean */ public function isAdmissionRuleAllowed($admission_rule) { foreach ($this->getAdmissionRules() as $one) { if (!$one->isCombinationAllowed($admission_rule)) { return false; } } return true; } public static function getGlobalLockedAdmissionSetId() { $db = DBManager::get(); $locked_set_id = $db->fetchColumn("SELECT cr.set_id FROM courseset_rule cr INNER JOIN coursesets USING(set_id) WHERE type='LockedAdmission' and private=1 and user_id='' LIMIT 1"); if (!$locked_set_id) { $cs_insert = $db->prepare("INSERT INTO coursesets (set_id,user_id,name,infotext,algorithm,private,mkdate,chdate) VALUES (?,?,?,?,'',?,?,?)"); $cs_r_insert = $db->prepare("INSERT INTO courseset_rule (set_id,rule_id,type,mkdate) VALUES (?,?,?,UNIX_TIMESTAMP())"); $locked_insert = $db->prepare("INSERT INTO lockedadmissions (rule_id,message,mkdate,chdate) VALUES (?,'Die Anmeldung ist gesperrt',UNIX_TIMESTAMP(),UNIX_TIMESTAMP())"); $locked_set_id = md5(uniqid('coursesets',1)); $name = 'Anmeldung gesperrt (global)'; $cs_insert->execute([$locked_set_id,'',$name,'',1,time(),time()]); $locked_rule_id = md5(uniqid('lockedadmissions',1)); $locked_insert->execute([$locked_rule_id]); $cs_r_insert->execute([$locked_set_id,$locked_rule_id,'LockedAdmission']); } return $locked_set_id; } public static function getConnectedcourseAdmissionSetId() { $db = DBManager::get(); $locked_set_id = $db->fetchColumn(" SELECT `courseset_rule`.`set_id` FROM `courseset_rule` INNER JOIN `coursesets` USING (`set_id`) WHERE `type` = 'ConnectedcourseAdmission' AND `private` = 1 AND `user_id` = '' LIMIT 1 "); if (!$locked_set_id) { $cs_insert = $db->prepare(" INSERT INTO coursesets (set_id, user_id, name, infotext, algorithm, private, mkdate, chdate) VALUES (?, ?, ?, ?, '', ?, ?, ?) "); $cs_r_insert = $db->prepare(" INSERT INTO `courseset_rule` (`set_id`, `rule_id`, `type`, `mkdate`) VALUES (?, ?, ?, UNIX_TIMESTAMP()) "); $locked_insert = $db->prepare(" INSERT INTO `lockedadmissions` (`rule_id`, `message`, `mkdate`, `chdate`) VALUES (?,'Die Anmeldung ist gesperrt', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) "); $locked_set_id = md5(uniqid('coursesets_connected_course',1)); $name = 'Verknüpfte Veranstaltung (global)'; $cs_insert->execute([$locked_set_id,'',$name,'',1,time(),time()]); $locked_rule_id = md5(uniqid('connectedcourse',1)); $locked_insert->execute([$locked_rule_id]); $cs_r_insert->execute([ $locked_set_id, $locked_rule_id, 'ConnectedcourseAdmission' ]); } return $locked_set_id; } public static function addCourseToSet($set_id, $course_id) { $db = DBManager::get(); $ok = $db->execute("INSERT IGNORE INTO seminar_courseset (set_id,seminar_id,mkdate) VALUES (?,?,UNIX_TIMESTAMP())", [$set_id, $course_id]); if ($ok) { StudipLog::log('SEM_CHANGED_ACCESS', $course_id, null, 'Zuordnung zu Anmeldeset', sprintf('Anmeldeset: %s', $set_id)); $course = Course::find($course_id); if ($course) { $course->chdate = time(); $course->lesezugriff = 1; $course->schreibzugriff = 1; $course->store(); } } return $ok; } public static function removeCourseFromSet($set_id, $course_id) { $db = DBManager::get(); $ok = $db->execute("DELETE FROM seminar_courseset WHERE set_id=? AND seminar_id=? LIMIT 1", [$set_id, $course_id]); if ($ok) { StudipLog::log('SEM_CHANGED_ACCESS', $course_id, null, 'Entfernung von Anmeldeset', sprintf('Anmeldeset: %s', $set_id)); //Delete priorities AdmissionPriority::unsetAllPrioritiesForCourse($course_id); Course::buildExisting(['seminar_id' => $course_id])->triggerChdate(); } return $ok; } public function __clone() { $this->courses = []; $this->id = null; $this->user_id = null; $cloned_rules = []; foreach ($this->admissionRules as $key => $rule) { $dolly = clone $rule; $cloned_rules[$dolly->id] = $dolly; } $this->admissionRules = $cloned_rules; } /** * @see UserFilterRange::canEdit() */ public function canEditFilter(User $user, UserFilter $filter): bool { if ($GLOBALS['perm']->have_perm('root', $user->id)) { return true; } // Check general permissions on course set creation/editing. $permission = $GLOBALS['perm']->have_perm('admin', $user->id) || ( Config::get()->ALLOW_DOZENT_COURSESET_ADMIN && $GLOBALS['perm']->have_perm('dozent', $user->id) ); // Get all rules where filter can be present. $ruleTypes = array_filter( $this->getAdmissionRules(), fn($rule) => in_array(get_class($rule), [ConditionalAdmission::class, PreferentialAdmission::class]) ); // Get my institute's IDs. $institutes = array_map( fn ($i) => $i['Institut_id'], Institute::getMyInstitutes($user->id) ); $matchingInstitutes = array_intersect(array_keys($this->institutes), $institutes); /* * Check whether: * - this course set has rules than can have UserFilter objects * - the given user is allowed to create/edit course sets at all * - this course set belongs to the given user or is not private and belongs to one of this user's institutes */ return $permission && count($ruleTypes) > 0 && ( $this->user_id === $user->id || !$this->private && count($matchingInstitutes) > 0 ); } } /* end of class CourseSet */