Skip to content
Snippets Groups Projects
Select Git revision
  • 9bbf1bda70c66ad3a53cbe5d3c0f4c9696b9396d
  • main default protected
  • studip-rector
  • ci-opt
  • course-members-export-as-word
  • data-vue-app
  • pipeline-improvements
  • webpack-optimizations
  • rector
  • icon-renewal
  • http-client-and-factories
  • jsonapi-atomic-operations
  • vueify-messages
  • tic-2341
  • 135-translatable-study-areas
  • extensible-sorm-action-parameters
  • sorm-configuration-trait
  • jsonapi-mvv-routes
  • docblocks-for-magic-methods
19 results

edit.php

Blame
  • 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 '';
        }
    }