Skip to content
Snippets Groups Projects
Course.php 92.4 KiB
Newer Older
Moritz Strohm's avatar
Moritz Strohm committed
            $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
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 1530 1531 1532

            //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) {