Skip to content
Snippets Groups Projects
ResourceRequest.class.php 82.9 KiB
Newer Older
<?php

/**
 * ResourceRequest.class.php - Contains a model class for resource requests.
 *
 * 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
 */


/**
 * ResourceRequest is a model class for resource requests.
 *
 * @property string id database column
 * @property string resource_id database column
 * @property string category_id database column
 * @property string course_id database column
 * @property string termin_id database column
 * @property string metadate_id database column
 * @property string begin database column
 * @property string end database column
 * @property string preparation_time databasse column
 * @property string marked database column
 *     There are four marking states:
 *     0 - not marked
 *     1 - red state
 *     2 - yellow state
 *     3 - green state
 * @property string user_id database column
 * @property string last_modified_by database column
 * @property string comment database column
 * @property string reply_comment database column
 * @property string reply_recipients database column:
 *     enum('requester', 'lecturer')
 * @property string closed database column, possible states are:
 *     0 - room-request is open
 *     1 - room-request has been processed, but no confirmation has been sent
 *     2 - room-request has been processed and a confirmation has been sent
 *     3 - room-request has been declined
 *
 * @property string mkdate database column
 * @property string chdate database column
 * @property Resource resource belongs_to Resource
 * @property User requester belongs_to User
 * @property User last_modifier belongs_to User
 *
 *
 * The attributes begin and end are only used in simple resource requests.
 * The "traditional" resource requests use either course_id, metadate_id
 * or termin_id to store the time ranges connected to the request.
 */
class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calendar\EventSource
{
    /**
     * The amount of defined marking states.
     */
    const MARKING_STATES = 4;

    protected static function configure($config = [])
    {
        $config['db_table'] = 'resource_requests';

        $config['belongs_to']['resource'] = [
            'foreign_key' => 'resource_id',
            'assoc_func'  => 'find'
        ];

        $config['belongs_to']['category'] = [
            'class_name'  => ResourceCategory::class,
            'foreign_key' => 'category_id',
            'assoc_func'  => 'find'
        ];

        $config['belongs_to']['user'] = [
            'foreign_key' => 'user_id',
            'assoc_func'  => 'find'
        ];

        $config['belongs_to']['last_modifier'] = [
            'foreign_key' => 'last_modified_by',
            'assoc_func'  => 'find'
        ];

        $config['belongs_to']['course'] = [
            'foreign_key' => 'course_id',
            'assoc_func'  => 'find'
        ];

        $config['belongs_to']['cycle'] = [
            'class_name'  => SeminarCycleDate::class,
            'foreign_key' => 'metadate_id'
        ];

        $config['belongs_to']['date'] = [
            'foreign_key' => 'termin_id'
        ];

        $config['has_many']['properties'] = [
            'class_name'        => ResourceRequestProperty::class,
            'foreign_key'       => 'id',
            'assoc_foreign_key' => 'request_id',
            'on_store'          => 'store',
            'on_delete'         => 'delete'
        ];

        $config['has_many']['appointments'] = [
            'class_name'        => ResourceRequestAppointment::class,
            'foreign_key'       => 'id',
            'assoc_foreign_key' => 'request_id',
            'on_store'          => 'store',
            'on_delete'         => 'delete'
        ];

        //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';
        }
        $config['registered_callbacks']['after_create'][] = 'cbLogNewRequest';
        $config['registered_callbacks']['after_store'][] = 'cbAfterStore';
        $config['registered_callbacks']['after_delete'][] = 'cbAfterDelete';


