aboutsummaryrefslogtreecommitdiff
path: root/lib/models/calendar/CalendarDateAssignment.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/models/calendar/CalendarDateAssignment.php')
-rw-r--r--lib/models/calendar/CalendarDateAssignment.php722
1 files changed, 722 insertions, 0 deletions
diff --git a/lib/models/calendar/CalendarDateAssignment.php b/lib/models/calendar/CalendarDateAssignment.php
new file mode 100644
index 0000000..e0e2135
--- /dev/null
+++ b/lib/models/calendar/CalendarDateAssignment.php
@@ -0,0 +1,722 @@
+<?php
+/**
+ * CalendarDateAssignment.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\Factory($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\Factory($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\Factory($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 ('DAILY', '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 '';
+ }
+}