Skip to content
Snippets Groups Projects
Select Git revision
  • 7342a207d8218d258ae3ea4435136953237aa9a5
  • main default protected
  • studip-rector
  • ci-opt
  • course-members-export-as-word
  • data-vue-app
  • pipeline-improvements
  • webpack-optimizations
  • rector
  • icon-renewal
  • http-client-and-factories
  • jsonapi-atomic-operations
  • vueify-messages
  • tic-2341
  • 135-translatable-study-areas
  • extensible-sorm-action-parameters
  • sorm-configuration-trait
  • jsonapi-mvv-routes
  • docblocks-for-magic-methods
19 results

courseware-activities-app.js

Blame
  • Forked from Stud.IP / Stud.IP
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ResourceBooking.class.php 66.73 KiB
    <?php
    
    /**
     * ResourceBooking.class.php - model class for resource bookings
     *
     * 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 ResourceBooking class is responsible for storing
     * bookings of resources in a specified time range
     * or a time interval.
     *
     * @property string id database column
     * @property string resource_id database column
     * @property string range_id database column
     *     The user, course etc. where the booking (booking)
     *     is associated with.
     * @property string booking_user_id database column
     *     The user who created the booking (booking).
     * @property string description database column
     * @property string begin database column
     * @property string end database column
     * @property string booking_type database column: The booking type.
     *     The following types are defined:
     *     0 = normal booking
     *     1 = reservation
     *     2 = lock
     *     3 = planned booking (reservation from external tools)
     *
     * @property string repeat_end database column
     * @property string repeat_quantity database column
     * @property string repetition_interval database column
     *     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 internal_comment database column
     * @property string mkdate database column
     * @property string chdate database column
     * @property Resource resource belongs_to Resource
     * @property User assigned_user belongs_to User
     * @property CourseDate assigned_course_date belongs_to CourseDate
     */
    class ResourceBooking extends SimpleORMap implements PrivacyObject, Studip\Calendar\EventSource
    {
        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['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_quantity || $this->repeat_end)) {
                    throw new InvalidArgumentException(
                        _('Es wurde ein Wiederholungsintervall ohne Begrenzung angegeben!')
                    );
                }
                if ((!$this->repeat_quantity) && ($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();
            }
    
            //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
            //(the moment this booking is saved).
            $derived_resource = $this->resource->getDerivedClassInstance();
            $user_has_booking_rights = $derived_resource->userHasBookingRights(
                $this->booking_user
            );
            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 = [];
            foreach ($time_intervals as $time_interval) {
                $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) {
                        if ($time_interval['begin']->format('Ymd') == $time_interval['end']->format('Ymd')) {
                            $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 {
                            $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()
        {
            $real_begin = new DateTime();
            $real_begin->setTimestamp($this->begin - $this->preparation_time);
            $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_quantity || $this->repeat_end)) {
                    throw new InvalidArgumentException(
                        _('Es wurde ein Wiederholungsintervall ohne Begrenzung angegeben!')
                    );
                }
                if ((!$this->repeat_quantity) && ($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 = clone $real_begin;
                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', [$real_begin, $end])) {
                    $delete_sql = 'begin < :end AND end > :begin AND resource_id = :resource_id ';
                    $sql_params = [
                        'begin' => $real_begin->getTimestamp(),
                        '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);
    
            $real_begin = new DateTime();
            $real_begin->setTimestamp($this->begin - $this->preparation_time);
            $end = new DateTime();
            $end->setTimestamp($this->end);
    
            $deleted_c = 0;
    
            $template_factory = new Flexi_TemplateFactory(
                $GLOBALS['STUDIP_BASE_PATH'] . '/locale/'
            );
    
            $affected_reservations =  ResourceBooking::findByResourceAndTimeRanges(
                $booking_resource,
                [
                    [
                        'begin' => $real_begin->getTimestamp(),
                        'end' => $end->getTimestamp(),
                    ]
                ],
                [1, 3],
                [$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_quantity > 0) || $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 == '0') {
                $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 == '1') {
                $event_classes[] = 'resource-reservation';
                $colour = $booking_plan_reservation_bg->__toString();
                $text_colour = $booking_plan_reservation_fg->__toString();
            } elseif ($this->booking_type == '2') {
                $event_classes[] = 'resource-lock';
                $colour = $booking_plan_lock_bg->__toString();
                $text_colour = $booking_plan_lock_fg->__toString();
            } elseif ($this->booking_type == '3') {
                $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(
                            'api.php/resources/booking/'
                          . $this->id . '/move',
                            [
                                'quiet' => '1',
                                'interval_id' => $interval->id
                            ]
                        ),
                        'move' => \URLHelper::getURL(
                            'api.php/resources/booking/'
                          . $this->id . '/move',
                            [
                                'quiet' => '1',
                                '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',
                    $interval->id,
                    'ResourceBooking',
                    $this->id,
                    'Resource',
                    $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';
                }
            }
        }
    
    
        /**
         * Sends a notification that the booking has been deleted
         * to the user that created the booking.
         */
        public function sendDeleteNotification()
        {
            if ($this->booking_type != '0') {
                //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);
            if (!$booking_resource || !$booking_user) {
                //Nothing we can do here.
                return;
            }
            if (User::findCurrent()->id === $booking_user->id) {
                return;
            }
    
            $template_factory = new Flexi_TemplateFactory(
                $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());
    
            $mail_text = $template->render();
    
            Message::send(
                User::findCurrent()->id,
                [$booking_user->username],
                _('Ihre Buchung wurde gelöscht'),
                $mail_text
            );
    
            restoreLanguage();
        }
    }