diff options
Diffstat (limited to 'lib/models/resources/ResourceRequest.php')
| -rw-r--r-- | lib/models/resources/ResourceRequest.php | 2468 |
1 files changed, 2468 insertions, 0 deletions
diff --git a/lib/models/resources/ResourceRequest.php b/lib/models/resources/ResourceRequest.php new file mode 100644 index 0000000..df77b19 --- /dev/null +++ b/lib/models/resources/ResourceRequest.php @@ -0,0 +1,2468 @@ +<?php + +/** + * ResourceRequest.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 + * + * 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. + * + * @property string $id database column + * @property string $course_id database column + * @property string $termin_id database column + * @property string $metadate_id database column + * @property string $user_id database column + * @property string $last_modified_by database column + * @property string $resource_id database column + * @property string|null $category_id database column + * @property string|null $comment database column + * @property string|null $reply_comment database column + * @property string $reply_recipients database column + * @property int $closed database column + * @property int|null $mkdate database column + * @property int|null $chdate database column + * @property int $begin database column + * @property int $end database column + * @property int $preparation_time database column + * @property int $marked database column + * @property SimpleORMapCollection|ResourceRequestProperty[] $properties has_many ResourceRequestProperty + * @property SimpleORMapCollection|ResourceRequestAppointment[] $appointments has_many ResourceRequestAppointment + * @property Resource $resource belongs_to Resource + * @property ResourceCategory|null $category belongs_to ResourceCategory + * @property User $user belongs_to User + * @property User $last_modifier belongs_to User + * @property Course $course belongs_to Course + * @property SeminarCycleDate $cycle belongs_to SeminarCycleDate + * @property CourseDate $date belongs_to CourseDate + */ +class ResourceRequest extends SimpleORMap implements PrivacyObject, Studip\Calendar\EventSource +{ + const MARK_NONE = 0; + const MARK_RED = 1; + const MARK_YELLOW = 2; + const MARK_GREEN = 3; + + const REPLY_REQUESTER = 'requester'; + const REPLY_LECTURER = 'lecturer'; + + const STATE_OPEN = 0; // room-request is open + const STATE_PENDING = 1; // room-request has been processed, but no confirmation has been sent + const STATE_CLOSED = 2; // room-request has been processed and a confirmation has been sent + const STATE_DECLINED = 3; // room-request has been declined + + /** + * The amount of defined marking states. + */ + const MARKING_STATES = 4; + + protected static function configure($config = []) + { + $config['db_table'] = 'resource_requests'; + + $config['belongs_to']['resource'] = [ + 'class_name' => Resource::class, + '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'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id', + 'assoc_func' => 'find' + ]; + + $config['belongs_to']['last_modifier'] = [ + 'class_name' => User::class, + 'foreign_key' => 'last_modified_by', + 'assoc_func' => 'find' + ]; + + $config['belongs_to']['course'] = [ + 'class_name' => Course::class, + 'foreign_key' => 'course_id', + 'assoc_func' => 'find' + ]; + + $config['belongs_to']['cycle'] = [ + 'class_name' => SeminarCycleDate::class, + 'foreign_key' => 'metadate_id' + ]; + + $config['belongs_to']['date'] = [ + 'class_name' => CourseDate::class, + '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 = ? ORDER BY mkdate ASC', + [self::STATE_OPEN] + ); + } + + /** + * 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 + $whole_sql = ' + 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 + . ' + GROUP BY id + ' + . $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) + { + $parameters = [':course_id' => $course_id]; + + $sql = ''; + if ($request_is_open) { + $sql .= "closed = :closed_state AND "; + $parameters[':closed_state'] = self::STATE_OPEN; + } + + $request = self::findOneBySql( + $sql . "termin_id = '' AND metadate_id = '' AND course_id = :course_id", + $parameters + ); + + if ($request) { + return $request->id; + } else { + return false; + } + } + + public static function existsByDate($date_id, $request_is_open = false) + { + $parameters = [':date_id' => $date_id]; + + $sql = ''; + if ($request_is_open) { + $sql .= "closed = :closed_state AND "; + $parameters[':closed_state'] = self::STATE_OPEN; + } + + $request = self::findOneBySql( + $sql . "termin_id = :date_id", + $parameters + ); + + if ($request) { + return $request->id; + } else { + return false; + } + } + + public static function existsByMetadate($metadate_id, $request_is_open = false) + { + $parameters = [':metadate_id' => $metadate_id]; + + $sql = ''; + if ($request_is_open) { + $sql .= "closed = :closed_state AND "; + $parameters[':closed_state'] = self::STATE_OPEN; + } + + $request = self::findOneBySql( + $sql . "metadate_id = :metadate_id", + $parameters + ); + + 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 == self::STATE_DECLINED) { + $this->sendRequestDeniedMail(); + StudipLog::log('RES_REQUEST_DENY', $this->course_id, $this->resource_id, $this->getLoggingInfoText()); + } elseif ($this->closed == self::STATE_PENDING || $this->closed == self::STATE_CLOSED) { + 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'; + return $class_name::buildExisting( + $this->toRawArray() + ); + } 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 == self::STATE_CLOSED + || $this->closed == self::STATE_DECLINED + ) { + //The request has already been closed. + return true; + } + + $this->closed = self::STATE_PENDING; + 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 = self::STATE_CLOSED; + 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), + self::STATE_OPEN, + [$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), + self::STATE_OPEN, + [$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), + [ResourceBooking::TYPE_NORMAL, ResourceBooking::TYPE_LOCK] + ); + } + 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), + [ResourceBooking::TYPE_NORMAL, ResourceBooking::TYPE_LOCK] + ); + } + 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) > 0) { + $start_date->setTimestamp($this->appointments->first()->appointment->date); + return $start_date; + } + + if ($this->termin_id) { + $start_date->setTimestamp($this->date->date); + return $start_date; + } + + if (isset($this->cycle) && count($this->cycle->dates) > 0) { + $first_date = $this->cycle->dates->first(); + if ($this->metadate_id && isset($first_date->date)) { + $start_date->setTimestamp($first_date->date); + return $start_date; + } + + if ($this->course_id && isset($first_date->date)) { + $start_date->setTimestamp($first_date->date); + return $start_date; + } + } + + if ($this->begin) { + $start_date->setTimestamp($this->begin); + return $start_date; + } + + return null; + } + + public function getEndDate() + { + $end_date = new DateTime(); + if (count($this->appointments) > 0) { + $end_date->setTimestamp($this->appointments->last()->appointment->end_time); + return $end_date; + } + + if ($this->termin_id) { + $end_date->setTimestamp($this->date->end_time); + return $end_date; + } + + if ($this->metadate_id) { + $date = $this->cycle->dates->last(); + if (!isset($date)) { + return null; + } + + $end_date->setTimestamp($this->cycle->dates->last()->end_time); + return $end_date; + } + + if ($this->course_id) { + $date = $this->course->dates->last(); + if (!isset($date)) { + return null; + } + + $end_date->setTimestamp($this->course->dates->last()->end_time); + return $end_date; + } + + if ($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 = []; + if ($this->course->cycles) { + foreach ($this->course->cycles as $cycle) { + $time_intervals[$cycle->id] = [ + 'metadate' => $cycle, + 'intervals' => [] + ]; + if ($cycle->dates) { + foreach ($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[$cycle->id]['intervals'][] = $interval; + } + } + } + } + if ($this->course->dates) { + $time_intervals[''] = [ + 'metadate' => null, + 'intervals' => [] + ]; + foreach ($this->course->dates as $date) { + if (!$with_past_intervals && $date->end_time < $now) { + continue; + } + if ($date->cycle instanceof SeminarCycleDate) { + //Metadates are already handled above. + 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['']['intervals'][] = $interval; + } + + if (empty($time_intervals['']['intervals'])) { + unset($time_intervals['']); + } + } + return $time_intervals; + } elseif ($this->begin && $this->end) { + if (!$with_past_intervals && $this->end < $now) { + return []; + } + if ($with_preparation_time) { + $interval = [ + 'begin' => $this->begin - $this->preparation_time, + 'end' => $this->end + ]; + } else { + $interval = [ + 'begin' => $this->begin, + 'end' => $this->end + ]; + } + $interval['range'] = 'User'; + $interval['range_id'] = $this->user_id; + + return [ + '' => [ + 'metadate' => null, + 'intervals' => [$interval] + ] + ]; + } else { + return []; + } + } + + /** + * Retrieves the time intervals for this request. + * + * @param bool $with_preparation_time Whether the preparation time + * of the request shall be prepended to the begin timestamp (true) + * or whether it should not be included at all (false). + * Defaults to false. + * + * @param bool $with_range Whether to include data of the Stud.IP range + * and its corresponding ID to the request (true) or not (false). + * Defaults to false. + * + * @param bool $with_past_intervals Whether to include past intervals (true) + * or only include intervals from the current time and the future (false). + * Defaults to true. + * + * @return string[][] A two-dimensional array of unix timestamps. + * The first dimension contains one entry for each date, + * the second dimension contains the start and end timestamp + * for the date. + * The second dimension uses the array keys 'begin' and 'end' + * for start and end date. + * If the @with_range parameter is set to true, the second array + * dimension also contains the key 'range' for specifying the + * range type and 'range_id' for specifying the ID of the + * range object. + * The range can be "CourseDate", "ResourceRequestAppointment" + * or "User". The last two can only be present for simple requests + * that are not bound to a course. The range "CourseDate" + * can only occur on course-bound requests. + */ + public function getTimeIntervals($with_preparation_time = false, $with_range = false, $with_past_intervals = true) + { + $now = time(); + if (count($this->appointments)) { + $time_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 + ]; + } + if ($with_range) { + $date = CourseDate::find($appointment->appointment_id); + + $interval['range'] = ResourceRequestAppointment::class; + $interval['range_id'] = $appointment->appointment_id; + $interval['booked_room'] = $date->room_booking->resource_id ?? null; + $interval['booking_id'] = $date->room_booking->id ?? null; + + } + $time_intervals[] = $interval; + } + 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 + ]; + } + if ($with_range) { + $interval['range'] = CourseDate::class; + $interval['range_id'] = $this->termin_id; + $interval['booked_room'] = $this->date->room_booking->resource_id ?? null; + $interval['booking_id'] = $this->date->room_booking->id ?? null; + } + return [$interval]; + } elseif ($this->metadate_id) { + $time_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 + ]; + } + if ($with_range) { + $interval['range'] = CourseDate::class; + $interval['range_id'] = $date->id; + $interval['booked_room'] = $date->room_booking->resource_id ?? null; + $interval['booking_id'] = $date->room_booking->id ?? null; + } + $time_intervals[] = $interval; + } + return $time_intervals; + } elseif ($this->course_id) { + $time_intervals = []; + if ($this->course->dates) { + foreach ($this->course->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 + ]; + } + if ($with_range) { + $interval['range'] = CourseDate::class; + $interval['range_id'] = $date->id; + $interval['booked_room'] = $date->room_booking->resource_id ?? null; + $interval['booking_id'] = $date->room_booking->id ?? null; + } + $time_intervals[] = $interval; + } + } + return $time_intervals; + } elseif ($this->begin && $this->end) { + if (!$with_past_intervals && $this->end < $now) { + return []; + } + if ($with_preparation_time) { + $interval = [ + 'begin' => $this->begin - $this->preparation_time, + 'end' => $this->end + ]; + } else { + $interval = [ + 'begin' => $this->begin, + 'end' => $this->end + ]; + } + if ($with_range) { + $interval['range'] = 'User'; + $interval['range_id'] = $this->user_id; + } + return [$interval]; + } else { + return []; + } + } + + + /** + * Returns a string representation of the time intervals for this request. + */ + public function getTimeIntervalStrings() + { + $strings = []; + $intervals = $this->getTimeIntervals(false, true); + foreach ($intervals as $interval) { + $room = ''; + + if ($interval['range'] === 'CourseDate') { + $date = call_user_func([$interval['range'], 'find'], $interval['range_id']); + if ($date->room_booking) { + $room_obj = Room::find($date->room_booking->resource_id); + if ($room_obj) { + $room = $room_obj->name; + } + } + } + + $same_day = date('Ymd', $interval['begin']) === date('Ymd', $interval['end']); + if ($same_day) { + $strings[] = strftime('%a. %x %R', $interval['begin']) + . ' - ' . strftime('%R', $interval['end']) + . ($room ? ', '. $room : ''); + } else { + $strings[] = strftime('%a. %x %R', $interval['begin']) + . ' - ' . strftime('%a %x %R', $interval['end']) + . ($room ? ', '. $room : ''); + } + + } + return $strings; + } + + + /** + * Filters the time intervals for this request + * by a specified time range. + * + * @see ResourceRequest::getTimeIntervals for the return format. + */ + public function getTimeIntervalsInTimeRange(DateTime $begin, DateTime $end) + { + $all_time_intervals = $this->getTimeIntervals(); + + $included_intervals = []; + foreach ($all_time_intervals as $interval) { + $interval_in_range = ( + ( + $interval['begin'] >= $begin->getTimestamp() + && + $interval['begin'] <= $end->getTimestamp() + ) + || + ( + $interval['end'] >= $begin->getTimestamp() + && + $interval['end'] <= $end->getTimestamp() + ) + ); + if ($interval_in_range) { + $included_intervals[] = $interval; + } + } + + return $included_intervals; + } + + + /** + * Returns a string representation of the ResourceRequest's type. + */ + public function getType() + { + if (count($this->appointments)) { + return 'appointments'; + } elseif ($this->termin_id) { + return 'date'; + } elseif ($this->metadate_id) { + return 'cycle'; + } elseif ($this->course_id) { + return 'course'; + } + return null; + } + + /** + * Returns a string representation of the status of the ResourceRequest. + */ + public function getStatus() + { + switch ($this->closed) { + case self::STATE_OPEN: + return 'open'; + case self::STATE_PENDING: + return 'pending'; + case self::STATE_CLOSED: + return 'closed'; + case self::STATE_DECLINED: + return 'declined'; + default: + return ''; + } + } + + + /** + * Returns a textual representation of the status of the ResourceRequest. + */ + public function getStatusText() + { + if ($this->isNew()) { + return _('Diese Anfrage wurde noch nicht gespeichert.'); + } + if ($this->closed == self::STATE_OPEN) { + return _('Die Anfrage wurde noch nicht bearbeitet.'); + } else if ($this->closed == self::STATE_DECLINED) { + return _('Die Anfrage wurde bearbeitet und abgelehnt.'); + } else { + return _('Die Anfrage wurde bearbeitet.'); + } + } + + + /** + * Returns a textual representation of the dates for which the request + * has been created. + * + * @param bool $as_array True, if an array with a string for each date + * (single or cycle date) shall be returned, false otherwise. + * + * @returns string|array Depending on the parameter $as_array, the text + * is returned as one string or as an array of strings for each date + * (single or cycle date). + */ + public function getDateString($as_array = false, $with_past_intervals = true) + { + $now = time(); + $strings = []; + $resource_name = ''; + if (count($this->appointments)) { + $parts = []; + foreach ($this->appointments as $rra) { + if (!$with_past_intervals && $rra->appointment->end_time < $now) { + continue; + } + if ($rra->appointment) { + $parts[] = $rra->appointment->getFullName('include-room'); + } + } + $strings[] = implode('; ', $parts); + } elseif ($this->termin_id) { + if ($this->date) { + if ($with_past_intervals || $this->date->end_time >= $now) { + $strings[] = $this->date->getFullName('include-room'); + } + } + } elseif ($this->metadate_id) { + if ($this->cycle) { + $this->cycle->dates->filter(function($date) use($with_past_intervals, $now) { + return $with_past_intervals || $date->end_time >= $now; + })->map(function($date) use(&$strings) { + $strings[] = $date->getFullName('include-room'); + }); + } + } elseif ($this->course_id) { + $course = new Seminar($this->course_id); + $strings[] = $course->getDatesTemplate('dates/seminar_html_roomplanning', + [ + 'shrink' => false, + 'show_room' => true, + 'with_past_intervals' => $with_past_intervals + ] + ); + } elseif ($this->begin && $this->end) { + $begin_date = date('Ymd', $this->begin); + $end_date = date('Ymd', $this->end); + if($this->resource) { + $resource_name = htmlReady($this->resource->getFullName()); + } + if ($begin_date == $end_date) { + $strings[] = strftime('%a., %x, %R', $this->begin) . ' - ' + . strftime('%R', $this->end) . ' ' . $resource_name; + } else { + //Begin and end are on differnt dates + $strings[] = strftime('%a., %x, %R', $this->begin) . ' - ' + . strftime('%a., %x, %R', $this->end) . ' ' . $resource_name; + } + } + + if ($as_array) { + return $strings; + } else { + return implode(';', $strings); + } + } + + + /** + * Returns a human-readable string describing the type of the request. + * + * @param bool $short If this parameter is set to true, only the + * type of the request is returned without any information about the + * appointments. Otherwise, appointment information like the + * date or the repetition are appended. Defaults to false. + * @return string + */ + public function getTypeString($short = false) + { + if (count($this->appointments) > 1) { + if ($short) { + return _('Einzeltermine'); + } else { + return sprintf(_('Einzeltermine (%sx)'), count($this->appointments)); + } + } elseif (count($this->appointments) === 1) { + $date = $this->appointments[0]->appointment; + if ($short || !$date) { + return _('Einzeltermin'); + } else { + return sprintf(_('Einzeltermin (%s)'), $date->getFullName()); + } + } elseif ($this->date) { + if ($short) { + return _('Einzeltermin'); + } else { + return sprintf(_('Einzeltermin (%s)'), $this->date->getFullName()); + } + } elseif ($this->cycle) { + if ($short) { + return _('Regelmäßige Termine'); + } else { + return sprintf( + _('Regelmäßige Termine (%s)'), + $this->cycle->toString('full') + ); + } + } elseif ($this->course) { + if ($short) { + return _('Alle Termine der Veranstaltung'); + } else { + return sprintf( + _('Alle Termine der Veranstaltung (%sx)'), + count($this->course->dates) + ); + } + } else { + return _('Einfache Anfrage'); + } + } + + + /** + * Returns an array of date objects which are affected + * by this ResourceRequest. + */ + public function getAffectedDates() + { + $dates = []; + switch ($this->getType()) { + case 'date': + $dates[] = $this->date; + break; + case 'cycle': + $dates = $this->cycle->dates->getArrayCopy(); + break; + case 'course': + $dates = $this->course->dates->getArrayCopy(); + break; + } + return $dates; + } + + + /** + * @param array $excluded_property_names + * Returns all resource property definitions for all properties + * which can be applied for this ResourceRequest by looking at the + * Resource category. If no resource category ID is set for the request + * an empty array is returned. + */ + public function getAvailableProperties($excluded_property_names = []) + { + if (!$this->category_id) { + //Without a category-ID we cannot find any property! + return []; + } + if (count($excluded_property_names)) { + return ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties + USING (property_id) + WHERE requestable = '1' AND category_id = :category_id + AND name NOT IN ( :excluded_property_names )", + [ + 'category_id' => $this->category_id, + 'excluded_property_names' => $excluded_property_names + ] + ); + } else { + return ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties + USING (property_id) + WHERE requestable = '1' AND category_id = :category_id", + [ + 'category_id' => $this->category_id + ] + ); + } + } + + + /** + * Returns a "compressed" array of resource request properties. + * @param array $excluded_property_names + * @return array An associative array where the keys represent the + * property names and the values represent the property states. + * Note that the value can be an array in case of range properties. + */ + public function getPropertyData($excluded_property_names = []) + { + $data = []; + foreach ($this->properties as $property) { + if ($property->definition->range_search) { + //Assume that a minimum value is requested: + $data[$property->name] = [$property->state]; + } else { + $data[$property->name] = $property->state; + } + } + return $data; + } + + /** + * @param $name + * @return bool + */ + public function propertyExists($name) + { + $db = DBManager::get(); + + $exists_stmt = $db->prepare( + "SELECT TRUE FROM resource_request_properties + INNER JOIN resource_property_definitions rpd + ON resource_request_properties.property_id = rpd.property_id + WHERE resource_request_properties.request_id = :request_id + AND rpd.name = :name"); + + $exists_stmt->execute( + [ + 'request_id' => $this->id, + 'name' => $name + ] + ); + + $exists = $exists_stmt->fetchColumn(0); + + return (bool)$exists; + } + + + /** + * @param $name + * Returns the state of the property specified by $name. + */ + public function getProperty($name) + { + if (!$this->propertyExists($name)) { + //A property with the name $name does not exist for this + //resource request object. + //In that case we can only return null, since resource requests + //store only those properties which are requested: + + return null; + } + + $db = DBManager::get(); + + $value_stmt = $db->prepare( + "SELECT resource_request_properties.state FROM resource_request_properties + INNER JOIN resource_property_definitions rpd + ON resource_request_properties.property_id = rpd.property_id + WHERE resource_request_properties.request_id = :request_id + AND rpd.name = :name"); + + $value_stmt->execute( + [ + 'request_id' => $this->id, + 'name' => $name + ] + ); + + $value = $value_stmt->fetchColumn(0); + + if (!$value) { + return null; + } + + return $value; + } + + + /** + * @param $name + * @return ResourceRequestProperty + * @throws InvalidResourceCategoryException If this resource category + * doesn't match the category of the resource request object. + * @throws ResourcePropertyException If the name of the + * resource request property is not defined for this resource category. + */ + public function getPropertyObject($name) + { + if (!$this->propertyExists($name)) { + //A property with the name $name does not exist for this + //resource object. If it is a mandatory property + //we can still try to create it: + + $property = $this->category->createDefinedResourceRequestProperty( + $this, + $name + ); + + $property->store(); + return $property; + } + + return ResourceRequestProperty::findOneBySql( + "INNER JOIN resource_property_definitions rpd + ON resource_request_properties.property_id = rpd.property_id + WHERE resource_request_properties.request_id = :request_id + AND rpd.name = :name", + [ + 'request_id' => $this->id, + 'name' => $name + ] + ); + } + + + /** + * @param string $name + * @param string $state + * @return bool True, if the property state could be set, false otherwise. + */ + public function setProperty($name, $state = '') + { + if (!$this->propertyExists($name)) { + //A property with the name $name does not exist for this + //resource object. If it is a mandatory property + //we can still try to create it: + + if ($this->category) { + $property = $this->category->createDefinedResourceRequestProperty( + $this, + $name, + $state + ); + return $property->store(); + } + return false; + } + + $property = $this->getPropertyObject($name); + + if ($property) { + $property->state = $state; + if ($property->isDirty()) { + return $property->store(); + } + return true; + } + } + + + /** + * Sets or unsets the properties for this resource request. + * + * @param array $property_list The properties which shall be set + * or unset. The array has the following structure: + * [ + * property_name => property_value + * ] + * + * @param bool $accept_null_values True, if a value of null + * shall be used when setting the property. + * If $accept_null_values is set to false all properties + * with a value equal to null will be deleted. + * + * @return null + */ + public function updateProperties($property_list = [], $accept_null_values = false) + { + //Delete all properties first then re-create them + //from the $property_list array: + $this->properties->delete(); + if (is_array($property_list)) { + foreach ($property_list as $name => $state) { + if ($state or $accept_null_values) { + //State is set or null values are allowed: + //create/update the property + $this->setProperty($name, $state); + } + } + } + $this->resetRelation('properties'); + } + + + public function deletePropertyIfExists($name = '') + { + if (!$this->propertyExists($name)) { + return true; + } else { + $property = $this->getPropertyObject($name); + return $property->delete(); + } + } + + + public function getRangeName() + { + if ($this->getRangeType() === 'course') { + $name = $this->getRangeObject()->getFullName(); + $name .= ' (' . implode(',', $this->getRangeObject()->getMembersWithStatus('dozent', true)->limit(3)->getValue('nachname')) . ')'; + } else { + $range_object = $this->getRangeObject(); + if ($range_object instanceof User) { + if (get_visibility_by_id($range_object->id)) { + $name = $range_object->getFullName(); + } else if ($this->user_id === $GLOBALS['user']->id) { + $name = $range_object->getFullName(); + } else { + $current_user = User::findCurrent(); + if ($current_user instanceof User) { + //If the current user has at least autor permissions + //(which are required to see all requests), they can + //see the name of the requester. + if ($this->resource_id && ($this->resource instanceof Resource) + && $this->resource->userHasPermission($current_user, 'autor')) { + $name = $range_object->getFullName(); + } else if (ResourceManager::userHasGlobalPermission($current_user, 'autor')) { + $name = $range_object->getFullName(); + } else { + return ''; + } + } else { + return ''; + } + } + } else { + $name = $range_object->getFullName(); + } + if ($this->comment) { + $name .= " \n" . $this->comment; + } + } + return $name; + } + + + public function isSimpleRequest() + { + return !$this->course_id && !$this->metadate_id && !$this->termin_id; + } + + + public function getRangeId() + { + //Check if the request belongs to a course: + if ($this->termin_id) { + return $this->date->range_id; + } elseif ($this->metadate_id) { + return $this->cycle->seminar_id; + } elseif ($this->course_id) { + return $this->course_id; + } + + //The request does not belong to a course and therefore + //belongs to a user: + return $this->user_id; + } + + + public function getRangeType() + { + if ($this->course_id || $this->termin_id || $this->metadate_id) { + return 'course'; + } + return 'user'; + } + + + public function getRangeObject() + { + if ($this->course_id) { + return $this->course; + } + if ($this->termin_id) { + return $this->date->course; + } + if ($this->metadate_id) { + return $this->cycle->course; + } + return $this->user; + } + + + /** + * This method sends a notification mail to all room administrators + * that informs them of this new request. + */ + public function sendNewRequestMail() + { + //First we must get all users who have admin permissions in the + //resource management system. Depending wheter a resource_id is set + //for this resource request either all admins of a resource or + //all admins of the resource management system must be informed. + + $now = time(); + if ($this->resource_id) { + //The resource-ID is set for this request: + //Get all admins of the resource and the resource management system. + $admin_users = User::findBySql( + "user_id IN ( + SELECT user_id FROM resource_permissions + WHERE ( + resource_id = :resource_id + OR resource_id = 'global' + ) + AND perms = 'admin' + UNION + SELECT user_id FROM resource_temporary_permissions + WHERE resource_id = :resource_id + AND perms = 'admin' + AND begin <= :now AND end >= :now + )", + [ + 'resource_id' => $this->resource_id, + 'now' => $now + ] + ); + } else { + //Get all admins of the resource management system. + $admin_users = User::findBySql( + "user_id IN ( + SELECT user_id FROM resource_permissions + WHERE resource_id = 'global' + AND perms = 'admin' + UNION + SELECT user_id FROM resource_temporary_permissions + WHERE resource_id = 'global' + AND perms = 'admin' + AND begin <= :now AND end >= :now + GROUP BY user_id + )", + [ + 'now' => $now + ] + ); + } + + if (!$admin_users) { + return; + } + + $factory = new Flexi\Factory( + $GLOBALS['STUDIP_BASE_PATH'] . '/locale/' + ); + + foreach ($admin_users as $user) { + $user_lang_path = getUserLanguagePath($user->id); + + $template = $factory->open( + $user_lang_path . '/LC_MAILS/new_resource_request.php' + ); + $template->set_attribute('request', $this); + + if ($this->resource instanceof Resource) { + $resource = $this->resource->getDerivedClassInstance(); + if ($resource instanceof Room) { + $template->set_attribute('requested_room', $resource->name); + } else { + $template->set_attribute('requested_resource', $resource->name); + } + } + + $mail_text = $template->render(); + + setLocaleEnv($user->preferred_language); + + if ($this->resource) { + $resource = $this->resource->getDerivedClassInstance(); + $template->set_attribute('derived_resource', $resource); + $mail_title = sprintf( + _('%1$s: Neue Anfrage in der Raumverwaltung'), + $resource->getFullName() + ); + } else { + $mail_title = sprintf( + _('Neue Anfrage in der Raumverwaltung') + ); + } + + Message::send( + User::findCurrent()->id, + $user->username, + $mail_title, + $mail_text + ); + + restoreLanguage(); + } + } + + + /** + * @param array $bookings + * This method sends a mail to inform the requester that + * the request has been closed. + */ + public function sendCloseRequestMailToRequester($bookings = []) + { + $factory = new Flexi\Factory( + $GLOBALS['STUDIP_BASE_PATH'] . '/locale/' + ); + + $requester_lang = $this->user->preferred_language; + $requester_lang_path = getUserLanguagePath($this->user->id); + setLocaleEnv($requester_lang); + + $template = $factory->open( + $requester_lang_path . '/LC_MAILS/close_resource_request.php' + ); + $template->set_attribute('request', $this); + if ($this->course) { + $lecturers = CourseMember::findByCourseAndStatus( + $this->course->id, + 'dozent' + ); + $lecturer_names = []; + foreach ($lecturers as $lecturer) { + if ($lecturer->user instanceof User) { + $lecturer_names[] = $lecturer->user->getFullName(); + } + } + + $lecturer_names = implode(', ', $lecturer_names); + $template->set_attribute('lecturer_names', $lecturer_names); + } + if (is_array($bookings)) { + $booked_rooms = []; + $booked_time_intervals = []; + $metadates = []; + $single_dates = []; + foreach ($bookings as $booking) { + if (!($booking instanceof ResourceBooking)) { + continue; + } + $booked_rooms[] = $booking->resource->name; + if ($booking->assigned_course_date instanceof CourseDate) { + $single_date = $booking->assigned_course_date; + $metadate = $single_date->cycle; + if ($metadate instanceof SeminarCycleDate) { + $metadates[$metadate->id] = $metadate; + } else { + $single_dates[$single_date->id] = $single_date; + } + } else { + $time_intervals = $booking->getTimeIntervals(); + foreach ($time_intervals as $time_interval) { + $booked_time_intervals[] = $time_interval->__toString(); + } + } + } + $booked_rooms = array_unique($booked_rooms); + sort($booked_rooms); + $template->set_attribute('booked_rooms', implode(', ', $booked_rooms)); + $template->set_attribute('metadates', $metadates); + $template->set_attribute('single_dates', $single_dates); + $template->set_attribute('booked_time_intervals', $booked_time_intervals); + } + + $mail_title = _('Ihre Anfrage wurde bearbeitet!'); + $mail_text = $template->render(); + + Message::send( + User::findCurrent()->id, + $this->user->username, + $mail_title, + $mail_text + ); + + restoreLanguage(); + } + + + /** + * @param array $bookings + * This method sends mails to the lecurers of the course (if any) + * where this request has been assigned to. The sent mail informs them + * about the closing of the request. + */ + public function sendCloseRequestMailToLecturers($bookings = []) + { + //Notify each lecturer of the course: + if ($this->course) { + $lecturers = CourseMember::findByCourseAndStatus( + $this->course->id, + 'dozent' + ); + + if ($lecturers) { + $factory = new Flexi\Factory( + $GLOBALS['STUDIP_BASE_PATH'] . '/locale/' + ); + + $lecturer_names = []; + foreach ($lecturers as $lecturer) { + if ($lecturer->user instanceof User) { + $lecturer_names[] = $lecturer->user->getFullName(); + } + } + $lecturer_names = implode(', ', $lecturer_names); + + $booked_rooms = []; + $booked_time_intervals = []; + $metadates = []; + $single_dates = []; + if (is_array($bookings)) { + foreach ($bookings as $booking) { + if (!($booking instanceof ResourceBooking)) { + continue; + } + $booked_rooms[] = $booking->resource->name; + if ($booking->assigned_course_date instanceof CourseDate) { + $single_date = $booking->assigned_course_date; + $metadate = $single_date->cycle; + if ($metadate instanceof SeminarCycleDate) { + $metadates[$metadate->id] = $metadate; + } else { + $single_dates[$single_date->id] = $single_date; + } + } else { + $time_intervals = $booking->getTimeIntervals(); + foreach ($time_intervals as $time_interval) { + $booked_time_intervals[] = $time_interval->__toString(); + } + } + } + } + $booked_rooms = array_unique($booked_rooms); + sort($booked_rooms); + $booked_rooms = implode(', ', $booked_rooms); + + foreach ($lecturers as $lecturer) { + $lec_lang = $lecturer->user->preferred_language; + $lec_lang_path = getUserLanguagePath($lecturer->user->id); + + setLocaleEnv($lec_lang); + + $template = $factory->open( + $lec_lang_path . '/LC_MAILS/close_resource_request.php' + ); + $template->set_attribute('request', $this); + $template->set_attribute('lecturer_names', $lecturer_names); + $template->set_attribute('booked_rooms', $booked_rooms); + $template->set_attribute('metadates', $metadates); + $template->set_attribute('single_dates', $single_dates); + $template->set_attribute('booked_time_intervals', $booked_time_intervals); + + $mail_title = _('Bearbeitung einer Anfrage!'); + $mail_text = $template->render(); + + Message::send( + User::findCurrent()->id, + $lecturer->user->username, + $mail_title, + $mail_text + ); + + restoreLanguage(); + } + } + } + } + + + /** + * This method sends a mail to inform the requester + * about the denial of the request. + */ + public function sendRequestDeniedMail() + { + //Get the user who made the request: + $user = $this->user; + if (!($user instanceof User)) { + //No mail to send. + return; + } + + //Load the mail template: + $factory = new Flexi\Factory( + $GLOBALS['STUDIP_BASE_PATH'] . '/locale/' + ); + $user_lang_path = getUserLanguagePath($user->id); + $template = $factory->open( + $user_lang_path . '/LC_MAILS/request_denied_mail.inc.php' + ); + + $range_object = $this->getRangeObject(); + $mail_title = _('Raumanfrage wurde abgelehnt'); + if($range_object instanceof Course) { + $mail_title .= ': ' . $range_object->getFullName(); + } + $mail_text = $template->render( + [ + 'request' => $this, + 'range_object' => $range_object + ] + ); + + //Send the mail: + Message::send( + User::findCurrent()->id, + $user->username, + $mail_title, + $mail_text + ); + } + + + public function isReadOnlyForUser(User $user) + { + $resource = $this->resource; + if (!$resource) { + //We cannot continue with the permission check. + return false; + } + $resource = $resource->getDerivedClassInstance(); + + return !$resource->userHasPermission($user, 'autor') + && ($this->user_id != $user->id); + } + + protected function convertToEventData(array $time_intervals, User $user) + { + $booking_plan_request_bg = ColourValue::find('Resources.BookingPlan.Request.Bg'); + $booking_plan_request_fg = ColourValue::find('Resources.BookingPlan.Request.Fg'); + $booking_plan_preparation_bg = ColourValue::find('Resources.BookingPlan.PreparationTime.Bg'); + $booking_plan_preparation_fg = ColourValue::find('Resources.BookingPlan.PreparationTime.Fg'); + + $user_is_resource_autor = false; + if ($this->resource_id && $this->resource instanceof Resource) { + $user_is_resource_autor = $this->resource->userHasPermission( + $user, + 'autor' + ); + } + $request_is_editable = $user_is_resource_autor || ($user->id == $this->user_id); + + $request_api_urls = []; + $request_view_urls = []; + + if ($request_is_editable) { + $request_api_urls = [ + 'resize' => URLHelper::getURL( + 'dispatch.php/resources/ajax/move_request/'. $this->id, + ['quiet' => true] + ), + 'move' => URLHelper::getURL( + 'dispatch.php/resources/ajax/move_request/'. $this->id, + ['quiet' => true] + ) + ]; + + $request_view_urls = [ + 'edit' => URLHelper::getURL( + 'dispatch.php/resources/room_request/edit/' + . $this->id + ) + ]; + if ( + $this->resource_id + && $this->resource instanceof Resource + && $this->resource->userHasBookingRights($user) + ) { + $request_view_urls['edit'] = URLHelper::getURL( + 'dispatch.php/resources/room_request/resolve/'. $this->id + ); + } + } + + $events = []; + + foreach ($time_intervals as $interval) { + $real_begin = $interval['begin']; + if ($this->preparation_time) { + $real_begin += (int)$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(), + $request_is_editable, + '', + '', + ResourceRequest::class, + $this->id, + Resource::class, + $this->resource_id, + $request_view_urls, + $request_api_urls + ); + } + + $begin = new DateTime(); + $begin->setTimestamp($real_begin); + $end = new DateTime(); + $end->setTimestamp($interval['end']); + + $events[] = new Studip\Calendar\EventData( + $begin, + $end, + $this->getRangeName(), + ['resource-request'], + $booking_plan_request_fg->__toString(), + $booking_plan_request_bg->__toString(), + $request_is_editable, + ResourceRequest::class, + $this->id, + Resource::class, + $this->resource_id, + Resource::class, + $this->resource_id, + $request_view_urls, + $request_api_urls + ); + } + + return $events; + } + + + public function getAllEventData() + { + return $this->convertToEventData( + $this->getTimeIntervals(true), + User::findCurrent() + ); + } + + + public function getEventDataForTimeRange(DateTime $begin, DateTime $end) + { + $intervals = $this->getTimeIntervals(true); + $time_intervals = []; + + $begin_timestamp = $begin->getTimestamp(); + $end_timestamp = $end->getTimestamp(); + + foreach ($intervals as $interval) { + if ((($interval['begin'] >= $begin_timestamp) + && ($interval['begin'] <= $end_timestamp)) || + (($interval['end'] >= $begin_timestamp) + && ($interval['end'] <= $end_timestamp)) || + (($interval['begin'] < $begin_timestamp) + && ($interval['end'] > $end_timestamp)) + ) { + $time_intervals[] = $interval; + } + } + + return $this->convertToEventData($time_intervals, User::findCurrent()); + } + + + public function getFilteredEventData( + $user_id = null, + $range_id = null, + $range_type = null, + $begin = null, + $end = null + ) + { + $intervals = $this->getTimeIntervals(true); + $time_intervals = []; + + if ($begin && $end) { + $begin_timestamp = $begin; + $end_timestamp = $end; + if ($begin instanceof DateTime) { + $begin_timestamp = $begin->getTimestamp(); + } + if ($end instanceof DateTime) { + $end_timestamp = $end->getTimestamp(); + } + + foreach ($intervals as $interval) { + if ((($interval['begin'] >= $begin_timestamp) + && ($interval['begin'] <= $end_timestamp)) || + (($interval['end'] >= $begin_timestamp) + && ($interval['end'] <= $end_timestamp)) || + (($interval['begin'] < $begin_timestamp) + && ($interval['end'] > $end_timestamp)) + ) { + $time_intervals[] = $interval; + } + } + } else { + $time_intervals = $intervals; + } + + if ($user_id) { + $user = User::find($user_id); + } else { + $user = User::findCurrent(); + } + + return $this->convertToEventData($time_intervals, $user); + } + + public function getPriority() + { + + $result = $this->getTimeIntervals(); + if (count($result) === 0) { + return null; + } + $first = $result[0]; + return round(($first['begin'] - time()) / 86400); + } + + public function getLoggingInfoText() + { + $props = ''; + foreach ($this->getPropertyData() as $name => $state) { + $props .= $name . '=' . $state . ' '; + } + $info['Anfrage'] = $this->getType(); + $info['Status'] = $this->getStatus(); + if ($this->category) { + $info['Raumtyp'] = $this->category->name; + } + if ($this->termin_id) { + $info['Termin'] = $this->termin_id; + } + if ($this->metadate_id) { + $info['Metadate'] = $this->metadate_id; + } + if ($props) { + $info['Eigenschaften'] = $props; + } + if ($this->comment) { + $info['Kommentar'] = $this->comment; + } + $txt = ''; + foreach ($info as $n => $m) { + $txt .= $n . ': ' . $m . ', '; + } + return trim($txt, ' ,'); + } +} |
