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