Skip to content
Snippets Groups Projects
Course.php 91.7 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 $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']['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'));
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