Skip to content
Snippets Groups Projects
Course.php 92.2 KiB
Newer Older
 * model class for table seminare
 *
 * 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      André Noack <noack@data-quest.de>
 * @copyright   2012 Stud.IP Core-Group
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
 * @category    Stud.IP
 *
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
 * @property string $id alias column for seminar_id
 * @property string $seminar_id database column
 * @property string|null $veranstaltungsnummer database column
 * @property string $institut_id database column
 * @property I18NString $name database column
 * @property I18NString|null $untertitel database column
 * @property int $status database column
 * @property I18NString $beschreibung database column
 * @property I18NString|null $ort database column
 * @property string|null $sonstiges database column
 * @property int $lesezugriff database column
 * @property int $schreibzugriff database column
 * @property I18NString|null $art database column
 * @property I18NString|null $teilnehmer database column
 * @property I18NString|null $vorrausetzungen database column
 * @property I18NString|null $lernorga database column
 * @property I18NString|null $leistungsnachweis database column
 * @property int $mkdate database column
 * @property int $chdate database column
 * @property string|null $ects database column
 * @property int|null $admission_turnout database column
 * @property int|null $admission_binding database column
 * @property int $admission_prelim database column
 * @property string|null $admission_prelim_txt database column
 * @property int $admission_disable_waitlist database column
 * @property int $visible database column
 * @property int|null $showscore database column
 * @property string|null $aux_lock_rule database column
 * @property int $aux_lock_rule_forced database column
 * @property string|null $lock_rule database column
 * @property int $admission_waitlist_max database column
 * @property int $admission_disable_waitlist_move database column
 * @property int $completion database column
 * @property string|null $parent_course database column
 * @property SimpleORMapCollection|CourseTopic[] $topics has_many CourseTopic
 * @property SimpleORMapCollection|CourseDate[] $dates has_many CourseDate
 * @property SimpleORMapCollection|CourseExDate[] $ex_dates has_many CourseExDate
 * @property SimpleORMapCollection|CourseMember[] $members has_many CourseMember
 * @property SimpleORMapCollection|Deputy[] $deputies has_many Deputy
 * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_many Statusgruppen
 * @property SimpleORMapCollection|AdmissionApplication[] $admission_applicants has_many AdmissionApplication
 * @property SimpleORMapCollection|DatafieldEntryModel[] $datafields has_many DatafieldEntryModel
 * @property SimpleORMapCollection|SeminarCycleDate[] $cycles has_many SeminarCycleDate
 * @property SimpleORMapCollection|BlubberThread[] $blubberthreads has_many BlubberThread
 * @property SimpleORMapCollection|ConsultationBlock[] $consultation_blocks has_many ConsultationBlock
 * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest
 * @property SimpleORMapCollection|Course[] $children has_many Course
 * @property SimpleORMapCollection|ToolActivation[] $tools has_many ToolActivation
 * @property SimpleORMapCollection|CourseMemberNotification[] $member_notifications has_many CourseMemberNotification
 * @property SimpleORMapCollection|Courseware\Unit[] $courseware_units has_many Courseware\Unit
 * @property Institute $home_institut belongs_to Institute
 * @property AuxLockRule|null $aux belongs_to AuxLockRule
 * @property Course|null $parent belongs_to Course
 * @property SimpleORMapCollection|Semester[] $semesters has_and_belongs_to_many Semester
 * @property SimpleORMapCollection|StudipStudyArea[] $study_areas has_and_belongs_to_many StudipStudyArea
 * @property SimpleORMapCollection|Institute[] $institutes has_and_belongs_to_many Institute
 * @property SimpleORMapCollection|UserDomain[] $domains has_and_belongs_to_many UserDomain
 * @property-read mixed $teachers additional field
 * @property mixed $end_time additional field
 * @property mixed $start_semester additional field
 * @property mixed $end_semester additional field
 * @property-read mixed $semester_text additional field
 * @property-read mixed $config additional field
