Select Git revision
CalendarDateAssignment.class.php
Forked from
Stud.IP / Stud.IP
Source project has a limited visibility.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CalendarDateAssignment.class.php 25.36 KiB
<?php
/**
* CalendarDateAssignment.class.php - Model class for calendar date assignments.
*
* CalendarDateAssignment represents the assignment of a calendar date
* to a specific calendar. The calendar is represented by a range-ID
* since it can be a personal calendar, course calendar or institute
* calendar.
*
* 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>
* @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
* @category Stud.IP
* @since 5.5
*
* @property string range_id The range-ID for the assignment.
* @property string calendar_date_id The ID of the calendar date for the assignment.
* @property string participation The participation status of the receiver (range_id).
* This column is an enum with the following values:
* - empty string: Participation status is unknown.
* - "ACCEPTED": The calendar owner accepted the date.
* - "DECLINED": The calendar owner declined the date.
* - "ACKNOWLEDGED": The calendar owner only acknowledged that the date exists
* but doesn't necessarily participate in it.
* @property string mkdate The creation date of the assignment.
* @property string chdate The modification date of the assignment.
* @property CalendarDate|null calendar_date The associated calendar date object.
*/
class CalendarDateAssignment extends SimpleORMap implements Event
{
/**
* @var bool This attribute allows the suppression of automatic mail sending
* when storing or deleting the calendar date assignment.
* By default, mails are sent.
*/
public $suppress_mails = false;
protected static function configure($config = [])
{
$config['db_table'] = 'calendar_date_assignments';
$config['belongs_to']['calendar_date'] = [
'class_name' => CalendarDate::class,
'foreign_key' => 'calendar_date_id',
'assoc_func' => 'find'
];
$config['belongs_to']['user'] = [
'class_name' => User::class,
'foreign_key' => 'range_id',
'assoc_func' => 'find'
];
$config['belongs_to']['course'] = [
'class_name' => Course::class,
'foreign_key' => 'range_id',
'assoc_func' => 'find'
];
$config['registered_callbacks']['after_create'][] = 'cbSendNewDateMail';
$config['registered_callbacks']['after_delete'][] = 'cbSendDateDeletedMail';
parent::configure($config);
}
public function cbSendNewDateMail()
{
if ($this->suppress_mails) {
return;
}
if ($this->range_id === $this->calendar_date->editor_id) {
return;
}
if (!$this->calendar_date || !$this->user) {
//Wrong calendar range (not a user) or invalid data set.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_created.php');
$template->set_attribute('date', $this->calendar_date);
$template->set_attribute('receiver', $this->user);
$mail_text = $template->render();
Message::send(
'____%system%____',
[$this->user->username],
sprintf(_('%s hat einen Termin im Kalender eingetragen'), $this->calendar_date->editor->getFullName()),
$mail_text
);
restoreLanguage();
}
public function cbSendDateDeletedMail()
{
if ($this->suppress_mails) {
return;
}
$actor = User::findCurrent() ?? $this->calendar_date->editor;
if ($this->range_id === $actor->id) {
//The user who deleted the date shall not get notified about this.
return;
}
if (!$this->calendar_date || !$this->user) {
//Wrong calendar range (not a user) or invalid data set.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_deleted.php');
$template->set_attribute('date', $this->calendar_date);
$template->set_attribute('actor', $actor);
$template->set_attribute('receiver', $this->user);
$mail_text = $template->render();
Message::send(
'____%system%____',
[$this->user->username],
sprintf(_('%s hat einen Termin im Kalender gelöscht'), $actor->getFullName()),
$mail_text
);
restoreLanguage();
}
/**
* Sends the participation status of the calendar the date
* is assigned to. This is only done for user calendars
* and not for course calendars.
*
* @return void
*/
public function sendParticipationStatus() : void
{
if (!($this->user instanceof User)) {
//The calendar date is assigned to a course calendar.
return;
}
if (!$this->participation || $this->participation === 'ACKNOWLEDGED') {
//Nothing shall be done in these two cases.
return;
}
if (empty($this->calendar_date->author->username)) {
//The calendar date has no author.
return;
}
if ($this->range_id === $this->calendar_date->author_id) {
//The author of the date changed their participation status.
//So they know what they did and do not have to be notified.
return;
}
$template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/');
setTempLanguage($this->range_id);
$lang_path = getUserLanguagePath($this->range_id);
$template = $template_factory->open($lang_path . '/LC_MAILS/date_participation.php');
$template->set_attribute('date_assignment', $this);
$mail_text = $template->render();
$subject = '';
if ($this->participation === 'ACCEPTED') {
$subject = sprintf(
_('%1$s hat Ihren Termin am %2$s angenommen'),
$this->user->getFullName(),
date('d.m.Y', $this->calendar_date->begin)
);
} elseif ($this->participation === 'DECLINED') {
$subject = sprintf(
_('%1$s hat Ihren Termin am %2$s abgelehnt'),
$this->user->getFullName(),
date('d.m.Y', $this->calendar_date->begin)
);
}
Message::send(
'____%system%____',
[$this->calendar_date->author->username],
$subject,
$mail_text
);
restoreLanguage();
}
/**
* Retrieves calendar dates inside a specified time range that are present in the calendar of a
* course or user. They can additionally be filtered by the access level and declined events
* can be filtered out, too.
*
* @param DateTime $begin The beginning of the time range.
*
* @param DateTime $end The end of the time range.
*
* @param string $range_id The ID of the course or user whose calendar dates shall be retrieved.
*
* @param array $access_levels The access level filter: Only include calendar dates that have one of the
* access levels in the list.
*
* @param bool $with_declined Include declined calendar dates (true) or filter them out (false).
* Defaults to false.
*
* @return CalendarDateAssignment[] A list of calendar date assignments in the time range that match the filters.
*/
public static function getEvents(
DateTime $begin,
DateTime $end,
string $range_id,
array $access_levels = ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'],
bool $with_declined = false
) : array
{
// Always use the timezone of the server:
$local_timezone = (new DateTime())->getTimezone();
$begin->setTimezone($local_timezone);
$end->setTimezone($local_timezone);
// one whole day as minimum (begin and end time stamp at the same day)
$begin->modify('midnight');
$end->modify('tomorrow -1 second');
$sql = "JOIN `calendar_dates`
ON calendar_date_id = `calendar_dates`.`id`
WHERE
`calendar_date_assignments`.`range_id` = :range_id
AND
`access` IN ( :access_levels ) ";
if (!$with_declined) {
$sql .= "AND `calendar_date_assignments`.`participation` <> 'DECLINED' ";
}
$sql_single = $sql . " AND
`calendar_dates`.`begin` < :end AND :begin < `calendar_dates`.`end`
";
$events = self::findBySql($sql_single, [
'range_id' => $range_id,
'begin' => $begin->getTimestamp(),
'end' => $end->getTimestamp(),
'access_levels' => $access_levels
]);
$sql_repetition = $sql . " AND `calendar_dates`.`begin` < :end AND `calendar_dates`.`repetition_type` IN ('DAYLY', 'WEEKLY', 'MONTHLY', 'YEARLY')
AND `calendar_dates`.`repetition_end` > :begin
";
$events = array_merge($events, self::findBySql($sql_repetition, [
'range_id' => $range_id,
'begin' => $begin->getTimestamp(),
'end' => $end->getTimestamp(),
'access_levels' => $access_levels
]));
$m_start = clone $begin;
$m_end = clone $end;
$events_created = [];
while ($m_start < $m_end) {
foreach ($events as $event) {
$e_start = clone $event->getBegin();
$e_end = clone $event->getEnd();
$e_expire = $event->getExpire();
$cal_start = DateTimeImmutable::createFromMutable($m_start);
$cal_end = DateTimeImmutable::createFromMutable($m_start)->modify('tomorrow -1 second');
$cal_noon = $cal_start->modify('noon');
// single events or first event
if (
($e_start >= $cal_start && $e_end <= $cal_end)
|| ($e_start >= $cal_start && $e_start <= $cal_end)
|| ($e_start < $cal_start && $e_end > $cal_end)
|| ($e_end > $cal_start && $e_start <= $cal_end)
) {
// exception for first event or single event
if (!$event->calendar_date->exceptions->findOneBy('date', $cal_start->format('Y-m-d'))
&& !isset($events_created[$event->calendar_date->id])) {
$events_created[$event->calendar_date->id . '_' . $event->calendar_date->begin] = $event;
}
} elseif ($e_expire > $cal_start) {
$events_created = array_merge($events_created, self::getRepetition($event, $cal_noon));
}
}
$m_start->modify('+1 day');
}
return $events_created;
}
private static function getRepetition(
CalendarDateAssignment $date,
DateTimeImmutable $cal_noon,
bool $calc_prev = true
): array
{
$rep_dates = [];
$ts = $date->getNoonDate();
if ($cal_noon >= $ts) {
if ($date->isRepeatedAtDate($cal_noon)) {
$rep_dates = array_merge($rep_dates, self::createRecurrentDate($date, $cal_noon));
}
if ($calc_prev) {
$rep_noon = $cal_noon->modify(sprintf('-%s days', $date->getDurationDays()));
$rep_dates = array_merge(
$rep_dates,
self::getRepetition(
$date,
$rep_noon,
false
)
);
}
}
return $rep_dates;
}
private function isRepeatedAtDate(DateTimeImmutable $cal_date): bool
{
$ts = $this->getNoonDate();
$pos = 1;
switch ($this->getRepetitionType()) {
case 'DAILY':
$pos = $cal_date->diff($ts)->days % $this->calendar_date->interval;
break;
case 'WEEKLY':
$cal_ts = $cal_date->modify('monday this week noon');
if ($cal_date >= $this->getBegin()) {
$pos = $cal_ts->diff($ts)->days % ($this->calendar_date->interval * 7);
if (
$pos === 0
&& strpos($this->calendar_date->days, $cal_date->format('N')) === false
) {
$pos = 1;
}
}
break;
case 'MONTHLY':
$cal_ts = $cal_date->modify('first day of this month noon');
$diff = $cal_ts->diff($ts);
$pos = ($diff->m + $diff->y * 12) % $this->calendar_date->interval;
if ($pos === 0) {
if (strlen($this->calendar_date->days)) {
$cal_ts_dom = $cal_ts->modify(sprintf('%s %s of this month noon',
$this->calendar_date->getOrdinalName(),
$this->calendar_date->getWeekdayName()));
if ($cal_ts_dom != $cal_date->setTime(12, 0)) {
$pos = 1;
}
} elseif ($this->calendar_date->offset !== $cal_date->format('j')) {
$pos = 1;
}
}
break;
case 'YEARLY':
$cal_ts = $cal_date->modify('first day of this year noon');
$diff = $cal_ts->diff($ts);
$pos = $diff->y % $this->calendar_date->interval;
if ($pos === 0) {
if (strlen($this->calendar_date->days)) {
$ts_doy = $ts->modify(sprintf('%s %s of %s-%s noon',
$this->calendar_date->getOrdinalName(),
$this->calendar_date->getWeekdayName(),
$cal_date->format('Y'),
$this->calendar_date->month));
if ($ts_doy->format('n-j') !== $cal_date->format('n-j')) {
$pos = 1;
}
} elseif (
$cal_date->format('n-j') !== sprintf(
'%s-%s',
$this->calendar_date->month,
$this->calendar_date->offset
)
) {
$pos = 1;
}
}
break;
default:
$pos = 1;
}
//Also check for exceptions before returning:
return $pos === 0
&& !$this->calendar_date->exceptions->findOneBy(
'date',
$cal_date->format('Y-m-d'));
}
private static function createRecurrentDate(
CalendarDateAssignment $date,
DateTimeImmutable $date_time
) : array
{
$date_begin = $date->getBegin();
$date_end = $date->getEnd();
$rec_date = clone $date;
$time_begin = $date_begin->format('H:i:s');
$time_end = $date_end->format('H:i:s');
$rec_date_begin = $date_time->modify(sprintf('today %s', $time_begin));
$rec_date_end = $rec_date_begin->add($date->getDuration())->modify($time_end);
$rec_date->calendar_date->begin = $rec_date_begin->getTimestamp();
$rec_date->calendar_date->end = $rec_date_end->getTimestamp();
$index = $date->calendar_date->id . '_' . $rec_date_begin->getTimestamp();
return [$index => $rec_date];
}
//Event interface implementation:
public function getObjectId() : string
{
return (string)$this->id;
}
public function getPrimaryObjectID(): string
{
return $this->calendar_date_id;
}
public function getObjectClass(): string
{
return static::class;
}
public function getTitle() : string
{
return $this->calendar_date->title ?? '';
}
public function getBegin(): DateTime
{
$begin = new DateTime();
$begin->setTimestamp($this->calendar_date->begin ?? 0);
return $begin;
}
public function getEnd(): DateTime
{
$end = new DateTime();
$end->setTimestamp($this->calendar_date->end ?? 0);
return $end;
}
public function getDuration(): DateInterval
{
$begin = $this->getBegin();
$end = $this->getEnd();
return $begin->diff($end);
}
/**
* Returns the "extent" in days of this date.
*
* @return int The "extent" in days of this date.
*/
public function getDurationDays(): int
{
return self::getExtent($this->getEnd(), $this->getBegin());
}
/**
* Returns the "extent" in days of this date.
* The extent is the number of days a date is displayed in a calendar.
*
* @return int The "extent" in days of this date.
*/
public static function getExtent(DateTimeInterface $date_begin, DateTimeInterface $date_end): int
{
$days_duration = $date_end->diff($date_begin)->days;
if ($date_begin->format('His') > $date_end->format('His')) {
$days_duration += 1;
}
return $days_duration;
}
public function getLocation(): string
{
return $this->calendar_date->location ?? '';
}
public function getUniqueId(): string
{
return $this->calendar_date->unique_id ?? '';
}
public function getDescription(): string
{
return $this->calendar_date->description ?? '';
}
public function getAdditionalDescriptions(): array
{
return [
_('Kategorie') => $this->calendar_date->getCategoryAsString(),
_('Sichtbarkeit') => $this->calendar_date->getVisibilityAsString(),
_('Wiederholung') => $this->calendar_date->getRepetitionAsString()
];
}
public function isAllDayEvent(): bool
{
$begin = $this->getBegin();
if ($begin->format('His') !== '000000') {
return false;
}
$end = $this->getEnd();
return $end->format('His') === '235959';
}
public function isWritable(string $user_id): bool
{
if ($this->calendar_date->author_id === $user_id) {
//The author may always modify one of their dates:
return true;
}
if ($this->calendar_date->isWritable($user_id)) {
//The date is writable.
return true;
}
//The user referenced by $user_id is not the author of the date.
//Check if they have write permissions to the calendar where the date is assigned to:
if ($this->user instanceof User) {
//It is a personal calendar. Check if the owner of the calendar has granted write permissions
//to the user:
return Contact::countBySQL(
"`owner_id` = :owner_id AND `user_id` = :user_id
AND `calendar_permissions` = 'WRITE'",
['owner_id' => $this->range_id, 'user_id' => $user_id]
) > 0;
} elseif ($this->course instanceof Course) {
//It is a course calendar.
return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id);
}
//No write permissions are granted.
return false;
}
public function getCreationDate(): DateTime
{
$mkdate = new DateTime();
$mkdate->setTimestamp($this->calendar_date->mkdate ?? 0);
return $mkdate;
}
public function getModificationDate(): DateTime
{
$chdate = new DateTime();
$chdate->setTimestamp($this->calendar_date->chdate ?? 0);
return $chdate;
}
public function getImportDate(): DateTime
{
$import_date = new DateTime();
$import_date->setTimestamp($this->calendar_date->import_date ?? 0);
return $import_date;
}
public function getAuthor(): ?User
{
return $this->calendar_date->author ?? null;
}
public function getEditor(): ?User
{
return $this->calendar_date->editor ?? null;
}
/**
* TODO calculate end of repetition for different types of repetition
* @return float|int|object
*/
public function getExpire()
{
if ($this->calendar_date->repetition_end > 0) {
$expire = $this->calendar_date->repetition_end;
} else {
$expire = CalendarDate::NEVER_ENDING;
}
$end = new DateTime();
$end->setTimestamp($expire);
return $end;
}
// TODO calculate ts for monthly and yearly repetition
public function getNoonDate()
{
$ts = DateTimeImmutable::createFromMutable($this->getBegin());
switch ($this->calendar_date->repetition_type) {
case 'DAILY':
return $ts->modify('noon');
case 'WEEKLY':
return $ts->modify('monday this week noon');
case 'MONTHLY':
return $ts->modify('first day of this month noon');
case 'YEARLY':
return $ts->modify('first day of this year noon');
default:
return $ts;
}
}
/**
* Returns the type of repetition.
*
* @return string The type of repetition.
*/
public function getRepetitionType(): string
{
return $this->calendar_date->repetition_type;
}
public function toEventData(string $user_id): \Studip\Calendar\EventData
{
$begin = $this->getBegin();
$end = $this->getEnd();
$all_day = $this->isAllDayEvent();
$hide_confidential_data = $this->calendar_date->access === 'CONFIDENTIAL'
&& $user_id !== $this->calendar_date->author_id;
$event_classes = ['user-date'];
$text_colour = '#000000';
$background_colour = '#ffffff';
$border_colour = '#000000';
if (!$hide_confidential_data) {
if ($this->calendar_date->user_category) {
//The date belongs to a personal category that gets a grey colour.
$background_colour = '#a7abaf';
$border_colour = '#a7abaf';
} else {
//The date belongs to a system category that has its own colours.
$text_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['fgcolor'] ?? $text_colour;
$background_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['bgcolor'] ?? $background_colour;
$border_colour = $GLOBALS['PERS_TERMIN_KAT'][$this->calendar_date->category]['border_color'] ?? $border_colour;
$event_classes[] = sprintf('user-date-category%d', $this->calendar_date->category);
}
}
$show_url_params = [];
if ($this->calendar_date->repetition_type) {
$show_url_params['selected_date'] = $begin->format('Y-m-d');
}
return new \Studip\Calendar\EventData(
$begin,
$end,
!$hide_confidential_data ? $this->getTitle() : '',
$event_classes,
$text_colour,
$background_colour,
$this->isWritable($user_id),
CalendarDateAssignment::class,
$this->id,
CalendarDate::class,
$this->calendar_date_id,
'user',
$this->range_id ?? '',
[
'show' => URLHelper::getURL('dispatch.php/calendar/date/index/' . $this->calendar_date_id, $show_url_params)
],
[
'resize_dialog' => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id),
'move_dialog' => URLHelper::getURL('dispatch.php/calendar/date/move/' . $this->calendar_date_id)
],
$this->participation === 'DECLINED' ? 'decline-circle-full' : '',
$border_colour,
$all_day
);
}
public function getRangeName() : string
{
if ($this->course instanceof Course) {
return $this->course->getFullName();
} elseif ($this->user instanceof User) {
return $this->user->getFullName();
}
return '';
}
public function getRangeAvatar() : ?Avatar
{
if ($this->course instanceof Course) {
return CourseAvatar::getAvatar($this->range_id);
} elseif ($this->user instanceof User) {
return Avatar::getAvatar($this->range_id);
}
return null;
}
public function getParticipationAsString() : string
{
if ($this->participation === '') {
return _('Abwartend');
} elseif ($this->participation === 'ACKNOWLEDGED') {
return _('Angenommen (keine Teilnahme)');
} elseif ($this->participation === 'ACCEPTED') {
return _('Angenommen');
} elseif ($this->participation === 'DECLINED') {
return _('Abgelehnt');
}
return '';
}
}