aboutsummaryrefslogtreecommitdiff
path: root/lib/models/resources/ResourceBooking.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/models/resources/ResourceBooking.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/models/resources/ResourceBooking.php')
-rw-r--r--lib/models/resources/ResourceBooking.php1944
1 files changed, 1944 insertions, 0 deletions
diff --git a/lib/models/resources/ResourceBooking.php b/lib/models/resources/ResourceBooking.php
new file mode 100644
index 0000000..977cf32
--- /dev/null
+++ b/lib/models/resources/ResourceBooking.php
@@ -0,0 +1,1944 @@
+<?php
+
+/**
+ * ResourceBooking.php - model class for resource bookings
+ *
+ * The ResourceBooking class is responsible for storing
+ * bookings of resources in a specified time range
+ * or a time interval.
+ *
+ * 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 Moritz Strohm <strohm@data-quest.de>
+ * @copyright 2017-2019
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @package resources
+ * @since 4.5
+ *
+ * The repetition_interval column contains a date interval string in a
+ * format that is accepted by the DateInterval class constructor.
+ * Examples for values of the repetition_interval column:
+ * - For an one month interval, the value is "P1M".
+ * - For an interval of two days, the value is "P2D".
+ * See the DateInterval documentation for more examples:
+ * https://secure.php.net/manual/en/class.dateinterval.php
+ *
+ * @property string $id database column
+ * @property string $resource_id database column
+ * @property string $range_id database column
+ * @property string|null $description database column
+ * @property int $begin database column
+ * @property int $end database column
+ * @property int|null $repeat_end database column
+ * @property int $mkdate database column
+ * @property int $chdate database column
+ * @property string|null $internal_comment database column
+ * @property int $preparation_time database column
+ * @property int $booking_type database column
+ * @property string $booking_user_id database column
+ * @property string $repetition_interval database column
+ * @property SimpleORMapCollection|ResourceBookingInterval[] $time_intervals has_many ResourceBookingInterval
+ * @property Resource $resource belongs_to Resource
+ * @property User $assigned_user belongs_to User
+ * @property CourseDate $assigned_course_date belongs_to CourseDate
+ * @property User $booking_user belongs_to User
+ * @property mixed $course_id additional field
+ * @property mixed $room_name additional field
+ * @property-read mixed $real_begin additional field
+ * @property-read mixed $real_begin_dt additional field
+ */
+class ResourceBooking extends SimpleORMap implements PrivacyObject, Studip\Calendar\EventSource
+{
+ const TYPE_NORMAL = 0;
+ const TYPE_RESERVATION = 1;
+ const TYPE_LOCK = 2;
+ const TYPE_PLANNED = 3;
+
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'resource_bookings';
+
+ $config['belongs_to']['resource'] = [
+ 'class_name' => Resource::class,
+ 'foreign_key' => 'resource_id',
+ 'assoc_func' => 'find'
+ ];
+ $config['belongs_to']['assigned_user'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'range_id',
+ 'assoc_func' => 'find'
+ ];
+ $config['belongs_to']['assigned_course_date'] = [
+ 'class_name' => CourseDate::class,
+ 'foreign_key' => 'range_id',
+ 'assoc_func' => 'find'
+ ];
+ $config['has_many']['time_intervals'] = [
+ 'class_name' => ResourceBookingInterval::class,
+ 'assoc_foreign_key' => 'booking_id',
+ 'on_delete' => 'delete'
+ ];
+ $config['belongs_to']['booking_user'] = [
+ 'class_name' => User::class,
+ 'foreign_key' => 'booking_user_id',
+ 'assoc_func' => 'find'
+ ];
+
+ $config['additional_fields']['course_id'] = ['assigned_course_date', 'range_id'];
+ $config['additional_fields']['room_name'] = ['resource', 'name'];
+
+ $config['additional_fields']['real_begin'] = [
+ 'get' => function (ResourceBooking $booking) {
+ return $booking->begin - $booking->preparation_time;
+ }
+ ];
+ $config['additional_fields']['real_begin_dt'] = [
+ 'get' => function (ResourceBooking $booking) {
+ $real_begin = new DateTime();
+ $real_begin->setTimestamp($booking->real_begin);
+ return $real_begin;
+ }
+ ];
+
+ $config['registered_callbacks']['after_store'][] = 'updateIntervals';
+ $config['registered_callbacks']['after_store'][] = 'createStoreLogEntry';
+ $config['registered_callbacks']['after_delete'][] = 'sendDeleteNotification';
+ $config['registered_callbacks']['after_delete'][] = 'createDeleteLogEntry';
+
+ //In regard to TIC 6460:
+ //As long as TIC 6460 is not implemented, we must add the validate
+ //method as a callback before storing the object.
+ if (!method_exists('SimpleORMap', 'validate')) {
+ $config['registered_callbacks']['before_store'][] = 'validate';
+ }
+
+ parent::configure($config);
+ }
+
+ private $assigned_user_type;
+
+ public function createStoreLogEntry()
+ {
+ if ($this->isSimpleBooking()) {
+ StudipLog::log(
+ 'RES_ASSIGN_SINGLE',
+ $this->resource_id,
+ null,
+ $this->__toString(),
+ null,
+ $GLOBALS['user']->id
+ );
+ } else {
+ StudipLog::log(
+ 'RES_ASSIGN_SEM',
+ $this->resource_id,
+ $this->course_id,
+ $this->__toString(),
+ null,
+ $GLOBALS['user']->id
+ );
+ }
+ }
+
+
+ public function createDeleteLogEntry()
+ {
+ if ($this->isSimpleBooking()) {
+ StudipLog::log(
+ 'RES_ASSIGN_DEL_SINGLE',
+ $this->resource_id,
+ null,
+ $this->__toString(),
+ null,
+ $GLOBALS['user']->id
+ );
+ } else {
+ StudipLog::log(
+ 'RES_ASSIGN_DEL_SEM',
+ $this->resource_id,
+ $this->course_id,
+ $this->__toString(),
+ null,
+ $GLOBALS['user']->id
+ );
+ }
+ }
+
+
+ /**
+ * Internal method that generated the SQL query used in
+ * findByResourceAndTimeRanges and countByResourceAndTimeRanges.
+ *
+ * @see findByResourceAndTimeRanges
+ * @inheritDoc
+ */
+ protected static function buildResourceAndTimeRangesSqlQuery(
+ Resource $resource,
+ $time_ranges = [],
+ $booking_types = [],
+ $excluded_booking_ids = []
+ )
+ {
+ if (!is_array($time_ranges)) {
+ throw new InvalidArgumentException(
+ _('Es wurde keine Liste mit Zeiträumen angegeben!')
+ );
+ }
+
+ //Check the array:
+ foreach ($time_ranges as $time_range) {
+ if ($time_range['begin'] > $time_range['end']) {
+ throw new InvalidArgumentException(
+ _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
+ );
+ }
+
+ if ($time_range['begin'] == $time_range['end']) {
+ throw new InvalidArgumentException(
+ _('Startzeitpunkt und Endzeitpunkt dürfen nicht identisch sein!')
+ );
+ }
+ }
+
+ $sql_params = [
+ 'resource_id' => $resource->id
+ ];
+
+ //First we build the SQL snippet for the case that the $booking_type
+ //variable is set to something different than null.
+ $booking_type_sql = '';
+ if (is_array($booking_types) && count($booking_types)) {
+ $booking_type_sql = ' AND booking_type IN ( :booking_types )';
+ $sql_params['booking_types'] = $booking_types;
+ }
+
+ //Then we build the snippet for excluded booking IDs, if specified.
+ $excluded_booking_ids_sql = '';
+ if (is_array($excluded_booking_ids) && count($excluded_booking_ids)) {
+ $excluded_booking_ids_sql = ' AND resource_booking_intervals.booking_id NOT IN ( :excluded_ids ) ';
+ $sql_params['excluded_ids'] = $excluded_booking_ids;
+ }
+
+ //Now we build the SQL snippet for the time intervals.
+ //These are repeated four times in the query below.
+ //First we use one template ($time_sql_template) and
+ //replace NUMBER with our counting variable $i.
+ //BEGIN and END are replaced below since the columns for
+ //BEGIN and END are different in the four cases where we
+ //repeat the SQL snippet for the time intervals.
+ $time_sql_template = '
+ (resource_booking_intervals.begin < :endNUMBER AND :beginNUMBER < resource_booking_intervals.end)
+ ';
+
+ $time_sql = '';
+ if ($time_ranges) {
+ $time_sql = ' AND (';
+
+ $i = 1;
+ foreach ($time_ranges as $time_range) {
+ if ($i > 1) {
+ $time_sql .= ' OR ';
+ }
+ $time_sql .= str_replace(
+ 'NUMBER',
+ $i,
+ $time_sql_template
+ );
+
+ if ($time_range['begin'] instanceof DateTime) {
+ $sql_params[('begin' . $i)] = $time_range['begin']->getTimestamp();
+ } else {
+ $sql_params[('begin' . $i)] = $time_range['begin'];
+ }
+ if ($time_range['end'] instanceof DateTime) {
+ $sql_params[('end' . $i)] = $time_range['end']->getTimestamp();
+ } else {
+ $sql_params[('end' . $i)] = $time_range['end'];
+ }
+
+ $i++;
+ }
+
+ $time_sql .= ') ';
+ }
+
+ //Check if the booking has a start and end timestamp set
+ //or if it has repetitions that have a matching timestamp.
+ //This is done in the rest of the SQL query:
+
+ $whole_sql = "resource_bookings.id IN (
+ SELECT resource_booking_intervals.booking_id FROM resource_booking_intervals WHERE
+ resource_booking_intervals.resource_id = :resource_id
+ AND resource_booking_intervals.takes_place = 1"
+ . $excluded_booking_ids_sql
+ . $time_sql
+ . " GROUP BY resource_booking_intervals.booking_id ORDER BY NULL)
+ $booking_type_sql
+";
+
+ return [
+ 'sql' => $whole_sql,
+ 'params' => $sql_params
+ ];
+ }
+
+
+ /**
+ * Retrieves all resource booking for the given resource and
+ * time range. By default, all booking are returned.
+ * To get only bookings of a certain type
+ * set the $booking_type parameter.
+ *
+ * @param Resource $resource The resource whose requests shall be retrieved.
+ * @param array $time_ranges An array with time ranges as DateTime objects.
+ * The array has the following structure:
+ * [
+ * [
+ * 'begin' => begin timestamp,
+ * 'end' => end timestamp
+ * ],
+ * ...
+ * ]
+ * @param array $booking_types An optional specification for the
+ * booking_type column in the database. More than one booking
+ * type can be specified.
+ * By default this is set to an empty array which means
+ * that resource booking are not filtered by the type column.
+ * The allowed resource booking types are specified in the
+ * class documentation.
+ *
+ * @param array $excluded_booking_ids An array of strings representing
+ * resource booking IDs. IDs specified in this array are excluded
+ * from the search.
+ * @return ResourceBooking[] An array of ResourceRequest objects.
+ * If no requests can be found, the array is empty.
+ *
+ * @throws InvalidArgumentException, if the time ranges are either not in an
+ * array matching the format description from above or if one of the
+ * following conditions is met in one of the time ranges:
+ * - begin > end
+ * - begin == end
+ */
+ public static function findByResourceAndTimeRanges(
+ Resource $resource,
+ $time_ranges = [],
+ $booking_types = [],
+ $excluded_booking_ids = []
+ )
+ {
+ //Build the SQL query and the parameter array.
+
+ $sql_data = self::buildResourceAndTimeRangesSqlQuery(
+ $resource,
+ $time_ranges,
+ $booking_types,
+ $excluded_booking_ids
+ );
+
+ //Call findBySql:
+ return self::findBySql($sql_data['sql'], $sql_data['params']);
+ }
+
+
+ /**
+ * Counts all resource bookings for the specified resource and
+ * time range. By default, all bookings are counted.
+ * To count only bookings of a certain type
+ * set the $booking_type parameter.
+ *
+ * @see findByResourceAndTimeRanges
+ * @inheritDoc
+ */
+ public static function countByResourceAndTimeRanges(
+ Resource $resource,
+ $time_ranges = [],
+ $booking_types = [],
+ $excluded_booking_ids = []
+ )
+ {
+ //Build the SQL query and the parameter array.
+
+ $sql_data = self::buildResourceAndTimeRangesSqlQuery(
+ $resource,
+ $time_ranges,
+ $booking_types,
+ $excluded_booking_ids
+ );
+
+ //Call countBySql:
+ return self::countBySql($sql_data['sql'], $sql_data['params']);
+ }
+
+
+ /**
+ * Deletes all resource bookings for the specified resource and
+ * time range. By default, all bookings are counted.
+ * To count only bookings of a certain type
+ * set the $booking_type parameter.
+ *
+ * @see findByResourceAndTimeRanges
+ * @inheritDoc
+ */
+ public static function deleteByResourceAndTimeRanges(
+ Resource $resource,
+ $time_ranges = [],
+ $booking_types = [],
+ $excluded_booking_ids = []
+ )
+ {
+ //Build the SQL query and the parameter array.
+ $sql_data = self::buildResourceAndTimeRangesSqlQuery(
+ $resource,
+ $time_ranges,
+ $booking_types,
+ $excluded_booking_ids
+ );
+
+ //Call deleteBySql:
+ return self::deleteBySql($sql_data['sql'], $sql_data['params']);
+ }
+
+
+ /**
+ * The SimpleORMap::store method is overloaded to allow forced booking
+ * of resource bookings, meaning that all other bookings of the
+ * resource of a booking are deleted when they overlap with this booking.
+ *
+ * @param bool $force_booking Whether booking shall be forced (true)
+ * or not (false). Defaults to false.
+ * @return bool
+ */
+ public function store($force_booking = false)
+ {
+ if ($force_booking == true) {
+ $this->deleteOverlappingBookings();
+ }
+ $this->deleteOverlappingReservations();
+
+ if (parent::store()) {
+ //Check if the booking is bound to a course date.
+ //If this is the case, check for existing bookings
+ //and delete them, so that there is only one booking
+ //for a course date:
+ $course_date_exists = CourseDate::exists($this->range_id);
+ if ($course_date_exists) {
+ self::deleteBySql(
+ 'range_id = :range_id AND id <> :this_id',
+ [
+ 'this_id' => $this->id,
+ 'range_id' => $this->range_id
+ ]
+ );
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * This validation method is called before storing an object.
+ */
+ public function validate()
+ {
+ if ((!$this->resource_id) || !($this->resource instanceof Resource)) {
+ throw new InvalidArgumentException(
+ _('Es wurde keine Ressource zur Buchung angegeben!')
+ );
+ }
+
+ if (!$this->range_id && !$this->description) {
+ throw new ResourceBookingRangeException(
+ _('Es muss eine Person oder ein Text zur Buchung eingegeben werden!')
+ );
+ }
+
+ if ($this->begin >= $this->end) {
+ throw new InvalidArgumentException(
+ _('Der Startzeitpunkt darf nicht hinter dem Endzeitpunkt liegen!')
+ );
+ }
+
+ if ($this->repetition_interval) {
+ if (!$this->repeat_end) {
+ throw new InvalidArgumentException(
+ _('Es wurde ein Wiederholungsintervall ohne Begrenzung angegeben!')
+ );
+ }
+ if ($this->real_begin > $this->repeat_end) {
+ throw new InvalidArgumentException(
+ _('Der Startzeitpunkt darf nicht hinter dem Ende der Wiederholungen liegen!')
+ );
+ }
+ }
+
+ // update the booking user
+ if (!$this->isNew() || !$this->booking_user_id) {
+ $this->booking_user = User::findCurrent();
+ }
+
+ $derived_resource = $this->resource->getDerivedClassInstance();
+
+ // check if the user needs booking rights on the resource
+ if (
+ $this->isFieldDirty('resource_id')
+ || $this->isFieldDirty('repetition_interval')
+ || $this->begin < $this->getPristineValue('begin')
+ || $this->end > $this->getPristineValue('end')
+ || $this->preparation_time > $this->getPristineValue('preparation_time')
+ || $this->repeat_end > $this->getPristineValue('repeat_end')
+ ) {
+
+ //Check if the user has booking rights on the resource.
+ //The user must have either permanent permissions or they have to
+ //have booking rights by a temporary permission in this moment
+ $user_has_booking_rights = $derived_resource->userHasBookingRights(
+ $this->booking_user, $this->begin, $this->end
+ );
+ if (!$user_has_booking_rights) {
+ throw new ResourcePermissionException(
+ sprintf(
+ _('Unzureichende Berechtigungen zum Buchen der Ressource %s!'),
+ $this->resource->name
+ )
+ );
+ }
+ }
+
+ $time_intervals = $this->calculateTimeIntervals(true);
+ $time_interval_overlaps = [];
+ $existing_deleted_intervals = [];
+ if (!$this->isNew()) {
+ $existing_deleted_intervals = array_filter(
+ $this->getTimeIntervals(),
+ function ($i): bool {
+ return !$i->takes_place;
+ }
+ );
+ }
+ foreach ($time_intervals as $time_interval) {
+ foreach ($existing_deleted_intervals as $deleted_interval) {
+ if (
+ $time_interval['begin']->getTimestamp() == $deleted_interval['begin']
+ && $time_interval['end']->getTimestamp() == $deleted_interval['end']
+ ) {
+ continue 2;
+ }
+ }
+ $is_locked = $derived_resource->isLocked(
+ $time_interval['begin'],
+ $time_interval['end'],
+ ($this->isNew() ? [] : [$this->id])
+ );
+ if ($is_locked) {
+ if ($time_interval['begin']->format('Ymd') == $time_interval['end']->format('Ymd')) {
+ $time_interval_overlaps[] = sprintf(
+ _('Gesperrt im Bereich vom %1$s bis %2$s'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('H:i')
+ );
+ } else {
+ $time_interval_overlaps[] = sprintf(
+ _('Gesperrt im Bereich vom %1$s bis zum %2$s'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('d.m.Y H:i')
+ );
+ }
+ } else {
+ $is_assigned = $derived_resource->isAssigned(
+ $time_interval['begin'],
+ $time_interval['end'],
+ ($this->isNew() ? [] : [$this->id])
+ );
+ if ($is_assigned) {
+ //Find the other booking:
+ $other_booking = self::findByResourceAndTimeRanges(
+ $derived_resource,
+ [$time_interval],
+ [self::TYPE_NORMAL, self::TYPE_LOCK],
+ [$this->id]
+ );
+ $course = null;
+ if (
+ count($other_booking) >= 1
+ && !empty($other_booking[0]->assigned_course_date->course)
+ ) {
+ $course = $other_booking[0]->assigned_course_date->course;
+ }
+ if ($time_interval['begin']->format('Ymd') == $time_interval['end']->format('Ymd')) {
+ if ($course) {
+ $time_interval_overlaps[] = sprintf(
+ _('Gebucht im Bereich vom %1$s bis %2$s durch die Veranstaltung %3$s.'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('H:i'),
+ $course->getFullName()
+ );
+ } else {
+ $time_interval_overlaps[] = sprintf(
+ _('Gebucht im Bereich vom %1$s bis %2$s'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('H:i')
+ );
+ }
+ } else {
+ if ($course) {
+ $time_interval_overlaps[] = sprintf(
+ _('Gebucht im Bereich vom %1$s bis zum %2$s durch die Veranstaltung %3$s.'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('d.m.Y H:i'),
+ $course->getFullName()
+ );
+ } else {
+ $time_interval_overlaps[] = sprintf(
+ _('Gebucht im Bereich vom %1$s bis zum %2$s'),
+ $time_interval['begin']->format('d.m.Y H:i'),
+ $time_interval['end']->format('d.m.Y H:i')
+ );
+ }
+ }
+ }
+ }
+ }
+ if ($time_interval_overlaps) {
+ throw new ResourceBookingOverlapException(
+ implode(', ', $time_interval_overlaps)
+ );
+ }
+
+ NotificationCenter::postNotification('ResourceBookingWillValidate', $this);
+ }
+
+
+ /**
+ * This method updates the intervals of this resource booking
+ * which are stored in the resource_booking_intervals table.
+ */
+ public function updateIntervals($keep_exceptions = true)
+ {
+ if ($keep_exceptions) {
+ //Delete all intervals with takes_place > 0
+ //and update the time intervals of the exceptions:
+ ResourceBookingInterval::deleteBySql(
+ "booking_id = :booking_id
+ AND
+ takes_place > '0'",
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+
+ //Get all interval exceptions:
+ $exceptions = ResourceBookingInterval::findBySql(
+ "booking_id = :booking_id
+ AND
+ takes_place < '1'",
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+
+ if ($exceptions) {
+ //Now we must compare the time intervals of the booking
+ //with the time intervals of the exceptions.
+ //If there is only a difference in hours we update
+ //the exceptions. Otherwise we delete the exceptions.
+
+ $repetition_interval = $this->getRepetitionInterval();
+ if (!$repetition_interval) {
+ //No repetition interval also means nothing left to do.
+ return;
+ }
+
+ $repetition_begin = new DateTime();
+ $repetition_begin->setTimestamp($this->begin - $this->preparation_time);
+ $date_end = new DateTime();
+ $date_end->setTimestamp($this->end);
+ $repetition_end = new DateTime();
+ $repetition_end->setTimestamp($this->repeat_end);
+ $repetition_interval = $this->getRepetitionInterval();
+
+ $duration = $repetition_begin->diff($date_end);
+
+ //Loop over all exceptions and check if they belong to
+ //one of the repetions:
+
+ $obsolete_exception_ids = [];
+ foreach ($exceptions as $exception) {
+ $exception_begin = new DateTime();
+ $exception_begin->setTimestamp($exception->begin);
+ $exception_end = new DateTime();
+ $exception_end->setTimestamp($exception->end);
+ $exc_begin_str = $exception_begin->format('Y-m-d');
+ $exc_end_str = $exception_end->format('Y-m-d');
+
+ $exception_obsolete = true;
+
+ $current_repetition = clone $repetition_begin;
+ while ($current_repetition < $repetition_end) {
+ $current_end = clone $current_repetition;
+ $current_end->add($duration);
+
+ $current_begin_str = $current_repetition->format('Y-m-d');
+ $current_end_str = $current_end->format('Y-m-d');
+
+ if ($current_begin_str == $exc_begin_str && $current_end_str == $exc_end_str) {
+ //We found one exception which needs to be updated.
+ $exception_obsolete = false;
+ $exception->begin = $current_repetition->getTimestamp();
+ $exception->end = $current_end->getTimestamp();
+ if ($exception->isDirty()) {
+ $exception->store();
+ }
+ //No need to loop the rest of the repetitions:
+ break;
+ }
+
+ $current_repetition->add($repetition_interval);
+ }
+
+ if ($exception_obsolete) {
+ $obsolete_exception_ids[] = $exception->id;
+ }
+ }
+
+ if ($obsolete_exception_ids) {
+ ResourceBookingInterval::deleteBySql(
+ 'interval_id IN ( :ids )',
+ [
+ 'ids' => $obsolete_exception_ids
+ ]
+ );
+ }
+ }
+ } else {
+ //We delete all existing intervals for this booking
+ //and re-create them.
+ ResourceBookingInterval::deleteBySql(
+ 'booking_id = :booking_id',
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+ }
+
+ ResourceBookingInterval::createFromBooking($this);
+ }
+
+
+ /**
+ * Deletes the ResourceBooking object if there are no
+ * ResourceBookingInterval objects attachted to it.
+ *
+ * @return null|bool If the ResourceBooking object still has
+ * ResourceBookingInterval objects attachted to it,
+ * null is returned. Otherwise the return value of the
+ * SimpleORMap::delete method for the ResourceBooking object
+ * is returned.
+ */
+ public function deleteIfNoInterval()
+ {
+ $intervals = ResourceBookingInterval::countBySql(
+ 'booking_id = :id',
+ [
+ 'id' => $this->id
+ ]
+ );
+ if ($intervals == 0) {
+ return $this->delete();
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Deletes all bookings in the time ranges of this resource booking.
+ * Such bookings would prevent saving this booking.
+ *
+ * @return int The amount of deleted bookings.
+ */
+ public function deleteOverlappingBookings()
+ {
+ $end = new DateTime();
+ $end->setTimestamp($this->end);
+ $repetition_end = new DateTime();
+ $repetition_end->setTimestamp($this->repeat_end);
+
+ $deleted_c = 0;
+ if ($this->repetition_interval) {
+ $repetition_interval = $this->getRepetitionInterval();
+
+ if (!$this->repeat_end) {
+ throw new InvalidArgumentException(
+ _('Es wurde ein Wiederholungsintervall ohne Begrenzung angegeben!')
+ );
+ }
+ if ($this->real_begin > $this->repeat_end) {
+ throw new InvalidArgumentException(
+ _('Der Startzeitpunkt darf nicht hinter dem Ende der Wiederholungen liegen!')
+ );
+ }
+
+ //Look in each repetition for overlapping bookings and delete them.
+ $current_date = $this->real_begin_dt;
+ while ($current_date <= $repetition_end) {
+ $current_begin = clone $current_date;
+ $current_end = clone $current_date;
+ $current_end->setTime(
+ intval($end->format('H')),
+ intval($end->format('i')),
+ intval($end->format('s'))
+ );
+ $derived_resource = $this->resource->getDerivedClassInstance();
+ if ($derived_resource->userHasPermission($this->booking_user, 'tutor', [$current_begin, $current_end])) {
+ //Sufficient permissions to delete bookings
+ //in the time frame.
+ $delete_sql = 'begin < :end AND end > :begin AND resource_id = :resource_id ';
+ $sql_params = [
+ 'begin' => $current_begin->getTimestamp(),
+ 'end' => $current_end->getTimestamp(),
+ 'resource_id' => $this->resource->id
+ ];
+ if (!$this->isNew()) {
+ $delete_sql .= 'booking_id <> :booking_id';
+ $sql_params['booking_id'] = $this->id;
+ }
+ $intervals = ResourceBookingInterval::findBySQL(
+ $delete_sql,
+ $sql_params
+ );
+
+ $affected_bookings = [];
+ foreach ($intervals as $interval) {
+ if ($interval->booking instanceof ResourceBooking) {
+ $affected_bookings[$interval->booking_id] = $interval->booking;
+ }
+ $deleted_c += $interval->delete();
+ }
+
+ foreach ($affected_bookings as $booking) {
+ $booking->deleteIfNoInterval();
+ }
+ }
+ $current_date = $current_date->add($repetition_interval);
+ }
+ } else {
+ $derived_resource = $this->resource->getDerivedClassInstance();
+ if ($derived_resource->userHasPermission($this->booking_user, 'autor', [$this->real_begin_dt, $end])) {
+ $delete_sql = 'begin < :end AND end > :begin AND resource_id = :resource_id ';
+ $sql_params = [
+ 'begin' => $this->real_begin,
+ 'end' => $end->getTimestamp(),
+ 'resource_id' => $this->resource->id
+ ];
+ if (!$this->isNew()) {
+ $delete_sql .= 'booking_id <> :booking_id';
+ $sql_params['booking_id'] = $this->id;
+ }
+ $intervals = ResourceBookingInterval::findBySQL(
+ $delete_sql,
+ $sql_params
+ );
+ $affected_bookings = [];
+ foreach ($intervals as $interval) {
+ if ($interval->booking instanceof ResourceBooking) {
+ $affected_bookings[$interval->booking_id] = $interval->booking;
+ }
+ $deleted_c += $interval->delete();
+ }
+
+ foreach ($affected_bookings as $booking) {
+ $booking->deleteIfNoInterval();
+ }
+ }
+ }
+
+ return $deleted_c;
+ }
+
+
+ /**
+ * Deletes all reservations in the time ranges of this resource booking.
+ *
+ * @return int The amount of deleted reservations.
+ */
+ public function deleteOverlappingReservations()
+ {
+ //Notify all persons who made reservations or who are assigned
+ //to the reservation about the new booking which overwrites
+ //their reservation:
+ $booking_resource = Resource::find($this->resource_id);
+ $booking_user = User::find($this->booking_user_id);
+
+ $deleted_c = 0;
+
+ $template_factory = new Flexi\Factory(
+ $GLOBALS['STUDIP_BASE_PATH'] . '/locale/'
+ );
+
+ $affected_reservations = ResourceBooking::findByResourceAndTimeRanges(
+ $booking_resource,
+ [
+ [
+ 'begin' => $this->real_begin,
+ 'end' => $this->end,
+ ]
+ ],
+ [self::TYPE_RESERVATION, self::TYPE_PLANNED],
+ [$this->id]
+ );
+ foreach ($affected_reservations as $reservation) {
+ if ($reservation->id == $this->id) {
+ continue;
+ }
+ if ($reservation->assigned_user && (
+ $reservation->range_id != $reservation->booking_user_id
+ )) {
+ //Inform the person who is assigned to the reservation:
+ setTempLanguage($reservation->assigned_user->id);
+ $lang_path = getUserLanguagePath($reservation->assigned_user->id);
+
+ $template = $template_factory->open(
+ $lang_path . '/LC_MAILS/overbooked_reservation.php'
+ );
+ $template->set_attribute('resource', $booking_resource);
+ $template->set_attribute('reservation', $reservation);
+ $template->set_attribute('booking_user', $booking_user);
+ $mail_text = $template->render();
+
+ Message::send(
+ User::findCurrent()->id,
+ [$reservation->assigned_user->username],
+ _('Reservierung überbucht'),
+ $mail_text
+ );
+
+ restoreLanguage();
+ }
+ if ($reservation->booking_user) {
+ //Inform the person who made the reservation:
+ setTempLanguage($reservation->booking_user->id);
+ $lang_path = getUserLanguagePath($reservation->booking_user->id);
+
+ $template = $template_factory->open(
+ $lang_path . '/LC_MAILS/overbooked_reservation.php'
+ );
+ $template->set_attribute('resource', $booking_resource);
+ $template->set_attribute('reservation', $reservation);
+ $template->set_attribute('booking_user', $booking_user);
+ $mail_text = $template->render();
+
+ Message::send(
+ User::findCurrent()->id,
+ [$reservation->booking_user->username],
+ _('Reservierung überbucht'),
+ $mail_text
+ );
+
+ restoreLanguage();
+ }
+
+ //Delete the reservation:
+ $deleted_c += $reservation->delete();
+ }
+ return $deleted_c;
+ }
+
+
+ /**
+ * Determines whether the resource booking ends on the same timestamp
+ * like the lecture time of one of the defined semesters.
+ *
+ * @return True, if the resource booking ends with a semester,
+ * false otherwise.
+ */
+ public function endsWithSemester()
+ {
+ return Semester::countBySql(
+ '(beginn <= :begin) AND (ende >= :begin)
+ AND vorles_ende = :repeat_end',
+ [
+ 'begin' => $this->begin,
+ 'repeat_end' => $this->repeat_end
+ ]
+ ) > 0;
+ }
+
+
+ /**
+ * Check if the specified user is the owner of the booking.
+ *
+ * @param User $user The user whose ownership shall be tested.
+ *
+ * @return bool True, if the specified user is the owner of the booking,
+ * false otherwise.
+ */
+ public function userIsOwner(User $user)
+ {
+ return $this->booking_user_id == $user->id;
+ }
+
+
+ /**
+ * Determines wheter the booking is read only for a specified user.
+ *
+ * @param User $user The user whose permissions shall be checked.
+ *
+ * @return bool True, if the specified user may only perform reading
+ * actions on the booking, false otherwise.
+ */
+ public function isReadOnlyForUser(User $user)
+ {
+ if ($this->isSimpleBooking()) {
+ //In case it is a simple booking, one has to be
+ //either resource tutor or the owner of the request.
+ if ($this->userIsOwner($user)) {
+ return false;
+ }
+ //Still no answer? Check, if the user is resource tutor.
+ $derived_resource = $this->resource->getDerivedClassInstance();
+ return !$derived_resource->userHasPermission($user, 'tutor');
+ }
+ //Non-simple bookings (course bookings etc.) are always read-only.
+ return true;
+ }
+
+
+ /**
+ * Determines whether this resource booking has a repetition in the
+ * specified time range.
+ * @param DateTime $begin
+ * @param DateTime $end
+ * @return bool True, if the booking has repetitions in the timeframe
+ * specified by $begin and $end, false otherwise.
+ */
+ public function isRepetitionInTimeframe(DateTime $begin, DateTime $end)
+ {
+ return ResourceBookingInterval::countBySql(
+ 'booking_id = :booking_id AND begin < :end AND end > :begin',
+ [
+ 'booking_id' => $this->id,
+ 'begin' => $begin->getTimestamp(),
+ 'end' => $end->getTimestamp()
+ ]
+ ) > 0;
+ }
+
+
+ /**
+ * Returns the DateInterval object according to the set repetition
+ * interval of this resource booking object.
+ *
+ * @return DateInterval|null A DateInterval object or null,
+ * if this booking has no repetition interval.
+ */
+ public function getRepetitionInterval()
+ {
+ $repetition_interval = null;
+ if ($this->repetition_interval) {
+ try {
+ if ($this->repetition_interval instanceof DateInterval) {
+ $repetition_interval = $this->repetition_interval;
+ } else {
+ $repetition_interval = new DateInterval($this->repetition_interval);
+ }
+ } catch (Exception $e) {
+ //Invalid repetition interval string.
+ throw new InvalidArgumentException(
+ sprintf(
+ _('Das Wiederholungsintervall ist in einem ungültigen Format (%s)!'),
+ $this->repetition_interval
+ )
+ );
+ }
+ }
+ return $repetition_interval;
+ }
+
+
+ public function getRepeatModeString()
+ {
+ $interval = $this->getRepetitionInterval();
+
+ if ($interval->m) {
+ switch ($interval->m) {
+ case 1: return _('jeden Monat');
+ case 2: return _('jeden zweiten Monat');
+ case 3: return _('jeden dritten Monat');
+ case 4: return _('jeden vierten Monat');
+ case 5: return _('jeden fünften Monat');
+ case 6: return _('jeden sechsten Monat');
+ case 7: return _('jeden siebten Monat');
+ case 8: return _('jeden achten Monat');
+ case 9: return _('jeden neunten Monat');
+ case 10: return _('jeden zehnten Monat');
+ case 11: return _('jeden elften Monat');
+ }
+ } elseif (($interval->d % 7) == 0) {
+ $week = round($interval->d / 7);
+ switch ($week) {
+ case 1: return _('jede Woche');
+ case 2: return _('jede zweite Woche');
+ case 3: return _('jede dritte Woche');
+ }
+ } elseif ($interval->d) {
+ if ($interval->d > 1) {
+ return sprintf(
+ _('jeden %d. Tag'),
+ $interval->d
+ );
+ } elseif ($interval->d == 1) {
+ return _('jeden Tag');
+ } else {
+ return _('ungültiges Zeitintervall');
+ }
+ }
+ }
+
+ /**
+ * Determines if the resource booking overlaps with another
+ * resource booking.
+ *
+ * @return bool True, if there are other bookings which overlap
+ * with this one, false otherwise.
+ */
+ public function hasOverlappingBookings()
+ {
+ //Get all intervals of this booking, loop over them
+ //and check if there are other intervals in the same
+ //timeframe for the same resource.
+
+ $intervals = ResourceBookingInterval::findBySql(
+ 'booking_id = :booking_id',
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+
+ if ($intervals) {
+ foreach ($intervals as $interval) {
+ $count = ResourceBookingInterval::countBySql(
+ 'booking_id <> :booking_id
+ AND
+ resource_id = :resource_id
+ AND takes_place = 1
+ AND
+ (
+ begin < :end AND :begin < end
+ )',
+ [
+ 'booking_id' => $this->id,
+ 'resource_id' => $this->resource_id,
+ 'begin' => $interval->begin,
+ 'end' => $interval->end
+ ]
+ );
+
+ if ($count) {
+ //We have found an interval which overlaps
+ //with an interval of this booking.
+ return true;
+ }
+ }
+ }
+
+ //If this booking has no intervals it means it doesn't have
+ //any time intervals at all. Therefore there can't be any
+ //overlapping bookings.
+ return false;
+ }
+
+ /**
+ * Gets the bookings that overlap with this booking.
+ *
+ * @return array Array of ResourceBooking objects
+ */
+ public function getOverlappingBookings()
+ {
+ //Get all intervals of this booking, loop over them
+ //and check if there are other intervals in the same
+ //timeframe for the same resource. If so, collect the
+ //booking-IDs and get all the bookings.
+
+ $intervals = ResourceBookingInterval::findBybooking_id($this->id);
+
+ if ($intervals) {
+ $db = DBManager::get();
+
+ $get_booking_id_stmt = $db->prepare(
+ 'SELECT DISTINCT booking_id
+ FROM resource_booking_intervals
+ WHERE
+ booking_id <> :booking_id
+ AND
+ resource_id = :resource_id
+ AND takes_place = 1
+ AND
+ (
+ begin < :end AND :begin < end
+ )'
+ );
+ $overlapping_booking_ids = [];
+ foreach ($intervals as $interval) {
+ $get_booking_id_stmt->execute(
+ [
+ 'booking_id' => $this->id,
+ 'resource_id' => $this->resource_id,
+ 'begin' => $interval->begin,
+ 'end' => $interval->end
+ ]
+ );
+
+ $overlapping_booking_ids = array_merge(
+ $overlapping_booking_ids,
+ $get_booking_id_stmt->fetchAll(
+ PDO::FETCH_COLUMN,
+ 0
+ )
+ );
+ }
+
+ if ($overlapping_booking_ids) {
+ $overlapping_booking_ids = array_unique(
+ $overlapping_booking_ids
+ );
+
+ return ResourceBooking::findMany(
+ $overlapping_booking_ids
+ );
+ }
+ }
+
+ //If this booking has no intervals it means it doesn't have
+ //any time intervals at all. Therefore there can't be any
+ //overlapping bookings.
+ return false;
+ }
+
+
+ /**
+ * Calculates all time intervals as begin and end timestamps
+ * by looking at the repetition settings, if any.
+ *
+ * @param bool $as_datetime Whether to return the timestamps
+ * as DateTime objects (true) or not (false). Defaults to false.
+ *
+ * @return array A two-dimensional array with each time interval
+ * for this booking. The array has the following structure:
+ * [
+ * [
+ * 'begin' => Begin timestamp.
+ * 'end' => End timestamp.
+ * ]
+ * ]
+ */
+ public function calculateTimeIntervals($as_datetime = false)
+ {
+ $interval_data = [];
+ $booking_begin = new DateTime();
+ $booking_begin->setTimestamp($this->begin);
+ if ($this->preparation_time) {
+ $booking_begin->setTimestamp(
+ $this->begin - $this->preparation_time
+ );
+ }
+ $booking_end = new DateTime();
+ $booking_end->setTimestamp($this->end);
+
+ //use begin and end to create the first interval:
+ $interval_data[] = [
+ 'begin' => (
+ $as_datetime
+ ? clone $booking_begin
+ : $booking_begin->getTimestamp()
+ ),
+ 'end' => (
+ $as_datetime
+ ? clone $booking_end
+ : $booking_end->getTimestamp()
+ )
+ ];
+
+ if ($this->repeat_end) {
+ //Repetition: we must check which repetition interval has been
+ //selected and then create entries for each repetition.
+ //Repetition starts with the begin date and ends with the
+ //"repeat_end" date.
+ $repetition_end = new DateTime();
+ $repetition_end->setTimestamp($this->repeat_end);
+ //The DateInterval constructor will throw an exception,
+ //if it cannot parse the string stored in $this->repetition_interval.
+ $repetition_interval = $this->getRepetitionInterval();
+
+ if ($repetition_interval instanceof DateInterval) {
+ $duration = $booking_begin->diff($booking_end);
+
+ //Check if end is later than begin to avoid
+ //infinite loops.
+ if ($repetition_end > $booking_begin) {
+ $current_begin = clone $booking_begin;
+ $current_begin->add($repetition_interval);
+ while ($current_begin < $repetition_end) {
+ $current_end = clone $current_begin;
+ $current_end->add($duration);
+ $interval_data[] = [
+ 'begin' => (
+ $as_datetime
+ ? clone $current_begin
+ : $current_begin->getTimestamp()
+ ),
+ 'end' => (
+ $as_datetime
+ ? clone $current_end
+ : $current_end->getTimestamp()
+ )
+ ];
+ $current_begin->add($repetition_interval);
+ }
+ } else {
+ //end timestamp is before begin timestamp:
+ //return an empty array
+ return [];
+ }
+ }
+ //Everything went fine.
+ }
+ return $interval_data;
+ }
+
+
+ /**
+ * Retrieves all time intervals for this resource booking.
+ *
+ * @return ResourceBookingInterval[] An array of
+ * ResourceBookingInterval objects.
+ */
+ public function getTimeIntervals($with_exceptions = true)
+ {
+ if ($with_exceptions) {
+ return ResourceBookingInterval::findBySQL(
+ "booking_id = :booking_id
+ ORDER BY begin ASC, end ASC",
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+ } else {
+ return ResourceBookingInterval::findBySQL(
+ "booking_id = :booking_id AND takes_place = '1'
+ ORDER BY begin ASC, end ASC",
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+ }
+ }
+
+
+ /**
+ * Retrieves all time intervals for this resource booking
+ * in a specified time range.
+ *
+ * @param DateTime $begin The begin of the time range.
+ * @param DateTime $end The end of the time range.
+ *
+ * @return ResourceBookingInterval[] An array of
+ * ResourceBookingInterval objects.
+ */
+ public function getTimeIntervalsInTimeRange(DateTime $begin, DateTime $end)
+ {
+ if ($begin > $end) {
+ //We don't serve querys with invalid time ranges here.
+ return [];
+ }
+
+ return ResourceBookingInterval::findBySql(
+ 'booking_id = :booking_id AND begin < :end AND end > :begin ORDER BY begin, end',
+ [
+ 'booking_id' => $this->id,
+ 'begin' => $begin->getTimestamp(),
+ 'end' => $end->getTimestamp()
+ ]
+ );
+ }
+
+
+ /**
+ * Returns all allocating users: Users who are associated with this booking
+ * through a request or a course date for which this booking has been made
+ * (all allocating users). If no user could be determined but the booking
+ * is bound to a course, the course name is returned instead.
+ *
+ * @deprecated
+ * @param bool $only_names Whether only the names of these users shall be
+ * returned (true) or user objects shall be returned (false).
+ *
+ * @return string[]|User[] Depending on the value of the $only_names
+ * parameter a string array or an user object array is returned.
+ * In case no user can be found, the array is empty.
+ * In case the parameter $only_names is set to true, the result
+ * may also contain a course name, if no users can be found but the
+ * booking is bound to a course.
+ */
+ public function getAssignedUsers($only_names = true)
+ {
+ //The only two cases which shall be handled are that
+ //a booking is assigned to a course or an user.
+ if ($this->getAssignedUserType() === 'course') {
+ //Return all persons associated with that course date.
+ if (count($this->assigned_course_date->dozenten)) {
+ if ($only_names) {
+ $lecturers = [];
+ foreach ($this->assigned_course_date->dozenten as $lecturer) {
+ $lecturers[] = $lecturer->getFullName();
+ }
+ return $lecturers;
+ } else {
+ return $this->assigned_course_date->dozenten;
+ }
+ } else {
+ $lecturers = User::findBySql(
+ "INNER JOIN seminar_user
+ USING (user_id)
+ WHERE
+ seminar_user.seminar_id = :course_id
+ AND
+ seminar_user.status = 'dozent'
+ ORDER BY nachname ASC, vorname ASC",
+ [
+ 'course_id' => $this->assigned_course_date->range_id
+ ]
+ );
+ if (!$lecturers) {
+ return [];
+ }
+ if ($only_names) {
+ $names = [];
+ foreach ($lecturers as $lecturer) {
+ $names[] = $lecturer->getFullName();
+ }
+ return $names;
+ } else {
+ return $lecturers;
+ }
+ }
+ } elseif ($this->getAssignedUserType() === 'user') {
+ if ($only_names) {
+ return [$this->assigned_user->getFullName()];
+ }
+ return [$this->assigned_user];
+ }
+
+ return [];
+ }
+
+ public function getAssignedUser()
+ {
+ if ($this->getAssignedUserType() === 'course') {
+ return $this->assigned_course_date->course;
+ }
+ if ($this->getAssignedUserType() === 'user') {
+ return $this->assigned_user;
+ }
+ }
+
+ public function getAssignedUserType()
+ {
+ if (isset($this->assigned_user_type)) {
+ return $this->assigned_user_type;
+ }
+ if ($this->assigned_course_date instanceof CourseDate) {
+ return $this->assigned_user_type = 'course';
+ }
+ if ($this->assigned_user instanceof User) {
+ return $this->assigned_user_type = 'user';
+ }
+ return $this->assigned_user_type = 'none';
+ }
+
+ public function getAssignedUserName()
+ {
+ $name = '';
+ if ($this->getAssignedUserType() === 'course') {
+ $name = $this->assigned_course_date->course->getFullName();
+ $name .= ' (' . implode(',', $this->assigned_course_date->course->getMembersWithStatus('dozent', true)->limit(3)->getValue('nachname')) . ')';
+ } elseif ($this->getAssignedUserType() === 'user') {
+ if (get_visibility_by_id($this->assigned_user->id) ||
+ ($this->assigned_user->id == $GLOBALS['user']->id)
+ ) {
+ $name = $this->assigned_user->getFullName();
+ if ($this->description) {
+ $name .= " \n" . $this->description;
+ }
+ } else {
+ //Check if the current user has at least user permissions
+ //on the resource. In that case, even invisible assigned
+ //users become visible.
+ $resource = $this->resource->getDerivedClassInstance();
+ $current_user = User::findCurrent();
+ if (($resource instanceof Resource) && ($current_user instanceof User)) {
+ if ($resource->userHasPermission($current_user, 'user')) {
+ $name = $this->assigned_user->getFullName();
+ if ($this->description) {
+ $name .= " \n" . $this->description;
+ }
+ }
+ }
+ }
+ } else {
+ $name = $this->description;
+ }
+ return $name;
+ }
+
+
+ /**
+ * Determines whether the booking is a simple booking
+ * that is not bound to course dates or similar Stud.IP objects.
+ */
+ public function isSimpleBooking()
+ {
+ return !($this->getAssignedUserType() === 'course');
+ }
+
+
+ /**
+ * Determins whether the booking has exceptions in repetitions or not.
+ * When the booking is bound to a course via a CourseDate instance,
+ * the exceptions are looked up using the corresponding metadate (if any).
+ * If a metadate exists to the course date and it has cancelled dates
+ * (metadate has CourseExDate instances assigned) then the booking has
+ * exceptions.
+ * If the booking is a simple booking, the exception status is determined
+ * by checking if the booking has ResourceBookingInterval instances
+ * that don't take place assigned to it. This is only checked,
+ * if a repetition interval is set for the booking so that such instances
+ * can exist.
+ *
+ * @return bool True, if the booking has exceptions in the repetition,
+ * false otherwise.
+ */
+ public function hasExceptions()
+ {
+ if ($this->isSimpleBooking()) {
+ //This is a simple booking: Check the repetition interval:
+ if ($this->repetition_interval) {
+ //A repetition interval exists: Check if booking intervals
+ //exist that don't take place:
+ return ResourceBookingInterval::countBySql(
+ "booking_id = :booking_id
+ AND takes_place = '0'",
+ [
+ 'booking_id' => $this->id
+ ]
+ ) > 0;
+ } else {
+ return false;
+ }
+ } else {
+ //Check if the course booking has exceptions (via metadate):
+ $metadate = $this->assigned_course_date->cycle;
+ if ($metadate instanceof SeminarCycleDate) {
+ //Check if the metadate has exceptions:
+ return CourseExDate::countBySql(
+ 'metadate_id = :metadate_id',
+ [
+ 'metadate_id' => $metadate->id
+ ]
+ ) > 0;
+ } else {
+ //No metadate is assigned to the course booking.
+ return false;
+ }
+ }
+ }
+
+
+ public function __toString()
+ {
+ return date('d.m.Y H:i', $this->begin)
+ . ' - '
+ . date('d.m.Y H:i', $this->end);
+ }
+
+
+ public function getTimeIntervalStrings()
+ {
+ $time_intervals = ResourceBookingInterval::findBySQL(
+ 'booking_id = :booking_id ORDER BY begin ASC, end ASC',
+ [
+ 'booking_id' => $this->id
+ ]
+ );
+
+ $strings = [];
+ foreach ($time_intervals as $interval) {
+ if (date('Ymd', $interval->begin) == date('Ymd', $interval->end)) {
+ $strings[] = sprintf(
+ '%1$s %2$s - %3$s',
+ date('d.m.Y', $interval->begin),
+ date('H:i', $interval->begin),
+ date('H:i', $interval->end)
+ );
+ } else {
+ $strings[] = sprintf(
+ '%1$s - %2$s',
+ date('d.m.Y H:i', $interval->begin),
+ date('d.m.Y H:i', $interval->end)
+ );
+ }
+ }
+ return $strings;
+ }
+
+
+ /**
+ * @inheritDoc
+ */
+ public static function exportUserData(StoredUserData $storage)
+ {
+ $user = User::find($storage->user_id);
+ $bookings = self::findBySql(
+ 'user_id = :user_id ORDER BY mkdate',
+ [
+ 'user_id' => $storage->user_id
+ ]
+ );
+
+ $rows = [];
+ foreach ($bookings as $booking) {
+ $rows[] = $booking->toRawArray();
+ }
+ $storage->addTabularData(
+ _('Buchungen von Ressourcen'),
+ 'resource_bookings',
+ $rows,
+ $user
+ );
+ }
+
+
+ public function convertToEventData(array $time_intervals, $user)
+ {
+ $booking_plan_booking_bg =
+ \ColourValue::find('Resources.BookingPlan.Booking.Bg');
+ $booking_plan_booking_fg =
+ \ColourValue::find('Resources.BookingPlan.Booking.Fg');
+ $booking_plan_simple_booking_with_exceptions_bg =
+ ColourValue::find('Resources.BookingPlan.SimpleBookingWithExceptions.Bg');
+ $booking_plan_simple_booking_with_exceptions_fg =
+ ColourValue::find('Resources.BookingPlan.SimpleBookingWithExceptions.Fg');
+ $booking_plan_reservation_bg = \ColourValue::find('Resources.BookingPlan.Reservation.Bg');
+ $booking_plan_reservation_fg = \ColourValue::find('Resources.BookingPlan.Reservation.Fg');
+ $booking_plan_lock_bg = \ColourValue::find('Resources.BookingPlan.Lock.Bg');
+ $booking_plan_lock_fg = \ColourValue::find('Resources.BookingPlan.Lock.Fg');
+ $booking_plan_planned_booking_bg = \ColourValue::find('Resources.BookingPlan.PlannedBooking.Bg');
+ $booking_plan_planned_booking_fg = \ColourValue::find('Resources.BookingPlan.PlannedBooking.Fg');
+ $booking_plan_preparation_bg = \ColourValue::find('Resources.BookingPlan.PreparationTime.Bg');
+ $booking_plan_preparation_fg = \ColourValue::find('Resources.BookingPlan.PreparationTime.Fg');
+ $booking_plan_course_booking_bg = \ColourValue::find('Resources.BookingPlan.CourseBooking.Bg');
+ $booking_plan_course_booking_fg = \ColourValue::find('Resources.BookingPlan.CourseBooking.Fg');
+
+ $colour = $booking_plan_booking_bg->__toString();
+ $text_colour = $booking_plan_booking_fg->__toString();
+ $event_classes = [];
+
+ if ($this->booking_type == self::TYPE_NORMAL) {
+ $event_classes[] = 'resource-booking';
+ //Check if the booking is a course booking:
+ if ($this->getAssignedUserType() === 'course') {
+ //It is a course date.
+ $event_classes[] = 'for-course';
+ $colour = $booking_plan_course_booking_bg->__toString();
+ $text_colour = $booking_plan_course_booking_fg->__toString();
+ }
+ } elseif ($this->booking_type == self::TYPE_RESERVATION) {
+ $event_classes[] = 'resource-reservation';
+ $colour = $booking_plan_reservation_bg->__toString();
+ $text_colour = $booking_plan_reservation_fg->__toString();
+ } elseif ($this->booking_type == self::TYPE_LOCK) {
+ $event_classes[] = 'resource-lock';
+ $colour = $booking_plan_lock_bg->__toString();
+ $text_colour = $booking_plan_lock_fg->__toString();
+ } elseif ($this->booking_type == self::TYPE_PLANNED) {
+ $event_classes[] = 'resource-planned-booking';
+ $colour = $booking_plan_planned_booking_bg->__toString();
+ $text_colour = $booking_plan_planned_booking_fg->__toString();
+ }
+
+ $booking_is_editable = false;
+ if ($user instanceof User) {
+ $booking_is_editable = !$this->isReadOnlyForUser($user);
+ }
+
+ $booking_api_urls = [];
+ $booking_view_urls = [
+ 'show' => \URLHelper::getURL(
+ 'dispatch.php/resources/booking/index/'
+ . $this->id
+ ),
+ ];
+ if ($booking_is_editable) {
+ $booking_view_urls['edit'] = \URLHelper::getURL(
+ 'dispatch.php/resources/booking/edit/'
+ . $this->id
+ );
+ }
+ $events = [];
+
+ foreach ($time_intervals as $interval) {
+ $real_begin = $interval['begin'];
+ if ($this->preparation_time) {
+ $real_begin += $this->preparation_time;
+ $begin = new DateTime();
+ $begin->setTimestamp($interval['begin']);
+ $end = new DateTime();
+ $end->setTimestamp($real_begin);
+ $events[] = new Studip\Calendar\EventData(
+ $begin,
+ $end,
+ _('Rüstzeit'),
+ ['preparation-time'],
+ $booking_plan_preparation_fg->__toString(),
+ $booking_plan_preparation_bg->__toString(),
+ $booking_is_editable,
+ 'ResourceBookingInterval',
+ $interval->id,
+ 'ResourceBooking',
+ $this->id,
+ 'Resource',
+ $this->resource_id,
+ $booking_view_urls
+ );
+ }
+
+ $event_title = '';
+ $icon = '';
+
+ if ($user instanceof User) {
+ $derived_resource = $this->resource->getDerivedClassInstance();
+ if ($derived_resource->userHasPermission($user, 'user') && $this->internal_comment) {
+ $icon = 'chat2';
+ }
+ }
+
+ if (!$this->isSimpleBooking()) {
+ if ($this->assigned_course_date->metadate_id) {
+ $icon = 'refresh';
+ }
+ } elseif ($this->repeat_end > $this->end) {
+ $icon = 'refresh';
+ }
+
+ if ($this->assigned_course_date instanceof CourseDate) {
+ $course = $this->assigned_course_date->course;
+ if ($course instanceof Course) {
+ $has_perms = $GLOBALS['perm']->have_studip_perm('user', $course->id, $user->id);
+ $vis_perms = $GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM, $user->id);
+ if ($has_perms || $vis_perms || $course->visible) {
+ $event_title = $this->getAssignedUserName();
+ }
+ }
+ } else {
+ $event_title = $this->getAssignedUserName();
+ }
+
+ $interval_api_urls = $booking_api_urls;
+ if ($booking_is_editable) {
+ //A note on the move URL:
+ //When used in a room group booking plan, the interval_id
+ //URL parameter is subject to modification in JavaScript.
+ //(lib/resources.js, method dropEventInRoomGroupBookingPlan)
+ $interval_api_urls = [
+ 'resize' => \URLHelper::getURL(
+ 'dispatch.php/resources/ajax/move_booking/' . $this->id,
+ [
+ 'quiet' => true,
+ 'interval_id' => $interval->id
+ ]
+ ),
+ 'move' => \URLHelper::getURL(
+ 'dispatch.php/resources/ajax/move_booking/' . $this->id,
+ [
+ 'quiet' => true,
+ 'interval_id' => $interval->id
+ ]
+ )
+ ];
+ }
+ $begin = new DateTime();
+ $begin->setTimestamp($real_begin);
+ $end = new DateTime();
+ $end->setTimestamp($interval['end']);
+ $events[] = new Studip\Calendar\EventData(
+ $begin,
+ $end,
+ $event_title,
+ $event_classes,
+ $text_colour,
+ $colour,
+ $booking_is_editable,
+ ResourceBookingInterval::class,
+ $interval->id,
+ ResourceBooking::class,
+ $this->id,
+ Resource::class,
+ $this->resource_id,
+ $booking_view_urls,
+ $interval_api_urls,
+ $icon
+ );
+ }
+
+ return $events;
+ }
+
+
+ public function getAllEventData()
+ {
+ return $this->convertToEventData(
+ $this->getTimeIntervals(false),
+ User::findCurrent()
+ );
+ }
+
+
+ public function getEventDataForTimeRange(DateTime $begin, DateTime $end)
+ {
+
+ return $this->getFilteredEventData(null, null, null, $begin, $end);
+ }
+
+
+ public function getFilteredEventData(
+ $user_id = null,
+ $range_id = null,
+ $range_type = null,
+ $begin = null,
+ $end = null
+ )
+ {
+ $sql = "booking_id = :booking_id AND takes_place = 1 ";
+ $sql_array = [
+ 'booking_id' => $this->id
+ ];
+
+ if ($begin && $end) {
+ if ($begin instanceof DateTime) {
+ $begin = $begin->getTimestamp();
+ }
+ if ($end instanceof DateTime) {
+ $end = $end->getTimestamp();
+ }
+ $sql .= "AND begin < :end AND :begin < end";
+ $sql_array['begin'] = $begin;
+ $sql_array['end'] = $end;
+ }
+ $time_intervals = ResourceBookingInterval::findBySQL(
+ $sql,
+ $sql_array
+ );
+
+ $user = null;
+ if ($user_id) {
+ $user = User::find($user_id);
+ } else {
+ $user = User::findCurrent();
+ }
+
+ return $this->convertToEventData($time_intervals, $user);
+ }
+
+ /**
+ * @return string
+ */
+ public function getRepetitionType()
+ {
+ if ($this->getAssignedUserType() === 'course') {
+ if ($this->assigned_course_date->metadate_id) {
+ return 'weekly';
+ } else {
+ return 'single';
+ }
+ } else {
+ $intervall = $this->getRepetitionInterval();
+ if (!$intervall) {
+ return 'single';
+ }
+ if ($intervall->m) {
+ return 'monthly';
+ }
+ if ($intervall->d % 7 === 0) {
+ return 'weekly';
+ }
+ if ($intervall->d) {
+ return 'daily';
+ }
+ }
+
+ return '';
+ }
+
+
+ /**
+ * Sends a notification that the booking has been deleted
+ * to the user that created the booking.
+ */
+ public function sendDeleteNotification()
+ {
+ if ($this->booking_type != self::TYPE_NORMAL) {
+ //We only handle real bookings in this method.
+ return;
+ }
+
+ if ($this->end < time()) {
+ //Bookings that lie in the past can be deleted without
+ //sending notifications.
+ return;
+ }
+
+ $booking_resource = Resource::find($this->resource_id);
+ $booking_user = User::find($this->booking_user_id);
+ $booking_course = null;
+ if ($this->course_id) {
+ $booking_course = Course::find($this->course_id);
+ }
+ if (!$booking_resource || !$booking_user) {
+ //Nothing we can do here.
+ return;
+ }
+ if (User::findCurrent()->id === $booking_user->id) {
+ return;
+ }
+
+ $template_factory = new Flexi\Factory(
+ $GLOBALS['STUDIP_BASE_PATH'] . '/locale/'
+ );
+ setTempLanguage($booking_user->id);
+ $lang_path = getUserLanguagePath($booking_user->id);
+
+ $template = $template_factory->open(
+ $lang_path . '/LC_MAILS/delete_booking_notification.php'
+ );
+ $template->set_attribute('resource', $booking_resource->getDerivedClassInstance());
+ $template->set_attribute('begin', $this->begin);
+ $template->set_attribute('end', $this->end);
+ $template->set_attribute('deleting_user', User::findCurrent());
+ $template->set_attribute('booking_course', $booking_course);
+
+ $mail_text = $template->render();
+
+ Message::send(
+ User::findCurrent()->id,
+ [$booking_user->username],
+ _('Ihre Buchung wurde gelöscht'),
+ $mail_text
+ );
+
+ restoreLanguage();
+ }
+}