aboutsummaryrefslogtreecommitdiff
path: root/lib/models/Course.php
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2024-09-02 07:50:49 +0000
committerMoritz Strohm <strohm@data-quest.de>2024-09-02 07:50:49 +0000
commitf00164f6f8b823872d0934830a466aeb2af7114b (patch)
treee810d56f7ad1ec8f1e1dd17affd0954f5a54aaf0 /lib/models/Course.php
parentafadde64a6a2017eabb36a3bdef412bb2d2692ba (diff)
StEP 3209, re #3209
Merge request studip/studip!2179
Diffstat (limited to 'lib/models/Course.php')
-rw-r--r--lib/models/Course.php1358
1 files changed, 1348 insertions, 10 deletions
diff --git a/lib/models/Course.php b/lib/models/Course.php
index d8bb2f5..ea6676c 100644
--- a/lib/models/Course.php
+++ b/lib/models/Course.php
@@ -144,6 +144,24 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
'on_delete' => 'delete',
'on_store' => 'store',
];
+ $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',
@@ -182,10 +200,12 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
'on_store' => 'store',
];
$config['has_and_belongs_to_many']['institutes'] = [
- 'class_name' => Institute::class,
- 'thru_table' => 'seminar_inst',
- 'on_delete' => 'delete',
- 'on_store' => 'store',
+ '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'] = [
@@ -201,6 +221,11 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
'assoc_foreign_key' => 'course_id',
'on_delete' => 'delete',
];
+ $config['has_many']['resource_bookings'] = [
+ 'class_name' => ResourceBooking::class,
+ 'assoc_foreign_key' => 'range_id',
+ 'on_delete' => 'delete'
+ ];
$config['belongs_to']['parent'] = [
'class_name' => Course::class,
'foreign_key' => 'parent_course'
@@ -221,6 +246,13 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
'on_delete' => 'delete',
];
+ $config['has_many']['config_values'] = [
+ 'class_name' => ConfigValue::class,
+ 'assoc_foreign_key' => 'range_id',
+ 'on_store' => 'store',
+ 'on_delete' => 'delete'
+ ];
+
$config['has_many']['courseware_units'] = [
'class_name' => \Courseware\Unit::class,
'assoc_foreign_key' => 'range_id',
@@ -278,10 +310,70 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
"UPDATE `seminare` SET `parent_course` = NULL WHERE `parent_course` = :course",
['course' => $course->id]
);
- DBManager::get()->execute(
- "DELETE FROM `forum_visits` WHERE `seminar_id` = ?",
- [$course->id]
- );
+
+ //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` = ?';
+ $statement = DBManager::get()->execute($query, [$course->id]);
+ $query = 'DELETE FROM `wiki_locks` WHERE `range_id` = ?';
+ $statement = DBManager::get()->execute($query, [$course->id]);
+ WikiPageConfig::deleteByRange_id($course->id);
+
+ //Remove all entries of the course in calendars:
+ $query = 'DELETE FROM `schedule_seminare` WHERE `seminar_id` = ?';
+ $statement = DBManager::get()->execute($query, [$course->id]);
+
+ //Remove connections to other e-learning systems:
+ if (Config::get()->ELEARNING_INTERFACE_ENABLE) {
+ $cms_types = ObjectConnections::GetConnectedSystems($course->id);
+ foreach ($cms_types as $system) {
+ if (empty($GLOBALS['connected_cms'][$system])) {
+ continue;
+ }
+ ELearningUtils::loadClass($system);
+ $del_cms += $GLOBALS['connected_cms'][$system]->deleteConnectedModules($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);
@@ -487,7 +579,7 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
});
}
- public function getFreeSeats()
+ public function getFreeSeats() : int
{
$free_seats = $this->admission_turnout - $this->getNumParticipants();
return max($free_seats, 0);
@@ -507,6 +599,327 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
}
/**
+ * 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
@@ -533,6 +946,579 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
return CourseMember::countByCourseAndStatus($this->id, $status);
}
+ /**
+ * 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:
+ CalendarScheduleModel::deleteSeminarEntries($user->id, $this->id);
+
+ //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();
@@ -564,6 +1550,188 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
}
/**
+ * 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
@@ -621,6 +1789,111 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
return trim(vsprintf($template[$format], array_map('trim', $data)));
}
+ /**
+ * 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").
@@ -656,6 +1929,44 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
}
/**
+ * 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
@@ -1073,6 +2384,34 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
return $plugin && $this->tools->findOneby('plugin_id', $plugin->getPluginId());
}
+
+ /**
+ * Returns the Plugin/Tool specified by its name in case it is
+ * activated in this course.
+ *
+ * @param string $name The name of the tool.
+ *
+ * @return StandardPlugin An instance for the tool.
+ *
+ * @throws \Studip\ToolException In case the tool is not activated.
+ */
+ public function getTool(string $name) : StandardPlugin
+ {
+ if ($this->isToolActive($name)) {
+ $plugin = PluginEngine::getPlugin($name);
+ if ($plugin instanceof StandardPlugin) {
+ return $plugin;
+ }
+ }
+ throw new \Studip\ToolException(
+ sprintf(
+ _('Das Werkzeug %s ist nicht aktiviert.'),
+ $name
+ ),
+ \Studip\ToolException::TOOL_NOT_ACTIVATED
+ );
+ }
+
/**
* returns all activated plugins/modules for this course
* @return StudipModule[]
@@ -1177,5 +2516,4 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe
return $result;
}
-
}