class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange, Studip\Calendar\Owner
Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * @var Semester initial start semester.
     */
    protected $initial_start_semester;

    /**
     * @var Semester initial end semester.
     */
    protected $initial_end_semester;

    protected static function configure($config = [])
    {
        $config['db_table'] = 'seminare';
        $config['has_many']['topics'] = [
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['dates'] = [
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store',
            'order_by'          => 'ORDER BY date'
        ];
        $config['has_many']['ex_dates'] = [
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store',
        ];
        $config['has_many']['members'] = [
            'assoc_func' => 'findByCourse',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['deputies'] = [
            'assoc_func' => 'findByRange_id',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['statusgruppen'] = [
            'class_name' => Statusgruppen::class,
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['admission_applicants'] = [
            'class_name' => AdmissionApplication::class,
            'assoc_func' => 'findByCourse',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['datafields'] = [
            'class_name' => DatafieldEntryModel::class,
            'assoc_func' => 'findByModel',
            'assoc_foreign_key' => function ($model, $params) {
                $model->setValue('range_id', $params[0]->id);
            },
            'foreign_key' => function ($course) {
                return [$course];
            },
            'on_delete' => 'delete',
            'on_store'  => 'store',
        ];
        $config['has_many']['cycles'] = [
            'class_name' => SeminarCycleDate::class,
            'assoc_func' => 'findBySeminar',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
Moritz Strohm's avatar
Moritz Strohm committed
        $config['has_many']['scm_entries'] = [
            'class_name'        => StudipScmEntry::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];
        $config['has_many']['wiki_pages'] = [
            'class_name'        => WikiPage::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
            'on_store'          => 'store'
        ];
        $config['has_many']['news'] = [
            'class_name'        => StudipNews::class,
            'thru_table'        => 'news_range',
            'thru_key'          => 'range_id',
            'thru_assoc_key'    => 'news_id',
        ];
        $config['has_many']['blubberthreads'] = [
            'class_name' => BlubberThread::class,
            'assoc_func' => 'findBySeminar',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_many']['consultation_blocks'] = [
            'class_name'        => ConsultationBlock::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete',
        ];

        $config['has_and_belongs_to_many']['semesters'] = [
            'thru_table'     => 'semester_courses',
            'thru_key'       => 'course_id',
            'thru_assoc_key' => 'semester_id',
            'order_by'       => 'ORDER BY beginn ASC',
            'on_delete'      => 'delete',
            'on_store'       => 'store',
        ];

        $config['belongs_to']['home_institut'] = [
            'foreign_key' => 'institut_id',
            'assoc_func'  => 'find',
        ];
        $config['belongs_to']['aux'] = [
            'foreign_key' => 'aux_lock_rule',
        ];
        $config['has_and_belongs_to_many']['study_areas'] = [
            'class_name' => StudipStudyArea::class,
            'thru_table' => 'seminar_sem_tree',
            'on_delete'  => 'delete',
            'on_store'   => 'store',
        ];
        $config['has_and_belongs_to_many']['institutes'] = [
Moritz Strohm's avatar
Moritz Strohm committed
            'class_name'     => Institute::class,
            'thru_table'     => 'seminar_inst',
            'thru_key'       => 'seminar_id',
            'thru_assoc_key' => 'institut_id',
            'on_delete'      => 'delete',
            'on_store'       => 'store',
        ];

        $config['has_and_belongs_to_many']['domains'] = [
            'thru_table'        => 'seminar_userdomains',
            'on_delete'          => 'delete',
            'on_store'           => 'store',
            'order_by'          => 'ORDER BY name',
        ];

        $config['has_many']['room_requests'] = [
            'assoc_foreign_key' => 'course_id',
            'on_delete'         => 'delete',
        ];
Moritz Strohm's avatar
Moritz Strohm committed
        $config['has_many']['resource_bookings'] = [
            'class_name'        => ResourceBooking::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'         => 'delete'
        ];
        $config['belongs_to']['parent'] = [
            'foreign_key' => 'parent_course'
        ];
        $config['has_many']['children'] = [
            'assoc_foreign_key' => 'parent_course',
            'order_by'          => 'GROUP BY seminar_id ORDER BY VeranstaltungsNummer, Name'
        ];
        $config['has_many']['tools'] = [
            'class_name'        => ToolActivation::class,
            'assoc_foreign_key' => 'range_id',
            'order_by'          => 'ORDER BY position',
            'on_delete'         => 'delete',
        ];
        $config['has_many']['member_notifications'] = [
            'class_name'        => CourseMemberNotification::class,
            'on_delete'         => 'delete',
        ];

Moritz Strohm's avatar
Moritz Strohm committed
        $config['has_many']['config_values'] = [
            'class_name'        => ConfigValue::class,
            'assoc_foreign_key' => 'range_id',
            'on_store'          => 'store',
            'on_delete'         => 'delete'
        ];

Ron Lucke's avatar
Ron Lucke committed
        $config['has_many']['courseware_units'] = [
            'class_name' => \Courseware\Unit::class,
            'assoc_foreign_key' => 'range_id',
            'on_delete'  => 'delete',
        ];

        $config['default_values']['lesezugriff'] = 1;
        $config['default_values']['schreibzugriff'] = 1;

        $config['additional_fields']['teachers'] = [
            'get' => 'getTeachers'
        ];
        $config['additional_fields']['end_time'] = true;

        $config['additional_fields']['start_semester'] = [
            'get' => 'getStartSemester',
            'set' => '_set_semester'
        ];
        $config['additional_fields']['end_semester'] = [
            'get' => 'getEndSemester',
            'set' => '_set_semester'
        ];
        $config['additional_fields']['semester_text'] = [
            'get' => 'getTextualSemester'
        ];

        $config['additional_fields']['config'] = [
            'get' => function (Course $course) {
                return $course->getConfiguration();
            }
        ];

        $config['notification_map']['after_create'] = 'CourseDidCreateOrUpdate';
        $config['notification_map']['after_store'] = 'CourseDidCreateOrUpdate';

        $config['i18n_fields']['name'] = true;
        $config['i18n_fields']['untertitel'] = true;
        $config['i18n_fields']['beschreibung'] = true;
        $config['i18n_fields']['art'] = true;
        $config['i18n_fields']['teilnehmer'] = true;
        $config['i18n_fields']['vorrausetzungen'] = true;
        $config['i18n_fields']['lernorga'] = true;
        $config['i18n_fields']['leistungsnachweis'] = true;
        $config['i18n_fields']['ort'] = true;

Moritz Strohm's avatar
Moritz Strohm committed
        $config['registered_callbacks']['before_store'][] = 'logStore';
        $config['registered_callbacks']['after_create'][] = 'setDefaultTools';
        $config['registered_callbacks']['after_delete'][] = function ($course) {
            CourseAvatar::getAvatar($course->id)->reset();
            FeedbackElement::deleteBySQL('course_id = ?', [$course->id]);
            // Remove subcourse relations, leaving subcourses intact.
            DBManager::get()->execute(
                "UPDATE `seminare` SET `parent_course` = NULL WHERE `parent_course` = :course",
                ['course' => $course->id]
            );
Moritz Strohm's avatar
Moritz Strohm committed

            //Delete forum entries:
            foreach (PluginEngine::getPlugins(ForumModule::class) as $forum_tool) {
                $forum_tool->deleteContents($course->id);
            }

            //Delete all files:
            $folder = Folder::findTopFolder($course->id);
            if ($folder) {
                $folder->delete();
            }

            //Unlink all news and delete them in RSS feeds:
            StudipNews::DeleteNewsRanges($course->id);
            StudipNews::UnsetRssId($course->id);

            //Cleanup remaining wiki table entries:
            $query = 'DELETE FROM `wiki_links` WHERE `range_id` = ?';
            DBManager::get()->execute($query, [$course->id]);
Moritz Strohm's avatar
Moritz Strohm committed

            //Remove all entries of the course in calendars:
Moritz Strohm's avatar
Moritz Strohm committed
            $query = 'DELETE FROM `schedule_courses` WHERE `course_id` = ?';
Moritz Strohm's avatar
Moritz Strohm committed
            $statement = DBManager::get()->execute($query, [$course->id]);

            //Remove all entries in object_user_vists for the course:
            object_kill_visits(null, $course->id);

            //Remove deputies:
            Deputy::deleteByRange_id($course->id);

            //Remove user domains:
            UserDomain::removeUserDomainsForSeminar($course->id);

            //Remove auto-insert entries:
            AutoInsert::deleteSeminar($course->id);

            //Remove assignments to admission sets:
            $cs = $this->getCourseSet();
            if ($cs) {
                CourseSet::removeCourseFromSet($cs->getId(), $course->id);
                $cs->load();
                if (!count($cs->getCourses()) && $cs->isGlobal() && $cs->getUserid() != '') {
                    $cs->delete();
                }
            }
            AdmissionPriority::unsetAllPrioritiesForCourse($course->id);

            //Create a log entry:
            StudipLog::log('SEM_ARCHIVE', $course->id, NULL, $course->getFullName('number-name-semester'));
        };

        parent::configure($config);
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * @param string $relation
     */
    public function initRelation($relation): void
    {
        if ($relation === 'semesters' && $this->relations[$relation] === null) {
            parent::initRelation($relation);
            $this->initial_start_semester = $this->getStartSemester();
            $this->initial_end_semester = $this->getEndSemester();
        }
        parent::initRelation($relation);
    }

    /**
     * Override of SimpleORMap::cbAfterInitialize for resetting the flags that indicate semester changes.
     *
     * @see SimpleORMap::cbAfterInitialize
     */
    protected function cbAfterInitialize($cb_type): void
    {
        parent::cbAfterInitialize($cb_type);
        //Reset the flags for the start and end semester:
        $this->initial_start_semester = null;
        $this->initial_end_semester = null;
    }

    /**
     * Returns the currently active course or false if none is active.
     *
     * @return Course object of currently active course, null otherwise
     * @since 3.0
     */
    public static function findCurrent()
    {
        if (Context::isCourse()) {
            return Context::get();
        }

        return null;
    }

    /**
     * Returns the associated mvv modules for a given course id.
     *
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
     * @param string     $course_id
     * @param array|null $statusses Limit the results by a given module status
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
    public static function getMVVModulesForCourseId(string $course_id, ?array $statusses = null): array
    {
        $query = "SELECT mvv_modul.*
                  FROM mvv_lvgruppe_seminar
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
                  JOIN `mvv_lvgruppe` ON (`mvv_lvgruppe_seminar`.`lvgruppe_id` = `mvv_lvgruppe`.`lvgruppe_id`)
                  JOIN `mvv_lvgruppe_modulteil` ON (`mvv_lvgruppe_seminar`.`lvgruppe_id` = `mvv_lvgruppe_modulteil`.`lvgruppe_id`)
                  JOIN `mvv_modulteil` ON (`mvv_lvgruppe_modulteil`.`modulteil_id` = `mvv_modulteil`.`modulteil_id`)
                  JOIN `mvv_modul` ON (`mvv_modulteil`.`modul_id` = `mvv_modul`.`modul_id`)
                  WHERE seminar_id = ?";
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
        $parameters = [$course_id];

        if ($statusses !== null) {
            $query .= ' AND `mvv_modul`.`stat` IN (?)';
            $parameters[] = $statusses;
        }

        return DBManager::get()->fetchAll($query, $parameters, function ($row) {
            return Modul::buildExisting($row);
        });
    }

    public function _set_semester($field, $value)
        $method = 'set' . ($field === 'start_semester' ? 'StartSemester' : 'EndSemester');
        $this->$method($value);
     * @param Semester $semester
    public function setStartSemester(Semester $semester)
        $end_semester = $this->semesters->last();
        $start_semester = $this->semesters->first();
        if ($start_semester && $start_semester->id === $semester->id) {
André Noack's avatar
André Noack committed
            return;
        }
        if ($end_semester) {
            if (count($this->semesters) > 1 && $end_semester->beginn < $semester->beginn) {
                throw new InvalidArgumentException('start-semester must start before end-semester');
            }
            foreach ($this->semesters as $key => $one_semester) {
André Noack's avatar
André Noack committed
                if ($one_semester->beginn < $semester->beginn) {
                    $this->semesters->offsetUnset($key);
                }
            }
        }
        $this->semesters[] = $semester;
        $this->semesters->orderBy('beginn asc');
        //add possibly missing semesters between start_semester and end_semester
        if (count($this->semesters) > 1 && $semester->beginn < $start_semester->beginn) {
            $this->setEndSemester($end_semester);
        }
    /**
     * @param Semester|null $semester
     */
    public function setEndSemester(?Semester $semester)
André Noack's avatar
André Noack committed
        $end_semester = $this->semesters->last();
        $start_semester = $this->semesters->first();
        if (
            (is_null($end_semester) && is_null($semester))
            || ($end_semester && $semester && $end_semester->id === $semester->id)) {
André Noack's avatar
André Noack committed
            return;
        }
        if ($start_semester) {
            if ($semester && $start_semester->beginn > $semester->beginn) {
                throw new InvalidArgumentException('end-semester must start after start-semester');
            }
            $this->semesters = [];
            if ($semester) {
                $all_semester = SimpleCollection::createFromArray(Semester::getAll());
                $this->semesters = $all_semester->findBy('beginn', [$start_semester->beginn, $semester->beginn], '>=<=');
            }
            if ($semester) {
                $this->semesters[] = $semester;
            }
        }
    }

    /**
     * Retrieves the first semester of a course, if applicable.
     *
     * @returns Semester|null Either the first semester of the course
     *     or null, if no semester could be found.
     */
    public function getStartSemester()
    {
Moritz Strohm's avatar
Moritz Strohm committed
        //this is called by __get() and therefore using magic properties is not always safe
        if ($this->relations['semesters'] === null) {
            $this->initRelation('semesters');
Moritz Strohm's avatar
Moritz Strohm committed
        return $this->relations['semesters']->first() ?? Semester::findCurrent();
    }

    /**
     * Retrieves the last semester of a course, if applicable.
     *
     * @returns Semester|null Either the last semester of the course
     *     or null, if no semester could be found.
     */
    public function getEndSemester()
    {
Moritz Strohm's avatar
Moritz Strohm committed
        //this is called by __get() and therefore using magic properties is not always safe
        if ($this->relations['semesters'] === null) {
            $this->initRelation('semesters');
Moritz Strohm's avatar
Moritz Strohm committed
        return $this->relations['semesters']->last();
    }

    /**
     * Returns the readable semester duration as as string
     * @return string : readable semester
     */
    public function getTextualSemester()
    {
        if (count($this->semesters) > 1) {
            return $this->start_semester->short_name . ' - ' . $this->end_semester->short_name;
        } elseif (count($this->semesters) === 1) {
            return $this->start_semester->short_name;
        }
    }

    /**
     * Returns true if this course has no end-semester. Else false.
     * @return bool : true if there is no end-semester
     */
    public function isOpenEnded()
    {
        return count($this->semesters) === 0;
    }

    /**
     * Returns if this course is in the given semester
     * @param Semester $semester : instance of the given semester
     * @return bool : true if this course is part of this semester
     */
    public function isInSemester(Semester $semester)
    {
        if (count($this->semesters) > 0) {
            foreach ($this->semesters as $s) {
                if ($s->id === $semester->id) {
                    return true;
                }
            }
            return false;
        } else {
    public function getTeachers()
    {
        return $this->members->filter(function ($m) {
            return $m['status'] === 'dozent';
        });
    }

Moritz Strohm's avatar
Moritz Strohm committed
    public function getFreeSeats() : int
    {
        $free_seats = $this->admission_turnout - $this->getNumParticipants();
        return max($free_seats, 0);
    }

    public function isWaitlistAvailable()
    {
        if ($this->admission_disable_waitlist) {
            return false;
        }

        if ($this->admission_waitlist_max) {
            return $this->admission_waitlist_max - $this->getNumWaiting() > 0;
        }

        return true;
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * Determines whether the course has at least one course set attached to it.
     *
     * @return bool True, if the course has at least one course set, false otherwise.
     */
    public function hasCourseSet() : bool
    {
        return CourseSet::countBySeminar_id($this->id) > 0;
    }

    /**
     * Retrieves the course set of th course, if the course is associated to a course set.
     *
     * @return CourseSet|null The course set of the course, if it is associated to one.
     */
    public function getCourseSet() : ?CourseSet
    {
        return CourseSet::getSetForCourse($this->id);
    }

    /**
     * Determines whether the number of participants in this course is limited
     * by a course set whose seat distribution is enabled.
     *
     * @return boolean True, if a course set exists and its seat distribution is enabled,
     *     false otherwise.
     */
    public function isAdmissionEnabled() : bool
    {
        $cs = $this->getCourseSet();
        return $cs && $cs->isSeatDistributionEnabled();
    }

    /**
     * Determines by the course set of the course (if any), whether the admission
     * is locked or not.
     *
     * @return bool True, if the admission is locked, false otherwise.
     */
    public function isAdmissionLocked() : bool
    {
        $cs = $this->getCourseSet();
        return $cs && $cs->hasAdmissionRule('LockedAdmission');
    }

    /**
     * Determines by looking at the course set (if any), whether the course
     * is password protected or not.
     *
     * @return bool True, fi the course is password protected, false otherwise.
     */
    public function isPasswordProtected() : bool
    {
        $cs = $this->getCourseSet();
        return $cs && $cs->hasAdmissionRule('PasswordAdmission');
    }

    /**
     * Determines if there is an admission time frame for this course by looking
     * at the course set (if any). If such a time frame exists, it is returned
     * as an associative array with the start and end timestamp.
     *
     * @returns array An associative array with the array keys "start_time" and "end_time"
     *     containing the start and end timestamp of the admission. In case no such time
     *     frame exists, an empty array is returned instead.
     */
    public function getAdmissionTimeFrame() : array
    {
        $cs = $this->getCourseSet();
        if ($cs && $cs->hasAdmissionRule(TimedAdmission::class)) {
            $rule = $cs->getAdmissionRule(TimedAdmission::class);
            return [
                'start_time' => $rule->getStartTime(),
                'end_time'   => $rule->getEndTime()
            ];
        }
        return [];
    }

    /**
     * Adds a user as preliminary member to this course.
     *
     * @param User $user The user to be added as preliminary member.
     * @param string $comment An optional comment for the preliminary membership.
     *
     * @return AdmissionApplication The AdmissionApplication object for the preliminary membership.
     *
     * @throws \Studip\Exception In case the user cannot be added as preliminary member.
     */
    public function addPreliminaryMember(User $user, string $comment = '') : AdmissionApplication
    {
        $new_admission_member = new AdmissionApplication();
        $new_admission_member->user_id = $user->id;
        $new_admission_member->position = 0;
        $new_admission_member->status = 'accepted';
        $new_admission_member->comment = $comment;

        $this->admission_applicants[] = $new_admission_member;
        if (!$new_admission_member->store()) {
            throw new \Studip\Exception(
                sprintf(
                    _('%1$s konnte nicht als vorläufig teilnehmende Person zur Veranstaltung %2$s hinzugefügt werden.'),
                    $user->getFullName(),
                    $this->name
                ),
                'add_preliminary_failed'
            );
        }
        if ($this->isStudygroup()) {
            StudygroupModel::applicationNotice($this->id, $user->id);
        }
        $course_set = $this->getCourseSet();
        if ($course_set) {
            AdmissionPriority::unsetPriority($course_set->getId(), $user->id, $this->id);
        }

        //Create a log entry:
        StudipLog::log('SEM_USER_ADD', $this->id, $user->id, 'accepted', 'Vorläufig akzeptiert');

        return $new_admission_member;
    }

    /**
     * Removes a preliminary member from the course.
     *
     * @param User $user The member to be removed.
     *
     * @throws \Studip\Exception In case the user is not a preliminary member or in case they
     *     cannot be removed as preliminary member.
     */
    public function removePreliminaryMember(User $user) : void
    {
        //Get the status of the user first:
        $application = AdmissionApplication::findOneBySQL(
            'seminar_id = :course_id AND user_id = :user_id',
            [
                'course_id' => $this->id,
                'user_id'   => $user->id
            ]
        );
        if (!$application) {
            throw new \Studip\Exception(
                sprintf(
                    _('%1$s ist nicht als vorläufig teilnehmende Person in der Veranstaltung %2$s eingetragen.'),
                    $user->getFullName(),
                    $this->name
                ),
                'preliminary_member_not_found'
            );
        }

        $deleted_from_course_set = false;
        $course_set = $this->getCourseSet();
        if ($course_set) {
            $deleted_from_course_set = AdmissionPriority::unsetPriority(
                $course_set->getId(),
                $user->id,
                $this->id
            );
        }
        if ($application->delete() || $deleted_from_course_set) {
            setTempLanguage($user->id);
            $message = '';
            if ($application->status === 'accepted') {
                $message = studip_interpolate(
                    _('Ihre vorläufige Anmeldung zur Veranstaltung %{name} wurde aufgehoben. Sie sind damit __nicht__ zugelassen worden.'),
                    ['name' => $this->getFullName()]
                );
            } else {
                $message = studip_interpolate(
                    _('Sie wurden von der Warteliste der Veranstaltung %{name} gestrichen. Sie sind damit __nicht__ zugelassen worden.'),
                    ['name' => $this->getFullName()]
                );
            }
            $messaging = new messaging();
            $messaging->insert_message(
                $message,
                $user->username,
                '____%system%____',
                false,
                false,
                '1',
                false,
                studip_interpolate(
                    _('%{course_name}: Sie wurden nicht zugelassen!'),
                    ['course_name' => $this->getFullName()]
                ),
                true
            );
            restoreLanguage();
            StudipLog::log('SEM_USER_DEL', $this->id, $user->id, 'Wurde aus der Veranstaltung entfernt');
        } else {
            throw new \Studip\Exception(
                sprintf(
                    _('%1$s konnte nicht als vorläufig teilnehmende Person aus der Veranstaltung %2$s entfernt werden.'),
                    $user->getFullName(),
                    $this->name
                ),
                'remove_preliminary_failed'
            );
        }
    }

    /**
     * Adds a user to the waitlist of this course.
     *
     * @param User $user The user to be added onto the waitlist.
     *
     * @param int $position The position of the user on the waitlist.
     *
     * @param bool $send_mail Whether to send a mail to the user that has been added
     *     (true) or not (false). Defaults to true.
     *
     * @return AdmissionApplication The AdmissionApplication object for the added user.
     *
     * @throws \Studip\Exception In case the user cannot be added onto the waitlist.
     */
    public function addMemberToWaitlist(
        User $user,
        int $position = PHP_INT_MAX,
        bool $send_mail = true
    ) : AdmissionApplication
    {
        $member_exists = AdmissionApplication::exists([$user->id, $this->id])
            || CourseMember::find([$this->id, $user->id]);
        if ($member_exists) {
            throw new \Studip\EnrolmentException(
                sprintf(
                    _('%1$s ist bereits Mitglied der Veranstaltung %2$s.'),
                    $user->getFullName(),
                    $this->name
                ),
                \Studip\EnrolmentException::ALREADY_MEMBER
            );
        }
        if ($position === PHP_INT_MAX) {
            //Append the user to the end of the waitlist.
            //NOTE: If this method is called two times at the same time for the
            //same course, there may be course members with the same position!
            $position = DBManager::get()->fetchColumn(
                "SELECT MAX(`position`)
                 FROM `admission_seminar_user`
                 WHERE `seminar_id` = :course_id
                   AND `status`='awaiting'",
                ['course_id' => $this->id]
            );
            if ($position === false) {
                //No members on the waitlist.
                $position = 0;
            }
        }
        $new_admission_member = new AdmissionApplication();
        $new_admission_member->user_id = $user->id;
        $new_admission_member->position = strval($position);
        $new_admission_member->status = 'awaiting';
        $new_admission_member->seminar_id = $this->id;
        if (!$new_admission_member->store()) {
            throw new \Studip\EnrolmentException(
                sprintf(
                    _('%1$s konnte nicht auf die Warteliste der Veranstaltung %2$s gesetzt werden.'),
                    $user->getFullName(),
                    $this->name
                ),
                \Studip\EnrolmentException::ADD_AWAITING_FAILED
            );
        }

        //Reset the admission_applicants relation:
        $this->resetRelation('admission_applicants');

        //Renumber all members on the waitlist:
        AdmissionApplication::renumberAdmission($this->id);

        //Create a log entry:
        StudipLog::log(
            'SEM_USER_ADD',
            $this->id,
            $user->id,
            'awaiting',
            sprintf('Auf Warteliste gesetzt, Position: %u', $position)
        );

        if ($send_mail) {
            setTempLanguage($user->id);
            $body = sprintf(
                _('Sie wurden auf die Warteliste der Veranstaltung %s gesetzt.'),
                $this->getFullName()
            );
            $messaging = new messaging();
            $messaging->insert_message(
                $body,
                $user->username,
                '____%system%____',
                false,
                false,
                '1',
                false,
                _('Auf die Warteliste einer Veranstaltung eingetragen'),
                true
            );
            restoreLanguage();
        }

        //Everything went fine: Re-load the new admission member before returning it,
        //since its position number may have changed during renumbering:
        return AdmissionApplication::findOneBySQL(
            '`user_id` = :user_id AND `seminar_id` = :course_id',
            ['user_id' => $user->id, 'course_id' => $this->id]
        );
    }

    /**
     * Retrieves the course category for this course.
     *
     * @return SeminarCategories The category object of the course.
     */
    public function getCourseCategory() : SeminarCategories
    {
        return SeminarCategories::GetByTypeId($this->status);
    }

    /**
     * Retrieves all members of a status
     *
     * @param String|Array $status        the status to filter with
     * @param bool         $as_collection return collection instead of array?
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
     * @return Array|SimpleCollection an array of all those members.
     */
    public function getMembersWithStatus($status, $as_collection = false)
    {
        $result = CourseMember::findByCourseAndStatus($this->id, $status);
        return $as_collection
             ? SimpleCollection::createFromArray($result)
             : $result;
    }

    /**
     * Retrieves the number of all members of a status
     *
     * @param String|Array $status  the status to filter with
     *
     * @return int the number of all those members.
     */
    public function countMembersWithStatus($status)
    {
        return CourseMember::countByCourseAndStatus($this->id, $status);
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * Adds a user to this course.
     *
     * @param User $user The user to be added.
     * @param string $permission_level The permission level the user shall get in the course.
     * @param bool $regard_contingent Whether to regard the contingent of the course (true)
     *     or whether to ignore it (false). Defaults to true.
     * @param bool $send_mail Whether to send a mail to the new participant (true) or not (false).
     *     Defaults to true.
     * @param bool $renumber_admission Whether to call AdmissionApplication::renumberAdmission when
     *     the admission of the user has been removed (true) or whether not to renumber the admission
     *     entries (false). Defaults to true.
     *     Setting this parameter to false is useful when adding several users at once and then
     *     manually call AdmissionApplication::renumberAdmission so that the entries are renumbered
     *     only once after all the users have been added.
     *
     * @return CourseMember The CourseMember object for the user.
     *
     * @throws \Studip\EnrolmentException In case the user is already in the course but cannot get a higher permission level or
     *     they are the only lecturer and can therefore not get a lower permission level.
     */
    public function addMember(
        User $user,
        string $permission_level = 'autor',
        bool $regard_contingent = true,
        bool $send_mail = true,
        bool $renumber_admission = true
    ) : CourseMember
    {
        //TODO: Put checks for entry into Course::getEnrolmentInformation.
        //Checks regarding the promotion/demotion of users in courses shall be
        //transferred to a new method.

        if (!in_array($permission_level, ['user', 'autor', 'tutor', 'dozent'])) {
            throw new \Studip\EnrolmentException(
                _('Die Rechtestufe ist für die Eintragung in eine Veranstaltung unpassend.'),
                \Studip\EnrolmentException::INVALID_PERMISSION_LEVEL
            );
        }

        $db = DBManager::get();

        //In case the course only allows users of the institute to be members,
        //we must check if the user is a member of the institute:
        $course_category = $this->getCourseCategory();
        if ($course_category->only_inst_user) {
            //Only institute members are allowed:
            $stmt = $db->prepare(
                "SELECT 1
                 FROM `user_inst`
                 JOIN `seminar_inst` USING (`institute_id`)
                 WHERE `user_inst`.`user_id` = :user_id
                   AND `seminar_inst`.`seminar_id` = :course_id"
            );
            $stmt->execute([
                'course_id' => $this->id,
                'user_id'   => $user->id,
            ]);
            $user_in_institute = $stmt->fetchColumn();
            if (!$user_in_institute) {
                throw new \Studip\EnrolmentException(
                    _('Die einzutragende Person ist kein Mitglied einer Einrichtung, zu der die Veranstaltung zugeordnet ist.'),
                    \Studip\EnrolmentException::NO_INSTITUTE_MEMBER
                );
            }
        }

        //Load the course member object:
        $course_member = CourseMember::findOneBySQL(
            '`seminar_id` = :course_id AND `user_id` = :user_id',
            ['course_id' => $this->id, 'user_id' => $user->id]
        );
        $new_member_position = $db->fetchColumn(
            'SELECT MAX(`position`) + 1
             FROM `seminar_user`
             WHERE `status` = :status
               AND `seminar_id` = :course_id',
            ['status' => $permission_level, 'course_id' => $this->id]
        ) ?? 0;
        $number_of_lecturers = CourseMember::countByCourseAndStatus($this->id, 'dozent');

        if (!$course_member) {
            $course_member = new CourseMember();
            $course_member->seminar_id = $this->id;
            $course_member->user_id    = $user->id;
            $course_member->status     = $permission_level;
        }
        $course_member->position = $new_member_position;
        if (in_array($permission_level, ['tutor', 'dozent'])) {
            //Tutors and lecturers are always visible in the course:
            $course_member->visible = 'yes';
        } else {
            //All others may decide for themselves:
            $course_member->visible = 'unknown';
        }

        $ranks = array_flip(['user', 'autor', 'tutor', 'dozent']);

        if ($course_member->isNew()) {
            //The user shall be added to the course. Before storing, we must check
            //if the contingent shall be regarded and if there is a free seat
            //for the user:

            //TODO: Move the following check back to controllers.
            //Background: Lecturers may enforce the entry of a student, but the latter must not
            //override the checks.
            if (
                $permission_level === 'autor'
                && $regard_contingent
                && $this->isAdmissionEnabled()
                && $this->getFreeSeats() < 1
            ) {
                //There is no free seat to add another member.
                throw new \Studip\EnrolmentException(
                    sprintf(
                        _('Für %s ist kein Platz mehr in der Veranstaltung frei.'),
                        $user->getFullName()
                    ),
                    \Studip\EnrolmentException::COURSE_IS_FULL
                );
            }

            $course_member->store();

            //Delete the user from admission applications:
            $application_removed = AdmissionApplication::deleteBySQL(
                '`user_id` = :user_id AND `seminar_id` = :course_id',
                ['user_id' => $user->id, 'course_id' => $this->id]
            );
            if ($application_removed && $renumber_admission) {
                //Renumber the waitlist or the other admission list:
                AdmissionApplication::renumberAdmission($this->id);
            }

            //Remove the user from the course set, if any:
            $course_set = $this->getCourseSet();
            $removed_from_course_set = 0;
            if ($course_set) {
                $removed_from_course_set = AdmissionPriority::unsetPriority($course_set->getId(), $user->id, $this->id);
            }

            if ($permission_level === 'dozent' && Config::get()->DEPUTIES_ENABLE) {
                //Delete a possible deputy entry for the lecturer:
                $deputy = Deputy::find([$this->id, $user->id]);
                if ($deputy) {
                    $deputy->delete();
                }

                //Assign all default deputies of the lecturer to the course
                //if they are not already a lecturer of the course:
                $unassigned_deputies = Deputy::findBySQL(
                    "`range_id` = :lecturer_id
                      AND `user_id` NOT IN (
                         SELECT `user_id` FROM `seminar_user`
                         WHERE `seminar_id` = :course_id
                         AND `status` = 'dozent'
                      )",
                    [
                        'lecturer_id' => $user->id,
                        'course_id'   => $this->id
                    ]
                );
                foreach ($unassigned_deputies as $deputy) {
                    Deputy::addDeputy($deputy->user_id, $this->id);
                }
            }

            //Delete course entries in the schedule:
Moritz Strohm's avatar
Moritz Strohm committed
            ScheduleCourseDate::deleteBySQL(
                'user_id = :user_id AND course_id = :course_id',
                [
                    'user_id'   => $user->id,
                    'course_id' => $this->id
                ]
            );
Moritz Strohm's avatar
Moritz Strohm committed
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529

            //Log the event:
            StudipLog::log('SEM_USER_ADD', $this->id, $user->id, $permission_level, 'Wurde in die Veranstaltung eingetragen');

            if ($this->parent instanceof Course) {
                $this->parent->addMember($user, $permission_level, false);
            }

            if ($send_mail) {
                setTempLanguage($user->id);
                $body = '';
                $subject = '';
                if ($application_removed) {
                    //Enrolment after being on the wait list:
                    $subject = _('Zulassung zur Veranstaltung');
                    $body = sprintf(
                        _('Sie wurden für die Veranstaltung %s zugelassen. Ihr Eintrag auf der Warteliste wurde daher entfernt.'),
                        $this->getFullName()
                    );
                } elseif ($removed_from_course_set) {
                    //Enrolment after being in a course set:
                    $subject = _('Zulassung zur Veranstaltung');
                    $body = sprintf(
                        _('Sie wurden für die Veranstaltung %s endgültig zugelassen.'),
                        $this->getFullName()
                    );
                } else {
                    //Direct enrolment without waitlist or course set:
                    $subject = _('Eintragung in Veranstaltung');
                    $body = sprintf(
                        _('Sie wurden in die Veranstaltung %s eingetragen.'),
                        $this->getFullName()
                    );
                }
                $messaging = new messaging();
                $messaging->insert_message(
                    $body,
                    $user->username,
                    '____%system%____',
                    false,
                    false,
                    '1',
                    false,
                    $subject,
                    true
                );
                restoreLanguage();
            }
        } elseif ($ranks[$course_member->status] < $ranks[$permission_level]
            && $course_member->status !== 'dozent' || $number_of_lecturers > 1) {
            //The user is already a member of the course. They shall either be promoted
            //or they are not a lecturer or there is more than one lecturer in the course
            //(please read this multiple times in case you are unsure about these conditions).

            $course_member->status = $permission_level;
            $course_member->position = $new_member_position;

            $success = !$course_member->isDirty() || $course_member->store();

            if (!$success) {
                throw new \Studip\EnrolmentException(
                    _('Die Person kann nicht hochgestuft werden.'),
                    \Studip\EnrolmentException::PROMOTION_NOT_POSSIBLE
                );
            }
        } elseif ($course_member->status === 'dozent' && $number_of_lecturers <= 1) {
            throw new \Studip\EnrolmentException(
                sprintf(
                    _('Die Person kann nicht herabgestuft werden, da mindestens eine lehrende Person (%1$s) in die Veranstaltung eingetragen sein muss! Tragen Sie deshalb zuerst eine weitere Person als lehrende Person (%1$s) ein und versuchen Sie es dann erneut!'),
                    get_title_for_status('dozent', 1, $this->status)
                ),
                \Studip\EnrolmentException::DEMOTION_NOT_POSSIBLE
            );
        }
        $this->resetRelation('members');

        return $course_member;
    }

    /**
     * Removes a user from this course.
     *
     * @param User $user The user to be removed.
     * @param bool $send_mail Whether to send a mail after the membership deletion
     *     (true) or not (false). Defaults to false.
     *
     * @return void If this method does not throw, everything went fine.
     *
     * @throws \Studip\MembershipException If the user cannot be removed from the course.
     */
    public function deleteMember(User $user, bool $send_mail = false) : void
    {
        $membership = CourseMember::findOneBySQL(
            'seminar_id = :course_id AND user_id = :user_id',
            ['course_id' => $this->id, 'user_id' => $user->id]
        );
        if (!$membership) {
            //The user is not a member of the course.
            throw new \Studip\MembershipException(
                sprintf(
                    _('%1$s ist kein Mitglied der Veranstaltung %2$s.'),
                    $user->getFullName(),
                    $this->name
                ),
                \Studip\MembershipException::NOT_A_MEMBER,
                $user
            );
        }

        if ($membership->status === 'dozent') {
            //Check if there are enough lecturers left:
            $lecturer_amount = CourseMember::countByCourseAndStatus($this->id, 'dozent');
            if ($lecturer_amount < 2) {
                //Not enough lecturers left.
                throw new \Studip\MembershipException(
                    sprintf(
                        _('In die Veranstaltung muss mindestens eine lehrende Person (%s) eingetragen sein. Um diese Person aus der Veranstaltung zu entfernen, muss zunächst eine weitere lehrende Person eingetragen werden.'),
                        get_title_for_status('dozent', 1, $this->status)
                    ),
                    \Studip\MembershipException::USER_IS_SOLE_LECTURER,
                    $user
                );
            }
        }

        //At this point, the user may be removed.
        $success = $membership->delete();
        if (!$success) {
            throw new \Studip\MembershipException(
                sprintf(
                    _('Es trat ein Fehler auf beim Austragen von %1$s aus der Veranstaltung %2$s.'),
                    $user->getFullName(),
                    $this->getFullname()
                ),
                \Studip\MembershipException::REMOVAL_FAILED,
                $user
            );
        }

        $removed_from_parent   = false;
        $removed_from_children = false;

        if ($this->parent_course) {
            //This course has a parent course.
            //Delete the user from the parent course if they are not part of
            //one of the other child courses.
            $other_memberships = CourseMember::countBySql(
                'JOIN `seminare` USING (`seminar_id`)
                 WHERE `user_id` = :user_id
                   AND `parent_course` = :parent_course_id
                   AND `seminar_id` <> :this_course_id',
                [
                    'user_id'          => $user->id,
                    'parent_course_id' => $this->parent_course->id,
                    'this_course_id'   => $this->id
                ]
            );
            if ($other_memberships === 0) {
                //No other memberships. We can delete the user from the parent course.
                $this->parent_course->deleteMember($user, false);
                $removed_from_parent = true;
            }
        }

        if ($this->children) {
            //The other way around: This course has child courses and because the user
            //has been removed from this course, they shall also be removed from all
            //child courses.
            foreach ($this->children as $child) {
                $child->deleteMember($user);
            }
            $removed_from_children = true;
        }

        if ($send_mail) {
            $messaging = new messaging();
            setTempLanguage($user->id);
            $subject = sprintf(_('%s: Anmeldung aufgehoben'), $this->getFullName());
            $body = sprintf(_('Ihre Anmeldung für die Veranstaltung %s wurde aufgehoben.'), $this->getFullName());
            $messaging->insert_message(
                $body,
                $user->username,
                '____%system%____',
                false,
                false,
                '1',
                false,
                $subject,
                true
            );
            restoreLanguage();
        }

        if ($membership->status === 'dozent') {
            //Special treatment for lecturers:
            //Remove them from course dates and remove them as deputies.

            $db = DBManager::get();
            $stmt = $db->prepare(
                'DELETE FROM `termin_related_persons`
                 WHERE `user_id` = :user_id
                   AND `range_id` IN (
                     SELECT `termin_id` FROM `termine`
                     WHERE `range_id` = :course_id
                   )'
            );
            $stmt->execute(['course_id' => $this->id, 'user_id' => $user->id]);

            if (Deputy::isActivated()) {
                //For all courses where the user is a deputy, they can be removed as deputy
                //from the course, if the other lecturers are no deputies and the current user
                //is not a deputy:
                $all_user_deputy_duties = Deputy::findByRange_id($user->id);
                foreach ($all_user_deputy_duties as $deputy_duty) {
                    $other_deputy_amount = Deputy::countBySql(
                        "JOIN `seminar_user`
                           ON `seminar_user`.`user_id` = `deputies`.`range_id`
                         WHERE `seminar_user`.`user_id` <> :deleted_user_id
                           AND `seminar_user`.`status` = 'dozent'",
                        ['deleted_user_id' => $user->id]
                    );
                    if ($other_deputy_amount === 0 && $GLOBALS['user']->id != $deputy_duty->user_id) {
                        Deputy::deleteBySQL(
                            '`range_id` = :course_id AND `user_id` = :deputy_id',
                            ['course_id' => $this->id, $deputy_duty->user_id]
                        );
                    }
                }
            }
        }

        //Delete data field entries that are related to the user and the course:
        DatafieldEntryModel::deleteBySQL(
            '`range_id` = :user_id AND `sec_range_id` = :course_id',
            ['user_id' => $user->id, 'course_id' => $this->id]
        );

        //Remove the user from course groups:
        if ($this->statusgruppen) {
            foreach ($this->statusgruppen as $group) {
                $group->removeUser($user->id, true);
            }
        }

        StudipLog::log('SEM_USER_DEL', $this->id, $user->id, 'Wurde aus der Veranstaltung entfernt');

        $this->resetRelation('members');

        //At this point, removal is complete.
    }

    /**
     * Moves a regular course member back onto the waitlist.
     *
     * @param User $user The course member to be moved back to the waitlist.
     * @param bool $send_mail Whether to send a mail to inform the user of them
     *     being moved back to the waitlist (true) or not (false). Defaults to false.
     *
     * @return void
     *
     * @throws \Studip\Exception In case the former course member cannot be moved to the waitlist.
     *
     * @throws \Studip\MembershipException In case the membership cannot be terminated.
     */
    public function moveMemberToWaitlist(User $user, bool $send_mail = false): void
    {
        $this->deleteMember($user);
        $this->addMemberToWaitlist($user, PHP_INT_MAX, false);

        if ($send_mail) {
            setTempLanguage($user->id);
            $subject = studip_interpolate(
                _('%{course}: Anmeldung aufgehoben, auf Warteliste gesetzt'),
                ['course' => $this->getFullName()]
            );
            $message = studip_interpolate(
                _('Sie wurden aus der Veranstaltung %{course} abgemeldet und auf die zugehörige Warteliste gesetzt.'),
                ['course' => $this->getFullName()]
            );
            messaging::sendSystemMessage($user->id, $subject, $message);
            restoreLanguage();
        }
    }

    /**
     * Swaps the course member position with another member. This is done by specifying a course member
     * and the new position where they shall be placed in the course.
     *
     * @param CourseMember $membership The course member to move to another position.
     *
     * @return int The new position of the course member.
     *
     * @throws \Studip\MembershipException In case when moving the member position was unsuccessful.
     */
    public function swapMemberPosition(CourseMember $membership, int $new_position): int
    {
        //At this point, the user is not at the highest position.
        //Load the member with the position $position + 1 and swap the positions.

        $next_member = CourseMember::findOneBySQL(
            '`seminar_id` = :course_id AND `status` = :permission_level AND `position` = :new_position',
            [
                'course_id'        => $this->id,
                'permission_level' => $membership->status,
                'new_position'    => strval($new_position)
            ]
        );
        $success = false;
        if ($next_member) {
            $swapped_position = $next_member->position;
            $next_member->position = $membership->position;
            $membership->position = $swapped_position;

            $next_member->store();
            $success = !$membership->isDirty() || $membership->store();
        } else {
            //There is a gap in the position numbers. The user can just be placed to the new position:
            $membership->position = $new_position;
            $success = !$membership->isDirty() || $membership->store();
        }

        if (!$success) {
            //Something went wrong.
            throw new \Studip\MembershipException(
                sprintf(
                    _('%1$s konnte nicht an die Position %2$u verschoben werden.'),
                    $membership->user->getFullName(),
                    $new_position
                ),
                \Studip\MembershipException::MOVING_POSITION_FAILED,
                $membership->user
            );
        }
        return (int) $membership->position;
    }

    /**
     * Moves a course member one position up.
     *
     * @param User $user The user to move up.
     *
     * @return int The new position of the user.
     */
    public function moveMemberUp(User $user) : int
    {
        $membership = CourseMember::findOneBySQL(
            '`seminar_id` = :course_id AND `user_id` = :user_id',
            ['course_id' => $this->id, 'user_id' => $user->id]
        );
        if (!$membership) {
            //The user is not a member.
            return -1;
        }

        if ($membership->position == 0) {
            //The user is already at the highest position.
            return 0;
        }
        return $this->swapMemberPosition($membership, intval($membership->position - 1));
    }

    /**
     * Moves a course member one position down.
     *
     * @param User $user The user to move down.
     *
     * @return int The new position of the user.
     */
    public function moveMemberDown(User $user) : int
    {
        $membership = CourseMember::findOneBySQL(
            '`seminar_id` = :course_id AND `user_id` = :user_id',
            ['course_id' => $this->id, 'user_id' => $user->id]
        );
        if (!$membership) {
            //The user is not a member.
            return -1;
        }

        //Get the maximum number for the permission level in the course:
        $stmt = DBManager::get()->prepare(
            'SELECT MAX(`position`)
             FROM `seminar_user`
             WHERE `seminar_id` = :course_id
               AND `status` = :permission_level'
        );
        $stmt->execute([
            'course_id'        => $this->id,
            'permission_level' => $membership->status,
        ]);
        $max_number = $stmt->fetchColumn();
        if ($max_number === false) {
            //Nothing there to move.
            return -1;
        }

        if ($membership->position == $max_number) {
            //The user is already at the lowest position.
            return (int) $max_number;
        }

        return $this->swapMemberPosition($membership, intval($membership->position + 1));
    }

    public function getNumParticipants()
    {
        return $this->countMembersWithStatus('user autor') + $this->getNumPrelimParticipants();
    }

    public function getNumPrelimParticipants()
    {
        return AdmissionApplication::countBySql(
            "seminar_id = ? AND status = 'accepted'",
            [$this->id]
        );
    }

    public function getNumWaiting()
    {
        return AdmissionApplication::countBySql(
            "seminar_id = ? AND status = 'awaiting'",
            [$this->id]
        );
    }

    public function getParticipantStatus($user_id)
    {
        $p_status = $this->members->findBy('user_id', $user_id)->val('status');
        if (!$p_status) {
            $p_status = $this->admission_applicants->findBy('user_id', $user_id)->val('status');
        }
        return $p_status;
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * Determines the enrolment status of the user and their possibilities
     * to join the course.
     *
     * @param string $user_id The ID of the user for which to get enrolment information.
     *
     * @return \Studip\EnrolmentInformation The enrolment information
     *     for the specified user.
     */
    public function getEnrolmentInformation(string $user_id) : \Studip\EnrolmentInformation
    {
        //Check the course itself:

        if ($this->getSemClass()->isGroup()) {
            return new \Studip\EnrolmentInformation(
                _('Diese Veranstaltung ist die Hauptveranstaltung einer Veranstaltungsgruppe. Sie können sich nur in die zugehörigen Unterveranstaltungen eintragen.'),
                \Studip\Information::INFO,
                'main_course',
                false
            );
        }

        //Check the course set and if the user is on an admission list:

        if ($course_set = $this->getCourseSet()) {
            $info = new \Studip\EnrolmentInformation('');
            $info->setCodeword('course_set');
            $info->setEnrolmentAllowed(true);
            $message = _('Die Anmeldung zu dieser Veranstaltung folgt bestimmten Regeln.');
            $priority = AdmissionPriority::getPrioritiesByUser($course_set->getId(), $user_id);
            if (!empty($priority[$this->id])) {
                if ($course_set->hasAdmissionRule('LimitedAdmission')) {
                    $message .= ' ' . sprintf(
                            _('Sie stehen auf der Anmeldeliste für die automatische Platzverteilung der Veranstaltung mit der Priorität %u.'),
                            $priority[$this->id]
                        );
                } else {
                    $message .= ' ' . _('Sie stehen auf der Anmeldeliste für die automatische Platzverteilung der Veranstaltung.');
                }
            }
            $info->setMessage($message);
            return $info;
        }

        if ($this->lesezugriff == '0' && Config::get()->ENABLE_FREE_ACCESS && !$GLOBALS['perm']->get_studip_perm($this->id, $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Für diese Veranstaltung ist keine Anmeldung erforderlich.'),
                \Studip\Information::INFO,
                'free_access',
                true
            );
        }

        //Check the visibility of the course for the user:
        if (
            !$this->visible
            && !$this->isStudygroup()
            && !$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM, $user_id)
        ) {
            return new \Studip\EnrolmentInformation(
                _('Sie dürfen sich in diese Veranstaltung nicht eintragen.'),
                \Studip\Information::INFO,
                'invisible',
                false
            );
        }

        //Check the lock rule for participants:
        if (LockRules::Check($this->id, 'participants')) {
            return new \Studip\EnrolmentInformation(
                _('Sie dürfen sich in diese Veranstaltung nicht selbst eintragen.'),
                \Studip\Information::INFO,
                'locked',
                false
            );
        }

        //Check the permissions of the user:

        $user = User::find($user_id);

        if (!$user) {
            return new \Studip\EnrolmentInformation(
                _('Sie sind nicht in Stud.IP angemeldet und können sich daher nicht in die Veranstaltung eintragen.'),
                \Studip\Information::WARNING,
                'nobody',
                false
            );
        }
        if (!$GLOBALS['perm']->have_perm('user', $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Sie haben keine ausreichende Berechtigung, um sich in die Veranstaltung einzutragen.'),
                \Studip\Information::INFO,
                'user',
                false
            );
        }
        if ($GLOBALS['perm']->have_perm('root', $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Sie haben root-Rechte und dürfen damit alles in Stud.IP.'),
                \Studip\Information::INFO,
                'root',
                true
            );
        }
        if ($GLOBALS['perm']->have_studip_perm('admin', $this->id, $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Sie verwalten diese Veranstaltung.'),
                \Studip\Information::INFO,
                'course_admin',
                true
            );
        }
        if ($GLOBALS['perm']->have_perm('admin', $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Als administrierende Person dürfen Sie sich nicht in eine Veranstaltung eintragen.'),
                \Studip\Information::INFO,
                'admin',
                false
            );
        }

        //Check the course membership:

        if ($GLOBALS['perm']->have_studip_perm('user', $this->id, $user_id)) {
            return new \Studip\EnrolmentInformation(
                _('Sie sind bereits in der Veranstaltung eingetragen.'),
                \Studip\Information::INFO,
                'already_member',
                true
            );
        }

        //Check the admission status:

        $admission_status = $user->admission_applications->findBy('seminar_id', $this->id)->val('status');
        if ($admission_status === 'accepted') {
            return new \Studip\EnrolmentInformation(
                _('Sie wurden für diese Veranstaltung vorläufig akzeptiert.'),
                \Studip\Information::INFO,
                'preliminary_accepted',
                false
            );
        } elseif ($admission_status === 'awaiting') {
            return new \Studip\EnrolmentInformation(
                _('Sie sind auf der Warteliste für diese Veranstaltung.'),
                \Studip\Information::INFO,
                'on_waitlist',
                false
            );
        }

        //Check the user domain:
        $user_domains = UserDomain::getUserDomainsForUser($user_id);
        if (count($user_domains) > 0) {
            //The user is in at least one domain. Check if the course is in one of them.
            $course_domains = UserDomain::getUserDomainsForSeminar($this->id);
            if (
                !UserDomain::checkUserVisibility($course_domains, $user_domains)
                && !$this->isStudygroup()
            ) {
                //The user is not in the same domain as the course and the course
                //is not a studygroup.
                return new \Studip\EnrolmentInformation(
                    _('Sie sind nicht in der gleichen Domäne wie die Veranstaltung und können sich daher nicht für die Veranstaltung eintragen.'),
                    \Studip\Information::INFO,
                    'wrong_domain',
                    false
                );
            }
        }

        //In all other cases, enrolment is allowed.
        return new \Studip\EnrolmentInformation(
            _('Sie können sich zur Veranstaltung anmelden.'),
            \Studip\Information::INFO,
            'allowed',
            true
        );
    }


    /**
    * Returns the semType object that is defined for the course
    *
    * @return SemType The semTypeObject for the course
    */
    public function getSemType()
    {
        $semTypes = SemType::getTypes();
        if (isset($semTypes[$this->status])) {
            return $semTypes[$this->status];
        }

        Log::error(sprintf('SemType not found id:%s status:%s', $this->id, $this->status));
        return new SemType(['name' => 'Fehlerhafter Veranstaltungstyp']);
    }

    /**
     * Returns the SemClass object that is defined for the course
     *
     * @return SemClass The SemClassObject for the course
     */
     public function getSemClass()
     {
         return $this->getSemType()->getClass();
     }

    /**
     * Returns the full name of a course. If the important course numbers
     * (IMPORTANT_SEMNUMBER) is set in global configs it will also display
     * the coursenumber
     *
     * @param string formatting template name
     * @return string Fullname
     */
    public function getFullName($format = 'default')
        $template = [
            'name'                 => '%1$s',
            'name-semester'        => '%1$s (%4$s)',
            'number-name'          => '%3$s %1$s',
            'number-name-semester' => '%3$s %1$s (%4$s)',
            'number-type-name'     => '%3$s %2$s: %1$s',
            'sem-duration-name'    => '%4$s',
            'type-name'            => '%2$s: %1$s',
            'type-number-name'     => '%2$s: %3$s %1$s',
        ];

        if ($format === 'default' || !isset($template[$format])) {
           $format = Config::get()->IMPORTANT_SEMNUMBER ? 'type-number-name' : 'type-name';
        }
        $sem_type = $this->getSemType();
        $data[0] = $this->name;
        $data[1] = $sem_type['name'];
        $data[2] = $this->veranstaltungsnummer;
        $data[3] = $this->getTextualSemester();
        return trim(vsprintf($template[$format], array_map('trim', $data)));
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * Retrieves all dates (regular and irregular) that take place
     * in a specified semester or a semester range.
     *
     * @param Semester|null $start_semester The semester for which to get all dates
     *     or the start semester of a semester range.
     * @param Semester|null $end_semester The end semester for a semester range.
     *     This can also be null in case only dates for one semester
     *     shall be retrieved.
     *
     * @param bool $with_cancelled_dates Whether to include cancelled dates (true) or not (false).
     *     Defaults to false.
     *
     * @return CourseDateList A collection of irregular and regular course dates.
     *
     * @throws \Studip\Exception In case that the end semester is before the start semester.
     */
    public function getAllDatesInSemester(
        ?Semester $start_semester = null,
        ?Semester $end_semester = null,
        bool $with_cancelled_dates = false
    ) : CourseDateList {
        $all_dates_of_course = !$start_semester && !$end_semester;

        if ($all_dates_of_course) {
            $collection = new CourseDateList();
            foreach ($this->cycles as $regular_date) {
                $collection->addRegularDate($regular_date);
            }
            foreach ($this->dates as $date) {
                if (!$date->metadate_id) {
                    $collection->addSingleDate($date);
                }
            }
            if ($with_cancelled_dates) {
                foreach ($this->ex_dates as $cancelled_date) {
                    $collection->addCancelledDate($cancelled_date);
                }
            }
            return $collection;
        } else {
            if (!$start_semester) {
                return new CourseDateList();
            }
            $beginning = $start_semester->beginn;
            $end = $start_semester->ende;
            if ($end_semester) {
                if ($end_semester->ende < $start_semester->beginn) {
                    throw new \Studip\Exception(
                        _('Das Endsemester darf nicht vor dem Startsemester liegen.'),
                        \Studip\Exception::END_BEFORE_BEGINNING
                    );
                }
                $end = $end_semester->ende;
            }

            $collection = new CourseDateList();

            SeminarCycleDate::findEachBySQL(
                function ($date) use ($collection) {
                    $collection->addCycleDate($date);
                },
                "`start_time` >= :beginning AND `end_time` <= :end
                    AND `seminar_id` = :course_id",
                [
                    'course_id' => $this->id,
                    'beginning' => $beginning,
                    'end' => $end
                ]
            );

            CourseDate::findEachBySQL(
                function ($date) use ($collection) {
                    $collection->addSingleDate($date);
                },
                "`date` >= :beginning AND `end_time` <= :end
                    AND `range_id` = :course_id
                    AND (`metadate_id` IS NULL OR `metadate_id` = '')",
                [
                    'course_id' => $this->id,
                    'beginning' => $beginning,
                    'end' => $end
                ]
            );

            if ($with_cancelled_dates) {
                CourseExDate::findEachBySQL(
                    function ($date) use ($collection) {
                        $collection->addCancelledDate($date);
                    },
                    "`date` >= :beginning AND `end_time` <= :end
                        AND `range_id` = :course_id
                        AND (`metadate_id` IS NULL OR `metadate_id` = '')",
                    [
                        'course_id' => $this->id,
                        'beginning' => $beginning,
                        'end' => $end
                    ]
                );
            }

            return $collection;
        }
    }


    /**
     * Retrieves the course dates including cancelled dates ("ex-dates").
     * The dates can be filtered by an optional time range. By default,
     * all dates are retrieved.
     *
     * @param int $range_begin The begin timestamp of the time range.
     * @param int $range_end The end timestamp of the time range.
     *
     * @returns SimpleCollection A collection of all retrieved dates and
     *     cancelled dates.
     */
    public function getDatesWithExdates($range_begin = 0, $range_end = 0)
    {
        $dates = [];
        if (($range_begin > 0) && ($range_end > 0) && ($range_end > $range_begin)) {
            $ex_dates = $this->ex_dates->findBy('content', '', '<>')
                          ->findBy('date', $range_begin, '>=')
                          ->findBy('end_time', $range_end, '<=');
            $dates = $this->dates->findBy('date', $range_begin, '>=')
                          ->findBy('end_time', $range_end, '<=');
            $dates->merge($ex_dates);
        } else {
            $dates = $this->ex_dates->findBy('content', '', '<>');
            $dates->merge($this->dates);
        }
        $dates->uasort(function($a, $b) {
            return $a->date - $b->date
                ?: strnatcasecmp($a->getRoomName(), $b->getRoomName());
        });
        return $dates;
    }

Moritz Strohm's avatar
Moritz Strohm committed
    /**
     * Retrieves the first date of the course that takes place.
     *
     * @return CourseDate|null Either the first date as CourseDate or null in case
     *     the course has no dates.
     */
    public function getFirstDate() : ?CourseDate
    {
        return $this->dates->first();
    }

    /**
     * Retrieves the next date for the course. If requested, the next cancelled
     * date is retrieved if no date can be found that takes place.
     *
     * The date must start in the future or within the past hour to be regarded
     * as next date.
     *
     * @param bool $include_cancelled Include cancelled dates (true) or not.
     *     Defaults to false.
     *
     * @return CourseDate|CourseExDate|null A CourseDate or CourseExDate representing
     *     the next date or null in case there is no next date. CourseExDate instances
     *     are only returned if $include_cancelled is set to true.
     */
    public function getNextDate(bool $include_cancelled = false)
    {
        $sql = '`range_id` = :course_id AND `date` > UNIX_TIMESTAMP() - 3600
                ORDER BY `date`, `end_time`';

        $date = CourseDate::findOneBySQL($sql, ['course_id' => $this->id]);
        if (!$date && $include_cancelled) {
            //Do the same with CourseExDate:
            $date = CourseExDate::findOneBySQL($sql, ['course_id' => $this->id]);
        }
        return $date;
    }

    /**
     * Sets this courses study areas to the given values.
     *
     * @param array $ids the new study areas
     * @return bool Changes successfully saved?
     */
    public function setStudyAreas($ids)
    {
        $old = $this->study_areas->pluck('sem_tree_id');
        $added = array_diff($ids, $old);
        $removed = array_diff($old, $ids);
        $success = false;
        if ($added || $removed) {

            $this->study_areas = SimpleCollection::createFromArray(StudipStudyArea::findMany($ids));

            if ($this->store()) {
                NotificationCenter::postNotification('CourseDidChangeStudyArea', $this);
                $success = true;

                foreach ($added as $one) {
                    StudipLog::log('SEM_ADD_STUDYAREA', $this->id, $one);

                    $area = $this->study_areas->find($one);
                    if ($area->isModule()) {
                        NotificationCenter::postNotification(
                            'CourseAddedToModule',
                            $area,
                            ['module_id' => $one, 'course_id' => $this->id]
                        );
                    }
                }

                foreach ($removed as $one) {
                    StudipLog::log('SEM_DELETE_STUDYAREA', $this->id, $one);

                    $area = StudipStudyArea::find($one);
                    if ($area->isModule()) {
                        NotificationCenter::postNotification(
                            'CourseRemovedFromModule',
                            $area,
                            ['module_id' => $one, 'course_id' => $this->id]
                        );
                    }
                }
            }
        }

        return $success;
    }

    /**
     * Is the current course visible for the current user?
     * @param string $user_id
     * @return bool Visible?
     */
    public function isVisibleForUser($user_id = null)
    {
        return $this->visible
            || $GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM, $user_id)
            || $GLOBALS['perm']->have_studip_perm('user', $this->id, $user_id);
    }

    /**
     * Returns a descriptive text for the range type.
     *
     * @return string
     */
    public function describeRange()
    {
        return _('Veranstaltung');
    }

    /**
     * Returns a unique identificator for the range type.
     *
     * @return string
     */
    public function getRangeType()
    {
        return 'course';
    }

    /**
     * Returns the id of the current range
     *
     * @return string
     */
    public function getRangeId()
    {
        return $this->id;
    }

    /**
     * {@inheritdoc}
     */
    public function getConfiguration()
    {
        return CourseConfig::get($this);
    }

    /**
     * Decides whether the user may access the range.
     *
     * @param string|null $user_id Optional id of a user, defaults to current user
     * @return bool
     * @todo Check permissions
     */
    public function isAccessibleToUser($user_id = null)
    {
        if ($user_id === null) {
            $user_id = $GLOBALS['user']->id;
        }
        return $GLOBALS['perm']->have_studip_perm('user', $this->id, $user_id);
    }

    /**
     * Decides whether the user may edit/alter the range.
     *
     * @param string|null $user_id Optional id of a user, defaults to current user
     * @return bool
     * @todo Check permissions
     */
    public function isEditableByUser($user_id = null)
    {
        if ($user_id === null) {
            $user_id = $GLOBALS['user']->id;
        }
        return $GLOBALS['perm']->have_studip_perm('tutor', $this->id, $user_id);
    }

    /**
     * Returns the appropriate icon for the completion status.
     *
     * Mapping (completion -> icon role):
     * - 0 => status-red
     * - 1 => status-yellow
     * - 2 => status-green
     *
     * @return Icon class
     */
    public function getCompletionIcon()
    {
        $role = Icon::ROLE_STATUS_RED;
        if ($this->completion == 1) {
            $role = Icon::ROLE_STATUS_YELLOW;
        } elseif ($this->completion == 2) {
            $role = Icon::ROLE_STATUS_GREEN;
        }
        return Icon::create('radiobutton-checked', $role);
    }

    /**
     * Returns the appropriate label for the completion status.
     *
     * @return string
     */
    public function getCompetionLabel(): string
    {
        return [
            0 => _('unvollständig'),
            1 => _('in Bearbeitung'),
            2 => _('fertig'),
        ][$this->completion] ?? _('undefiniert');
    }

    /**
     * Generates a general log entry if the course were changed.
     * Furthermore, this method emits notifications when the
     * start and/or the end semester has/have changed.
     */
    protected function logStore()
    {
Moritz Strohm's avatar
Moritz Strohm committed
        if (!$this->isNew()) {
            if ($this->initial_start_semester?->id !== $this->start_semester?->id) {
                //Log change of start semester:
                StudipLog::log('SEM_SET_STARTSEMESTER', $this->id, isset($this->start_semester) ? $this->start_semester->name : _('unbegrenzt'));
                NotificationCenter::postNotification('CourseDidChangeSchedule', $this);
            }
            if ($this->initial_end_semester?->id !== $this->end_semester?->id) {
                StudipLog::log('SEM_SET_ENDSEMESTER', $this->id, $this->getTextualSemester());
                NotificationCenter::postNotification('CourseDidChangeSchedule', $this);
            }
Moritz Strohm's avatar
Moritz Strohm committed
            $log = [];
            if ($this->isFieldDirty('admission_prelim')) {
                $log[] = $this->admission_prelim ? _('Neuer Anmeldemodus: Vorläufiger Eintrag') : _('Neuer Anmeldemodus: Direkter Eintrag');
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_binding')) {
                $log[] = $this->admission_binding ? _('Anmeldung verbindlich') : _('Anmeldung unverbindlich');
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_turnout')) {
                $log[] = sprintf(_('Neue Teilnehmerzahl: %s'), (int)$this->admission_turnout);
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_disable_waitlist')) {
                $log[] = $this->admission_disable_waitlist ? _('Warteliste aktiviert') : _('Warteliste deaktiviert');
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_waitlist_max')) {
                $log[] = sprintf(_('Plätze auf der Warteliste geändert: %u'), (int)$this->admission_waitlist_max);
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_disable_waitlist_move')) {
                $log[] = $this->admission_disable_waitlist ? _('Nachrücken aktiviert') : _('Nachrücken deaktiviert');
Moritz Strohm's avatar
Moritz Strohm committed
            if ($this->isFieldDirty('admission_prelim_txt')) {
                if ($this->admission_prelim_txt) {
                    $log[] = sprintf(_('Neuer Hinweistext bei vorläufigen Eintragungen: %s'), strip_tags(kill_format($this->admission_prelim_txt)));
                } else {
                    $log[] = _('Hinweistext bei vorläufigen Eintragungen wurde entfert');
                }
            }
Moritz Strohm's avatar
Moritz Strohm committed
            if (!empty($log)) {
                StudipLog::log(
                    'SEM_CHANGED_ACCESS',
                    $this->id,
                    null,
                    '',
                    implode(' - ', $log)
Loading
Loading full blame...