        parent::configure($config);
    }

    /**
     * @inheritDoc
     */
    public static function exportUserdata(StoredUserData $storage)
    {
        $user = User::find($storage->user_id);

        $requests = self::findBySql(
            'user_id = :user_id ORDER BY mkdate',
            [
                'user_id' => $storage->user_id
            ]
        );

        $request_rows = [];
        foreach ($requests as $request) {
            $request_rows[] = $request->toRawArray();
        }
        $storage->addTabularData(
            _('Ressourcenanfragen'),
            'resource_requests',
            $request_rows,
            $user
        );
    }

    /**
     * Retrieves all resource requests from the database.
     *
     * @return ResourceRequest[] An array of ResourceRequests objects
     *     or an empty array, if no resource requests are stored
     *     in the database.
     */
    public static function findAll()
    {
        return self::findBySql('TRUE ORDER BY mkdate ASC');
    }

    /**
     * Retrieves all open resource requests from the database.
     *
     * @return ResourceRequest[] An array of ResourceRequests objects
     *     or an empty array, if no open resource requests are stored
     *     in the database.
     */
    public static function findOpen()
    {
        return self::findBySql("closed = '0' ORDER BY mkdate ASC");
    }

    /**
     * Internal method that generated the SQL query used in
     * findByResourceAndTimeRanges and countByResourceAndTimeRanges.
     *
     * @see findByResourceAndTimeRanges
     * @inheritDoc
     */
    protected static function buildResourceAndTimeRangesSqlQuery(
        Resource $resource,
        $time_ranges = [],
        $closed_status = null,
        $excluded_request_ids = [],
        $additional_conditions = '',
        $additional_parameters = []
    )
    {
        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 $closed_status
        //variable is set to something different than null.
        $closed_status_sql = '';
        if ($closed_status !== null) {
            $closed_status_sql    = ' AND (resource_requests.closed = :status) ';
            $sql_params['status'] = strval($closed_status);
        }

        //Then we build the snipped for excluded request IDs, if specified.
        $excluded_request_ids_sql = '';
        if (is_array($excluded_request_ids) && count($excluded_request_ids)) {
            $excluded_request_ids_sql   = ' AND resource_requests.id NOT IN ( :excluded_ids ) ';
            $sql_params['excluded_ids'] = $excluded_request_ids;
        }

        //Now we build the SQL snippet for the time intervals.
        //These are repeated four times in the query below.
        //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 = '';
        if ($time_ranges) {
            $time_sql = 'AND (';

            $i = 1;
            foreach ($time_ranges as $time_range) {
                if ($i > 1) {
                    $time_sql .= ' OR ';
                }
                $time_sql .= sprintf('BEGIN < :end%d AND END > :begin%d ', $i, $i);

                $sql_params[('begin' . $i)] = $time_range['begin'];
                $sql_params[('end' . $i)]   = $time_range['end'];

                $i++;
            }

            $time_sql .= ') ';
        }

        //Check if the request has a start and end timestamp set or if it belongs
        //to a date, a metadate or a course.
        //This is done in the rest of the SQL query:

        // FIXME this subselect looks unnecessarily complex
                SELECT id FROM resource_requests
                WHERE
                resource_id = :resource_id
        '
            . str_replace(
                ['BEGIN', 'END'],
                ['(CAST(begin AS SIGNED) - preparation_time)', 'end'],
                $time_sql
            )
            . $closed_status_sql
            . '
                UNION
                SELECT id FROM resource_requests
                INNER JOIN termine USING (termin_id)
                WHERE
                resource_id = :resource_id
                   '
            . str_replace(
                ['BEGIN', 'END'],
                [
                    '(CAST(termine.date AS SIGNED) - resource_requests.preparation_time)',
                    'termine.end_time'
                ],
                $time_sql
            )
            . $closed_status_sql
            . '
                UNION
                SELECT id FROM resource_requests
                INNER JOIN termine USING (metadate_id)
                WHERE
                resource_id = :resource_id
                   '
            . str_replace(
                ['BEGIN', 'END'],
                [
                    '(CAST(termine.date AS SIGNED) - resource_requests.preparation_time)',
                    'termine.end_time'
                ],
                $time_sql
            )
            . $closed_status_sql
            . '
            UNION
            SELECT id FROM resource_requests
            INNER JOIN termine
            ON resource_requests.course_id = termine.range_id
            WHERE
            resource_id = :resource_id
                   '
            . str_replace(
                ['BEGIN', 'END'],
                [
                    '(CAST(termine.date AS SIGNED) - resource_requests.preparation_time)',
                    'termine.end_time'
                ],
                $time_sql
            )
            . $closed_status_sql
            . '
            . $excluded_request_ids_sql;
        $request_ids = DBManager::get()->fetchFirst($whole_sql, $sql_params);
        $whole_sql = "resource_requests.id IN(:request_ids)";
        $sql_params = ['request_ids' => $request_ids];
        if ($additional_conditions) {
            $whole_sql .= ' AND ' . $additional_conditions;
            if ($additional_parameters) {
                $sql_params = array_merge($sql_params, $additional_parameters);
            }
        }
        $whole_sql .= ' ORDER BY mkdate ASC';

        return [
            'sql'    => $whole_sql,
            'params' => $sql_params
        ];
    }

    /**
     * Retrieves all resource requests for the given resource and
     * time range. By default, all requests are returned.
     * To get only open or closed requests set the $closed_status 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 mixed $closed_status An optional status for the closed column in the
     *     database. By default this is set to null which means that
     *     resource requests are not filtered by the status column field.
     *     A value of 0 means only open requests are retrived.
     *     A value of 1 means only closed requests are retrieved.
     *
     * @param array $excluded_request_ids An array of strings representing
     *     resource request IDs. IDs specified in this array are excluded from
     *     the search.
     * @return ResourceRequest[] 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 = [],
        $closed_status = null,
        $excluded_request_ids = [],
        $additional_conditions = '',
        $additional_parameters = []
    )
    {
        //Build the SQL query and the parameter array.

        $sql_data = self::buildResourceAndTimeRangesSqlQuery(
            $resource,
            $time_ranges,
            $closed_status,
            $excluded_request_ids,
            $additional_conditions,
            $additional_parameters
        );

        //Call findBySql:
        return self::findBySql($sql_data['sql'], $sql_data['params']);
    }

    public static function countByResourceAndTimeRanges(
        Resource $resource,
        $time_ranges = [],
        $closed_status = null,
        $excluded_request_ids = [],
        $additional_conditions = '',
        $additional_parameters = []
    )
    {
        $sql_data = self::buildResourceAndTimeRangesSqlQuery(
            $resource,
            $time_ranges,
            $closed_status,
            $excluded_request_ids,
            $additional_conditions,
            $additional_parameters
        );

        return self::countBySql($sql_data['sql'], $sql_data['params']);
    }

    public static function findByCourse($course_id)
    {
        return self::findOneBySql(
            "termin_id = '' AND metadate_id = '' AND course_id = :course_id",
            [
                'course_id' => $course_id
            ]
        );
    }

    public static function findByDate($date_id)
    {
        return self::findOneBySql(
            'termin_id = :date_id',
            [
                'date_id' => $date_id
            ]
        );
    }

    public static function findByMetadate($metadate_id)
    {
        return self::findOneBySql(
            'metadate_id = :metadate_id',
            [
                'metadate_id' => $metadate_id
            ]
        );
    }

    public static function existsByCourse($course_id, $request_is_open = false)
    {
        $sql = '';
        if ($request_is_open) {
            $sql .= "closed = '0' AND ";
        }

        $request = self::findOneBySql(
            $sql . "termin_id = '' AND metadate_id = '' AND course_id = :course_id",
            [
                'course_id' => $course_id
            ]
        );

        if ($request) {
            return $request->id;
        } else {
            return false;
        }
    }

    public static function existsByDate($date_id, $request_is_open = false)
    {
        $sql = '';
        if ($request_is_open) {
            $sql .= "closed = '0' AND ";
        }

        $request = self::findOneBySql(
            $sql . "termin_id = :date_id",
            [
                'date_id' => $date_id
            ]
        );

        if ($request) {
            return $request->id;
        } else {
            return false;
        }
    }

    public static function existsByMetadate($metadate_id, $request_is_open = false)
    {
        $sql = '';
        if ($request_is_open) {
            $sql .= "closed = '0' AND ";
        }

        $request = self::findOneBySql(
            $sql . "metadate_id = :metadate_id",
            [
                'metadate_id' => $metadate_id
            ]
        );

        if ($request) {
            return $request->id;
        } else {
            return false;
        }
    }

    /**
     * A callback method that creates a Stud.IP log entry
     * when a new request has been made.
     */
    public function cbLogNewRequest()
    {
        $this->sendNewRequestMail();
        StudipLog::log('RES_REQUEST_NEW', $this->course_id, $this->resource_id, $this->getLoggingInfoText());
    }

    /**
     * A callback method that send a mail
     * when a new request has been udpated.
     */
    public function cbAfterStore()
    {
        if ($this->isFieldDirty('closed')) {
            if ($this->closed == 3) {
                $this->sendRequestDeniedMail();
                StudipLog::log('RES_REQUEST_DENY', $this->course_id, $this->resource_id, $this->getLoggingInfoText());
            } elseif ($this->closed == 1 || $this->closed == 2) {
                StudipLog::log('RES_REQUEST_RESOLVE', $this->course_id, $this->resource_id, $this->getLoggingInfoText());
            }
        } else {
            StudipLog::log('RES_REQUEST_UPDATE', $this->course_id, $this->resource_id, $this->getLoggingInfoText());
    public function cbAfterDelete()
    {
        StudipLog::log('RES_REQUEST_DEL', $this->course_id, $this->resource_id, $this->getLoggingInfoText());
    }

    /**
     * This validation method is called before storing an object.
     */
    public function validate()
    {
        if (!$this->resource_id && !$this->category_id) {
            throw new Exception(
                _('Eine Anfrage muss einer konkreten Ressource oder deren Kategorie zugewiesen sein!')
            );
        }
    }

    public function getDerivedClassInstance()
    {
        if (!$this->resource) {
            //We cannot determine a derived class.
            return $this;
        }
        $class_name = $this->resource->class_name;

        if ($class_name == 'Resource') {
            //This is already the correct class.
            return $this;
        }

        if (is_subclass_of($class_name, 'Resource')) {
            //Now we append 'Request' to the class name:
            $class_name         = $class_name . 'Request';
            $converted_resource = $class_name::buildExisting(
                $this->toRawArray()
            );
            return $converted_resource;
        } else {
            //$class_name does not contain the name of a subclass
            //of Resource. That's an error!
            throw new NoResourceClassException(
                sprintf(
                    _('Die Klasse %1$s ist keine Spezialisierung der Ressourcen-Kernklasse!'),
                    $class_name
                )
            );
        }
    }

    /**
     * Sets the range fields (termin_id, metadate_id, course_id)
     * or the ResourceRequestAppointment objects related to this request
     * according to the range type and its range-IDs specified as parameters
     * for this method. The ResourceRequest object is not stored after
     * setting the fields / related objects.
     *
     * @param string $range_type The range type for this request. One of
     *     the following: 'date', 'cycle', 'course' or 'date-multiple'.
     *
     * @param array $range_ids An array of range-IDs to be set for the
     *     specified range type. This is mostly an array of size one
     *     since the fields termin_id, metadate_id and course_id only
     *     accept one ID. The range type 'date-multiple' accepts multiple
     *     IDs.
     *
     * @return void No return value.
     */
    public function setRangeFields($range_type = '', $range_ids = [])
    {
        if ($range_type == 'date') {
            $this->termin_id   = $range_ids[0];
            $this->metadate_id = '';
        } elseif ($range_type == 'cycle') {
            $this->termin_id   = '';
            $this->metadate_id = $range_ids[0];
        } elseif ($range_type == 'date-multiple') {
            $this->termin_id   = '';
            $this->metadate_id = '';
            $appointments      = [];
            foreach ($range_ids as $range_id) {
                $app                 = new ResourceRequestAppointment();
                $app->appointment_id = $range_id;
                $appointments[]      = $app;
            }
            $this->appointments = $appointments;
        } elseif ($range_type == 'course') {
            $this->termin_id   = '';
            $this->metadate_id = '';
            $this->course_id   = $range_ids[0];
        }
    }

    /**
     * Closes the requests and sends out notification mails.
     * If the request is closed and a resource has been booked,
     * it can be passed as parameter to be included in the notification mails.
     *
     * @param bool $notify_lecturers Whether to notify lecturers of a course
     *     (true) or not (false). Defaults to false. Note that this parameter
     *     is only useful in case the request is bound to a course, either
     *     directly or via a course date or a course cycle date.
     *
     * @param ResourceBooking $bookings The resource bookings that have been
     *     created from this request.
     * @return bool @TODO
     */
    public function closeRequest($notify_lecturers = false, $bookings = [])
    {
        if ($this->closed >= 2) {
            //The request has already been closed.
            return true;
        }

        $this->closed = 1;
        if ($this->isDirty()) {
            $this->store();
        }

        //Now we send the confirmation mail to the requester:
        $this->sendCloseRequestMailToRequester($bookings);

        if ($notify_lecturers) {
            $this->sendCloseRequestMailToLecturers($bookings);
        }

        //Sending successful: The request is closed.
        $this->closed = 2;
        if ($this->isDirty()) {
            return $this->store();
        }
        return true;
    }

    /**
     * Returns the resource requests whose time ranges overlap
     * with those of this resource request.
     *
     * @return ResourceRequest[] An array of ResourceRequest objects.
     */
    public function getOverlappingRequests()
    {
        if ($this->resource) {
            return self::findByResourceAndTimeRanges(
                $this->resource,
                $this->getTimeIntervals(true),
                0,
                [$this->id]
            );
        }
        return [];
    }

    /**
     * Counts the resource requests whose time ranges overlap
     * with those of this resource request.
     *
     * @return int The amount of overlapping resource requests.
     */
    public function countOverlappingRequests()
    {
        if ($this->resource) {
            return self::countByResourceAndTimeRanges(
                $this->resource,
                $this->getTimeIntervals(true),
                0,
                [$this->id]
            );
        }
        return 0;
    }

    /**
     * Returns the resource bookings whose time ranges overlap
     * with those of this resource request.
     *
     * @return ResourceBooking[] An array of ResourceBooking objects.
     */
    public function getOverlappingBookings()
    {
        if ($this->resource) {
            return ResourceBooking::findByResourceAndTimeRanges(
                $this->resource,
                $this->getTimeIntervals(true),
                [0, 2]
            );
        }
        return [];
    }

    /**
     * Counts the resource bookings whose time ranges overlap
     * with those of this resource request.
     *
     * @return int The amount of overlapping resource bookings.
     */
    public function countOverlappingBookings()
    {
        if ($this->resource) {
            return ResourceBooking::countByResourceAndTimeRanges(
                $this->resource,
                $this->getTimeIntervals(true),
                [0, 2]
            );
        }
        return 0;
    }

    /**
     * Returns the repetion interval if regular appointments are used
     * for this request.
     *
     * @return DateInterval|null In case regular appointments are used
     *     for this request a DateInterval is returned.
     *     Otherwise null is returned.
     */
    public function getRepetitionInterval()
    {
        if ($this->metadate_id) {
            //It is a set of regular appointments.
            //We just have to compute the time difference between the first
            //two appointments to get the interval.

            $first_date  = $this->cycle->dates[0];
            $second_date = $this->cycle->dates[1];

            if (!$first_date || !$second_date) {
                //Either only one date is in the set of regular appointments
                //or there is a database error. We cannot continue.
                return null;
            }

            $first_datetime = new DateTime();
            $first_datetime->setTimestamp($first_date->date);
            $second_datetime = new DateTime();
            $second_datetime->setTimestamp($second_date->date);

            return $first_datetime->diff($second_datetime);
        }

        return null;
    }

    public function getStartDate()
    {
        $start_date = new DateTime();
        if (count($this->appointments)) {
            $start_date->setTimestamp($this->appointments[0]->appointment->date);
            return $start_date;
        } elseif ($this->termin_id) {
            $start_date->setTimestamp($this->date->date);
            return $start_date;
        } elseif ($this->metadate_id) {
            $start_date->setTimestamp($this->cycle->dates[0]->date);
            return $start_date;
        } elseif ($this->course_id) {
            $start_date = new DateTime();
            $start_date->setTimestamp($this->course->dates[0]->date);
            return $start_date;
        } elseif ($this->begin) {
            $start_date->setTimestamp($this->begin);
            return $start_date;
        }
        return null;
    }

    public function getEndDate()
    {
        $end_date = new DateTime();
        if (count($this->appointments)) {
            $end_date->setTimestamp($this->appointments->last()->appointment->end_time);
            return $end_date;
        } elseif ($this->termin_id) {
            $end_date->setTimestamp($this->date->end_time);
            return $end_date;
        } elseif ($this->metadate_id) {
            $end_date->setTimestamp($this->cycle->dates->last()->end_time);
            return $end_date;
        } elseif ($this->course_id) {
            $end_date = new DateTime();
            $end_date->setTimestamp($this->course->dates->last()->end_time);
            return $end_date;
        } elseif ($this->end) {
            $end_date->setTimestamp($this->end);
            return $end_date;
        }
        return null;
    }

    public function getStartSemester()
    {
        $start_date = $this->getStartDate();
        if ($start_date instanceof DateTime) {
            return Semester::findByTimestamp($start_date->getTimestamp());
        }
        return null;
    }

    public function getEndSemester()
    {
        $end_date = $this->getEndDate();
        if ($end_date instanceof DateTime) {
            return Semester::findByTimestamp($end_date->getTimestamp());
        }
        return null;
    }

    public function getRepetitionEndDate()
    {
        $repetition_interval = $this->getRepetitionInterval();

        if (!$repetition_interval) {
            //There is no repetition.
            return null;
        }

        return $this->getEndDate();
    }

    /**
     * Retrieves the time intervals by looking at metadate objects
     * and other time interval sources and returns them grouped by metadate.
     * @param bool $with_preparation_time @TODO
     * @return mixed[][][] A three-dimensional array with
     *     the following structure:
     *     - The first dimension has the metadate-id as index. For single dates
     *       an empty string is used as index.
     *     - The second dimension contains two elements:
     *       - 'metadate' => The metadate object. This is only set, if the
     *                       request is for a metadate.
     *       - 'intervals' => The time intervals.
     *     - The third dimension contains a time interval
     *       in the following format:
     *       [
     *           'begin' => The begin timestamp
     *           'end' => The end timestamp
     *           'range' => The name of the range class that provides the range_id.
     *               This is usually the name of the SORM class.
     *           'range_id' => The ID of the single date or ResourceRequestAppointment.
     *       ]
     */
    public function getGroupedTimeIntervals($with_preparation_time = false, $with_past_intervals = true)
        $now = time();
        if (count($this->appointments)) {
            $time_intervals = [
                '' => [
                    'metadate'  => null,
                    'intervals' => []
                ]
            ];
            foreach ($this->appointments as $appointment) {
                if (!$with_past_intervals && $appointment->appointment->end_time < $now) {
                    continue;
                }
                if ($with_preparation_time) {
                    $interval = [
                        'begin' => $appointment->appointment->date - $this->preparation_time,
                        'end'   => $appointment->appointment->end_time
                    ];
                } else {
                    $interval = [
                        'begin' => $appointment->appointment->date,
                        'end'   => $appointment->appointment->end_time
                    ];
                }

                $date = CourseDate::find($appointment->appointment_id);
                $interval['range']                 = 'CourseDate';
                $interval['range_id']              = $appointment->appointment_id;
                $interval['booked_room']           = $date->room_booking->resource_id;
                $interval['booking_id']            = $date->room_booking->id;
                $time_intervals['']['intervals'][] = $interval;
            }

            if (empty($time_intervals['']['intervals'])) {
                return [];
            } else {
                return $time_intervals;
            }
        } elseif ($this->termin_id) {
            if (!$with_past_intervals && $this->date->end_time < $now) {
                return [];
            }
            if ($with_preparation_time) {
                $interval = [
                    'begin' => $this->date->date - $this->preparation_time,
                    'end'   => $this->date->end_time
                ];
            } else {
                $interval = [
                    'begin' => $this->date->date,
                    'end'   => $this->date->end_time
                ];
            }

            $date = CourseDate::find($this->termin_id);
            $interval['range']       = 'CourseDate';
            $interval['range_id']    = $this->termin_id;
            $interval['booked_room'] = $date->room_booking->resource_id;
            $interval['booking_id']  = $date->room_booking->id;

            if (!empty($interval)) {
                return [
                    '' => [
                        'metadate'  => null,
                        'intervals' => [$interval]
                    ]
                ];
            } else {
                return [];
            }
        } elseif ($this->metadate_id) {
            $time_intervals = [
                $this->metadate_id => [
                    'metadate'  => $this->cycle,
                    'intervals' => []
                ]
            ];
            foreach ($this->cycle->dates as $date) {
                if (!$with_past_intervals && $date->end_time < $now) {
                    continue;
                }
                if ($with_preparation_time) {
                    $interval = [
                        'begin' => $date->date - $this->preparation_time,
                        'end'   => $date->end_time
                    ];
                } else {
                    $interval = [
                        'begin' => $date->date,
                        'end'   => $date->end_time
                    ];
                }
                $interval['range']                                 = 'CourseDate';
                $interval['range_id']                              = $date->id;
                $interval['booked_room']                           = $date->room_booking->resource_id;
                $interval['booking_id']                            = $date->room_booking->id;
                $time_intervals[$this->metadate_id]['intervals'][] = $interval;
            }
            return $time_intervals;
        } elseif ($this->course_id) {
            $time_intervals = [];