* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
*/
require_once 'lib/dates.inc.php';
class SingleDate
{
var $termin_id = '';
var $date_typ = 1;
var $metadate_id = '';
var $date = 0;
var $end_time = 0;
var $mkdate = 0;
var $chdate = 0;
var $orig_ex = false;
var $ex_termin = false;
var $range_id = '';
var $author_id = '';
var $resource_id = '';
var $raum = '';
var $request_id = NULL;
var $requestData = NULL;
var $update = false;
var $issues = NULL;
var $messages = NULL;
var $content = '';
var $room_request = NULL;
var $related_persons = [];
var $related_groups = [];
/**
* Return the SingleDate instance of the given id
*
* @param string the id of the instance
* @return SingleDate the SingleDate instance
*/
function getInstance($singledate_id)
{
static $singledate_object_pool;
if ($singledate_id) {
if (is_object($singledate_object_pool[$singledate_id]) && $singledate_object_pool[$singledate_id]->getTerminId() == $singledate_id) {
return $singledate_object_pool[$singledate_id];
} else {
$singledate_object_pool[$singledate_id] = new SingleDate($singledate_id);
return $singledate_object_pool[$singledate_id];
}
} else {
return new SingleDate();
}
}
function __construct($data = '')
{
global $user, $id;
$termin_id = '';
if ($data instanceOf CourseDate || $data instanceof CourseExDate) {
$single_date_data = $data->toArray();
$single_date_data['ex_termin'] = $data instanceOf CourseDate ? 0 : 1;
$single_date_data['resource_id'] = $data->room_booking->resource_id ?: '';
if ($data instanceOf CourseDate) {
$single_date_data['related_persons'] = $data->dozenten->pluck('user_id');
$single_date_data['related_groups'] = $data->statusgruppen->pluck('statusgruppe_id');
}
$this->fillValuesFromArray($single_date_data);
} else {
if (is_array($data)) {
if ($data['termin_id']) $termin_id = $data['termin_id'];
if ($data['seminar_id']) $id = $data['seminar_id'];
} else {
$termin_id = $data;
}
if ($termin_id !== '') {
$this->termin_id = $termin_id;
$this->update = true;
$this->restore();
} else {
$this->termin_id = md5(uniqid('SingleDate', 1));
$this->author_id = $user->id;
$this->range_id = $id;
$this->mkdate = time();
$this->chdate = time();
$this->update = false;
}
}
}
public function __toString()
{
return $this->toString();
}
function getStartTime()
{
return $this->date;
}
function setTime($start, $end)
{
if (($start == 0) || ($end == 0)) return false;
if (($this->date != $start) || ($this->end_time != $end)) {
// validate the passed variables: they have to be ints and $start
// has to be smaller than $end
if ($this->validate($start, $end)) {
$before = $this->toString();
// if the time-span has been shortened, keep the room-booking,
// otherwise remove it.
if ($this->resource_id) {
if ($start >= $this->date && $end <= $this->end_time) {
//Shrink the booking.
if ($assign_id = SingleDateDB::getAssignID($this->termin_id)) {
$assign_object = new ResourceBooking($assign_id);
$assign_object->resource_id = $this->resource_id;
$assign_object->begin = $start;
$assign_object->end = $end;
$assign_object->repeat_end = $end;
$assign_object->store();
}
} else {
$this->killAssign();
}
}
$this->date = $start;
$this->end_time = $end;
$after = $this->toString();
// logging
if ($before) {
StudipLog::log('SINGLEDATE_CHANGE_TIME', $this->range_id, $this->id, $before . ' -> ' . $after);
} else {
StudipLog::log('SEM_ADD_SINGLEDATE', $this->range_id, $this->id, $after);
}
return true;
}
return false;
}
return false;
}
function getEndTime()
{
return $this->end_time;
}
function setComment($comment)
{
$this->content = $comment;
}
function getComment()
{
if (!$this->isExTermin()) return '';
return $this->content;
}
function getMetaDateID()
{
return $this->metadate_id;
}
function setMetaDateID($id)
{
if ($id != '') {
$this->metadate_id = $id;
return true;
} else {
$this->metadate_id = 0;
return false;
}
}
function getRangeID()
{
return $this->range_id;
}
function setDateType($typ)
{
$this->date_typ = $typ;
return true;
}
function getDateType()
{
return $this->date_typ;
}
function getTypeName()
{
global $TERMIN_TYP;
return $TERMIN_TYP[$this->date_typ]['name'];
}
function getAuthorID()
{
return $this->author_id;
}
function getChDate()
{
return $this->chdate;
}
function getMkDate()
{
return $this->mkdate;
}
function setSeminarID($seminar_id)
{
$this->range_id = $seminar_id;
}
function getSeminarID()
{
return $this->range_id;
}
function getSingleDateID()
{
return $this->termin_id;
}
function getResourceID()
{
return $this->resource_id;
}
function getTerminID()
{
return $this->termin_id;
}
function getFreeRoomText()
{
return $this->raum;
}
function setFreeRoomText($freeRoomText)
{
$this->raum = $freeRoomText;
}
function getCycleID()
{
return $this->metadate_id;
}
/**
* @deprecated
*/
function killIssue()
{
$issue_ids = $this->getIssueIDs();
if (count($issue_ids) > 0) {
CourseTopic::deleteBySQL("issue_id IN (?)", $issue_ids);
foreach ($issue_ids as $issue_id) {
unset($this->issues[$issue_id]);
}
}
}
function delete()
{
$cache = \Studip\Cache\Factory::getCache();
$cache->expire('course/undecorated_data/' . $this->range_id);
$this->chdate = time();
$this->killAssign();
return SingleDateDB::deleteSingleDate($this->termin_id, $this->ex_termin);
}
function store()
{
$cache = \Studip\Cache\Factory::getCache();
$cache->expire('course/undecorated_data/' . $this->range_id);
$this->chdate = time();
if ($this->ex_termin) {
$this->killAssign();
}
// date_typ = 0 defaults to TERMIN_TYP[1] because there never exists one with zero
if (!$this->date_typ) $this->date_typ = 1;
if ($this->orig_ex != $this->ex_termin) {
SingleDateDB::deleteSingleDate($this->termin_id, $this->orig_ex);
}
return SingleDateDB::storeSingleDate($this);
}
function restore()
{
if (!($data = SingleDateDB::restoreSingleDate($this->termin_id))) {
return false;
}
$this->fillValuesFromArray($data);
return true;
}
function setExTermin($ex)
{
if ($ex != $this->ex_termin) {
$this->update = false;
$this->ex_termin = $ex;
return true;
}
return false;
}
function isExTermin()
{
return $this->ex_termin;
}
function isPresence()
{
return $GLOBALS['TERMIN_TYP'][$this->date_typ]['sitzung'] ? true : false;
}
function isUpdate()
{
return $this->update;
}
function isHoliday()
{
$name = null;
foreach (SemesterHoliday::getAll() as $val) {
if (($val['beginn'] <= $this->date) && ($val['ende'] >= $this->end_time)) {
$name = $val['name'];
}
}
if (!$name) {
$holy_type = holiday($this->date);
$name = $holy_type['name'];
}
if ($name) {
return $name;
} else {
return false;
}
}
function fillValuesFromArray($daten)
{
$this->metadate_id = $daten['metadate_id'];
$this->termin_id = $daten['termin_id'];
if ($daten['date_typ'] != 0) {
$this->date_typ = $daten['date_typ'];
} else {
// if no date_typ is specified it defaults to 1
$this->date_typ = 1;
}
$this->date = $daten['date'];
$this->end_time = $daten['end_time'];
$this->mkdate = $daten['mkdate'];
$this->chdate = $daten ['chdate'];
$this->ex_termin = $daten['ex_termin'] ?? null;
$this->orig_ex = $daten['ex_termin'] ?? null;
$this->range_id = $daten['range_id'];
$this->author_id = $daten['autor_id'];
$this->resource_id = $daten['resource_id'];
$this->raum = $daten['raum'];
$this->content = $daten['content'];
$this->update = true;
$this->related_persons = is_array($daten['related_persons']) ? $daten['related_persons'] : [];
$this->related_groups = is_array($daten['related_groups']) ? $daten['related_groups'] : [];
return true;
}
function toString()
{
$end_hours = strtotime(strftime('%H:%M', $this->end_time));
$start_hours = strtotime(strftime('%H:%M', $this->date));
if (!$this->date) {
return null;
} elseif ((($end_hours - $start_hours) / 60 / 60) > 23) {
return sprintf('%s (%s)', strftime('%A, %d.%m.%Y', $this->date),
_('ganztägig'));
} else {
return sprintf('%s - %s', strftime('%A, %d.%m.%Y %H:%M', $this->date),
strftime('%H:%M', $this->end_time));
}
}
function bookRoom($room_id, $preparation_time = 0)
{
if ($this->ex_termin || !$room_id) {
return false;
}
// create a resource-object of the passed room
$room = Room::find($room_id);
// there is no room with the passed id
if (!$room) {
return false;
}
// check permissions (is current user allowed to book the passed room?)
if (!$room->userHasBookingRights(User::findCurrent(), $this->date, $this->end_time)) {
return false;
}
// clear the freetext-field, if we book a room
$this->setFreeRoomText('');
$this->store();
//If there is already a room assigned, "change" the booking.
//Otherwise create a new one.
if ($this->resource_id != '') {
$this->changeAssign($room, $preparation_time);
} else {
$this->insertAssign($room, $preparation_time);
}
return $room;
}
/**
* This method converts overlap data about an overlapping booking
* to a string that can be used to output overlap information to the user.
* Only one overlap is converted by this method. For multiple overlaps
* this method must be called multiple times.
*
* @param ResourceBooking $booking The overlapping booking.
*
* @return string A string representation of the overlap.
*/
protected function getOverlapMessage(ResourceBooking $booking)
{
$message = '';
if ($booking->booking_type == ResourceBooking::TYPE_LOCK) {
$message .= sprintf(
_('Vom %1$s, %2$s Uhr bis zum %3$s, %4$s Uhr (Sperrzeit)') . "\n",
date("d.m.Y", $booking->begin),
date("H:i", $booking->begin),
date("d.m.Y", $booking->end),
date("H:i", $booking->end)
);
} else {
$course = Course::find($booking->course_id);
if ($course) {
$user_has_permissions = $GLOBALS['perm']->have_studip_perm(
'dozent',
$course->id,
$GLOBALS['user']->id
);
if ($user_has_permissions) {
$course_link = URLHelper::getLink(
'dispatch.php/course/timesrooms/index',
[
'cid' => $course->id
]
);
$message .= sprintf(
_('Am %1$s von %2$s bis %3$s Uhr durch Veranstaltung %4$s') . "\n",
date('d.m.Y', $booking->begin),
date('H:i', $booking->begin),
date('H:i', $booking->end),
sprintf(
'%2$s',
$course_link,
htmlReady($course->name)
)
);
} else {
$course_link = URLHelper::getLink(
'dispatch.php/course/details',
[
'sem_id' => $course->id
]
);
$message .= sprintf(
_('Am %1$s von %2$s bis %3$s Uhr durch Veranstaltung %4$s') . "\n",
date('d.m.Y', $booking->begin),
date('H:i', $booking->begin),
date('H:i', $booking->end),
sprintf(
'%2$s',
$course_link,
htmlReady($course->name)
)
);
}
} else {
$message .= sprintf(
_('Am %1$s von %2$s bis %3$s Uhr belegt von "%4$s"') . "\n",
date("d.m.Y", $booking->begin),
date("H:i", $booking->begin),
date("H:i", $booking->end),
htmlReady($booking->description)
);
}
}
return $message;
}
private function insertAssign(Room $room, $preparation_time = 0)
{
$begin = new DateTime();
$begin->setTimestamp($this->date);
$end = new DateTime();
$end->setTimestamp($this->end_time);
//If the following code is executed a new room booking can be created:
try {
$booking = $room->createBooking(
User::findCurrent(),
$this->termin_id,
[
[
'begin' => $begin,
'end' => $end
]
],
null,
0,
null,
$preparation_time * 60
);
if ($booking instanceof ResourceBooking) {
$booking->deleteOverlappingReservations();
$room_link_string = sprintf(
'%2$s',
$room->getActionLink(),
htmlReady($room->name)
);
SingleDateDB::storeSingleDate($this);
$msg = sprintf(
_('Für den Termin %1$s wurde der Raum %2$s gebucht.'),
$this->toString(),
$room_link_string
);
$this->messages['success'][] = $msg;
}
} catch (ResourceBookingRangeException $e) {
$this->messages['error'][] = _('Fehler beim Verknüpfen der Raumbelegung mit dem Einzeltermin!');
return false;
} catch (ResourceBookingOverlapException $e) {
$error_message = sprintf(
_('Für den Termin %1$s konnte der Raum %2$s nicht gebucht werden, da es Überschneidungen mit folgenden Terminen gibt:'),
$this->toString(),
htmlReady($room->name)
) . '
';
$overlapping_bookings = array_merge(
$room->getResourceBookings($begin, $end),
$room->getResourceLocks($begin, $end)
);
foreach ($overlapping_bookings as $overlapping_booking) {
$course_link = null;
if ($overlapping_booking->course) {
$user_is_lecturer = $GLOBALS['perm']->have_studip_perm(
'dozent',
$overlapping_booking->course->id,
$GLOBALS['user']->id
);
if ($user_is_lecturer) {
$course_link = URLHelper::getLink(
'dispatch.php/course/timesrooms/index',
[
'cid' => $overlapping_booking->course->id
]
);
}
}
$error_message .= $this->getOverlapMessage(
$overlapping_booking
);
}
$this->messages['error'][] = $error_message;
return false;
} catch (ResourcePermissionException $e) {
$this->messages['error'][] = $e->getMessage();
return false;
} catch (ResourceBookingException $e) {
$this->messages['error'][] = $e->getMessage();
return false;
}
return true;
}
private function changeAssign(Room $room, $preparation_time = 0)
{
$max_preparation_time = (Config::get()->RESOURCES_MAX_PREPARATION_TIME);
if ($preparation_time > $max_preparation_time) {
$this->messages['error'][] = sprintf(
_('Die eingegebene Rüstzeit überschreitet das erlaubte Maximum von %d Minuten!'),
$max_preparation_time
);
return false;
}
if ($assign_id = SingleDateDB::getAssignID($this->termin_id)) {
$changeAssign = new ResourceBooking($assign_id);
$changeAssign->resource_id = $room->id;
$changeAssign->range_id = $this->termin_id;
$changeAssign->begin = $this->date;
$changeAssign->end = $this->end_time;
$changeAssign->repeat_end = $this->end_time;
$changeAssign->repetition_interval = '';
if ($preparation_time > 0) {
$changeAssign->preparation_time = $preparation_time * 60;
}
$room_link_string = sprintf(
'%2$s',
$room->getActionLink(),
htmlReady($room->name)
);
$overlaps = $changeAssign->getOverlappingBookings();
if (is_array($overlaps) && (sizeof($overlaps) > 0)) {
$msg = sprintf(
_('Für den Termin %1$s konnte der Raum %2$s nicht gebucht werden, da es Überschneidungen mit folgenden Terminen gibt:'),
$this->toString(),
$room_link_string
) . '
';
foreach ($overlaps as $overlap) {
$msg .= $this->getOverlapMessage($overlap);
}
$this->messages['error'][] = $msg;
return false;
}
$this->resource_id = $room->id;
try {
$changeAssign->store();
} catch (ResourceBookingOverlapException $e) {
$room = $changeAssign->resource->getDerivedClassInstance();
if($room instanceof Room) {
$room_text = $link = sprintf(
'%2$s',
$room->getActionLink(),
htmlReady($room->name)
);
} else {
$room_text = $changeAssign->resource->name;
}
$this->messages['error'][] = sprintf(
_('%1$s: Die Buchung vom %2$s bis %3$s konnte wegen Überlappungen nicht gespeichert werden: %4$s'),
$room_text,
date('d.m.Y H:i', $changeAssign->begin),
date('H:i', $changeAssign->end),
htmlReady($e->getMessage())
);
}
$msg = sprintf(
_('Für den Termin %1$s wurde der Raum %2$s gebucht.'),
$this->toString(),
$room_link_string
);
$this->messages['success'][] = $msg;
return true;
}
return false;
}
function killAssign()
{
$this->resource_id = '';
if ($assign_id = SingleDateDB::getAssignID($this->termin_id)) {
$assign_object = new ResourceBooking($assign_id);
$assign_object->delete();
}
}
/**
* Returns the room name for this SingleDate object.
*
* @returns string The room name.
*/
public function getRoom()
{
if (!$this->resource_id) {
return null;
} else {
$room = Room::find($this->resource_id);
return $room->name;
}
}
function readIssueIDs()
{
if (!$this->issues) {
if ($data = SingleDateDB::getIssueIDs($this->termin_id)) {
foreach ($data as $val) {
$this->issues[$val['issue_id']] = $val['issue_id'];
}
}
}
return true;
}
function getIssueIDs()
{
$this->readIssueIDs();
return $this->issues;
}
function addIssueID($issue_id)
{
$this->readIssueIDs();
$this->issues[$issue_id] = $issue_id;
return true;
}
function deleteIssueID($issue_id)
{
$this->readIssueIDs();
unset($this->issues[$issue_id]);
SingleDateDB::deleteIssueID($issue_id, $this->termin_id);
return true;
}
function getMessages()
{
$temp = $this->messages;
$this->messages = NULL;
return $temp;
}
// checks, if the single-date has plausible values
function validate($start = 0, $end = 0)
{
if ($start == 0) {
$start = $this->date;
}
if ($end == 0) {
$end = $this->end_time;
}
if ($start < 100000) return false;
if ($end < 100000) return false;
if ($start > $end) {
$this->messages['error'][] = _("Die Endzeitpunkt darf nicht vor dem Anfangszeitpunkt liegen!");
return false;
}
return true;
}
/**
* returns a html representation of the date
*
* @param array optional variables which are passed to the template
* @return string the html-representation of the date
*
* @author Till Glöggler
*/
function getDatesHTML($params = [])
{
$template = $GLOBALS['template_factory']->open('dates/date_html.php');
$template->set_attributes($params);
return $this->getDatesTemplate($template);
}
/**
* returns a representation without html of the date
*
* @param array optional variables which are passed to the template
* @return string the representation of the date without html
*
* @author Till Glöggler
*/
function getDatesExport($params = [])
{
$template = $GLOBALS['template_factory']->open('dates/date_export.php');
$params['link'] = false;
$template->set_attributes($params);
return $this->getDatesTemplate($template);
}
/**
* returns a xml-representation of the date
*
* @param array optional variables which are passed to the template
* @return string the xml-representation of the date
*
* @author Till Glöggler
*/
function getDatesXML($params = [])
{
$template = $GLOBALS['template_factory']->open('dates/date_xml.php');
$template->set_attributes($params);
return $this->getDatesTemplate($template);
}
/**
* returns a representation of the date with a specifiable template
*
* @param mixed this can be a template-object or a string pointing to a template in path_to_studip/templates
* @return string the template output of the date
*
* @author Till Glöggler
*/
function getDatesTemplate($template)
{
if (!$template instanceof Flexi\Template && is_string($template)) {
$template = $GLOBALS['template_factory']->open($template);
}
$template->set_attribute('date', $this);
return $template->render();
}
/**
* adds a given user_id as a related person to the date
* @param string $user_id user_id from auth_user_md5 of the person to be added
*/
public function addRelatedPerson($user_id)
{
$this->related_persons[] = $user_id;
$this->related_persons = array_unique($this->related_persons);
}
/**
* unsets a given user_id from the array of related persons
* @param string $user_id user_id from auth_user_md5 of the person to be added
*/
public function deleteRelatedPerson($user_id)
{
if (!$this->related_persons) {
$sem = Seminar::getInstance($this->getSeminarID());
$this->related_persons = array_keys($sem->getMembers('dozent'));
}
foreach ($this->related_persons as $key => $related_person) {
if ($related_person === $user_id) {
unset($this->related_persons[$key]);
}
}
}
/**
* gets all user_ids of related persons of this date
* @return array of user_ids
*/
public function getRelatedPersons()
{
if (count($this->related_persons)) {
return $this->related_persons;
} else {
$sem = Seminar::getInstance($this->getSeminarID());
return array_keys($sem->getMembers('dozent'));
}
}
/**
* clears all related persons (in the interface this means that all dozents are
* marked as related to the date)
*/
public function clearRelatedPersons()
{
$this->related_persons = [];
}
/**
* adds a given statusgruppe_id as a related group to the date
* @param string $statusgruppe_id statusgruppe_id from statusgruppen of the group to be added
*/
public function addRelatedGroup($statusgruppe_id)
{
$this->related_groups[] = $statusgruppe_id;
$this->related_groups = array_unique($this->related_groups);
}
/**
* unsets a given statusgruppe_id from the array of related statusgruppen
* @param string $statusgruppe_id statusgruppe_id from statusgruppen of the group to be removed
*/
public function deleteRelatedGroup($statusgruppe_id)
{
if (!$this->related_groups) {
$groups = Statusgruppen::findBySeminar_id($this->getSeminarID());
$this->related_groups = array_map(function ($g) {
return $g->getId();
}, $groups);
}
foreach ($this->related_groups as $key => $related_group) {
if ($related_group === $statusgruppe_id) {
unset($this->related_groups[$key]);
}
}
}
/**
* gets all statusgruppe_ids of related groups of this date
* @return array of statusgruppe_ids
*/
public function getRelatedGroups()
{
if (count($this->related_groups)) {
return $this->related_groups;
} else {
$groups = Statusgruppen::findBySeminar_id($this->getSeminarID());
return array_map(function ($g) {
return $g->getId();
}, $groups);
}
}
/**
* clears all related groups
*/
public function clearRelatedGroups()
{
$this->related_groups = [];
$this->messages['success'][] = _('Die beteiligten Gruppen wurden zurückgesetzt!');
}
}