/** * Configure settings for several courses at once. * * @param String $set_id course set ID to fetch courses from * @param String $csv export course members to file */ public function configure_courses_action($set_id, $csv = null) { if (Request::isXhr()) { $this->response->add_header('X-Title', _('Ausgewählte Veranstaltungen konfigurieren')); } $courseset = new CourseSet($set_id); $this->set_id = $courseset->getId(); $this->courses = Course::findMany($courseset->getCourses(), "ORDER BY VeranstaltungsNummer, Name"); $this->applications = AdmissionPriority::getPrioritiesStats($courseset->getId()); $distinct_members = array(); $multi_members = array(); foreach ($this->courses as $course) { $all_members = $course->members->findBy('status', words('user autor'))->pluck('user_id'); $all_members = array_merge($all_members, $course->admission_applicants->findBy('status', words('accepted awaiting'))->pluck('user_id')); $all_members = array_unique($all_members); foreach ($all_members as $one) { $multi_members[$one]++; } $distinct_members = array_unique(array_merge($distinct_members, $all_members)); } $multi_members = array_filter($multi_members, function ($a) { return $a > 1; }); $this->count_distinct_members = count($distinct_members); $this->count_multi_members = count($multi_members); if ($csv == 'csv') { $captions = array(_("Nummer"), _("Name"), _("versteckt"), _("Zeiten"), _("Dozenten"), _("max. Teilnehmer"), _("Teilnehmer aktuell"), _("Anzahl Anmeldungen"), _("Anzahl Anmeldungen Prio 1"), _("Warteliste"), _("max. Anzahl Warteliste"), _("vorläufige Anmeldung"), _("verbindliche Anmeldung")); $data = array(); foreach ($this->courses as $course) { $row = array(); $row[] = $course->veranstaltungsnummer; $row[] = $course->name; $row[] = $course->visible ? _("nein") : _("ja"); $row[] = join('; ', $course->cycles->toString()); $row[] = join(', ', $course->members->findBy('status', 'dozent')->orderBy('position')->pluck('Nachname')); $row[] = $course->admission_turnout; $row[] = $course->getNumParticipants(); $row[] = $this->applications[$course->id]['c']; $row[] = $this->applications[$course->id]['h']; $row[] = $course->admission_disable_waitlist ? _("nein") : _("ja"); $row[] = $course->admission_waitlist_max > 0 ? $course->admission_waitlist_max : ''; $row[] = $course->admission_prelim ? _("ja") : _("nein"); $row[] = $course->admission_binding ? _("ja") : _("nein"); $data[] = $row; } $tmpname = md5(uniqid('tmp')); if (array_to_csv($data, $GLOBALS['TMP_PATH'] . '/' . $tmpname, $captions)) { $this->redirect(GetDownloadLink($tmpname, 'Veranstaltungen_' . $courseset->getName() . '.csv', 4, 'force')); return; } } if (in_array($csv, words('download_all_members download_multi_members'))) { $liste = array(); $multi_members = $all_participants = array(); foreach ($this->courses as $course) { $participants = $course->members->findBy('status', words('user autor'))->toGroupedArray('user_id', words('username vorname nachname email status')); $participants += $course->admission_applicants->findBy('status', words('accepted awaiting'))->toGroupedArray('user_id', words('username vorname nachname email status')); $all_participants += $participants; foreach (array_keys($participants) as $one) { $multi_members[$one][] = $course->name . ($course->veranstaltungsnummer ? '|' . $course->veranstaltungsnummer : ''); } foreach ($participants as $user_id => $part) { $liste[] = array($part['username'], $part['vorname'], $part['nachname'], $part['email'], $course->name . ($course->veranstaltungsnummer ? '|' . $course->veranstaltungsnummer : ''), $part['status']); } } if ($csv == 'download_all_members') { $captions = array(_("Username"), _("Vorname"), _("Nachname"), _("Email"), _("Veranstaltung"), _("Status")); if (count($liste)) { $tmpname = md5(uniqid('tmp')); if (array_to_csv($liste, $GLOBALS['TMP_PATH'] . '/' . $tmpname, $captions)) { $this->redirect(GetDownloadLink($tmpname, 'Gesamtteilnehmerliste_' . $courseset->getName() . '.csv', 4, 'force')); return; } } } else { $liste = array(); $multi_members = array_filter($multi_members, function ($a) { return count($a) > 1; }); $c = 0; $max_count = array(); foreach ($multi_members as $user_id => $courses) { $member = $all_participants[$user_id]; $liste[$c] = array($member['username'], $member['vorname'], $member['nachname'], $member['email']); foreach ($courses as $one) { $liste[$c][] = $one; } $max_count[] = count($courses); $c++; } $captions = array(_("Nutzername"), _("Vorname"), _("Nachname"), _("Email")); foreach (range(1, max($max_count)) as $num) { $captions[] = _("Veranstaltung") . ' ' . $num; } if (count($liste)) { $tmpname = md5(uniqid('tmp')); if (array_to_csv($liste, $GLOBALS['TMP_PATH'] . '/' . $tmpname, $captions)) { $this->redirect(GetDownloadLink($tmpname, 'Mehrfachanmeldungen_' . $courseset->getName() . '.csv', 4, 'force')); return; } } } } if (Request::submitted('configure_courses_save')) { CSRFProtection::verifyUnsafeRequest(); $admission_turnouts = Request::intArray('configure_courses_turnout'); $admission_waitlists = Request::intArray('configure_courses_disable_waitlist'); $admission_waitlists_max = Request::intArray('configure_courses_waitlist_max'); $admission_bindings = Request::intArray('configure_courses_binding'); $admission_prelims = Request::intArray('configure_courses_prelim'); $hidden = Request::intArray('configure_courses_hidden'); $ok = 0; foreach ($this->courses as $course) { if ($GLOBALS['perm']->have_studip_perm('admin', $course->id)) { $do_update_admission = $course->admission_turnout < $admission_turnouts[$course->id]; $course->admission_turnout = $admission_turnouts[$course->id]; $course->admission_disable_waitlist = isset($admission_waitlists[$course->id]) ? 0 : 1; $course->admission_waitlist_max = $course->admission_disable_waitlist ? 0 : $admission_waitlists_max[$course->id]; $course->admission_binding = @$admission_bindings[$course->id] ?: 0; $course->admission_prelim = @$admission_prelims[$course->id] ?: 0; $course->visible = @$hidden[$course->id] ? 0 : 1; $ok += $course->store(); if ($do_update_admission) { update_admission($course->id); } } } if ($ok) { PageLayout::postMessage(MessageBox::success(_("Die zugeordneten Veranstaltungen wurden konfiguriert."))); } $this->redirect($this->url_for('admission/courseset/configure/' . $courseset->getId())); return; } }
/** * Distribute seats for several courses in a course set using the given * user priorities. * * @param CourseSet $courseSet The course set containing the courses * that seats shall be distributed for. * @see CourseSet */ private function distributeByPriorities($courseSet) { Log::DEBUG('start seat distribution for course set: ' . $courseSet->getId()); $limited_admission = $courseSet->getAdmissionRule('LimitedAdmission'); //all users with their priorities $claiming_users = AdmissionPriority::getPriorities($courseSet->getId()); //all users which have bonus/malus $factored_users = $courseSet->getUserFactorList(); //all users with their max number of courses $max_seats_users = array_combine(array_keys($claiming_users), array_map(function ($u) use($limited_admission) { return $limited_admission->getMaxNumberForUser($u); }, array_keys($claiming_users))); //unlucky users get a bonus for the next round $bonus_users = array(); //users / courses für later waitlist distribution $waiting_users = array(); //number of already distributed seats for users $distributed_users = array(); $prio_mapper = function ($users, $course_id) use($claiming_users) { $mapper = function ($u) use($course_id) { return isset($u[$course_id]) ? $u[$course_id] : null; }; return array_filter(array_map($mapper, array_intersect_key($claiming_users, array_flip($users)))); }; //sort courses by highest count of prio 1 applicants $stats = AdmissionPriority::getPrioritiesStats($courseSet->getId()); $courses = array_map(function ($a) { return $a['h']; }, $stats); arsort($courses, SORT_NUMERIC); $max_prio = AdmissionPriority::getPrioritiesMax($courseSet->getId()); //count already manually distributed places $distributed_users = $this->countParticipatingUsers(array_keys($courses), array_keys($claiming_users)); Log::DEBUG('already distributed users: ' . print_r($distributed_users, 1)); //walk through all prios with all courses foreach (range(1, $max_prio) as $current_prio) { foreach (array_keys($courses) as $course_id) { $current_claiming = array(); $course = Course::find($course_id); $free_seats = $course->getFreeSeats(); //find users with current prio for this course, if they still need a place foreach ($claiming_users as $user_id => $prio_courses) { if ($prio_courses[$course_id] == $current_prio && $distributed_users[$user_id] < $max_seats_users[$user_id]) { //exclude participants if (!$course->getParticipantStatus($user_id)) { $current_claiming[$user_id] = 1; if (isset($factored_users[$user_id])) { $current_claiming[$user_id] *= $factored_users[$user_id]; } } else { Log::DEBUG(sprintf('user %s is already %s in course %s, ignoring', $user_id, $course->getParticipantStatus($user_id), $course->id)); } } } //give maximum bonus to users which were unlucky before foreach (array_keys($current_claiming) as $user_id) { if ($bonus_users[$user_id] > 0) { $current_claiming[$user_id] = $bonus_users[$user_id] * count($current_claiming) + 1; $bonus_users[$user_id]--; } } Log::DEBUG(sprintf('distribute %s seats on %s claiming with prio %s in course %s', $free_seats, count($current_claiming), $current_prio, $course->id)); Log::DEBUG('users to distribute: ' . print_r($current_claiming, 1)); $current_claiming = $this->rollTheDice($current_claiming); Log::DEBUG('the die is cast: ' . print_r($current_claiming, 1)); $chosen_ones = array_slice(array_keys($current_claiming), 0, $free_seats); Log::DEBUG('chosen ones: ' . print_r($chosen_ones, 1)); $this->addUsersToCourse($chosen_ones, $course, $prio_mapper($chosen_ones, $course->id)); foreach ($chosen_ones as $one) { $distributed_users[$one]++; } if ($free_seats < count($current_claiming)) { $remaining_ones = array_slice(array_keys($current_claiming), $free_seats); foreach ($remaining_ones as $one) { $bonus_users[$one]++; $waiting_users[$current_prio][$course_id][] = $one; } } } } //distribute to waitlists if applicable Log::DEBUG('waiting list: ' . print_r($waiting_users, 1)); foreach ($waiting_users as $current_prio => $current_prio_waiting_courses) { foreach ($current_prio_waiting_courses as $course_id => $users) { $users = array_filter($users, function ($user_id) use($distributed_users, $max_seats_users) { return $distributed_users[$user_id] < $max_seats_users[$user_id]; }); $course = Course::find($course_id); Log::DEBUG(sprintf('distribute waitlist of %s with prio %s in course %s', count($users), $current_prio, $course->id)); if (!$course->admission_disable_waitlist) { if ($course->admission_waitlist_max) { $free_seats_waitlist = $course->admission_waitlist_max - $course->getNumWaiting(); $free_seats_waitlist = $free_seats_waitlist < 0 ? 0 : $free_seats_waitlist; } else { $free_seats_waitlist = count($users); } $waiting_list_ones = array_slice($users, 0, $free_seats_waitlist); Log::DEBUG('waiting list ones: ' . print_r($waiting_list_ones, 1)); $this->addUsersToWaitlist($waiting_list_ones, $course, $prio_mapper($waiting_list_ones, $course->id)); foreach ($waiting_list_ones as $one) { $distributed_users[$one]++; } } else { $free_seats_waitlist = 0; } if ($free_seats_waitlist < count($users)) { $remaining_ones = array_slice($users, $free_seats_waitlist); Log::DEBUG('remaining ones: ' . print_r($remaining_ones, 1)); $this->notifyRemainingUsers($remaining_ones, $course, $prio_mapper($remaining_ones, $course->id)); } } } }
/** * A person applies for a course. */ function apply_action() { $user_id = $GLOBALS['user']->id; $courseset = CourseSet::getSetForCourse($this->course_id); $this->course_name = PageLayout::getTitle(); if ($courseset) { $errors = $courseset->checkAdmission($user_id, $this->course_id); if (count($errors)) { $this->courseset_message = $courseset->toString(true); $this->admission_error = MessageBox::error(_("Die Anmeldung war nicht erfolgreich."), $errors); foreach ($courseset->getAdmissionRules() as $rule) { $admission_form .= $rule->getInput(); } if ($admission_form) { $this->admission_form = $admission_form; } } else { if ($courseset->isSeatDistributionEnabled()) { if ($courseset->hasAlgorithmRun()) { if ($courseset->getSeatDistributionTime()) { $msg = _("Die Plätze in dieser Veranstaltung wurden automatisch verteilt."); } if (StudipLock::get('enrolment' . $this->course_id)) { $course = Course::find($this->course_id); if ($course->getFreeSeats() && !$course->getNumWaiting()) { $enrol_user = true; } else { if ($course->isWaitlistAvailable()) { $seminar = new Seminar($course); if ($seminar->addToWaitlist($user_id, 'last')) { $msg_details[] = sprintf(_("Alle Plätze sind belegt, Sie wurden daher auf Platz %s der Warteliste gesetzt."), $maxpos); } } else { $this->admission_error = MessageBox::error(_("Die Anmeldung war nicht erfolgreich. Alle Plätze sind belegt und es steht keine Warteliste zur Verfügung.")); } } } else { $this->admission_error = MessageBox::error(_("Die Anmeldung war wegen technischer Probleme nicht erfolgreich. Bitte versuchen Sie es später noch einmal.")); } } else { $msg = _("Die Plätze in dieser Veranstaltung werden automatisch verteilt."); if ($limit = $courseset->getAdmissionRule('LimitedAdmission')) { $msg_details[] = sprintf(_("Diese Veranstaltung gehört zu einem Anmeldeset mit %s Veranstaltungen. Sie können maximal %s davon belegen. Bei der Verteilung werden die von Ihnen gewünschten Prioritäten berücksichtigt."), count($courseset->getCourses()), $limit->getMaxNumber()); $this->user_max_limit = $limit->getMaxNumberForUser($user_id); if (get_config('IMPORTANT_SEMNUMBER')) { $order = "ORDER BY VeranstaltungsNummer, Name"; } else { $order = "ORDER BY Name"; } $this->priocourses = Course::findMany($courseset->getCourses(), $order); $this->user_prio = AdmissionPriority::getPrioritiesByUser($courseset->getId(), $user_id); $this->max_limit = $limit->getMaxNumber(); $this->prio_stats = AdmissionPriority::getPrioritiesStats($courseset->getId()); $this->already_claimed = count($this->user_prio); } else { $this->priocourses = Course::find($this->course_id); $this->already_claimed = array_key_exists($this->course_id, AdmissionPriority::getPrioritiesByUser($courseset->getId(), $user_id)); } $msg_details[] = _("Zeitpunkt der automatischen Verteilung: ") . strftime("%x %X", $courseset->getSeatDistributionTime()); $this->num_claiming = count(AdmissionPriority::getPrioritiesByCourse($courseset->getId(), $this->course_id)); if ($this->already_claimed) { $msg_details[] = _("Sie sind bereits für die Verteilung angemeldet."); } } $this->courseset_message = MessageBox::info($msg, $msg_details); } else { $enrol_user = true; } } } else { $enrol_user = true; } if ($enrol_user) { $course = Seminar::GetInstance($this->course_id); if ($course->admission_prelim) { if (!$course->isStudygroup() && $course->admission_prelim_txt && !Request::submitted('apply')) { $this->admission_prelim_txt = $course->admission_prelim_txt; $this->admission_prelim_comment = Config::get()->ADMISSION_PRELIM_COMMENT_ENABLE; $this->admission_form = $this->render_template_as_string('course/enrolment/prelim'); } else { if (Request::get('admission_comment')) { $admission_comment = get_fullname() . ': ' . Request::get('admission_comment'); } else { $admission_comment = ''; } if ($course->addPreliminaryMember($user_id, $admission_comment)) { if ($course->isStudygroup()) { if (StudygroupModel::isInvited($user_id, $this->course_id)) { // an invitation exists, so accept the join request automatically $status = 'autor'; StudygroupModel::accept_user(get_username($user_id), $this->course_id); StudygroupModel::cancelInvitation(get_username($user_id), $this->course_id); $success = sprintf(_("Sie wurden in die Veranstaltung %s als %s eingetragen."), $course->getName(), get_title_for_status($status, 1)); PageLayout::postMessage(MessageBox::success($success)); } else { $success = sprintf(_("Sie wurden auf die Anmeldeliste der Studiengruppe %s eingetragen. Die Moderatoren der Studiengruppe können Sie jetzt freischalten."), $course->getName()); PageLayout::postMessage(MessageBox::success($success)); } } else { $success = sprintf(_("Sie wurden in die Veranstaltung %s vorläufig eingetragen."), $course->getName()); PageLayout::postMessage(MessageBox::success($success)); } } } } else { $status = 'autor'; if ($course->addMember($user_id, $status)) { $success = sprintf(_("Sie wurden in die Veranstaltung %s als %s eingetragen."), $course->getName(), get_title_for_status($status, 1)); PageLayout::postMessage(MessageBox::success($success)); $this->enrol_user = true; if (StudygroupModel::isInvited($user_id, $this->course_id)) { // delete an existing invitation StudygroupModel::cancelInvitation(get_username($user_id), $this->course_id); } } } unset($this->courseset_message); } StudipLock::release(); }