diff --git a/app/controllers/calendar/calendar.php b/app/controllers/calendar/calendar.php index 2e17ff2eec5c7162d6def7d42bb887e932a49cce..eecf90016a393a74bebc00c52ff41fa3020c00b4 100644 --- a/app/controllers/calendar/calendar.php +++ b/app/controllers/calendar/calendar.php @@ -1,636 +1,896 @@ <?php -/* - * The controller for the personal 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since - */ class Calendar_CalendarController extends AuthenticatedController { public function before_filter(&$action, &$args) { parent::before_filter($action, $args); - PageLayout::setHelpKeyword('Basis.Terminkalender'); - $this->settings = $GLOBALS['user']->cfg->CALENDAR_SETTINGS; - if ($this->settings['start'] < 0 || $this->settings['start'] > 23) { - $this->settings['start'] = 0; - } - if ($this->settings['end'] < 0 || $this->settings['end'] > 23) { - $this->settings['end'] = 23; - } - if (!in_array($this->settings['view'], ['day','week','month','year'])) { - $this->settings['view'] = 'week'; - } - if (!is_array($this->settings)) { - $this->settings = Calendar::getDefaultUserSettings(); - } - URLHelper::bindLinkParam('atime', $this->atime); - $this->atime = Request::int('atime', time()); - $this->category = Request::int('category'); - $this->last_view = Request::option('last_view', - $this->settings['view']); - $this->action = $action; - $this->restrictions = [ - 'STUDIP_CATEGORY' => $this->category ?: null, - // hide events with status 3 (CalendarEvent::PARTSTAT_DECLINED) - 'STUDIP_GROUP_STATUS' => !empty($this->settings['show_declined']) ? null : [0,1,2,5] - ]; - if ($this->category) { - URLHelper::bindLinkParam('category', $this->category); - } - - $this->range_id = ''; - if (Config::get()->COURSE_CALENDAR_ENABLE - && !Request::get('self') - && Course::findCurrent()) { - $current_seminar = new Seminar(Course::findCurrent()); - if ($current_seminar->getSlotModule('calendar') instanceOf CoreCalendar) { - $this->range_id = $current_seminar->id; - Navigation::activateItem('/course/calendar'); - } - } - if (!$this->range_id) { - $this->range_id = Request::option('range_id', $GLOBALS['user']->id); - Navigation::activateItem('/calendar/calendar'); - URLHelper::bindLinkParam('range_id', $this->range_id); + if (!Context::isCourse() && Navigation::hasItem('/calendar')) { + Navigation::activateItem('/calendar'); } - - URLHelper::bindLinkParam('last_view', $this->last_view); } - protected function createSidebar($active = null, $calendar = null) - { - $active = $active ?: $this->last_view; - $sidebar = Sidebar::Get(); - - $views = new ViewsWidget(); - $views->addLink(_('Tag'), $this->url_for($this->base . 'day')) - ->setActive($active == 'day'); - $views->addLink(_('Woche'), $this->url_for($this->base . 'week')) - ->setActive($active == 'week'); - $views->addLink(_('Monat'), $this->url_for($this->base . 'month')) - ->setActive($active == 'month'); - $views->addLink(_('Jahr'), $this->url_for($this->base . 'year')) - ->setActive($active == 'year'); - $sidebar->addWidget($views); - } - protected function createSidebarFilter() + protected function buildSidebar($schedule = false) { - $tmpl_factory = $this->get_template_factory(); - - $filters = new SidebarWidget(); - $filters->setTitle('Auswahl'); - - $tmpl = $tmpl_factory->open('calendar/single/_jump_to'); - $tmpl->atime = $this->atime; - $tmpl->action = $this->action; - $tmpl->action_url = $this->url_for('calendar/single/jump_to'); - $filters->addElement(new WidgetElement($tmpl->render())); - - $tmpl = $tmpl_factory->open('calendar/single/_select_category'); - $tmpl->action_url = $this->url_for(); - $tmpl->category = $this->category; - $filters->addElement(new WidgetElement($tmpl->render())); - Sidebar::get()->addWidget($filters); - - if (Config::get()->CALENDAR_GROUP_ENABLE - || Config::get()->COURSE_CALENDAR_ENABLE) { - $tmpl = $tmpl_factory->open('calendar/single/_select_calendar'); - $tmpl->range_id = $this->range_id; - $tmpl->action_url = $this->url_for('calendar/group/switch'); - $tmpl->view = $this->action; - $filters->addElement(new WidgetElement($tmpl->render())); - - $settings = new OptionsWidget(); - $settings->addCheckbox( - _('Abgelehnte Termine anzeigen'), - $this->settings['show_declined'] ?? false, - $this->url_for($this->base . 'show_declined', ['show_declined' => 1]), - $this->url_for($this->base . 'show_declined', ['show_declined' => 0]) + $sidebar = Sidebar::get(); + + $actions = new ActionsWidget(); + if ($schedule) { + $actions->addLink( + _('Neuer Eintrag'), + $this->url_for('calendar/calendar/add_schedule_entry'), + Icon::create('add'), + ['data-dialog' => 'size=default'] + ); + } else { + $actions->addLink( + _('Termin anlegen'), + $this->url_for('calendar/date/add'), + Icon::create('add'), + ['data-dialog' => 'size=auto'] ); - Sidebar::get()->addWidget($settings); + } + + if (!$GLOBALS['perm']->have_perm('admin')) { + $actions->addLink( + _('Veranstaltung auswählen'), + $this->url_for('calendar/calendar/add_courses'), + Icon::create('add'), + ['data-dialog' => 'size=medium'] + ); + } + if (!$schedule) { + $actions->addLink( + _('Termine exportieren'), + $this->url_for('calendar/calendar/export'), + Icon::create('export'), + ['data-dialog' => 'size=auto'] + ); + $actions->addLink( + _('Termine importieren'), + $this->url_for('calendar/calendar/import'), + Icon::create('import'), + ['data-dialog' => 'size=auto'] + ); + $actions->addLink( + _('Kalender veröffentlichen'), + $this->url_for('calendar/calendar/publish'), + Icon::create('export'), + ['data-dialog' => 'size=auto'] + ); + } + if (!$schedule && Config::get()->CALENDAR_GROUP_ENABLE) { + $actions->addLink( + _('Kalender teilen'), + $this->url_for('calendar/calendar/share'), + Icon::create('share'), + ['data-dialog' => 'size=default'] + ); + } + $actions->addLink( + _('Drucken'), + 'javascript:void(window.print());', + Icon::create('print') + ); + $actions->addLink( + _('Einstellungen'), + $this->url_for('settings/calendar'), + Icon::create('settings'), + ['data-dialog' => 'size=auto;reload-on-close'] + ); + $sidebar->addWidget($actions); + + if (!$schedule) { + $date = new DateSelectWidget(); + $date->setDate(\Studip\Calendar\Helper::getDefaultCalendarDate()); + $date->setCalendarControl(true); + $sidebar->addWidget($date); } } - public function index_action() + protected function getUserCalendarSlotSettings() : array { - // switch to the view the user has selected in his personal settings - $default_view = $this->settings['view'] ?: 'week'; - - // Remove cid - if (Request::option('self')) { - Context::close(); - - $this->redirect(URLHelper::getURL('dispatch.php/' . $this->base - . $default_view . '/' . $GLOBALS['user']->id, [], true)); - } else { - $this->redirect(URLHelper::getURL('dispatch.php/' . $this->base - . $default_view)); - } + return [ + 'day' => \Studip\Calendar\Helper::getCalendarSlotDuration('day'), + 'week' => \Studip\Calendar\Helper::getCalendarSlotDuration('week'), + 'day_group' => \Studip\Calendar\Helper::getCalendarSlotDuration('day_group'), + 'week_group' => \Studip\Calendar\Helper::getCalendarSlotDuration('week_group') + ]; } - public function edit_action($range_id = null, $event_id = null) + public function index_action() { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - $this->event = $this->calendar->getEvent($event_id); - - if ($this->event->isNew()) { - // $this->event = $this->calendar->getNewEvent(); - if (Request::get('isdayevent')) { - $this->event->setStart(mktime(0, 0, 0, date('n', $this->atime), - date('j', $this->atime), date('Y', $this->atime))); - $this->event->setEnd(mktime(23, 59, 59, date('n', $this->atime), - date('j', $this->atime), date('Y', $this->atime))); + PageLayout::setTitle(_('Kalender')); + + if (Request::isPost()) { + //In case the checkbox of the options widget is clicked, the resulting + //POST request must be catched here and result in a redirect. + CSRFProtection::verifyUnsafeRequest(); + if (Request::bool('show_declined')) { + $this->redirect('calendar/calendar', ['show_declined' => '1']); } else { - $this->event->setStart($this->atime); - $this->event->setEnd($this->atime + 3600); + $this->redirect('calendar/calendar'); } - $this->event->setAuthorId($GLOBALS['user']->id); - $this->event->setEditorId($GLOBALS['user']->id); - $this->event->setAccessibility('PRIVATE'); - if (!Request::isXhr()) { - PageLayout::setTitle($this->getTitle($this->calendar, _('Neuer Termin'))); + return; + } + + if (!Context::isCourse() && Navigation::hasItem('/calendar/calendar')) { + Navigation::activateItem('/calendar/calendar'); + } + + $view = Request::get('view', 'single'); + $group_view = false; + $timeline_view = false; + if (Config::get()->CALENDAR_GROUP_ENABLE) { + $group_view = in_array($view, ['group', 'timeline']); + $timeline_view = $view === 'timeline'; + } + + $calendar_owner = null; + $selected_group = null; + $user_id = Request::option('user_id', User::findCurrent()->id); + $group_id = Request::option('group_id'); + + if (Config::get()->CALENDAR_GROUP_ENABLE) { + if ($group_id) { + $selected_group = ContactGroup::find($group_id); + if ($selected_group->owner_id !== User::findCurrent()->id) { + //Thou shalt not see the groups of others! + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } + $view = $view === 'timeline' ? 'timeline' : 'group'; + } elseif ($user_id) { + $calendar_owner = User::getCalendarOwner($user_id); + $view = 'single'; } } else { - // open read only events and course events not as form - // show information in dialog instead - if (!$this->event->havePermission(Event::PERMISSION_WRITABLE) - || $this->event instanceof CourseEvent) { - if (!$this->event instanceof CourseEvent && $this->event->attendees->count() > 1) { - if ($this->event->group_status) { - $this->redirect($this->url_for('calendar/single/edit_status/' . implode('/', - [$this->range_id, $this->event->event_id]))); - } else { - $this->redirect($this->url_for('calendar/single/event/' . implode('/', - [$this->range_id, $this->event->event_id]))); + //Show the calendar of the current user. + $view = 'single'; + $calendar_owner = User::findCurrent(); + } + + //Check for permissions: + $read_permissions = false; + $write_permissions = false; + if ($calendar_owner) { + $read_permissions = $calendar_owner->isCalendarReadable(); + $write_permissions = $calendar_owner->isCalendarWritable(); + } elseif ($selected_group) { + //Count on how many group member calendars the current user has read or write permissions: + foreach ($selected_group->items as $item) { + if ($item->user) { + if ($item->user->isCalendarReadable()) { + $read_permissions = true; + } + if ($item->user->isCalendarWritable()) { + $write_permissions = true; } - } else { - $this->redirect($this->url_for('calendar/single/event/' . implode('/', - [$this->range_id, $this->event->event_id]))); } - return null; - } - if (!Request::isXhr()) { - PageLayout::setTitle($this->getTitle($this->calendar, _('Termin bearbeiten'))); + if ($read_permissions && $write_permissions) { + //We only need to determine one read and one write permission to set the relevant fullcalendar + //properties. The action to add/edit a date determines in which calendars the current user + //may write into. + break; + } } } + if (!$read_permissions) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } - if (Config::get()->CALENDAR_GROUP_ENABLE - && $this->calendar->getRange() == Calendar::RANGE_USER) { - - if (Config::get()->CALENDAR_GRANT_ALL_INSERT) { - $search_obj = SQLSearch::get("SELECT DISTINCT auth_user_md5.user_id, " - . "{$GLOBALS['_fullname_sql']['full_rev_username']} as fullname, " - . "auth_user_md5.perms, auth_user_md5.username " - . "FROM auth_user_md5 " - . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) " - . 'WHERE auth_user_md5.user_id <> ' . DBManager::get()->quote($GLOBALS['user']->id) - . ' AND (username LIKE :input OR Vorname LIKE :input ' - . "OR CONCAT(Vorname,' ',Nachname) LIKE :input " - . "OR CONCAT(Nachname,' ',Vorname) LIKE :input " - . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input " - . ") ORDER BY fullname ASC", - _('Person suchen'), 'user_id'); - } else { - $search_obj = SQLSearch::get("SELECT DISTINCT auth_user_md5.user_id, " - . "{$GLOBALS['_fullname_sql']['full_rev_username']} as fullname, " - . "auth_user_md5.perms, auth_user_md5.username " - . "FROM calendar_user " - . "LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id " - . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) " - . 'WHERE calendar_user.user_id = ' - . DBManager::get()->quote($GLOBALS['user']->id) - . ' AND calendar_user.permission > ' . Event::PERMISSION_READABLE - . ' AND auth_user_md5.user_id <> ' . DBManager::get()->quote($GLOBALS['user']->id) - . ' AND (username LIKE :input OR Vorname LIKE :input ' - . "OR CONCAT(Vorname,' ',Nachname) LIKE :input " - . "OR CONCAT(Nachname,' ',Vorname) LIKE :input " - . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input " - . ") ORDER BY fullname ASC", - _('Person suchen'), 'user_id'); + $this->buildSidebar(false); + + $sidebar = Sidebar::get(); + + if (Config::get()->CALENDAR_GROUP_ENABLE) { + if ($calendar_owner && $calendar_owner->id === User::findCurrent()->id) { + //The user is viewing their own calendar. + $options = new OptionsWidget(); + $options->addCheckbox( + _('Abgelehnte Termine anzeigen'), + Request::bool('show_declined'), + $this->url_for('calendar/calendar', ['show_declined' => '1']), + $this->url_for('calendar/calendar') + ); + $sidebar->addWidget($options); } - // SEMBBS - // Eintrag von Terminen bereits ab PERMISSION_READABLE - /* - $search_obj = new SQLSearch('SELECT DISTINCT auth_user_md5.user_id, ' - . $GLOBALS['_fullname_sql']['full_rev'] . ' as fullname, username, perms ' - . 'FROM calendar_user ' - . 'LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id ' - . 'LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) ' - . 'WHERE calendar_user.user_id = ' - . DBManager::get()->quote($GLOBALS['user']->id) - . ' AND calendar_user.permission >= ' . Event::PERMISSION_READABLE - . ' AND (username LIKE :input OR Vorname LIKE :input ' - . "OR CONCAT(Vorname,' ',Nachname) LIKE :input " - . "OR CONCAT(Nachname,' ',Vorname) LIKE :input " - . 'OR Nachname LIKE :input OR ' - . $GLOBALS['_fullname_sql']['full_rev'] . ' LIKE :input ' - . ') ORDER BY fullname ASC', - _('Nutzer suchen'), 'user_id'); - // SEMBBS - * - */ - - - $this->quick_search = QuickSearch::get('user_id', $search_obj) - ->fireJSFunctionOnSelect('STUDIP.Messages.add_adressee') - ->withButton(); - - // $default_selected_user = array($this->calendar->getRangeId()); - $this->mps = MultiPersonSearch::get('add_adressees') - ->setLinkText(_('Mehrere Teilnehmende hinzufügen')) - // ->setDefaultSelectedUser($default_selected_user) - ->setTitle(_('Mehrere Teilnehmende hinzufügen')) - ->setExecuteURL($this->url_for($this->base . 'edit')) - ->setJSFunctionOnSubmit('STUDIP.Messages.add_adressees') - ->setSearchObject($search_obj); - $owners = SimpleORMapCollection::createFromArray( - CalendarUser::findByUser_id($this->calendar->getRangeId())) - ->pluck('owner_id'); - foreach (Calendar::getGroups($GLOBALS['user']->id) as $group) { - $this->mps->addQuickfilter( - $group->name, - $group->members->filter( - function ($member) use ($owners) { - if (in_array($member->user_id, $owners)) { - return $member; - } - })->pluck('user_id') + //Check if the user has groups. If so, display a select widget to select a group. + $groups = ContactGroup::findBySQL( + 'owner_id = :owner_id ORDER BY name ASC', + [ + 'owner_id' => User::findCurrent()->id + ] + ); + if ($groups) { + $available_groups = []; + + //Check if the user has at least read permissions for the calendar of one user of one group: + foreach ($groups as $group) { + foreach ($group->items as $item) { + if ($item->user && $item->user->isCalendarReadable()) { + $available_groups[] = $group; + break 1; + } + } + } + if ($available_groups) { + $group_select = new SelectWidget( + _('Gruppe'), + $this->url_for('calendar/calendar/index', ['view' => 'group']), + 'group_id' + ); + $options = [ + '' => _('(bitte wählen)') + ]; + foreach ($available_groups as $available_group) { + $options[$available_group->id] = $available_group->name; + } + $group_select->setOptions($options); + $group_select->setSelection($group_id); + $sidebar->addWidget($group_select); + } + } + //Get all calendars where the user has access to: + $other_users = User::findBySql( + "INNER JOIN `contact` c + ON `auth_user_md5`.`user_id` = c.`owner_id` + WHERE c.`user_id` = :current_user_id + AND c.`calendar_permissions` <> '' + ORDER BY `auth_user_md5`.`Vorname` ASC, `auth_user_md5`.`Nachname` ASC", + ['current_user_id' => User::findCurrent()->id] + ); + if ($other_users) { + $calendar_select = new SelectWidget( + _('Kalender auswählen'), + $this->url_for('calendar/calendar'), + 'user_id' ); + $select_options = [ + '' => _('(bitte wählen)'), + User::findCurrent()->id => _('Eigener Kalender') + ]; + foreach ($other_users as $user) { + $select_options[$user->id] = $user->getFullName(); + } + $calendar_select->setOptions($select_options, Request::get('user_id')); + $sidebar->addWidget($calendar_select); } } - $stored = false; - if (Request::submitted('store')) { - $stored = $this->storeEventData($this->event, $this->calendar); + if (Config::get()->CALENDAR_GROUP_ENABLE && $selected_group) { + $views = new ViewsWidget(); + $views->setTitle(_('Kalenderansicht')); + $views->addLink( + _('Gruppenkalender'), + $this->url_for('calendar/calendar', ['view' => 'group', 'group_id' => $group_id]) + )->setActive($view === 'group'); + $views->addLink( + _('Zeitleiste'), + $this->url_for('calendar/calendar', ['view' => 'timeline', 'group_id' => $group_id]) + )->setActive($view === 'timeline'); + $sidebar->addWidget($views); } - if ($stored !== false) { - if ($stored === 0) { - if (Request::isXhr()) { - header('X-Dialog-Close: 1'); - exit; - } else { - PageLayout::postMessage(MessageBox::success(_('Der Termin wurde nicht geändert.'))); - $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]); + $calendar_resources = []; + $calendar_group_title = ''; + if ($group_view && $selected_group) { + //All users in the selected group that have granted read permissions to the user can be shown. + foreach ($selected_group->items as $item) { + if ($item->user && $item->user->isCalendarReadable()) { + $calendar_resources[] = [ + 'id' => $item->user_id, + 'title' => $item->user ? $item->user->getFullName() : '', + 'parent_name' => '' + ]; } - } else { - PageLayout::postMessage(MessageBox::success(_('Der Termin wurde gespeichert.'))); - $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]); } + $calendar_group_title = $selected_group->name; } - $this->createSidebar('edit', $this->calendar); - $this->createSidebarFilter(); + $fullcalendar_studip_urls = []; + if ($write_permissions) { + if ($calendar_owner) { + $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add', ['user_id' => $calendar_owner->id]); + } elseif ($selected_group) { + $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add', ['group_id' => $group->id]); + } + } + + $calendar_settings = User::findCurrent()->getConfiguration()->CALENDAR_SETTINGS ?? []; + + //Map calendar settings to fullcalendar settings: + + $default_view = 'timeGridWeek'; + if ($timeline_view) { + $default_view = 'resourceTimelineWeek'; + if ($calendar_settings['view'] === 'day') { + $default_view = 'resourceTimelineDay'; + } + } elseif (!empty($calendar_settings['view'])) { + if ($calendar_settings['view'] === 'day') { + $default_view = 'timeGridDay'; + } elseif ($calendar_settings['view'] === 'month') { + $default_view = 'dayGridMonth'; + } + } + + $slot_durations = $this->getUserCalendarSlotSettings(); + + //Create the fullcalendar object: + $default_date = \Studip\Calendar\Helper::getDefaultCalendarDate(); + + $data_url_params = []; + if (Request::bool('show_declined')) { + $data_url_params['show_declined'] = '1'; + } + if ($timeline_view) { + $data_url_params['timeline_view'] = '1'; + } + + $this->fullcalendar = Studip\Fullcalendar::create( + _('Kalender'), + [ + 'editable' => $write_permissions, + 'selectable' => $write_permissions, + 'studip_urls' => $fullcalendar_studip_urls, + 'dialog_size' => 'auto', + 'minTime' => sprintf('%02u:00', $calendar_settings['start'] ?? 8), + 'maxTime' => sprintf('%02u:00', $calendar_settings['end'] ?? 20), + 'defaultDate' => $default_date->format('Y-m-d'), + 'allDaySlot' => !$group_view, + 'allDayText' => '', + 'header' => [ + 'left' => ( + $timeline_view + ? 'resourceTimelineWeek,resourceTimelineDay' + : 'dayGridYear,dayGridMonth,timeGridWeek,timeGridDay' + ), + 'right' => 'prev,today,next' + ], + 'weekNumbers' => true, + 'views' => [ + 'dayGridMonth' => [ + 'eventTimeFormat' => ['hour' => 'numeric', 'minute' => '2-digit'], + 'displayEventEnd' => true + ], + 'timeGridWeek' => [ + 'columnHeaderFormat' => ['weekday' => 'short', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true], + 'weekends' => $calendar_settings['type_week'] === 'LONG', + 'slotDuration' => $slot_durations['week'] + ], + 'timeGridDay' => [ + 'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true], + 'slotDuration' => $slot_durations['day'] + ], + 'resourceTimelineWeek' => [ + 'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true], + 'titleFormat' => ['year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit'], + 'weekends' => $calendar_settings['type_week'] === 'LONG', + 'slotDuration' => $slot_durations['week_group'] + ], + 'resourceTimelineDay' => [ + 'columnHeaderFormat' => ['weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true], + 'titleFormat' => ['year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit'], + 'slotDuration' => $slot_durations['day_group'] + ] + ], + 'defaultView' => $default_view, + 'timeGridEventMinHeight' => 20, + 'eventSources' => [ + [ + 'url' => $this->url_for( + ( + $group_view + ? 'calendar/calendar/calendar_group_data/' . $selected_group->id + : 'calendar/calendar/calendar_data/' . $calendar_owner->id + ), + $data_url_params + ), + 'method' => 'GET', + 'extraParams' => [] + ] + ], + 'resources' => $calendar_resources, + 'resourceLabelText' => $calendar_group_title + ] + ); } - public function edit_status_action($range_id, $event_id) + public function course_action($course_id) { - global $user; + PageLayout::setTitle(_('Veranstaltungskalender')); - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - $this->event = $this->calendar->getEvent($event_id); - $stored = false; - $old_status = $this->event->group_status; + if (!$course_id || !Config::get()->CALENDAR_GROUP_ENABLE || !Config::get()->COURSE_CALENDAR_ENABLE) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } - if (Request::submitted('store')) { + $course = Course::find($course_id); + if (!$course) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } - if ($this->event->isNew() - || !Config::get()->CALENDAR_GROUP_ENABLE - || !$this->calendar->havePermission(Calendar::PERMISSION_OWN) - || !$this->calendar->getRange() == Calendar::RANGE_USER - || !$this->event->havePermission(Event::PERMISSION_READABLE)) { - throw new AccessDeniedException(); + if (!$course->isVisibleForUser() || !$course->isCalendarReadable()) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } + + if (Navigation::hasItem('/course/calendar')) { + Navigation::activateItem('/course/calendar'); + } + + $sidebar = Sidebar::get(); + + $actions = new ActionsWidget(); + $actions->addLink( + _('Termin anlegen'), + $this->url_for('calendar/date/add/course_' . $course->id), + Icon::create('add'), + ['data-dialog' => 'size=default'] + ); + $actions->addLink( + _('Drucken'), + 'javascript:void(window.print());', + Icon::create('print') + ); + $actions->addLink( + _('Einstellungen'), + $this->url_for('settings/calendar'), + Icon::create('settings'), + ['data-dialog' => 'reload-on-close'] + ); + $sidebar->addWidget($actions); + + $date = new DateSelectWidget(); + $date->setCalendarControl(true); + $sidebar->addWidget($date); + + //Create the fullcalendar object: + + $calendar_writable = $course->isCalendarWritable(); + $calendar_settings = User::findCurrent()->getConfiguration()->CALENDAR_SETTINGS ?? []; + $slot_settings = $this->getUserCalendarSlotSettings(); + + $fullcalendar_studip_urls = []; + if ($calendar_writable) { + $fullcalendar_studip_urls['add'] = $this->url_for('calendar/date/add/course_' . $course->id); + } + + $this->fullcalendar = Studip\Fullcalendar::create( + _('Veranstaltungskalender'), + [ + 'editable' => $calendar_writable, + 'selectable' => $calendar_writable, + 'studip_urls' => $fullcalendar_studip_urls, + 'minTime' => sprintf('%02u:00', $calendar_settings['start'] ?? 8), + 'maxTime' => sprintf('%02u:00', $calendar_settings['end'] ?? 20), + 'allDaySlot' => true, + 'allDayText' => '', + 'header' => [ + 'left' => 'dayGridYear,dayGridMonth,timeGridWeek,timeGridDay', + 'right' => 'prev,today,next' + ], + 'weekNumbers' => true, + 'views' => [ + 'dayGridMonth' => [ + 'eventTimeFormat' => ['hour' => 'numeric', 'minute' => '2-digit'], + 'displayEventEnd' => true + ], + 'timeGridWeek' => [ + 'columnHeaderFormat' => [ 'weekday' => 'short', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true ], + 'weekends' => $calendar_settings['type_week'] === 'LONG', + 'slotDuration' => $slot_settings['week'] + ], + 'timeGridDay' => [ + 'columnHeaderFormat' => [ 'weekday' => 'long', 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit', 'omitCommas' => true ], + 'slotDuration' => $slot_settings['day'] + ] + ], + 'defaultView' => 'timeGridWeek', + 'timeGridEventMinHeight' => 20, + 'eventSources' => [ + [ + 'url' => $this->url_for('calendar/calendar/calendar_data/course_' . $course->id), + 'method' => 'GET', + 'extraParams' => [] + ] + ] + ] + ); + } + + public function calendar_data_action($range_and_id) + { + $range_and_id = explode('_', $range_and_id); + $range = ''; + $range_id = ''; + if (!empty($range_and_id[1])) { + $range = $range_and_id[0]; + $range_id = $range_and_id[1]; + } + if (!$range) { + //Show the personal calendar of the current user: + $range = 'user'; + $range_id = User::findCurrent()->id; + } + $owner = null; + if (!$range_id) { + //Assume a user calendar. $range contains the user-ID. + $owner = User::getCalendarOwner($range); + } elseif ($range === 'user') { + $owner = User::getCalendarOwner($range_id); + } elseif ($range === 'course') { + $owner = Course::getCalendarOwner($range_id); + } + + if (!$owner || !$owner->isCalendarReadable()) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } + + $begin = Request::getDateTime('start', \DateTime::RFC3339); + $end = Request::getDateTime('end', \DateTime::RFC3339); + if (!($begin instanceof \DateTime) || !($end instanceof \DateTime)) { + //No time range specified. + throw new InvalidArgumentException('Invalid parameters!'); + } + + $calendar_events = CalendarDateAssignment::getEvents( + $begin, + $end, + $owner->id, + ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'], + Request::bool('show_declined', false) + ); + + $result = []; + + foreach ($calendar_events as $date) { + $event = $date->toEventData(User::findCurrent()->id); + $result[] = $event->toFullcalendarEvent(); + } + + if ($range === 'user') { + //Include course dates of courses that shall be displayed in the calendar: + $course_dates = CalendarCourseDate::getEvents($begin, $end, $owner->id); + foreach ($course_dates as $course_date) { + $event = $course_date->toEventData(User::findCurrent()->id); + $event->background_colour = '#ffffff'; + $event->text_colour = '#000000'; + $event->border_colour = '#000000'; + $event->event_classes = []; + $result[] = $event->toFullcalendarEvent(); + } + //Include relevant cancelled course dates: + $cancelled_course_dates = CalendarCourseExDate::getEvents($begin, $end, $owner->id); + foreach ($cancelled_course_dates as $cancelled_course_date) { + $event = $cancelled_course_date->toEventData(User::findCurrent()->id); + $event->background_colour = '#ffffff'; + $event->text_colour = '#000000'; + $event->border_colour = '#000000'; + $event->event_classes = []; + $result[] = $event->toFullcalendarEvent(); } + } + //At this point, everything went fine. We can save the beginning as default date + //if the current user is looking at their own calendar: + if ($owner instanceof User && $owner->id === User::findCurrent()->id) { + $_SESSION['calendar_date'] = $begin->format('Y-m-d'); + } + $this->render_json($result); + } - $status = Request::int('status', 1); - if ($status > 0 && $status < 6) { - $this->event->group_status = $status; - $stored = $this->event->store(); + public function calendar_group_data_action($group_id) + { + $begin = Request::getDateTime('start', \DateTime::RFC3339); + $end = Request::getDateTime('end', \DateTime::RFC3339); + $timeline_view = Request::bool('timeline_view', false); + + if (!($begin instanceof \DateTime) || !($end instanceof \DateTime)) { + //No time range specified. + throw new InvalidArgumentException('Invalid parameters!'); + } + + $group = null; + $users = []; + if ($group_id) { + //Get the group first: + $group = ContactGroup::find($group_id); + if ($group->owner_id !== User::findCurrent()->id) { + throw new AccessDeniedException(); + } + foreach ($group->items as $item) { + if ($item->user->isCalendarReadable()) { + $users[] = $item->user; + } + } + if (!$users) { + //No user has granted read access to the calendar for the current user. + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); } + } - if ($stored !== false) { - if ($stored === 0) { - if (Request::isXhr()) { - header('X-Dialog-Close: 1'); - exit; - } else { - PageLayout::postMessage(MessageBox::success(_('Der Teilnahmestatus wurde nicht geändert.'))); - $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]); - } - } else { - // send message to organizer... - if ($this->event->author_id != $user->id) { - setTempLanguage($this->event->author_id); - $message = new messaging(); - $msg_text = sprintf(_('%s hat den Terminvorschlag für "%s" am %s von %s auf %s geändert.'), - get_fullname(), $this->event->getTitle(), - strftime('%c', $this->event->getStart()), - $this->event->toStringGroupStatus($old_status), $this->event->toStringGroupStatus()); - if ($status == CalendarEvent::PARTSTAT_DELEGATED) { - $msg_text .= "\n" - . sprintf(_('Der Termin wird akzeptiert, aber %s nimmt nicht selbst am Termin teil.'), - get_fullname()); - } - $subject = sprintf(_('Terminvorschlag am %s von %s %s'), - strftime('%c', $this->event->getStart()), get_fullname(), $this->event->toStringGroupStatus()); - $msg_text .= "\n\n**" . _('Beginn') . ':** '; - if ($this->event->isDayEvent()) { - $msg_text .= strftime('%x ', $this->event->getStart()); - $msg_text .= _('ganztägig'); - } else { - $msg_text .= strftime('%c', $this->event->getStart()); - } - $msg_text .= "\n**" . _('Ende') . ':** '; - if ($this->event->isDayEvent()) { - $msg_text .= strftime('%x ', $this->event->getEnd()); - } else { - $msg_text .= strftime('%c', $this->event->getEnd()); - } - $msg_text .= "\n**" . _('Zusammenfassung') . ':** ' . $this->event->getTitle() . "\n"; - if ($event_data = $this->event->getDescription()) { - $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n"; - } - if ($event_data = $this->event->toStringCategories()) { - $msg_text .= '**' . _('Kategorie') . ":** $event_data\n"; - } - if ($event_data = $this->event->toStringPriority()) { - $msg_text .= '**' . _('Priorität') . ":** $event_data\n"; - } - if ($event_data = $this->event->toStringAccessibility()) { - $msg_text .= '**' . _('Zugriff') . ":** $event_data\n"; - } - if ($event_data = $this->event->toStringRecurrence()) { - $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n"; - } - $member = []; - foreach ($this->event->attendees as $attendee) { - if ($attendee->range_id == $this->event->getAuthorId()) { - $member[] = $attendee->user->getFullName() - . ' ('. _('Organisator') . ')'; - } else { - $member[] = $attendee->user->getFullName() - . ' (' . $this->event->toStringGroupStatus($attendee->group_status) - . ')'; - } - } - $msg_text .= '**' . _('Teilnehmende') . ':** ' . implode(', ', $member); - $msg_text .= "\n\n" . _('Hier kommen Sie direkt zum Termin in Ihrem Kalender:') . "\n"; - $msg_text .= URLHelper::getURL('dispatch.php/calendar/single/edit/' - . $this->event->getAuthorId() . '/' . $this->event->event_id); - $message->insert_message( - addslashes($msg_text), - [get_username($this->event->getAuthorId())], - $this->event->range_id, - '', '', '', '', addslashes($subject)); - restoreLanguage(); + $result = []; + + foreach ($users as $user) { + $events = CalendarDateAssignment::getEvents($begin, $end, $user->id); + if ($events) { + foreach ($events as $event) { + $data = $event->toEventData(User::findCurrent()->id); + if (!$timeline_view) { + $data->title = $user->getFullName(); } - PageLayout::postMessage(MessageBox::success(_('Der Teilnahmestatus wurde gespeichert.'))); - $this->relocate('calendar/single/' . $this->last_view, ['atime' => $this->atime]); + $result[] = $data->toFullcalendarEvent(); } } } - - $this->createSidebar('edit', $this->calendar); - $this->createSidebarFilter(); + $this->render_json($result); } - public function switch_action() + public function add_courses_action() { - $default_view = $this->settings['view'] ?: 'week'; - $view = Request::option('last_view', $default_view); - $this->range_id = Request::option('range_id', $GLOBALS['user']->id); - $object_type = get_object_type($this->range_id); - switch ($object_type) { - case 'user': - URLHelper::addLinkParam('cid', ''); - $this->redirect($this->url_for('calendar/single/' - . $view . '/' . $this->range_id)); - break; - case 'sem': - case 'inst': - case 'fak': - URLHelper::addLinkParam('cid', $this->range_id); - $this->redirect($this->url_for('calendar/single/' - . $view . '/' . $this->range_id)); - break; - case 'group': - URLHelper::addLinkParam('cid', ''); - $this->redirect($this->url_for('calendar/group/' - . $view . '/' . $this->range_id)); - break; + $selected_semester_pseudo_id = Request::option('semester_id'); + $this->selected_semesters_id = ''; + $this->available_semester_data = []; + $semesters = Semester::getAll(); + foreach ($semesters as $semester) { + $this->available_semester_data[$semester['id']] = [ + 'id' => $semester['id'], + 'name' => $semester['name'] + ]; } - } + $this->available_semester_data = array_reverse($this->available_semester_data); - public function jump_to_action() - { - $date = Request::get('jmp_date'); - if ($date) { - $atime = strtotime($date . strftime(' %T', $this->atime)); + if (!$selected_semester_pseudo_id) { + $selected_semester_pseudo_id = User::findCurrent()->getConfiguration()->MY_COURSES_SELECTED_CYCLE; + if (!Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS && $selected_semester_pseudo_id === 'all') { + $selected_semester_pseudo_id = 'next'; + } + if (!$selected_semester_pseudo_id) { + $selected_semester_pseudo_id = Config::get()->MY_COURSES_DEFAULT_CYCLE; + } + } + if ($selected_semester_pseudo_id === 'next') { + $semester = Semester::findNext(); + $this->selected_semester_id = $semester->id; + } elseif (in_array($selected_semester_pseudo_id, ['all', 'current'])) { + $semester = Semester::findCurrent(); + $this->selected_semester_id = $semester->id; + } elseif ($selected_semester_pseudo_id === 'last') { + $semester = Semester::findPrevious(); + $this->selected_semester_id = $semester->id; } else { - $atime = 'now'; + $this->selected_semester_id = $selected_semester_pseudo_id ?? ''; + if (!Semester::exists($this->selected_semesters_id)) { + $this->selected_semester_id = ''; + } + } + + $this->selected_course_ids = SimpleCollection::createFromArray( + CourseMember::findBySQL( + 'user_id = :user_id AND bind_calendar = 1', + ['user_id' => User::findCurrent()->id] + ) + )->pluck('seminar_id'); + + $this->semester_data = []; + $all_semesters = Semester::getAll(); + foreach ($all_semesters as $semester) { + $data = [ + 'id' => $semester->id, + 'name' => $semester->name, + 'courses' => [] + ]; + $this->semester_data[] = $data; + } + + if (Request::submitted('add')) { + CSRFProtection::verifyUnsafeRequest(); + + $course_ids = Request::getArray('courses_course_ids'); + foreach ($course_ids as $course_id => $selected) { + $course_membership = CourseMember::findOneBySQL( + 'seminar_id = :course_id AND user_id = :user_id', + [ + 'course_id' => $course_id, + 'user_id' => User::findCurrent()->id + ] + ); + if ($course_membership) { + $course_membership->bind_calendar = $selected ? '1' : '0'; + $course_membership->store(); + } + } + PageLayout::postSuccess(_('Die Zuordnung von Veranstaltungen zum Kalender wurde aktualisiert.')); + $this->redirect('calendar/calendar'); } - $action = Request::option('action', 'week'); - $this->range_id = $this->range_id ?: $GLOBALS['user']->id; - $this->redirect($this->url_for($this->base . $action, - ['atime' => $atime, 'range_id' => $this->range_id])); } - public function show_declined_action () + public function export_action() { - $config = UserConfig::get($GLOBALS['user']->id); - $this->settings['show_declined'] = Request::int('show_declined') ? '1' : '0'; - // var_dump($this->settings); exit; - $config->store('CALENDAR_SETTINGS', $this->settings); - $action = Request::option('action', 'week'); - $this->range_id = $this->range_id ?: $GLOBALS['user']->id; - $this->redirect($this->url_for($this->base . $action, - ['range_id' => $this->range_id])); + PageLayout::setTitle(_('Termine exportieren')); + $this->begin = new DateTimeImmutable(); + $this->end = $this->begin->add(new DateInterval('P1Y')); + $this->dates_to_export = 'user'; + if (Request::submitted('export')) { + CSRFProtection::verifyUnsafeRequest(); + $this->begin = Request::getDateTime('begin', 'd.m.Y'); + $this->end = Request::getDateTime('end', 'd.m.Y'); + if ($this->begin >= $this->end) { + PageLayout::postError(_('Der Startzeitpunkt darf nicht nach dem Endzeitpunkt liegen!')); + return; + } + $this->dates_to_export = Request::get('dates_to_export'); + if (!in_array($this->dates_to_export, ['user', 'course', 'all'])) { + PageLayout::postError(_('Bitte wählen Sie aus, welche Termine exportiert werden sollen!')); + return; + } + $ical = ''; + $calendar_export = new ICalendarExport(); + if ($this->dates_to_export === 'user') { + $ical = $calendar_export->exportCalendarDates(User::findCurrent()->id, $this->begin, $this->end); + } elseif ($this->dates_to_export === 'course') { + $ical = $calendar_export->exportCourseDates(User::findCurrent()->id, $this->begin, $this->end); + $ical .= $calendar_export->exportCourseExDates(User::findCurrent()->id, $this->begin, $this->end); + } elseif ($this->dates_to_export === 'all') { + $ical = $calendar_export->exportCalendarDates(User::findCurrent()->id, $this->begin, $this->end); + $ical .= $calendar_export->exportCourseDates(User::findCurrent()->id, $this->begin, $this->end); + $ical .= $calendar_export->exportCourseExDates(User::findCurrent()->id, $this->begin, $this->end); + } + $ical = $calendar_export->writeHeader() . $ical . $calendar_export->writeFooter(); + $this->response->add_header('Content-Type', 'text/calendar;charset=utf-8'); + $this->response->add_header('Content-Disposition', 'attachment; filename="studip.ics"'); + $this->response->add_header('Content-Transfer-Encoding', 'binary'); + $this->response->add_header('Pragma', 'public'); + $this->response->add_header('Cache-Control', 'private'); + $this->response->add_header('Content-Length', strlen($ical)); + $this->render_text($ical); + } } - protected function storeEventData(CalendarEvent $event, SingleCalendar $calendar) + public function import_action() {} + + public function import_file_action() { - $messages = []; - if (Request::int('isdayevent')) { - $dt_string = Request::get('start_date') . ' 00:00:00'; - } else { - $dt_string = sprintf( - '%s %u:%02u', - Request::get('start_date'), - Request::int('start_hour'), - Request::int('start_minute') - ); - } - $event->setStart($this->parseDateTime($dt_string)); - if (Request::int('isdayevent')) { - $dt_string = Request::get('end_date') . ' 23:59:59'; - } else { - $dt_string = sprintf( - '%s %u:%02u', - Request::get('end_date'), - Request::int('end_hour'), - Request::int('end_minute') - ); + if (Request::submitted('import')) { + CSRFProtection::verifySecurityToken(); + $range_id = Context::getId() ?? User::findCurrent()->id; + $calendar_import = new ICalendarImport($range_id); + $calendar_import->convertPublicToPrivate(Request::bool('import_as_private_imp')); + $calendar_import->import(file_get_contents($_FILES['importfile']['tmp_name'])); + $import_count = $calendar_import->getCountEvents(); + PageLayout::postSuccess(sprintf( + ngettext( + 'Ein Termin wurde importiert.', + 'Es wurden %u Termine importiert.', + $import_count + ), + $import_count + )); + $this->redirect($this->url_for('calendar/calendar/')); } - $event->setEnd($this->parseDateTime($dt_string)); + } - if (!$this->validate_datetime(sprintf('%02u:%02u',Request::int('start_hour'),Request::int('start_minute'))) - || !$this->validate_datetime(sprintf('%02u:%02u',Request::int('end_hour'),Request::int('end_minute'))) - ) { - $messages[] = _('Die Start- und/oder Endzeit ist ungültig!'); + public function share_action() + { + PageLayout::setTitle(_('Kalender teilen')); + if (!Config::get()->CALENDAR_GROUP_ENABLE) { + throw new FeatureDisabledException(); } - if ($event->getStart() > $event->getEnd()) { - $messages[] = _('Die Startzeit muss vor der Endzeit liegen.'); + $calendar_contacts = Contact::findBySql( + "JOIN `auth_user_md5` USING (`user_id`) + WHERE `contact`.`owner_id` = :user_id + AND `contact`.`calendar_permissions` <> '' + ORDER BY `auth_user_md5`.`Vorname`, `auth_user_md5`.`Nachname`", + [ + 'user_id' => User::findCurrent()->id + ] + ); + $user_data = []; + foreach ($calendar_contacts as $contact) { + $user_data[$contact->user_id] = [ + 'id' => $contact->user_id, + 'name' => $contact->friend->getFullName(), + 'write_permissions' => $contact->calendar_permissions === 'WRITE' + ]; } + $this->selected_users_json = json_encode($user_data, JSON_FORCE_OBJECT); + $this->searchtype = new StandardSearch('user_id', ['simple_name' => true]); - $event->setTitle(Request::get('summary', '')); - $event->event->description = Request::get('description', ''); - $event->setUserDefinedCategories(Request::get('categories', '')); - $event->event->location = Request::get('location', ''); - $event->event->category_intern = Request::int('category_intern', 1); - $event->setAccessibility(Request::option('accessibility', 'PRIVATE')); - $event->setPriority(Request::int('priority', 0)); + if (Request::submitted('share')) { + CSRFProtection::verifyUnsafeRequest(); + $selected_user_ids = Request::getArray('calendar_permissions', []); + $write_permissions = Request::getArray('calendar_write_permissions', []); - if (!$event->getTitle()) { - $messages[] = _('Es muss eine Zusammenfassung angegeben werden.'); - } + //Add/update contacts with calendar permissions: - $rec_type = Request::option('recurrence', 'single'); - $expire = Request::option('exp_c', 'never'); - $rrule = [ - 'linterval' => null, - 'sinterval' => null, - 'wdays' => null, - 'month' => null, - 'day' => null, - 'rtype' => 'SINGLE', - 'count' => null, - 'expire' => null - ]; - if ($expire == 'count') { - $rrule['count'] = Request::int('exp_count', 10); - } else if ($expire == 'date') { - if (Request::isXhr()) { - $exp_date = Request::get('exp_date'); - } else { - $exp_date = Request::get('exp_date'); - } - $exp_date = $exp_date ?: strftime('%x', time()); - $rrule['expire'] = $this->parseDateTime($exp_date . ' 12:00'); - } - switch ($rec_type) { - case 'daily': - if (Request::option('type_daily', 'day') === 'day') { - $rrule['linterval'] = Request::int('linterval_d', 1); - $rrule['rtype'] = 'DAILY'; - } else { - $rrule['linterval'] = 1; - $rrule['wdays'] = '12345'; - $rrule['rtype'] = 'WEEKLY'; + foreach ($selected_user_ids as $user_id) { + $user = User::find($user_id); + if (!$user) { + //No user? No contact! + continue; } - break; - case 'weekly': - $rrule['rtype'] = 'WEEKLY'; - $rrule['linterval'] = Request::int('linterval_w', 1); - $rrule['wdays'] = implode('', Request::intArray('wdays', - [strftime('%u', $event->getStart())])); - break; - case 'monthly': - $rrule['rtype'] = 'MONTHLY'; - if (Request::option('type_m', 'day') === 'day') { - $rrule['linterval'] = Request::int('linterval_m1', 1); - $rrule['day'] = Request::int('day_m', - strftime('%e', $event->getStart())); - } else { - $rrule['linterval'] = Request::int('linterval_m2', 1); - $rrule['sinterval'] = Request::int('sinterval_m', 1); - $rrule['wdays'] = Request::int('wday_m', - strftime('%u', $event->getStart())); + $contact = Contact::findOneBySql( + 'owner_id = :owner_id AND user_id = :user_id', + [ + 'owner_id' => User::findCurrent()->id, + 'user_id' => $user_id + ] + ); + if (!$contact) { + $contact = new Contact(); + $contact->owner_id = User::findCurrent()->id; + $contact->user_id = $user->id; } - break; - case 'yearly': - $rrule['rtype'] = 'YEARLY'; - $rrule['linterval'] = 1; - if (Request::option('type_y', 'day') === 'day') { - $rrule['day'] = Request::int('day_y', - strftime('%e', $event->getStart())); - $rrule['month'] = Request::int('month_y1', - date('n', $event->getStart())); + if (in_array($user->id, $write_permissions)) { + $contact->calendar_permissions = 'WRITE'; } else { - $rrule['sinterval'] = Request::int('sinterval_y', 1); - $rrule['wdays'] = Request::int('wday_y', - strftime('%u', $event->getStart())); - $rrule['month'] = Request::int('month_y2', - date('n', $event->getStart())); + $contact->calendar_permissions = 'READ'; } - break; - } - if (sizeof($messages)) { - PageLayout::postMessage(MessageBox::error(_('Bitte Eingaben korrigieren'), $messages)); - return false; - } else { - $event->setRecurrence($rrule); - $exceptions = array_diff(Request::getArray('exc_dates'), - Request::getArray('del_exc_dates')); - $event->setExceptions($this->parseExceptions($exceptions)); - // if this is a group event, store event in the calendars of each attendee - if (Config::get()->CALENDAR_GROUP_ENABLE) { - $attendee_ids = Request::optionArray('attendees'); - return $calendar->storeEvent($event, $attendee_ids); - } else { - return $calendar->storeEvent($event); + $contact->store(); } - } - } - /** - * Parses a string with exception dates from input form and returns an array - * with all dates as unix timestamp identified by an internally used pattern. - * - * @param string $exc_dates - * @return array An array of unix timestamps. - */ - protected function parseExceptions($exc_dates) { - $matches = []; - $dates = []; - preg_match_all('%(\d{1,2})\h*([/.])\h*(\d{1,2})\h*([/.])\h*(\d{4})\s*%', - implode(' ', $exc_dates), $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - if ($match[2] == '/') { - $dates[] = strtotime($match[1].'/'.$match[3].'/'.$match[5]); + //Revoke calendar permissions for all users that aren't in the list + //of selected users: + if ($selected_user_ids) { + $stmt = DBManager::get()->prepare( + "UPDATE `contact` SET `calendar_permissions` = '' + WHERE `owner_id` = :owner_id + AND `user_id` NOT IN ( :user_ids )" + ); + $stmt->execute([ + 'owner_id' => User::findCurrent()->id, + 'user_ids' => $selected_user_ids + ]); } else { - $dates[] = strtotime($match[1].$match[2].$match[3].$match[4].$match[5]); + $stmt = DBManager::get()->prepare( + "UPDATE `contact` SET `calendar_permissions` = '' + WHERE `owner_id` = :owner_id" + ); + $stmt->execute(['owner_id' => User::findCurrent()->id]); } + + PageLayout::postSuccess( + _('Die Kalenderfreigaben wurden geändert.') + ); + $this->response->add_header('X-Dialog-Close', '1'); } - return $dates; } - /** - * Parses a string as date time in the format "j.n.Y H:i:s" and returns the - * corresponding unix time stamp. - * - * @param string $dt_string The date time string. - * @return int A unix time stamp - */ - protected function parseDateTime($dt_string) + public function publish_action() { - $dt_array = date_parse_from_format('j.n.Y H:i:s', $dt_string); - return mktime($dt_array['hour'], $dt_array['minute'], $dt_array['second'], - $dt_array['month'], $dt_array['day'], $dt_array['year']); - } + $this->short_id = null; + if (Request::submitted('delete_id')) { + CSRFProtection::verifySecurityToken(); + IcalExport::deleteKey(User::findCurrent()->id); + PageLayout::postSuccess(_('Die Adresse, unter der Ihre Termine abrufbar sind, wurde gelöscht')); + } + + if (Request::submitted('new_id')) { + CSRFProtection::verifySecurityToken(); + $this->short_id = IcalExport::setKey(User::findCurrent()->id); + PageLayout::postSuccess(_('Eine Adresse, unter der Ihre Termine abrufbar sind, wurde erstellt.')); + } else { + $this->short_id = IcalExport::getKeyByUser(User::findCurrent()->id); + } + $text = ''; + if (Request::submitted('submit_email')) { + $email_reg_exp = '/^([-.0-9=?A-Z_a-z{|}~])+@([-.0-9=?A-Z_a-z{|}~])+\.[a-zA-Z]{2,6}$/i'; + if (preg_match($email_reg_exp, Request::get('email')) !== 0) { + $subject = '[' .Config::get()->UNI_NAME_CLEAN . ']' . _('Exportadresse für Ihre Termine'); + $text .= _('Diese Email wurde vom Stud.IP-System verschickt. Sie können auf diese Nachricht nicht antworten.') . "\n\n"; + $text .= _('Über diese Adresse erreichen Sie den Export für Ihre Termine:') . "\n\n"; + $text .= $GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/ical/index/' + . IcalExport::getKeyByUser(User::findCurrent()->id); + StudipMail::sendMessage(Request::get('email'), $subject, $text); + PageLayout::postSuccess(_('Die Adresse wurde verschickt!')); + } else { + PageLayout::postError(_('Bitte geben Sie eine gültige Email-Adresse an.')); + } + $this->short_id = IcalExport::getKeyByUser(User::findCurrent()->id); + } + PageLayout::setTitle(_('Kalender veröffentlichen')); + } } diff --git a/app/controllers/calendar/contentbox.php b/app/controllers/calendar/contentbox.php index 129fb59c38ba87af34dd7923d56bafa9f1014bf9..5d77ed4a21afa3f24f02b89aff1e6a36a6771f2f 100644 --- a/app/controllers/calendar/contentbox.php +++ b/app/controllers/calendar/contentbox.php @@ -24,6 +24,7 @@ class Calendar_ContentboxController extends StudipController $this->admin = false; $this->single = false; $this->userRange = false; + $this->course_range = false; $this->termine = []; // Fetch time if needed @@ -36,6 +37,8 @@ class Calendar_ContentboxController extends StudipController $range_id = [$range_id]; } + $this->titles = []; + foreach ($range_id as $id) { switch (get_object_type($id, ['user', 'sem'])) { case 'user': @@ -44,6 +47,7 @@ class Calendar_ContentboxController extends StudipController break; case 'sem': $this->parseSeminar($id); + $this->course_range = true; break; } } @@ -79,93 +83,91 @@ class Calendar_ContentboxController extends StudipController private function parseSeminar($id) { $course = Course::find($id); - $dates = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><'); - $this->termine = []; - foreach ($dates as $courseDate) { - // Build info - $info = []; - if (count($courseDate->dozenten) > 0) { - $info[_('Durchführende Lehrende')] = implode(', ', $courseDate->dozenten->getFullname()); - } - if (count($courseDate->statusgruppen) > 0) { - $info[_('Beteiligte Gruppen')] = implode(', ', $courseDate->statusgruppen->getValue('name')); - } - - // Store for view - $description = ''; - if ($courseDate instanceof CourseExDate) { - $description = $courseDate->content; - } elseif ($courseDate->cycle instanceof SeminarCycleDate) { - $description = $courseDate->cycle->description; + $this->termine = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><'); + foreach ($this->termine as $course_date) { + if ($this->course_range) { + //Display only date and time: + $this->titles[$course_date->id] = $course_date->getFullname('include-room'); + } else { + //Include the course title: + $this->titles[$course_date->id] = $course_date->getFullname('verbose'); } - $this->termine[] = [ - 'id' => $courseDate->id, - 'chdate' => $courseDate->chdate, - 'title' => $courseDate->getFullname() . (count($courseDate->topics) > 0 ? ', ' . implode(', ', $courseDate->topics->getValue('title')) : ''), - 'description' => $description, - 'topics' => $courseDate->topics->toArray('title description'), - 'room' => $courseDate->getRoomName(), - 'info' => $info - ]; } } private function parseUser($id) { - $restrictions = $GLOBALS['user']->id === $id ? [] : ['CLASS' => 'PUBLIC']; - $events = SingleCalendar::getEventList( - $id, - $this->start, - $this->start + $this->timespan, - null, - $restrictions - ); + $begin = new DateTime(); + $begin->setTimestamp($this->start); + $end = new DateTime(); + $end->setTimestamp($this->start + $this->timespan); + $this->termine = []; - // Prepare termine - foreach ($events as $termin) { - // Exclude events that begin after the given time range - if ($termin->getStart() > $this->start + $this->timespan) { + + if ($GLOBALS['user']->id === $id) { + //The current user is looking at their dates. + //Get course dates, too: + $relevant_courses = Course::findBySQL( + "JOIN `seminar_user` USING (`seminar_id`) + WHERE `user_id` = :user_id", + ['user_id' => $id] + ); + foreach ($relevant_courses as $course) { + $course_dates = $course->getDatesWithExdates()->findBy('end_time', [$this->start, $this->start + $this->timespan], '><'); + foreach ($course_dates as $course_date) { + $this->titles[$course_date->id] = sprintf( + '%1$s: %2$s', + $course_date->course->name, + $course_date->getFullname() + ); + $this->termine[] = $course_date; + } + } + } + + //Get personal dates: + + $assignments = []; + if (User::findCurrent()->id === $id) { + $assignments = CalendarDateAssignment::getEvents($begin, $end, $id); + } else { + //Only show public events: + $assignments = CalendarDateAssignment::getEvents($begin, $end, $id, ['PUBLIC']); + } + foreach ($assignments as $assignment) { + //Exclude events that begin after the given time range: + if ($assignment->calendar_date->begin > $this->start + $this->timespan) { continue; } + $title = ''; + // Adjust title - if (date('Ymd', $termin->getStart()) == date('Ymd')) { - $title = _('Heute') . date(', H:i', $termin->getStart()); + if (date('Ymd', $assignment->calendar_date->begin) == date('Ymd')) { + $title = _('Heute') . date(', H:i', $assignment->calendar_date->begin); } else { - $title = mb_substr(strftime('%a', $termin->getStart()), 0, 2); - $title .= date('. d.m.Y, H:i', $termin->getStart()); + $title = mb_substr(strftime('%a', $assignment->calendar_date->begin), 0, 2); + $title .= date('. d.m.Y, H:i', $assignment->calendar_date->begin); } - if ($termin->getStart() < $termin->getEnd()) { - if (date('Ymd', $termin->getStart()) < date('Ymd', $termin->getEnd())) { - $title .= ' - ' . mb_substr(strftime('%a', $termin->getEnd()), 0, 2); - $title .= date('. d.m.Y, H:i', $termin->getEnd()); + if ($assignment->calendar_date->begin < $assignment->calendar_date->end) { + if (date('Ymd', $assignment->calendar_date->begin) < date('Ymd', $assignment->calendar_date->end)) { + $title .= ' - ' . mb_substr(strftime('%a', $assignment->calendar_date->end), 0, 2); + $title .= date('. d.m.Y, H:i', $assignment->calendar_date->end); } else { - $title .= ' - ' . date('H:i', $termin->getEnd()); + $title .= ' - ' . date('H:i', $assignment->calendar_date->end); } } - if ($termin->getTitle()) { - $tmp_titel = mila($termin->getTitle()); //Beschneiden des Titels - $title .= ', ' . $tmp_titel; + if ($assignment->calendar_date->title) { + //Cut the title: + $tmp_title = mila($assignment->calendar_date->title); + $title .= ', ' . $tmp_title; } + $this->titles[$assignment->getObjectId()] = $title; // Store for view - $this->termine[] = [ - 'id' => $termin->id, - 'type' => get_class($termin), - 'range_id' => $termin->range_id, - 'event_id' => $termin->event_id, - 'chdate' => $termin->chdate, - 'title' => $title, - 'description' => $termin->getDescription(), - 'room' => $termin->getLocation(), - 'info' => [ - _('Kategorie') => $termin->toStringCategories(), - _('Priorität') => $termin->toStringPriority(), - _('Sichtbarkeit') => $termin->toStringAccessibility(), - _('Wiederholung') => $termin->toStringRecurrence()] - ]; + $this->termine[] = $assignment; } } } diff --git a/app/controllers/calendar/date.php b/app/controllers/calendar/date.php new file mode 100644 index 0000000000000000000000000000000000000000..32cf99cb2ee738945ddf9c6eecbba1228942746e --- /dev/null +++ b/app/controllers/calendar/date.php @@ -0,0 +1,828 @@ +<?php + +class Calendar_DateController extends AuthenticatedController +{ + protected function getCategoryOptions() + { + if (empty($GLOBALS['PERS_TERMIN_KAT'])) { + return []; + } + $options = []; + foreach ($GLOBALS['PERS_TERMIN_KAT'] as $key => $data) { + $options[$key] = $data['name']; + } + if (!array_key_exists(255, $options)) { + $options[255] = _('Sonstige'); + } + return $options; + } + + protected function getCalendarOwner($range_and_id) + { + $range = ''; + $range_id = ''; + $range_and_id = explode('_', $range_and_id ?? []); + if (!empty($range_and_id[1])) { + $range = $range_and_id[0]; + $range_id = $range_and_id[1]; + } + if (!$range) { + //Show the personal calendar of the current user: + $range = 'user'; + $range_id = $GLOBALS['user']->id; + } + + $owner = null; + if (!$range_id) { + //Assume a user calendar. $range contains the user-ID. + $owner = User::getCalendarOwner($range); + } else { + if ($range === 'user') { + $owner = User::getCalendarOwner($range_id); + } elseif ($range === 'course') { + $owner = Course::getCalendarOwner($range_id); + } + } + + if (!$owner || !$owner->isCalendarReadable()) { + throw new AccessDeniedException(_('Sie dürfen diesen Kalender nicht sehen!')); + } + return $owner; + } + + /** + * A helper method to determine whether the current user may write the date. + * + * @return Studip\Calendar\Owner[] The owners in which the current user may add a date. + */ + protected function getCalendarOwnersWithWriteAccess(?CalendarDate $date, ?Studip\Calendar\Owner $owner) : array + { + $result = []; + if ($owner instanceof Course) { + //For course calendars, only the course can be the owner. + $result[$owner->id] = $owner; + return $result; + } + if ($date) { + foreach ($date->calendars as $calendar) { + if ($calendar->user) { + $result[$calendar->user->id] = $calendar->user; + } elseif ($calendar->course) { + $result[$calendar->course->id] = $calendar->course; + } + } + } else { + if ($group_id = Request::get('group_id')) { + $group = ContactGroup::find($group_id); + if ($group) { + foreach ($group->items as $item) { + if ($item->user && $item->user->isCalendarWritable()) { + $result[$item->user_id] = $item->user; + } + } + } + } elseif ($user_id = Request::get('user_id', $GLOBALS['user']->id)) { + $user = User::find($user_id); + if ($user && $user->isCalendarWritable()) { + $result[$user->id] = $user; + } + } + if ($other_calendar_ids = Request::getArray('other_calendar_ids')) { + foreach ($other_calendar_ids as $other_calendar_id) { + $user = User::find($other_calendar_id); + if ($user && $user->isCalendarWritable()) { + $result[$user->id] = $user; + } + } + } + } + return $result; + } + + public function index_action($date_id) + { + $this->date = CalendarDate::find($date_id); + if (!$this->date) { + PageLayout::postError(_('Der angegebene Termin wurde nicht gefunden.')); + return; + } + if (!$this->date->isVisible($GLOBALS['user']->id)) { + throw new AccessDeniedException(); + } + PageLayout::setTitle( + sprintf( + _('%1$s (am %2$s von %3$s bis %4$s Uhr)'), + $this->date->title, + date('d.m.Y', $this->date->begin), + date('H:i', $this->date->begin), + date('H:i', $this->date->end) + ) + ); + $this->selected_date = ''; + if ($this->date->repetition_type) { + $this->selected_date = Request::get('selected_date'); + } + $this->calendar_assignments = CalendarDateAssignment::findBySql( + "INNER JOIN `auth_user_md5` + ON `calendar_date_assignments`.`range_id` = `auth_user_md5`.`user_id` + WHERE + `calendar_date_id` = :calendar_date_id", + ['calendar_date_id' => $this->date->id] + ); + $this->participation_message = null; + $this->user_participation_status = ''; + $this->all_assignments_writable = false; + $this->is_group_date = count($this->calendar_assignments) > 1; + + if ($this->calendar_assignments) { + $writable_assignment_c = 0; + $more_than_one_assignment = count($this->calendar_assignments) > 1; + //Find the calendar assignment of the user and set the participation message + //according to the participation status. + foreach ($this->calendar_assignments as $index => $assignment) { + if ($assignment->range_id === $GLOBALS['user']->id && $this->is_group_date) { + $this->user_participation_status = $assignment->participation; + if ($assignment->participation === 'ACCEPTED') { + $this->participation_message = MessageBox::info(_('Sie nehmen am Termin teil.')); + } elseif ($assignment->participation === 'DECLINED') { + $this->participation_message = MessageBox::info(_('Sie nehmen nicht am Termin teil.')); + } elseif ($assignment->participation === 'ACKNOWLEDGED') { + $this->participation_message = MessageBox::info(_('Sie haben den Termin zur Kenntnis genommen.')); + } else { + $this->participation_message = MessageBox::info(_('Sie haben keine Angaben zur Teilnahme gemacht.')); + } + if ($more_than_one_assignment) { + $writable_assignment_c++; + } else { + //We don't need the users own assignment in the list of assignments + //when there is only one assignment to the users own calendar. + unset($this->calendar_assignments[$index]); + + } + } else { + if ($assignment->isWritable($GLOBALS['user']->id)) { + $writable_assignment_c++; + } + } + } + + $this->all_assignments_writable = $writable_assignment_c === count($this->calendar_assignments); + + //Order all calendar assignments by type and name: + uasort($this->calendar_assignments, function ($a, $b) { + $compare_name = ($a->course instanceof Course && $b->course instanceof Course) + || ($a->user instanceof User && $b->user instanceof User); + if ($compare_name) { + $a_name = ''; + if ($a->course instanceof Course) { + $a_name = $a->course->getFullname(); + } elseif ($a->user instanceof User) { + $a_name = $a->user->getFullName(); + } + $b_name = ''; + if ($b->course instanceof Course) { + $b_name = $b->course->getFullname(); + } elseif ($b->user instanceof User) { + $b_name = $b->user->getFullName(); + } + if ($a_name < $b_name) { + return -1; + } elseif ($a_name > $b_name) { + return 1; + } else { + return 0; + } + } else { + //Compare types. + $a_is_course = $a->course instanceof Course; + if ($a_is_course) { + return -1; + } else { + //$b is a course: + return 1; + } + } + }); + } + } + + public function add_action($range_and_id = '') + { + PageLayout::setTitle(_('Termin anlegen')); + + $owner = $this->getCalendarOwner($range_and_id); + + $this->date = new CalendarDate(); + if (Request::submitted('begin') && Request::submitted('end')) { + $this->date->begin = Request::get('begin'); + $this->date->end = Request::get('end'); + $this->date->repetition_end = $this->date->end; + } else { + $time = new DateTime(); + $time = $time->add(new DateInterval('PT1H')); + $time->setTime(intval($time->format('H')), 0, 0); + $this->date->begin = $time->getTimestamp(); + $time = $time->add(new DateInterval('PT30M')); + $this->date->end = $time->getTimestamp(); + $this->date->repetition_end = $this->date->end; + } + if ($owner instanceof Course) { + $this->form_post_link = $this->link_for('calendar/date/add/course_' . $owner->id); + } else { + //Personal calendar or group calendar + $this->form_post_link = $this->link_for('calendar/date/add'); + } + $this->handleForm('add', $owner); + } + + public function edit_action($date_id) + { + PageLayout::setTitle(_('Termin bearbeiten')); + + $this->date = CalendarDate::find($date_id); + if (!$this->date) { + throw new Exception(_('Der Termin wurde nicht gefunden!')); + } + //Set the repetition end date to the end of the date in case it isn't set: + if (!$this->date->repetition_end) { + $this->date->repetition_end = $this->date->end; + } + + $this->form_post_link = $this->link_for('calendar/date/edit/' . $this->date->id); + $this->handleForm(); + } + + protected function handleForm($mode = 'edit', $owner = null) + { + $this->form_errors = []; + $this->calendar_assignment_items = []; + + $this->writable_calendars = $this->getCalendarOwnersWithWriteAccess($mode === 'edit' ? $this->date : null, $owner); + if (!$this->writable_calendars) { + throw new AccessDeniedException(); + } + $this->user_id = Request::get('user_id', $owner->id ?? ''); + $this->group_id = ''; + if (!$owner) { + $this->group_id = Request::get('group_id'); + } + $this->owner_id = $owner ? $owner->id : ''; + + $this->category_options = $this->getCategoryOptions(); + $this->exceptions = []; + + if (!$owner || !($owner instanceof Course)) { + $this->user_quick_search_type = null; + $this->multi_person_search = null; + if (Config::get()->CALENDAR_GROUP_ENABLE) { + if (Config::get()->CALENDAR_GRANT_ALL_INSERT) { + $this->user_quick_search_type = new StandardSearch('user_id'); + } else { + //Only get those users where the current user has + //write access to the calendar. + $this->user_quick_search_type = new SQLSearch( + "SELECT + `auth_user_md5`.`user_id`, " + . $GLOBALS['_fullname_sql']['full'] . " AS fullname + FROM `auth_user_md5` + INNER JOIN `contact` + ON `auth_user_md5`.`user_id` = `contact`.`owner_id` + INNER JOIN `user_info` + ON `user_info`.`user_id` = `auth_user_md5`.`user_id` + WHERE + `auth_user_md5`.`user_id` <> " . DBManager::get()->quote($GLOBALS['user']->id) . " + AND `contact`.`user_id` = " . DBManager::get()->quote($GLOBALS['user']->id) . " + AND `contact`.`calendar_permissions` = 'WRITE' + AND ( + `auth_user_md5`.`username` LIKE :input + OR CONCAT(`auth_user_md5`.`Vorname`, ' ', `auth_user_md5`.`Nachname`) LIKE :input + OR CONCAT(`auth_user_md5`.`Nachname`, ' ', `auth_user_md5`.`Vorname`) LIKE :input + OR `auth_user_md5`.`Nachname` LIKE :input + OR " . $GLOBALS['_fullname_sql']['full'] . " LIKE :input + ) + GROUP BY `auth_user_md5`.`user_id` + ORDER BY fullname ASC", + _('Person suchen'), + 'user_id' + ); + } + } + } + + if ($this->date->isNew()) { + if (!($owner instanceof Course)) { + //Assign the date to the calendar of the current user by default: + $user = User::findCurrent(); + if ($user) { + $this->calendar_assignment_items[] = [ + 'value' => $user->id, + 'name' => $user->getFullName(), + 'deletable' => true + ]; + } + } + } else { + $exceptions = CalendarDateException::findBySql( + 'calendar_date_id = :date_id ORDER BY `date` ASC', + ['date_id' => $this->date->id] + ); + foreach ($exceptions as $exception) { + $this->exceptions[] = $exception->date; + } + + $calendars_assignments = CalendarDateAssignment::findByCalendar_date_id($this->date->id); + foreach ($calendars_assignments as $assignment) { + $range_avatar = $assignment->getRangeAvatar(); + $this->calendar_assignment_items[] = [ + 'value' => $assignment->range_id, + 'name' => $assignment->getRangeName(), + 'deletable' => true + ]; + } + } + + $this->all_day_event = false; + if ($mode === 'add' && Request::get('all_day') === '1') { + $this->all_day_event = true; + } else { + $begin = new DateTime(); + $begin->setTimestamp(intval($this->date->begin)); + $end = new DateTime(); + $end->setTimestamp(intval($this->date->end)); + $duration = $end->diff($begin); + if ($duration->h === 23 && $duration->i === 59 && $duration->s === 59 && $begin->format('H:i:s') === '00:00:00') { + //The event starts at midnight and ends on 23:59:59. It is an all-day event. + $this->all_day_event = true; + } + } + + if (!Request::isPost()) { + return; + } + if (Request::submitted('save')) { + CSRFProtection::verifyUnsafeRequest(); + + if ($this->date->isNew()) { + $this->date->author_id = $GLOBALS['user']->id; + } + $this->date->editor_id = $GLOBALS['user']->id; + + $begin = Request::getDateTime('begin', 'd.m.Y H:i'); + $end = Request::getDateTime('end', 'd.m.Y H:i'); + if (Request::get('all_day') === '1') { + $this->all_day_event = true; + $begin->setTime(0,0,0); + $end = clone $begin; + $end->setTime(23,59,59); + } + $this->date->begin = $begin->getTimestamp(); + $this->date->end = $end->getTimestamp(); + if (!$this->date->begin) { + $this->form_errors[_('Beginn')] = _('Bitte geben Sie einen Startzeitpunkt ein.'); + } + if (!$this->date->end && !$this->all_day_event) { + $this->form_errors[_('Ende')] = _('Bitte geben Sie einen Endzeitpunkt ein.'); + } + if ($this->date->begin && $this->date->end && ($this->date->end < $this->date->begin)) { + $this->form_errors[_('Ende')] = _('Der Startzeitpunkt darf nicht nach dem Endzeitpunkt liegen!'); + } + + $this->date->title = Request::get('title'); + if (!$this->date->title) { + $this->form_errors[_('Titel')] = _('Bitte geben Sie einen Titel ein.'); + } + + $this->date->access = Request::get('access'); + if (!in_array($this->date->access, ['PUBLIC', 'CONFIDENTIAL', 'PRIVATE'])) { + $this->form_errors[_('Zugriff')] = _('Bitte wählen Sie einen Zugriffstyp aus.'); + } + + $this->date->description = Request::get('description'); + + $this->date->category = Request::get('category'); + if (!in_array($this->date->category, array_keys($this->category_options))) { + $this->form_errors[_('Kategorie')] = _('Bitte wählen Sie eine gültige Kategorie aus.'); + } + + $this->date->user_category = Request::get('user_category'); + + $this->date->location = Request::get('location'); + + //Store the repetition information: + + $this->date->clearRepetitionFields(); + $this->date->repetition_type = Request::get('repetition_type', ''); + if (!in_array($this->date->repetition_type, ['', 'DAILY', 'WEEKLY', 'WORKDAYS', 'MONTHLY', 'YEARLY'])) { + $this->form_errors[_('Wiederholung')] = _('Bitte wählen Sie ein gültiges Wiederholungsintervall aus.'); + } + if ($this->date->repetition_type !== '') { + $this->date->interval = ''; + if (in_array($this->date->repetition_type, ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'])) { + $this->date->interval = Request::get('repetition_interval'); + } + + if ($this->date->repetition_type === 'WEEKLY') { + $dow = array_unique(Request::getArray('repetition_dow')); + foreach ($dow as $day) { + if ($day < 1 || $day > 7) { + $this->form_errors[_('Wiederholung an bestimmtem Wochentag')] = _('Bitte wählen Sie einen Wochentag zwischen Montag und Sonntag aus.'); + } + } + $this->date->days = implode('', $dow); + } elseif ($this->date->repetition_type === 'WORKDAYS') { + //Special case: The "WORKDAYS" repetition type is a shorthand type + //for a weekly repetition from Monday to Friday. + $this->date->repetition_type = 'WEEKLY'; + $this->date->days = '12345'; + $this->date->interval = '1'; + } elseif ($this->date->repetition_type === 'MONTHLY') { + $month_type = Request::get('repetition_month_type'); + if ($month_type === 'dom') { + $this->date->offset = Request::get('repetition_dom'); + } elseif ($month_type === 'dow') { + $this->date->days = Request::get('repetition_dow'); + $this->date->offset = Request::get('repetition_dow_week'); + } + } elseif ($this->date->repetition_type === 'YEARLY') { + $month = Request::get('repetition_month'); + if ($month < 1 || $month > 12) { + $this->form_errors[_('Monat')] = _('Bitte wählen Sie einen Monat zwischen Januar und Dezember aus.'); + } + $this->date->month = $month; + $month_type = Request::get('repetition_month_type'); + if ($month_type === 'dom') { + $this->date->offset = Request::get('repetition_dom'); + } elseif ($month_type === 'dow') { + $this->date->days = Request::get('repetition_dow'); + $this->date->offset = Request::get('repetition_dow_week'); + } + } + + $end_type = Request::get('repetition_rep_end_type'); + if ($end_type === 'end_date') { + $end_date = Request::getDateTime('repetition_rep_end_date', 'd.m.Y'); + $end_date->setTime(23,59,59); + $this->date->repetition_end = $end_date->getTimestamp(); + } elseif ($end_type === 'end_count') { + $this->date->number_of_dates = Request::get('repetition_number_of_dates'); + } else { + //Repetition never ends: + $this->date->repetition_end = CalendarDate::NEVER_ENDING; + } + } + + $assigned_calendar_ids = Request::getArray('assigned_calendar_ids'); + if (!$assigned_calendar_ids || (count($assigned_calendar_ids) === 0)) { + $this->form_errors[_('Teilnehmende Personen')] = _('Der Termin ist keinem Kalender zugewiesen!'); + } + + if ($this->form_errors) { + return; + } + + $stored = false; + if ($this->date->isDirty()) { + $stored = $this->date->store(); + } else { + $stored = true; + } + if (!$stored) { + PageLayout::postError( + _('Beim Speichern des Termins ist ein Fehler aufgetreten.') + ); + return; + } + + //Assign the calendar date to all writable calendars. + + //Check the assigned calendar-IDs first if they are valid: + $valid_assigned_calendar_ids = []; + if (($owner instanceof Course)) { + //Set the course as calendar: + $allowed_calendar_ids = [$owner->id]; + } else { + //Assign the date to the calendars of all the selected users: + $allowed_calendar_ids = [$GLOBALS['user']->id]; + if (Config::get()->CALENDAR_GROUP_ENABLE) { + $allowed_calendar_results = $this->user_quick_search_type->getResults('%%%%'); + foreach ($allowed_calendar_results as $result) { + $allowed_calendar_ids[] = $result[0]; + } + } + } + + foreach ($assigned_calendar_ids as $assigned_calendar_id) { + if (Course::exists($assigned_calendar_id) || User::exists($assigned_calendar_id)) { + //Valid ID of an existing calendar (range-ID). + if (in_array($assigned_calendar_id, $allowed_calendar_ids)) { + //The calendar is writable. + $valid_assigned_calendar_ids[] = $assigned_calendar_id; + } + } + } + if (count($valid_assigned_calendar_ids) < 1) { + PageLayout::postError( + _('Die Zuweisungen des Termins zu Kalendern sind ungültig!') + ); + return; + } + + //Remove the date from all user calendars that aren't in the array of writable calendars. + CalendarDateAssignment::deleteBySQL( + '`range_id` NOT IN ( :owner_ids ) AND `calendar_date_id` = :calendar_date_id', + ['owner_ids' => $allowed_calendar_ids, 'calendar_date_id' => $this->date->id] + ); + + //Now add the date to all selected calendars: + foreach($valid_assigned_calendar_ids as $assigned_calendar_id) { + $assignment = CalendarDateAssignment::findOneBySql( + 'range_id = :assigned_calendar_id AND calendar_date_id = :calendar_date_id', + [ + 'assigned_calendar_id' => $assigned_calendar_id, + 'calendar_date_id' => $this->date->id + ] + ); + if (!$assignment) { + $assignment = new CalendarDateAssignment(); + $assignment->range_id = $assigned_calendar_id; + $assignment->calendar_date_id = $this->date->id; + $assignment->store(); + } + } + + //Clear all exceptions for the event and set them again: + CalendarDateException::deleteByCalendar_date_id($this->date->id); + $new_exceptions = Request::getArray('exceptions'); + $stored_c = 0; + foreach ($new_exceptions as $exception) { + $date_parts = explode('-', $exception); + if (count($date_parts) === 3) { + //Should be a valid date string. + $e = new CalendarDateException(); + $e->calendar_date_id = $this->date->id; + $e->date = $exception; + if ($e->store()) { + $stored_c++; + } + } + } + if ($stored_c === count($new_exceptions)) { + PageLayout::postSuccess(_('Der Termin wurde gespeichert.')); + } else { + PageLayout::postWarning(_('Der Termin wurde gespeichert, aber nicht mit allen Terminausfällen!')); + } + if (Request::submitted('selected_date')) { + $selected_date = Request::getDateTime('selected_date'); + if ($selected_date) { + //Set the calendar default date to the previously selected date: + $_SESSION['calendar_date'] = $selected_date->format('Y-m-d'); + } + } else { + //Set the calendar default date to the beginning of the date: + $_SESSION['calendar_date'] = $begin->format('Y-m-d'); + } + $this->response->add_header('X-Dialog-Close', '1'); + } + } + + public function move_action($date_id) + { + $this->date = CalendarDate::find($date_id); + if (!$this->date) { + throw new InvalidArgumentException( + _('Der angegebene Termin wurde nicht gefunden.') + ); + } + if (!$this->date->isWritable($GLOBALS['user']->id)) { + throw new AccessDeniedException( + _('Sie sind nicht berechtigt, diesen Termin zu ändern.') + ); + } + + $this->begin = Request::getDateTime('begin', \DateTime::RFC3339); + $this->end = Request::getDateTime('end', \DateTime::RFC3339); + if (!$this->begin || !$this->end) { + throw new InvalidArgumentException(); + } + + if ($this->date->repetition_type) { + PageLayout::setTitle(_('Verschieben eines Termins aus einer Terminserie')); + //Show the dialog to decide what shall be done with the repetition. + if (Request::submitted('move')) { + CSRFProtection::verifyUnsafeRequest(); + $repetition_handling = Request::get('repetition_handling'); + $store_old_date = false; + if ($repetition_handling === 'create_single_date') { + //Create a new date with the new time range and then + //create an exception for the old date. + $new_date = new CalendarDate(); + $new_date->setData($this->date->toArray()); + $new_date->id = $new_date->getNewId(); + $new_date->unique_id = ''; + $new_date->begin = $this->begin->getTimestamp(); + $new_date->end = $this->end->getTimestamp(); + $new_date->author_id = $GLOBALS['user']->id; + $new_date->editor_id = $GLOBALS['user']->id; + $new_date->clearRepetitionFields(); + $new_date->store(); + foreach ($this->date->calendars as $calendar) { + $new_date_calendar = new CalendarDateAssignment(); + $new_date_calendar->calendar_date_id = $new_date->id; + $new_date_calendar->range_id = $calendar->range_id; + $new_date_calendar->store(); + } + $exception = CalendarDateException::findBySQL( + '`calendar_date_id` = :calendar_date_id AND `date` = :date', + [ + 'calendar_date_id' => $this->date->id, + 'date' => $this->begin->format('Y-m-d') + ] + ); + if (!$exception) { + $exception = new CalendarDateException(); + $exception->calendar_date_id = $this->date->id; + $exception->date = $this->begin->format('Y-m-d'); + $exception->store(); + } + $this->response->add_header('X-Dialog-Close', '1'); + return; + } elseif ($repetition_handling === 'change_times') { + //Set the new time for begin and end: + $date_begin = new DateTime(); + $date_begin->setTimestamp($this->date->begin); + $date_begin->setTime( + intval($this->begin->format('H')), + intval($this->begin->format('i')), + intval($this->begin->format('s')) + ); + $this->date->begin = $date_begin->getTimestamp(); + $date_end = new DateTime(); + $date_end->setTimestamp($this->date->end); + $date_end->setTime( + intval($this->end->format('H')), + intval($this->end->format('i')), + intval($this->end->format('s')) + ); + $this->date->end = $date_end->getTimestamp(); + + //Set the editor-ID: + $this->date->editor_id = $GLOBALS['user']->id; + + $store_old_date = true; + } elseif ($repetition_handling === 'change_all') { + $this->date->begin = $this->begin->getTimestamp(); + if ($this->date->repetition_end && intval($this->date->repetition_end) != pow(2,31) - 1) { + //The repetition end date is set to one specific date. + //It must be recalculated from the end date. + $old_end = new DateTime(); + $old_end->setTimestamp($this->date->end); + $old_repetition_end = new DateTime(); + $old_repetition_end ->setTimestamp($this->date->repetition_end); + $distance = $old_end->diff($old_repetition_end); + $this->date->end = $this->end->getTimestamp(); + $new_repetition_end = clone $this->end; + $new_repetition_end = $new_repetition_end->add($distance); + $this->date->repetition_end = $new_repetition_end->getTimestamp(); + } + $this->date->end = $this->end->getTimestamp(); + + //Set the editor-ID: + $this->date->editor_id = $GLOBALS['user']->id; + + $store_old_date = true; + } else { + //Invalid choice. + PageLayout::postError(_('Ungültige Auswahl!')); + return; + } + if ($store_old_date) { + $success = false; + if ($this->date->isDirty()) { + $success = $this->date->store(); + } else { + $success = true; + } + if ($success) { + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_nothing(); + } else { + throw new Exception(_('Der Termin konnte nicht gespeichert werden.')); + } + } + } + } else { + //Set the new date and time directly. + $this->date->begin = $this->begin->getTimestamp(); + $this->date->end = $this->end->getTimestamp(); + //Set the editor-ID: + $this->date->editor_id = $GLOBALS['user']->id; + + $success = false; + if ($this->date->isDirty()) { + $success = $this->date->store(); + } else { + $success = true; + } + if ($success) { + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_nothing(); + } else { + throw new Exception(_('Der Termin konnte nicht gespeichert werden.')); + } + } + } + + public function delete_action($date_id) + { + PageLayout::setTitle(_('Termin löschen')); + $this->date = CalendarDate::find($date_id); + if (!$this->date) { + PageLayout::postError( + _('Der Termin wurde nicht gefunden!') + ); + $this->render_nothing(); + } + $this->date_has_repetitions = !empty($this->date->repetition_type); + $this->selected_date = null; + if ($this->date_has_repetitions) { + $this->selected_date = Request::getDateTime('selected_date'); + if (!$this->selected_date) { + $this->selected_date = new DateTime(); + $this->selected_date->setTimestamp($this->date->begin); + } + } + $this->repetition_handling = Request::get('repetition_handling', 'create_exception'); + if (Request::submitted('delete')) { + $delete_whole_date = false; + CSRFProtection::verifyUnsafeRequest(); + if ($this->date_has_repetitions) { + if ($this->repetition_handling === 'create_exception') { + $exception = new CalendarDateException(); + $exception->calendar_date_id = $this->date->id; + $exception->date = $this->selected_date->format('Y-m-d'); + if ($exception->store()) { + PageLayout::postSuccess( + sprintf( + _('Die Ausnahme am %s wurde der Terminserie hinzugefügt.'), + $this->selected_date->format('d.m.Y') + ) + ); + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_nothing(); + } else { + PageLayout::postError( + sprintf( + _('Die Ausnahme am %s konnte der Terminserie nicht hinzugefügt werden.'), + $this->selected_date->format('d.m.Y') + ) + ); + } + } elseif ($this->repetition_handling === 'delete_all') { + $delete_whole_date = true; + } + } else { + $delete_whole_date = true; + } + if ($delete_whole_date) { + if ($this->date->delete()) { + if ($this->date_has_repetitions) { + PageLayout::postSuccess(_('Die Terminserie wurde gelöscht!')); + } else { + PageLayout::postSuccess(_('Der Termin wurde gelöscht!')); + } + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_nothing(); + } else { + if ($this->date_has_repetitions) { + PageLayout::postError(_('Die Terminserie konnte nicht gelöscht werden!')); + } else { + PageLayout::postError(_('Der Termin konnte nicht gelöscht werden!')); + } + } + } + } + } + + public function participation_action($date_id) + { + $this->calendar_assignment = CalendarDateAssignment::find([$GLOBALS['user']->id, $date_id]); + if (!$this->calendar_assignment) { + throw new AccessDeniedException(); + } + CSRFProtection::verifyUnsafeRequest(); + + $participation = Request::get('participation'); + if (!in_array($participation, ['', 'ACCEPTED', 'DECLINED', 'ACKNOWLEDGED'])) { + throw new InvalidArgumentException(); + } + + $this->calendar_assignment->participation = $participation; + if ($this->calendar_assignment->isDirty()) { + $this->calendar_assignment->store(); + $this->calendar_assignment->sendParticipationStatus(); + } + $this->response->add_header('X-Dialog-Close', '1'); + PageLayout::postSuccess(_('Ihre Teilnahmestatus wurde geändert.')); + $this->render_nothing(); + } +} diff --git a/app/controllers/calendar/group.php b/app/controllers/calendar/group.php deleted file mode 100644 index f440a570cfbf6804049c1b1604ab4e4eb1338f86..0000000000000000000000000000000000000000 --- a/app/controllers/calendar/group.php +++ /dev/null @@ -1,336 +0,0 @@ -<?php - -require_once 'app/controllers/calendar/calendar.php'; -require_once 'app/controllers/authenticated_controller.php'; - -class Calendar_GroupController extends Calendar_CalendarController -{ - public function before_filter(&$action, &$args) - { - $this->base = 'calendar/group/'; - parent::before_filter($action, $args); - } - - protected function createSidebar($active = 'week', $calendar = null) - { - parent::createSidebar($active, $calendar); - $sidebar = Sidebar::Get(); - $actions = new ActionsWidget(); - $actions->addLink(_('Termin anlegen'), - $this->url_for('calendar/group/edit'), - Icon::create('add'), - ['data-dialog' => 'size=auto']); - $actions->addLink(_('Kalender freigeben'), - $this->url_for('calendar/single/manage_access/' . $GLOBALS['user']->id, - ['group_filter' => $this->range_id]), - Icon::create('community'), - ['id' => 'calendar-open-manageaccess', - 'data-dialog' => '', 'data-dialogname' => 'manageaccess']); - $sidebar->addWidget($actions); - } - - protected function getTitle($group) - { - $title = sprintf(_('Terminkalender der Gruppe "%s"'), $group->name); - return $title; - } - - public function index_action() - { - // switch to the view the user has selected in his personal settings - $default_view = $this->settings['view'] ?: 'week'; - $this->redirect($this->url_for('calendar/group/' . $default_view)); - } - - public function edit_action($range_id = null, $event_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - // get group and the calendars of the members - // the first calendar is the calendar of the actual user - $this->calendar = new SingleCalendar($GLOBALS['user']->id); - $group = $this->getGroup($this->calendar); - $this->attendee_ids = []; - if ($group) { - $calendar_owners = CalendarUser::getOwners($GLOBALS['user']->id, - Calendar::PERMISSION_WRITABLE)->pluck('owner_id'); - $members = $group->members->pluck('user_id'); - $user_id = Request::option('user_id'); - $this->attendee_ids = array_intersect($calendar_owners, $members); - $this->attendee_ids[] = $GLOBALS['user']->id; - if ($user_id && in_array($user_id, $this->attendee_ids)) { - $this->attendee_ids = [$user_id]; - } - } - - $this->event = $this->calendar->getEvent($event_id); - - if ($this->event->isNew()) { - $this->event = $this->calendar->getNewEvent(); - if (Request::get('isdayevent')) { - $this->event->setStart(mktime(0, 0, 0, date('n', $this->atime), - date('j', $this->atime), date('Y', $this->atime))); - $this->event->setEnd(mktime(23, 59, 59, date('n', $this->atime), - date('j', $this->atime), date('Y', $this->atime))); - } else { - $this->event->setStart($this->atime); - $this->event->setEnd($this->atime + 3600); - } - $this->event->setAuthorId($GLOBALS['user']->id); - $this->event->setEditorId($GLOBALS['user']->id); - $this->event->setAccessibility('PRIVATE'); - if ($this->attendee_ids) { - foreach ($this->attendee_ids as $attendee_id) { - $attendee_event = clone $this->event; - $attendee_event->range_id = $attendee_id; - $this->attendees[] = $attendee_event; - } - } - if (!Request::isXhr()) { - PageLayout::setTitle($this->getTitle($this->calendar, _('Neuer Termin'))); - } - } else { - // open read only events and course events not as form - // show information in dialog instead - if (!$this->event->havePermission(Event::PERMISSION_WRITABLE) - || $this->event instanceof CourseEvent) { - $this->redirect($this->url_for('calendar/single/event/' . implode('/', - [$this->range_id, $this->event->event_id]))); - return null; - } - $this->attendees = $this->event->attendees; - if (!Request::isXhr()) { - PageLayout::setTitle($this->getTitle($this->calendar, _('Termin bearbeiten'))); - } - } - - if (Config::get()->CALENDAR_GROUP_ENABLE - && $this->calendar->getRange() == Calendar::RANGE_USER) { - $search_obj = new SQLSearch("SELECT auth_user_md5.user_id, {$GLOBALS['_fullname_sql']['full_rev']} as fullname, username, perms " - . "FROM calendar_user " - . "LEFT JOIN auth_user_md5 ON calendar_user.owner_id = auth_user_md5.user_id " - . "LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id) " - . 'WHERE calendar_user.user_id = ' - . DBManager::get()->quote($GLOBALS['user']->id) - . ' AND calendar_user.permission > ' . Event::PERMISSION_READABLE - . ' AND (username LIKE :input OR Vorname LIKE :input ' - . "OR CONCAT(Vorname,' ',Nachname) LIKE :input " - . "OR CONCAT(Nachname,' ',Vorname) LIKE :input " - . "OR Nachname LIKE :input OR {$GLOBALS['_fullname_sql']['full_rev']} LIKE :input " - . ") ORDER BY fullname ASC", - _('Nutzer suchen'), 'user_id'); - $this->quick_search = QuickSearch::get('user_id', $search_obj) - ->fireJSFunctionOnSelect('STUDIP.Messages.add_adressee'); - - // $default_selected_user = array($this->calendar->getRangeId()); - $this->mps = MultiPersonSearch::get('add_adressees') - ->setLinkText(_('Mehrere Teilnehmende hinzufügen')) - // ->setDefaultSelectedUser($default_selected_user) - ->setTitle(_('Mehrere Teilnehmende hinzufügen')) - ->setExecuteURL($this->url_for($this->base . 'edit')) - ->setJSFunctionOnSubmit('STUDIP.Messages.add_adressees') - ->setSearchObject($search_obj); - $owners = SimpleORMapCollection::createFromArray( - CalendarUser::findByUser_id($this->calendar->getRangeId())) - ->pluck('owner_id'); - foreach (Calendar::getGroups($GLOBALS['user']->id) as $group) { - $this->mps->addQuickfilter( - $group->name, - $group->members->filter( - function ($member) use ($owners) { - if (in_array($member->user_id, $owners)) { - return $member; - } - })->pluck('user_id') - ); - } - } - - $stored = false; - if (Request::submitted('store')) { - $stored = $this->storeEventData($this->event, $this->calendar); - } - - if ($stored !== false) { - // switch back to group context - $this->range_id = $group->getId(); - if ($stored === 0) { - if (Request::isXhr()) { - header('X-Dialog-Close: 1'); - exit; - } else { - PageLayout::postSuccess(_('Der Termin wurde nicht geändert.')); - $this->relocate('calendar/group/' . $this->last_view, ['atime' => $this->atime]); - } - } else { - PageLayout::postSuccess(_('Der Termin wurde gespeichert.')); - $this->relocate('calendar/group/' . $this->last_view, ['atime' => $this->atime]); - } - } else { - $this->createSidebar('edit', $this->calendar); - $this->createSidebarFilter(); - $this->render_template('calendar/single/edit', $this->layout); - } - } - - public function day_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - // get group and the calendars of the members - // the first calendar is the calendar of the actual user - $this->calendars[0] = SingleCalendar::getDayCalendar( - $GLOBALS['user']->id, $this->atime); - $group = $this->getGroup($this->calendars[0]); - foreach ($group->members as $member) { - $calendar = new SingleCalendar($member->user_id); - if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) { - $this->calendars[] = SingleCalendar::getDayCalendar($calendar, - $this->atime, null, $this->restrictions); - } - } - - PageLayout::setTitle($this->getTitle($group) - . ' - ' . _('Tagesansicht')); - Navigation::activateItem('/calendar/calendar'); - - $this->last_view = 'day'; - - $this->createSidebar('day'); - $this->createSidebarFilter(); - } - - /** - * Returns the Statusgruppe for the given calendar. - * - * @param SingleCalendar The calendar of the group owner. - * @return Statusgruppen The found group. - * @throws AccessDeniedException If the group does not exists or the owner - * of the calendar is not the owner of the group. - */ - private function getGroup($calendar) - { - $group = Statusgruppen::find($this->range_id); - if (!$group) { - throw new AccessDeniedException(); - } - // is the user the owner of this group - if ($group->range_id != $calendar->getRangeId()) { - // not the owner... - throw new AccessDeniedException(); - } - return $group; - } - - public function week_action($range_id = null) - { - $this->calendars = []; - $this->range_id = $range_id ?: $this->range_id; - $timestamp = mktime(12, 0, 0, date('n', $this->atime), - date('j', $this->atime), date('Y', $this->atime)); - $monday = $timestamp - 86400 * (strftime('%u', $timestamp) - 1); - $day_count = $this->settings['type_week'] == 'SHORT' ? 5 : 7; - // one calendar for each day for the actual user - for ($i = 0; $i < $day_count; $i++) { - // one calendar holds the events of one day - $this->calendars[0][$i] = - SingleCalendar::getDayCalendar($GLOBALS['user']->id, - $monday + $i * 86400, null, $this->restrictions); - } - // check and get the group - $group = $this->getGroup($this->calendars[0][0]); - $n = 1; - foreach ($group->members as $member) { - $calendar = new SingleCalendar($member->user_id); - if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) { - for ($i = 0; $i < $day_count; $i++) { - $this->calendars[$n][$i] = - SingleCalendar::getDayCalendar($member->user_id, - $monday + $i * 86400, null, $this->restrictions); - } - $n++; - } - } - - PageLayout::setTitle($this->getTitle($group) - . ' - ' . _('Wochenansicht')); - Navigation::activateItem('/calendar/calendar'); - - $this->last_view = 'week'; - - $this->createSidebar('week'); - $this->createSidebarFilter(); - } - - public function month_action($range_id = null) - { - $this->calendars = []; - $this->range_id = $range_id ?: $this->range_id; - $month_start = mktime(12, 0, 0, date('n', $this->atime), 1, date('Y', $this->atime)); - $month_end = mktime(12, 0, 0, date('n', $this->atime), date('t', $this->atime), date('Y', $this->atime)); - $adow = strftime('%u', $month_start) - 1; - $cor = date('n', $this->atime) == 3 ? 1 : 0; - $this->first_day = $month_start - $adow * 86400; - $this->last_day = ((42 - ($adow + date('t', $this->atime))) % 7 + $cor) * 86400 + $month_end; - // one calendar each day for the actual user - for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) { - $this->calendars[0][] = SingleCalendar::getDayCalendar( - $GLOBALS['user']->id, $start_day, null, $this->restrictions); - } - // check and get the group - $group = $this->getGroup($this->calendars[0][0]); - $n = 1; - // get the calendars of the group members - foreach ($group->members as $member) { - $calendar = new SingleCalendar($member->user_id); - if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) { - for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) { - $this->calendars[$n][] = - SingleCalendar::getDayCalendar($member->user_id, - $start_day, null, $this->restrictions); - } - $n++; - } - } - PageLayout::setTitle($this->getTitle($group) - . ' - ' . _('Monatssicht')); - Navigation::activateItem('/calendar/calendar'); - - $this->last_view = 'month'; - - $this->createSidebar('month'); - $this->createSidebarFilter(); - } - - public function year_action($range_id = null) - { - $this->calendars = []; - $this->count_lists = []; - - $this->range_id = $range_id ?: $this->range_id; - $start = mktime(0, 0, 0, 1, 1, date('Y', $this->atime)); - $end = mktime(23, 59, 59, 12, 31, date('Y', $this->atime)); - $this->calendars[0] = new SingleCalendar( - $GLOBALS['user']->id, $start, $end); - $this->count_lists[0] = $this->calendars[0]->getListCountEvents(); - - // check and get the group - $group = $this->getGroup($this->calendars[0]); - $n = 1; - // get the calendars of the group members - foreach ($group->members as $member) { - $calendar = new SingleCalendar($member->user_id); - if ($calendar->havePermission(Calendar::PERMISSION_READABLE)) { - $this->calendars[$n] = $calendar->setStart($start)->setEnd($end); - $this->count_lists[$n] = $this->calendars[$n]->getListCountEvents(); - $n++; - } - } - - PageLayout::setTitle($this->getTitle($group) - . ' - ' . _('Jahresansicht')); - Navigation::activateItem("/calendar/calendar"); - - $this->last_view = 'year'; - $this->createSidebar('year'); - $this->createSidebarFilter(); - } -} diff --git a/app/controllers/calendar/instschedule.php b/app/controllers/calendar/instschedule.php deleted file mode 100644 index 452e71db0db701b3ac11cddd4ca05f7542928120..0000000000000000000000000000000000000000 --- a/app/controllers/calendar/instschedule.php +++ /dev/null @@ -1,190 +0,0 @@ -<?php -# Lifter010: TODO - -/** - * This controller displays an institute-calendar for seminars - * - * 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 Till Glöggler <tgloeggl@uos.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 2.0 - */ -class Calendar_InstscheduleController extends AuthenticatedController -{ - /** - * this action is the main action of the schedule-controller, setting the environment for the timetable, - * accepting a comma-separated list of days. - * - * @param string $days a list of an arbitrary mix of the numbers 0-6, separated with a comma (e.g. 1,2,3,4,5 (for Monday to Friday, the default)) - */ - function index_action($days = false) - { - if ($GLOBALS['perm']->have_perm('admin')) { - $inst_mode = true; - } - $my_schedule_settings = $GLOBALS['user']->cfg->SCHEDULE_SETTINGS; - // set the days to be displayed - if ($days === false) { - if (Request::getArray('days')) { - $this->days = array_keys(Request::getArray('days')); - } else { - $this->days = CalendarScheduleModel::getDisplayedDays($my_schedule_settings['glb_days']); - } - } else { - $this->days = explode(',', $days); - } - - // try to find the correct institute-id - $institute_id = Request::option('institute_id', Context::getId()); - - if (!$institute_id) { - $institute_id = $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT; - } - - if (!$institute_id || (in_array(get_object_type($institute_id), words('inst fak')) === false)) { - throw new Exception(sprintf(_('Kann Einrichtungskalendar nicht anzeigen!' - . 'Es wurde eine ungültige Instituts-Id übergeben (%s)!', $institute_id))); - } - - // load semester-data and current semester - $this->semesters = Semester::findAllVisible(false); - - if (Request::option('semester_id')) { - $this->current_semester = Semester::find(Request::option('semester_id')); - } else { - $this->current_semester = Semester::findCurrent(); - } - - $this->entries = (array)CalendarInstscheduleModel::getInstituteEntries($GLOBALS['user']->id, - $this->current_semester, 8, 20, $institute_id, $this->days); - - Navigation::activateItem('/course/main/schedule'); - PageLayout::setHelpKeyword('Basis.TerminkalenderStundenplan'); - PageLayout::setTitle(Context::getHeaderLine().' - '._('Veranstaltungs-Stundenplan')); - - $zoom = Request::int('zoom', 0); - $this->controller = $this; - $this->calendar_view = new CalendarWeekView($this->entries, 'instschedule'); - $this->calendar_view->setHeight(40 + (20 * $zoom)); - $this->calendar_view->setRange($my_schedule_settings['glb_start_time'], $my_schedule_settings['glb_end_time']); - $this->calendar_view->groupEntries(); // if enabled, group entries with same start- and end-date - - URLHelper::addLinkParam('zoom', $zoom); - URLHelper::addLinkParam('semester_id', $this->current_semester['semester_id']); - - $style_parameters = [ - 'whole_height' => $this->calendar_view->getOverallHeight(), - 'entry_height' => $this->calendar_view->getHeight() - ]; - - $factory = new Flexi_TemplateFactory($this->dispatcher->trails_root . '/views'); - PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters)); - - if (Request::option('printview')) { - PageLayout::addStylesheet('print.css'); - - // remove all stylesheets that are not used for printing to have a more reasonable printing preview - PageLayout::addHeadElement('script', [], "$('head link[media=screen]').remove();"); - } else { - PageLayout::addStylesheet('print.css', ['media' => 'print']); - } - - Helpbar::Get()->addPlainText(_('Information'), _('Der Stundenplan zeigt die regelmäßigen Veranstaltungen dieser Einrichtung.'), Icon::create('info')); - - $views = new ViewsWidget(); - $views->addLink(_('klein'), URLHelper::getURL('', ['zoom' => 0]))->setActive($zoom == 0); - $views->addLink(_('mittel'), URLHelper::getURL('', ['zoom' => 2]))->setActive($zoom == 2); - $views->addLink(_('groß'), URLHelper::getURL('', ['zoom' => 4]))->setActive($zoom == 4); - $views->addLink(_('extra groß'), URLHelper::getURL('', ['zoom' => 7]))->setActive($zoom == 7); - - Sidebar::Get()->addWidget($views); - $actions = new ActionsWidget(); - $actions->addLink(_('Druckansicht'), - $this->url_for('calendar/instschedule/index/'. implode(',', $this->days), - ['printview' => 'true', - 'semester_id' => $this->current_semester['semester_id']]), - Icon::create('print'), - ['target' => '_blank']); - - // Only admins should have the ability to change their schedule settings here - they have no other schedule - if ($GLOBALS['perm']->have_perm('admin')) { - $actions->addLink(_("Darstellung ändern"), - $this->url_for('calendar/schedule/settings'), - Icon::create('admin'), - ['data-dialog' => ''] - ); - - // only show this setting if we have indeed a faculty where children might exist - if (Context::get()->isFaculty()) { - if ($GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN) { - $actions->addLink(_("Untergeordnete Institute ignorieren"), - $this->url_for('calendar/instschedule/include_children/0'), - Icon::create('checkbox-checked') - ); - } else { - $actions->addLink(_("Untergeordnete Institute einbeziehen"), - $this->url_for('calendar/instschedule/include_children/1'), - Icon::create('checkbox-unchecked') - ); - } - } - } - - Sidebar::Get()->addWidget($actions); - $semesterSelector = new SemesterSelectorWidget($this->url_for('calendar/instschedule'), 'semester_id', 'post'); - $semesterSelector->includeAll(false); - Sidebar::Get()->addWidget($semesterSelector); - - } - - /** - * Returns an HTML fragment of a grouped entry in the schedule of an institute. - * - * @param string $start the start time of the group, e.g. "1000" - * @param string $end the end time of the group, e.g. "1200" - * @param string $seminars the IDs of the courses - * @param string $day numeric day to show - * - * @return void - */ - function groupedentry_action($start, $end, $seminars, $day) - { - $this->response->add_header('Content-Type', 'text/html; charset=utf-8'); - - // strucutre of an id: seminar_id-cycle_id - // we do not need the cycle id here, so we trash it. - $seminar_list = []; - - foreach (explode(',', $seminars) as $seminar) { - $zw = explode('-', $seminar); - $this->seminars[$zw[0]] = Seminar::getInstance($zw[0]); - } - - $this->start = mb_substr($start, 0, 2) .':'. mb_substr($start, 2, 2); - $this->end = mb_substr($end, 0, 2) .':'. mb_substr($end, 2, 2); - - $day_names = [_("Montag"),_("Dienstag"),_("Mittwoch"), - _("Donnerstag"),_("Freitag"),_("Samstag"),_("Sonntag")]; - - $this->day = $day_names[(int)$day]; - - $this->render_template('calendar/instschedule/_entry_details'); - } - - /** - * Toggle config setting to include children in schedule for the current faculty - * - * @param int $include_childs 0 / false to exclude children 1 / true to include them - */ - function include_children_action($include_childs) - { - $GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', $include_childs ? 1 : 0); - - $this->redirect('calendar/instschedule/index'); - } -} diff --git a/app/controllers/calendar/schedule.php b/app/controllers/calendar/schedule.php index 6fb51c401e254c61e3ca8a7136352ee8f2e04650..bdd0f51c2b4597c49ce2c7a307dc11f4ecd55df8 100644 --- a/app/controllers/calendar/schedule.php +++ b/app/controllers/calendar/schedule.php @@ -144,7 +144,7 @@ class Calendar_ScheduleController extends AuthenticatedController ]; $factory = new Flexi_TemplateFactory($this->dispatcher->trails_root . '/views'); - PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters), 'screen, print'); + PageLayout::addStyle($factory->render('calendar/schedule/stylesheet', $style_parameters), 'screen, print'); if (Request::option('printview')) { $this->calendar_view->setReadOnly(); @@ -243,11 +243,7 @@ class Calendar_ScheduleController extends AuthenticatedController $this->render_template('calendar/schedule/_entry_course'); } else if ($id) { $entry_columns = CalendarScheduleModel::getScheduleEntries($GLOBALS['user']->id, 0, 0, $id); - $entries = []; - $entry_columns = array_pop($entry_columns); - if ($entry_columns) { - $entries = $entry_columns->getEntries(); - } + $entries = array_pop($entry_columns)->getEntries(); $this->show_entry = array_pop($entries); $this->render_template('calendar/schedule/_entry_schedule'); } diff --git a/app/controllers/calendar/single.php b/app/controllers/calendar/single.php deleted file mode 100644 index af802bf6d14395c29922fafec91e33724293561a..0000000000000000000000000000000000000000 --- a/app/controllers/calendar/single.php +++ /dev/null @@ -1,571 +0,0 @@ -<?php -/* - * This is the controller for the single calendar view - * - * 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -require_once 'app/controllers/calendar/calendar.php'; - -class Calendar_SingleController extends Calendar_CalendarController -{ - public function before_filter(&$action, &$args) { - $this->base = 'calendar/single/'; - parent::before_filter($action, $args); - } - - protected function createSidebar($active = null, $calendar = null) - { - parent::createSidebar($active, $calendar); - $sidebar = Sidebar::Get(); - if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) { - $actions = new ActionsWidget(); - $actions->addLink( - _('Termin anlegen'), - $this->url_for('calendar/single/edit'), - Icon::create('add'), - ['data-dialog' => 'size=auto'] - ); - if ($calendar->havePermission(Calendar::PERMISSION_OWN)) { - if (Config::get()->CALENDAR_GROUP_ENABLE) { - $actions->addLink( - _('Kalender freigeben'), - $this->url_for('calendar/single/manage_access'), - Icon::create('community'), - [ - 'id' => 'calendar-open-manageaccess', - 'data-dialog' => '', - 'data-dialogname' => 'manageaccess' - ] - ); - } - $actions->addLink( - _('Veranstaltungstermine'), - $this->url_for('calendar/single/seminar_events'), - Icon::create('seminar'), - ['data-dialog' => 'size=auto'] - ); - } - $sidebar->addWidget($actions); - } - if ($calendar->havePermission(Calendar::PERMISSION_OWN)) { - $export = new ExportWidget(); - $export->addLink(_('Termine exportieren'), - $this->url_for('calendar/single/export_calendar'), - Icon::create('download'), - ['data-dialog' => 'size=auto'] - )->setActive($active == 'export_calendar'); - $export->addLink( - _('Termine importieren'), - $this->url_for('calendar/single/import'), - Icon::create('upload'), - ['data-dialog' => 'size=auto'] - )->setActive($active == 'import'); - $export->addLink( - _('Kalender teilen'), - $this->url_for('calendar/single/share'), - Icon::create('group2'), - ['data-dialog' => 'size=auto'] - )->setActive($active == 'share'); - $sidebar->addWidget($export); - } - } - - public function day_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = SingleCalendar::getDayCalendar($this->range_id, - $this->atime, null, $this->restrictions); - - PageLayout::setTitle($this->getTitle($this->calendar, _('Tagesansicht'))); - - $this->last_view = 'day'; - - $this->createSidebar('day', $this->calendar); - $this->createSidebarFilter(); - } - - public function week_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $timestamp = mktime(12, 0, 0, date('n', $this->atime), date('j', $this->atime), date('Y', $this->atime)); - $monday = $timestamp - 86400 * (strftime('%u', $timestamp) - 1); - $day_count = $this->settings['type_week'] == 'SHORT' ? 5 : 7; - - $this->calendars = []; - for ($i = 0; $i < $day_count; $i++) { - $this->calendars[$i] = - SingleCalendar::getDayCalendar( - $this->range_id, - $monday + $i * 86400, - null, - $this->restrictions - ); - } - - PageLayout::setTitle($this->getTitle($this->calendars[0], _('Wochenansicht'))); - - $this->last_view = 'week'; - - $this->createSidebar('week', $this->calendars[0]); - $this->createSidebarFilter(); - } - - public function month_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $month_start = mktime(12, 0, 0, date('n', $this->atime), 1, date('Y', $this->atime)); - $month_end = mktime(12, 0, 0, date('n', $this->atime), date('t', $this->atime), date('Y', $this->atime)); - $adow = strftime('%u', $month_start) - 1; - $cor = date('n', $this->atime) == 3 ? 1 : 0; - $this->first_day = $month_start - $adow * 86400; - $this->last_day = ((42 - ($adow + date('t', $this->atime))) % 7 + $cor) * 86400 + $month_end; - $this->calendars = []; - for ($start_day = $this->first_day; $start_day <= $this->last_day; $start_day += 86400) { - $this->calendars[] = SingleCalendar::getDayCalendar($this->range_id, - $start_day, null, $this->restrictions); - } - - PageLayout::setTitle($this->getTitle($this->calendars[0], _('Monatsansicht'))); - - $this->last_view = 'month'; - $this->createSidebar('month', $this->calendars[0]); - $this->createSidebarFilter(); - } - - public function year_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $start = mktime(0, 0, 0, 1, 1, date('Y', $this->atime)); - $end = mktime(23, 59, 59, 12, 31, date('Y', $this->atime)); - $this->calendar = new SingleCalendar($this->range_id, $start, $end); - $this->count_list = $this->calendar->getListCountEvents(null, null, - $this->restrictions); - - PageLayout::setTitle($this->getTitle($this->calendar, _('Jahresansicht'))); - - $this->last_view = 'year'; - $this->createSidebar('year', $this->calendar); - $this->createSidebarFilter(); - } - - public function event_action($range_id = null, $event_id = null) - { - PageLayout::setTitle(_('Termindaten')); - - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - $this->event = $this->calendar->getEvent($event_id); - - $this->createSidebar('edit', $this->calendar); - $this->createSidebarFilter(); - } - - public function delete_action($range_id, $event_id) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - if ($this->calendar->deleteEvent($event_id, true)) { - PageLayout::postSuccess(_('Der Termin wurde gelöscht.')); - } - $this->redirect($this->url_for('calendar/single/' . $this->last_view)); - } - - public function delete_recurrence_action($range_id, $event_id, $atime) - { - $this->range_id = $range_id ?: $this->range_id; - $calendar = new SingleCalendar($this->range_id); - $event = $calendar->getEvent($event_id); - if ($event->getRecurrence('rtype') != 'SINGLE') { - $exceptions = $event->getExceptions(); - $exceptions[] = $atime; - $event->setExceptions($exceptions); - if ($event->store() !== false) { - PageLayout::postSuccess(strftime(_('Termin am %x aus Serie gelöscht.'), $atime)); - } - } - $this->redirect($this->url_for('calendar/single/' . $this->last_view)); - } - - public function export_event_action($event_id, $range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $calendar = new SingleCalendar($this->range_id); - $event = $calendar->getEvent($event_id); - if (!$event->isNew()) { - $calender_writer = new CalendarWriterICalendar(); - $export = new CalendarExportFile($calender_writer); - $export->exportFromObjects($event); - $export->sendFile(); - } - $this->render_nothing(); - } - - public function export_calendar_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - - if (Request::submitted('export')) { - $calender_writer = new CalendarWriterICalendar(); - $export = new CalendarExportFile($calender_writer); - if (Request::get('event_type') == 'user') { - $types = ['CalendarEvent']; - } else if (Request::get('event_type') == 'course') { - $types = ['CourseEvent', 'CourseCancelledEvent']; - } else { - $types = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent']; - } - if (Request::get('export_time') == 'date') { - $exstart = $this->parseDateTime(Request::get('export_start')); - $exend = $this->parseDateTime(Request::get('export_end')); - } else { - $exstart = 0; - $exend = Calendar::CALENDAR_END; - } - $export->exportFromDatabase($this->calendar->getRangeId(), $exstart, $exend, $types); - $export->sendFile(); - $this->render_nothing(); - exit; - } - - PageLayout::setTitle($this->getTitle($this->calendar, _('Termine exportieren'))); - - $this->createSidebar('export_calendar', $this->calendar); - $this->createSidebarFilter(); - } - - public function import_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - - if ($this->calendar->havePermission(Calendar::PERMISSION_OWN)) { - if (Request::submitted('import')) { - CSRFProtection::verifySecurityToken(); - $calender_parser = new CalendarParserICalendar(); - $import = new CalendarImportFile($calender_parser, $_FILES['importfile']); - if (Request::get('import_as_private_imp')) { - $import->changePublicToPrivate(); - } - $import->importIntoDatabase($range_id); - $import_count = $import->getCount(); - PageLayout::postMessage(MessageBox::success( - sprintf('Es wurden %s Termine importiert.', $import_count))); - $this->redirect($this->url_for('calendar/single/' . $this->last_view)); - } - } - PageLayout::setTitle($this->getTitle($this->calendar, _('Termine importieren'))); - $this->createSidebar('import', $this->calendar); - $this->createSidebarFilter(); - } - - public function share_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - - $this->short_id = null; - if ($this->calendar->havePermission(Calendar::PERMISSION_OWN)) { - if (Request::submitted('delete_id')) { - CSRFProtection::verifySecurityToken(); - IcalExport::deleteKey($GLOBALS['user']->id); - PageLayout::postSuccess(_('Die Adresse, unter der Ihre Termine abrufbar sind, wurde gelöscht')); - } - - if (Request::submitted('new_id')) { - CSRFProtection::verifySecurityToken(); - $this->short_id = IcalExport::setKey($GLOBALS['user']->id); - PageLayout::postSuccess(_('Eine Adresse, unter der Ihre Termine abrufbar sind, wurde erstellt.')); - } else { - $this->short_id = IcalExport::getKeyByUser($GLOBALS['user']->id); - } - - $text = ""; - if (Request::submitted('submit_email')) { - $email_reg_exp = '/^([-.0-9=?A-Z_a-z{|}~])+@([-.0-9=?A-Z_a-z{|}~])+\.[a-zA-Z]{2,6}$/i'; - if (preg_match($email_reg_exp, Request::get('email')) !== 0) { - $subject = '[' .Config::get()->UNI_NAME_CLEAN . ']' . _('Exportadresse für Ihre Termine'); - $text .= _('Diese Email wurde vom Stud.IP-System verschickt. Sie können auf diese Nachricht nicht antworten.') . "\n\n"; - $text .= _('Über diese Adresse erreichen Sie den Export für Ihre Termine:') . "\n\n"; - $text .= $GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/ical/index/' - . IcalExport::getKeyByUser($GLOBALS['user']->id); - StudipMail::sendMessage(Request::get('email'), $subject, $text); - PageLayout::postSuccess(_('Die Adresse wurde verschickt!')); - } else { - PageLayout::postError(_('Bitte geben Sie eine gültige Email-Adresse an.')); - } - $this->short_id = IcalExport::getKeyByUser($GLOBALS['user']->id); - } - } - PageLayout::setTitle($this->getTitle($this->calendar, _('Kalender teilen oder einbetten'))); - - $this->createSidebar('share', $this->calendar); - $this->createSidebarFilter(); - } - - public function manage_access_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - - $all_calendar_users = - CalendarUser::getUsers($this->calendar->getRangeId()); - - $this->filter_groups = Statusgruppen::findByRange_id( - $this->calendar->getRangeId()); - - $this->users = []; - $this->group_filter_selected = Request::option('group_filter', 'list'); - if ($this->group_filter_selected != 'list') { - $contact_group = Statusgruppen::find($this->group_filter_selected); - $calendar_users = []; - foreach ($contact_group->members as $member) { - $calendar_users[] = new CalendarUser([$this->calendar->getRangeId(), $member->user_id]); - } - $this->calendar_users = SimpleORMapCollection::createFromArray($calendar_users); - } else { - $this->group_filter_selected = 'list'; - $this->calendar_users = $all_calendar_users; - } - - $this->own_perms = []; - foreach ($this->calendar_users as $calendar_user) { - $other_user = CalendarUser::find([$calendar_user->user_id, $this->calendar->getRangeId()]); - if ($other_user) { - $this->own_perms[$calendar_user->user_id] = $other_user->permission; - } else { - $this->own_perms[$calendar_user->user_id] = Calendar::PERMISSION_FORBIDDEN; - } - $this->users[mb_strtoupper(SimpleCollection::translitLatin1($calendar_user->nachname[0]))][] = $calendar_user; - } - - ksort($this->users); - $this->users = array_map(function ($g) { - return SimpleCollection::createFromArray($g)->orderBy('nachname, vorname'); - }, $this->users); - - $this->mps = MultiPersonSearch::get('calendar-manage_access') - ->setTitle(_('Personhinzufügen')) - ->setLinkText(_('Person hinzufügen')) - ->setDefaultSelectedUser($all_calendar_users->pluck('user_id')) - ->setJSFunctionOnSubmit('STUDIP.CalendarDialog.closeMps') - ->setExecuteURL($this->url_for('calendar/single/add_users/' . $this->calendar->getRangeId())) - ->setSearchObject(new StandardSearch('user_id')); - - PageLayout::setTitle($this->getTitle($this->calendar, _('Kalender freigeben'))); - - $this->createSidebar('manage_access', $this->calendar); - $this->createSidebarFilter(); - } - - public function add_users_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - if (Request::isXhr()) { - $added_users = Request::optionArray('added_users'); - } else { - $mps = MultiPersonSearch::load('calendar-manage_access'); - $added_users = $mps->getAddedUsers(); - $mps->clearSession(); - } - - $added = 0; - foreach ($added_users as $user_id) { - $user_to_add = User::find($user_id); - if ($user_to_add) { - $calendar_user = new CalendarUser( - [$this->calendar->getRangeId(), $user_to_add->id]); - if ($calendar_user->isNew()) { - $calendar_user->permission = Calendar::PERMISSION_READABLE; - $added += $calendar_user->store(); - } - } - } - if ($added) { - PageLayout::postSuccess(sprintf( - ngettext( - 'Eine Person wurde mit der Berechtigung zum Lesen des Kalenders hinzugefügt.', - '%s Personen wurden mit der Berechtigung zum Lesen des Kalenders hinzugefügt.', - $added - ), - $added - )); - } - - if (Request::isXhr()) { - $this->response->add_header('X-Dialog-Close', 1); - $this->response->set_status(200); - $this->render_nothing(); - } else { - $this->redirect($this->url_for('calendar/single/manage_access/' . $this->calendar->getRangeId())); - } - } - - public function remove_user_action($range_id = null, $user_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $user_id = $user_id ?: Request::option('user_id'); - $this->calendar = new SingleCalendar($this->range_id); - $calendar_user = new CalendarUser([$this->calendar->getRangeId(), $user_id]); - if (!$calendar_user->isNew()) { - $name = $calendar_user->user->getFullname(); - $calendar_user->delete(); - } - if (Request::isXhr()) { - $this->response->set_status(200); - $this->render_nothing(); - } else { - PageLayout::postSuccess(sprintf(_('Person %s wurde entfernt.', htmlReady($name)))); - $this->redirect($this->url_for('calendar/single/manage_access/' . $this->calendar->getRangeId())); - } - } - - public function store_permissions_action($range_id = null) - { - $this->range_id = $range_id ?: $this->range_id; - $this->calendar = new SingleCalendar($this->range_id); - - $deleted = 0; - $read = 0; - $write = 0; - $submitted_permissions = Request::intArray('perm'); - foreach ($submitted_permissions as $user_id => $new_perm) { - $calendar_user = new CalendarUser([$this->calendar->getRangeId(), $user_id]); - if (!$calendar_user->isNew() && $new_perm == 1) { - $deleted += $calendar_user->delete(); - $new_perm = 0; - } - if ($new_perm >= Calendar::PERMISSION_READABLE - && $calendar_user->permission != $new_perm) { - $calendar_user->permission = $new_perm; - if ($calendar_user->store()) { - if ($new_perm == Calendar::PERMISSION_READABLE) { - $read++; - } else { - $write++; - } - } - } - } - $sum = $deleted + $read + $write; - if ($sum) { - if ($deleted) { - $details[] = sprintf(ngettext('Einer Person wurde die Berechtigungen entzogen.', - '%s Personen wurden die Berechtigungen entzogen.', $deleted), $deleted); - } - if ($read) { - $details[] = sprintf(ngettext('Eine Person wurde auf leseberechtigt gesetzt.', - '%s Personen wurden auf leseberechtigt gesetzt.', $read), $read); - } - if ($write) { - $details[] = sprintf(ngettext('Eine Person wurde auf schreibberechtigt gesetzt.', - '%s Personen wurden auf schreibberechtigt gesetzt.', $write), $write); - } - PageLayout::postSuccess(sprintf( - ngettext('Die Berechtigungen von einer Person wurde geändert.', - 'Die Berechtigungen von %s Personen wurden geändert.', - $sum), $sum), - $details - ); - // no message if the group was changed - } else if (!Request::submitted('calendar_group_submit')) { - PageLayout::postSuccess(_('Es wurden keine Berechtigungen geändert.')); - } - $this->redirect($this->url_for( - 'calendar/single/manage_access/' . $this->calendar->getRangeId(), - ['group_filter' => Request::option('group_filter', 'list')]) - ); - } - - public function seminar_events_action($order_by = null, $order = 'asc') - { - $config_sem = $GLOBALS['user']->cfg->MY_COURSES_SELECTED_CYCLE; - if (!Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS && $config_sem == 'all') { - $config_sem = 'future'; - } - $this->sem_data = Semester::findAllVisible(); - - $sem = $config_sem ?: Config::get()->MY_COURSES_DEFAULT_CYCLE; - if (Request::option('sem_select')) { - $sem = Request::get('sem_select', $sem); - } - if (!in_array($sem, words('future all last current')) && isset($sem)) { - Request::set('sem_select', $sem); - } - $this->group_field = 'sem_number'; - $this->order_by = $order_by; - $this->config_sem_number = Config::get()->IMPORTANT_SEMNUMBER; - // Needed parameters for selecting courses - $params = [ - 'group_field' => $this->group_field, - 'order_by' => $order_by, - 'order' => $order, - 'studygroups_enabled' => false, - 'deputies_enabled' => false - ]; - - $this->sem_courses = MyRealmModel::getPreparedCourses($sem, $params); - $semesters = new SimpleCollection(Semester::getAll()); - $this->sem = $sem; - $this->semesters = $semesters->orderBy('beginn desc'); - - $this->bind_calendar = SimpleCollection::createFromArray( - CourseMember::findBySQL('user_id = ? AND bind_calendar = 1', [$GLOBALS['user']->id]) - )->pluck('seminar_id'); - - } - - public function store_selected_sem_action() - { - CSRFProtection::verifySecurityToken(); - if (Request::submitted('store')) { - $selected_sems = Request::intArray('selected_sem'); - $courses = SimpleORMapCollection::createFromArray( - CourseMember::findBySQL('user_id = ? AND Seminar_id IN (?)', - [$GLOBALS['user']->id, array_keys($selected_sems)])); - $courses->each(function ($a) use ($selected_sems) { - $a->bind_calendar = $selected_sems[$a->seminar_id]; - $a->store(); - }); - PageLayout::postSuccess(_('Die Auswahl der Veranstaltungen wurde gespeichert.')); - } - $this->redirect($this->url_for('calendar/single/' . $this->last_view)); - } - - /** - * Retrieve the title of the calendar depending on calendar owner (range). - * - * @param SingleCalendar $calendar The calendar - * @param string $title_end Additional text - * @return string The complete title for the headline - */ - protected function getTitle(SingleCalendar $calendar, $title_end) - { - $status = ''; - if ($calendar->getRangeId() == $GLOBALS['user']->id) { - $title = _('Mein persönlicher Terminkalender'); - } else { - if ($calendar->getRange() == Calendar::RANGE_USER) { - $title = sprintf(_('Terminkalender von %s'), - $calendar->range_object->getFullname()); - } else { - $title = Context::getHeaderLine(); - } - if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) { - $status = ' (' . _('schreibberechtigt') . ')'; - } else { - $status = ' (' . _('leseberechtigt') . ')'; - } - } - return $title . ' - ' . $title_end . $status ; - } -} diff --git a/app/controllers/contact.php b/app/controllers/contact.php index 0225149222cc7b116c88faa9088a77efeb5ed882..7dd2b0581b3544b92daec411f60508247e80d533 100644 --- a/app/controllers/contact.php +++ b/app/controllers/contact.php @@ -20,15 +20,15 @@ class ContactController extends AuthenticatedController parent::before_filter($action, $args); // Load statusgroups - $this->groups = SimpleCollection::createFromArray(Statusgruppen::findByRange_id(User::findCurrent()->id)); + $this->groups = SimpleCollection::createFromArray(ContactGroup::findByOwner_id(User::findCurrent()->id)); // Load requested group if (!empty($args[0])) { - $this->group = $this->groups->findOneBy('statusgruppe_id', $args[0]); + $this->group = $this->groups->findOneBy('id', $args[0]); //Check for cheaters - if ($this->group->range_id != User::findCurrent()->id) { - throw new AccessDeniedException; + if ($this->group->owner_id !== User::findCurrent()->id) { + throw new AccessDeniedException(); } } @@ -43,16 +43,17 @@ class ContactController extends AuthenticatedController // Check if we need to add contacts $mps = MultiPersonSearch::load('contacts'); $imported = 0; - foreach ($mps->getAddedUsers() as $userId) { - $user_to_add = User::find($userId); + foreach ($mps->getAddedUsers() as $user_id) { + $user_to_add = User::find($user_id); if ($user_to_add) { $new_contact = [ 'owner_id' => User::findCurrent()->id, - 'user_id' => $user_to_add->id]; + 'user_id' => $user_to_add->id, + ]; if ($filter && $this->group) { - $new_contact['group_assignments'][] = [ - 'statusgruppe_id' => $this->group->id, - 'user_id' => $user_to_add->id + $new_contact['groups'][] = [ + 'group_id' => $this->group->id, + 'user_id' => $user_to_add->id, ]; } $imported += (bool)Contact::import($new_contact)->store(); @@ -74,7 +75,7 @@ class ContactController extends AuthenticatedController if ($filter) { $selected = $this->group; - $contacts = SimpleCollection::createFromArray(User::findMany($selected->members->pluck('user_id'))); + $contacts = SimpleCollection::createFromArray(User::findMany($selected->items->pluck('user_id'))); } else { $selected = false; $contacts = User::findCurrent()->contacts; @@ -125,7 +126,7 @@ class ContactController extends AuthenticatedController $contact = Contact::find([User::findCurrent()->id, User::findByUsername($contact_username)->id]); if ($contact) { if ($group) { - $contact->group_assignments->unsetBy('statusgruppe_id', $group); + $contact->groups->unsetBy('group_id', $group); if ($contact->store()) { $removed_group_number++; } @@ -144,7 +145,7 @@ class ContactController extends AuthenticatedController $contact = Contact::find([User::findCurrent()->id, User::findByUsername(Request::username('user'))->id]); if ($contact) { if ($group) { - $contact->group_assignments->unsetBy('statusgruppe_id', $group); + $contact->group_assignments->unsetBy('group_id', $group); if ($contact->store()) { PageLayout::postSuccess(_('Der Kontakt wurde aus der Gruppe entfernt.')); } @@ -161,8 +162,8 @@ class ContactController extends AuthenticatedController public function editGroup_action() { if (!$this->group) { - $this->group = new Statusgruppen(); - $this->group->range_id = User::findCurrent()->id; + $this->group = new ContactGroup(); + $this->group->owner_id = User::findCurrent()->id; } if (Request::submitted('store')) { CSRFProtection::verifyRequest(); diff --git a/app/controllers/ical.php b/app/controllers/ical.php index 4ecdc13755fdef584fbb38a7c5739b2d744347c3..44ff2a1c97b8f1ec31f8c6e27492f1e7ac5bf136 100644 --- a/app/controllers/ical.php +++ b/app/controllers/ical.php @@ -51,17 +51,14 @@ class iCalController extends StudipController $GLOBALS['user'] = new Seminar_User($user_id); $GLOBALS['perm'] = new Seminar_Perm(); - $extype = 'ALL_EVENTS'; - $calender_writer = new CalendarWriterICalendar(); - $export = new CalendarExport($calender_writer); - $export->exportFromDatabase($user_id, strtotime('-4 week'), 2114377200, 'ALL_EVENTS'); - - if ($GLOBALS['_calendar_error']->getMaxStatus(ErrorHandler::ERROR_CRITICAL)) { - $this->set_status(500); - $this->render_nothing(); - return; - } - $content = join($export->getExport()); + $end = DateTime::createFromFormat('U', '2114377200'); + $start = new DateTime(); + $start->modify('-4 week'); + $ical_export = new ICalendarExport(); + $ical = $ical_export->exportCalendarDates($user_id, $start, $end) + . $ical_export->exportCourseDates($user_id, $start, $end) + . $ical_export->exportCourseExDates($user_id, $start, $end); + $content = $ical_export->writeHeader() . $ical . $ical_export->writeFooter(); if (mb_stripos($_SERVER['HTTP_USER_AGENT'], 'google-calendar') !== false) { $content = str_replace(['CLASS:PRIVATE','CLASS:CONFIDENTIAL'], 'CLASS:PUBLIC', $content); } diff --git a/app/controllers/institute/overview.php b/app/controllers/institute/overview.php index f511bd47ef720df04459db73bd202f33943d52c0..66d55e1b42b31663c2d2bd50de6456f147217f40 100644 --- a/app/controllers/institute/overview.php +++ b/app/controllers/institute/overview.php @@ -144,10 +144,6 @@ class Institute_OverviewController extends AuthenticatedController $response = $this->relay('questionnaire/widget/' . $this->institute_id . '/institute'); $this->questionnaires = $response->body; } - - // Fetch dates - $response = $this->relay("calendar/contentbox/display/$this->institute_id/1210000"); - $this->dates = $response->body; } } diff --git a/app/controllers/institute/schedule.php b/app/controllers/institute/schedule.php new file mode 100644 index 0000000000000000000000000000000000000000..1c5d091252b0c3f4e34a60915ab0318154333027 --- /dev/null +++ b/app/controllers/institute/schedule.php @@ -0,0 +1,179 @@ +<?php +class Institute_ScheduleController extends AuthenticatedController +{ + public function before_filter(&$action, &$args) + { + parent::before_filter($action, $args); + + if (Navigation::hasItem('/course/main')) { + Navigation::activateItem('/course/main'); + } + + if (!$GLOBALS['perm']->have_studip_perm('autor', Context::getId())) { + throw new AccessDeniedException(); + } + } + + public function index_action($institute_id) + { + PageLayout::setTitle(_('Veranstaltungs-Stundenplan')); + + if (Navigation::hasItem('/course/main/schedule')) { + Navigation::activateItem('/course/main/schedule'); + } + + $semester = null; + if (Request::submitted('semester_id')) { + $semester = Semester::find(Request::option('semester_id')); + } else { + $semester = Semester::findCurrent(); + } + + $extra_params = []; + if ($semester) { + $extra_params['semester_id'] = $semester->id; + } + + $sidebar = Sidebar::get(); + $semester_widget = new SemesterSelectorWidget($this->url_for('institute/schedule/index/' . $institute_id)); + if ($semester) { + $semester_widget->setSelection($semester->id); + } + $sidebar->addWidget($semester_widget); + + $calendar_settings = $GLOBALS['user']->cfg->CALENDAR_SETTINGS ?? []; + $week_slot_duration = \Studip\Calendar\Helper::getCalendarSlotDuration('week'); + + $this->fullcalendar = \Studip\Fullcalendar::create( + _('Veranstaltungs-Stundenplan'), + [ + 'minTime' => '08:00', + 'maxTime' => '20:00', + 'allDaySlot' => false, + 'header' => [ + 'left' => '', + 'right' => '' + ], + 'views' => [ + 'timeGridWeek' => [ + 'columnHeaderFormat' => ['weekday' => 'long'], + 'weekends' => $calendar_settings['type_week'] === 'LONG', + 'slotDuration' => $week_slot_duration + ] + ], + 'defaultView' => 'timeGridWeek', + 'defaultDate' => date('Y-m-d'), + 'timeGridEventMinHeight' => 20, + 'eventSources' => [ + [ + 'url' => $this->url_for('institute/schedule/data/' . $institute_id), + 'method' => 'GET', + 'extraParams' => $extra_params + ] + ] + ] + ); + } + + + public function data_action($institute_id) + { + //Fullcalendar sets the week time range in which to put the course dates + //of the semester. Therefore, start and end are handled in here. + $begin = Request::getDateTime('start', \DateTime::RFC3339); + $end = Request::getDateTime('end', \DateTime::RFC3339); + if (!($begin instanceof DateTime) || !($end instanceof DateTime)) { + //No time range specified. + throw new InvalidArgumentException('Invalid parameters!'); + } + + $semester_id = Request::option('semester_id'); + $semester = Semester::find($semester_id); + if (!$semester) { + $this->render_json([]); + return; + } + + //Get all regular course dates for that semester: + $cycle_dates = SeminarCycleDate::findBySql( + 'JOIN `termine` USING (`metadate_id`) + JOIN `seminare` USING (`seminar_id`) + JOIN `seminar_inst` USING (`seminar_id`) + WHERE `seminar_inst`.`institut_id` = :institute_id + AND ( + `termine`.`date` BETWEEN :begin AND :end + OR `termine`.`end_time` BETWEEN :begin AND :end + ) + GROUP BY `metadate_id`', + [ + 'institute_id' => $institute_id, + 'begin' => $semester->beginn, + 'end' => $semester->ende + ] + ); + + if (!$cycle_dates) { + $this->render_json([]); + return; + } + + foreach ($cycle_dates as $cycle_date) { + //Calculate a fake begin and end that lies in the week + //fullcalendar has specified. + $fake_begin = clone $begin; + $fake_end = clone $begin; + if ($cycle_date->weekday > 1) { + $fake_begin = $fake_begin->add(new DateInterval('P' . ($cycle_date->weekday - 1) . 'D')); + $fake_end = $fake_end->add(new DateInterval('P' . ($cycle_date->weekday - 1) . 'D')); + } + $start_time_parts = explode(':', $cycle_date->start_time); + $end_time_parts = explode(':', $cycle_date->end_time); + $fake_begin->setTime( + (int) $start_time_parts[0], + (int) $start_time_parts[1], + (int) $start_time_parts[2] + ); + $fake_end->setTime( + (int) $end_time_parts[0], + (int) $end_time_parts[1], + (int) $end_time_parts[2] + ); + + //Get the course colour: + $course_membership = CourseMember::findOneBySQL( + 'seminar_id = :course_id AND user_id = :user_id', + [ + 'course_id' => $cycle_date->seminar_id, + 'user_id' => $GLOBALS['user']->id + ] + ); + $event_classes = []; + if ($course_membership) { + $event_classes[] = sprintf('course-color-%u', $course_membership->gruppe); + } + + $event = new \Studip\Calendar\EventData( + $fake_begin, + $fake_end, + $cycle_date->course->getFullName(), + $event_classes, + '', + '', + false, + 'SeminarCycleDate', + $cycle_date->id, + '', + '', + 'course', + $cycle_date->seminar_id, + [ + 'show' => $this->url_for('course/details', ['cid' => $cycle_date->seminar_id, 'link_to_course' => '1']) + ] + ); + + $result[] = $event->toFullcalendarEvent(); + } + + $this->render_json($result); + } +} diff --git a/app/controllers/settings/calendar.php b/app/controllers/settings/calendar.php index 5704246cca1ff41528b955a121c12967bcc39110..b2f4f1c55af12a359cd431fd4689e49f09efeac1 100644 --- a/app/controllers/settings/calendar.php +++ b/app/controllers/settings/calendar.php @@ -34,7 +34,7 @@ class Settings_CalendarController extends Settings_SettingsController parent::before_filter($action, $args); PageLayout::setHelpKeyword('Basis.MyStudIPTerminkalender'); - PageLayout::setTitle(_('Einstellungen des Terminkalenders anpassen')); + PageLayout::setTitle(_('Einstellungen des Kalenders anpassen')); Navigation::activateItem('/profile/settings/calendar_new'); } @@ -71,6 +71,11 @@ class Settings_CalendarController extends Settings_SettingsController ]); PageLayout::postSuccess(_('Ihre Einstellungen wurden gespeichert')); - $this->redirect('settings/calendar'); + if (Request::isDialog()) { + $this->response->add_header('X-Dialog-Close', '1'); + $this->render_nothing(); + } else { + $this->redirect('settings/calendar'); + } } } diff --git a/app/views/calendar/calendar/add_courses.php b/app/views/calendar/calendar/add_courses.php new file mode 100644 index 0000000000000000000000000000000000000000..cf3d282ca6498decfea1f89a5bd58a1aea415a21 --- /dev/null +++ b/app/views/calendar/calendar/add_courses.php @@ -0,0 +1,15 @@ +<form class="default" method="post" action="<?= $controller->link_for('calendar/calendar/add_courses') ?>"> + <?= CSRFProtection::tokenTag() ?> + <fieldset class="simplevue"> + <legend><?= _('Veranstaltungen für den Kalender auswählen') ?></legend> + <my-courses-coloured-table name="courses" + :selected_course_ids="<?= htmlReady(json_encode($selected_course_ids)) ?>" + :default_semester_id="'<?= htmlReady($selected_semester_id) ?>'" + :semester_data="<?= htmlReady(json_encode($available_semester_data)) ?>" + ></my-courses-coloured-table> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Übernehmen'), 'add') ?> + <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?> + </div> +</form> diff --git a/app/views/calendar/calendar/course.php b/app/views/calendar/calendar/course.php new file mode 100644 index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01 --- /dev/null +++ b/app/views/calendar/calendar/course.php @@ -0,0 +1 @@ +<?= $fullcalendar ?> diff --git a/app/views/calendar/calendar/export.php b/app/views/calendar/calendar/export.php new file mode 100644 index 0000000000000000000000000000000000000000..3fa302eeab003393f1631589c0f3fb0ddfef7b09 --- /dev/null +++ b/app/views/calendar/calendar/export.php @@ -0,0 +1,45 @@ +<?php +/** + * @var Calendar_CalendarController $controller + * @var string $user_id + * @var string $dates_to_export + * @var DateTimeImmutable $begin + * @var DateTimeImmutable $end + */ +?> +<form class="default" method="post" + action="<?= $controller->link_for('calendar/calendar/export/' . $user_id) ?>"> + <?= CSRFProtection::tokenTag() ?> + <fieldset> + <legend><?= _('Termine exportieren') ?></legend> + <label> + <?= _('Zu exportierende Termine') ?> + <select name="dates_to_export"> + <option value="user" + <?= $dates_to_export === 'user' ? 'selected' : '' ?>> + <?= _('Persönliche Termine') ?> + </option> + <option value="course" + <?= $dates_to_export === 'course' ? 'selected' : '' ?>> + <?= _('Veranstaltungstermine') ?> + </option> + <option value="all" + <?= $dates_to_export === 'all' ? 'selected' : '' ?>> + <?= _('Alle Termine') ?> + </option> + </select> + </label> + <label> + <?= _('Startdatum') ?> + <input type="text" value="<?= htmlReady($begin->format('d.m.Y')) ?>" name="begin" data-date-picker> + </label> + <label> + <?= _('Enddatum') ?> + <input type="text" value="<?= htmlReady($end->format('d.m.Y')) ?>" name="end" data-date-picker> + </label> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Exportieren'), 'export') ?> + <?= \Studip\Button::createCancel(_('Abbrechen')) ?> + </div> +</form> diff --git a/app/views/calendar/single/import.php b/app/views/calendar/calendar/import.php similarity index 59% rename from app/views/calendar/single/import.php rename to app/views/calendar/calendar/import.php index db1feb3feccc2ced0e70043bd85cc50f31e4748f..d91025a6078f938f15e1a402509dc7d151a14815 100644 --- a/app/views/calendar/single/import.php +++ b/app/views/calendar/calendar/import.php @@ -1,27 +1,30 @@ -<? -use Studip\Button, Studip\LinkButton; +<?php +/** + * @var Calendar_CalendarController $controller + */ ?> -<form action="<?= $controller->link_for('calendar/single/import/' . $calendar->getRangeId(), ['atime' => $atime, 'last_view' => $last_view]) ?>" method="post" enctype="multipart/form-data" class="default"> +<form class="default" + method="post" + data-dialog="size=auto" + enctype="multipart/form-data" + action="<?= $controller->link_for('calendar/calendar/import_file/') ?>"> <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>"> <?= CSRFProtection::tokenTag() ?> <fieldset> <legend> <?= sprintf(_('Termine importieren')) ?> </legend> - <label for="event-type"> <input type="checkbox" name="import_privat" value="1" checked> <?= _('Öffentliche Termine als "privat" importieren') ?> </label> - - <label class="file-upload"> + <label> <span class="required"><?= _('Datei zum Importieren wählen') ?></span> <input required type="file" name="importfile" accept=".ics,.ifb,.iCal,.iFBf"> </label> </fieldset> - <footer data-dialog-button> - <?= Button::createAccept(_('Termine importieren'), 'import') ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?> + <?= \Studip\Button::create(_('Importieren'), 'import') ?> + <?= \Studip\Button::createCancel(_('Abbrechen')) ?> </footer> </form> diff --git a/app/views/calendar/calendar/index.php b/app/views/calendar/calendar/index.php new file mode 100644 index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01 --- /dev/null +++ b/app/views/calendar/calendar/index.php @@ -0,0 +1 @@ +<?= $fullcalendar ?> diff --git a/app/views/calendar/single/share.php b/app/views/calendar/calendar/publish.php similarity index 87% rename from app/views/calendar/single/share.php rename to app/views/calendar/calendar/publish.php index eef5ca3086e2aa66e1fdd40821320fb3abcd5dbc..71901b506ed2f099827aa05c502ade79f211db1b 100644 --- a/app/views/calendar/single/share.php +++ b/app/views/calendar/calendar/publish.php @@ -1,10 +1,5 @@ <? use Studip\Button, Studip\LinkButton; ?> -<? if (Request::isXhr()) : ?> - <? foreach (PageLayout::getMessages() as $messagebox) : ?> - <?= $messagebox ?> - <? endforeach ?> -<? endif; ?> -<form data-dialog="size=auto" action="<?= $controller->url_for('calendar/single/share/' . $calendar->getRangeId()) ?>" method="post" class="default"> +<form data-dialog="size=auto" action="<?= $controller->link_for('calendar/calendar/publish/') ?>" method="post" class="default"> <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>"> <?= CSRFProtection::tokenTag() ?> @@ -54,7 +49,7 @@ <? endif ?> <? if (!Request::isXhr()) : ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?> + <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?> <? endif; ?> </footer> </form> diff --git a/app/views/calendar/calendar/share.php b/app/views/calendar/calendar/share.php new file mode 100644 index 0000000000000000000000000000000000000000..69663d260c2318829cced12434b29663bdbe0394 --- /dev/null +++ b/app/views/calendar/calendar/share.php @@ -0,0 +1,13 @@ +<form class="default" method="post" + action="<?= $controller->link_for('calendar/calendar/share') ?>" + data-dialog="reload-on-close"> + <?= CSRFProtection::tokenTag() ?> + <fieldset class="simplevue"> + <calendar-permissions-table name="calendar" + :selected_users="<?= htmlReady($selected_users_json ?? '{}') ?>" + searchtype="<?= htmlReady((string) $searchtype) ?>"></calendar-permissions-table> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Aktualisieren'), 'share') ?> + </div> +</form> diff --git a/app/views/calendar/contentbox/_termin.php b/app/views/calendar/contentbox/_termin.php index 3c3fcffc942cfc45c7ca25b7c2f53009be414e2c..ded7d0bc2289be1fca13770262e914f2e8d381df 100644 --- a/app/views/calendar/contentbox/_termin.php +++ b/app/views/calendar/contentbox/_termin.php @@ -1,53 +1,88 @@ -<article class="studip toggle <?= ContentBoxHelper::classes($termin['id']) ?>" id="<?= $termin['id'] ?>"> +<article class="studip toggle <?= ContentBoxHelper::classes($termin->getObjectId()) ?>" + id="<?= htmlReady($termin->getObjectId()) ?>"> <header> <h1> - <a href="<?= ContentBoxHelper::href($termin['id']) ?>"> - <?= Icon::create('date', 'inactive')->asImg(['class' => 'text-bottom']) ?> - <?= htmlReady($termin['title']) ?> + <a href="<?= ContentBoxHelper::href($termin->getObjectId()) ?>"> + <?= Icon::create('date', Icon::ROLE_INACTIVE)->asImg(['class' => 'text-bottom']) ?> + <?= htmlReady($titles[$termin->getObjectId()] ?? $termin->getTitle()) ?> </a> </h1> <nav> <span> - <?= $termin['room'] ? _('Raum') . ': ' . formatLinks($termin['room']) : '' ?> + <?= $termin->getLocation() ? _('Raum') . ': ' . formatLinks($termin->getLocation()) : '' ?> </span> - <? if($admin && $isProfile && $termin['type'] === 'CalendarEvent'): ?> - <a href="<?= URLHelper::getLink('dispatch.php/calendar/single/edit/' . $termin['range_id'] . '/' . $termin['event_id'], ['source_page' => 'dispatch.php/profile']) ?>"> - <?= Icon::create('edit', 'clickable')->asImg(['class' => 'text-bottom']) ?> - </a> - <? endif; ?> + <? if ($admin && $isProfile && $termin->getObjectClass() === 'CalendarDateAssignment') : ?> + <a href="<?= URLHelper::getLink('dispatch.php/calendar/calendar') ?>" + title="<?= _('Zum Kalender') ?>"> + <?= Icon::create('schedule')->asImg(['class' => 'text-bottom']) ?> + </a> + <? if ($termin->calendar_date->isWritable($GLOBALS['user']->id)) : ?> + <a href="<?= URLHelper::getLink('dispatch.php/calendar/date/edit/' . $termin->getPrimaryObjectId()) ?>" + title="<?= _('Termin bearbeiten') ?>" + data-dialog> + <?= Icon::create('edit')->asImg(['class' => 'text-bottom']) ?> + </a> + <? endif ?> + <? elseif (!$course_range && in_array($termin->getObjectClass(), ['CourseDate', 'CourseExDate'])) : ?> + <a href="<?= URLHelper::getLink('dispatch.php/course/dates', ['cid' => $termin->getPrimaryObjectId()]) ?>" + title="<?= _('Zur Veranstaltung') ?>"> + <?= Icon::create('seminar')->asImg(['class'=> 'text-bottom']) ?> + </a> + <? endif ?> </nav> </header> <div> - <? $themen = $termin['topics'] ?? [] ?> - <? if ($termin['description'] || count($themen)) : ?> - <p><?= formatReady($termin['description']) ?></p> - <? if (count($themen)) : ?> - <? foreach ($themen as $thema) : ?> - <h3> - <?= Icon::create('topic', Icon::ROLE_INFO)->asImg(20, ['class' => "text-bottom"]) ?> - <?= htmlReady($thema['title']) ?> - </h3> - <div> - <?= formatReady($thema['description']) ?> - </div> - <? endforeach ?> - <? endif ?> + <? + $themen = []; + if ($termin instanceof CourseDate) { + $themen = $termin->topics->toArray('title description'); + } + $description = ''; + if ($termin instanceof CourseExDate) { + $description = $termin->content; + } elseif ($termin instanceof CourseDate && isset($termin->cycle)) { + $description = $termin->cycle->description; + } else { + $description = $termin->getDescription(); + } + ?> + <? if ($description || count($themen) > 0) : ?> + <p><?= formatReady($description) ?></p> + <? if (count($themen)) : ?> + <? foreach ($themen as $thema) : ?> + <h3> + <?= Icon::create('topic', Icon::ROLE_INFO)->asImg(20, ['class' => "text-bottom"]) ?> + <?= htmlReady($thema['title']) ?> + </h3> + <div> + <?= formatReady($thema['description']) ?> + </div> + <? endforeach ?> + <? endif ?> <? else : ?> <?= _('Keine Beschreibung vorhanden') ?> <? endif ?> <ul class="list-csv" style="text-align: center;"> - <? foreach($termin['info'] as $type => $info): ?> - <? if (trim($info)) : ?> - <li> - <small> - <? if (!is_numeric($type)): ?> - <em><?= htmlReady($type) ?>:</em> - <? endif; ?> - <?= htmlReady(trim($info)) ?> - </small> - </li> - <? endif ?> - <? endforeach; ?> + <? foreach ($termin->getAdditionalDescriptions() as $type => $info) : ?> + <? if (trim($info)) : ?> + <li> + <small> + <? if (!is_numeric($type)): ?> + <em><?= htmlReady($type) ?>:</em> + <? endif; ?> + <?= htmlReady(trim($info)) ?> + </small> + </li> + <? endif ?> + <? endforeach ?> </ul> + <? if (!$course_range && in_array($termin->getObjectClass(), [CourseDate::class, CourseExDate::class])) : ?> + <div> + <a href="<?= URLHelper::getLink('dispatch.php/course/dates', ['cid' => $termin->getPrimaryObjectId()]) ?>"> + <?= Icon::create('link-intern')->asImg(['class'=> 'text-bottom']) ?> + <?= _('Zur Veranstaltung') ?> + </a> + </div> + <? endif ?> </div> </article> diff --git a/app/views/calendar/contentbox/display.php b/app/views/calendar/contentbox/display.php index 41498f4e19cce6f8e3762895d6cc8783c2f52e91..c0387fd98194edacc0c48dd69fa6aeb6ef916125 100644 --- a/app/views/calendar/contentbox/display.php +++ b/app/views/calendar/contentbox/display.php @@ -1,36 +1,35 @@ -<? if ($admin || $termine): ?> -<article class="studip"> - <header> - <h1> - <?= Icon::create('schedule', 'info')->asImg() ?> - <?= htmlReady($title) ?> - </h1> - <nav> - <? if ($admin): ?> - <? if ($isProfile): ?> - <a href="<?= URLHelper::getLink('dispatch.php/calendar/single/edit/', ['source_page' => 'dispatch.php/profile']) ?>"> - <?= Icon::create('add', 'clickable')->asImg(['class' => 'text-bottom']) ?> - </a> +<? if ($admin || $termine) : ?> + <article class="studip"> + <header> + <h1> + <?= Icon::create('schedule', 'info')->asImg() ?> + <?= htmlReady($title) ?> + </h1> + <nav> + <? if ($admin) : ?> + <? if ($isProfile) : ?> + <a href="<?= URLHelper::getLink('dispatch.php/calendar/date/add') ?>" + data-dialog="reload-on-close" + title="<?= _('Neuen Termin anlegen') ?>"> + <?= Icon::create('add', 'clickable')->asImg(['class' => 'text-bottom']) ?> + </a> + <? else: ?> + <a href="<?= URLHelper::getLink("dispatch.php/course/timesrooms", ['cid' => $range_id]) ?>" + title="<?= _('Neuen Termin anlegen') ?>"> + <?= Icon::create('admin', 'clickable')->asImg(['class' => 'text-bottom']) ?> + </a> + <? endif ?> + <? endif ?> + </nav> + </header> + <? if ($termine) : ?> + <? foreach ($termine as $termin) : ?> + <?= $this->render_partial('calendar/contentbox/_termin.php', ['termin' => $termin, 'course_range' => $course_range]) ?> + <? endforeach ?> <? else: ?> - <a href="<?= URLHelper::getLink("dispatch.php/course/timesrooms", ['cid' => $range_id]) ?>"> - <?= Icon::create('admin', 'clickable')->asImg(['class' => 'text-bottom']) ?> - </a> - <? endif; ?> - <? endif; ?> - </nav> - </header> - <? if ($termine): ?> - <? foreach ($termine as $termin): ?> - <?= $this->render_partial('calendar/contentbox/_termin.php', ['termin' => $termin]); ?> - <? endforeach; ?> -<? else: ?> - <section> - <? if ($isProfile): ?> - <?= _('Es sind keine aktuellen Termine vorhanden. Um neue Termine zu erstellen, klicken Sie rechts auf das Plus.') ?> - <? else: ?> - <?= _('Es sind keine aktuellen Termine vorhanden. Um neue Termine zu erstellen, klicken Sie rechts auf die Zahnräder.') ?> - <? endif; ?> - </section> - <? endif; ?> -</article> -<? endif; ?> + <section> + <?= _('Es sind keine aktuellen Termine vorhanden. Zum Anlegen neuer Termine können Sie die Aktion „Neuen Termin anlegen“ benutzen.') ?> + </section> + <? endif ?> + </article> +<? endif ?> diff --git a/app/views/calendar/date/_add_edit_form.php b/app/views/calendar/date/_add_edit_form.php new file mode 100644 index 0000000000000000000000000000000000000000..7c922e41f6191629e1811a58fe51cf5f13f77b7f --- /dev/null +++ b/app/views/calendar/date/_add_edit_form.php @@ -0,0 +1,154 @@ +<form class="default new-calendar-date-form" method="post" action="<?= $form_post_link ?>" + data-dialog="reload-on-close"> + <?= CSRFProtection::tokenTag() ?> + + <? if ($return_path = Request::get('return_path')) : ?> + <input type="hidden" name="return_path" value="<?= htmlReady($return_path) ?>"> + <? endif ?> + + <? if ($user_id) : ?> + <input type="hidden" name="user_id" value="<?= htmlReady($user_id) ?>"> + <? endif ?> + <? if ($group_id) : ?> + <input type="hidden" name="group_id" value="<?= htmlReady($group_id) ?>"> + <? endif ?> + + <article aria-live="assertive" + class="validation_notes studip"> + <header> + <h1> + <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(['class' => 'text-bottom validation_notes_icon']) ?> + <?= _('Hinweise zum Ausfüllen des Formulars') ?> + </h1> + </header> + <div class="required_note"> + <div aria-hidden="true"> + <?= _('Pflichtfelder sind mit Sternchen gekennzeichnet.') ?> + </div> + <div class="sr-only"> + <?= _('Dieses Formular enthält Pflichtfelder.') ?> + </div> + </div> + <? if ($form_errors) : ?> + <div> + <?= _('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') ?> + <ul> + <? foreach ($form_errors as $field => $error) : ?> + <li><?= htmlReady($field) ?>: <?= htmlReady($error) ?></li> + <? endforeach ?> + </ul> + </div> + <? endif ?> + </article> + + <fieldset> + <legend><?= _('Grunddaten') ?></legend> + <label class="studiprequired"> + <?= _('Titel') ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <input type="text" name="title" required="required" + value="<?= htmlReady($date->title) ?>"> + </label> + <div class="hgroup"> + <label class="studiprequired"> + <?= _('Beginn') ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <input type="text" name="begin" class="begin-input" data-datetime-picker + required="required" value="<?= date('d.m.Y H:i', $date->begin) ?>"> + </label> + <label class="studiprequired"> + <?= _('Ende') ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <input type="text" name="end" class="end-input" data-datetime-picker + required="required" value="<?= date('d.m.Y H:i', $date->end) ?>"> + </label> + </div> + <label> + <input type="checkbox" name="all_day" value="1" <?= $all_day_event ? 'checked' : '' ?> + data-deactivates=".new-calendar-date-form input[name='end']"> + <?= _('Ganztägiger Termin') ?> + </label> + <label> + <?= _('Zugriff') ?> + <div class="flex-row"> + <select name="access"> + <option value="PUBLIC" <?= $date->access === 'PUBLIC' ? 'selected' : '' ?>> + <?= _('Öffentlich zugänglich') ?> + </option> + <option value="PRIVATE" <?= $date->access === 'PRIVATE' ? 'selected' : '' ?>> + <?= _('Privat') ?> + </option> + <option value="CONFIDENTIAL" <?= $date->access === 'CONFIDENTIAL' ? 'selected' : '' ?>> + <?= _('Vertraulich') ?> + </option> + </select> + <?= tooltipIcon( + _('Öffentliche Termine sind systemweit sichtbar. Private Termine sind für Personen, denen der Kalender freigegeben wurde, sichtbar. Vertrauliche Termine sind hingegen nur für einen selbst sichtbar.') + ) ?> + </div> + </label> + <label> + <?= _('Beschreibung') ?> + <textarea name="description"><?= htmlReady($date->description) ?></textarea> + </label> + <label class="studiprequired"> + <?= _('Kategorie') ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <select class="" name="category" required> + <? foreach ($category_options as $key => $option) : ?> + <option value="<?= htmlReady($key) ?>" <?= $key === intval($date->category) ? 'selected' : '' ?>> + <?= htmlReady($option) ?> + </option> + <? endforeach ?> + </select> + </label> + <label> + <?= _('Eigene Kategorie') ?> + <input type="text" name="user_category" value="<?= htmlReady($date->user_category) ?>"> + </label> + <label> + <?= _('Ort') ?> + <input type="text" name="location" value="<?= htmlReady($date->location) ?>"> + </label> + </fieldset> + <fieldset class="simplevue"> + <legend><?= _('Wiederholung') ?></legend> + <?= $date->getRepetitionInputHtml('repetition') ?> + </fieldset> + <fieldset class="simplevue"> + <legend><?= _('Ausnahmen') ?></legend> + <date-list-input name="exceptions" :selected_dates="<?= htmlReady(json_encode($exceptions)) ?>"></date-list-input> + </fieldset> + <? if (Config::get()->CALENDAR_GROUP_ENABLE && $user_quick_search_type) : ?> + <fieldset class="simplevue"> + <legend><?= _('Teilnehmende Personen') ?></legend> + <editable-list + name="assigned_calendar_ids" + quicksearch="<?= htmlReady($user_quick_search_type) ?>" + :items="<?= htmlReady(json_encode($calendar_assignment_items)) ?>" + ></editable-list> + </fieldset> + <? elseif ($calendar_assignment_items) : ?> + <? foreach ($calendar_assignment_items as $item) : ?> + <input type="hidden" name="assigned_calendar_ids[]" value="<?= htmlReady($item['value']) ?>"> + <? endforeach ?> + <? elseif ($owner_id): ?> + <input type="hidden" name="assigned_calendar_ids[]" value="<?= htmlReady($owner_id) ?>"> + <? endif ?> + + <footer data-dialog-button> + <? if ($date->isNew()) : ?> + <?= \Studip\Button::create(_('Anlegen'), 'save') ?> + <? else : ?> + <?= \Studip\Button::create(_('Speichern'), 'save') ?> + <? endif ?> + <? if (!$date->isNew()) : ?> + <?= \Studip\LinkButton::create( + _('Löschen'), + $controller->url_for('calendar/date/delete/' . $date->id), + ['data-dialog' => 'reload-on-close'] + ) ?> + <? endif ?> + <?= \Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('calendar/calendar')) ?> + </footer> +</form> diff --git a/app/views/calendar/date/add.php b/app/views/calendar/date/add.php new file mode 100644 index 0000000000000000000000000000000000000000..29c94c216205d49355fc83fd634333b8796e9c9b --- /dev/null +++ b/app/views/calendar/date/add.php @@ -0,0 +1,4 @@ +<?= $this->render_partial('calendar/date/_add_edit_form', [ + 'action_link' => $controller->link_for('calendar/date/add'), + 'event' => null +]) ?> diff --git a/app/views/calendar/date/delete.php b/app/views/calendar/date/delete.php new file mode 100644 index 0000000000000000000000000000000000000000..ccb982973d5700158800bff93a8fa942a3b8339c --- /dev/null +++ b/app/views/calendar/date/delete.php @@ -0,0 +1,51 @@ +<form class="default" method="post" data-dialog="reload-on-close" + action="<?= $controller->link_for('calendar/date/delete/' . $date->id) ?>"> + <?= CSRFProtection::tokenTag() ?> + <? if ($date_has_repetitions) : ?> + <input type="hidden" name="selected_date" value="<?= $selected_date->format('Y-m-d') ?>"> + <fieldset> + <legend><?= _('Es handelt sich um einen Termin in einer Terminserie. Was möchten Sie tun?') ?></legend> + <label> + <input type="radio" name="repetition_handling" value="create_exception" + <?= $repetition_handling === 'create_exception' ? 'checked' : '' ?>> + <?= sprintf( + _('Am %s soll aus dem Einzeltermin eine Ausnahme der Terminserie werden.'), + $selected_date->format('d.m.Y') + ) ?> + </label> + <label> + <input type="radio" name="repetition_handling" value="delete_all" + <?= $repetition_handling === 'delete_all' ? 'checked' : '' ?>> + <?= _('Die gesamte Terminserie soll gelöscht werden.') ?> + </label> + </fieldset> + <? else : ?> + <?= MessageBox::warning(_('Soll der folgende Termin wirklich gelöscht werden?')) ?> + <? endif ?> + <fieldset> + <legend><?= _('Informationen') ?></legend> + <dl> + <dt><?= _('Zeitbereich') ?></dt> + <dd> + <?= htmlReady(date('d.m.Y H:i', $date->begin)) ?> + - + <?= htmlReady(date('d.m.Y H:i', $date->end)) ?> + </dd> + <dt><?= _('Titel') ?></dt> + <dd><?= htmlReady($date->title) ?></dd> + <? if ($date->description) : ?> + <dt><?= _('Beschreibung') ?></dt> + <dd><?= htmlReady($date->description) ?></dd> + <? endif ?> + <dt><?= _('Zugriff') ?></dt> + <dd><?= htmlReady($date->getAccessAsString()) ?></dd> + <? if ($date->repetition_type) : ?> + <dt><?= _('Wiederholung') ?></dt> + <dd><?= htmlReady($date->getRepetitionAsString()) ?></dd> + <? endif ?> + </dl> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Löschen'), 'delete') ?> + </div> +</form> diff --git a/app/views/calendar/date/edit.php b/app/views/calendar/date/edit.php new file mode 100644 index 0000000000000000000000000000000000000000..6090dfcbf4e2bd27b4060b5b9e9b854973adf187 --- /dev/null +++ b/app/views/calendar/date/edit.php @@ -0,0 +1,4 @@ +<?= $this->render_partial('calendar/date/_add_edit_form', [ + 'action_link' => $controller->link_for('calendar/date/edit/' . $date->id), + 'date' => $date +]) ?> diff --git a/app/views/calendar/date/index.php b/app/views/calendar/date/index.php new file mode 100644 index 0000000000000000000000000000000000000000..faec7c76cb7119d9b16f7c99724b868ba6577a1f --- /dev/null +++ b/app/views/calendar/date/index.php @@ -0,0 +1,135 @@ +<? if ($participation_message) : ?> + <?= $participation_message ?> + <article class="studip"> + <header><h1><?= _('Neuen Teilnahmestatus wählen') ?></h1></header> + <section> + <form class="default" method="post" data-dialog="reload-on-close" + action="<?= $controller->link_for('calendar/date/participation/' . $date->id) ?>"> + <?= CSRFProtection::tokenTag() ?> + <fieldset> + <? if ($user_participation_status) : ?> + <label> + <input type="radio" name="participation" value="" + data-activates="button[name='update_participation']"> + <?= _('Abwartend') ?> + </label> + <? endif ?> + <? if ($user_participation_status !== 'ACCEPTED') : ?> + <label> + <input type="radio" name="participation" value="ACCEPTED" + data-activates="button[name='update_participation']"> + <?= _('Angenommen') ?> + </label> + <? endif ?> + <? if ($user_participation_status !== 'DECLINED') : ?> + <label> + <input type="radio" name="participation" value="DECLINED" + data-activates="button[name='update_participation']"> + <?= _('Abgelehnt') ?> + </label> + <? endif ?> + <? if ($user_participation_status !== 'ACKNOWLEDGED') : ?> + <label> + <input type="radio" name="participation" value="ACKNOWLEDGED" + data-activates="button[name='update_participation']"> + <?= _('Angenommen (keine Teilnahme)') ?> + </label> + <? endif ?> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Teilnahmestatus ändern'), 'update_participation') ?> + </div> + </form> + </section> + </article> +<? endif ?> +<? if ($date->description) : ?> + <article class="studip"> + <header><h1><?= _('Beschreibung') ?></h1></header> + <section><?= htmlReady($date->description) ?></section> + </article> +<? endif ?> +<article class="studip"> + <header><h1><?= _('Informationen') ?></h1></header> + <section> + <dl> + <dt><?= _('Zeit') ?></dt> + <dd><?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?></dd> + <dt><?= _('Kategorie') ?></dt> + <dd><?= htmlReady($date->getCategoryAsString()) ?></dd> + <dt><?= _('Zugriff') ?></dt> + <dd><?= htmlReady($date->getAccessAsString()) ?></dd> + <? if ($date->repetition_type) : ?> + <dt><?= _('Wiederholung') ?></dt> + <dd><?= htmlReady($date->getRepetitionAsString()) ?></dd> + <? endif ?> + <? if ( + $date->author && $date->editor + && ( + ($date->author_id !== User::findCurrent()->id) + || ($date->editor_id !== User::findCurrent()->id) + ) + ) : ?> + <dt><?= _('Bearbeitung') ?></dt> + <dd> + <? if ($date->author->id === $date->editor->id) : ?> + <? if ($date->mkdate === $date->chdate) : ?> + <?= sprintf( + _('Erstellt von %s'), + htmlReady($date->author->getFullName()) + ) ?> + <? else : ?> + <?= sprintf( + _('Erstellt und zuletzt bearbeitet von %s'), + htmlReady($date->author->getFullName()) + ) ?> + <? endif ?> + <? else : ?> + <?= sprintf( + _('Erstellt von %1$s, zuletzt bearbeitet von %2$s'), + htmlReady($date->author->getFullName()), + htmlReady($date->editor->getFullName()) + ) ?> + <? endif ?> + </dd> + <? endif ?> + </dl> + </section> +</article> +<? if ($is_group_date) : ?> + <article class="studip"> + <header><h1><?= _('Teilnahmen') ?></h1></header> + <section> + <table class="default"> + <body> + <? foreach ($calendar_assignments as $assignment) : ?> + <tr> + <td><?= htmlReady($assignment->getRangeName()) ?></td> + <td><?= htmlReady($assignment->getParticipationAsString()) ?></td> + </tr> + <? endforeach ?> + </body> + </table> + </section> + </article> +<? endif ?> +<div data-dialog-button> + <? if ($date->isWritable(User::findCurrent()->id) && $all_assignments_writable) : ?> + <? + $button_params = []; + if ($selected_date) { + $button_params['selected_date'] = $selected_date; + } + ?> + <?= Studip\LinkButton::create( + _('Bearbeiten'), + $controller->url_for('calendar/date/edit/' . $date->id, array_merge($button_params, ['return_path' => '/calendar/calendar'])), + ['data-dialog' => 'size=auto;reload-on-close'] + ) ?> + <?= \Studip\LinkButton::create( + _('Löschen'), + $controller->url_for('calendar/date/delete/' . $date->id, $button_params), + ['data-dialog' => 'reload-on-close'] + ) ?> + <? endif ?> +</div> diff --git a/app/views/calendar/date/move.php b/app/views/calendar/date/move.php new file mode 100644 index 0000000000000000000000000000000000000000..3ea2b89a9ff26f2101059f919b570e2455c9e6d7 --- /dev/null +++ b/app/views/calendar/date/move.php @@ -0,0 +1,22 @@ +<?= MessageBox::info(_('Es handelt sich um einen Termin in einer Terminserie. Was möchten Sie tun?')) ?> +<form class="default" method="post" data-dialog="reload-on-close" + action="<?= $controller->link_for('calendar/date/move/' . $date->id) ?>"> + <?= CSRFProtection::tokenTag() ?> + <input type="hidden" name="begin" value="<?= htmlReady($begin->format(\DateTime::RFC3339)) ?>"> + <input type="hidden" name="end" value="<?= htmlReady($end->format(\DateTime::RFC3339)) ?>"> + <label> + <input type="radio" name="repetition_handling" value="create_single_date"> + <?= _('Der Termin soll aus der Terminserie herausgelöst werden.') ?> + </label> + <label> + <input type="radio" name="repetition_handling" value="change_times"> + <?= _('Start- und Enduhrzeit der gesamten Terminserie soll geändert werden.') ?> + </label> + <label> + <input type="radio" name="repetition_handling" value="change_all"> + <?= _('Die gesamte Terminserie soll verschoben werden und erst am gewählten Datum beginnen.') ?> + </label> + <div data-dialog-button> + <?= \Studip\Button::create(_('Verschieben'), 'move') ?> + </div> +</form> diff --git a/app/views/calendar/group/_attendees.php b/app/views/calendar/group/_attendees.php deleted file mode 100644 index 3fbe27eb3eee883d575e9347d5085777c580e484..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/_attendees.php +++ /dev/null @@ -1,82 +0,0 @@ -<fieldset class="collapsed"> - <legend> - <?= _('Teilnehmende/n hinzufügen') ?> - </legend> - - <a class="toggler"> - <? - if ($event->attendees->count()) { - $count_attendees = $event->attendees->filter( - function ($att, $k) use ($calendar) { - if ($att->range_id != $calendar->getRangeId()) { - return $att; - } - })->count(); - } else { - $count_attendees = 0; - } - ?> - <? if ($count_attendees) : ?> - <? if ($count_attendees < $event->attendees->count()) : ?> - <?= sprintf(ngettext('(%s weitere/r Teilnehmende/r)', '(%s weitere Teilnehmende)', $count_attendees), $count_attendees) ?> - <? else : ?> - <?= sprintf(_('(%s Teilnehmende)'), $count_attendees) ?> - <? endif; ?> - <? endif; ?> - </a> - - <div> - <label for="user_id_1"><h4><?= _('Teilnehmende') ?></h4></label> - <ul class="clean" id="adressees"> - <li id="template_adressee" style="display: none;" class="adressee"> - <input type="hidden" name="attendees[]" value=""> - <span class="visual"></span> - <a class="remove_adressee"><?= Icon::create('trash', 'clickable')->asImg(16, ['class' => 'text-bottom']) ?></a> - </li> - <? if ($event->isNew()) : ?> - <li style="padding: 0px;" class="adressee"> - <input type="hidden" name="attendees[]" value="<?= $event->owner->id ?>"> - <span class="visual"> - <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $event->owner->username], true) ?>"><?= htmlReady($event->owner->getFullname()) ?></a> - </span> - <a class="remove_adressee"><?= Icon::create('trash', 'clickable')->asImg(16, ['class' => 'text-bottom']) ?></a> - </li> - <? endif; ?> - <? $group_status = [ - CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'), - CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'), - CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'), - CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'), - CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?> - <? foreach ($event->attendees as $attendee) : ?> - <? if ($attendee->owner) : ?> - <li style="padding: 0px;" class="adressee"> - <input type="hidden" name="attendees[]" value="<?= htmlReady($attendee->owner->id) ?>"> - <span class="visual"> - <a href="<?= URLHelper::getLink('dispatch.php/profile', ['username' => $attendee->owner->username], true) ?>"><?= htmlReady($attendee->owner->getFullname()) ?></a> - <? if ($event->havePermission(Event::PERMISSION_OWN, $attendee->owner->id)) : ?> - (<?= _('Organisator') ?>) - <? elseif ($group_status[$attendee->group_status]) : ?> - (<?= $group_status[$attendee->group_status] ?>) - <? endif; ?> - </span> - <a class="remove_adressee"><?= Icon::create('trash', 'clickable', ['title' => _('Teilnehmende/n entfernen')])->asImg(16, ['class' => 'text-bottom']) ?></a> - </li> - <? endif; ?> - <? endforeach ?> - </ul> - - <section> - <?= $quick_search->render() ?> - <br clear="both"> - </section> - - <section> - <?= $mps->render(); ?> - </section> - - <script> - STUDIP.MultiPersonSearch.init(); - </script> - </div> -</fieldset> diff --git a/app/views/calendar/group/_tooltip.php b/app/views/calendar/group/_tooltip.php deleted file mode 100644 index aa8a9b134efe64994487123edada388754bf4d1d..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/_tooltip.php +++ /dev/null @@ -1,27 +0,0 @@ -<? $events = $events ?: $calendar->events ?> -<? if (sizeof($events)) : ?> -<div class="calendar-tooltip tooltip-content"> - <h3><?= htmlReady($calendar->range_object->getFullname('no_title')) ?></h3> - <? foreach ($events as $event) : ?> - <div> - <? if (date('Ymd', $event->getStart()) == date('Ymd', $event->getEnd())) : ?> - <? if ($event->isDayEvent()) : ?> - <?= strftime('%x ', $event->getStart()) . '(' . _('ganztägig') . ')' ?> - <? else : ?> - <?= strftime('%x %X', $event->getStart()) . strftime(' - %X', $event->getEnd()) ?> - <? endif; ?> - <? else : ?> - <? if ($event->isDayEvent()) : ?> - <?= strftime('%x', $event->getStart()) . strftime(' - %x', $event->getEnd()) . '(' . _('ganztägig') . ')' ?> - <? else : ?> - <?= strftime('%x %X', $event->getStart()) . strftime(' - %x %X', $event->getEnd()) ?> - <? endif; ?> - <? endif; ?> - </div> - <div> - <?= htmlReady($event->getTitle()) ?> - </div> - <hr> - <? endforeach; ?> -</div> -<? endif; ?> diff --git a/app/views/calendar/group/_tooltip_year.php b/app/views/calendar/group/_tooltip_year.php deleted file mode 100644 index edd81cad0a5efec791e8153b1cdd905dd5fa2ca8..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/_tooltip_year.php +++ /dev/null @@ -1,21 +0,0 @@ -<? $i = 0 ?> -<? $html = '' ?> -<? $list_day = date('Ymd', $aday) ?> -<? foreach ($calendars as $calendar) : ?> - <? if ($count_lists[$i][$list_day]) : ?> - <? - $html .= '<div>' - . sprintf(ngettext('%s hat 1 Termin', '%s hat %s Termine', - count($count_lists[$i][$list_day])), - $calendar->range_object->getFullname('no_title'), - count($count_lists[$i][$list_day])) - . '</div>'; - ?> - <? endif; ?> - <? $i++ ?> -<? endforeach; ?> -<? if ($html) : ?> -<div class="calendar-tooltip tooltip-content"> - <?= $html ?> -</div> -<? endif; ?> diff --git a/app/views/calendar/group/day.php b/app/views/calendar/group/day.php deleted file mode 100644 index a452320489ad2d264a05333058dc9cb56890af90..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/day.php +++ /dev/null @@ -1,128 +0,0 @@ -<? -$step_day = $settings['step_day_group']; -$step = 3600 / $step_day; -// add one cell for day events -$cells = (($settings['end'] - $settings['start']) / (1 / $step)) + 2; -$width1 = floor(90 / $cells); -$width2 = 10 + (90 - $width1 * $cells); -$start = $settings['start'] * 3600; -$end = ($settings['end'] + 1) * 3600; -?> -<table id="main_content" style="width:100%; table-layout:fixed;"> - <thead> - <tr> - <td style="text-align: center; width: 10%; height: 40px;"> - <div style="text-align: left; width: 20%; display: inline-block; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/day', ['atime' => $atime - 86400]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Tag zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <?= strftime(_('%x'), strtotime('-1 day', $calendars[0]->getStart())) ?> - </a> - </div> - <div class="calhead" style="width: 50%; display: inline-block;"> - <?= strftime('%A, %e. %B %Y', $atime) ?> - <div style="text-align: center; font-size: 12pt; color: #bbb; height: auto; overflow: visible; font-weight: bold;"><? $hd = holiday($atime); echo $holiday['name']; ?></div> - </div> - <div style="text-align: right; width: 20%; display: inline-block; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/day', ['atime' => $atime + 86400]) ?>"> - <?= strftime(_('%x'), strtotime('+1 day', $calendars[0]->getStart())) ?> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Tag vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </div> - </td> - </tr> - </thead> - <tbody> - <tr> - <td> - <div style="overflow:auto; width:100%;"> - <table style="width: 100%;"> - <? $time = mktime(0, 0, 0, date('n', $atime), date('j', $atime), date('Y', $atime)); ?> - <? if ($step_day < 3600) : $colsp = ' colspan="' . $step . '"'; endif ?> - <tr> - <td class="precol1w" nowrap="nowrap" style="text-align: center; width: <?= $width2 ?>%;"> - <?= _('Mitglied') ?> - </td> - <td class="precol1w" style="text-align: center; width: <?= $width1 ?>%"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendars[0]->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendars[0]->getStart(), 'isdayevent' => '1']) ?>"> - <?= Icon::create('schedule', 'clickable')->asImg() ?> - </a> - </td> - <? for ($i = $time + $start; $i < $time + $end; $i += $step_day) : ?> - <? if (!($i % 3600)) : ?> - <td<?= $colsp ?> class="precol1w" style="text-align: center;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead"> - <?= date('G', $i) ?> - </a> - </td> - <? endif ?> - <? endfor ?> - </tr> - <? foreach ($calendars as $calendar) : ?> - <? $adapted = $calendar->adapt_events($start, $end, $settings['step_day_group']); ?> - <tr> - <td style="width: <?= $width2 ?>%; white-space: nowrap;" class="month"> - <a class="calhead" href="<?= $controller->url_for('calendar/single/day/' . $calendar->getRangeId(), ['atime' => $atime,]); ?>"> - <?= htmlReady(($calendar->havePermission(Calendar::PERMISSION_OWN) ? _('Eigener Kalender') : get_fullname($calendar->getRangeId(), 'no_title_short'))) ?> - </a> - </td> - <? // display day events - $js_events = []; ?> - <? for ($i = 0; $i < sizeof($adapted['day_events']); $i++) : - $js_events[] = $calendar->events[$adapted['day_map'][$i]]; - endfor; ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%; text-align: right;" class="calendar-day-edit<?= sizeof($js_events) ? ' calendar-group-events' : ' lightmonth' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?> - <a title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendar->getStart(), 'isdayevent' => '1', 'user_id' => $calendar->getRangeId()]) ?>">+</a> - <? else : ?> - <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%;" class="<?= sizeof($js_events) ? 'calendar-group-events' : 'lightmonth' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?> - <? endif; ?> - </td> - <? $k = 0; - for ($i = $start + $calendar->getStart(); $i < $end + $calendar->getStart(); $i += $step_day) : - if (!($i % 3600)) { - $k++; - } - $js_events = []; - ?> - <? for ($j = 0; $j < sizeof($adapted['events']); $j++) : ?> - <? if (($adapted['events'][$j]->getStart() <= $i && $adapted['events'][$j]->getEnd() > $i) || ($adapted['events'][$j]->getStart() > $i && $adapted['events'][$j]->getStart() < $i + $step_day)) : - $js_events[] = $calendar->events[$adapted['map'][$j]]; - endif; ?> - <? endfor; ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?>style="width: <?= $width1 ?>%; text-align: right;" class="calendar-day-edit<?= sizeof($js_events) ? ' calendar-group-events' : ' lightmonth' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?> - <a title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i, 'user_id' => $calendar->getRangeId()]) ?>">+</a> - <? else : ?> - <td <?= sizeof($js_events) ? 'data-tooltip ' : '' ?> style="width: <?= $width1 ?>%; text-align: right;" class="<?= sizeof($js_events) ? 'calendar-group-events' : 'lightmonth' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $calendar, 'events' => $js_events]) ?> - <? endif; ?> - </td> - <? endfor ?> - </tr> - <? endforeach; ?> - <tr> - <td style="width: <?= $width2 ?>%;" class="precol1w"> </td> - <td width="<?= $width1 ?>%" class="precol1w" style="text-align: center;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendars[0]->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $calendars[0]->getStart(), 'isdayevent' => '1']) ?>"> - <?= Icon::create('schedule', 'clickable')->asImg() ?> - </a> - </td> - <? for ($i = $time + $start; $i < $time + $end; $i += $step_day) : ?> - <? if (!($i % 3600)) : ?> - <td<?= $colsp ?> class="precol1w" style="text-align: center;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead"> - <?= date('G', $i) ?> - </a> - </td> - <? endif ?> - <? endfor ?> - </tr> - </table> - </div> - </td> - </tr> - </tbody> -</table> diff --git a/app/views/calendar/group/month.php b/app/views/calendar/group/month.php deleted file mode 100644 index 67212dd7992683538bf65663a8e4f24c25de2cc8..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/month.php +++ /dev/null @@ -1,110 +0,0 @@ -<? $month = $calendar->view; ?> -<table class="calendar-month"> - <thead> - <tr> - <td colspan="8" style="text-align: center; vertical-align: middle;"> - <div style="text-align: left; display: inline-block; width: 33%; white-space: nowrap;"> - <a style="padding-right: 2em;" href="<?= $controller->url_for('calendar/group/month', ['atime' => mktime(12, 0, 0, date('n', $calendars[0][15]->getStart()), 15, date('Y', $calendars[0][15]->getStart()) - 1)]) ?>"> - <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <?= strftime('%B %Y', strtotime('-1 year', $calendars[0][15]->getStart())) ?> - </a> - <a href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0][0]->getStart() - 1]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <?= strftime('%B %Y', strtotime('-1 month', $calendars[0][15]->getStart())) ?> - </a> - </div> - <div class="calhead" style="text-align: center; display: inline-block; width: 33%;"> - <?= htmlReady(strftime("%B ", $calendars[0][15]->getStart())) .' '. date('Y', $calendars[0][15]->getStart()); ?> - </div> - <div style="text-align: right; display: inline-block; width: 33%; white-space: nowrap;"> - <a style="padding-right: 2em;" href="<?= $controller->url_for('calendar/group/month', ['atime' => strtotime('+1 month', $calendars[0][15]->getStart())]) ?>"> - <?= strftime('%B %Y', strtotime('+1 month', $calendars[0][15]->getStart())) ?> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - <a href="<?= $controller->url_for('calendar/group/month', ['atime' => mktime(12, 0, 0, date('n', $calendars[0][15]->getStart()), 15, date('Y', $calendars[0][15]->getEnd()) + 1)]) ?>"> - <?= strftime('%B %Y', strtotime('+1 year', $calendars[0][15]->getStart())) ?> - <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </div> - </td> - </tr> - <tr class="calendar-month-weekdays"> - <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?> - <? foreach ($week_days as $week_day) : ?> - <td class="precol1w"> - <?= strftime('%a', $week_day) ?> - </td> - <? endforeach; ?> - <td align="center" class="precol1w"> - <?= _('Woche') ?> - </td> - </tr> - </thead> - <tbody> - <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?> - <? $aday = date('j', $i); ?> - <? - $class_day = ''; - if (($aday - $j - 1 > 0) || ($j - $aday > 6)) { - $class_cell = 'lightmonth'; - $class_day = 'light'; - } elseif (date('Ymd', $i) == date('Ymd')) { - $class_cell = 'celltoday'; - } else { - $class_cell = 'month'; - } - $hday = holiday($i); - - if ($j % 7 == 0) { - ?><tr><? - } - ?> - <td class="<?= $class_cell ?>"> - <? if (($j + 1) % 7 == 0) : ?> - <a class="<?= $class_day . 'sday' ?>" href="<?= $controller->url_for('calendar/group/day/' . $this->range_id, ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <? if ($hday["name"] != "") : ?> - <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div> - <? endif; ?> - <? foreach($calendars as $user_calendars) : ?> - <? $count = sizeof($user_calendars[$j]->events) ?> - <? if ($count) : ?> - <div data-tooltip=""> - <a class="inday calendar-event-text" href="<?= $controller->url_for('calendar/single/day/' . $user_calendars[$j]->getRangeId(), ['atime' => $user_calendars[$j]->getStart()]) ?>"><?= htmlReady($user_calendars[$j]->range_object->getFullname('no_title')) ?></a> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $user_calendars[$j]]) ?> - </div> - <? endif; ?> - <? endforeach; ?> - </td> - <td class="lightmonth calendar-month-week"> - <a style="font-weight: bold;" class="calhead" href="<?= $controller->url_for('calendar/group/week/' . $this->range_id, ['atime' => $i]) ?>"><?= strftime("%V", $i) ?></a> - </td> - </tr> - <? else : ?> - <? $hday_class = ['day', 'day', 'shday', 'hday'] ?> - <? if ($hday['col']) : ?> - <a class="<?= $class_day . $hday_class[$hday['col']] ?>" href="<?= $controller->url_for('calendar/group/day', ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div> - <? else : ?> - <a class="<?= $class_day . 'day' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <? endif; ?> - <? foreach($calendars as $user_calendars) : ?> - <? $count = sizeof($user_calendars[$j]->events) ?> - <? if ($count) : ?> - <div data-tooltip> - <a class="inday calendar-event-text" href="<?= $controller->url_for('calendar/single/day/' . $user_calendars[$j]->getRangeId(), ['atime' => $user_calendars[$j]->getStart()]) ?>"><?= htmlReady($user_calendars[$j]->range_object->getFullname('no_title')) ?></a> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $user_calendars[$j]]) ?> - </div> - <? endif; ?> - <? endforeach; ?> - </td> - <? endif; ?> - <? endfor; ?> - </tr> - </tbody> -</table> diff --git a/app/views/calendar/group/week.php b/app/views/calendar/group/week.php deleted file mode 100644 index cdac0b5dadca1b4c284c84f3f1086930a04ebf2f..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/week.php +++ /dev/null @@ -1,144 +0,0 @@ -<? -$width1 = 0; -$width2 = 0; -$cols = ceil(($settings['end'] - $settings['start'] + 1) * 3600 / $settings['step_week_group']) + 1; -$start = $settings['start'] * 3600; -$end = ($settings['end'] + 1) * 3600; -$wlength = count($calendars[0]) - 1; -?> -<table style="width: 100%"> - <tr> - <td colspan="<?= $colspan_2 ?>" style="vertical-align: middle; text-align: center;"> - <div style="text-align: left; width: 25%; display: inline-block; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/week', ['atime' => mktime(12, 0, 0, date('n', $atime), date('j', $atime) - 7, date('Y', $atime))]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Eine Woche zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <?= sprintf(_('%u. Woche'), strftime('%V'), strtotime('-1 week', $atime)) ?> - </a> - </div> - <div style="display: inline-block; text-align: center;" class="calhead"> - <? printf(_("%s. Woche vom %s bis %s"), strftime("%V", $calendars[0][0]->getStart()), strftime("%x", $calendars[0][0]->getStart()), strftime("%x", $calendars[0][$wlength]->getEnd())) ?> - </div> - <div style="width: 25%; text-align: right; display: inline-block; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/week', ['atime' => mktime(12, 0, 0, date('n', $atime), date('j', $atime) + 7, date('Y', $atime))]) ?>"> - <?= sprintf(_('%u. Woche'), strftime('%V', strtotime('+1 week', $atime))) ?> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Eine Woche vor')])->asImg(16, ["style" => 'vertical-align: text-top;']) ?> - </a> - </div> - </td> - </tr> -</table> -<div style="overflow:auto; width:100%;"> - <table id="main_content" style="width: 100%;"> - <thead> - <tr> - <td width="<?= $width2 ?>%" class="precol1w"> </td> - <? $time = $calendars[0][0]->getStart(); ?> - <? for ($i = 0; $i <= $wlength; $i++) : ?> - <td colspan="<?= $cols ?>" style="text-align: center;" class="precol1w"> - <a href="<?= $controller->url_for('calendar/group/day/' . $this->range_id, ['atime' => $time]) ?>" class="calhead"> - <?= strftime('%a', $time) . ' ' . date('d', $time) ?> - </a> - </td> - <? $time += 86400; ?> - <? endfor ?> - </tr> - <tr> - <td width="<?= $width2 ?>%" class="precol1w" style="text-align: center;"> - <?= _('Mitglied') ?> - </td> - <? foreach ($calendars[0] as $day) : ?> - <td class="precol1w" style="text-align: center; width: <?= $width1 ?>%;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $day->getStart(), 'isdayevent' => '1']) ?>"> - <?= Icon::create('schedule', 'clickable')->asImg() ?> - </a> - </td> - <? for ($i = $day->getStart() + $start; $i < $day->getStart() + $end; $i += 3600 * ceil($settings['step_week_group'] / 3600)) : ?> - <td colspan="<?= ceil(3600 / $settings['step_week_group']) ?>" class="precol2w" style="text-align: center;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead"> - <?= (date('G', $i) < 10 ? ' ' . date('G', $i) . ' ' : date('G', $i)) ?> - </a> - </td> - <? endfor ?> - <? endforeach ?> - </tr> - </thead> - <tbody> - <? foreach ($calendars as $user_calendar) : ?> - <tr> - <td style="width: <?= $width2 ?>%; white-space: nowrap;" class="month"> - <a class="calhead" href="<?= $controller->url_for('calendar/single/week/' . $user_calendar[0]->getRangeId(), ['atime' => $atime]) ?>"> - <?= htmlReady($user_calendar[0]->havePermission(Calendar::PERMISSION_OWN) ? _('Eigener Kalender') : get_fullname($user_calendar[0]->getRangeId(), 'no_title_short')) ?> - </a> - </td> - <? $k = 1; ?> - <? foreach ($user_calendar as $day) : ?> - <? - if (date('Ymd', $day->getStart()) == date('Ymd')) { - $css_class = 'celltoday'; - } else { - if ($k % 2) { - $css_class = 'lightmonth'; - } else { - $css_class = 'month'; - } - } - $k++; - $adapted = $day->adapt_events($start, $end, $settings['step_week_group']); - - // display day events - $js_events = []; ?> - <? for ($i = 0; $i < count($adapted['day_events']); $i++) : ?> - <? $js_events[] = $day->events[$adapted['day_map'][$i]]; ?> - <? endfor; ?> - <? if (count($js_events) && $day->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="calendar-day-edit <?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?> - <a title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $day->getStart(), 'isdayevent' => '1', 'user_id' => $day->getRangeId()]) ?>">+</a> - <? else : ?> - <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="<?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?> - <? endif;?> - </td> - - <? for ($i = $start + $day->getStart(); $i < $end + $day->getStart(); $i += $settings['step_week_group']) : ?> - <? $js_events = []; ?> - <? for ($j = 0; $j < count($adapted['events']); $j++) : ?> - <? if (($adapted['events'][$j]->getStart() <= $i && $adapted['events'][$j]->getEnd() > $i) || ($adapted['events'][$j]->getStart() > $i && $adapted['events'][$j]->getStart() < $i + $settings['step_week_group'])) : ?> - <? $js_events[] = $day->events[$adapted['map'][$j]]; ?> - <? endif ?> - <? endfor ?> - <? if ($day->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="calendar-day-edit <?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?> - <a title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i, 'user_id' => $day->getRangeId()]) ?>">+</a> - <? else : ?> - <td <?= count($js_events) ? 'data-tooltip ' : '' ?>style="text-align: right; width: <?= $width1 ?>%" class="<?= $css_class ?><?= count($js_events) ? ' calendar-group-events' : '' ?>"> - <?= $this->render_partial('calendar/group/_tooltip', ['calendar' => $day, 'events' => $js_events]) ?> - <? endif; ?> - </td> - <? endfor; ?> - <? endforeach; ?> - </tr> - <? endforeach; ?> - </tbody> - <tfoot> - <tr> - <td style="width:<?= $width2 ?>%; text-align: center;" class="precol1"> </td> - <? foreach ($calendars[0] as $day) : ?> - <td style="width:<?= $width1 ?>%; text-align: center;" class="precol1w"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $day->getStart()) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $atime, 'isdayevent' => '1']) ?>"> - <?= Icon::create('schedule', 'clickable')->asImg() ?> - </a> - </td> - <? for ($i = $day->getStart() + $start; $i < $day->getStart() + $end; $i += 3600 * ceil($settings['step_week_group'] / 3600)) : ?> - <td colspan="<?= ceil(3600 / $settings['step_week_group']) ?>" class="precol2w" style="text-align: center;"> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Termin um %R Uhr'), $i) ?>" href="<?= $controller->url_for('calendar/group/edit/' . $range_id, ['atime' => $i]) ?>" class="calhead"> - <?= (date('G', $i) < 10 ? ' ' . date('G', $i) . ' ' : date('G', $i)) ?> - </a> - </td> - <? endfor; ?> - <? endforeach; ?> - </tr> - </tfoot> - </table> -</div> diff --git a/app/views/calendar/group/year.php b/app/views/calendar/group/year.php deleted file mode 100644 index b07f7493c7cc7456b41145827b218652db9ed72c..0000000000000000000000000000000000000000 --- a/app/views/calendar/group/year.php +++ /dev/null @@ -1,122 +0,0 @@ -<table style="width: 100%;"> - <tr> - <td colspan="3" style="text-align: center; vertical-align: middle;"> - <div style="text-align: left; display: inline-block; width: 20%; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/year', ['atime' => strtotime('-1 year', $atime)]) ?>"> - <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-bottom;']) ?> - <?= strftime('%Y', strtotime('-1 year', $atime)) ?> - </a> - </div> - <div class="calhead" style="text-align: center; display: inline-block; width:50%;"> - <?= date('Y', $calendars[0]->getStart()) ?> - </div> - <div style="text-align: right; display: inline-block; width: 20%; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/group/year', ['atime' => strtotime('+1 year', $atime)]) ?>"> - <?= strftime('%Y', strtotime('+1 year', $atime)) ?> - <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-bottom;']) ?> - </a> - </div> - </td> - </tr> - <tr> - <td colspan="3" class="blank"> - <table style="width: 100%;"> - <? $days_per_month = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - if (date('L', $calendars[0]->getStart())) { - $days_per_month[2]++; - } - ?> - <tr> - <? for ($i = 1; $i < 13; $i++) : ?> - <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?> - <td style="text-align: center; width: 8%;"> - <a class="calhead" href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0]->getStart() + $ts_month]) ?>"> - <?= strftime('%B', $ts_month); ?> - </a> - </td> - <? endfor; ?> - </tr> - <? $now = date('Ymd'); ?> - <? for ($i = 1; $i < 32; $i++) : ?> - <tr> - <? for ($month = 1; $month < 13; $month++) : ?> - <? $aday = mktime(12, 0, 0, $month, $i, date('Y', $calendars[0]->getStart())); ?> - <? if ($i <= $days_per_month[$month]) : ?> - <? $wday = date('w', $aday); - // emphasize current day - if (date('Ymd', $aday) == $now) { - $day_class = ' class="celltoday"'; - } else if ($wday == 0 || $wday == 6) { - $day_class = ' class="weekend"'; - } else { - $day_class = ' class="weekday"'; - } - ?> - <? if ($month == 1) : ?> - <td<?= $day_class ?> height="25"> - <? else : ?> - <td<?= $day_class ?>> - <? endif; ?> - <? $tooltip = $this->render_partial('calendar/group/_tooltip_year', - ['aday' => $aday, 'calendars' => $calendars, 'count_lists' => $count_lists]) ?> - <? if (trim($tooltip)) : ?> - <table style="width: 100%;"> - <tr> - <td<?= $day_class ?>> - <? endif; ?> - <? $weekday = strftime('%a', $aday); ?> - - <? $hday = holiday($aday); ?> - <? if ($hday['col'] == '1') : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? elseif ($hday['col'] == '2' || $hday['col'] == '3') : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="hday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? else : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/group/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? endif; ?> - <? if (trim($tooltip)) : ?> - </td> - <td<?= $day_class ?> style="text-align: right;" data-tooltip=""> - <?= Icon::create('date', 'clickable', ['title' => $event_count_txt])->asImg(16, ["alt" => $event_count_txt]); ?> - <?= $tooltip ?> - </td> - </tr> - </table> - <? endif; ?> - </td> - <? else : ?> - <td class="weekday"> </td> - <? endif; ?> - <? endfor; ?> - </tr> - <? endfor; ?> - <tr> - <? $ts_month = 0; ?> - <? for ($i = 1; $i < 13; $i++) : ?> - <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?> - <td align="center" width="8%"> - <a class="calhead" href="<?= $controller->url_for('calendar/group/month', ['atime' => $calendars[0]->getStart() + $ts_month]) ?>"> - <?= strftime('%B', $ts_month); ?> - </a> - </td> - <? endfor; ?> - </tr> - </table> - </td> - </tr> -</table> diff --git a/app/views/calendar/instschedule/_entry_details.php b/app/views/calendar/instschedule/_entry_details.php deleted file mode 100644 index 7fd4a38bd2bea1cd1f59def4476122bbd91e4dec..0000000000000000000000000000000000000000 --- a/app/views/calendar/instschedule/_entry_details.php +++ /dev/null @@ -1,29 +0,0 @@ -<table class="default"> - <caption> - <?= sprintf(_('Veranstaltungen mit regelmäßigen Zeiten am %s, %s Uhr'), htmlReady($day), htmlReady($start) .' - '. htmlReady($end)) ?> - </caption> - <colgroup> - <col width="15%"> - <col width="85%"> - </colgroup> - <thead> - <tr> - <th><?= _('Nummer') ?></th> - <th><?= _('Name') ?></th> - </tr> - </thead> - <tbody> - <? foreach ($seminars as $seminar) : ?> - <tr class="<?= TextHelper::cycle('table_row_odd', 'table_row_even')?>"> - <td><?= htmlReady($seminar->getNumber()) ?></td> - <td> - <a href="<?= URLHelper::getLink('dispatch.php/course/details/', ['sem_id' => $seminar->getId()]) ?>"> - <?= Icon::create('link-intern', 'clickable')->asImg() ?> - <?= htmlReady($seminar->getName()) ?> - </a> - </td> - </tr> - <? endforeach ?> - </tbody> -</table> -<br> diff --git a/app/views/calendar/instschedule/index.php b/app/views/calendar/instschedule/index.php deleted file mode 100644 index bcfa72ee721e708dea2d365abbe13aae1c0c5eb3..0000000000000000000000000000000000000000 --- a/app/views/calendar/instschedule/index.php +++ /dev/null @@ -1,15 +0,0 @@ -<h1> - <?= htmlReady(Context::getHeaderLine()) ?> <?= _("im") ?> - <?= htmlReady($current_semester['name']) ?> -</h1> - -<? if (Request::get('show_settings')) : ?> - <div class="ui-widget-overlay" style="width: 100%; height: 100%; z-index: 1001;"></div> - <?= $this->render_partial('calendar/schedule/_dialog', [ - 'content_for_layout' => $this->render_partial('calendar/schedule/settings', [ - 'settings' => $my_schedule_settings]), - 'title' => _('Darstellung ändern') - ]) ?> -<? endif ?> - -<?= $calendar_view->render() ?> diff --git a/app/views/calendar/stylesheet.php b/app/views/calendar/schedule/stylesheet.php similarity index 100% rename from app/views/calendar/stylesheet.php rename to app/views/calendar/schedule/stylesheet.php diff --git a/app/views/calendar/single/_attendees.php b/app/views/calendar/single/_attendees.php deleted file mode 100644 index c2ee568892e39934a5559d7a1d6bf8c9bcb6dbd7..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_attendees.php +++ /dev/null @@ -1,31 +0,0 @@ -<? use Studip\Button, Studip\LinkButton; ?> -<? $show_members = $event->attendees->findOneBy('range_id', - $calendar->getRangeId(), '!=') ?> -<? // Entkommentieren, wenn Mitglieder eines Termins sichtbar sein - // sollen, auch wenn man nicht selbst Mitglied ist und ... ?> -<? // $show_members_visiter = $event->attendees->findOneBy('range_id', $GLOBALS['user']->id) ?> -<? // folgende Zeile auskommentieren (siehe _attendees.php). ?> -<? $show_members_visiter = true; ?> -<? if ($show_members && $show_members_visiter) : ?> - <? $group_status = [ - CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'), - CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'), - CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'), - CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'), - CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?> - <div> - <b><?= _('Teilnehmende:') ?></b> - <?= implode(', ', $event->attendees->map( - function ($att) use ($event, $group_status) { - $profil_link = ObjectdisplayHelper::link($att->user); - if ($event->havePermission(Event::PERMISSION_OWN, $att->user->getId())) { - $profil_link .= ' (' . _('Organisator') . ')'; - } else { - if ($group_status[$att->group_status]) { - $profil_link .= ' (' . $group_status[$att->group_status] . ')'; - } - } - return $profil_link; - })); ?> - </div> -<? endif; ?> diff --git a/app/views/calendar/single/_calhead.php b/app/views/calendar/single/_calhead.php deleted file mode 100644 index 0d3e777a55fb7fca6ac1452ebe1f43c52c75518f..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_calhead.php +++ /dev/null @@ -1,16 +0,0 @@ -<div class="calhead" style="white-space: nowrap; position: relative;"> - <label> - <?= $calLabel ?> - <?= Icon::create('arr_1down', 'clickable') ?> - - <input type="text" - id="date-chooser" - value="<?= strftime('%F', $atime) ?>" - data-url="<?= $controller->url_for('calendar/single/' . $calType, ['atime' => '%ATIME%']) ?>" - style="width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; left: 50%; z-index: -1;"> - </label> - - <script> - jQuery('#date-chooser').datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { window.location = $(this).data('url').replace(encodeURI("%ATIME%"), Math.floor($(this).datepicker('getDate').valueOf() / 1000)) } }) - </script> -</div> diff --git a/app/views/calendar/single/_calhead_label_day.php b/app/views/calendar/single/_calhead_label_day.php deleted file mode 100644 index df4f8bcfab4b1b9d23996f136809ea9f41e8911d..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_calhead_label_day.php +++ /dev/null @@ -1,3 +0,0 @@ -<span class="hidden-tiny-down"><?= strftime('%A, ', $atime) ?></span> -<?= strftime('%d.%m.%Y', $atime) ?> -<span class="hidden-medium-down" style="font-size: 12pt; color: #bbb; font-weight: bold;"><? if ($hd = holiday($atime)) echo $hd['name']; ?></span> diff --git a/app/views/calendar/single/_calhead_label_week.php b/app/views/calendar/single/_calhead_label_week.php deleted file mode 100644 index fc0a5446ab4fe0cbc740d4edb4fec8a49bcf8093..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_calhead_label_week.php +++ /dev/null @@ -1,3 +0,0 @@ -<? printf(_("%s. Woche"), strftime("%V", $calendars[0]->getStart())) ?> -<span class="hidden-large-up"><?= date('Y', $calendars[0]->getStart()) ?></span> -<span class="hidden-medium-down"><? printf(_("vom %s bis %s"), strftime("%x", $calendars[0]->getStart()), strftime("%x", $calendars[$week_type - 1]->getStart())) ?></span> diff --git a/app/views/calendar/single/_day.php b/app/views/calendar/single/_day.php deleted file mode 100644 index 8b70fe9ffcf560af924fc2f385185d38b2413cbc..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_day.php +++ /dev/null @@ -1,90 +0,0 @@ -<? -$at = date('G', $atime); -if ($at >= $settings['start'] - && $at <= $settings['end'] || !$atime) { - $start = $settings['start'] * 3600; - $end = $settings['end'] * 3600; -} elseif ($at < $settings['start']) { - $start = 0; - $end = ($settings['start'] + 2) * 3600; -} else { - $start = ($settings['end'] - 2) * 3600; - $end = 23 * 3600; -} -$em = $calendar->createEventMatrix($start, $end, $settings['step_day']); -$max_columns = $em['max_cols'] ?: 1; -?> - -<nav class="calendar-nav"> - <span style="white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/single/day', ['atime' => strtotime('-1 day', $atime)]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Tag zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <span class="hidden-tiny-down"> - <?= strftime(_('%x'), strtotime('-1 day', $calendar->getStart())) ?> - </span> - </a> - </span> - - <? - $calType = 'day'; - $calLabel = $this->render_partial('calendar/single/_calhead_label_day'); - ?> - - <?= $this->render_partial('calendar/single/_calhead', compact('calendar', 'atime', 'calType', 'calLabel')) ?> - - <span style="white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/single/day', ['atime' => strtotime('+1 day', $atime)]) ?>"> - <span class="hidden-tiny-down"> - <?= strftime(_('%x'), strtotime('+1 day', $calendar->getStart())) ?> - </span> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Tag vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </span> -</nav> - -<table class="calendar-day"> - <colgroup> - <col style="max-width: 2em; width: 2em;"> - <? if ($settings['step_day'] < 3600) : ?> - <col style="max-width: 2em; width: 2em;"> - <? $max_columns_head = $max_columns + 3 ?> - <? else : ?> - <? $max_columns_head = $max_columns + 2 ?> - <? endif; ?> - <col span="<?= $em['max_cols'] ?: '1' ?>" style="width: <?= 100 / ($em['max_cols'] ?: 1) ?>%"> - <col style="max-width: 0.8em; width: 0.8em;"> - </colgroup> - <thead> - <? if ($start > 0) : ?> - <tr> - <td align="center"<?= $settings['step_day'] < 3600 ? ' colspan="2"' : '' ?>> - <a href="<?= $controller->url_for('calendar/single/day', ['atime' => ($atime - (date('G', $atime) * 3600 - $start + 3600))]) ?>"> - <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?> - </a> - </td> - <td colspan="<?= $max_columns + 1 ?>"> - </td> - </tr> - <? endif; ?> - </thead> - <tbody> - <?= $this->render_partial('calendar/single/_day_table', ['start' => $start, 'end' => $end, 'em' => $em]) ?> - </tbody> - <tfoot> - <? if ($end / 3600 < 23) : ?> - <tr> - <td align="center"<?= $settings['step_day'] < 3600 ? ' colspan="2"' : '' ?>> - <a href="<?= $controller->url_for('calendar/single/day', ['atime' => ($atime + $end - date('G', $atime) * 3600 + 3600)]) ?>"> - <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?> - </a> - </td> - <td colspan="<?= $max_columns + 1 ?>"> - </td> - </tr> - <? else : ?> - <tr> - <td> </td> - </tr> - <? endif ?> - </tfoot> -</table> diff --git a/app/views/calendar/single/_day_cell.php b/app/views/calendar/single/_day_cell.php deleted file mode 100644 index 31845efc63c1ed45e388772932c6a7057b793580..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_day_cell.php +++ /dev/null @@ -1,54 +0,0 @@ -<? $link_notset = true ?> -<? $atime_new = $calendar->getStart() + $i * $step ?> -<? if (empty($em['term'][$row])) : ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td class="calendar-day-edit <?= $class_cell ?>" <?= ($em['max_cols'] > 0 ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>> - <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a> - </td> - <? else : ?> - <td class="calendar-day-edit <?= $class_cell ?>" <?= ($em['max_cols'] > 0 ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>> - </td> - <? endif; ?> -<? $link_notset = false ?> -<? else : ?> - <? for ($j = 0; $j < $em['colsp'][$row]; $j++) : ?> - <? $event = $em['term'][$row][$j]; ?> - <? $mapped_event = $calendar->events[$em['mapping'][$row][$j]]; ?> - <? if (is_object($event)) : ?> - <td data-tooltip<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?><?= ($em['rows'][$row][$j] > 1 ? ' rowspan="' . $em['rows'][$row][$j] . '"' : '') ?> class="<?= $event instanceof CourseEvent ? 'calendar-course-category' : 'calendar-category' ?><?= $event->getCategory() ?> calendar-day-event"> - <? if ($em['rows'][$row][$j] > 1) : ?> - <div> - <?= date('H.i-', $mapped_event->getStart()) . date('H.i', $mapped_event->getEnd()) ?> - </div> - <? endif ?> - <div class="calendar-day-event-title"> - <a title="<?= _('Termin bearbeiten') ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId() . '/' . $event->event_id, ['atime' => $atime_new, 'evtype' => $event->getType()]) ?>"><?= htmlReady($event->getTitle()) ?></a> - <?= $this->render_partial('calendar/single/_tooltip', ['event' => $mapped_event]) ?> - </div> - </td> - <? elseif ($event == '#') : ?> - <td class="<?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>> - <span class="inday"> </span> - </td> - <? elseif ($event == '') : ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td class="calendar-day-edit <?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>> - <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a> - </td> - <? else : ?> - <td class="calendar-day-edit <?= $class_cell ?>"<?= ($em['cspan'][$row][$j] > 1 ? ' colspan="' . $em['cspan'][$row][$j] . '"' : '') ?>></td> - <? endif ?> - <? $link_notset = false; ?> - <? break; ?> - <? endif ?> - <? endfor ?> -<? endif ?> -<? if ($link_notset) : ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <td class="calendar-day-edit <?= $class_cell ?>"> - <a title="<?= strftime(_('Neuer Termin am %x, %R Uhr'), $atime_new) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $atime_new]) ?>">+</a> - </td> - <? else : ?> - <td class="calendar-day-edit <?= $class_cell ?>"></td> - <? endif; ?> -<? endif ?> diff --git a/app/views/calendar/single/_day_dayevents.php b/app/views/calendar/single/_day_dayevents.php deleted file mode 100644 index 397f0bfb7897c273fb7030a666fee519fecb5f38..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_day_dayevents.php +++ /dev/null @@ -1,33 +0,0 @@ -<? if (isset($em['day_events']) && count($em['day_events']) > 0) : ?> - <td class="<?= $class_cell ?>" style="padding: 0px;" <?= (($em['max_cols'] > 0) ? ' colspan="' . ($em['max_cols']) . '"' : '') ?>> - <table style="width: 100%; border-spacing: 0;"> - <? $i = 0; ?> - <? foreach ($em['day_events'] as $day_event) : ?> - <tr> - <? if ($day_event->getPermission() == Event::PERMISSION_CONFIDENTIAL) : ?> - <td class="calendar-category<?= $day_event->getCategory() ?>"> - <?= htmlReady($day_event->getTitle()) ?> - </td> - <? else : ?> - <td data-tooltip onclick="STUDIP.Dialog.fromElement(jQuery(this).children('a').first(), {size: 'auto'}); return false;" class="calendar-category<?= $day_event->getCategory() ?>"> - <?= $this->render_partial('calendar/single/_tooltip', ['event' => $calendar->events[$em['day_map'][$i]]]) ?> - <a style="color:#fff;" data-dialog="size=auto" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId() . '/' . $day_event->event_id, ['isdayevent' => '1']) ?>"><?= htmlReady($day_event->getTitle()) ?></a> - </td> - <? endif; ?> - </tr> - <? $i++; ?> - <? endforeach ?> - </table> - </td> - <td class="calendar-day-edit <?= $class_cell ?>" onclick="STUDIP.Dialog.fromElement(jQuery(this).children('a').first(), {size: 'auto'}); return false;"> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $calendar->getStart(), 'isdayevent' => '1']) ?>">+</a> - <? endif; ?> - </td> -<? else : ?> - <td class="calendar-day-edit <?= $class_cell ?>" <?= (($em['max_cols'] > 0) ? ' colspan="' . ($em['max_cols'] + 1) . '"' : '') ?>> - <? if ($calendar->havePermission(Calendar::PERMISSION_WRITABLE)) : ?> - <a data-dialog="size=auto" title="<?= strftime(_('Neuer Tagestermin am %x'), $calendar->getStart()) ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendar->getRangeId(), ['atime' => $calendar->getStart(), 'isdayevent' => '1']) ?>">+</a> - <? endif; ?> - </td> -<? endif; ?> diff --git a/app/views/calendar/single/_day_table.php b/app/views/calendar/single/_day_table.php deleted file mode 100644 index 1b0ebcd4a559b2ee5479804bb2114fec63beaa3e..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_day_table.php +++ /dev/null @@ -1,28 +0,0 @@ -<? -if ($settings['step_day'] >= 3600) { - $rowspan_precol = ''; -} else { - $rowspan_precol = ' rowspan="' . 3600 / $settings['step_day'] . '"'; -} -?> -<tr> - <td class="precol1w" <?= $rowspan_precol ? ' colspan="2"' : '' ?>><?= _('Tag') ?></td> - <?= $this->render_partial('calendar/single/_day_dayevents', ['em' => $em]); ?> -</tr> -<? for ($i = $start / $settings['step_day']; $i < $end / $settings['step_day'] + 3600 / $settings['step_day']; $i++) : ?> -<? $row = $i - $start / $settings['step_day']; ?> -<tr> - <? if (($i * $settings['step_day']) % 3600 == 0) : ?> - <td class="precol1w" <?= $rowspan_precol ?>> - <?= $i / (3600 / $settings['step_day']) ?> - </td> - <? endif ?> - <? if ($settings['step_day'] % 3600 != 0) : ?> - <? $minute = ($row % (3600 / $settings['step_day'])) * ($settings['step_day'] / 60); ?> - <td class="precol2w" style="height: 20px; width: 1%; padding-right: 3px;"> - <?= $minute ? $minute : '00' ?> - </td> - <? endif ?> - <?= $this->render_partial('calendar/single/_day_cell', ['events' => $calendar->events, 'start' => $start, 'em' => $em, 'row' => $row, 'i' => $i, 'step' => $settings['step_day']]); ?> -</tr> -<? endfor; ?> \ No newline at end of file diff --git a/app/views/calendar/single/_edit_status.php b/app/views/calendar/single/_edit_status.php deleted file mode 100644 index b5f41aa19d2ab0afefac3d824ef4308603572399..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_edit_status.php +++ /dev/null @@ -1,48 +0,0 @@ -<? use Studip\Button, Studip\LinkButton; ?> -<form action="" method="post"> - <div> - <b><?= _('Eigener Teilnahmestatus') ?>:</b> - <? $group_status = [ - CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'), - CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'), - CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'), - CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)')] ?> - <ul> - <? foreach ($group_status as $value => $name) : ?> - <ul class="list-unstyled"> - <label> - <input type="radio" value="<?= $value ?>" name="status" <?= $value == $event->group_status ? ' checked' : '' ?>> - <?= $name ?> - </label> - </li> - <? endforeach; ?> - </ul> - </div> - <div> - <? $author = $event->getAuthor() ?> - <? if ($author) : ?> - <?= sprintf(_('Eingetragen am: %s von %s'), - strftime('%x, %X', $event->mkdate), - htmlReady($author->getFullName('no_title'))) ?> - <? endif; ?> - </div> - <? if ($event->event->mkdate < $event->event->chdate) : ?> - <? $editor = $event->getEditor() ?> - <? if ($editor) : ?> - <div> - <?= sprintf(_('Zuletzt bearbeitet am: %s von %s'), - strftime('%x, %X', $event->chdate), - htmlReady($editor->getFullName('no_title'))) ?> - </div> - <? endif; ?> - <? endif; ?> - <div style="text-align: center;" data-dialog-button> - <?= Button::create(_('Speichern'), 'store', ['title' => _('Termin speichern')]) ?> - <? if ($event->havePermission(Event::PERMISSION_DELETABLE)) : ?> - <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?> - <? endif; ?> - <? if (!Request::isXhr()) : ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?> - <? endif; ?> - </div> -</form> diff --git a/app/views/calendar/single/_event_data.php b/app/views/calendar/single/_event_data.php deleted file mode 100644 index da9a5feb3c408f81bb3fd57c9e095932cf353558..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_event_data.php +++ /dev/null @@ -1,100 +0,0 @@ -<? use Studip\LinkButton; ?> -<div> - <h4><?= htmlReady($event->getTitle()) ?></h4> - <div> - <b><?= _('Beginn') ?>:</b> <?= strftime('%c', $event->getStart()) ?> - </div> - <div> - <b><?= _('Ende') ?>:</b> <?= strftime('%c', $event->getEnd()) ?> - </div> - <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?> - <? if ($event instanceof CourseEvent) : ?> - <div> - <b><?= _('Veranstaltung') ?>:</b> - <? if ($GLOBALS['perm']->have_studip_perm('user', $event->range_id)) : ?> - <a href="<?= URLHelper::getLink('dispatch.php/course/details/?cid=' . $event->range_id) ?>"> - <? else : ?> - <a href="<?= URLHelper::getLink('dispatch.php/course/details/index/' . $event->range_id) ?>"> - <? endif; ?> - <?= htmlReady($event->course->getFullname()) ?> - </a> - </div> - <? endif;?> - <? if ($text = $event->getDescription()) : ?> - <div> - <b><?= _('Beschreibung') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringCategories()) : ?> - <div> - <b><?= _('Kategorie') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->getLocation()) : ?> - <div> - <b><?= _('Raum/Ort') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringPriority()) : ?> - <div> - <b><?= _('Priorität') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringAccessibility()) : ?> - <div> - <b><?= _('Zugriff') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringRecurrence()) : ?> - <div> - <b><?= _('Wiederholung') ?>:</b> <?= htmlReady($text) ?> - </div> - <? endif; ?> - <? if ($event instanceof CalendarEvent) : ?> - <? if (Config::get()->CALENDAR_GROUP_ENABLE) : ?> - <?= $this->render_partial('calendar/single/_attendees.php') ?> - <? if ($calendar->havePermission(Calendar::PERMISSION_OWN) - && $event->toStringGroupStatus()) : ?> - <?= $this->render_partial('calendar/single/_edit_status') ?> - <? else : ?> - <div style="text-align: center;" data-dialog-button> - <? if ($event->havePermission(Event::PERMISSION_DELETABLE)) : ?> - <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?> - <? endif; ?> - <? if (!Request::isXhr()) : ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?> - <? endif; ?> - </div> - <? endif; ?> - <? endif; ?> - <? else : ?> - <? // durchführende Lehrende ?> - <? $related_persons = $event->dozenten; ?> - <? if (sizeof($related_persons)) : ?> - <div> - <b><?= ngettext('Durchführende Lehrperson', 'Durchführende Lehrende', sizeof($related_persons)) ?>:</b> - <ul class="list-unstyled"> - <? foreach ($related_persons as $related_person) : ?> - <li> - <?= ObjectdisplayHelper::link($related_person) ?> - </li> - <? endforeach; ?> - </ul> - </div> - <? endif; ?> - <? // related groups ?> - <? $related_groups = $event->getRelatedGroups(); ?> - <? if (sizeof($related_groups)) : ?> - <div> - <b><?= _('Betroffene Gruppen') ?>:</b> - <?= htmlReady(implode(', ', $related_groups->pluck('name'))) ?> - </div> - <? endif; ?> - <? if (!Request::isXhr()) : ?> - <div style="text-align: center;" data-dialog-button> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?> - </div> - <? endif; ?> - <? endif; ?> - <? endif; ?> -</div> diff --git a/app/views/calendar/single/_include_month.php b/app/views/calendar/single/_include_month.php deleted file mode 100644 index 6942d2f02be67d5633ba655007b593af9ba21922..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_include_month.php +++ /dev/null @@ -1,135 +0,0 @@ -<? $now = mktime(12, 0, 0, date('n', time()), date('j', time()), date('Y', time())); ?> -<table class="blank"> - <tr> - <td style="text-align: center;"> - <table style="width: 100%;"> - <tr> - <td colspan="8" style="vertical-align: top; text-align: center; white-space:nowrap;"> - <div style="float:left; width:15%;"> - <? if ($mod == 'NONAVARROWS') : ?> - - <? else : ?> - <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt) - 1)]) ?>"> - <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg() ?> - </a> - <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt) - 1, 1, date('Y', $imt))]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg() ?> - </a> - <? endif; ?> - </div> - <div class="precol1w" style="float:left; text-align:center; width:70%;"> - <?= sprintf("%s %s\n", strftime('%B', $imt), date('Y', $imt)) ?> - </div> - <div style="float:right; width:15%;"> - <? if ($mod == 'NONAVARROWS') : ?> - - <? else : ?> - <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt) + 1, 1, date('Y', $imt))]) ?>"> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg() ?> - </a> - <a href="<?= $controller->url_for($href, ['imt' => mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt) + 1)]) ?>"> - <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg() ?> - </a> - <? endif; ?> - </div> - </td> - </tr> - </table> - </td> - </tr> - <tr> - <td class="blank"> - <table class="blank"> - <tr> - <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?> - <? foreach ($week_days as $week_day) : ?> - <td align="center" class="precol2w" width="25"> - <?= strftime('%a', $week_day) ?> - </td> - <? endforeach; ?> - <td class="precol2w" width="25"> </td> - </tr> - <? $adow = date('w', mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt))); ?> - <? if ($adow == 0) : ?> - <? $adow = 6; ?> - <? else : ?> - <? $adow--; ?> - <? endif; ?> - <? $first_day = mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt)) - $adow * 86400; ?> - <? $cor = 0; ?> - <? if (date('n', $imt) == 3) : ?> - <? $cor = 1; ?> - <? endif; ?> - <? $last_day = ((42 - ($adow + date('t', mktime(12, 0, 0, date('n', $imt), 1, date('Y', $imt))))) % 7 + $cor) * 86400 - + mktime(12, 0, 0, date('n', $imt), date('t', $imt), date('Y', $imt)); ?> - <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?> - <? - $aday = date('j', $i); - $style = ''; - if (($aday - $j - 1 > 0) || ($j - $aday > 6)) { - $style = 'light'; - } - $hday = holiday($i); - ?> - <? if (abs($now - $i) < 43199 && !($style == 'light')) : ?> - <td class="celltoday" align="center" width="25" height="25"> - <? elseif (date('m', $i) != date('n', $imt)) : ?> - <td class="lightmonth" align="center" width="25" height="25"> - <? else : ?> - <td class="month" align="center" width="25" height="25"> - <? endif; ?> - <? $js_inc = ''; ?> - <? if (!empty($js_include) && is_array($js_include)) : ?> - <? - $js_inc = " onClick=\"{$js_include['function']}("; - if (sizeof($js_include['parameters'])) { - $js_inc .= implode(", ", $js_include['parameters']) . ", "; - } - $js_inc .= "'" . date('m', $i) . "', '$aday', '" . date('Y', $i) . "')\""; - ?> - <? endif; ?> - <? if (abs($atime - $i) < 43199) : ?> - <? $aday = '<span class="current">'.$aday.'</span>' ?> - <? endif; ?> - <? if (($j + 1) % 7 == 0) : ?> - <a class="<?= $style ?>sday" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= is_array($hday) ? tooltip($hday['name'] ?: '') : '' ?> <?= $js_inc ?>> - <?= $aday ?> - </a> - </td> - <td class="lightmonth" style="text-align: center; width: 25px; height: 25px;"> - <a href="<?= $controller->url_for('calendar/single/week/', ['atime' => $i]) ?>"> - <span class="kwmin"><?= strftime('%V', $i) ?></span> - </a> - </td> - </tr> - <? else : ?> - <? if (is_array($hday)) : ?> - <? switch ($hday['col']) { - case 1: - ?><a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= tooltip($hday['name']) . $js_inc ?>> - <?= $aday ?> - </a><? - break; - case 2: - case 3; - ?><a class="<?= $style ?>hday" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= tooltip($hday['name']) . $js_inc ?>> - <?= $aday ?> - </a><? - break; - default: - ?><a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= $js_inc ?>> - <?= $aday ?> - </a> - <?}?> - <? else : ?> - <a class="<?= $style ?>day" href="<?= $controller->url_for($href, ['atime' => $i]) ?>" <?= $js_inc ?>> - <?= $aday ?> - </a> - <? endif ?> - </td> - <? endif; ?> - <? endfor; ?> - </table> - </td> - </tr> -</table> diff --git a/app/views/calendar/single/_jump_to.php b/app/views/calendar/single/_jump_to.php deleted file mode 100644 index b5e96bb21dea10779aba54903930b5a6dcc07dc7..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_jump_to.php +++ /dev/null @@ -1,13 +0,0 @@ -<form class="default" action="<?= $action_url ?>" method="post" name="jump_to"> - <input type="hidden" name="action" value="<?= $action ?>"> - - <section class="hgroup"> - <?= _('Gehe zu:') ?> - <input size="10" style="width: 16em;" type="text" id="jmp_date" name="jmp_date" type="text" value="<?= strftime('%x', $atime)?>"> - <?= Icon::create('accept', 'clickable')->asInput(['class' => 'text-top']) ?> - </section> -</form> - -<script> - jQuery('#jmp_date').datepicker(); -</script> diff --git a/app/views/calendar/single/_select_calendar.php b/app/views/calendar/single/_select_calendar.php deleted file mode 100644 index 7b2b94279f57c826e5207efea3908407a4e4f95f..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_select_calendar.php +++ /dev/null @@ -1,80 +0,0 @@ -<form name="select_calendars" class="default" method="post" action="<?= htmlReady($action_url) ?>"> - - <section class="hgroup"> - <?= _('Kalender') ?> - <select class="sidebar-selectlist submit-upon-select" style="width: 16em;" name="range_id"> - <option value="<?= get_userid() ?>"<?= get_userid() === $range_id ? ' selected' : '' ?>> - <?= _("Eigener Kalender") ?> - </option> - <? $groups = Calendar::getGroups($GLOBALS['user']->id); ?> - <? if (count($groups)) : ?> - <optgroup style="font-weight:bold;" label="<?= _('Gruppenkalender:') ?>"> - <? foreach ($groups as $group) : ?> - <option value="<?= $group->getId() ?>"<?= ($range_id == $group->getId() ? ' selected' : '') ?>> - <?= htmlReady($group->name) ?> - </option> - <? endforeach ?> - </optgroup> - <? endif; ?> - <? $calendar_users = CalendarUser::getOwners($GLOBALS['user']->id)->getArrayCopy(); ?> - <? usort($calendar_users, function ($a, $b) { - return strnatcmp($a->owner->Nachname, $b->owner->Nachname); - }); ?> - <? if (count($calendar_users)) : ?> - <optgroup style="font-weight:bold;" label="<?= _('Einzelkalender:') ?>"> - <? foreach ($calendar_users as $calendar_user) : ?> - <? if (!$calendar_user->owner) { - continue; - } ?> - <option value="<?= $calendar_user->owner_id ?>"<?= ($range_id == $calendar_user->owner_id ? ' selected' : '') ?>> - <?= htmlReady($calendar_user->owner->getFullname('full_rev')) ?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <?/* - if ($GLOBALS['perm']->have_perm('dozent')) { - $lecturers = Calendar::GetLecturers(); - } else { - $lecturers = array(); - } - */ - $lecturers = []; - ?> - <? if (count($lecturers)) : ?> - <optgroup style="font-weight:bold;" label="<?= _('Lehrendenkalender:') ?>"> - <? foreach ($lecturers as $lecturer) : ?> - <option value="<?= $lecturer['id'] ?>"<?= ($range_id == $lecturer['id'] ? ' selected' : '') ?>> - <?= htmlReady(my_substr($lecturer['name'] . " ({$lecturer['username']})", 0, 30)) ?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <? if (Config::get()->COURSE_CALENDAR_ENABLE) : ?> - <? $courses = Calendar::GetCoursesActivatedCalendar($GLOBALS['user']->id); ?> - <? if (count($courses)) : ?> - <optgroup style="font-weight:bold;" label="<?= _('Veranstaltungskalender:') ?>"> - <? foreach ($courses as $course) : ?> - <option value="<?= $course->id ?>"<?= ($range_id == $course->id ? ' selected' : '') ?>> - <?= htmlReady($course->getFullname()) ?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <? $insts = Calendar::GetInstituteActivatedCalendar($GLOBALS['user']->id); ?> - <? if (count($insts)) : ?> - <optgroup style="font-weight:bold;" label="<?= _('Einrichtungskalender:') ?>"> - <? foreach ($insts as $inst_id => $inst_name) : ?> - <option value="<?= $inst_id ?>"<?= ($range_id == $inst_id ? ' selected' : '') ?>> - <?= htmlReady(my_substr($inst_name, 0, 30)); ?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - <? endif ?> - </select> - - <input type="hidden" name="view" value="<?= $view ?>"> - <?= Icon::create('accept', 'clickable')->asInput(['class' => "text-top"]) ?> - </section> -</form> diff --git a/app/views/calendar/single/_select_category.php b/app/views/calendar/single/_select_category.php deleted file mode 100644 index fe86bdc04d74203c12f6f73808cba357a19f724c..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_select_category.php +++ /dev/null @@ -1,16 +0,0 @@ -<form class="default" name="filter_categories" method="post" action="<?= $action_url ?>"> - - <section class="hgroup"> - <?= _('Kategorie') ?> - <select class="sidebar-selectlist nested-select submit-upon-select" style="width: 16em;" name="category"> - <option value=""><?= _('Alle Kategorien') ?></option> - <? foreach (Config::get()->getValue('PERS_TERMIN_KAT') as $key => $cat) : ?> - <option value="<?= $key ?>"<?= ($category == $key ? ' selected="selected"' : '') ?> class="calendar-category<?= $key ?>" data-color-class="calendar-category<?= $key ?>"> - <?= htmlReady($cat['name']) ?> - </option> - <? endforeach; ?> - </select> - - <?= Icon::create('accept', 'clickable')->asInput(['class' => "text-top"]) ?> - </section> -</form> diff --git a/app/views/calendar/single/_semester_filter.php b/app/views/calendar/single/_semester_filter.php deleted file mode 100644 index 016f1e537b0958586eec93fddde1885f1170ec86..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_semester_filter.php +++ /dev/null @@ -1,30 +0,0 @@ -<form data-dialog action="<?= $controller->url_for('calendar/single/seminar_events/')?>" class="default"> - <section class="hgroup"> - <label> - <?= _('Semesterfilter') ?>: - <select name="sem_select" class="submit-upon-select"> - <option <?= ($sem == 'current' ? 'selected' : '')?> value="current"><?= _('Aktuelles Semester') ?></option> - <option <?= ($sem == 'future' ? 'selected' : '')?> value="future"><?= _('Aktuelles und nächstes Semester') ?></option> - <option <?= ($sem == 'last' ? 'selected' : '')?> value="last"><?= _('Aktuelles und letztes Semester') ?></option> - <option <?= ($sem == 'lastandnext' ? 'selected' : '')?> value="lastandnext"><?= _('Letztes, aktuelles, nächstes Semester') ?></option> - <? if (Config::get()->MY_COURSES_ENABLE_ALL_SEMESTERS) : ?> - <option <?= ($sem == 'all' ? 'selected' : '')?> value="all"><?= _('Alle Semester') ?></option> - <? endif ?> - - <? if (!empty($semesters)) : ?> - <optgroup label="<?=_('Semester auswählen')?>"> - <? foreach ($semesters as $semester) :?> - <option value="<?=$semester->id?>" <?= ($sem == $semester->id ? 'selected' : '')?>> - <?= htmlReady($semester->name)?> - </option> - <? endforeach ?> - </optgroup> - <? endif ?> - </select> - </label> - </section> - - <noscript> - <?= \Studip\Button::createAccept(_('Auswählen'))?> - </noscript> -</form> diff --git a/app/views/calendar/single/_tooltip.php b/app/views/calendar/single/_tooltip.php deleted file mode 100644 index 0979ec2c981f311a8567186a8fe97dada8a35896..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/_tooltip.php +++ /dev/null @@ -1,113 +0,0 @@ -<div class="calendar-tooltip tooltip-content"> - <h4><?= htmlReady($event->getTitle()) ?></h4> - <div> - <b><?= _('Beginn') ?>:</b> <?= strftime('%c', $event->getStart()) ?> - </div> - <div> - <b><?= _('Ende') ?>:</b> <?= strftime('%c', $event->getEnd()) ?> - </div> - <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?> - <? if ($event instanceof CourseEvent) : ?> - <div> - <b><?= _('Veranstaltung') ?>:</b> <?= htmlReady($event->course->getFullname()) ?> - </div> - <? endif;?> - <? if ($text = $event->getDescription()) : ?> - <div> - <b><?= _('Beschreibung') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringCategories()) : ?> - <div> - <b><?= _('Kategorie') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->getLocation()) : ?> - <div> - <b><?= _('Raum/Ort') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringPriority()) : ?> - <div> - <b><?= _('Priorität') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringAccessibility()) : ?> - <div> - <b><?= _('Zugriff') ?>:</b> <?= htmlReady(mila($text, 50)) ?> - </div> - <? endif; ?> - <? if ($text = $event->toStringRecurrence()) : ?> - <div> - <b><?= _('Wiederholung') ?>:</b> <?= htmlReady($text) ?> - </div> - <? endif; ?> - <? endif; ?> - <? if ($event->havePermission(Event::PERMISSION_READABLE)) : ?> - <? if ($event instanceof CalendarEvent - && Config::get()->CALENDAR_GROUP_ENABLE - && $calendar->getRange() == Calendar::RANGE_USER) : ?> - <? $group_status = [ - CalendarEvent::PARTSTAT_TENTATIVE => _('Abwartend'), - CalendarEvent::PARTSTAT_ACCEPTED => _('Angenommen'), - CalendarEvent::PARTSTAT_DECLINED => _('Abgelehnt'), - CalendarEvent::PARTSTAT_DELEGATED => _('Angenommen (keine Teilnahme)'), - CalendarEvent::PARTSTAT_NEEDS_ACTION => ''] ?> - <? $show_members = $event->attendees->findOneBy('range_id', - $calendar->getRangeId(), '!=') ?> - <? // Entkommentieren, wenn Mitglieder eines Termins sichtbar sein - // sollen, auch wenn man nicht selbst Mitglied ist und ... ?> - <? // $show_members_visiter = $event->attendees->findOneBy('range_id', $GLOBALS['user']->id) ?> - <? // folgende Zeile auskommentieren (siehe _attendees.php). ?> - <? $show_members_visiter = true; ?> - <? if ($show_members && $show_members_visiter) : ?> - <div> - <b><?= _('Teilnehmende:') ?></b> - <?= implode(', ', $event->attendees->map( - function ($att) use ($event, $group_status) { - if ($event->havePermission(Event::PERMISSION_OWN, $att->owner->id)) { - $ret = htmlReady($att->owner->getFullname()) - . ' (' . _('Organisator') . ')'; - } else { - $ret = htmlReady($att->owner->getFullname()); - if ($group_status[$att->group_status]) { - $ret .= ' (' . $group_status[$att->group_status] . ')'; - } - } - return $ret; - })); ?> - </div> - <? endif; ?> - <? endif; ?> - <? if ($event instanceof CourseEvent) : ?> - <? // durchführende Lehrende ?> - <? $related_persons = $event->dozenten; ?> - <? if (sizeof($related_persons)) : ?> - <div> - <b><?= ngettext('Durchführende Lehrperson', 'Durchführende Lehrende', sizeof($related_persons)) ?>:</b> - <ul class="list-unstyled"> - <? foreach ($related_persons as $related_person) : ?> - <li> - <?= htmlReady($related_person->getFullName()) ?> - </li> - <? endforeach; ?> - </ul> - </div> - <? endif; ?> - <? // related groups ?> - <? $related_groups = $event->getRelatedGroups(); ?> - <? if (sizeof($related_groups)) : ?> - <div> - <b><?= _('Betroffene Gruppen') ?>:</b> - <ul class="list-unstyled"> - <? foreach ($related_groups as $group) : ?> - <li> - <?= htmlReady($group->name) ?> - </li> - <? endforeach; ?> - </ul> - </div> - <? endif; ?> - <? endif; ?> - <? endif; ?> -</div> diff --git a/app/views/calendar/single/day.php b/app/views/calendar/single/day.php deleted file mode 100644 index 448509cd1725294f021b86211ed3da707b9a24c0..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/day.php +++ /dev/null @@ -1,13 +0,0 @@ -<div style="width: 100%; display: flex; flex-wrap: wrap;"> - <div style="flex-grow:2; flex-basis: 60%;"> - <?= $this->render_partial('calendar/single/_day'); ?> - </div> - <div class="hidden-medium-down" style="flex-grow:1; padding-left:1em;"> - <? $imt = Request::int('imt', mktime(12, 0, 0, date('n', $atime) - 1, date('j', $atime), date('Y', $atime))) ?> - <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => '']) ?> - <? $imt = mktime(12, 0, 0, date('n', $imt) + 1, date('j', $imt), date('Y', $imt)) ?> - <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => 'NONAVARROWS']) ?> - <? $imt = mktime(12, 0, 0, date('n', $imt) + 1, date('j', $imt), date('Y', $imt)) ?> - <?= $this->render_partial('calendar/single/_include_month', ['imt' => $imt, 'href' => '', 'mod' => 'NONAVARROWS']) ?> - </div> -</div> diff --git a/app/views/calendar/single/edit.php b/app/views/calendar/single/edit.php deleted file mode 100644 index c22f7a6d9caea7463221f52cc27d0104a3000821..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/edit.php +++ /dev/null @@ -1,454 +0,0 @@ -<? use Studip\Button, Studip\LinkButton; ?> -<? if (Request::isXhr()) : ?> - <? foreach (PageLayout::getMessages() as $messagebox) : ?> - <?= $messagebox ?> - <? endforeach ?> -<? endif; ?> -<form data-dialog="" method="post" action="<?= $controller->url_for($base . 'edit/' . $range_id . '/' . $event->event_id) ?>" class="default collapsable"> - <?= CSRFProtection::tokenTag() ?> - - <fieldset> - <legend> - <? if ($event->isNew()) : ?> - <?= _('Neuen Termin anlegen') ?> - <? else : ?> - <?= _('Termin bearbeiten') ?> - <? endif; ?> - </legend> - - <label class="hidden-tiny-down"> - <input type="checkbox" name="isdayevent" value="1" <?= $event->isDayEvent() ? 'checked' : '' ?> - onChange="jQuery(this).closest('fieldset').find('input[size=\'2\']').prop('disabled', function (i,val) { return !val; });"> - <?= _('Ganztägig') ?> - </label> - - <section class="required"> - <?= _('Beginn') ?> - </section> - - <label class="col-3"> - <?= _('Datum') ?> - <input type="text" name="start_date" id="start-date" value="<?= strftime('%x', $event->getStart()) ?>" size="12" required> - </label> - - <label class="col-3"> - <?= _('Uhrzeit') ?> - - <div class="hgroup"> - <input class="size-s no-hint" - type="text" - name="start_hour" - value="<?= date('G', $event->getStart()) ?>" - size="2" - maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?> - aria-label="Stunde"> - : - <input class="size-s no-hint" - type="text" - name="start_minute" - value="<?= date('i', $event->getStart()) ?>" - size="2" - maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?> - aria-label="Minuten"> - </div> - </label> - - <section class="required"> - <?= _('Ende') ?> - </section> - - <label class="col-3"> - <?= _('Datum') ?> - <input type="text" name="end_date" id="end-date" value="<?= strftime('%x', $event->getEnd()) ?>" size="12" required> - </label> - - <label class="col-3"> - <?= _('Uhrzeit') ?> - - <div class="hgroup"> - <input class="size-s no-hint" - type="text" - name="end_hour" - value="<?= date('G', $event->getEnd()) ?>" - size="2" - aria-label="<?= _("Stunde") ?>" - maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>> - : - <input class="size-s no-hint" - type="text" - name="end_minute" - value="<?= date('i', $event->getEnd()) ?>" - size="2" - aria-label="<?= _("Minuten") ?>" - maxlength="2"<?= $event->isDayEvent() ? ' disabled' : '' ?>> - </div> - </label> - - <label> - <span class="required"> - <?= _('Zusammenfassung') ?> - </span> - - <input type="text" size="50" name="summary" id="summary" value="<?= htmlReady($event->getTitle()) ?>"> - </label> - - <label> - <?= _('Beschreibung') ?> - <textarea rows="2" cols="40" id="description" name="description"><?= htmlReady($event->getDescription()) ?></textarea> - </label> - - <label class="col-3"> - <?= _('Kategorie') ?> - <select name="category_intern" id="category-intern" class="nested-select"> - <? foreach ($GLOBALS['PERS_TERMIN_KAT'] as $key => $category) : ?> - <option value="<?= $key ?>" <?= $key == $event->getCategory() ? 'selected' : '' ?> class="calendar-category<?= $key ?>" data-color-class="calendar-category<?= $key ?>"> - <?= htmlReady($category['name']) ?> - </option> - <? endforeach; ?> - </select> - </label> - - <label class="col-3"> - <?= tooltipicon(_('Sie können beliebige Kategorien in das Freitextfeld eingeben. Trennen Sie einzelne Kategorien bitte durch ein Komma.')) ?> - <input type="text" name="categories" value="<?= htmlReady($event->getUserDefinedCategories()) ?>" - placeholder="<?= _('Eigener Kategoriename') ?>"> - </label> - - <label> - <?= _('Raum/Ort') ?> - <input type="text" name="location" id="location" value="<?= htmlReady($event->getLocation()) ?>"> - </label> - - <? if ($calendar->getPermissionByUser($GLOBALS['user']->id) == Calendar::PERMISSION_OWN) : ?> - <? $info = _('Private und vertrauliche Termine sind nur für Sie sichtbar.') ?> - - <? /* SEMBBS nur private und vertrauliche Termine - <? $info = _('Private und vertrauliche Termine sind nur für Sie sichtbar. Öffentliche Termine werden auf ihrer internen Homepage auch anderen Nutzern bekanntgegeben.') ?> - * - */ ?> - - <? elseif ($calendar->getRange() == Calendar::RANGE_SEM) : ?> - <? $info = _('In Veranstaltungskalendern können nur private Termine angelegt werden.') ?> - <? elseif ($calendar->getRange() == Calendar::RANGE_INST) : ?> - <? $info = _('In Einrichtungskalendern können nur private Termine angelegt werden.') ?> - <? else : ?> - <? $info = _('Im Kalender eines anderen Nutzers können Sie nur private oder vertrauliche Termine einstellen. Vertrauliche Termine sind nur für Sie und den Kalenderbesitzer sichtbar. Alle anderen sehen den Termin nur als Besetztzeit.') ?> - <? endif; ?> - - <label for="accessibility"> - <?= _('Zugriff') ?> - <?= tooltipicon($info) ?> - - <select name="accessibility" id="accessibility" size="1"> - <? foreach ($event->getAccessibilityOptions($calendar->getPermissionByUser($GLOBALS['user']->id)) as $key => $option) : ?> - <option value="<?= $key ?>"<?= $event->getAccessibility() == $key ? ' selected' : '' ?>><?= $option ?></option> - <? endforeach; ?> - </select> - </label> - - <label> - <?= _('Priorität') ?> - - <? $priority_names = [_('Keine Angabe'), _('Hoch'), _('Mittel'), _('Niedrig')] ?> - <select name="priority" id="priority" size="1"> - <? foreach ($priority_names as $key => $priority) : ?> - <option value="<?= $key ?>"<?= $key == $event->getPriority() ? ' selected' : '' ?>><?= $priority ?></option> - <? endforeach; ?> - </select> - </label> - - <? if (!$event->isNew() && Config::get()->CALENDAR_GROUP_ENABLE) : ?> - <section> - <? $author = $event->getAuthor() ?> - <? if ($author) : ?> - <?= sprintf(_('Eingetragen am: %s von %s'), - strftime('%x, %X', $event->mkdate), - htmlReady($author->getFullName('no_title'))) ?> - <? endif; ?> - </section> - <? if ($event->event->mkdate < $event->event->chdate) : ?> - <? $editor = $event->getEditor() ?> - <? if ($editor) : ?> - <section> - <?= sprintf(_('Zuletzt bearbeitet am: %s von %s'), - strftime('%x, %X', $event->chdate), - htmlReady($editor->getFullName('no_title'))) ?> - </section> - <? endif; ?> - <? endif; ?> - <? endif; ?> - </fieldset> - - - <fieldset class="collapsed"> - <legend> - <?= _('Wiederholung') ?> - <? if ($event->getRecurrence('rtype') != 'SINGLE') : ?> - (<?= $event->toStringRecurrence() ?>) - <? endif ?> - </legend> - - <h2><?= _('Wiederholungsart') ?></h2> - - <section> - <? $linterval = $event->getRecurrence('linterval') ?: '1' ?> - <? $rec_type = $event->toStringRecurrence(true) ?> - <ul class="recurrences"> - <li> - <label class="rec-label"> - <input type="radio" class="rec-select" id="rec-none" name="recurrence" value="single"<?= $event->getRecurrence('rtype') == 'SINGLE' ? ' checked' : '' ?>> - <?= _('Keine') ?> - <?= tooltipIcon(_('Der Termin wird nicht wiederholt.')) ?> - </label> - </li> - <li> - <label class="rec-label"> - <input type="radio" class="rec-select" id="rec-daily" name="recurrence" value="daily"<?= $event->getRecurrence('rtype') == 'DAILY' ? ' checked' : '' ?>> - <?= _('Täglich') ?> - </label> - - <div class="rec-content" id="rec-content-daily"> - <div class="hgroup"> - <label> - <input type="radio" name="type_daily" value="day"<?= in_array($rec_type, ['daily', 'xdaily']) ? ' checked' : '' ?>> - <?= sprintf(_('Jeden %s. Tag'), '<input type="text" size="3" name="linterval_d" value="' . $linterval . '">') ?> - </label> - </div> - - <label> - <input type="radio" name="type_daily" value="workday"<?= $rec_type == 'workdaily' ? ' checked' : '' ?>> - <?= _('Jeden Werktag') ?> - </label> - </div> - </li> - <li> - <? $wdays = [ - '1' => _('Montag'), - '2' => _('Dienstag'), - '3' => _('Mittwoch'), - '4' => _('Donnerstag'), - '5' => _('Freitag'), - '6' => _('Samstag'), - '7' => _('Sonntag')] ?> - <label class="rec-label" for="rec-weekly"> - <input type="radio" class="rec-select" id="rec-weekly" name="recurrence" value="weekly"<?= $event->getRecurrence('rtype') == 'WEEKLY' ? ' checked' : '' ?>> - <?= _('Wöchentlich') ?> - </label> - <div class="rec-content" id="rec-content-weekly"> - <div class="hgroup"> - <label> - <?= sprintf(_('Jede %s. Woche am:'), '<input type="text" size="3" name="linterval_w" value="' . $linterval . '">') ?> - </label> - </div> - <div> - <? $aday = $event->getRecurrence('wdays') ?: date('N', $event->getStart()) ?> - <? foreach ($wdays as $key => $wday) : ?> - <label style="white-space: nowrap;"> - <input type="checkbox" name="wdays[]" value="<?= $key ?>"<?= mb_strpos((string) $aday, (string) $key) !== false ? ' checked' : '' ?>> - <?= $wday ?> - </label> - <? endforeach; ?> - </div> - </div> - </li> - <li> - <? $mdays = [ - '1' => _('Ersten'), - '2' => _('Zweiten'), - '3' => _('Dritten'), - '4' => _('Vierten'), - '5' => _('Letzten')] ?> - <? $mdays_options = '' ?> - <? $mday_selected = $event->getRecurrence('sinterval') ?> - <? foreach ($mdays as $key => $mday) : - $mdays_options .= '<option value="' . $key . '"'; - if ($key == $mday_selected) { - $mdays_options .= ' selected'; - } - $mdays_options .= '>' . $mday . '</option>'; - endforeach; ?> - <? $wdays_options = '' ?> - <? $wday_selected = $event->getRecurrence('wdays') ?: date('N', $event->getStart()) ?> - <? foreach ($wdays as $key => $wday) : - $wdays_options .= '<option value="' . $key . '"'; - if ($key == $wday_selected) { - $wdays_options .= ' selected'; - } - $wdays_options .= '>' . $wday . '</option>'; - endforeach; ?> - - <label class="rec-label" for="rec-monthly"> - <input type="radio" class="rec-select" id="rec-monthly" name="recurrence" value="monthly"<?= $event->getRecurrence('rtype') == 'MONTHLY' ? ' checked' : '' ?>> - <?= _('Monatlich') ?> - </label> - <div class="rec-content" id="rec-content-monthly"> - <div class="hgroup"> - <label> - <input type="radio" value="day" name="type_m"<?= in_array($rec_type, ['mday_monthly', 'mday_xmonthly']) ? ' checked' : '' ?>> - <? $mday = $event->getRecurrence('day') ?: date('j', $event->getStart()) ?> - <?= sprintf(_('Wiederholt am %s. jeden %s. Monat'), - '<input type="text" name="day_m" size="2" value="' - . $mday . '">', - '<input type="text" name="linterval_m1" size="3" value="' - . $linterval . '">') ?> - </label> - </div> - <div class="hgroup"> - <label> - <input type="radio" value="wday" name="type_m"<?= in_array($rec_type, ['xwday_xmonthly', 'lastwday_xmonthly', 'xwday_monthly', 'lastwday_monthly']) ? ' checked' : '' ?>> - <?= sprintf(_('Jeden %s alle %s Monate'), - '<select size="1" name="sinterval_m">' . $mdays_options . '</select>' - . '<select size="1" name="wday_m">' . $wdays_options . '</select>', - '<input type="text" class="no-hint" size="3" maxlength="3" name="linterval_m2" value="' - . $linterval . '">')?> - </label> - </div> - </div> - </li> - <li> - <? $months = [ - '1' => _('Januar'), - '2' => _('Februar'), - '3' => _('März'), - '4' => _('April'), - '5' => _('Mai'), - '6' => _('Juni'), - '7' => _('Juli'), - '8' => _('August'), - '9' => _('September'), - '10' => _('Oktober'), - '11' => _('November'), - '12' => _('Dezember')] ?> - <? $months_options = '' ?> - <? $month_selected = $event->getRecurrence('month') ?: date('n', $event->getStart()) ?> - <? foreach ($months as $key => $month) : - $months_options .= '<option value="' . $key . '"'; - if ($key == $month_selected) { - $months_options .= ' selected'; - } - $months_options .= '>' . $month . '</option>'; - endforeach; ?> - - <label class="rec-label" for="rec-yearly"> - <input type="radio" class="rec-select" id="rec-yearly" name="recurrence" value="yearly"<?= $event->getRecurrence('rtype') == 'YEARLY' ? ' checked' : '' ?>> - <?= _('Jährlich') ?> - </label> - <div class="rec-content" id="rec-content-yearly"> - <div class="hgroup"> - <label> - <input type="radio" value="day" name="type_y"<?= $rec_type == 'mday_month_yearly' ? ' checked' : '' ?>> - <?= sprintf(_('Jeden %s. %s'), - '<input type="text" size="2" maxlength="2" name="day_y" value="' - . ($event->getRecurrence('day') ?: date('j', $event->getStart())) . '">', - '<select size="1" name="month_y1">' . $months_options . '</select>') ?> - </label> - </div> - - <div class="hgroup"> - <label> - <input type="radio" value="wday" name="type_y"<?= in_array($rec_type, ['xwday_month_yearly', 'lastwday_month_yearly']) ? ' checked' : '' ?>> - <?= sprintf(_('Jeden %s im %s'), - '<select size="1" name="sinterval_y">' . $mdays_options . '</select>' - . '<select size="1" name="wday_y">' . $wdays_options . '</select>', - '<select size="1" name="month_y2">' . $months_options . '</select>') ?> - </label> - </div> - </div> - </li> - </ul> - </section> - - <h2><?= _('Wiederholung endet') ?></h2> - - <label> - <? $checked = (!$event->getRecurrence('expire') || $event->getRecurrence('expire') >= Calendar::CALENDAR_END) && !$event->getRecurrence('count') ?> - <input type="radio" name="exp_c" value="never"<?= $checked ? ' checked' : '' ?>> - <?= _('Nie') ?> - </label> - - <? $checked = $event->getRecurrence('expire') && $event->getRecurrence('expire') < Calendar::CALENDAR_END && !$event->getRecurrence('count') ?> - - <section class="hgroup"> - <label> - <input type="radio" name="exp_c" value="date"<?= $checked ? ' checked' : '' ?>> - <? $exp_date = $event->getRecurrence('expire') != Calendar::CALENDAR_END ? $event->getRecurrence('expire') : $event->getEnd() ?> - <?= sprintf(_('Am %s'), - '<input type="text" class="size-s" name="exp_date" id="exp-date" value="' - . strftime('%x', $exp_date) . '">') ?> - </label> - </section> - - <section class="hgroup"> - <? $checked = $event->getRecurrence('count') ?> - <label> - <input type="radio" name="exp_c" value="count"<?= $checked ? ' checked' : '' ?>> - <? $exp_count = $event->getRecurrence('count') ?: '10' ?> - <?= sprintf(_('Nach %s Wiederholungen'), - '<input type="text" size="5" name="exp_count" value="' - . $exp_count . '">') ?> - </label> - </section> - - - <label for="exc-dates"> - <?= _('Ausnahmen') ?> - </label> - - <ul id="exc-dates"> - <? $exceptions = $event->getExceptions(); ?> - <? sort($exceptions, SORT_NUMERIC); ?> - <? foreach ($exceptions as $exception) : ?> - <li> - <label class="undecorated"> - <input type="checkbox" name="del_exc_dates[]" value="<?= strftime('%d.%m.%Y', $exception) ?>" style="display: none;"> - <span><?= strftime('%x', $exception) ?><?= Icon::create('trash', 'clickable', ['title' => _('Ausnahme löschen')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?></span> - </label> - <input type="hidden" name="exc_dates[]" value="<?= strftime('%d.%m.%Y', $exception) ?>"> - </li> - <? endforeach; ?> - </ul> - - <div class="hgroup"> - <input style="vertical-align: top; opacity: 0.8;" - type="text" size="12" name="exc_date" id="exc-date" value="" - placeholder="<?= _("Datum eingeben") ?>"> - <span style="vertical-align: top;" onclick="STUDIP.CalendarDialog.addException(); return false;"> - <?= Icon::create('add', 'clickable', ['title' => _('Ausnahme hinzufügen')])->asInput(['class' => 'text-bottom']) ?> - </span> - </div> - </fieldset> - - <? if (Config::get()->CALENDAR_GROUP_ENABLE && $calendar->getRange() == Calendar::RANGE_USER) : ?> - <?= $this->render_partial('calendar/group/_attendees') ?> - <? endif; ?> - - <footer data-dialog-button> - <?= Button::create(_('Speichern'), 'store', ['title' => _('Termin speichern')]) ?> - - <? if (!$event->isNew()) : ?> - <? if ($event->getRecurrence('rtype') != 'SINGLE') : ?> - <?= LinkButton::create(_('Aus Serie löschen'), $controller->url_for('calendar/single/delete_recurrence/' . implode('/', $event->getId()) . '/' . $atime)) ?> - <? endif; ?> - <?= LinkButton::create(_('Löschen'), $controller->url_for('calendar/single/delete/' . implode('/', $event->getId()))) ?> - <? endif; ?> - <? if (!Request::isXhr()) : ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view, [$event->getStart()])) ?> - <? endif; ?> - </footer> -</form> -<script> - jQuery('#start-date').datepicker({ - altField: '#end-date' - }); - jQuery('#end-date').datepicker(); - jQuery('#exp-date').datepicker(); - jQuery('#exc-date').datepicker(); - - $('ul.recurrences input[type=radio][id^=rec]').bind('change', function() { - $('.rec-content').hide(); - - if ($(this).is(':checked')) { - $(this).parent().siblings('.rec-content').show(); - } - }) -</script> diff --git a/app/views/calendar/single/edit_status.php b/app/views/calendar/single/edit_status.php deleted file mode 100644 index 3e3bfc3e6d99678b64e8aa165c6fb26d4d6f022e..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/edit_status.php +++ /dev/null @@ -1,3 +0,0 @@ -<form action="<?= $controller->url_for($base . 'edit_status/' . $range_id . '/' . $event->event_id) ?>" method="post"> - <?= $this->render_partial('calendar/single/_event_data') ?> -</form> diff --git a/app/views/calendar/single/event.php b/app/views/calendar/single/event.php deleted file mode 100644 index e9d544f8199f2a6d8f06a51ed9055120bf7ba284..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/event.php +++ /dev/null @@ -1 +0,0 @@ -<?= $this->render_partial('calendar/single/_event_data') ?> diff --git a/app/views/calendar/single/export_calendar.php b/app/views/calendar/single/export_calendar.php deleted file mode 100644 index 66f86c5dbb6ea0259a053a54b03882211ad0388b..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/export_calendar.php +++ /dev/null @@ -1,54 +0,0 @@ -<? use Studip\Button, Studip\LinkButton; ?> -<? if (Request::isXhr()) : ?> - <? foreach (PageLayout::getMessages() as $messagebox) : ?> - <?= $messagebox ?> - <? endforeach ?> -<? endif; ?> -<form action="<?= $controller->url_for('calendar/single/export_calendar/' . $calendar->getRangeId(), ['atime' => $atime, 'last_view' => $last_view]) ?>" method="post" name="sync_form" id="calendar_sync" class="default"> - <fieldset> - <legend> - <?= sprintf(_('Termine exportieren')) ?> - </legend> - - <label for="event-type"> - <?= _('Welche Termine sollen exportiert werden') ?>: - - <select name="event_type" id="event-type" size="1"> - <option value="user" selected><?= _('Nur eigene Termine') ?></option> - <option value="course"><?= _('Nur Veranstaltungs-Termine') ?></option> - <option value="all"><?= _('Alle Termine') ?></option> - </select> - </label> - - <label> - <input type="radio" name="export_time" value="all" id="export-all" checked> - <?= _('Alle Termine exportieren') ?> - </label> - - <label> - <input type="radio" name="export_time" value="date" id="export-date"> - <?= _('Nur Termin in folgendem Zeitraum exportieren') ?> - </label> - - <section class="hgroup"> - <? $start = strtotime('now') ?> - <? $end = strtotime('+1 year') ?> - <input id="export-start" type="text" name="export_start" class="no-hint" - maxlength="10" class="hasDatepicker" value="<?= strftime('%x', $start) ?>"> - <input id="export-end" type="text" name="export_end" class="no-hint" - maxlength="10" class="hasDatepicker" value="<?= strftime('%x', $end) ?>"> - </section> - </fieldset> - - <footer data-dialog-button> - <?= Button::createAccept(_('Termine exportieren'), 'export', ['title' => _('Termine exportieren')]) ?> - - <? if (!Request::isXhr()) : ?> - <?= LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?> - <? endif; ?> - </footer> -</form> -<script> - jQuery('#export-start').datepicker(); - jQuery('#export-end').datepicker(); -</script> diff --git a/app/views/calendar/single/manage_access.php b/app/views/calendar/single/manage_access.php deleted file mode 100644 index 4f7be8f86b8e952f36d73493981b59ba15b50fae..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/manage_access.php +++ /dev/null @@ -1,99 +0,0 @@ -<? if (Request::isXhr()) : ?> - <? foreach (PageLayout::getMessages() as $messagebox) : ?> - <?= $messagebox ?> - <? endforeach ?> -<? endif; ?> -<form id="calendar-manage-access" data-dialog="" method="post" action="<?= $controller->url_for('calendar/single/store_permissions/' . $calendar->getRangeId()) ?>"> - <? CSRFProtection::tokenTag() ?> - <? $perms = [1 => _('Keine'), 2 => _('Lesen'), 4 => _('Schreiben')] ?> - <table class="default"> - <caption> - <?= _('Bestehende Freigaben') ?> - <span class="actions" style="font-size: 0.8em;"> - <label> - <?= _('Auswahl') ?>: - <select name="group_filter" size="1" class="submit-upon-select"> - <option value="list"<?= $group_filter_selected == 'list' ? ' selected' : '' ?>><?= _('Alle Personen anzeigen') ?></option> - <? foreach ($filter_groups as $filter_group) : ?> - <option value="<?= $filter_group->getId() ?>"<?= $group_filter_selected == $filter_group->getId() ? ' selected' : '' ?>><?= htmlReady($filter_group->name) ?></option> - <? endforeach; ?> - </select> - </label> - <?= Icon::create('accept', 'clickable') - ->asInput([ - 'id' => "calendar-group-submit", - 'name' => "calendar_group_submit", - 'class' => "text-top"]) ?> - <span style="padding-left: 1em;"> - <?= $mps->render() ?> - </span> - <script> - STUDIP.MultiPersonSearch.init(); - </script> - </span> - </caption> - <thead> - <tr> - <th> - <?= _('Name') ?> - </th> - <th> - <?= _('Berechtigung') ?> - </th> - <th> - <?= _('Eigene Berechtigung') ?> - </th> - <th class="actions"> - <?= _('Aktionen') ?> - </th> - </tr> - </thead> - <tbody> - <? foreach ($users as $header => $usergroup): ?> - <tr id="letter_<?= $header ?>" class="calendar-user-head"> - <th colspan="4"> - <?= $header ?> - </th> - </tr> - <? foreach ($usergroup as $user): ?> - <tr id="contact_<?= $user->user_id ?>"> - <td> - <?= ObjectdisplayHelper::avatarlink($user->user) ?> - </td> - <td style="white-space: nowrap;"> - <label> - <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_FORBIDDEN ?>" - <?= $user->permission < Calendar::PERMISSION_READABLE ? ' checked' : '' ?>> - <?= _('Keine') ?> - </label> - <label> - <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_READABLE ?>" - <?= $user->permission == Calendar::PERMISSION_READABLE ? ' checked' : '' ?>> - <?= _('Lesen') ?> - </label> - <label> - <input type="radio" name="perm[<?= $user->user_id ?>]" value="<?= Calendar::PERMISSION_WRITABLE ?>" - <?= $user->permission == Calendar::PERMISSION_WRITABLE ? ' checked' : '' ?>> - <?= _('Schreiben') ?> - </label> - </td> - <td> - <?= $perms[$own_perms[$user->user_id]] ?> - </td> - <td class="actions"> - <a title="<?= _('Benutzer entfernen') ?>" onClick="STUDIP.CalendarDialog.removeUser(this);" href="<?= $controller->url_for('calendar/single/remove_user/' . $calendar->getRangeId(), ['user_id' => $user->user_id]) ?>"> - <?= Icon::create('trash', 'clickable')->asImg() ?> - </a> - </td> - </tr> - <? endforeach; ?> - <? endforeach; ?> - </tbody> - </table> - <div style="text-align: center;" data-dialog-button> - <?= Studip\Button::create(_('Speichern'), 'store') ?> - <? if (!Request::isXhr()) : ?> - <?= Studip\LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?> - <? endif; ?> - </div> -</form> diff --git a/app/views/calendar/single/month.php b/app/views/calendar/single/month.php deleted file mode 100644 index da715f5e0fe531e93f8fb5cb236226f26f75b037..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/month.php +++ /dev/null @@ -1,109 +0,0 @@ -<nav class="calendar-nav" style="vertical-align: middle"> - <span style="white-space: nowrap;"> - <a class="hidden-medium-down" style="padding-right: 2em;" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('-1 year', $atime)]) ?>"> - <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(['style' => 'vertical-align: text-top;']) ?> - <?= strftime('%B %Y', strtotime('-1 year', $atime)) ?> - </a> - <a class="hidden-tiny-down" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('-1 month', $atime)]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Einen Monat zurück')])->asImg(['style' => 'vertical-align: text-top;']) ?> - <?= strftime('%B %Y', strtotime('-1 month', $atime)) ?> - </a> - </span> - - <? - $calType = 'month'; - $calLabel = htmlReady(strftime("%B ", $calendars[15]->getStart())) .' '. date('Y', $calendars[15]->getStart()); - ?> - - <?= $this->render_partial('calendar/single/_calhead', compact('atime', 'calType', 'calLabel')) ?> - - <span style="text-align: right; white-space: nowrap;"> - <a class="hidden-tiny-down" style="padding-right: 2em;" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('+1 month', $atime)]) ?>"> - <?= strftime('%B %Y', strtotime('+1 month', $atime)) ?> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Einen Monat vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - <a class="hidden-medium-down" href="<?= $controller->url_for('calendar/single/month', ['atime' => strtotime('+1 year', $atime)]) ?>"> - <?= strftime('%B %Y', strtotime('+1 year', $atime)) ?> - <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </span> -</nav> - -<div class="table-scrollbox-horizontal"> -<table class="calendar-month"> - <thead> - <tr class="calendar-month-weekdays"> - <? $week_days = [39092400, 39178800, 39265200, 39351600, 39438000, 39524400, 39610800]; ?> - <? foreach ($week_days as $week_day) : ?> - <td class="precol1w"> - <?= strftime('%a', $week_day) ?> - </td> - <? endforeach; ?> - <td align="center" class="precol1w"> - <?= _('Woche') ?> - </td> - </tr> - </thead> - <tbody> - <? for ($i = $first_day, $j = 0; $i <= $last_day; $i += 86400, $j++) : ?> - <? $aday = date('j', $i); ?> - <? - $class_day = ''; - if (($aday - $j - 1 > 0) || ($j - $aday > 6)) { - $class_cell = 'lightmonth'; - $class_day = 'light'; - } elseif (date('Ymd', $i) == date('Ymd')) { - $class_cell = 'celltoday'; - } else { - $class_cell = 'month'; - } - $hday = holiday($i); - - if ($j % 7 == 0) { - ?><tr><? - } - ?> - <td class="<?= $class_cell ?>"> - <? if (($j + 1) % 7 == 0) : ?> - <a class="<?= $class_day . 'sday' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <? if (!empty($hday['name'])) : ?> - <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div> - <? endif; ?> - <? foreach ($calendars[$j]->events as $event) : ?> - <div data-tooltip> - <a data-dialog="size=auto" title="<?= _('Termin bearbeiten') ?>" class="inday <?= $event instanceof CourseEvent ? 'calendar-course-event-text' : 'calendar-event-text' ?><?= $event->getCategory() ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendars[$j]->getRangeId() . '/' . $event->event_id, ['atime' => $event->getStart()]) ?>"><?= htmlReady($event->getTitle()) ?></a> - <?= $this->render_partial('calendar/single/_tooltip', ['event' => $event, 'calendar' => $calendars[$j]]) ?> - </div> - <? endforeach; ?> - </td> - <td class="lightmonth calendar-month-week"> - <a style="font-weight: bold;" class="calhead" href="<?= $controller->url_for('calendar/single/week', ['atime' => $i]) ?>"><?= strftime("%V", $i) ?></a> - </td> - </tr> - <? else : ?> - <? $hday_class = ['day', 'day', 'shday', 'hday'] ?> - <? if (!empty($hday['col'])) : ?> - <a class="<?= $class_day . $hday_class[$hday['col']] ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <div style="color: #aaaaaa;" class="inday"><?= $hday['name'] ?></div> - <? else : ?> - <a class="<?= $class_day . 'day' ?>" href="<?= $controller->url_for('calendar/single/day', ['atime' => $i]) ?>"> - <?= $aday ?> - </a> - <? endif; ?> - <? foreach ($calendars[$j]->events as $event) : ?> - <div data-tooltip> - <a data-dialog="size=auto" title="<?= _('Termin bearbeiten') ?>" class="inday <?= $event instanceof CourseEvent ? 'calendar-course-event-text' : 'calendar-event-text' ?><?= $event->getCategory() ?>" href="<?= $controller->url_for('calendar/single/edit/' . $calendars[$j]->getRangeId() . '/' . $event->event_id, ['atime' => $event->getStart()]) ?>"><?= htmlReady($event->getTitle()) ?></a> - <?= $this->render_partial('calendar/single/_tooltip', ['event' => $event, 'calendar' => $calendars[$j]]) ?> - </div> - <? endforeach; ?> - </td> - <? endif; ?> - <? endfor; ?> - </tr> - </tbody> -</table> -</div> diff --git a/app/views/calendar/single/seminar_events.php b/app/views/calendar/single/seminar_events.php deleted file mode 100644 index 06ed5faefdeaad8a62a416704e43bfca81e70819..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/seminar_events.php +++ /dev/null @@ -1,104 +0,0 @@ -<? if (!empty($sem_courses)) : ?> - <?= $this->render_partial('calendar/single/_semester_filter') ?> - <? $_order = (!$order_by || $order == 'desc') ? 'asc' : 'desc' ?> - <form action="<?= $controller->url_for('calendar/single/store_selected_sem') ?>" method="post"> - <?= CSRFProtection::tokenTag() ?> - <div id="my_seminars"> - <? foreach ($sem_courses as $sem_key => $course_group) : ?> - <table class="default collapsable"> - <caption> - <?= htmlReady($sem_data[$sem_key]['name'] ?? _('Unbekanntes Semester')) ?> - </caption> - <colgroup> - <col width="7px"> - <col width="25px"> - <? if ($config_sem_number) : ?> - <col width="10%"> - <? endif ?> - <col> - <col width="45px"> - <col width="10%"> - </colgroup> - <thead> - <tr class="sortable"> - <th></th> - <th></th> - <? if ($config_sem_number) : ?> - <th class=<?= ($order_by == 'veranstaltungsnummer') ? ($order == 'desc') ? 'sortdesc' : 'sortasc' : '' ?>> - <a href="<?= $controller->url_for(sprintf('my_courses/index/veranstaltungsnummer/%s', $_order)) ?>"> - <?= _('Nr.') ?> - </a> - </th> - <? endif ?> - <th - class=<?= ($order_by == 'name') ? ($order == 'desc') ? 'sortdesc' : 'sortasc' : '' ?>> - <a href="<?= $controller->url_for(sprintf('calendar/single/seminar_events/name/%s', $_order)) ?>" data-dialog="size=auto"> - <?= _('Name') ?> - </a> - </th> - <th></th> - <th><?= _('Auswahl') ?></th> - </tr> - </thead> - <? foreach ($course_group as $course) : ?> - <? $sem_class = $course['sem_class']; ?> - <tr> - <td class="gruppe<?= $course['gruppe'] ?>"></td> - <td> - <? if ($sem_class['studygroup_mode']) : ?> - <?= StudygroupAvatar::getAvatar($course['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $course['name']]) - ?> - <? else : ?> - <?= CourseAvatar::getAvatar($course['seminar_id'])->getImageTag(Avatar::SMALL, ['title' => $course['name']]) - ?> - <? endif ?> - </td> - <? if($config_sem_number) :?> - <td><?= $course['veranstaltungsnummer']?></td> - <? endif?> - <td style="text-align: left"> - <a href="<?= URLHelper::getLink('seminar_main.php', ['auswahl' => $course['seminar_id']]) ?>" - <?= $course['visitdate'] <= $course['chdate'] ? 'style="color: red;"' : '' ?>> - <?= htmlReady($course['name']) ?> - <?= ($course['is_deputy'] ? ' ' . _('[Vertretung]') : '');?> - </a> - <? if ($course['visible'] == 0) : ?> - <?= _('[versteckt]') ?> - <? endif ?> - </td> - <td> - <? if (!$sem_class['studygroup_mode']) : ?> - <a data-dialog href="<?= $controller->url_for(sprintf('course/details/index/%s', $course['seminar_id'])) ?>"> - <? $params = tooltip2(_('Veranstaltungsdetails')); ?> - <? $params['style'] = 'cursor: pointer'; ?> - <?= Icon::create('info-circle', 'inactive')->asImg(20, $params) ?> - </a> - <? else : ?> - <?= Assets::img('blank.gif', ['width' => 20, 'height' => 20]); ?> - <? endif ?> - </td> - <td style="text-align: center;"> - <input type="hidden" name="selected_sem[<?= $course['seminar_id'] ?>]" value="0"> - <input type="checkbox" name="selected_sem[<?= $course['seminar_id'] ?>]" value="1"<?= in_array($course['seminar_id'], $bind_calendar) ? ' checked' : '' ?>> - </td> - </tr> - <? endforeach ?> - </table> - <? endforeach ?> - </div> - <div style="text-align: center;" data-dialog-button> - <?= Studip\Button::create(_('Speichern'), 'store') ?> - <? if (!Request::isXhr()) : ?> - <?= Studip\LinkButton::create(_('Abbrechen'), $controller->url_for('calendar/single/' . $last_view)) ?> - <? endif; ?> - </div> - </form> -<? else : ?> - <?= PageLayout::postMessage(MessageBox::info(_('Es wurden keine Veranstaltungen gefunden. Mögliche Ursachen:'), [ - sprintf(_('Sie haben zur Zeit keine Veranstaltungen belegt, an denen Sie teilnehmen können.<br>Bitte nutzen Sie %s<b>Veranstaltung suchen / hinzufügen</b>%s um sich für Veranstaltungen anzumelden.'),'<a href="' . URLHelper::getLink('dispatch.php/search/courses') . '">', '</a>'), - _('In dem ausgewählten <b>Semester</b> wurden keine Veranstaltungen belegt.').'<br>'._('Wählen Sie links im <b>Semesterfilter</b> ein anderes Semester aus') - ]))?> -<? endif ?> -<? if (!empty($my_bosses) && is_array($my_bosses) && count($my_bosses)) : ?> - <?= $this->render_partial('my_courses/_deputy_bosses'); ?> -<? endif ?> diff --git a/app/views/calendar/single/week.php b/app/views/calendar/single/week.php deleted file mode 100644 index 98f831773e53922bc82a98c595b0b56a1490893d..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/week.php +++ /dev/null @@ -1,199 +0,0 @@ -<? -$at = date('G', $atime); -if ($at >= $settings['start'] - && $at <= $settings['end'] || !$atime) { - $start = $settings['start']; - $end = $settings['end']; -} elseif ($at < $settings['start']) { - $start = 0; - $end = $settings['start'] + 2; -} else { - $start = $settings['end'] - 2; - $end = 23; -} -$tab_arr = []; -$max_columns = 0; -$week_type = $settings['type_week'] == 'SHORT' ? 5 : 7; -$rows = ($end - $start + 1) * 3600 / $settings['step_week']; - -for ($i = 0; $i < $week_type; $i++) { - $tab_arr[$i] = $calendars[$i]->createEventMatrix($start * 3600, $end * 3600, $settings['step_week']); - if ($tab_arr[$i]['max_cols']) { - $max_columns += ($tab_arr[$i]['max_cols'] + 1); - } else { - $max_columns++; - } -} - -$rowspan = ceil(3600 / $settings['step_week']); -$height = ' height="20"'; - -if ($rowspan > 1) { - $colspan_1 = ' colspan="2"'; - $colspan_2 = $max_columns + 4; - $width_daycols = 100 - (4 + $week_type) * 0.1; -} else { - $colspan_1 = ''; - $colspan_2 = $max_columns + 2; - $width_daycols = 100 - (2 + $week_type) * 0.1; -} -?> - -<nav class="calendar-nav" style="vertical-align: middle"> - <span style="white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => strtotime('-1 week', $atime)]) ?>"> - <?= Icon::create('arr_1left', 'clickable', ['title' => _('Eine Woche zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <span class="hidden-tiny-down"><?= sprintf(_('%u. Woche'), strftime('%V', strtotime('-1 week', $atime))) ?></span> - </a> - </span> - - <? - $calType = 'week'; - $calLabel = $this->render_partial('calendar/single/_calhead_label_week', compact('week_type')); - ?> - - <?= $this->render_partial('calendar/single/_calhead', compact('atime', 'calType', 'calLabel')) ?> - - <span style="white-space: nowrap; text-align: right;"> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => strtotime('+1 week', $atime)]) ?>"> - <span class="hidden-tiny-down"><?= sprintf(_('%u. Woche'), strftime('%V', strtotime('+1 week', $atime))) ?></span> - <?= Icon::create('arr_1right', 'clickable', ['title' => _('Eine Woche vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </span> -</nav> - -<table id="main_content" class="calendar-week"> - <colgroup> - <col style="max-width: 1.5em; width: 1.5em;"> - <? if ($rowspan > 1) : ?> - <col style="max-width: 1.5em; width: 1.5em;"> - <? endif; ?> - <? for ($i = 0; $i < $week_type; $i++) : ?> - <? if ($tab_arr[$i]['max_cols'] > 0) : ?> - <? $event_cols = $tab_arr[$i]['max_cols'] ?: 1; ?> - <col span="<?= $event_cols ?>" style="width: <?= 100 / $week_type / $event_cols ?>%"> - <col style="max-width: 0.9em; width: 0.9em;"> - <? else : ?> - <col style="width: <?= 100 / $week_type ?>%"> - <? endif; ?> - <? endfor; ?> - <col class="hidden-tiny-down" style="max-width: 1.5em; width: 1.5em;"> - <? if ($rowspan > 1) : ?> - <col class="hidden-tiny-down" style="max-width: 1.5em; width: 1.5em;"> - <? endif; ?> - </colgroup> - <thead> - <tr> - <td style="text-align: center; white-space: nowrap;" <?= $colspan_1 ?>> - <? if ($start > 0) : ?> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($start - 1, 0, 0, date('n', $atime), date('j', $atime), date('Y', $atime))]) ?>"> - <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?> - </a> - <? endif ?> - </td> - <? // weekday and date as title for each column ?> - <? for ($i = 0; $i < $week_type; $i++) : ?> - <td style="text-align:center; font-weight:bold;"<?= ($tab_arr[$i]['max_cols'] > 0 ? ' colspan="' . ($tab_arr[$i]['max_cols'] + 1) . '"' : '' ) ?>> - <a class="calhead" href="<?= $controller->url_for('calendar/single/day', ['atime' => $calendars[$i]->getStart()]) ?>"> - <span class="hidden-tiny-down"><?= strftime('%a', $calendars[$i]->getStart()) ?></span> <?= date('d', $calendars[$i]->getStart()) ?> - </a> - <? if ($holiday = holiday($calendars[$i]->getStart())) : ?> - <div class="hidden-tiny-down" style="font-size:9pt; color:#bbb; height:auto; overflow:visible; font-weight:bold;"><?= $holiday['name'] ?></div> - <? endif ?> - </td> - <? endfor ?> - <td style="text-align: center; white-space: nowrap;" <?= $colspan_1 ?>> - <? if ($start > 0) : ?> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($start - 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>"> - <?= Icon::create('arr_1up', 'clickable', ['title' => _('Früher')])->asImg() ?> - </a> - <? endif ?> - </td> - </tr> - </thead> - <tbody> - <tr> - <? // Zeile mit Tagesterminen ausgeben ?> - <td class="precol1w"<?= $colspan_1 ?> height="20"> - <?= _("Tag") ?> - </td> - <? for ($i = 0; $i < $week_type; $i++) : ?> - <? - if (date('Ymd', $calendars[$i]->getStart()) == date('Ymd')) { - $class_cell = 'celltoday'; - } else { - $class_cell = ''; - } - ?> - <?= $this->render_partial('calendar/single/_day_dayevents', ['em' => $tab_arr[$i], 'calendar' => $calendars[$i], 'class_cell' => $class_cell]) ?> - <? endfor ?> - <td class="precol1w"<?= $colspan_1 ?>> - <?= _('Tag') ?> - </td> - </tr> - <? $j = $start ?> - <? for ($i = 0; $i < $rows; $i++) : ?> - <tr> - <? if ($i % $rowspan == 0) : ?> - <? if ($rowspan == 1) : ?> - <td class="precol1w"<?= $height ?>><?= $j ?></td> - <? else : ?> - <td class="precol1w" rowspan="<?= $rowspan ?>"><?= $j ?></td> - <? endif ?> - <? endif ?> - <? if ($rowspan > 1) : ?> - <? $minutes = (60 / $rowspan) * ($i % $rowspan); ?> - <? if ($minutes == 0) : ?> - <td class="precol2w"<?= $height ?>>00</td> - <? else : ?> - <td class="precol2w"<?= $height ?>><?= $minutes ?></td> - <? endif ?> - <? endif ?> - <? for ($y = 0; $y < $week_type; $y++) : ?> - <? - if (date('Ymd', $calendars[$y]->getStart()) == date('Ymd')) { - $class_cell = 'celltoday'; - } else { - $class_cell = ''; - } - ?> - <?= $this->render_partial('calendar/single/_day_cell', ['calendar' => $calendars[$y], 'em' => $tab_arr[$y], 'row' => $i, 'start' => $start * 3600, 'i' => $i + ($start * 3600 / $settings['step_week']), 'step' => $settings['step_week'], 'class_cell' => $class_cell]); ?> - <? endfor ?> - <? if ($rowspan > 1) : ?> - <? if ($minutes == 0) : ?> - <td class="precol2w"<?= $height ?>>00</td> - <? else : ?> - <td class="precol2w"<?= $height ?>><?= $minutes ?></td> - <? endif ?> - <? endif ?> - <? if (($i + 2) % $rowspan == 0) : ?> - <? if ($rowspan == 1) : ?> - <td class="precol1w"<?= $height ?>><?= $j ?></td> - <? else : ?> - <td class="precol1w" rowspan="<?= $rowspan ?>"><?= $j ?></td> - <? endif ?> - <? $j = $j + ceil($settings['step_week'] / 3600); ?> - <? endif ?> - </tr> - <? endfor ?> - </tbody> - <tfoot> - <tr> - <td<?= $colspan_1 ?> style="text-align:center;"> - <? if ($end < 23) : ?> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($end + 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>"> - <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?> - </a> - <? endif ?> - </td> - <td colspan="<?= $max_columns ?>"> </td> - <td<?= $colspan_1 ?> style="text-align:center;"> - <? if ($end < 23) : ?> - <a href="<?= $controller->url_for('calendar/single/week', ['atime' => mktime($end + 1, 0, 0, date('n', $calendars[0]->getStart()), date('j', $calendars[0]->getStart()), date('Y', $calendars[0]->getStart()))]) ?>"> - <?= Icon::create('arr_1down', 'clickable', ['title' => _('Später')])->asImg() ?> - </a> - <? endif ?> - </td> - </tr> - </tfoot> -</table> diff --git a/app/views/calendar/single/year.php b/app/views/calendar/single/year.php deleted file mode 100644 index 0ebafe1431e387cffe983578dd5a5ff4d976b13c..0000000000000000000000000000000000000000 --- a/app/views/calendar/single/year.php +++ /dev/null @@ -1,145 +0,0 @@ -<div class="calendar-single-year"> - - <nav class="calendar-nav" style="vertical-align: middle"> - <span style="white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/single/year', ['atime' => strtotime('-1 year', $atime)]) ?>"> - <?= Icon::create('arr_2left', 'clickable', ['title' => _('Ein Jahr zurück')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - <?= strftime('%Y', strtotime('-1 year', $atime)) ?> - </a> - </span> - - <? - $calType = 'year'; - $calLabel = date('Y', $calendar->getStart()); - ?> - - <?= $this->render_partial('calendar/single/_calhead', compact('calendar', 'atime', 'calType', 'calLabel')) ?> - - <span style="text-align: right; white-space: nowrap;"> - <a href="<?= $controller->url_for('calendar/single/year', ['atime' => strtotime('+1 year', $atime)]) ?>"> - <?= strftime('%Y', strtotime('+1 year', $atime)) ?> - <?= Icon::create('arr_2right', 'clickable', ['title' => _('Ein Jahr vor')])->asImg(16, ['style' => 'vertical-align: text-top;']) ?> - </a> - </span> - </nav> - - <div class="table-scrollbox-horizontal"> - <table class="calendar-single-year--table" width="100%"> - <? $days_per_month = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - if (date('L', $calendar->getStart())) { - $days_per_month[2]++; - } - ?> - - <thead> - <tr> - <? $ts_month = 0; ?> - <? for ($i = 1; $i < 13; $i++) : ?> - <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?> - <th align="center" width="8%"> - <a class="calhead" href="<?= $controller->url_for('calendar/single/month', ['atime' => $calendar->getStart() + $ts_month]) ?>"> - <b><?= strftime('%B', $ts_month); ?></b> - </a> - </th> - <? endfor; ?> - </tr> - </thead> - - <tbody> - <? - $now = date('Ymd'); - $count = 0; - ?> - <? for ($i = 1; $i < 32; $i++) : ?> - <tr> - <? for ($month = 1; $month < 13; $month++) : ?> - - <? $aday = mktime(12, 0, 0, $month, $i, date('Y', $calendar->getStart())); ?> - <? $iday = date('Ymd', $aday); ?> - <? if ($i <= $days_per_month[$month]) : ?> - <? $wday = date('w', $aday); - // emphasize current day - if (date('Ymd', $aday) == $now) { - $day_class = ' class="celltoday"'; - } else if ($wday == 0 || $wday == 6) { - $day_class = ' class="weekend"'; - } else { - $day_class = ' class="weekday"'; - } - ?> - - <td <?= $day_class ?> <?= $month == 1 ? 'height="25"' : '' ?>> - - <? if (isset($count_list[$iday]) && count($count_list[$iday])) : ?> - <table width="100%" cellspacing="0" cellpadding="0"> - <tr> - <td<?= $day_class ?>> - <? endif; ?> - - <? $weekday = strftime('%a', $aday); ?> - - <span class="yday"> - <? $hday = holiday($aday); ?> - <? if (is_array($hday)) : ?> - <? if ($hday['col'] == '1') : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? elseif ($hday['col'] == '2' || $hday['col'] == '3') : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="hday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? endif ?> - <? else : ?> - <? if (date('w', $aday) == '0') : ?> - <a style="font-weight:bold;" class="sday" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? $count++; ?> - <? else : ?> - <a style="font-weight:bold;" class="day" href="<?= $controller->url_for('calendar/single/day', ['atime' => $aday]) ?>"><?= $i ?></a> <?= $weekday; ?> - <? endif; ?> - <? endif; ?> - </span> - - <? if (isset($count_list[$iday]) && count($count_list[$iday])) : ?> - <? $event_count_txt = sprintf(ngettext('1 Termin', '%s Termine', count($count_list[$iday])), count($count_list[$iday])) ?> - </td> - <td<?= $day_class ?> align="right"> - <?= Icon::create('date', 'clickable', ['title' => $event_count_txt])->asImg(16, ["alt" => $event_count_txt]); ?> - </td> - </tr> - </table> - <? endif; ?> - - </td> - - <? else : ?> - <td class="weekday"> </td> - <? endif; ?> - - <? endfor; ?> - </tr> - <? endfor; ?> - </tbody> - - <tfoot> - <tr> - <? $ts_month = 0; ?> - <? for ($i = 1; $i < 13; $i++) : ?> - <? $ts_month += ( $days_per_month[$i] - 1) * 86400; ?> - <th align="center" width="8%"> - <a class="calhead" href="<?= $controller->url_for('calendar/single/month', ['atime' => $calendar->getStart() + $ts_month]) ?>"> - <b><?= strftime('%B', $ts_month); ?></b> - </a> - </th> - <? endfor; ?> - </tr> - </tfoot> - </table> - </div> -</div> diff --git a/app/views/course/cancel_dates/index.php b/app/views/course/cancel_dates/index.php index e427849a49c18eaf52ca19a37ca3fe21d81ec470..3c092bc90fb465d145b63143af904bb13d4e765a 100644 --- a/app/views/course/cancel_dates/index.php +++ b/app/views/course/cancel_dates/index.php @@ -12,7 +12,7 @@ <label> <?= _('Kommentar') ?> - <?= tooltipIcon(_('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Terminkalender eingeblendet.')) ?> + <?= tooltipIcon(_('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Kalender angezeigt.')) ?> <textarea wrap="virtual" name="cancel_dates_comment" id="cancel_dates_comment"></textarea> </label> <label> diff --git a/app/views/course/dates/details-edit.php b/app/views/course/dates/details-edit.php index 2fec69cb07be8a5072abc8914be2d899cfe448f9..0f91d7226300369ad20c49fdbf69028ffc3574d7 100644 --- a/app/views/course/dates/details-edit.php +++ b/app/views/course/dates/details-edit.php @@ -1,4 +1,5 @@ -<form name="edit_termin" action='<?= $controller->url_for('course/dates/save_details/' . $date->id) ?>' method="post" class="default" data-termin-id="<?= htmlReady($date->id) ?>"> +<form name="edit_termin" action='<?= $controller->url_for('course/dates/save_details/' . $date->id) ?>' method="post" class="default" data-termin-id="<?= htmlReady($date->id) ?>" + data-course-id="<?= htmlReady(Context::getID()) ?>"> <?= CSRFProtection::tokenTag() ?> <fieldset> @@ -93,6 +94,17 @@ </div> </fieldset> <? endif; ?> +<? if (!empty($date->room_booking->resource)) : ?> + <? $room = $date->room_booking->resource->getDerivedClassInstance() ?> + <? if ($room instanceof Resource) : ?> + <fieldset> + <legend><?= _('Raum') ?></legend> + <section> + <?= htmlReady($room->getFullName()) ?> + </section> + </fieldset> + <? endif ?> +<? endif ?> <? if (count($groups) > 0): ?> <fieldset> @@ -158,6 +170,12 @@ ['data-dialog' => ''] ) ?> <? endif ?> + <? if (Request::submitted('extra_buttons')) : ?> + <?= Studip\LinkButton::create( + _('Zur Veranstaltung'), + $controller->url_for('course/details', ['cid' => $date->range_id]) + ) ?> + <? endif ?> </footer> </form> diff --git a/app/views/course/dates/details.php b/app/views/course/dates/details.php index 5a772682a6cd47696c6df232a54164a1d53ac7c3..96f7e2eb7da9dc89b935482c565e71959702af16 100644 --- a/app/views/course/dates/details.php +++ b/app/views/course/dates/details.php @@ -39,6 +39,17 @@ </td> </tr> <? endif; ?> + <? if (!empty($date->room_booking->resource)) : ?> + <? $room = $date->room_booking->resource->getDerivedClassInstance() ?> + <? if ($room instanceof Resource) : ?> + <tr> + <td><strong><?= _('Raum') ?></strong></td> + <td> + <?= htmlReady($room->getFullName()) ?> + </td> + </tr> + <? endif ?> + <? endif ?> <? if (count($date->statusgruppen) > 0): ?> <tr> <td><strong><?= _('Beteiligte Gruppen') ?></strong></td> @@ -96,3 +107,8 @@ STUDIP.Table.enhanceSortableTable($('#course_date_files')); </script> <? endif; ?> +<? if (Request::bool('extra_buttons') && $GLOBALS['perm']->have_studip_perm('user', $course->id)) : ?> + <div data-dialog-button> + <?= \Studip\LinkButton::create(_('Zur Veranstaltung'), $controller->url_for('course/details', ['cid' => $course->id])) ?> + </div> +<? endif ?> diff --git a/app/views/course/details/index.php b/app/views/course/details/index.php index 15cee6ca6cd142e9c1218b7eb6809a6c041bf520..583f715b934c41c4da7b189e63452dfe792ea6e3 100644 --- a/app/views/course/details/index.php +++ b/app/views/course/details/index.php @@ -522,4 +522,9 @@ if (!empty($mvv_tree)) : ?> <? endif ?> </footer> <? endif ?> -<?= Feedback::getHTML($course->id, Course::class) ?> +<? if (Request::bool('link_to_course') && $GLOBALS['perm']->have_studip_perm('autor', $course->id)) : ?> + <footer data-dialog-button> + <?= \Studip\LinkButton::create(_('Direkt zur Veranstaltung'), URLHelper::getURL('dispatch.php/course/overview', ['cid' => $course->id]))?> + </footer> +<? endif ?> +<?= Feedback::getHTML($course->id, 'Course'); ?> diff --git a/app/views/course/timesrooms/_cancel_form.php b/app/views/course/timesrooms/_cancel_form.php index eaadaf19d6b8a30899482f70084b30f3cb3be71f..ee59d8933f0dd4c0b4ecf8deb7b110f13e2a3f0a 100644 --- a/app/views/course/timesrooms/_cancel_form.php +++ b/app/views/course/timesrooms/_cancel_form.php @@ -6,7 +6,7 @@ if (isset($termin) && $termin instanceof CourseExDate) { } ?> <p> - <strong> <?= _('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Terminkalender eingeblendet.') ?></strong> + <strong> <?= _('Wenn Sie die nicht stattfindenden Termine mit einem Kommentar versehen, werden die Ausfalltermine im Ablaufplan weiterhin dargestellt und auch im Kalender angezeigt.') ?></strong> </p> <label for="cancel_comment"> diff --git a/app/views/institute/overview/index.php b/app/views/institute/overview/index.php index 3543d665eec66da0f9d481180c882033e11a89bc..cb347b002d5e2abe30894568ed2762a7f531eb8b 100644 --- a/app/views/institute/overview/index.php +++ b/app/views/institute/overview/index.php @@ -50,7 +50,6 @@ </article> <?= $news ?> -<?= $dates ?> <?= $evaluations ?> <?= $questionnaires ?> diff --git a/app/views/institute/schedule/index.php b/app/views/institute/schedule/index.php new file mode 100644 index 0000000000000000000000000000000000000000..23602dbb62490dc895f4651d4220c631f8af4c01 --- /dev/null +++ b/app/views/institute/schedule/index.php @@ -0,0 +1 @@ +<?= $fullcalendar ?> diff --git a/app/views/settings/calendar.php b/app/views/settings/calendar.php index 6833a0a0f7ce597ebca354337ceb09fc8228ebfe..889120ad72af01ec907c516095630ba012ebbff2 100644 --- a/app/views/settings/calendar.php +++ b/app/views/settings/calendar.php @@ -4,8 +4,7 @@ use Studip\Button, Studip\LinkButton; $cal_views = [ 'day' => _('Tagesansicht'), 'week' => _('Wochenansicht'), - 'month' => _('Monatsansicht'), - 'year' => _('Jahresansicht'), + 'month' => _('Monatsansicht') ]; $cal_deletes = [ 12 => _('12 Monate nach Ablauf'), @@ -27,18 +26,19 @@ $cal_step_weeks = [ ]; ?> -<form method="post" action="<?= $controller->url_for('settings/calendar/store') ?>" class="default"> +<form method="post" action="<?= $controller->link_for('settings/calendar/store') ?>" class="default" + <?= Request::isDialog() ? 'data-dialog="reload-on-close"' : '' ?>> <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>"> <?= CSRFProtection::tokenTag() ?> <fieldset> <legend> - <?= _('Einstellungen des Terminkalenders') ?> + <?= _('Einstellungen des Kalenders') ?> </legend> <label> <?= _('Startansicht') ?> - <select name="cal_view" id="cal_view" size="1"> + <select name="cal_view" id="cal_view"> <? foreach ($cal_views as $index => $label): ?> <option value="<?= $index ?>" <? if ($view == $index) echo 'selected'; ?>> <?= $label ?> @@ -48,15 +48,14 @@ $cal_step_weeks = [ </label> <label> - <?= _('Wochenansicht') ?> - <select name="cal_type_week"> - <option value="LONG"<?= $type_week == 'LONG' ? ' selected' : "" ?>> - <?= _('7 Tage-Woche') ?> - </option> - <option value="SHORT"<?= $type_week == 'SHORT' ? ' selected' : "" ?>> - <?= _('5 Tage-Woche') ?> - </option> - </select> + <input type="radio" name="cal_type_week" value="LONG" + <?= $type_week == 'LONG' ? 'checked' : "" ?>> + <?= _('Alle Wochentage in der Wochenansicht anzeigen.') ?> + </label> + <label> + <input type="radio" name="cal_type_week" value="SHORT" + <?= $type_week == 'SHORT' ? 'checked' : "" ?>> + <?= _('Nur Montag bis Freitag in der Wochenansicht anzeigen.') ?> </label> </fieldset> @@ -65,34 +64,27 @@ $cal_step_weeks = [ <?= _('Einzelterminkalender') ?> </legend> - <div> - <?= _('Zeitraum der Tages- und Wochenansicht') ?> - <section class="hgroup"> - <label> - <?= _("Von") ?> - <select name="cal_start" aria-label="<?= _('Startzeit der Tages- und Wochenansicht') ?>" class="size-s"> - <? for ($i = 0; $i < 24; $i += 1): ?> - <option value="<?= $i ?>" <? if ($start == $i) echo 'selected'; ?>> - <?= sprintf('%02u:00', $i) ?> - </option> - <? endfor; ?> - </select> - <?= _("Uhr") ?> - </label> - - <label> - <?= _("Bis") ?> - <select name="cal_end" aria-label="<?= _('Endzeit der Tages- und Wochenansicht') ?>" class="size-s"> - <? for ($i = 0; $i < 24; $i += 1): ?> - <option value="<?= $i ?>" <? if ($end == $i) echo 'selected'; ?>> - <?= sprintf('%02u:00', $i) ?> - </option> - <? endfor; ?> - </select> - <?= _("Uhr") ?>. - </label> - </section> - </div> + <label> + <?= _('Startuhrzeit') ?> + <select name="cal_start" aria-label="<?= _('Startzeit der Tages- und Wochenansicht') ?>" class="size-s"> + <? for ($i = 0; $i < 24; $i += 1): ?> + <option value="<?= $i ?>" <? if ($start == $i) echo 'selected'; ?>> + <?= sprintf(_('%02u:00 Uhr'), $i) ?> + </option> + <? endfor; ?> + </select> + </label> + + <label> + <?= _('Enduhrzeit') ?> + <select name="cal_end" aria-label="<?= _('Endzeit der Tages- und Wochenansicht') ?>" class="size-s"> + <? for ($i = 0; $i < 24; $i += 1): ?> + <option value="<?= $i ?>" <? if ($end == $i) echo 'selected'; ?>> + <?= sprintf(_('%02u:00 Uhr'), $i) ?> + </option> + <? endfor; ?> + </select> + </label> <label> <?= _('Zeitintervall der Tagesansicht') ?> @@ -153,7 +145,7 @@ $cal_step_weeks = [ </fieldset> <? endif ?> - <footer> + <footer data-dialog-button> <? if (Request::option('atime')): ?> <input type="hidden" name="atime" value="<?= Request::option('atime') ?>"> <? endif ?> diff --git a/app/views/settings/general.php b/app/views/settings/general.php index db1d01d3cc4a903517abf1d351c78f86071eba91..4bd543388973171c5cbced5bd4df62508a5fa35b 100644 --- a/app/views/settings/general.php +++ b/app/views/settings/general.php @@ -3,7 +3,7 @@ $start_pages = [ '' => _('keine'), 1 => _('Meine Veranstaltungen'), 3 => _('Mein Stundenplan'), - 5 => _('Mein Terminkalender'), + 5 => _('Mein Kalender'), 4 => _('Mein Adressbuch'), 6 => _('Mein globaler Blubberstream'), 7 => _('Mein Arbeitsplatz'), diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index 3ef8fb60fa1ae0a05ff5a0ff896abcdaaeae0cfc..156224897e5ffbd5e370070c4f621a033e0807ef 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -205,23 +205,112 @@ $TERMIN_TYP[7]=array("name"=>_("Vorlesung"), "sitzung"=>1); // more types can be added here -// Configure the categories for the personal calendar -$PERS_TERMIN_KAT[1]=array("name"=>_("Sonstiges")); -$PERS_TERMIN_KAT[2]=array("name"=>_("Sitzung")); -$PERS_TERMIN_KAT[3]=array("name"=>_("Vorbesprechung")); -$PERS_TERMIN_KAT[4]=array("name"=>_("Klausur")); -$PERS_TERMIN_KAT[5]=array("name"=>_("Exkursion")); -$PERS_TERMIN_KAT[6]=array("name"=>_("Sondersitzung")); -$PERS_TERMIN_KAT[7]=array("name"=>_("Prüfung")); -$PERS_TERMIN_KAT[8]=array("name"=>_("Telefonat")); -$PERS_TERMIN_KAT[9]=array("name"=>_("Besprechung")); -$PERS_TERMIN_KAT[10]=array("name"=>_("Verabredung")); -$PERS_TERMIN_KAT[11]=array("name"=>_("Geburtstag")); -$PERS_TERMIN_KAT[12]=array("name"=>_("Familie")); -$PERS_TERMIN_KAT[13]=array("name"=>_("Urlaub")); -$PERS_TERMIN_KAT[14]=array("name"=>_("Reise")); -$PERS_TERMIN_KAT[15]=array("name"=>_("Vorlesung")); -// more categories can be added here +//Configuration for the date categories in the personal calendar: +$PERS_TERMIN_KAT = [ + '1' => [ + 'name' => _('Sonstiges'), + 'border_color' => '#682C8B', + 'bgcolor' => '#682C8B', + 'fgcolor' => '#ffffff' + ], + '2' => [ + 'name' => _('Sitzung'), + 'border_color' => '#B02E7C', + 'bgcolor' => '#B02E7C', + 'fgcolor' => '#000000' + ], + '3' => [ + 'name' => _('Vorbesprechung'), + 'border_color' => '#D60000', + 'bgcolor' => '#D60000', + 'fgcolor' => '#ffffff' + ], + '4' => [ + 'name' => _('Klausur'), + 'border_color' => '#F26E00', + 'bgcolor' => '#F26E00', + 'fgcolor' => '#000000' + ], + '5' => [ + 'name' => _('Exkursion'), + 'border_color' => '#FFBD33', + 'bgcolor' => '#FFBD33', + 'fgcolor' => '#000000' + ], + '6' => [ + 'name' => _('Sondersitzung'), + 'border_color' => '#6EAD10', + 'bgcolor' => '#6EAD10', + 'fgcolor' => '#000000' + ], + '7' => [ + 'name' => _('Prüfung'), + 'border_color' => '#008512', + 'bgcolor' => '#008512', + 'fgcolor' => '#000000' + ], + '8' => [ + 'name' => _('Telefonat'), + 'border_color' => '#129C94', + 'bgcolor' => '#129C94', + 'fgcolor' => '#000000' + ], + '9' => [ + 'name' => _('Besprechung'), + 'border_color' => '#A85D45', + 'bgcolor' => '#A85D45', + 'fgcolor' => '#000000' + ], + '10' => [ + 'name' => _('Verabredung'), + 'border_color' => '#A480B9', + 'bgcolor' => '#A480B9', + 'fgcolor' => '#000000' + ], + '11' => [ + 'name' => _('Geburtstag'), + 'border_color' => '#D082B0', + 'bgcolor' => '#D082B0', + 'fgcolor' => '#000000' + ], + '12' => [ + 'name' => _('Familie'), + 'border_color' => '#E76666', + 'bgcolor' => '#E76666', + 'fgcolor' => '#000000' + ], + '13' => [ + 'name' => _('Urlaub'), + 'border_color' => '#F7A866', + 'bgcolor' => '#F7A866', + 'fgcolor' => '#000000' + ], + '14' => [ + 'name' => _('Reise'), + 'border_color' => '#FFD785', + 'bgcolor' => '#FFD785', + 'fgcolor' => '#000000' + ], + '15' => [ + 'name' => _('Vorlesung'), + 'border_color' => '#A8CE70', + 'bgcolor' => '#A8CE70', + 'fgcolor' => '#000000' + ], + '16' => [ + 'name' => _('Videokonferenz'), + 'border_color' => '#8bbd40', + 'bgcolor' => '#8bbd40', + 'fgcolor' => '#000000' + ], + '255' => [ + 'name' => _('Sonstige'), + 'border_color' => '#A7ABAF', + 'bgcolor' => '#A7ABAF', + 'fgcolor' => '#000000' + ] + //More categories can be added here. +]; //preset for academic titles - add further titles to the array, if necessary $TITLE_FRONT_TEMPLATE = array("","Prof.","Prof. Dr.","Dr.","PD Dr.","Dr. des.","Dr. med.","Dr. rer. nat.","Dr. forest.", diff --git a/db/migrations/1.160_step_00283_update_calendar_settings.php b/db/migrations/1.160_step_00283_update_calendar_settings.php index f54956bf19893a5b7ea3aacb98b9c1ed6b2ebb7c..82a15849c7ef3b8809e0b4c16a28da3b1dea4d4f 100644 --- a/db/migrations/1.160_step_00283_update_calendar_settings.php +++ b/db/migrations/1.160_step_00283_update_calendar_settings.php @@ -17,7 +17,17 @@ class Step00283UpdateCalendarSettings extends Migration { 'showmonth' => 'month', 'showyear' => 'year']; $res = DBManager::get()->query("SELECT user_id FROM `user_config` WHERE field = 'CALENDAR_SETTINGS'"); - $default_settings = Calendar::getDefaultUserSettings(); + $default_settings = [ + 'view' => 'week', + 'start' => '9', + 'end' => '20', + 'step_day' => '900', + 'step_week' => '1800', + 'type_week' => 'LONG', + 'step_week_group' => '3600', + 'step_day_group' => '3600', + 'show_declined' => '0' + ]; Config::get()->store('CALENDAR_SETTINGS', $default_settings); foreach ($res as $row) { $config = new UserConfig($row['user_id']); diff --git a/db/migrations/5.4.1.1_alter_calendar_tables.php b/db/migrations/5.4.1.1_alter_calendar_tables.php new file mode 100644 index 0000000000000000000000000000000000000000..b1f477b0c1c8374b83b430f55ad2566102ca7326 --- /dev/null +++ b/db/migrations/5.4.1.1_alter_calendar_tables.php @@ -0,0 +1,247 @@ +<?php + + +class AlterCalendarTables extends Migration +{ + public function description() + { + return 'Alters the tables for the personal calendar and related tables.'; + } + + + protected function migrateEventData() + { + $db = DBManager::get(); + + $db->exec("RENAME TABLE `event_data` TO calendar_dates"); + + //Move the content of the "day" column into the "offset" column + //which is still called "sinterval" at this point: + $db->exec( + "UPDATE `calendar_dates` + SET `sinterval` = `day` + WHERE `day` <> ''" + ); + + $db->exec( + "ALTER TABLE `calendar_dates` + DROP COLUMN `ts`, + DROP COLUMN `duration`, + DROP COLUMN `priority`, + DROP COLUMN `day`, + CHANGE COLUMN event_id id CHAR(32) COLLATE latin1_bin NOT NULL, + CHANGE COLUMN uid unique_id VARCHAR(255) UNIQUE NOT NULL, + CHANGE COLUMN start begin INT(11) NOT NULL DEFAULT 0, + CHANGE COLUMN end end INT(11) NOT NULL DEFAULT 0, + CHANGE COLUMN summary title VARCHAR(255) NOT NULL DEFAULT '', + CHANGE COLUMN class access ENUM('PUBLIC', 'PRIVATE', 'CONFIDENTIAL') COLLATE latin1_bin NOT NULL DEFAULT 'PRIVATE', + CHANGE COLUMN categories user_category VARCHAR(64) NULL DEFAULT '', + CHANGE COLUMN category_intern category TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', + CHANGE COLUMN location location VARCHAR(255) NULL DEFAULT '', + CHANGE COLUMN linterval `interval` TINYINT(2) NULL DEFAULT 0, + CHANGE COLUMN sinterval `offset` TINYINT(2) NULL DEFAULT 0, + CHANGE COLUMN wdays days VARCHAR(7) NULL DEFAULT '', + CHANGE COLUMN rtype repetition_type ENUM('SINGLE', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY') DEFAULT 'SINGLE', + CHANGE COLUMN `count` number_of_dates SMALLINT(5) UNSIGNED NOT NULL DEFAULT '1', + CHANGE COLUMN `expire` repetition_end BIGINT(10) NOT NULL DEFAULT '0', + CHANGE COLUMN mkdate mkdate INT(11) UNSIGNED NOT NULL DEFAULT 0, + CHANGE COLUMN chdate chdate INT(11) UNSIGNED NOT NULL DEFAULT 0, + CHANGE COLUMN importdate import_date INT(11) NOT NULL DEFAULT 0" + ); + + $get_stmt = $db->prepare("SELECT `id`, `exceptions` FROM `calendar_dates`"); + $exception_stmt = $db->prepare( + "INSERT INTO `calendar_date_exceptions` + (`calendar_date_id`, `date`, `mkdate`, `chdate`) + VALUES + (:calendar_date_id, :date, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())" + ); + $get_stmt->execute(); + while ($row = $get_stmt->fetch()) { + //Migrate exceptions: + $exceptions = explode(',', $row['exceptions'] ?? ''); + foreach ($exceptions as $exception) { + $exception_stmt->execute([ + 'calendar_date_id' => $row['id'], + 'date' => date('Y-m-d', intval(trim($exception))) + ]); + } + } + + $db->exec( + "ALTER TABLE `calendar_dates` DROP COLUMN `exceptions`" + ); + } + + + protected function migrateCalendarEvent() + { + $db = DBManager::get(); + + $db->exec( + "RENAME TABLE `calendar_event` TO calendar_date_assignments" + ); + + $db->exec( + "ALTER TABLE `calendar_date_assignments` + ADD COLUMN participation ENUM('', 'ACCEPTED', 'DECLINED', 'ACKNOWLEDGED') COLLATE latin1_bin NOT NULL DEFAULT '', + CHANGE COLUMN event_id calendar_date_id CHAR(32) COLLATE latin1_bin NOT NULL, + CHANGE COLUMN group_status old_group_status TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + CHANGE COLUMN mkdate mkdate INT(11) NOT NULL DEFAULT 0, + CHANGE COLUMN chdate chdate INT(11) NOT NULL DEFAULT 0" + ); + + $db->exec( + "UPDATE `calendar_date_assignments` + SET `participation` = IF ( + `old_group_status` = '2', + 'ACCEPTED', + IF (`old_group_status` = '3', + 'DECLINED', + IF (`old_group_status` = '4', + 'ACKNOWLEDGED', + '' + ) + ) + )" + ); + + $db->exec("ALTER TABLE `calendar_date_assignments` DROP COLUMN `old_group_status`"); + } + + + protected function migrateCalendarUser() + { + //All entries from calendar_user are transferred to the contacts table + //which gets an extra column so that it can store the calendar access level. + $db = DBManager::get(); + + $db->exec( + "ALTER TABLE `contact` + CHANGE COLUMN mkdate mkdate INT(11) NOT NULL DEFAULT 0, + ADD COLUMN chdate INT(11) NOT NULL DEFAULT 0, + ADD COLUMN calendar_permissions ENUM('', 'READ', 'WRITE') COLLATE latin1_bin NOT NULL DEFAULT ''" + ); + + $db->exec( + "INSERT INTO `contact` + (`owner_id`, `user_id`, `calendar_permissions`, `mkdate`, `chdate`) + SELECT `owner_id`, `user_id`, + IF(`permission` = '4', 'WRITE', IF(`permission` = '2', 'READ', '')) AS calendar_permissions, + `mkdate`, `chdate` + FROM `calendar_user` + ON DUPLICATE KEY UPDATE `calendar_permissions` = calendar_permissions" + ); + + $db->exec("DROP TABLE `calendar_user`"); + } + + + protected function addContactGroups() + { + $db = DBManager::get(); + + $db->exec( + "CREATE TABLE IF NOT EXISTS `contact_groups` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `owner_id` CHAR(32) COLLATE latin1_bin NOT NULL, + `old_group_id` CHAR(32) COLLATE latin1_bin NOT NULL, + `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) + )" + ); + $db->exec( + "CREATE TABLE IF NOT EXISTS `contact_group_items` ( + `group_id` BIGINT UNSIGNED NOT NULL, + `user_id` CHAR(32) COLLATE latin1_bin NOT NULL, + `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY(`group_id`, `user_id`) + )" + ); + + //Migrate entries from statusgruppen and statusgruppe_user: + $old_groups = $db->query( + "SELECT `statusgruppe_id`, `name`, `range_id`, `mkdate`, `chdate` + FROM `statusgruppen` + WHERE `range_id` IN ( + SELECT `user_id` FROM `auth_user_md5` + )" + )->fetchAll(PDO::FETCH_ASSOC); + + $new_group_stmt = $db->prepare( + "INSERT INTO `contact_groups` + (`name`, `owner_id`, `old_group_id`, `mkdate`, `chdate`) + VALUES (:name, :user_id, :old_group_id, :mkdate, :chdate)" + ); + + $group_member_stmt = $db->prepare( + "INSERT INTO `contact_group_items` + (`group_id`, `user_id`, `mkdate`, `chdate`) + SELECT `contact_groups`.`id` AS group_id, `user_id`, `statusgruppe_user`.`mkdate` as mkdate, `statusgruppe_user`.`mkdate` AS chdate + FROM `statusgruppe_user` + INNER JOIN `contact_groups` + ON `statusgruppe_user`.`statusgruppe_id` = `contact_groups`.`old_group_id` + WHERE `statusgruppe_id` = :old_group_id" + ); + $old_member_delete_stmt = $db->prepare("DELETE FROM `statusgruppe_user` WHERE `statusgruppe_id` = :old_group_id"); + + foreach ($old_groups as $old_group) { + $new_group_stmt->execute([ + 'name' => $old_group['name'], + 'user_id' => $old_group['range_id'], + 'old_group_id' => $old_group['statusgruppe_id'], + 'mkdate' => $old_group['mkdate'], + 'chdate' => $old_group['chdate'] + ]); + $group_member_stmt->execute([ + 'old_group_id' => $old_group['statusgruppe_id'] + ]); + $old_member_delete_stmt->execute([ + 'old_group_id' => $old_group['statusgruppe_id'] + ]); + } + + //Delete old status groups: + $db->exec( + "DELETE FROM `statusgruppen` WHERE `range_id` IN ( + SELECT `user_id` FROM `auth_user_md5` + )" + ); + + //Delete the old group ID: + $db->exec("ALTER TABLE `contact_groups` DROP COLUMN `old_group_id`"); + } + + protected function up() + { + $db = DBManager::get(); + + $db->exec( + "CREATE TABLE IF NOT EXISTS `calendar_date_exceptions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `calendar_date_id` CHAR(32) COLLATE latin1_bin NOT NULL, + `date` DATE NOT NULL, + `mkdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + `chdate` INT(11) UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) + )" + ); + + $this->migrateEventData(); + + $this->migrateCalendarEvent(); + + $this->migrateCalendarUser(); + + $this->addContactGroups(); + } + + + protected function down() + { + //I see nothing, I hear nothing, I know nothing! NOTHING!! + } +} diff --git a/db/studip_default_data.sql b/db/studip_default_data.sql index 96bb42b532233e0d408ab75a8c5065aba467bddc..df7ee8ba6b47fa93a763d4f842e03430044290d6 100644 --- a/db/studip_default_data.sql +++ b/db/studip_default_data.sql @@ -162,7 +162,7 @@ INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `c INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('BLUBBER_GLOBAL_MESSENGER_ACTIVATE', '1', 'boolean', 'global', 'global', 1591630778, 1591630778, 'Ist Blubber unter Community global aktiv? Blubber in Veranstaltungen wird über das Plugin Blubber aktiviert oder deaktiviert.'); INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('BLUBBER_GLOBAL_THREAD_OPTOUT', '1', 'boolean', 'global', 'global', 1640797278, 1640797278, 'Gibt an, ob beim globalen Blubber Thread ein Opt-Out-Verfahren genutzt werden soll'); INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_ENABLE', '1', 'boolean', 'global', 'calendar', 1293118059, 1293118059, 'Schaltet ein oder aus, ob der Kalender global verfügbar ist.'); -INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GRANT_ALL_INSERT', '0', 'boolean', 'global', 'calendar', 1462287762, 1462287762, 'Ermöglicht das Eintragen von Terminen in alle Nutzerkalender, ohne Beachtung des Rechtesystems.'); +INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GRANT_ALL_INSERT', '1', 'boolean', 'global', 'calendar', 1462287762, 1462287762, 'Ermöglicht das Eintragen von Terminen in alle Kalender der Nutzenden, ohne Beachtung des Rechtesystems.'); INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_GROUP_ENABLE', '0', 'boolean', 'global', 'calendar', 1326799692, 1326799692, 'Schaltet die Gruppenterminkalender-Funktionen ein.'); INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CALENDAR_SETTINGS', '{\"view\":\"week\",\"start\":\"9\",\"end\":\"20\",\"step_day\":\"900\",\"step_week\":\"1800\",\"type_week\":\"LONG\",\"step_week_group\":\"3600\",\"step_day_group\":\"3600\"}', 'array', 'user', '', 1403258015, 1403258015, 'persönliche Einstellungen des Kalenders'); INSERT INTO `config` (`field`, `value`, `type`, `range`, `section`, `mkdate`, `chdate`, `description`) VALUES('CONSULTATION_ALLOW_DOCENTS_RESERVING', '1', 'boolean', 'global', 'Terminvergabe', 1557244743, 1557244743, 'Lehrende können sich bei anderen Lehrenden anmelden'); diff --git a/lib/bootstrap-autoload.php b/lib/bootstrap-autoload.php index 69910b24f39553b80681a48f5f432fff01728d55..6f3f4a713f2be5564a86948f3369518fdc9325b4 100644 --- a/lib/bootstrap-autoload.php +++ b/lib/bootstrap-autoload.php @@ -8,6 +8,7 @@ StudipAutoloader::register(); // General classes folders StudipAutoloader::addAutoloadPath('lib/models'); +StudipAutoloader::addAutoloadPath('lib/models/calendar'); StudipAutoloader::addAutoloadPath('lib/models/resources'); StudipAutoloader::addAutoloadPath('lib/classes'); StudipAutoloader::addAutoloadPath('lib/classes', 'Studip'); diff --git a/lib/calendar/CalendarColumn.class.php b/lib/calendar/CalendarColumn.class.php index 3c071f1760855f764f45af7833d752bd2732b71f..cc3abea5310f239cb32e63f58932a79c4c407b97 100644 --- a/lib/calendar/CalendarColumn.class.php +++ b/lib/calendar/CalendarColumn.class.php @@ -13,6 +13,8 @@ * @author Rasmus Fuhse <fuhse@data-quest.de> * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP + * + * @deprecated since Stud.IP 5.5 */ class CalendarColumn diff --git a/lib/calendar/CalendarExport.class.php b/lib/calendar/CalendarExport.class.php deleted file mode 100644 index c9c10f6aac8ccf869d9c8127387c5a7a2128bc69..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarExport.class.php +++ /dev/null @@ -1,87 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarExport.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarExport -{ - protected $_writer; - protected $export; - private $count; - - public function __construct(&$writer) - { - $this->_writer = $writer; - } - - public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = NULL) - { - global $_calendar_error, $user; - - if (!$range_id) { - $range_id = $user->id; - } - $calendar = new SingleCalendar($range_id); - - $this->_export($this->_writer->writeHeader()); - $calendar->getEvents($event_types, $start, $end); - - foreach ($calendar->events as $event) { - $this->_export($this->_writer->write($event)); - } - $this->count = sizeof($calendar->events); - - $this->_export($this->_writer->writeFooter()); - } - - public function exportFromObjects($events) - { - global $_calendar_error; - - $this->_export($this->_writer->writeHeader()); - - $this->count = 0; - foreach ($events as $event) { - $this->_export($this->_writer->write($event)); - $this->count++; - } - - if (!sizeof($events)) { - $message = _('Es wurden keine Termine exportiert.'); - } else { - $message = sprintf(ngettext('Es wurde 1 Termin exportiert', 'Es wurden %s Termine exportiert', sizeof($events)), sizeof($events)); - } - - $this->_export($this->_writer->writeFooter()); - } - - public function _export($exp) - { - if (!empty($exp)) { - $this->export[] = $exp; - } - } - - public function getExport() - { - return $this->export; - } - - public function getCount() - { - return $this->count; - } -} diff --git a/lib/calendar/CalendarExportException.class.php b/lib/calendar/CalendarExportException.class.php deleted file mode 100644 index fbdabda7d47bceac93695b56535c8978ac594945..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarExportException.class.php +++ /dev/null @@ -1,18 +0,0 @@ -<?php -/** - * CalendarExportException.php - indicates an error during - * calendar export or import - * - * 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP -*/ - -class CalendarExportException extends Exception -{ -} diff --git a/lib/calendar/CalendarExportFile.class.php b/lib/calendar/CalendarExportFile.class.php deleted file mode 100644 index 1e0f3fb898fd8cacd9aa29af794bd96b83df329e..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarExportFile.class.php +++ /dev/null @@ -1,122 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarExportFile.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarExportFile extends CalendarExport -{ - private $file_name = 'studip'; - private $tmp_file_name; - private $path; - - public function __construct(&$writer, $path = null, $file_name = null) - { - global $TMP_PATH; - - parent::__construct($writer); - - if (!$file_name) { - $this->tmp_file_name = $this->makeUniqueFilename(); - $this->file_name .= '.' . $writer->getDefaultFileNameSuffix(); - } else { - $this->file_name = $file_name; - $this->tmp_file_name = $file_name; - } - - if (!$path) { - $this->path = $TMP_PATH . '/'; - } - - $this->_writer = $writer; - } - - public function exportFromDatabase($range_id = null, $start = 0, $end = Calendar::CALENDAR_END, $event_types = null, $except = null) - { - $this->_createFile(); - parent::exportFromDatabase($range_id, $start, $end, $event_types, $except); - $this->_closeFile(); - } - - public function exportFromObjects($events) - { - $this->_createFile(); - parent::exportFromObjects($events); - $this->_closeFile(); - } - - public function sendFile() - { - if (file_exists($this->path . $this->tmp_file_name)) { - header('Location: ' . FileManager::getDownloadURLForTemporaryFile($this->tmp_file_name, $this->file_name)); - } else { - throw new CalendarExportException(_('Die Export-Datei konnte nicht erstellt werden!')); - } - } - - public function makeUniqueFileName() - { - return md5(uniqid(rand() . "Stud.IP Calendar")); - } - - // returns file handle - public function getExport() - { - return $this->export; - } - - public function getFileName() - { - return $this->file_name; - } - - public function getTempFileName() - { - return $this->tmp_file_name; - } - - public function _createFile() - { - if (!(is_dir($this->path))) { - if (!mkdir($this->path)) { - var_dump($this->path); exit; - throw new CalendarExportException(_('Das Export-Verzeichnis konnte nicht angelegt werden!')); - } else { - if (!chmod($this->path, 0777)) { - throw new CalendarExportException(_('Die Zugriffsrechte auf das Export-Verzeichnis konnten nicht geändert werden!')); - } - } - } - if (file_exists($this->path . $this->tmp_file_name)) { - if (!unlink($this->path . $this->tmp_file_name)) { - throw new CalendarExportException(_('Eine bestehende Export-Datei konnte nicht gelöscht werden!')); - } - } - $this->export = fopen($this->path . $this->tmp_file_name, "wb"); - if (!$this->export) { - throw new CalendarExportException(_("Die Export-Datei konnte nicht erstellt werden!")); - } - } - - public function _export($exp) - { - fwrite($this->export, $exp); - } - - public function _closeFile() - { - fclose($this->export); - } -} diff --git a/lib/calendar/CalendarImport.class.php b/lib/calendar/CalendarImport.class.php deleted file mode 100644 index 803a58dd3f29ec7be3b2786941ddf35c9e256116..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarImport.class.php +++ /dev/null @@ -1,91 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarImport.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarImport -{ - - const IGNORE_ERRORS = 1; - - protected $_parser; - private $data; - private $public_to_private = false; - - public function __construct(CalendarParser &$parser, $data = null) - { - $this->_parser = $parser; - $this->data = $data; - } - - public function getContent() - { - return $this->data; - } - - public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS) - { - $this->_parser->changePublicToPrivate($this->public_to_private); - if ($this->_parser->parseIntoDatabase($range_id, $this->getContent(), $ignore)) { - return true; - } - - return false; - } - - public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS) - { - $this->_parser->changePublicToPrivate($this->public_to_private); - if ($this->_parser->parseIntoObjects($this->getContent(), $ignore)) { - return true; - } - - return false; - } - - public function getObjects() - { - return $objects =& $this->_parser->getObjects(); - } - - public function getCount() - { - return $this->_parser->getCount($this->getContent()); - } - - public function changePublicToPrivate($value = TRUE) - { - $this->public_to_private = $value; - } - - public function getClientIdentifier() - { - if (!$client_identifier = $this->_parser->getClientIdentifier()) { - return $this->_parser->getClientIdentifier($this->getContent()); - } - return $client_identifier; - } - - public function setImportSem($do_import) - { - if ($do_import) { - $this->_parser->import_sem = true; - } else { - $this->_parser->import_sem = false; - } - } - -} diff --git a/lib/calendar/CalendarImportFile.class.php b/lib/calendar/CalendarImportFile.class.php deleted file mode 100644 index 3e2ba15c15a835e219fa27017ed9754d3c4cc8b6..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarImportFile.class.php +++ /dev/null @@ -1,141 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarImportFile.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarImportFile extends CalendarImport -{ - - private $file; - private $path; - - /** - * - */ - public function __construct(&$parser, $file, $path = '') - { - parent::__construct($parser); - $this->file = $file; - $this->path = $path; - } - - /** - * - */ - public function getContent() - { - $data = ''; - if (!$file = @fopen($this->file['tmp_name'], 'rb')) { - throw new CalendarExportException(_("Die Import-Datei konnte nicht geöffnet werden!")); - return false; - } - if ($file) { - while (!feof($file)) { - $data .= fread($file, 1024); - } - fclose($file); - } - return $data; - } - - /** - * - */ - public function getFileName() - { - return $this->file['name']; - } - - /** - * - */ - public function getFileType() - { - return $this->_parser->getType(); - } - - /** - * - */ - public function getFileSize() - { - if (file_exists($this->file['tmp_name'])) { - return filesize($this->file['tmp_name']); - } - return false; - } - - /** - * - */ - public function checkFile() - { - return true; - } - - /** - * - */ - public function importIntoDatabase($range_id, $ignore = CalendarImport::IGNORE_ERRORS) - { - if ($this->checkFile()) { - parent::importIntoDatabase($range_id, $ignore); - return true; - } - throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!')); - return false; - } - - /** - * - */ - public function importIntoObjects($ignore = CalendarImport::IGNORE_ERRORS) - { - global $_calendar_error; - - if ($this->checkFile()) { - parent::importIntoObjects($ignore); - return true; - } - throw new CalendarExportException(_('Die Datei konnte nicht gelesen werden!')); - } - - /** - * - */ - public function deleteFile() - { - if (!unlink($this->file['tmp_name'])) { - throw new CalendarExportException(_("Die Datei konnte nicht gelöscht werden!")); - return false; - } - return true; - } - - /** - * - */ - public function _getFileExtension() - { - $i = mb_strrpos($this->file['name'], '.'); - if (!$i) { - return ''; - } - $l = mb_strlen($this->file['name']) - $i; - $ext = mb_substr($this->file['name'], $i + 1, $l); - return $ext; - } -} diff --git a/lib/calendar/CalendarParser.class.php b/lib/calendar/CalendarParser.class.php deleted file mode 100644 index 75780763f78c42186b45e50725d0724cb509907f..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarParser.class.php +++ /dev/null @@ -1,132 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarParser.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarParser -{ - private $events = []; - protected $components; - private $type; - private $number_of_events; - protected $public_to_private = false; - protected $client_identifier; - private $time; - protected $import_sem = false; - - public function __construct() - { - $this->client_identifier = ''; - } - - public function parse($data, $ignore = null) - { - foreach ($data as $properties) { - if ($this->public_to_private && $properties['CLASS'] == 'PUBLIC') { - $properties['CLASS'] = 'PRIVATE'; - } - $properties['CATEGORIES'] = implode(', ', $properties['CATEGORIES']); - $this->components[] = $properties; - } - } - - public function getCount($data) - { - return 0; - } - - public function parseIntoDatabase($range_id, $data, $ignore) - { - if ($this->parseIntoObjects($range_id, $data, $ignore)) { - foreach ($this->events as $event) { - $event->store(); - } - return true; - } - - return false; - } - - public function parseIntoObjects($range_id, $data, $ignore) - { - $this->time = time(); - if ($this->parse($data, $ignore)) { - if (is_array($this->components)) { - foreach ($this->components as $component) { - $calendar_event = CalendarEvent::findByUid($component['UID'], $range_id); - if ($calendar_event) { - $this->setProperties($calendar_event, $component); - $calendar_event->setRecurrence($component['RRULE']); - $this->events[] = $calendar_event; - } else { - $calendar_event = new CalendarEvent(); - $event = new EventData(); - $event->author_id = $GLOBALS['user']->id; - $event->event_id = $event->getNewId(); - $event->uid = $component['UID']; - $calendar_event->range_id = $range_id; - $calendar_event->event_id = $event->event_id; - $calendar_event->event = $event; - $this->setProperties($calendar_event, $component); - $calendar_event->setRecurrence($component['RRULE']); - $this->events[] = $calendar_event; - } - } - } - return true; - } - $message = _('Die Import-Daten konnten nicht verarbeitet werden!'); - - return false; - } - - private function setProperties($calendar_event, $component) - { - $calendar_event->setStart($component['DTSTART']); - $calendar_event->setEnd($component['DTEND']); - $calendar_event->setTitle($component['SUMMARY']); - $calendar_event->event->description = $component['DESCRIPTION']; - $calendar_event->setAccessibility($component['CLASS']); - $calendar_event->setUserDefinedCategories($component['CATEGORIES']); - $calendar_event->event->category_intern = $component['STUDIP_CATEGORY'] ?: 1; - $calendar_event->setPriority($component['PRIORITY'] ?? 0); - $calendar_event->event->location = $component['LOCATION']; - $calendar_event->setExceptions($component['EXDATE']); - $calendar_event->event->mkdate = $component['CREATED'] ?? time(); - $calendar_event->event->chdate = $component['LAST-MODIFIED'] ?: $component['CREATED'] ?? time(); - $calendar_event->event->importdate = $this->time; - } - - public function getType() - { - return $this->type; - } - - public function &getObjects() - { - return $objects =& $this->events; - } - - public function changePublicToPrivate($value = true) - { - $this->public_to_private = $value; - } - - public function getClientIdentifier($data = null) - { - return $this->client_identifier; - } -} diff --git a/lib/calendar/CalendarView.class.php b/lib/calendar/CalendarView.class.php index e3bfd0583272e47ccec0cf08630b0617b7fec46c..fc69127247a2fad95430364d794a4bba3c051929 100644 --- a/lib/calendar/CalendarView.class.php +++ b/lib/calendar/CalendarView.class.php @@ -38,6 +38,8 @@ * print $plan->render(); * * @since 2.0 + * + * @deprecated since Stud.IP 5.5 */ class CalendarView @@ -213,7 +215,7 @@ class CalendarView 'entry_height' => $this->getHeight() ]; $factory = new Flexi_TemplateFactory(dirname(__file__).'/../../app/views'); - PageLayout::addStyle($factory->render('calendar/stylesheet', $style_parameters)); + PageLayout::addStyle($factory->render('calendar/schedule/stylesheet', $style_parameters)); $template = $GLOBALS['template_factory']->open("calendar/calendar_view.php"); $template->set_attribute("calendar_view", $this); diff --git a/lib/calendar/CalendarWeekView.class.php b/lib/calendar/CalendarWeekView.class.php index e0d0a6bb77b659a8a5a2feaf4ce3a9fef689ffa0..c1e0f24718ff2d9fb78b479fd045aa44fb3b2028 100644 --- a/lib/calendar/CalendarWeekView.class.php +++ b/lib/calendar/CalendarWeekView.class.php @@ -20,6 +20,8 @@ * Kind of bean class for the calendar view. * * @since 2.0 + * + * @deprecated since Stud.IP 5.5 */ class CalendarWeekView extends CalendarView diff --git a/lib/calendar/CalendarWidgetView.php b/lib/calendar/CalendarWidgetView.php index 946592cdf73f0d69c9359d14d274e29a1fe38890..df980ff3953e529e720c799566d975ed670d08f5 100644 --- a/lib/calendar/CalendarWidgetView.php +++ b/lib/calendar/CalendarWidgetView.php @@ -5,6 +5,8 @@ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> * @license GPL2 or any later version * @since Stud.IP 3.4 + * + * @deprecated since Stud.IP 5.5 */ class CalendarWidgetView extends CalendarWeekView { diff --git a/lib/calendar/CalendarWriter.class.php b/lib/calendar/CalendarWriter.class.php deleted file mode 100644 index 941c12386ac5aa5abff796b986a4ceed167768d2..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarWriter.class.php +++ /dev/null @@ -1,53 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarWriter.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarWriter -{ - var $default_filename_suffix; - var $format; - var $client_identifier; - - public function __construct() - { - // initialize error handler - $GLOBALS['_calendar_error'] = new ErrorHandler(); - } - - public function write(Event &$event) - { - return $event->properties; - } - - public function writeHeader() - { - } - - public function writeFooter() - { - } - - public function getDefaultFilenameSuffix() - { - return $this->default_filename_suffix; - } - - public function getFormat() - { - return $this->format; - } -} diff --git a/lib/calendar/CalendarWriterICalendar.class.php b/lib/calendar/CalendarWriterICalendar.class.php deleted file mode 100644 index e65a89f98d52ebaf01c9341fb35ca4b8ebbf62ec..0000000000000000000000000000000000000000 --- a/lib/calendar/CalendarWriterICalendar.class.php +++ /dev/null @@ -1,629 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarWriterICalendar.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -define('CALENDAR_WEEKSTART', 'MO'); - -class CalendarWriterICalendar extends CalendarWriter -{ - var $newline = "\r\n"; - - public function __construct() - { - - parent::__construct(); - $this->default_filename_suffix = "ics"; - $this->format = "iCalendar"; - } - - public function writeHeader() - { - - // Default values - $header = "BEGIN:VCALENDAR" . $this->newline; - $header .= "VERSION:2.0" . $this->newline; - if ($this->client_identifier) { - $header .= "PRODID:" . $this->client_identifier . $this->newline; - } else { - $host = $_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'], PHP_URL_HOST); - $header .= "PRODID:-//Stud.IP@{$host}//Stud.IP_iCalendar Library"; - $header .= " //EN" . $this->newline; - } - $header .= "METHOD:PUBLISH" . $this->newline; - - // time zone definition CET/CEST - $header .= 'CALSCALE:GREGORIAN' . $this->newline - . 'BEGIN:VTIMEZONE' . $this->newline - . 'TZID:Europe/Berlin' . $this->newline - . 'BEGIN:DAYLIGHT' . $this->newline - . 'TZOFFSETFROM:+0100' . $this->newline - . 'TZOFFSETTO:+0200' . $this->newline - . 'TZNAME:CEST' . $this->newline - . 'DTSTART:19700329T020000' . $this->newline - . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3' . $this->newline - . 'END:DAYLIGHT' . $this->newline - . 'BEGIN:STANDARD' . $this->newline - . 'TZOFFSETFROM:+0200' . $this->newline - . 'TZOFFSETTO:+0100' . $this->newline - . 'TZNAME:CET' . $this->newline - . 'DTSTART:19701025T030000' . $this->newline - . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . $this->newline - . 'END:STANDARD' . $this->newline - . 'END:VTIMEZONE' . $this->newline; - - return $header; - } - - public function writeFooter() - { - return "END:VCALENDAR" . $this->newline; - } - - /** - * Export this component as iCalendar format - * - * @param object $event The event to export. - * @return String iCalendar formatted data - */ - public function write(Event &$event) - { - - $match_pattern_1 = ['\\', '\n', ';', ',']; - $replace_pattern_1 = ['\\\\', '\\n', '\;', '\,']; - $match_pattern_2 = ['\\', '\n', ';']; - $replace_pattern_2 = ['\\\\', '\\n', '\;']; - $exdate_time = 0; - - $result = "BEGIN:VEVENT" . $this->newline; - - foreach ($event->getProperties() as $name => $value) { - $params = []; - $params_str = ''; - - if ($name === 'SUMMARY') { - $value = $event->getTitle(); - } - if ($value === '' || is_null($value)) { - continue; - } - - switch ($name) { - // not supported event properties - case 'SEMNAME': - case 'EXPIRE': - case 'STUDIP_AUTHOR_ID': - case 'STUDIP_EDITOR_ID': - case 'STUDIP_ID': - case 'BEGIN': - case 'END': - case 'EVENT_TYPE': - case 'SEM_ID': - case 'STUDIP_GROUP_STATUS': - case 'STUDIP_CATEGORY': - continue 2; - - // text fields - case 'SUMMARY': - $value = str_replace($match_pattern_1, $replace_pattern_1, $value); - break; - case 'DESCRIPTION': - $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getDescription()); - break; - case 'LOCATION': - $value = str_replace($match_pattern_1, $replace_pattern_1, $event->getLocation()); - break; - - case 'CATEGORIES': - $value = $this->_exportCategories($event); - break; - - // Date fields - case 'LAST-MODIFIED': - case 'CREATED': - case 'COMPLETED': - $value = $this->_exportDateTime($value, true); - break; - - case 'DTSTAMP': - $value = $this->_exportDateTime(time(), true); - break; - - case 'DTSTART': - $exdate_time = $value; - case 'DTEND': - if ($event->isDayEvent()) { - $params['VALUE'] = 'DATE'; - $params_str = ';VALUE=DATE'; - $value++; - } - case 'DUE': - case 'RECURRENCE-ID': - if (array_key_exists('VALUE', $params)) { - if ($params['VALUE'] == 'DATE') { - $value = $this->_exportDate($value); - } else { - $value = $this->_exportDateTime($value); - $params_str = ';TZID=Europe/Berlin'; - } - } else { - $value = $this->_exportDateTime($value); - $params_str = ';TZID=Europe/Berlin'; - } - break; - - case 'EXDATE': - if (array_key_exists('VALUE', $params)) { - $value = $this->_exportExDate($value, $params['VALUE']); - } else { - $value = $this->_exportExDateTime($value, $exdate_time); - } - $params_str = ';TZID=Europe/Berlin'; - break; - - case 'RDATE': - if (array_key_exists('VALUE', $params)) { - if ($params['VALUE'] == 'DATE') { - $value = $this->_exportDate($value); - } else if ($params['VALUE'] == 'PERIOD') { - $value = $this->_exportPeriod($value); - } else { - $value = $this->_exportDateTime($value); - } - } else { - $value = $this->_exportDateTime($value); - } - break; - - case 'TRIGGER': - if (array_key_exists('VALUE', $params)) { - if ($params['VALUE'] == 'DATE-TIME') { - $value = $this->_exportDateTime($value); - } else if ($params['VALUE'] == 'DURATION') { - $value = $this->_exportDuration($value); - } - } else { - $value = $this->_exportDuration($value); - } - break; - - // Duration fields - case 'DURATION': - $value = $this->_exportDuration($value); - break; - - // Period of time fields - case 'FREEBUSY': - $value_str = ''; - foreach ($value as $period) { - $value_str .= empty($value_str) ? '' : ','; - $value_str .= $this->_exportPeriod($period); - } - $value = $value_str; - break; - - - // UTC offset fields - case 'TZOFFSETFROM': - case 'TZOFFSETTO': - $value = $this->_exportUtcOffset($value); - break; - - // Integer fields - case 'PERCENT-COMPLETE': - if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL) - $value = ''; - case 'REPEAT': - case 'SEQUENCE': - $value = "$value"; - break; - - case 'PRIORITY': - if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL) - $value = '0'; - else { - switch ($value) { - case 1: - $value = '1'; - break; - case 2: - $value = '5'; - break; - case 3: - $value = '9'; - break; - default: - $value = '0'; - } - } - break; - - // Geo fields - case 'GEO': - if ($event->getPermission() == Event::PERMISSION_CONFIDENTIAL) - $value = ''; - else - $value = $value['latitude'] . ',' . $value['longitude']; - break; - - // Recursion fields - case 'EXRULE': - case 'RRULE': - if ($event->getRecurrence('rtype') != 'SINGLE') - $value = $this->_exportRecurrence($value); - else - continue 2; - break; - - case "UID": - $value = "$value"; - } - if ($name) { - $attr_string = "$name$params_str:$value"; - $result .= $this->_foldLine($attr_string) . $this->newline; - } - } - // if ($event->isGroupEvent()) { - if ($event instanceof CalendarEvent && $event->attendees->count() > 1) { - $result .= $this->_exportGroupEventProperties($event); - } - // $result .= 'DTSTAMP:' . $this->_exportDateTime(time()) . $this->newline; - $result .= "END:VEVENT" . $this->newline; - - return $result; - } - - /** - * Export a UTC Offset field - * - * @param array $value - * @return String UTC offset field iCalendar formatted - */ - public function _exportUtcOffset($value) - { - $offset = $value['ahead'] ? '+' : '-'; - $offset .= sprintf('%02d%02d', $value['hour'], $value['minute']); - if (array_key_exists('second', $value)) { - $offset .= sprintf('%02d', $value['second']); - } - - return $offset; - } - - /** - * Export a Time Period field - * - * @param array $value - * @return String Period field iCalendar formatted - */ - public function _exportPeriod($value) - { - $period = $this->_exportDateTime($value['start']); - $period .= '/'; - if (array_key_exists('duration', $value)) { - $period .= $this->_exportDuration($value['duration']); - } else { - $period .= $this->_exportDateTime($value['end']); - } - return $period; - } - - /** - * Export a DateTime field - * - * @param int $value Unix timestamp - * @return String Date and time (UTC) iCalendar formatted - */ - public function _exportDateTime($value, $utc = false) - { - -// $TZOffset = 3600 * mb_substr(date('O', $value), 0, 3); -// $TZOffset += 60 * mb_substr(date('O', $value), 3, 2); - //transform local time in UTC - if ($utc) { - $value -= date('Z', $value); - } - - return $this->_exportDate($value) . 'T' . $this->_exportTime($value, $utc); - } - - /** - * Export a Time field - * - * @param int $value Unix timestamp - * @return String Time (UTC) iCalendar formatted - */ - public function _exportTime($value, $utc = false) - { - $time = date("His", $value); - if ($utc) { - $time .= 'Z'; - } - - return $time; - } - - /** - * Export a Date field - */ - public function _exportDate($value) - { - return date("Ymd", $value); - } - - /** - * Export a duration value - */ - public function _exportDuration($value) - { - $duration = ''; - if ($value < 0) { - $value *= - 1; - $duration .= '-'; - } - $duration .= 'P'; - - $weeks = floor($value / (7 * 86400)); - $value = $value % (7 * 86400); - if ($weeks) { - $duration .= $weeks . 'W'; - } - - $days = floor($value / (86400)); - $value = $value % (86400); - if ($days) { - $duration .= $days . 'D'; - } - - if ($value) { - $duration .= 'T'; - - $hours = floor($value / 3600); - $value = $value % 3600; - if ($hours) { - $duration .= $hours . 'H'; - } - - $mins = floor($value / 60); - $value = $value % 60; - if ($mins) { - $duration .= $mins . 'M'; - } - - if ($value) { - $duration .= $value . 'S'; - } - } - - return $duration; - } - - /** - * Export a recurrence rule - */ - public function _exportRecurrence($value) - { - $rrule = []; - // the last day of week in a MONTHLY or YEARLY recurrence in the - // Stud.IP calendar is 5, in iCalendar it is -1 - if ($value['sinterval'] == '5') - $value['sinterval'] = '-1'; - - if ($value['count']) - unset($value['expire']); - - foreach ($value as $r_param => $r_value) { - if ($r_value) { - switch ($r_param) { - case 'rtype': - $rrule[] = 'FREQ=' . $r_value; - break; - case 'expire': - // end of unix epoche (this is also the end of Stud.IP epoche ;-) ) - if ($r_value < Calendar::CALENDAR_END) - $rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true); - break; - case 'linterval': - $rrule[] = 'INTERVAL=' . $r_value; - break; - case 'wdays': - switch ($value['rtype']) { - case 'WEEKLY': - $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value); - break; - // Some CUAs (e.g. Outlook) don't understand the nWDAY syntax - // (where n is the nth ocurrence of the day in a given period of - // time and WDAY is the day of week) the RRULE uses the BYSETPOS - // rule. - case 'MONTHLY': - case 'YEARLY': - $rrule[] = 'BYDAY=' . $value['sinterval'] . $this->_exportWdays($r_value); - $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value); - // The Stud.IP calendar don't support multiple values in a - // comma separated list. - - if ($value['sinterval']) - $rrule[] = 'BYSETPOS=' . $value['sinterval']; - - break; - } - break; - case 'day': - $rrule[] = 'BYMONTHDAY=' . $r_value; - break; - case 'month': - $rrule[] = 'BYMONTH=' . $r_value; - break; - case 'count': - $rrule[] = 'COUNT=' . $r_value; - break; - } - } - } - - if ($value['rtype'] == 'WEEKLY' && CALENDAR_WEEKSTART != 'MO') { - $rrule[] = 'WKST=' . CALENDAR_WEEKSTART; - } - - return implode(';', $rrule); - } - - /** - * Return the Stud.IP calendar wdays attribute of a event recurrence - */ - public function _exportWdays($value) - { - $wdays_map = ['1' => 'MO', '2' => 'TU', '3' => 'WE', '4' => 'TH', '5' => 'FR', - '6' => 'SA', '7' => 'SU']; - $wdays = []; - preg_match_all('/(\d)/', $value, $matches); - foreach ($matches[1] as $match) { - $wdays[] = $wdays_map[$match]; - } - - return implode(',', $wdays); - } - - public function _exportExDate($value, $param) - { - $exdates = []; - $date_times = explode(',', $value); - foreach ($date_times as $date_time) { - $exdates[] = $this->_exportDate($date_time); - } - - return implode(',', $exdates); - } - - public function _exportExDateTime($value, $param) - { - $exdates = []; - $date_times = explode(',', $value); - foreach ($date_times as $date_time) { - $exdates[] = $this->_exportDate($date_time) . 'T' . $this->_exportTime($param); - } - - return implode(',', $exdates); - } - - private function _exportGroupEventProperties(Event $event) - { - $organizer = User::find($event->getAuthorId()); - if ($organizer) { - $properties = $this->_foldLine('ORGANIZER;CN="' - . $organizer->getFullName() - . '":mailto:' . $organizer->Email) - . $this->newline; - } else { - $properties = $this->_foldLine('ORGANIZER;CN="' - . _('unbekannt') - . '":mailto:' . $GLOBALS['user']->email) - . $this->newline; - } - foreach ($event->attendees as $event_member) { - if ($event->getAuthorId() == $event_member->user->id) { - if ($event_member->user) { - $properties .= $this->_foldLine('ATTENDEE;' - . 'ROLE=REQ-PARTICIPANT;' - . 'CN="' . $event_member->user->getFullName() - . '":mailto:' . $event_member->user->Email) - . $this->newline; - } else { - $properties = ''; - /* - $properties .= $this->_foldLine('ATTENDEE;' - . 'ROLE=REQ-PARTICIPANT;' - . 'CN="' . _('unbekannt') . '"') - . $this->newline; - * - */ - } - } else { - if ($event_member->user) { - switch ($event_member->group_status) { - case CalendarEvent::PARTSTAT_ACCEPTED : - $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT' - . ';PARTSTAT=ACCEPTED'; - break; - case CalendarEvent::PARTSTAT_DELEGATED : - $attendee = 'ATTENDEE;ROLE=NON-PARTICIPANT' - . ';PARTSTAT=ACCEPTED' - . ';DELEGATED-TO="mailto:' - . $this->getFacultyEmail($organizer->getId()) - . '"'; - break; - case CalendarEvent::PARTSTAT_DECLINED : - $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT' - . ';PARTSTAT=DECLINED'; - break; - default : - $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'; - $attendee .= ';PARTSTAT=TENTATIVE'; - $attendee .= ';RSVP=TRUE'; - - } - $attendee .= ';CN="' . $event_member->user->getFullName() - . '":mailto:' . $event_member->user->Email; - /* - } else { - $attendee .= ';CN="' . _('unbekannt') . '"'; - } - * - */ - $properties .= $this->_foldLine($attendee) . $this->newline; - } - } - } - return $properties; - } - - public function getFacultyEmail($user_id) - { - $stmt = DBManager::get()->prepare('SELECT email FROM Institute i ' - . 'LEFT JOIN user_inst ui USING(institut_id) ' - . 'WHERE i.Institut_id = fakultaets_id AND user_id = ?'); - $stmt->execute([$user_id]); - return $stmt->fetchColumn(); - } - - public function _exportCategories($event) - { - return implode(',' ,$event->toStringCategories(true)); - } - - /** - * Return the folded version of a line - */ - public function _foldLine($line) - { - $line = preg_replace('/(\r\n|\n|\r)/', '\n', $line); - if (mb_strlen($line) > 75) { - $foldedline = ''; - while ($line !== '') { - $maxLine = mb_substr($line, 0, 75); - $cutPoint = max(60, max(mb_strrpos($maxLine, ';'), mb_strrpos($maxLine, ':')) + 1); - - $foldedline .= ( empty($foldedline)) ? - mb_substr($line, 0, $cutPoint) : - $this->newline . ' ' . mb_substr($line, 0, $cutPoint); - - $line = (mb_strlen($line) <= $cutPoint) ? '' : mb_substr($line, $cutPoint); - } - return $foldedline; - } - return $line; - } -} diff --git a/lib/calendar/lib/CalendarError.class.php b/lib/calendar/lib/CalendarError.class.php deleted file mode 100644 index f796e1a98f42927938ba494bd0f748bd140fc64b..0000000000000000000000000000000000000000 --- a/lib/calendar/lib/CalendarError.class.php +++ /dev/null @@ -1,56 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * Error.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - - -class CalendarError -{ - - var $status; - var $message; - var $file; - var $line; - - public function __construct($status, $message, $file = '', $line = '') - { - $this->status = $status; - $this->message = $message; - $this->file = $file; - $this->line = $line; - } - - public function getStatus() - { - return $this->status; - } - - public function getMessage() - { - return $this->message; - } - - public function getFile() - { - return $this->file; - } - - public function getLine() - { - return $this->line; - } - -} diff --git a/lib/calendar/lib/ErrorHandler.class.php b/lib/calendar/lib/ErrorHandler.class.php deleted file mode 100644 index 390b5d521c019845d70eddc583dbcb11de07e210..0000000000000000000000000000000000000000 --- a/lib/calendar/lib/ErrorHandler.class.php +++ /dev/null @@ -1,119 +0,0 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * ErrorHandler.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class ErrorHandler -{ - // this is the state of the error handling if no error has occured - const ERROR_NORMAL = 1; - // this is the state of the error handling a message has to be displayed - const ERROR_MESSAGE = 2; - // this is the state of the error handling if something was going wrong but without data-loss - const ERROR_WARNING = 4; - // this is the state of the error handling if a critical error occured (maybe with data loss) - const ERROR_CRITICAL = 8; - // this is the state of the error handling if a fatal error occured and the execution of the process (e.g. import of events) was stopped - const ERROR_FATAL = 16; - - var $errors; - var $status; - - public function __construct() - { - $this->errors = []; - $this->status = ErrorHandler::ERROR_NORMAL; - } - - public function getStatus($status = NULL) - { - if ($status === NULL) - return $this->status; - - return $status & $this->status; - } - - public function getMaxStatus($status) - { - if ($status <= $this->status) - return true; - - return false; - } - - public function getMinStatus($status) - { - if ($status >= $this->status) - return true; - - return false; - } - - public function getErrors($status = NULL) - { - if ($status === NULL) - return $this->errors; - - return $this->errors[$status]; - } - - public function getAllErrors() - { - $status = [ErrorHandler::ERROR_FATAL, ErrorHandler::ERROR_CRITICAL, ErrorHandler::ERROR_WARNING, - ErrorHandler::ERROR_MESSAGE, ErrorHandler::ERROR_NORMAL]; - $errors = []; - foreach ($status as $stat) { - if (is_array($this->errors[$stat])) { - $errors = array_merge($errors, $this->errors[$stat]); - } - } - return $errors; - } - - public function nextError($status) - { - if(is_array($this->errors[$status])) { - return reset($this->errors[$status]); - } - return false; - } - - public function throwError($status, $message, $file = '', $line = '') - { - $this->errors[$status][] = new CalendarError($status, $message, $file, $line); - $this->status |= $status; - reset($this->errors[$status]); - if ($status == ErrorHandler::ERROR_FATAL) { - echo '<b>'; - while ($error = $this->nextError(ErrorHandler::ERROR_FATAL)) { - echo '<br />' . $error->getMessage(); - } - echo '</b><br />'; - page_close(); - exit; - } - } - - public function throwSingleError($index, $status, $message, $file = '', $line = '') - { - static $index_list = []; - - if ($index_list[$index] != 1) { - $this->throwError($status, $message, $file, $line); - $index_list[$index] = 1; - } - } -} diff --git a/lib/classes/Event.class.php b/lib/classes/Event.class.php deleted file mode 100644 index 6254884e16d30302608227a015681c6b66ea0080..0000000000000000000000000000000000000000 --- a/lib/classes/Event.class.php +++ /dev/null @@ -1,223 +0,0 @@ -<?php - -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ - -interface Event -{ - const PERMISSION_FORBIDDEN = 0; - const PERMISSION_CONFIDENTIAL = 1; - const PERMISSION_READABLE = 2; - const PERMISSION_DELETABLE = 3; - const PERMISSION_WRITABLE = 4; - const PERMISSION_OWN = 5; - - /** - * Returns a list of all categories the event belongs to. - * Returns an empty string if no permission. - * - * @return string All categories as list. - */ - public function toStringCategories(); - - /** - * Returns an array that represents the recurrence rule for this event. - * If an index is given, returns only this field of the rule. - * - * @return array|string The array with th recurrence rule or only one field. - */ - public function getRecurrence($index = null); - - /** - * TODO Wird das noch benötigt? - */ - public function getType(); - - /** - * Returns the title of this event. - * If the user has not the permission Event::PERMISSION_READABLE, - * the title is "Keine Berechtigung.". - * - * @return string - */ - public function getTitle(); - - /** - * Returns the starttime as unix timestamp of this event. - * - * @return int The starttime of this event as a unix timestamp - */ - public function getStart(); - - /** - * Returns the endtime as unix timestamp of this event. - * - * @return int the endtime of this event as a unix timestamp - */ - public function getEnd(); - - /** - * Returns the duration of this event in seconds. - * - * @return int the duration of this event in seconds - */ - function getDuration(); - - /** - * Returns the location. - * Without permission or the location is not set an empty string is returned. - * - * @return string The location - */ - function getLocation(); - - /** - * Returns the global uni id of this event. - * - * @return string The global unique id. - */ - public function getUid(); - - /** - * Returns the description of the topic. - * If the user has no permission or the event has no topic - * or the topics have no descritopn an empty string is returned. - * - * @return String the description - */ - function getDescription(); - - /** - * Returns the index of the category. - * If the user has no permission, 255 is returned. - * - * @see config/config.inc.php $TERMIN_TYP - * @return int The index of the category - */ - public function getCategory(); - - /** - * Returns the user id of the last editor. - * - * @return null|int The editor id. - */ - public function getEditorId(); - - /** - * Returns whether the event is a all day event. - * - * @return - */ - public function isDayEvent(); - - /** - * Returns the accessibility of this event. The value is not influenced by - * the permission of the actual user. - * - * According to RFC5545 the accessibility (property CLASS) is represented - * by the 3 values PUBLIC, PRIVATE and CONFIDENTIAL. In RFC5545 the default - * value is PUBLIC. In Stud.IP the default is PRIVATE. - * - * @return string The accessibility as string. - */ - function getAccessibility(); - - /** - * Returns the unix timestamp of the last change. - * - * @access public - */ - public function getChangeDate(); - - /** - * Returns the date time the event was imported. - * - * TODO not sure if we need this anymore - * - * @return int Date time of import as unix timestamp: - */ - function getImportDate(); - - /** - * Returns all properties of this event. - * The name of the properties correspond to the properties of the - * iCalendar calendar data exchange format. There are a few properties with - * the suffix STUDIP_ which have no eqivalent in the iCalendar format. - * - * DTSTART: The start date-time as unix timestamp. - * DTEND: The end date-time as unix timestamp. - * SUMMARY: The short description (title) that will be displayed in the views. - * DESCRIPTION: The long description. - * UID: The global unique id of this event. - * CLASS: - * CATEGORIES: A comma separated list of categories. - * PRIORITY: The priority. - * LOCATION: The location. - * EXDATE: A comma separated list of unix timestamps. - * CREATED: The creation date-time as unix timestamp. - * LAST-MODIFIED: The date-time of last modification as unix timestamp. - * DTSTAMP: The cration date-time of this instance of the event as unix - * timestamp. - * RRULE: All data for the recurrence rule for this event as array. - * EVENT_TYPE: - * - * - * @return array The properties of this event. - */ - public function getProperties(); - - /** - * Returns the value of property with given name. - * - * @param type $name See CalendarEvent::getProperties() for accepted values. - * @return mixed The value of the property. - * @throws InvalidArgumentException - */ - public function getProperty($name); - - public function havePermission($permission, $user_id = null); - - public function getPermission($user_id = null); - - /** - * Returns the priority in a human readable form. - * If the user has no permission an epmty string will be returned. - * - * @return string The priority as a string. - */ - public function toStringPriority(); - - /** - * Returns the accessibilty in a human readable form. - * If the user has no permission an epmty string will be returned. - * - * @return string The accessibility as string. - */ - public function toStringAccessibility(); - - /** - * Returns a string representation of the recurrence rule. - * If $only_type is true returns only the type of the recurrence. - * - * @param bool $only_type If true returns only the type of recurrence. - * @return string The recurrence rule - human readable - */ - public function toStringRecurrence($only_type = false); - - /** - * Returns the author of this event as user object. - * - * @return User|null User object. - */ - public function getAuthor(); - - /** - * Returns the editor of this event as user object. - * - * @return User|null User object. - */ - public function getEditor(); -} \ No newline at end of file diff --git a/lib/classes/Event.interface.php b/lib/classes/Event.interface.php new file mode 100644 index 0000000000000000000000000000000000000000..23f092b33877822e53f113459073bf549652fb57 --- /dev/null +++ b/lib/classes/Event.interface.php @@ -0,0 +1,184 @@ +<?php + +/* + * Event.interface.php - An interface for calendar events. + * + * 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. + * + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + */ + + +/** + * The Event interface represents calendar events. + */ +interface Event +{ + /** + * Retrieves events that lie in a given time range. + * + * @param DateTime $begin The beginning of the time range. + * + * @param DateTime $end The end of the time range. + * + * @param string $range_id The range for which to get the events. This may be a user-ID, + * course-ID or another kind of ID. + * + * @return Event[] An array with event objects. + */ + public static function getEvents(DateTime $begin, DateTime $end, string $range_id) : array; + + /** + * Returns the ID of the event. This is the ID that is only + * valid inside of Stud.IP. + * + * @return string The ID of the event object. + */ + public function getObjectId() : string; + + /** + * Returns the ID of the primary object where this object is linked to + * in a primary-secondary relationship where this object is a secondary object. + * + * Example: A course date is a secondary object and the course it belongs to + * is the primary object. + * + * @return string The ID of the primary object or an empty string if the + * implementation of the Event interface is a class of primary objects. + */ + public function getPrimaryObjectID() : string; + + /** + * Returns the class of the Event implementation. + * + * @return string The class name of the Event instance. + */ + public function getObjectClass() : string; + + /** + * Returns the title of this event. + * If the user has not the permission Event::PERMISSION_READABLE, + * the title is "Keine Berechtigung.". + * + * @return string The title of the event. + */ + public function getTitle() : string; + + /** + * Returns the start time of the event. + * + * @return DateTime The start time of the event. + */ + public function getBegin() : DateTime; + + /** + * Returns the end time of the event. + * + * @return DateTime The end time of the event. + */ + public function getEnd() : DateTime; + + /** + * Returns the duration of the event. + * + * @return DateInterval The duration of the event. + */ + public function getDuration() : DateInterval; + + /** + * Returns the location where the event takes place, if applicable. + * + * @return string The location of the event. + */ + public function getLocation() : string; + + /** + * Returns the global unique id of the event. + * + * @return string The global unique id of the event. + */ + public function getUniqueId() : string; + + /** + * Returns the description of the event. + * + * @return string The description of the event. + */ + public function getDescription() : string; + + /** + * Returns additional descriptions of the Event object. + * These are specific for each implementation. + * + * @return array Additional descriptions for the Event implementation. + * Each array key represents a heading for the description and the + * value contains the description itself as plain text. + * In case this is not applicable for the implementation, + * an empty array is returned. + */ + public function getAdditionalDescriptions() : array; + + /** + * Returns whether the event is an all day event or not. + * + * @return bool True, if the event is an all day event, false otherwise. + */ + public function isAllDayEvent() : bool; + + /** + * Determines whether the specified user has write permissions for the event. + * + * @param string $user_id The user for which to check write permissions. + * + * @return bool True, if the user has write permissions, false otherwise. + */ + public function isWritable(string $user_id) : bool; + + /** + * Returns the creation date of the event. + * + * @return DateTime The creation date of the event. + */ + public function getCreationDate() : DateTime; + + /** + * Returns the modification date of the event. + * + * @return DateTime The modification date of the event. + */ + public function getModificationDate() : DateTime; + + /** + * Returns the import date of the event. + * + * @return DateTime The import date of the event. + */ + public function getImportDate() : DateTime; + + /** + * Returns the author of this event as user object. + * + * @return User|null The user object of the author of the event, if available. + */ + public function getAuthor() : ?User; + + /** + * Returns the editor of this event as user object. + * + * @return User|null The user object of the editor of the event, if available. + */ + public function getEditor() : ?User; + + /** + * Returns a JSON-encoded fullcalendar event object that represents the event. + * + * @param $user_id string The user for which to generate the fullcalendar event. + * + * @return \Studip\Calendar\EventData The EventData representation of the event. + */ + public function toEventData(string $user_id) : \Studip\Calendar\EventData; +} diff --git a/lib/classes/IcalExport.php b/lib/classes/IcalExport.php index f011c5e79fd8a4f16942ebaf31d62cb5570d5367..2bb16d5342bbfa9349b88fb18897394cb19b768a 100644 --- a/lib/classes/IcalExport.php +++ b/lib/classes/IcalExport.php @@ -50,18 +50,25 @@ class IcalExport while ($length--) { while (1) { $rnd = rand(48, 122); - if ($rnd < 48) + if ($rnd < 48) { continue; - if ($rnd > 57 && $rnd < 65) + } + if ($rnd > 57 && $rnd < 65) { continue; - if ($rnd > 90 && $rnd < 97) + } + if ($rnd > 90 && $rnd < 97) { continue; - if ($rnd > 122) + } + if ($rnd > 122) { continue; + } $char = chr($rnd); - if ($rejected[$char] > 1) { + if (isset($rejected[$char]) && $rejected[$char] > 1) { continue; } + if (!isset($rejected[$char])) { + $rejected[$char] = 0; + } $rejected[$char]++; $ret .= $char; break; diff --git a/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php b/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php index 0ee6ded0218906d0274ff88bd6442c26696d926e..250d1442427bb1135bf96fb4f571b2c6c0a73942 100644 --- a/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php +++ b/lib/classes/JsonApi/Routes/Events/CourseEventsIndex.php @@ -27,19 +27,15 @@ class CourseEventsIndex extends JsonApiController throw new AuthorizationFailedException(); } - $dates = $course->dates->map(function ($courseDate) { - return new \CourseEvent($courseDate->id); - }); - $exDates = $course->ex_dates->map(function ($courseDate) { - return new \CourseCancelledEvent($courseDate->id); - }); - - $allDates = array_merge($dates, $exDates); - usort($allDates, function ($date1, $date2) { + $all_dates = array_merge( + $course->dates->getArrayCopy(), + $course->ex_dates->getArrayCopy() + ); + usort($all_dates, function ($date1, $date2) { return intval($date1->date) <=> intval($date2->date); }); list($offset, $limit) = $this->getOffsetAndLimit(); - return $this->getPaginatedContentResponse(array_slice($allDates, $offset, $limit), count($allDates)); + return $this->getPaginatedContentResponse(array_slice($all_dates, $offset, $limit), count($all_dates)); } } diff --git a/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php b/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php index c8080c46803add729bb99fc62535f270da4b4de9..c068e6e7b5c8ad0bb4d016cd826f4db8555a8f11 100644 --- a/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php +++ b/lib/classes/JsonApi/Routes/Events/UserEventsIcal.php @@ -24,14 +24,14 @@ class UserEventsIcal extends NonJsonApiController throw new RecordNotFoundException(); } - $writer = new \CalendarWriterICalendar(); - $export = new \CalendarExport($writer); - $export->exportFromDatabase($observedUser->id, 0, 2114377200, ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent']); - if ($GLOBALS['_calendar_error']->getMaxStatus(\ErrorHandler::ERROR_CRITICAL)) { - throw new InternalServerError(); - } + $end = \DateTime::createFromFormat('U', '2114377200'); + $start = new \DateTime(); + $ical_export = new \ICalendarExport(); + $ical = $ical_export->exportCalendarDates($observedUser->id, $start, $end) + . $ical_export->exportCourseDates($observedUser->id, $start, $end) + . $ical_export->exportCourseExDates($observedUser->id, $start, $end); + $content = $ical_export->writeHeader() . $ical . $ical_export->writeFooter(); - $content = implode($export->getExport()); $response->getBody()->write($content); return $response->withHeader('Content-Type', 'text/calendar') diff --git a/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php b/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php index 353382bb687b3668abccb29e01c85e5447fe02ce..192d279bb45c4425c580c980559b99002394dfc2 100644 --- a/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php +++ b/lib/classes/JsonApi/Routes/Events/UserEventsIndex.php @@ -38,7 +38,14 @@ class UserEventsIndex extends JsonApiController } $end = strtotime('+2 weeks', $start); - $list = \SingleCalendar::getEventList($user->id, $start, $end, $user->id); + //See the RecordNotFoundException above: This route only lets user + //retrieve the dates of their own calendar. So no permission check + //is needed. + $start_dt = new \DateTime(); + $start_dt->setTimestamp($start); + $end_dt = new \DateTime(); + $end_dt->setTimestamp($end); + $list = \CalendarDateAssignment::getEvents($start_dt, $end_dt, $user->id); list($offset, $limit) = $this->getOffsetAndLimit(); return $this->getPaginatedContentResponse( diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php index 71aadf7124a99463ac5de3ca0e962e9c1d8c2d1f..a0d21a3af044e82490a1bf287c1cd5e33de4b968 100644 --- a/lib/classes/JsonApi/SchemaMap.php +++ b/lib/classes/JsonApi/SchemaMap.php @@ -18,15 +18,16 @@ class SchemaMap \BlubberStatusgruppeThread::class => Schemas\BlubberStatusgruppeThread::class, \BlubberThread::class => Schemas\BlubberThread::class, - \CalendarEvent::class => Schemas\CalendarEvent::class, + \CalendarDateAssignment::class => Schemas\CalendarEvent::class, \ConsultationBlock::class => Schemas\ConsultationBlock::class, \ConsultationBooking::class => Schemas\ConsultationBooking::class, \ConsultationSlot::class => Schemas\ConsultationSlot::class, \ConfigValue::class => Schemas\ConfigValue::class, - \CourseEvent::class => Schemas\CourseEvent::class, \ContentTermsOfUse::class => Schemas\ContentTermsOfUse::class, \Course::class => Schemas\Course::class, \CourseMember::class => Schemas\CourseMember::class, + \CourseDate::class => Schemas\CourseEvent::class, + \CourseExDate::class => Schemas\CourseEvent::class, \FeedbackElement::class => Schemas\FeedbackElement::class, \FeedbackEntry::class => Schemas\FeedbackEntry::class, \JsonApi\Models\ForumCat::class => Schemas\ForumCategory::class, diff --git a/lib/classes/JsonApi/Schemas/CalendarEvent.php b/lib/classes/JsonApi/Schemas/CalendarEvent.php index 7b348d5478d8a456eb38abafaf64196376372cf4..fc5a1d81a47b8de83cf22a9811fe655884e2f5e5 100644 --- a/lib/classes/JsonApi/Schemas/CalendarEvent.php +++ b/lib/classes/JsonApi/Schemas/CalendarEvent.php @@ -18,17 +18,15 @@ class CalendarEvent extends SchemaProvider public function getAttributes($resource, ContextInterface $context): iterable { return [ - 'title' => $resource->title, - 'description' => $resource->getDescription(), - 'start' => date('c', $resource->getStart()), - 'end' => date('c', $resource->getEnd()), - 'categories' => $resource->toStringCategories(true), - 'location' => $resource->getLocation(), -// TODO: 'is-canceled' => $singledate->isHoliday() ?: false, - - 'mkdate' => date('c', $resource->mkdate), - 'chdate' => date('c', $resource->chdate), - 'recurrence' => $resource->getRecurrence(), + 'title' => $resource->calendar_date->title, + 'description' => $resource->calendar_date->description, + 'start' => date('c', $resource->calendar_date->begin), + 'end' => date('c', $resource->calendar_date->end), + 'categories' => $resource->calendar_date->getCategoryAsString(), + 'location' => $resource->calendar_date->location, + 'mkdate' => date('c', $resource->calendar_date->mkdate), + 'chdate' => date('c', $resource->calendar_date->chdate), + 'recurrence' => $resource->calendar_date->getRepetitionAsString(), ]; } @@ -39,7 +37,9 @@ class CalendarEvent extends SchemaProvider { $relationships = []; - if ($owner = $resource->getOwner()) { + $owner = $resource->user ?? $resource->course; + + if ($owner) { $link = $this->createLinkToResource($owner); $relationships = [ self::REL_OWNER => [self::RELATIONSHIP_LINKS => [Link::RELATED => $link], self::RELATIONSHIP_DATA => $owner], diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php index 09f11754e4016148866e640446d57d5fe06d1867..29a0c376380e0f28c5d6c20fb340110c2edf27a9 100644 --- a/lib/classes/JsonApi/Schemas/Course.php +++ b/lib/classes/JsonApi/Schemas/Course.php @@ -388,4 +388,29 @@ class Course extends SchemaProvider return array_merge($relationships, [self::REL_STATUS_GROUPS => $relation]); } + + /** + * @inheritdoc + */ + public function hasResourceMeta($resource): bool + { + return true; + } + + /** + * @inheritdoc + */ + public function getResourceMeta($resource) + { + $avatar = \CourseAvatar::getAvatar($resource->id); + + return [ + 'avatar' => [ + 'small' => $avatar->getURL(\Avatar::SMALL), + 'medium' => $avatar->getURL(\Avatar::MEDIUM), + 'normal' => $avatar->getURL(\Avatar::NORMAL), + ], + ]; + } + } diff --git a/lib/classes/JsonApi/Schemas/CourseEvent.php b/lib/classes/JsonApi/Schemas/CourseEvent.php index 77f1d3157f40023c3359d25935c39227af381317..e90ae0d030307fe84aba334e4a8461a8d191941e 100644 --- a/lib/classes/JsonApi/Schemas/CourseEvent.php +++ b/lib/classes/JsonApi/Schemas/CourseEvent.php @@ -18,16 +18,16 @@ class CourseEvent extends SchemaProvider public function getAttributes($resource, ContextInterface $context): iterable { return [ - 'title' => $resource->title, + 'title' => isset($resource->course) ? $resource->course->getFullName() : '', 'description' => $resource->getDescription(), - 'start' => date('c', $resource->getStart()), - 'end' => date('c', $resource->getEnd()), - 'categories' => array_filter($resource->toStringCategories(true)), - 'location' => $resource->getLocation(), - + 'start' => date('c', $resource->date), + 'end' => date('c', $resource->end_time), + 'categories' => '', + 'location' => $resource->raum ?? '', + 'is-cancelled' => $resource instanceof \CourseExDate, 'mkdate' => date('c', $resource->mkdate), 'chdate' => date('c', $resource->chdate), - 'recurrence' => $resource->getRecurrence(), + 'recurrence' => isset($resource->cycle) ? $resource->cycle->toString() : '', ]; } diff --git a/lib/classes/JsonApi/Schemas/Semester.php b/lib/classes/JsonApi/Schemas/Semester.php index 75aef03c96bb3af028733dbcbbcb8d8e79d3a3eb..f66f90ca62e88a0f7dd6bfc455ac236be98a4c94 100644 --- a/lib/classes/JsonApi/Schemas/Semester.php +++ b/lib/classes/JsonApi/Schemas/Semester.php @@ -23,6 +23,7 @@ class Semester extends SchemaProvider 'start-of-lectures' => date('c', $semester->vorles_beginn), 'end-of-lectures' => date('c', $semester->vorles_ende), 'visible' => (bool) $semester->visible, + 'is-current' => $semester->isCurrent(), ]; } diff --git a/lib/classes/Privacy.php b/lib/classes/Privacy.php index cbeb0a114025ffcf6d13bf1cf6b56498b4077c1f..670f30234093b751dafee85e209acf21e56855ec 100644 --- a/lib/classes/Privacy.php +++ b/lib/classes/Privacy.php @@ -28,7 +28,7 @@ class Privacy ], 'date' => [ 'CalendarEvent', - 'EventData', + 'CalendarDate', 'CourseDate', 'CourseExDate', ], diff --git a/lib/classes/UserManagement.class.php b/lib/classes/UserManagement.class.php index 1a49aed31240f6310df5b683fe8546dde6c91e12..7dd38eaa570d5020e530f86e4086d190b44b43a1 100644 --- a/lib/classes/UserManagement.class.php +++ b/lib/classes/UserManagement.class.php @@ -1169,16 +1169,6 @@ class UserManagement if ($count) { $msg .= 'info§' . sprintf(_('%s Einträge aus den Terminen gelöscht.'), $count) . '§'; } - // delete membership in group calendars - if (Config::get()->CALENDAR_GROUP_ENABLE) { - $count = CalendarUser::deleteBySQL( - 'owner_id = :user_id OR user_id = :user_id', - [':user_id' => $user_id] - ); - if ($count) { - $msg .= 'info§' . sprintf(_('%s Verknüpfungen mit Gruppenterminkalendern gelöscht.'), $count) . '§'; - } - } } // delete all messages send or received by this user diff --git a/lib/classes/calendar/Calendar.php b/lib/classes/calendar/Calendar.php deleted file mode 100644 index abb2ec793d6d9086fadc83a895fc3932e8335d4a..0000000000000000000000000000000000000000 --- a/lib/classes/calendar/Calendar.php +++ /dev/null @@ -1,187 +0,0 @@ -<?php -/** - * Calendar.class.php - Holds some additional functions and constants - * related to the personal 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 3.2 - */ -class Calendar -{ - /** - * The (positive) end of unix epche - */ - const CALENDAR_END = 0x7FFFFFFF; - - /** - * The user is the owner of the calendar. - */ - const PERMISSION_OWN = 16; - - /** - * The user has administrative access to the calendar. - * Means, he is not the owner but have the same rights. - * Not in use at the moment. - */ - const PERMISSION_ADMIN = 8; - - /** - * The user can add new events and edit existing events in the calendar. - * If the owner of the calendar has created an confidential event, the only - * information the user get is the start and end time. The event is shown as - * busy time in the views for him. - * If the user adds a confidential event, only he and the owner has full - * access to it. The event is shown as busy time to all other users. - */ - const PERMISSION_WRITABLE = 4; - - /** - * The user can read all information of all events, except events marked as - * confidential. These events are shown as busy times in the views. - * The user can not add new events nor edit existing events. - */ - const PERMISSION_READABLE = 2; - - /** - * The user is not allowed to get any information about the calendar. - * The user has no access to the calendar but he see public events on the - * profile of the owner. - */ - const PERMISSION_FORBIDDEN = 1; - - /** - * The calendar is related to one user. He is the owner of the calendar. - */ - const RANGE_USER = 1; - - /** - * The calendar is related to a group of users - * ("contact group" or Statusgruppe). - * Not used at the moment. - * The implemeted group functionality shows all personal calendars of the - * members of a contact group. It is not a shared calendar where all members - * have access to. - */ - const RANGE_GROUP = 2; - - /** - * The calendar is a module of a course or studygroup. All members with - * status author, tutor or dozent have write access (PERMISSION_WRITABLE). - * Users with local status user has only read access (PERMISSION_READABLE). - */ - const RANGE_SEM = 3; - - /** - * The calendar is a module of an institute or faculty. All members with - * status author, tutor or dozent have write access (PERMISSION_WRITABLE). - * Users with local status user has only read access (PERMISSION_READABLE). - */ - const RANGE_INST = 4; - - /** - * Retrieves all contact groups (statusgruppen) owned by the given user - * where at least one member has granted access to his calender for the user. - * - * @param string $user_id User id of the owner. - * @return type - */ - public static function getGroups($user_id) - { - $groups = []; - $calendar_owners = CalendarUser::getOwners($user_id)->pluck('owner_id'); - $sg_groups = SimpleORMapCollection::createFromArray( - Statusgruppen::findByRange_id($user_id)) - ->orderBy('position') - ->pluck('statusgruppe_id'); - if (sizeof($calendar_owners)) { - $sg_users = StatusgruppeUser::findBySQL( - 'statusgruppe_id IN(?) AND user_id IN(?)', - [$sg_groups, $calendar_owners]); - foreach ($sg_users as $sg_user) { - $groups[$sg_user->group->id] = $sg_user->group; - } - } - return $groups; - } - - public static function GetInstituteActivatedCalendar($user_id) - { - - $ret = []; - Institute::findAndMapBySQL(function($i) use (&$ret) { - if ($i->isToolActive('CoreCalendar')) { - $ret[$i->id] = $i->name; - } - }, - "JOIN user_inst USING(Institut_id) - WHERE user_id = ? AND inst_perms IN ('admin','dozent','tutor','autor') - ORDER BY Name ASC", - [$user_id] - ); - return $ret; - } - - /** - * - * @param string $user_id - * @return array - */ - public static function GetCoursesActivatedCalendar($user_id) - { - $courses_user = SimpleCollection::createFromArray( - CourseMember::findByUser($user_id)); - $courses = $courses_user->filter(function ($c) { - if ($c->course->isToolActive('CoreCalendar')) { - return $c; - } - }); - return $courses->pluck('course'); - } - - public static function GetLecturers() - { - $stmt = DBManager::get()->prepare("SELECT aum.username, " - . "CONCAT(aum.Nachname,', ',aum.vorname) as fullname, " - . "aum.user_id FROM auth_user_md5 aum WHERE perms = 'dozent' " - . "ORDER BY fullname"); - $stmt->execute(); - $lecturers = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - if ($row['user_id'] != $GLOBALS['user']->id) { - $lecturers[] = ['name' => $row['fullname'], - 'username' => $row['username'], 'id' => $row['user_id']]; - } - } - return $lecturers; - } - - /** - * Returns an array of default user settings for the calendar or a specific - * value if the index is given. - * - * @param string $index Index of setting to get. - * @return string|array Array of settings or one setting - */ - public static function getDefaultUserSettings($index = null) - { - $default = [ - 'view' => 'week', - 'start' => '9', - 'end' => '20', - 'step_day' => '900', - 'step_week' => '1800', - 'type_week' => 'LONG', - 'step_week_group' => '3600', - 'step_day_group' => '3600', - 'show_declined' => '0' - ]; - return (is_null($index) ? $default : $default[$index]); - } -} diff --git a/lib/classes/calendar/CalendarInstscheduleModel.php b/lib/classes/calendar/CalendarInstscheduleModel.php deleted file mode 100644 index e99da9d17011ab2d6fe6805dfd7f29a9ef7d8b1a..0000000000000000000000000000000000000000 --- a/lib/classes/calendar/CalendarInstscheduleModel.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php -# Lifter010: TODO - -/* - * This class is the model for the institute-calendar for seminars - * - * 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 Till Glöggler <tgloeggl@uos.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - */ - -require_once __DIR__ . '/default_color_definitions.php'; - -/** - * Pseudo-namespace containing helper methods for the calendar of institutes. - * - * @since 2.0 - */ -class CalendarInstscheduleModel -{ - /** - * Returns a schedule entry for a course - * - * @param string $seminar_id the ID of the course - * @param string $user_id the ID of the user - * @param string $cycle_id optional; if given, specifies the ID of the entry - * @return array an array containing the properties of the entry - */ - static function getSeminarEntry($seminar_id, $user_id, $cycle_id = false) - { - $ret = []; - - $sem = new Seminar($seminar_id); - foreach ($sem->getCycles() as $cycle) { - if (!$cycle_id || $cycle->getMetaDateID() == $cycle_id) { - $entry = []; - - $entry['id'] = $seminar_id; - $entry['cycle_id'] = $cycle->getMetaDateId(); - $entry['start_formatted'] = sprintf("%02d", $cycle->getStartStunde()) .':' - . sprintf("%02d", $cycle->getStartMinute()); - $entry['end_formatted'] = sprintf("%02d", $cycle->getEndStunde()) .':' - . sprintf("%02d", $cycle->getEndMinute()); - - $entry['start'] = ((int)$cycle->getStartStunde() * 100) + ($cycle->getStartMinute()); - $entry['end'] = ((int)$cycle->getEndStunde() * 100) + ($cycle->getEndMinute()); - $entry['day'] = $cycle->getDay(); - $entry['content'] = $sem->getNumber() . ' ' . $sem->getName(); - $entry['url'] = URLHelper::getLink('dispatch.php/calendar/instschedule/entry/' . $seminar_id - . '/' . $cycle->getMetaDateId()); - $entry['onClick'] = "function(id) { STUDIP.Instschedule.showSeminarDetails('$seminar_id', '" - . $cycle->getMetaDateId() ."'); }"; - - $entry['title'] = ''; - $ret[] = $entry; - } - } - - return $ret; - } - - - /** - * Returns an array of CalendarColumn's, containing the seminar-entries - * for the passed user (in the passed semester belonging to the passed institute) - * The start- and end-hour are used to constrain the returned - * entries to the passed time-period. The passed days constrain the entries - * to these. - * - * @param string $user_id the ID of the user - * @param array $semester an array containing the "beginn" of the semester - * @param int $start_hour the start hour - * @param int $end_hour the end hour - * @param string $institute_id the ID of the institute - * @param array $days the days to be displayed - * - * @return array an array containing the entries - */ - static function getInstituteEntries($user_id, $semester, $start_hour, $end_hour, $institute_id, $days) - { - // fetch seminar-entries, show invisible seminars if the user has enough perms - $visibility_perms = $GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM); - - $inst_ids = []; - $institut = new Institute($institute_id); - - if (!$institut->isFaculty() || $GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN) { - // If the institute is not a faculty or the child insts are included, - // pick the institute IDs of the faculty/institute and of all sub-institutes. - $inst_ids[] = $institute_id; - if ($institut->isFaculty()) { - foreach ($institut->sub_institutes->pluck("Institut_id") as $institut_id) { - $inst_ids[] = $institut_id; - } - } - } else { - // If the institute is a faculty and the child insts are not included, - // pick only the institute id of the faculty: - $inst_ids[] = $institute_id; - } - - $stmt = DBManager::get()->prepare("SELECT * FROM seminare - LEFT JOIN seminar_inst ON (seminare.Seminar_id = seminar_inst.seminar_id) - LEFT JOIN semester_courses ON (semester_courses.course_id = seminare.Seminar_id) - WHERE seminar_inst.institut_id IN (:institute) - AND (start_time <= :begin AND (semester_courses.semester_id IS NULL OR semester_courses.semester_id = :semester_id)) - " - . (!$visibility_perms ? " AND visible='1'" : "")); - - $stmt->bindValue(':begin', $semester['beginn']); - $stmt->bindValue(':semester_id', $semester['id']); - $stmt->bindValue(':institute', $inst_ids, StudipPDO::PARAM_ARRAY); - $stmt->execute(); - - while ($entry = $stmt->fetch()) { - $seminars[$entry['Seminar_id']] = $entry; - } - - if (is_array($seminars)) foreach ($seminars as $data) { - $entries = self::getSeminarEntry($data['Seminar_id'], $user_id); - - foreach ($entries as $entry) { - unset($entry['url']); - $entry['onClick'] = 'function(id) { STUDIP.Instschedule.showInstituteDetails(id); }'; - $entry['visible'] = 1; - - if (($entry['start'] >= $start_hour * 100 && $entry['start'] <= $end_hour * 100 - || $entry['end'] >= $start_hour * 100 && $entry['end'] <= $end_hour * 100)) { - - $entry['color'] = DEFAULT_COLOR_SEM; - - $day_number = ($entry['day'] + 6) % 7; - if (!isset($ret[$day_number])) { - $ret[$day_number] = CalendarColumn::create($day_number); - } - $ret[$day_number]->addEntry($entry); - } - } - } - - return CalendarScheduleModel::addDayChooser($ret, $days, 'instschedule'); - } -} diff --git a/lib/classes/calendar/CalendarScheduleModel.php b/lib/classes/calendar/CalendarScheduleModel.php index ff9bce8e754a1cee1cbe0a17893f8bb0a7db7784..468b7e23842143d45a486cc48dcc8023a6c1ea2e 100644 --- a/lib/classes/calendar/CalendarScheduleModel.php +++ b/lib/classes/calendar/CalendarScheduleModel.php @@ -20,6 +20,8 @@ require_once __DIR__ . '/default_color_definitions.php'; * Pseudo-namespace containing helper methods for the schedule. * * @since 2.0 + * + * @deprecated since Stud.IP 5.5 */ class CalendarScheduleModel { diff --git a/lib/calendar/EventData.class.php b/lib/classes/calendar/EventData.class.php similarity index 89% rename from lib/calendar/EventData.class.php rename to lib/classes/calendar/EventData.class.php index 9ab4377d8317b1544e6f5d81b8a2f337ef30da79..82afe6f3b7751e53dd3357d35f771ff168b6856e 100644 --- a/lib/calendar/EventData.class.php +++ b/lib/classes/calendar/EventData.class.php @@ -22,6 +22,8 @@ class EventData public $view_urls; public $api_urls; public $icon; + public $border_colour; + public $all_day; public function __construct( \DateTime $begin, @@ -39,7 +41,9 @@ class EventData string $range_id, Array $view_urls = [], Array $api_urls = [], - string $icon = '' + string $icon = '', + string $border_colour = '', + bool $all_day = false ) { $this->begin = $begin; @@ -58,6 +62,8 @@ class EventData $this->view_urls = $view_urls; $this->api_urls = $api_urls; $this->icon = $icon; + $this->border_colour = $border_colour ?: $background_colour; + $this->all_day = $all_day; } @@ -71,10 +77,12 @@ class EventData 'resourceId' => $this->range_id, 'start' => $this->begin->format('Y-m-d\TH:i:s'), 'end' => $this->end->format('Y-m-d\TH:i:s'), + 'allDay' => $this->all_day, 'title' => $this->title, 'classNames' => $this->event_classes, 'textColor' => $this->text_colour, 'color' => $this->background_colour, + 'borderColor' => $this->border_colour, 'editable' => $this->editable, 'studip_weekday_begin' => $this->begin->format('N'), 'studip_weekday_end' => $this->end->format('N'), diff --git a/lib/calendar/EventSource.interface.php b/lib/classes/calendar/EventSource.interface.php similarity index 100% rename from lib/calendar/EventSource.interface.php rename to lib/classes/calendar/EventSource.interface.php diff --git a/lib/classes/calendar/Helper.php b/lib/classes/calendar/Helper.php new file mode 100644 index 0000000000000000000000000000000000000000..10294f42a99c6a52dd63879596ed4cea59557e03 --- /dev/null +++ b/lib/classes/calendar/Helper.php @@ -0,0 +1,107 @@ +<?php + +namespace Studip\Calendar; + +class Helper +{ + /** + * Retrieves the time slot duration in the calendar for a specified calendar type + * and either the current user or a specific user. + * + * @param string $calendar_type The calendar type for which to retrieve the slot duration. + * Valid values: 'week', 'day', 'week_group' (week group calendar), 'week_day' (day group calendar). + * Defaults to 'week'. + * @param string $user_id The user for which to retrieve the slot duration. Defaults to an + * empty string which then in turn means the current users slot duration is retrieved. + * + * @return string The slot duration as a time string in the form HH:MM:SS. + */ + public static function getCalendarSlotDuration(string $calendar_type = 'week', string $user_id = '') : string + { + $default_slot_duration = '00:30:00'; + + $user_config = new \UserConfig($user_id ?: $GLOBALS['user']->id); + $calendar_settings = $user_config->CALENDAR_SETTINGS; + + if ( + $calendar_type === 'week' + && !empty($calendar_settings['step_week']) + ) { + $step_week = (int) $calendar_settings['step_week']; + $hours = floor($step_week / 3600); + $minutes = round(($step_week - $hours * 3600) / 60); + return sprintf('%1$02u:%2$02u:00', $hours, $minutes); + } elseif ( + $calendar_type === 'day' + && !empty($calendar_settings['step_day']) + ) { + $step_day = (int) $calendar_settings['step_day']; + $hours = floor($step_day / 3600); + $minutes = round(($step_day - $hours * 3600) / 60); + return sprintf('%1$02u:%2$02u:00', $hours, $minutes); + } elseif ( + $calendar_type === 'week_group' + && !empty($calendar_settings['step_week_group']) + ) { + $step_week = (int) $calendar_settings['step_week_group']; + $hours = floor($step_week / 3600); + $minutes = round(($step_week - $hours * 3600) / 60); + return sprintf('%1$02u:%2$02u:00', $hours, $minutes); + } elseif ( + $calendar_type === 'day_group' + && !empty($calendar_settings['step_day_group']) + ) { + $step_day = (int) $calendar_settings['step_day_group']; + $hours = floor($step_day / 3600); + $minutes = round(($step_day - $hours * 3600) / 60); + return sprintf('%1$02u:%2$02u:00', $hours, $minutes); + } + + // An unknown slot type or no appropriate match before: + // Return the default duration. + return $default_slot_duration; + } + + + /** + * Retrieves the default calendar date by various methods. + * + * @return \DateTime The default date for the calendar. + * This defaults to the current date if no other date + * can be retrieved. + */ + public static function getDefaultCalendarDate() : \DateTime + { + $default_date = new \DateTime(); + if (\Request::submitted('date')) { + $date = \Request::getDateTime('date', 'Y-m-d'); + if ($date instanceof \DateTime) { + $default_date = $date; + //Update the session value: + $_SESSION['calendar_date'] = $default_date->format('Y-m-d'); + } + } elseif (\Request::submitted('semester_id')) { + //A semester-ID is set, but no specific date that would override it. + //Use the first lecture week of the semester as default date. + $semester_id = \Request::option('semester_id'); + $semester = \Semester::find($semester_id); + if ($semester) { + $default_date->setTimestamp($semester->vorles_beginn); + //Update the session value: + $_SESSION['calendar_date'] = $default_date->format('Y-m-d'); + } + } elseif (!empty($_SESSION['calendar_date'])) { + $date = \DateTime::createFromFormat( + 'Y-m-d', + $_SESSION['calendar_date'], + $default_date->getTimezone() + ); + if ($date instanceof \DateTime) { + $default_date = $date; + } + } + $default_date->setTime(0,0,0); + + return $default_date; + } +} diff --git a/lib/classes/calendar/ICalendarExport.class.php b/lib/classes/calendar/ICalendarExport.class.php new file mode 100644 index 0000000000000000000000000000000000000000..fba097446b36023a4d340df484db3a9abb3e0923 --- /dev/null +++ b/lib/classes/calendar/ICalendarExport.class.php @@ -0,0 +1,638 @@ +<?php +/** + * ICalendarExport.class.php + * + * 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 Peter Thienel <thienel@data-quest.de> + * @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 + */ + + +class ICalendarExport +{ + /** + * Line break used in iCalendar + */ + const NEWLINE = "\r\n"; + + /** + * Default start of the week + */ + const WEEKSTART = 'MO'; + + /** + * Holds the time (as unix timestamp) used for + * the timestamp in every exported iCalendar object. + * + * @var int $time + */ + private $time = 0; + + public function __construct() + { + $this->default_filename_suffix = "ics"; + $this->format = "iCalendar"; + } + + public function exportCalendarDates(string $range_id, DateTimeInterface $start, DateTimeInterface $end): string + { + if ($this->time === 0) { + $this->time = time(); + } + $dates = CalendarDate::findBySQL( + "LEFT JOIN `calendar_date_assignments` + ON `calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id` + WHERE + `calendar_date_assignments`.`range_id` = :range_id + AND ( + (`calendar_dates`.`begin` <= :end + AND `calendar_dates`.`end` >= :begin) + OR (`calendar_dates`.`repetition_type` != 'SINGLE' + AND (`calendar_dates`.`repetition_end` >= :end + OR `calendar_dates`.`repetition_end` = 0) + AND `calendar_dates`.`begin` < :end))", + [ + ':range_id' => $range_id, + ':begin' => $start->getTimestamp(), + ':end' => $end->getTimestamp(), + ] + ); + $ical = ''; + foreach ($dates as $date) { + $ical .= $this->writeICalEvent($this->prepareCalendarDate($date)); + } + return $ical; + } + + public function exportCourseDates(string $user_id, DateTimeInterface $start, DateTimeInterface $end) + { + if ($this->time === 0) { + $this->time = time(); + } + $dates = CourseDate::findBySql( + "LEFT JOIN `seminar_user` + ON `termine`.`range_id` = `seminar_user`.`Seminar_id` + WHERE + `seminar_user`.`user_id` = :user_id + AND `seminar_user`.`bind_calendar` = 1 + AND (`termine`.`date` <= :end + AND `termine`.`end_time` >= :begin)", + [ + ':user_id' => $user_id, + ':begin' => $start->getTimestamp(), + ':end' => $end->getTimestamp(), + ] + ); + $ical = ''; + foreach ($dates as $date) { + $ical .= $this->writeICalEvent($this->prepareCourseDate($date)); + } + return $ical; + } + + public function exportCourseExDates(string $user_id, DateTimeInterface $start, DateTimeInterface $end) + { + if ($this->time === 0) { + $this->time = time(); + } + $dates = CourseExDate::findBySql( + "LEFT JOIN `seminar_user` + ON `ex_termine`.`range_id` = `seminar_user`.`Seminar_id` + WHERE + `seminar_user`.`user_id` = :user_id + AND `seminar_user`.`bind_calendar` = 1 + AND (`ex_termine`.`date` <= :end + AND `ex_termine`.`end_time` >= :begin)", + [ + ':user_id' => $user_id, + ':begin' => $start->getTimestamp(), + ':end' => $end->getTimestamp(), + ] + ); + $ical = ''; + foreach ($dates as $date) { + $ical .= $this->writeICalEvent($this->prepareCourseDate($date)); + } + return $ical; + } + + /** + * @param CalendarDate $date + * @return array + */ + public function prepareCalendarDate(CalendarDate $date): array + { + $properties = + [ + 'SUMMARY' => $date->title, + 'DESCRIPTION' => $date->description, + 'LOCATION' => $date->location, + 'CATEGORIES' => $date->getCategoryAsString(), + 'LAST-MODIFIED' => $date->chdate, + 'CREATED' => $date->mkdate, + 'DTSTAMP' => $this->time, + 'DTSTART' => $date->begin, + 'DTEND' => $date->end, + 'EXDATE' => implode(',', $date->exceptions->pluck('date')), + 'PRIORITY' => 5, + 'RRULE' => [ + 'type' => $date->repetition_type, + 'offset' => $date->offset, + 'interval' => $date->interval, + 'days' => $date->days, + 'count' => $date->number_of_dates, + 'expire' => $date->repetition_end, + 'month' => $date->month + ] + ]; + return $properties; + } + + public function prepareCourseDate(CourseDate $date): array + { + $properties = + [ + 'SUMMARY' => $date->course->getFullname(), + 'DESCRIPTION' => '', + 'LOCATION' => $date->getRoomName(), + 'CATEGORIES' => $GLOBALS['TERMIN_TYP'][$date->date_typ]['name'], + 'LAST-MODIFIED' => $date->chdate, + 'CREATED' => $date->mkdate, + 'DTSTAMP' => $this->time, + 'DTSTART' => $date->date, + 'DTEND' => $date->end_time, + 'PRIORITY' => '' + ]; + return $properties; + } + + /** + * Returns an iCalendar header with a rudimentary time zone definition. + * + * @return string The iCalendar header. + */ + public function writeHeader() + { + // Default values + $header = "BEGIN:VCALENDAR" . self::NEWLINE; + $header .= "VERSION:2.0" . self::NEWLINE; + if (isset($this->client_identifier)) { + $header .= "PRODID:" . $this->client_identifier . self::NEWLINE; + } else { + $server_name = $_SERVER['SERVER_NAME'] ?? 'unknown'; + + $header .= "PRODID:-//Stud.IP@{$server_name}//Stud.IP_iCalendar Library"; + $header .= " //EN" . self::NEWLINE; + } + $header .= "METHOD:PUBLISH" . self::NEWLINE; + + // time zone definition CET/CEST + $header .= 'CALSCALE:GREGORIAN' . self::NEWLINE + . 'BEGIN:VTIMEZONE' . self::NEWLINE + . 'TZID:Europe/Berlin' . self::NEWLINE + . 'BEGIN:DAYLIGHT' . self::NEWLINE + . 'TZOFFSETFROM:+0100' . self::NEWLINE + . 'TZOFFSETTO:+0200' . self::NEWLINE + . 'TZNAME:CEST' . self::NEWLINE + . 'DTSTART:19700329T020000' . self::NEWLINE + . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3' . self::NEWLINE + . 'END:DAYLIGHT' . self::NEWLINE + . 'BEGIN:STANDARD' . self::NEWLINE + . 'TZOFFSETFROM:+0200' . self::NEWLINE + . 'TZOFFSETTO:+0100' . self::NEWLINE + . 'TZNAME:CET' . self::NEWLINE + . 'DTSTART:19701025T030000' . self::NEWLINE + . 'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10' . self::NEWLINE + . 'END:STANDARD' . self::NEWLINE + . 'END:VTIMEZONE' .self::NEWLINE; + + return $header; + } + + /** + * Returns the footer. + * + * @return string + */ + public function writeFooter() + { + return "END:VCALENDAR" . self::NEWLINE; + } + + /** + * Export prepared calendar data as iCalendar. + * + * @param array $properties The event to export. + * @return string iCalendar formatted data + */ + public function writeICalEvent(array $properties): string + { + $result = "BEGIN:VEVENT" . self::NEWLINE; + + foreach ($properties as $name => $value) { + $params = []; + $params_str = ''; + if ($value === '' || is_null($value)) { + continue; + } + switch ($name) { + // not supported event properties + case 'SEMNAME': + continue 2; + + // Text fields + case 'SUMMARY': + $value = $this->quoteText($value); + break; + case 'DESCRIPTION': + $value = $this->quoteText($value); + break; + case 'LOCATION': + $value = $this->quoteText($value); + break; + case 'CATEGORIES': + $value = $this->quoteText($value); + break; + + // Date fields + case 'LAST-MODIFIED': + case 'CREATED': + case 'COMPLETED': + $value = $this->_exportDateTime($value, true); + break; + + case 'DTSTAMP': + $value = $this->_exportDateTime(time(), true); + break; + + case 'DTSTART': + $exdate_time = $value; + case 'DTEND': + case 'DUE': + case 'RECURRENCE-ID': + if (array_key_exists('VALUE', $params)) { + if ($params['VALUE'] == 'DATE') { + $value = $this->_exportDate($value); + } else { + $value = $this->_exportDateTime($value); + $params_str = ';TZID=Europe/Berlin'; + } + } else { + $value = $this->_exportDateTime($value); + $params_str = ';TZID=Europe/Berlin'; + } + break; + + case 'EXDATE': + if (array_key_exists('VALUE', $params)) { + $value = $this->exportExDate($value); + } else { + $value = $this->exportExDateTime($value); + } + $params_str = ';TZID=Europe/Berlin'; + break; + + // Integer fields + case 'PERCENT-COMPLETE': + case 'REPEAT': + case 'SEQUENCE': + $value = "$value"; + break; + + case 'PRIORITY': + switch ($value) { + case 1: + $value = '1'; + break; + case 2: + $value = '5'; + break; + case 3: + $value = '9'; + break; + default: + $value = '0'; + } + break; + + // Geo fields + case 'GEO': + $value = $value['latitude'] . ',' . $value['longitude']; + break; + + // Recursion fields + case 'EXRULE': + case 'RRULE': + if ($value['type'] !== 'SINGLE') { + $value = $this->_exportRecurrence($value); + } + break; + + case "UID": + $value = "$value"; + } + if ($name && !is_array($value)) { + $attr_string = $name . $params_str . ':' . $value; + $result .= $this->foldLine($attr_string) . self::NEWLINE; + } + } + if (isset($properties['GROUP_EVENT'])) { + $result .= $this->exportGroupEventProperties($properties['GROUP_EVENT']); + } + $result .= "END:VEVENT" . self::NEWLINE; + + return $result; + } + + /** + * Quotes some characters accordingly to iCalendar format. + * + * @param string $text The text to quote. + * @return string The quoted text. + */ + public function quoteText(string $text): string + { + $match = ['\\', '\n', ';', ',']; + $replace = ['\\\\', '\\n', '\;', '\,']; + return str_replace($match, $replace, $text); + } + + /** + * Export a DateTime field + * + * @param int $value Unix timestamp + * @return String Date and time (UTC) iCalendar formatted + */ + public function _exportDateTime($value, $utc = false) + { + $date_time = new DateTime(); + $date_time->setTimestamp($value); + //transform local time in UTC + if ($utc) { + $tz_utc = new DateTimeZone('UTC'); + $date_time->setTimezone($tz_utc); + return $date_time->format('Ymd\THis\Z'); + } + return $date_time->format('Ymd\THis'); + } + + /** + * Export a Time field + * + * @param int $value Unix timestamp + * @return String Time (UTC) iCalendar formatted + */ + public function _exportTime($value, $utc = false) + { + $time = date("His", $value); + if ($utc) { + $time .= 'Z'; + } + + return $time; + } + + /** + * Export a Date field + */ + public function _exportDate($value) + { + return date("Ymd", $value); + } + + /** + * Export a recurrence rule + */ + public function _exportRecurrence($value) + { + $rrule = []; + // the last day of week in a MONTHLY or YEARLY recurrence in the + // Stud.IP calendar is 5, in iCalendar it is -1 + if ($value['offset'] == '5') { + $value['offset'] = '-1'; + } + + if ($value['count']) { + unset($value['expire']); + } + + foreach ($value as $r_param => $r_value) { + if ($r_value) { + switch ($r_param) { + case 'type': + $rrule[] = 'FREQ=' . $r_value; + break; + case 'expire': + if ($r_value < CalendarDate::NEVER_ENDING) + $rrule[] = 'UNTIL=' . $this->_exportDateTime($r_value, true); + break; + case 'interval': + $rrule[] = 'INTERVAL=' . $r_value; + break; + case 'days': + switch ($value['type']) { + case 'WEEKLY': + $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value); + break; + // Some CUAs (e.g. Outlook) don't understand the nWDAY syntax + // (where n is the nth ocurrence of the day in a given period of + // time and WDAY is the day of week) the RRULE uses the BYSETPOS + // rule. + case 'MONTHLY': + case 'YEARLY': + $rrule[] = 'BYDAY=' . $value['offset'] . $this->_exportWdays($r_value); + $rrule[] = 'BYDAY=' . $this->_exportWdays($r_value); + if ($value['offset']) { + $rrule[] = 'BYSETPOS=' . $value['offset']; + } + break; + } + break; + case 'day': + $rrule[] = 'BYMONTHDAY=' . $r_value; + break; + case 'month': + $rrule[] = 'BYMONTH=' . $r_value; + break; + case 'count': + $rrule[] = 'COUNT=' . $r_value; + break; + } + } + } + + if ($value['type'] === 'WEEKLY' && self::WEEKSTART != 'MO') { + $rrule[] = 'WKST=' . self::WEEKSTART; + } + + return implode(';', $rrule); + } + + /** + * Return the days from CalendarDate::days as attribute of a event recurrence. + * + * @param string $value + * @return string + */ + public function _exportWdays(string $value): string + { + $wdays_map = ['1' => 'MO', '2' => 'TU', '3' => 'WE', '4' => 'TH', '5' => 'FR', + '6' => 'SA', '7' => 'SU']; + $wdays = []; + preg_match_all('/(\d)/', $value, $matches); + foreach ($matches[1] as $match) { + $wdays[] = $wdays_map[$match]; + } + return implode(',', $wdays); + } + + /** + * Formats dates of exception. + * + * @param string $value Unix timestamps as csv list. + * @return string The formatted Exceptions. + */ + public function exportExDate(string $value): string + { + $exdates = []; + $date_times = explode(',', $value); + foreach ($date_times as $date_time) { + $exdates[] = $this->_exportDate($date_time); + } + return implode(',', $exdates); + } + + /** + * Formats date times of exception. + * + * @param string $value Unix timestamps as csv list. + * @return string The formatted Exceptions. + */ + public function exportExDateTime(string $value): string + { + $ex_dates = []; + $ex_date_times = explode(',', $value); + foreach ($ex_date_times as $ex_date_time) { + $date_time = new DateTime(); + $date_time->setTimestamp($ex_date_time); + $ex_dates[] = $date_time->format('Ymd\THis'); + } + return implode(',', $ex_dates); + } + + /** + * Returns iCalendar group event properties if the date has mor than one attendee. + * + * @param CalendarDate $date The date object to extract the group data from. + * @return string The formatted group event properties. + */ + private function exportGroupEventProperties(CalendarDate $date): string + { + if (!count($date->calendars)) { + return ''; + } + $organizer = $date->author; + if ($organizer) { + $properties = $this->foldLine('ORGANIZER;CN="' + . $organizer->getFullName() + . '":mailto:' . $organizer->Email) + . self::NEWLINE; + } else { + $properties = $this->foldLine('ORGANIZER;CN="' + . _('unbekannt') + . '":mailto:' . $GLOBALS['user']->email) + . self::NEWLINE; + } + foreach ($date->calendars as $calendar) { + if ($date->author_id === $calendar->range_id) { + if ($calendar->user) { + $properties .= $this->foldLine('ATTENDEE;' + . 'ROLE=REQ-PARTICIPANT;' + . 'CN="' . $calendar->user->getFullName() + . '":mailto:' . $calendar->user->Email) + . self::NEWLINE; + } else { + $properties = ''; + } + } else { + if ($calendar->user) { + switch ($calendar->participation) { + case 'ACCEPTED' : + $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT' + . ';PARTSTAT=ACCEPTED'; + break; + case 'ACKNOWLEDGED' : + $attendee = 'ATTENDEE;ROLE=NON-PARTICIPANT' + . ';PARTSTAT=ACCEPTED' + . ';DELEGATED-TO="mailto:' + . $this->getFacultyEmail($organizer->id) + . '"'; + break; + case 'DECLINED' : + $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT' + . ';PARTSTAT=DECLINED'; + break; + default : + $attendee = 'ATTENDEE;ROLE=REQ-PARTICIPANT'; + $attendee .= ';PARTSTAT=TENTATIVE'; + $attendee .= ';RSVP=TRUE'; + + } + $attendee .= ';CN="' . $calendar->user->getFullName() + . '":mailto:' . $calendar->user->Email; + $properties .= $this->foldLine($attendee) . self::NEWLINE; + } + } + } + return $properties; + } + + /** + * @param string $user_id + * @return string + */ + private function getFacultyEmail(string $user_id): string + { + $stmt = DBManager::get()->prepare(' + SELECT `email` + FROM `Institute` + LEFT JOIN `user_inst` USING(`institut_id`) + WHERE `Institute`.`Institut_id` = `fakultaets_id` + AND `user_id` = ?'); + $stmt->execute([$user_id]); + return $stmt->fetchColumn(); + } + + /** + * Returns the folded version of a text line. + * + * @param string $line + * @return string + */ + private function foldLine(string $line): string + { + $line = preg_replace('/(\r\n|\n|\r)/', '\n', $line); + if (mb_strlen($line) > 75) { + $foldedline = ''; + while ($line !== '') { + $maxLine = mb_substr($line, 0, 75); + $cutPoint = max(60, max(mb_strrpos($maxLine, ';'), mb_strrpos($maxLine, ':')) + 1); + + $foldedline .= ( empty($foldedline)) ? + mb_substr($line, 0, $cutPoint) : + self::NEWLINE . ' ' . mb_substr($line, 0, $cutPoint); + + $line = (mb_strlen($line) <= $cutPoint) ? '' : mb_substr($line, $cutPoint); + } + return $foldedline; + } + return $line; + } +} diff --git a/lib/calendar/CalendarParserICalendar.class.php b/lib/classes/calendar/ICalendarImport.class.php similarity index 59% rename from lib/calendar/CalendarParserICalendar.class.php rename to lib/classes/calendar/ICalendarImport.class.php index 241580af198eef066427216cc20587ad6d18b27b..e78696d58fe4254855fae850b779ede07b1c6010 100644 --- a/lib/calendar/CalendarParserICalendar.class.php +++ b/lib/classes/calendar/ICalendarImport.class.php @@ -1,91 +1,88 @@ -<? -# Lifter002: TODO -# Lifter007: TODO - -/** - * CalendarParserICalendar.class.php - * - * 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 Peter Thienel <thienel@data-quest.de>, Suchi & Berg GmbH <info@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @package calendar - */ - -class CalendarParserICalendar extends CalendarParser +<?php +class ICalendarImport { - public $type = ''; - protected $count = null; + private $range_id; - public function __construct() + private $count = 0; + + private $dates = []; + + private $import_time; + + private $convert_to_private = false; + + public function __construct($range_id) { - parent::__construct(); - $this->type = 'iCalendar'; - // initialize error handler - $GLOBALS['_calendar_error'] = new ErrorHandler(); + $this->range_id = $range_id; + $this->import_time = time(); } - public function getCount($data) + public function import($ical_data) + { + $this->parse($ical_data); + } + + public function countEvents($ical_data) { $matches = []; if (is_null($this->count)) { // Unfold any folded lines - $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data); - preg_match_all('/(BEGIN:VEVENT(\r\n|\r|\n)[\W\w]*?END:VEVENT\r?\n?)/', $data, $matches); + $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $ical_data); + preg_match_all('/(BEGIN:VEVENT(\r\n|\r|\n)[\W\w]*?END:VEVENT\r?\n?)/', $ical_data, $matches); $this->count = sizeof($matches[1]); } return $this->count; } + public function getCountEvents() : int + { + return (int) $this->count; + } + + public function convertPublicToPrivate(bool $to_private = true) : void + { + $this->convert_to_private = $to_private; + } + /** * Parse a string containing vCalendar data. * * @access private - * @param String $data The data to parse - * + * @param string $data The data to parse */ - public function parse($data, $ignore = null) + public function parse(string $data) { - global $_calendar_error, $PERS_TERMIN_KAT; - // match categories $studip_categories = []; $i = 1; - foreach ($PERS_TERMIN_KAT as $cat) { + foreach ($GLOBALS['PERS_TERMIN_KAT'] as $cat) { $studip_categories[mb_strtolower($cat['name'])] = $i++; } // Unfold any folded lines // the CR is optional for files imported from Korganizer (non-standard) - $data = preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data); + $data = $this->unfoldLine($data); if (!preg_match('/BEGIN:VCALENDAR(\r\n|\r|\n)([\W\w]*)END:VCALENDAR\r?\n?/', $data, $matches)) { - $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Import-Datei ist keine gültige iCalendar-Datei!")); - return false; + throw new UnexpectedValueException(); } // client identifier - if (!$this->_parseClientIdentifier($matches[2])) { - return false; + if (!$this->parseClientIdentifier($matches[2])) { + throw new UnexpectedValueException(); } // All sub components if (!preg_match_all('/BEGIN:VEVENT(\r\n|\r|\n)([\w\W]*?)END:VEVENT(\r\n|\r|\n)/', $matches[2], $v_events)) { - $_calendar_error->throwError(ErrorHandler::ERROR_MESSAGE, _("Die importierte Datei enthält keine Termine.")); - return true; + // _("Die importierte Datei enthält keine Termine.") + throw new UnexpectedValueException(); } if ($this->count) { $this->count = 0; } foreach ($v_events[2] as $v_event) { - $properties['CLASS'] = 'PRIVATE'; - // Parse the remain attributes if (preg_match_all('/(.*):(.*)(\r|\n)+/', $v_event, $matches)) { $properties = []; @@ -111,7 +108,7 @@ class CalendarParserICalendar extends CalendarParser if ($params['ENCODING']) { switch ($params['ENCODING']) { case 'QUOTED-PRINTABLE': - $value = $this->_qp_decode($value); + $value = $this->qp_decode($value); break; case 'BASE64': @@ -139,7 +136,7 @@ class CalendarParserICalendar extends CalendarParser $categories[] = $category; } else if (!$properties['STUDIP_CATEGORY']) { $properties['STUDIP_CATEGORY'] - = $studip_categories[mb_strtolower($category)]; + = $studip_categories[mb_strtolower($category)]; } } $properties[$tag] = implode(',', $categories); @@ -147,12 +144,11 @@ class CalendarParserICalendar extends CalendarParser // Date fields case 'DCREATED': // vCalendar property name for "CREATED" - $tag = "CREATED"; case 'DTSTAMP': case 'COMPLETED': case 'CREATED': case 'LAST-MODIFIED': - $properties[$tag] = $this->_parseDateTime($value); + $properties[$tag] = $this->parseDateTime($value); break; case 'DTSTART': @@ -162,30 +158,30 @@ class CalendarParserICalendar extends CalendarParser $check['DAY_EVENT'] = true; case 'DUE': case 'RECURRENCE-ID': - $properties[$tag] = $this->_parseDateTime($value); + $properties[$tag] = $this->parseDateTime($value); break; case 'RDATE': if (array_key_exists('VALUE', $params)) { if ($params['VALUE'] == 'PERIOD') { - $properties[$tag] = $this->_parsePeriod($value); + $properties[$tag] = $this->parsePeriod($value); } else { - $properties[$tag] = $this->_parseDateTime($value); + $properties[$tag] = $this->parseDateTime($value); } } else { - $properties[$tag] = $this->_parseDateTime($value); + $properties[$tag] = $this->parseDateTime($value); } break; case 'TRIGGER': if (array_key_exists('VALUE', $params)) { if ($params['VALUE'] == 'DATE-TIME') { - $properties[$tag] = $this->_parseDateTime($value); + $properties[$tag] = $this->parseDateTime($value); } else { - $properties[$tag] = $this->_parseDuration($value); + $properties[$tag] = $this->parseDuration($value); } } else { - $properties[$tag] = $this->_parseDuration($value); + $properties[$tag] = $this->parseDuration($value); } break; @@ -198,12 +194,12 @@ class CalendarParserICalendar extends CalendarParser foreach ($values[1] as $value) { if (array_key_exists('VALUE', $params)) { if ($params['VALUE'] == 'DATE-TIME') { - $dates[] = $this->_parseDateTime($value); + $dates[] = $this->parseDateTime($value); } else if ($params['VALUE'] == 'DATE') { - $dates[] = $this->_parseDate($value); + $dates[] = $this->parseDate($value); } } else { - $dates[] = $this->_parseDateTime($value); + $dates[] = $this->parseDateTime($value); } } // some iCalendar exports (e.g. KOrganizer) use an EXDATE-entry for every @@ -213,7 +209,7 @@ class CalendarParserICalendar extends CalendarParser // Duration fields case 'DURATION': - $attibutes[$tag] = $this->_parseDuration($value); + $attibutes[$tag] = $this->parseDuration($value); break; // Period of time fields @@ -222,7 +218,7 @@ class CalendarParserICalendar extends CalendarParser $periods = []; preg_match_all('/,([^,]*)/', ',' . $value, $values); foreach ($values[1] as $value) { - $periods[] = $this->_parsePeriod($value); + $periods[] = $this->parsePeriod($value); } $properties[$tag] = $periods; @@ -231,11 +227,11 @@ class CalendarParserICalendar extends CalendarParser // UTC offset fields case 'TZOFFSETFROM': case 'TZOFFSETTO': - $properties[$tag] = $this->_parseUtcOffset($value); + $properties[$tag] = $this->parseUtcOffset($value); break; case 'PRIORITY': - $properties[$tag] = $this->_parsePriority($value); + $properties[$tag] = $this->parsePriority($value); break; case 'CLASS': @@ -269,7 +265,7 @@ class CalendarParserICalendar extends CalendarParser // Recursion fields case 'EXRULE': case 'RRULE': - $properties[$tag] = $this->_parseRecurrence($value); + $properties[$tag] = $this->parseRecurrence($value); break; default: @@ -279,20 +275,22 @@ class CalendarParserICalendar extends CalendarParser } } - if (!$properties['RRULE']['rtype']) + if (!$properties['RRULE']['rtype']) { $properties['RRULE'] = ['rtype' => 'SINGLE']; + } - if (!$properties['LAST-MODIFIED']) - $properties['LAST-MODIFIED'] = $properties['CREATED']; + if (!$properties['LAST-MODIFIED']) { + $properties['LAST-MODIFIED'] = $properties['DTSTAMP'] ?: $properties['CREATED'] ?? time(); + } if (!$properties['DTSTART'] || ($properties['EXDATE'] && !$properties['RRULE'])) { - $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!")); - $this->count = 0; - return false; + // _("Die Datei ist keine gültige iCalendar-Datei!") + throw new UnexpectedValueException(); } - if (!$properties['DTEND']) + if (!$properties['DTEND']) { $properties['DTEND'] = $properties['DTSTART']; + } // day events starts at 00:00:00 and ends at 23:59:59 if ($check['DAY_EVENT']) @@ -300,7 +298,7 @@ class CalendarParserICalendar extends CalendarParser // default: all imported events are set to private if (!$properties['CLASS'] - || ($this->public_to_private && $properties['CLASS'] == 'PUBLIC')) { + || ($this->convert_to_private && $properties['CLASS'] == 'PUBLIC')) { $properties['CLASS'] = 'PRIVATE'; } @@ -312,11 +310,10 @@ class CalendarParserICalendar extends CalendarParser * */ - $this->components[] = $properties; + $this->createDateFromProperties($properties); } else { - $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!")); - $this->count = 0; - return false; + // _("Die Datei ist keine gültige iCalendar-Datei!") + throw new InvalidValuesException(); } $this->count++; } @@ -324,163 +321,210 @@ class CalendarParserICalendar extends CalendarParser return true; } + private function createDateFromProperties($properties) + { + $date = CalendarDate::findOneBySQL( + 'LEFT JOIN `calendar_date_assignments` + ON `calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id` + WHERE `calendar_dates`.`unique_id` = :uid + AND `calendar_date_assignments`.`range_id` = :range_id', + [ + ':uid' => $properties['UID'], + ':range_id' => $this->range_id + ] + ); + + if (!$date) { + $date = new CalendarDate(); + $date->id = $date->getNewId(); + $date->author_id = $this->range_id; + $date->editor_id = $this->range_id; + $range_date = new CalendarDateAssignment(); + $range_date->range_id = $this->range_id; + $range_date->participation = ''; + $date->calendars[] = $range_date; + } + + $date->begin = $properties['DTSTART']->getTimestamp(); + $date->end = $properties['DTEND']->getTimestamp(); + $date->title = $properties['SUMMARY']; + $date->description = $properties['DESCRIPTION']; + $date->access = $properties['CLASS'] ?? 'PRIVATE'; + $date->user_category = $properties['CATEGORIES']; + $date->category = $properties['STUDIP_CATEGORY'] ?: 1; + $date->priority = $properties['PRIORITY'] ?? ''; + $date->location = $properties['LOCATION']; + if (is_array($properties['EXDATE'])) { + foreach ($properties['EXDATE'] as $exdate) { + $exception = new CalendarDateException(); + $exception->date = $exdate->format('Y-m-d'); + $date->exceptions[] = $exception; + } + } + $date->mkdate = $properties['CREATED'] ? $properties['CREATED']->getTimestamp() : time(); + if (isset($properties['LAST-MODIFIED'])) { + $date->chdate = $properties['LAST-MODIFIED']->getTimestamp(); + } else { + $date->chdate = $date->mkdate; + } + $date->import_date = $this->import_time; + $date->unique_id = $properties['UID']; + + $this->setRecurrenceRule($date, $properties['RRULE']); + $date->store(); + } + + private function setRecurrenceRule(CalendarDate $date, $rrule) + { + $date->interval = $rrule['linterval'] ?? 1; + if (strlen($rrule['wdays'] ?? '')) { + $date->offset = $rrule['sinterval'] ?? 0; + $date->days = $rrule['wdays'] ?? null; + } else { + $date->offset = $rrule['day'] ?? 0; + $date->days = $rrule['sinterval'] ?? null; + } + $date->month = $rrule['month'] ?? null; + $date->repetition_type = $rrule['rtype'] ?? 'SINGLE'; + $date->number_of_dates = $rrule['count'] ?? 1; + $date->repetition_end = $rrule['expire'] ?? 0; + } + + private function unfoldLine($data) + { + return preg_replace('/\x0D?\x0A[\x20\x09]/', '', $data); + } + /** * Parse a UTC Offset field */ - private function _parseUtcOffset($text) + private function parseUtcOffset($offset_text) { $offset = 0; - if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $matches)) { + if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $offset_text, $matches)) { $offset += 3600 * intval($matches[2]); $offset += 60 * intval($matches[3]); $offset *= ( $matches[1] == '+' ? 1 : -1); if (array_key_exists(4, $matches)) { $offset += intval($matches[4]); } - return $offset; - } else { - return false; } + return $offset; } /** * Parse a Time Period field */ - private function _parsePeriod($text) + private function parsePeriod($period_text): array { - $matches = explode('/', $text); + $matches = explode('/', $period_text); - $start = $this->_parseDateTime($matches[0]); + $start = $this->parseDateTime($matches[0]); - if ($duration = $this->_parseDuration($matches[1])) { + if ($duration = $this->parseDuration($matches[1])) { return ['start' => $start, 'duration' => $duration]; - } else if ($end = $this->_parseDateTime($matches[1])) { + } else if ($end = $this->parseDateTime($matches[1])) { return ['start' => $start, 'end' => $end]; } + return []; } /** * Parse a DateTime field */ - private function _parseDateTime($text) + private function parseDateTime(String $date_time) { - $dateParts = explode('T', $text); - if (count($dateParts) != 2 && !empty($text)) { - // not a date time field but may be just a date field - if (!$date = $this->_parseDate($text)) { - return $date; - } - $date = $this->_parseDate($text); - return mktime(0, 0, 0, $date['month'], $date['mday'], $date['year']); + $parts = explode('T', $date_time); + if (count($parts) != 2) { + // not a date time string but may be just a date string + $date = $this->parseDate($date_time); + return DateTimeImmutable::createFromFormat('YmdHis', implode('', $date) . '000000'); } - if (!$date = $this->_parseDate($dateParts[0])) { - return $date; - } - if (!$time = $this->_parseTime($dateParts[1])) { - return $time; - } + $date = $this->parseDate($parts[0]); + $time = $this->parseTime($parts[1]); if ($time['zone'] == 'UTC') { - return gmmktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']); + $time_zone = new DateTimeZone('UTC'); } else { - return mktime($time['hour'], $time['minute'], $time['second'], $date['month'], $date['mday'], $date['year']); + $time_zone = new DateTimeZone('Europe/Berlin'); } + return DateTimeImmutable::createFromFormat( + 'YmdHis', + implode('', $date) . $time['hour'] . $time['minute'] . $time['second'], + $time_zone + ); } /** * Parse a Time field */ - private function _parseTime($text) + private function parseTime($time_text): array { - if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $matches)) { - $time['hour'] = intval($matches[1]); - $time['minute'] = intval($matches[2]); - $time['second'] = intval($matches[3]); + $matches = []; + if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $time_text, $matches)) { + $time['hour'] = $matches[1]; + $time['minute'] = $matches[2]; + $time['second'] = $matches[3]; if (array_key_exists(4, $matches)) { $time['zone'] = 'UTC'; } else { $time['zone'] = 'LOCAL'; } return $time; - } else { - return false; } + throw new InvalidValuesException(); } /** * Parse a Date field */ - private function _parseDate($text) + private function parseDate($date_text): array { - if (mb_strlen(trim($text)) !== 8) { - return false; + $matches = []; + if (preg_match('/([0-9]{4})([0-9]{2})([0-9]{2})/', $date_text, $matches)) { + $date['year'] = $matches[1]; + $date['month'] = $matches[2]; + $date['mday'] = $matches[3]; + return $date; } - - $date['year'] = intval(mb_substr($text, 0, 4)); - $date['month'] = intval(mb_substr($text, 4, 2)); - $date['mday'] = intval(mb_substr($text, 6, 2)); - - return $date; + throw new InvalidValuesException(); } /** * Parse a Duration Value field */ - private function _parseDuration($text) + private function parseDuration($interval_text): DateInterval { - if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $matches)) { - // weeks - $duration = 7 * 86400 * intval($matches[3]); - if (count($matches) > 4) { - // days - $duration += 86400 * intval($matches[4]); - } - if (count($matches) > 5) { - // hours - $duration += 3600 * intval($matches[7]); - // mins - if (array_key_exists(8, $matches)) { - $duration += 60 * intval($matches[8]); - } - // secs - if (array_key_exists(9, $matches)) { - $duration += intval($matches[9]); - } - } - // sign - if ($matches[1] == "-") { - $duration *= - 1; - } - - return $duration; - } else { - return false; - } + return new DateInterval($interval_text); } - private function _parsePriority($value) + private function parsePriority($value) { $value = intval($value); if ($value > 0 && $value < 5) { - return 1; + return 'HIGH'; } if ($value == 5) { - return 2; + return 'MEDIUM'; } if ($value > 5 && $value < 10) { - return 3; + return 'LOW'; } - return 0; + return ''; } /** - * Parse a Recurrence field + * Parse a recurrence rule. + * + * @param $text string The text of the recurrence rule. + * @return array The translated recurrence rule as array. + * @throws InvalidValuesException */ - private function _parseRecurrence($text) + private function parseRecurrence($text): array { global $_calendar_error; @@ -498,13 +542,14 @@ class CalendarParserICalendar extends CalendarParser $r_rule['rtype'] = trim($match[2]); break; default: - $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")); - break; + throw new InvalidValuesException( + _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.") + ); } break; case 'UNTIL' : - $r_rule['expire'] = $this->_parseDateTime($match[2]); + $r_rule['expire'] = $this->parseDateTime($match[2]); break; case 'COUNT' : @@ -520,22 +565,22 @@ class CalendarParserICalendar extends CalendarParser case 'BYHOUR' : case 'BYWEEKNO' : case 'BYYEARDAY' : - $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")); - break; - + throw new InvalidValuesException( + _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.") + ); case 'BYDAY' : - $byday = $this->_parseByDay($match[2]); + $byday = $this->parseByDay($match[2]); $r_rule['wdays'] = $byday['wdays']; if ($byday['sinterval']) $r_rule['sinterval'] = $byday['sinterval']; break; case 'BYMONTH' : - $r_rule['month'] = $this->_parseByMonth($match[2]); + $r_rule['month'] = $this->parseByMonth($match[2]); break; case 'BYMONTHDAY' : - $r_rule['day'] = $this->_parseByMonthDay($match[2]); + $r_rule['day'] = $this->parseByMonthDay($match[2]); break; case 'BYSETPOS': @@ -551,7 +596,7 @@ class CalendarParserICalendar extends CalendarParser return $r_rule; } - private function _parseByDay($text) + private function parseByDay($text) { global $_calendar_error; @@ -564,12 +609,15 @@ class CalendarParserICalendar extends CalendarParser $wdays .= $wdays_map[$match[2]]; if ($match[1]) { if (!$sinterval && ((int) $match[1]) > 0 || $match[1] == '-1') { - if ($match[1] == '-1') + if ($match[1] == '-1') { $sinterval = '5'; - else + } else { $sinterval = $match[1]; + } } else { - $_calendar_error->throwSingleError('parse', ErrorHandler::ERROR_WARNING, _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.")); + throw new InvalidValuesException( + _("Der Import enthält Kalenderdaten, die Stud.IP nicht korrekt darstellen kann.") + ); } } } @@ -577,42 +625,40 @@ class CalendarParserICalendar extends CalendarParser return $wdays ? ['wdays' => $wdays, 'sinterval' => $sinterval] : false; } - private function _parseByMonthDay($text) + private function parseByMonthDay($text) { $days = explode(',', $text); - if (sizeof($days) > 1 || ((int) $days[0]) < 0) { + if (count($days) > 1 || ((int) $days[0]) < 0) { return false; } return $days[0]; } - private function _parseByMonth($text) + private function parseByMonth($text) { $months = explode(',', $text); - if (sizeof($months) > 1) { + if (count($months) > 1) { return false; } return $months[0]; } - private function _qp_decode($value) + private function qp_decode($value) { return preg_replace_callback("/=([0-9A-F]{2})/", function ($m) {return chr(hexdec($m[1]));}, $value); } - private function _parseClientIdentifier(&$data) + private function parseClientIdentifier(&$data) { global $_calendar_error; if ($this->client_identifier == '') { - if (!preg_match('/PRODID((;[\W\w]*)*):([\W\w]+?)(\r\n|\r|\n)/', $data, $matches)) { - $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!")); - return false; - } elseif (!trim($matches[3])) { - $_calendar_error->throwError(ErrorHandler::ERROR_CRITICAL, _("Die Datei ist keine gültige iCalendar-Datei!")); - return false; + if (!preg_match('/PRODID((;[\W\w]*)*):([\W\w]+?)(\r\n|\r|\n)/', $data, $matches) + || !trim($matches[3])) { + // _("Die Datei ist keine gültige iCalendar-Datei!") + throw new InvalidValuesException(); } else { $this->client_identifier = trim($matches[3]); } @@ -623,7 +669,7 @@ class CalendarParserICalendar extends CalendarParser public function getClientIdentifier($data = null) { if (!is_null($data)) { - $this->_parseClientIdentifier($data); + $this->parseClientIdentifier($data); } return $this->client_identifier; diff --git a/lib/classes/calendar/Owner.interface.php b/lib/classes/calendar/Owner.interface.php new file mode 100644 index 0000000000000000000000000000000000000000..a7c2519ed350fcc697647e347b42bc28763a79e7 --- /dev/null +++ b/lib/classes/calendar/Owner.interface.php @@ -0,0 +1,40 @@ +<?php + +namespace Studip\Calendar; + +/** + * The Studip\Calendar\Owner interface defines methods that classes whose instances own calendars + * shall implement to faciliate permission checks for that calendars. + */ +interface Owner +{ + /** + * Retrieves the Owner object for a specified owner-ID. + * + * @param string $owner_id The ID of the owner. + * + * @return Owner|null Either the Owner object if it can be found or null in case + * it cannot be found. + */ + public static function getCalendarOwner(string $owner_id) : ?Owner; + + /** + * Determines whether the specified user has read permissions to the calendar. + * + * @param string|null $user_id The ID of the user for which to determine write permissions. + * Defaults to the current user if no user-ID is provided. + * + * @return bool True, if the user has read permissions, false otherwise. + */ + public function isCalendarReadable(?string $user_id = null) : bool; + + /** + * Determines whether the specified user has write permissions to the calendar. + * + * @param string|null $user_id The ID of the user for which to determine write permissions. + * Defaults to the current user if no user-ID is provided. + * + * @return bool True, if the user has write permissions, false otherwise. + */ + public function isCalendarWritable(?string $user_id = null) : bool; +} diff --git a/lib/classes/calendar/SingleCalendar.php b/lib/classes/calendar/SingleCalendar.php deleted file mode 100644 index 938db1a134a238d047ecbef3573995e7f14257dc..0000000000000000000000000000000000000000 --- a/lib/classes/calendar/SingleCalendar.php +++ /dev/null @@ -1,1488 +0,0 @@ -<?php -/** - * SingleCalendar.php - Model class for a 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 3.2 - */ - -class SingleCalendar -{ - /** - * This collection holds all Events in this calendar. - * - * @var SimpleORMapCollection Collection of Objects which inherits Event. - */ - public $events; - - /** - * The owner of this calendar. - * - * @var Object A Stud.IP object of type User, Institute or Course. - */ - public $range_object; - - public $type; - - public $range; - - /** - * The start of this calendar. - * - * @var int Unix timestamp. - */ - public $start; - - /** - * The end of this calendar. - * - * @var int Unix timestamp. - */ - public $end; - - public $ts; - - public function __construct($range_id, $start = null, $end = null) - { - $this->setRangeObject($range_id); - $this->start = $start ?: 0; - $this->end = $end ?: Calendar::CALENDAR_END; - $this->events = new SimpleORMapCollection(); - $this->events->setClassName('Event'); - $this->type = get_class($this->range_object); - } - - /** - * Sets the range object and checks whether the calendar is available. - * - * @param string $range_id The id of a course, institute or user. - * @throws AccessDeniedException - */ - private function setRangeObject($range_id) - { - $this->range_object = get_object_by_range_id($range_id); - if (!is_object($this->range_object)) { - throw new AccessDeniedException(); - } - $range_map = [ - 'User' => Calendar::RANGE_USER, - 'Course' => Calendar::RANGE_SEM, - 'Institute' => Calendar::RANGE_INST - ]; - $this->range = $range_map[get_class($this->range_object)]; - if ($this->range == Calendar::RANGE_INST - || $this->range == Calendar::RANGE_SEM) { - - if (!$this->range_object->isToolActive('CoreCalendar')) { - throw new AccessDeniedException(); - } - } - } - - /** - * Returns all events of given class names between start and end. - * Returns events of all types if no class names are given. - * - * @param array|null $class_names The names of classes that implements Event. - * @param int $start The start date time. - * @param int $end The end date time. - * @return \SingleCalendar This calendar object. - * @throws InvalidArgumentException - */ - public function getEvents($class_names = null, $start = null, $end = null) - { - $start = !is_null($start) ? $start : $this->start; - $end = !is_null($end) ? $end : $this->end; - if (!is_array($class_names)) { - $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent']; - } - $events = $this->events->getArrayCopy(); - foreach ($class_names as $type) { - if (in_array('Event', class_implements($type))) { - $events = array_merge($events, $type::getEventsByInterval( - $this->range_object->getId(), new DateTime('@' . $start), - new DateTime('@' . $end))->getArrayCopy()); - } else { - throw new InvalidArgumentException(sprintf('Class %s does not implements Event.', $type)); - } - } - $this->events = SimpleORMapCollection::createFromArray($events, false); - $this->events->setClassName('Event'); - return $this; - } - - /** - * Stores the event in the calendars of all attendees. - * - * @param CalendarEvent $event The event to store. - * @param array $attendee_ids The user ids of the attendees. - * @return bool|int The number of stored events or false if an error occured. - */ - public function storeEvent(CalendarEvent $event, $attendee_ids = []) - { - $attendee_ids = array_filter($attendee_ids, function($id) { - return trim($id) != ''; - }); - if (!$attendee_ids) { - $attendee_ids = [$GLOBALS['user']->id]; - } - if (count($attendee_ids) === 1) { - if (!$this->havePermission(Calendar::PERMISSION_WRITABLE)) { - return false; - } - $is_new = $event->isNew(); - $stored = $event->store(); - if ($stored !== false && $this->getRange() == Calendar::RANGE_USER - && $this->getRangeId() != $GLOBALS['user']->id) { - $this->sendStoreMessage($event, $is_new); - } - return $stored; - } - - if (in_array($this->getRangeId(), $attendee_ids)) { - // set default status if the organizer is an attendee... - $event->group_status = CalendarEvent::PARTSTAT_TENTATIVE; - } - if ($event->isNew()) { - return $this->storeAttendeeEvents($event, $attendee_ids); - } - - if (!$event->havePermission(Event::PERMISSION_WRITABLE)) { - return false; - } - - return $this->storeAttendeeEvents($event, $attendee_ids); - } - - /** - * Helper function for SingleCalendar::storeEvent(). - * - * @param CalendarEvent $event The event to store. - * @param type $attendee_ids The user ids of the attendees. - * @return bool|int The number of stored events or false if an error occured. - */ - private function storeAttendeeEvents(CalendarEvent $event, $attendee_ids) - { - $ret = 0; - $new_attendees = []; - $recipient_ids = []; - $is_new = false; - foreach ($attendee_ids as $attendee_id) { - if (trim($attendee_id)) { - $attendee_calendar = new SingleCalendar($attendee_id); - - // SEMBBS - // Gruppentermine können ab Calendar::PERMISSION_READABLE angelegt werden - // if ($attendee_calendar->havePermission(Calendar::PERMISSION_READABLE)) { - - if ($attendee_calendar->havePermission(Calendar::PERMISSION_WRITABLE) - || Config::get()->CALENDAR_GRANT_ALL_INSERT) { - $attendee_event = new CalendarEvent( - [$attendee_calendar->getRangeId(), $event->event_id]); - $attendee_event->event = $event->event; - $is_new = $attendee_event->isNew(); - if ($is_new) { - $attendee_event->group_status = $event->group_status; - } - $stored = $attendee_event->store(); - if ($stored !== false) { - // send message if not own calendar - if (!$attendee_calendar->havePermission(Calendar::PERMISSION_OWN)) { - $recipient_ids[] = $attendee_event->range_id; - } - $new_attendees[] = $attendee_event->range_id; - $ret += $stored; - } - } - } - } - if (count($recipient_ids)) { - $this->sendStoreMessage($attendee_event, $is_new, $recipient_ids); - } - - $events_delete = CalendarEvent::findBySQL('event_id = ? AND range_id NOT IN(?)', - [$event->event_id, $new_attendees]); - foreach ($events_delete as $event_delete) { - $calendar = new SingleCalendar($event_delete->range_id); - $calendar->deleteEvent($event_delete); - } - return $ret; - } - - /** - * Sets the start date time by given unix timestamp. - * - * @param int $start Unix timestamp. - */ - public function setStart($start) - { - $this->start = $start; - return $this; - } - - /** - * Returns the start date time of this calendar as a unix timestamp. - * - * @return int Unix timestamp. - */ - public function getStart() - { - return $this->start; - } - - /** - * Sets the end date time by given unix timestamp. - * - * @param int $end Unix timestamp. - */ - public function setEnd($end) - { - $this->end = $end; - return $this; - } - - /** - * Returns the end date time of this calendar as a unix timestamp. - * - * @return int Unix timestamp. - */ - public function getEnd() - { - return $this->end; - } - - /** - * Returns a event by given $event_id. Returns a new event of type - * CalendarEvent with default data if the id is null or unknown. - * If $class_names is set, only these types of Object will be returned. - * - * @param string $event_id - * @param array $class_names Names of classes which inherits Event. - * @return Event|null The found event, a new CalendarEvent or null if no - * event other than a CalendarEvent was found. - */ - public function getEvent($event_id = null, $class_names = null) - { - if (!is_array($class_names)) { - $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent']; - } - foreach ($class_names as $type) { - if ($type == 'CalendarEvent') { - $event = CalendarEvent::find([$this->getRangeId(), $event_id]); - } else { - $event = $type::find($event_id); - } - if ($event && $event->havePermission(Event::PERMISSION_READABLE)) { - return $event; - } - } - return $this->getNewEvent(); - } - - /** - * Creates a new event, sets some default data and returns it. - * - * @return \CalendarEvent The new event. - */ - public function getNewEvent() - { - $event_data = new EventData(); - $event_data->setId($event_data->getNewId()); - $now = time(); - $event_data->start = $now; - $event_data->end = $now + 3600; - $calendar_event = new CalendarEvent(); - $calendar_event->setId([$this->getRangeId(), $event_data->getId()]); - $calendar_event->event = $event_data; - return $calendar_event; - } - - /** - * Sorts all events by start time. - */ - public function sortEvents() - { - $this->events->orderBy('start'); - } - - /** - * An alias for SingleCalendar::getRangeId(). - * - * @see SingleCalendar::getRangeId() - */ - public function getId() - { - return $this->getRangeId(); - } - - /** - * Returns the range id of this calendar. - * Possible range id are for objects of type user, inst, fak, group. - * - * @return string The range id. - */ - public function getRangeId() - { - return $this->range_object->getId(); - } - - /** - * Returns the object range of this calendar. - * - * @return int The object range. - */ - public function getRange() - { - return $this->range; - } - - /** - * Returns the range object (user, course, institute) of this calendar. - * - * @return int The object range. - */ - public function getRangeObject() - { - return $this->range_object; - } - - /** - * Returns the permission of the given user for this calendar. - * - * @param string $user_id User id. - * @return int The calendar permission. - */ - public function getPermissionByUser($user_id = null) - { - static $user_permission = []; - - $user_id = $user_id ?: $GLOBALS['user']->id; - $id = $user_id . $this->getRangeId(); - if (!empty($user_permission[$id])) { - return $user_permission[$id]; - } - // own calendar - if ($this->range == Calendar::RANGE_USER - && $this->getRangeId() == $user_id) { - $user_permission[$id] = Calendar::PERMISSION_OWN; - return $user_permission[$id]; - } - switch ($this->type) { - case 'User' : - // alle Lehrenden haben gegenseitig schreibenden Zugriff, ab dozent immer schreibenden Zugriff - /* - if ($GLOBALS['perm']->have_perm('dozent') && $GLOBALS['perm']->get_perm($this->range_object->getId()) == 'dozent') { - return Calendar::PERMISSION_WRITABLE; - } - * - */ - $cal_user = CalendarUser::find([$this->getRangeId(), $user_id]); - if ($cal_user) { - switch ($cal_user->permission) { - case 1 : - $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN; - break; - case 2 : - $user_permission[$id] = Calendar::PERMISSION_READABLE; - break; - case 4 : - $user_permission[$id] = Calendar::PERMISSION_WRITABLE; - break; - default : - $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN; - } - } else { - $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN; - } - break; - /* - case 'group' : - $stmt = DBManager::get()->prepare('SELECT range_id FROM statusgruppen WHERE statusgruppe_id = ?'); - $stmt->execute(array($range_id)); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - if ($result) { - if ($result['range_id'] == $user_id) { - return Calendar::PERMISSION_OWN; - } - } - return Calendar::PERMISSION_FORBIDDEN; - * - */ - case 'Course' : - switch ($GLOBALS['perm']->get_studip_perm($this->range_object->getId(), $user_id)) { - case 'user' : - case 'autor' : - $user_permission[$id] = Calendar::PERMISSION_READABLE; - break; - case 'tutor' : - case 'dozent' : - case 'admin' : - case 'root' : - $user_permission[$id] = Calendar::PERMISSION_WRITABLE; - break; - default : - $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN; - } - break; - case 'Institute' : - switch ($GLOBALS['perm']->get_studip_perm($this->range_object->getId(), $user_id)) { - case 'user' : - $user_permission[$id] = Calendar::PERMISSION_READABLE; - break; - case 'autor' : - $user_permission[$id] = Calendar::PERMISSION_READABLE; - break; - case 'tutor' : - case 'dozent' : - case 'admin' : - case 'root' : - $user_permission[$id] = Calendar::PERMISSION_WRITABLE; - break; - default : - // readable for all - $user_permission[$id] = Calendar::PERMISSION_READABLE; - } - break; - default : - $user_permission[$id] = Calendar::PERMISSION_FORBIDDEN; - } - return $user_permission[$id]; - } - - /** - * Returns whether the given user has at least the $permission to this calendar. - * It checks for the actual user if $user_id is null. - * - * @param int $permission An accepted calendar permission. - * @param string|null $user_id The id of the user. - * @return bool True if the user has at least the given permission. - */ - public function havePermission($permission, $user_id = null) - { - $user_id = $user_id ?: $GLOBALS['user']->id; - return ($permission <= $this->getPermissionByUser($user_id)); - } - - /** - * Returns whether the given user has the $permission to this calendar. - * It checks for the actual user if $user_id is null. - * - * @param int $permission An accepted calendar permission. - * @param string|null $user_id The id of the user. - * @return bool True if the user has the given permission. - */ - public function checkPermission($permission, $user_id = null) - { - $user_id = $user_id ?: $GLOBALS['user']->id; - return $permission == $this->getPermissionByUser($user_id); - } - - /** - * Sends a message to the owner of the calendar that a new event was inserted - * or an old event was modified by another user. - * - * @param CalendarEvent $event The new or updated event. - * @param bool $is_new True if the event is new. - * @param null|array Array with user_ids of the recipients. If not set the - * owner of the given event is used as recipient. - */ - protected function sendStoreMessage($event, $is_new, $recipient_ids = null) - { - $message = new messaging(); - $event_data = ''; - - if ($is_new) { - $msg_text = sprintf(_("%s hat einen neuen Termin in Ihren Kalender eingetragen."), get_fullname()); - $subject = strftime(_('Neuer Termin am %c'), $event->getStart()); - $msg_text .= "\n\n**"; - } else { - $msg_text = sprintf(_("%s hat einen Termin in Ihrem Kalender geändert."), get_fullname()); - $subject = strftime(_('Termin am %c geändert'), $event->getStart()); - $msg_text .= "\n\n**"; - } - $msg_text .= _('Zeit') . ':' . '** ' . strftime(' %c - ', $event->getStart()) - . strftime('%c', $event->getEnd()) . "\n**"; - $msg_text .= _("Zusammenfassung") . ':** ' . $event->getTitle() . "\n"; - if ($event_data = $event->getDescription()) { - $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n"; - } - if ($event_data = $event->toStringCategories()) { - $msg_text .= '**' . _('Kategorie') . ":** $event_data\n"; - } - if ($event_data = $event->toStringPriority()) { - $msg_text .= '**' . _('Priorität') . ":** $event_data\n"; - } - if ($event_data = $event->toStringAccessibility()) { - $msg_text .= '**' . _('Zugriff') . ":** $event_data\n"; - } - if ($event_data = $event->toStringRecurrence()) { - $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n"; - } - if (Config::get()->CALENDAR_GROUP_ENABLE && $event->attendees->count()) { - $msg_text .= '**' . _("Teilnehmende") . ':** '; - $msg_text .= implode(', ', $event->attendees->map( - function ($att) use ($event) { - $att_name = $att->user->getFullname(); - if ($event->havePermission(Event::PERMISSION_OWN, $att->user->getId())) { - $att_name .= ' (' . _('Organisator') . ')'; - } else { - if ($event->toStringGroupStatus()) { - $att_name .= ' (' . $att->toStringGroupStatus() . ')'; - } - } - return $att_name; - })); - $msg_text .= "\n"; - } - $msg_text .= "\n\n" . _('Hier kommen Sie direkt zum Termin in Ihrem Kalender:') . "\n"; - $msg_text .= URLHelper::getURL('dispatch.php/calendar/single/edit/' - . implode('/', $event->getId()), false); - - $recipient_unames = is_array($recipient_ids) - ? array_map('get_username', $recipient_ids) - : [get_username($event->range_id)]; - - $message->insert_message($msg_text, $recipient_unames, - '____%system%____', '', '', '', '', $subject); - } - - - /** - * Deletes a calendar event with regard of the consultation component. - * Depending whether a CalenderEvent instance is related to a consultation booking - * or not, the deletion has to be done differently. - * - * @param CalendarEvent $event - * @return bool True on success, false on failure. - */ - protected function deleteEventWithConsultation(CalendarEvent $event) : bool - { - // If the event belongs to a consultation booking, cancel the booking, - // so that it is available for others. - $booking = ConsultationBooking::findOneByStudent_event_id($event->event_id); - if ($booking) { - // Delete the event indirectly by cancelling the consultation booking: - $booking->cancel(); - return true; - } - - //Delete the consultation event from the consultation block. - $consultation_event = ConsultationEvent::findOneByEvent_id($event->event_id); - // Check if the slot is empty and delete it, if so. - if ($consultation_event && $consultation_event->slot) { - $consultation_event->slot->bookings->cancel(); - $consultation_event->slot->delete(); - return true; - } - - // Delete the event - return $event->delete(); - } - - /** - * Deletes an event from this calendar. - * - * @param string|object $calendar_event The id of an event or an event object of type CalendarEvent. - * @param boolean $all If true all events of a group event will be deleted. - * @return boolean|int The number of deleted events. False if the event was not deleted. - */ - public function deleteEvent($calendar_event, $all = false) - { - if (!is_object($calendar_event)) { - $calendar_event = CalendarEvent::find( - [$this->getRangeId(), $calendar_event]); - } - if (!$calendar_event - || !is_a($calendar_event, 'CalendarEvent') - || !$calendar_event->havePermission(Event::PERMISSION_DELETABLE)) { - return false; - } - if ($this->havePermission(Calendar::PERMISSION_WRITABLE) - || $calendar_event->havePermission(Event::PERMISSION_OWN)) { - - if (!($calendar_event - && $calendar_event->havePermission(Event::PERMISSION_WRITABLE))) { - return false; - } - - if (!is_a($calendar_event, 'CalendarEvent')) { - return false; - } - - if ($this->getRange() == Calendar::RANGE_USER) { - $event_message = clone $calendar_event; - $author_id = $calendar_event->getAuthorId(); - $deleted = $this->deleteEventWithConsultation($calendar_event); - if ($deleted && !$this->havePermission(Calendar::PERMISSION_OWN)) { - $this->sendDeleteMessage($event_message); - } - if ($all && $deleted && $author_id == $this->getRangeId()) { - CalendarEvent::findEachBySQL(function ($ce) use ($deleted) { - $calendar = new SingleCalendar($ce->range_id); - $deleted += $calendar->deleteEvent($ce); - }, 'event_id = ?', [$event_message->event_id]); - } - return $deleted; - } else if ($this->getRange() == Calendar::RANGE_SEM) { - $deleted = $this->deleteEventWithConsultation($calendar_event); - return $deleted; - } - } - return false; - } - - /** - * Sends a message to the owner of the calendar that this event was deleted - * by another user. - * - * @param CalendarEvent $event The deleted event. - * @param null|array Array with user_ids of the recipients. If not set the - * owner of the given event is used as recipient. - */ - protected function sendDeleteMessage($event, $recipient_ids = null) - { - $message = new messaging(); - $event_data = ''; - - $subject = strftime(_('Termin am %c gelöscht'), $event->getStart()); - $msg_text = sprintf(_("%s hat folgenden Termin in Ihrem Kalender gelöscht:"), get_fullname()); - $msg_text .= "\n\n"; - - $msg_text .= '**' . _('Zeit') . ':**' . strftime(' %c - ', $event->getStart()) - . strftime('%c', $event->getEnd()) . "\n"; - $msg_text .= '**' . _('Zusammenfassung') . ':** ' . $event->getTitle() . "\n"; - if ($event_data = $event->getDescription()) { - $msg_text .= '**' . _('Beschreibung') . ":** $event_data\n"; - } - if ($event_data = $event->toStringCategories()) { - $msg_text .= '**' . _('Kategorie') . ":** $event_data\n"; - } - if ($event_data = $event->toStringPriority()) { - $msg_text .= '**' . _('Priorität') . ":** $event_data\n"; - } - if ($event_data = $event->toStringAccessibility()) { - $msg_text .= '**' . _('Zugriff') . ":** $event_data\n"; - } - if ($event_data = $event->toStringRecurrence()) { - $msg_text .= '**' . _('Wiederholung') . ":** $event_data\n"; - } - $recipient_unames = is_array($recipient_ids) - ? array_map('get_username', $recipient_ids) - : [get_username($event->range_id)]; - $message->insert_message($msg_text, $recipient_unames, - '____%system%____', '', '', '', '', $subject); - } - - /** - * Returns an array of all events (with calculated recurrences) - * in the given time range. - * - * @param string $owner_id The user id of calendar owner. - * @param int $time A unix timestamp of this day. - * @param string $user_id The id of the user who gets access to the calendar (optional, default current user) - * @param array $restrictions An array with key value pairs of properties to filter the result (optional). - * @param array $class_names Array of class names. The class must implement Event (optional). - * @return array All events in the given time range (with calculated recurrences) - */ - public static function getEventList($owner_id, $start, $end, $user_id = null, - $restrictions = null, $class_names = null) - { - $user_id = $user_id ?: $GLOBALS['user']->id; - $end_time = mktime(12, 0, 0, date('n', $end), date('j', $end), date('Y', $end)); - $start_day = date('j', $start); - $events = []; - do { - $time = mktime(12, 0, 0, date('n', $start), $start_day, date('Y', $start)); - $start_day++; - $day = self::getDayCalendar($owner_id, $time, $user_id, $restrictions, $class_names); - foreach ($day->events as $event) { - $event_key = implode('', (array) $event->getId()) . $event->getStart(); - $events["$event_key"] = $event; - } - } while ($time <= $end_time); - return $events; - } - - /** - * Returns a SingleCalendar object with all events of the given owner or - * SingleCalendar object for one day set by timestamp. - * - * @param string|SingleCalendar $owner The user id of calendar owner or a calendar object. - * @param int $time A unix timestamp of this day. - * @param string $user_id The id of the user who gets access to the calendar (optional, default current user) - * @param array $restrictions An array with key value pairs of properties to filter the result (optional). - * @param array $class_names Array of class names. The class must implement Event (optional). - * @return \SingleCalendar Calendar Object with all events of given day. - */ - public static function getDayCalendar($owner, $time, $user_id = null, - $restrictions = null, $class_names = null) - { - $user_id = $user_id ?: $GLOBALS['user']->id; - if (!is_array($class_names)) { - $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent']; - } - - $day = date('Y-m-d-', $time); - $start = DateTime::createFromFormat('Y-m-d-H:i:s', $day . '00:00:00'); - $end = DateTime::createFromFormat('Y-m-d-H:i:s', $day . '23:59:59'); - if (is_object($owner)) { - if ($owner instanceof SingleCalendar) { - $calendar = $owner; - $calendar->setStart($start->format('U'))->setEnd($end->format('U')); - } else { - throw new InvalidArgumentException('The owner must be a user id or an object of type SingleCalendar.'); - } - } else { - $calendar = new SingleCalendar($owner, - $start->format('U'), $end->format('U')); - } - $calendar->getEvents($class_names)->sortEvents(); - - $dow = date('w', $calendar->getStart()); - $month = date('n', $calendar->getStart()); - $year = date('Y', $calendar->getStart()); - $events_created = []; - - foreach ($calendar->events as $event) { - if (!$calendar->havePermission(Calendar::PERMISSION_READABLE, $user_id) - && $event->getAccessibility() != 'PUBLIC') { - continue; - } - if (!$event->havePermission(Event::PERMISSION_CONFIDENTIAL, $user_id)) { - continue; - } - if (!SingleCalendar::checkRestriction($event, $restrictions)) { - continue; - } - $properties = $event->getProperties(); - $ts = mktime(12, 0, 0, date('n', $calendar->start), date('j', $calendar->start), date('Y', $calendar->start)); - $rep = $properties['RRULE']; - $duration = (int) ((mktime(12, 0, 0, date('n', $properties['DTEND']), date('j', $properties['DTEND']), date('Y', $properties['DTEND'])) - - mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART']))) - / 86400); - // single events or first event - if ($properties['DTSTART'] >= $calendar->getStart() - && $properties['DTEND'] <= $calendar->getEnd()) { - self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'], - $calendar->getStart(), $calendar->getEnd(), $events_created); - } elseif ($properties['DTSTART'] >= $calendar->getStart() - && $properties['DTSTART'] <= $calendar->getEnd()) { - self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'], - $calendar->getStart(), $calendar->getEnd(), $events_created); - } elseif ($properties['DTSTART'] < $calendar->getStart() - && $properties['DTEND'] > $calendar->getEnd()) { - self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'], - $calendar->getStart(), $calendar->getEnd(), $events_created); - } elseif ($properties['DTEND'] > $calendar->getStart() - && $properties['DTEND'] <= $calendar->getEnd()) { - self::createDayViewEvent($event, $properties['DTSTART'], $properties['DTEND'], - $calendar->getStart(), $calendar->getEnd(), $events_created); - } - switch ($rep['rtype']) { - case 'DAILY': - /* - if ($calendar->getEnd() > $rep['expire'] + $duration * 86400) { - continue; - } - * - */ - if ($end > $rep['expire'] + $duration * 86400) { - continue 2; - } - $ts = $ts + (date('I', $rep['ts']) * 3600); - $pos = (($ts - $rep['ts']) / 86400) % $rep['linterval']; - $start = $ts - $pos * 86400; - $end = $start + $duration * 86400; - self::createDayViewEvent($event, $start, $end, $calendar->getStart(), - $calendar->getEnd(), $events_created); - break; - case 'WEEKLY': - $rep['ts'] = $rep['ts'] + ((date('I', $rep['ts']) - date('I', $ts)) * 3600); - for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) { - $pos = ((($ts - $dow * 86400) - $rep['ts']) / 86400 - - ($rep['wdays'][$i] - 1) + $dow) - % ($rep['linterval'] * 7); - $start = $ts - $pos * 86400; - $end = $start + $duration * 86400; - if ($start >= $properties['DTSTART'] && $start <= $ts && $end >= $ts) { - self::createDayViewEvent($event, $start, $end, - $calendar->getStart(), $calendar->getEnd(), $events_created); - } - } - break; - case 'MONTHLY': - if ($rep['day']) { - $lwst = mktime(12, 0, 0, $month - - ((($year - date('Y', $rep['ts'])) * 12 - + ($month - date('n', $rep['ts']))) % $rep['linterval']), - $rep['day'], $year); - $hgst = $lwst + $duration * 86400; - self::createDayViewEvent($event, $lwst, $hgst, $calendar->getStart(), - $calendar->getEnd(), $events_created); - break; - } - if ($rep['sinterval']) { - $mon = $month - $rep['linterval']; - do { - $lwst = mktime(12, 0, 0, $mon - - ((($year - date('Y', $rep['ts'])) * 12 - + ($mon - date('n', $rep['ts']))) % $rep['linterval']), - 1, $year) + ($rep['sinterval'] - 1) * 604800; - $aday = strftime('%u', $lwst); - $lwst -= ( $aday - $rep['wdays']) * 86400; - if ($rep['sinterval'] == 5) { - if (date('j', $lwst) < 10) { - $lwst -= 604800; - } - if (date('n', $lwst) == date('n', $lwst + 604800)) { - $lwst += 604800; - } - } else { - if ($aday > $rep['wdays']) { - $lwst += 604800; - } - } - $hgst = $lwst + $duration * 86400; - if ($ts >= $lwst && $ts <= $hgst) { - self::createDayViewEvent($event, $lwst, $hgst, - $calendar->getStart(), $calendar->getEnd(), $events_created); - } - $mon += $rep['linterval']; - } while ($lwst < $ts); - } - break; - case 'YEARLY': - if ($ts < $rep['ts']) { - break; - } - if ($rep['day']) { - if (date('Y', $properties['DTEND']) - date('Y', $properties['DTSTART'])) { - $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'], - $year - (($year - date('Y', $rep['ts'])) % $rep['linterval']) - - $rep['linterval']); - $hgst = $lwst + 86400 * $duration; - if ($ts >= $lwst && $ts <= $hgst) { - self::createDayViewEvent($event, $lwst, $hgst, - $calendar->getStart(), $calendar->getEnd(), $events_created); - break; - } - } - $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'], - $year - (($year - date('Y', $rep['ts'])) % $rep['linterval'])); - $hgst = $lwst + 86400 * $duration; - self::createDayViewEvent($event, $lwst, $hgst, $calendar->getStart(), - $calendar->getEnd(), $events_created); - break; - } - $ayear = $year - 1; - do { - if ($rep['sinterval']) { - $lwst = mktime(12, 0, 0, $rep['month'], - 1 + ($rep['sinterval'] - 1) * 7, $ayear); - $aday = strftime('%u', $lwst); - $lwst -= ( $aday - $rep['wdays']) * 86400; - if ($rep['sinterval'] == 5) { - if (date('j', $lwst) < 10) { - $lwst -= 604800; - } - if (date('n', $lwst) == date('n', $lwst + 604800)) { - $lwst += 604800; - } - } elseif ($aday > $rep['wdays']) { - $lwst += 604800; - } - $ayear++; - $hgst = $lwst + $duration * 86400; - if ($ts >= $lwst && $ts <= $hgst) { - self::createDayViewEvent($event, $lwst, $hgst, - $calendar->getStart(), $calendar->getEnd(), $events_created); - } - } - } while ($lwst < $ts); - } - } - $calendar->events->exchangeArray(array_values($events_created)); - return $calendar; - } - - /** - * Creates events for the day view. - * - * @param Event $event - * @param int $lwst - * @param int $hgst - * @param int $cl_start - * @param int $cl_end - * @param array $events_created - * @return boolean - */ - private static function createDayViewEvent($event, $lwst, $hgst, - $cl_start, $cl_end, Array &$events_created) - { - $lwst = mktime(12, 0, 0, date('n', $lwst), date('j', $lwst), date('Y', $lwst)); - $hgst = mktime(12, 0, 0, date('n', $hgst), date('j', $hgst), date('Y', $hgst)); - - // if this date is in the exceptions? - if ($event->getProperty('EXDATE')) { - $exdates = explode(',', $event->getProperty('EXDATE')); - foreach ($exdates as $exdate) { - if ($exdate > 0 && $exdate >= $lwst && $exdate <= $hgst) { - return false; - } - } - } - // is event expired? - $rrule = $event->getRecurrence(); - if ($rrule['rtype'] != 'SINGLE' && $rrule['expire'] > 0 && $rrule['expire'] < $hgst) { - return false; - } - $start = mktime(date('G', $event->getStart()), date('i', $event->getStart()), - date('s', $event->getStart()), date('n', $lwst), date('j', $lwst), date('Y', $lwst)); - $end = mktime(date('G', $event->getEnd()), date('i', $event->getEnd()), - date('s', $event->getEnd()), date('n', $hgst), date('j', $hgst), date('Y', $hgst)); - - if (($start <= $cl_start && $end >= $cl_end) - || ($start >= $cl_start && $start < $cl_end) - || ($end > $cl_start && $end <= $cl_end)) { - - $key = implode('', (array) $event->getId()) . $start; - if (empty($events_created[$key])) { - $new_event = clone $event; - $new_event->setStart($start); - $new_event->setEnd($end); - $events_created[$key] = $new_event; - } - } - - return true; - } - - /** - * Returns an array with all days between start and end of this SingleCalendar. - * The keys are the timestamps of the days (12:00) and the values are number - * of events for a day. - * - * @param string $user_id Use the permissions of this user. - * @param array $restrictions - * @return array An array with year day as key and number of events per day as value. - */ - public function getListCountEvents($class_names = null, $user_id = null, $restrictions = null) - { - if (!is_array($class_names)) { - $class_names = ['CalendarEvent', 'CourseEvent', 'CourseCancelledEvent', 'CourseMarkedEvent']; - } - $end = $this->getEnd(); - $start = $this->getStart(); - $year = date('Y', $start); - $end_ts = mktime(12, 0, 0, date('n', $end), date('j', $end), date('Y', $end)); - $start_ts = mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start)); - $this->getEvents($class_names)->sortEvents(); - $daylist = []; - $this->ts = mktime(12, 0, 0, 1, 1, $year); - foreach ($this->events as $event) { - if (!$event->havePermission(Event::PERMISSION_CONFIDENTIAL, $user_id)) { - continue; - } - if (!SingleCalendar::checkRestriction($event, $restrictions)) { - continue; - } - $properties = $event->getProperties(); - - $rep = $properties['RRULE']; - $duration = (int) ((mktime(12, 0, 0, date('n', $properties['DTEND']), date('j', $properties['DTEND']), date('Y', $properties['DTEND'])) - - mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART']))) - / 86400); - - // single event or first event - $lwst = mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART'])); - if ($start_ts > $lwst) { - $adate = $start_ts; - } else { - $adate = $lwst; - } - $hgst = $lwst + $duration * 86400; - while ($adate >= $start_ts && $adate <= $end_ts && $adate <= $hgst) { - $md_date = $adate - date('I', $adate) * 3600; - $this->countListEvent($properties, $md_date, $properties['DTSTART'], $properties['DTEND'], $daylist); - $adate += 86400; - } - - switch ($rep['rtype']) { - case 'DAILY' : - if ($rep['ts'] < $start) { - // brauche den ersten Tag nach $start an dem dieser Termin wiederholt wird - if ($rep['linterval'] == 1) { - $adate = $this->ts; - } else { - $adate = $this->ts + ($rep['linterval'] - (($this->ts - $rep['ts']) / 86400) - % $rep['linterval']) * 86400; - } - while ($adate <= $end_ts && $adate >= $this->ts && $adate <= $rep['expire']) { - $hgst = $adate + $duration * 86400; - $md_date = $adate; - while ($md_date <= $end_ts && $md_date >= $this->ts && $md_date <= $hgst) { - $md_date -= 3600 * date('I', $md_date); - $this->countListEvent($properties, $md_date, $adate, $hgst, $daylist); - $md_date += 86400; - } - $adate += $rep['linterval'] * 86400; - } - } else { - $adate = $rep['ts']; - } - while ($adate <= $end_ts && $adate >= $this->ts && $adate <= $rep['expire']) { - $hgst = $adate + $duration * 86400; - $md_date = $adate; - while ($md_date <= $end_ts && $md_date >= $this->ts && $md_date <= $hgst) { - $md_date += 3600 * date('I', $md_date); - $this->countListEvent($properties, $md_date, $adate, $hgst, $daylist); - $md_date += 86400; - } - $adate += $rep['linterval'] * 86400; - } - break; - - case 'WEEKLY' : - if ($properties['DTSTART'] >= $start && $properties['DTSTART'] <= $end) { - $lwst = mktime(12, 0, 0, date('n', $properties['DTSTART']), date('j', $properties['DTSTART']), date('Y', $properties['DTSTART'])); - $hgst = $lwst + $duration * 86400; - if ($rep['ts'] != $adate) { - $wdate = $lwst; - while ($wdate <= $end_ts && $wdate >= $start_ts && $wdate <= $hgst) { - // $md_date = $wdate - date('I', $wdate) * 3600; - $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist); - $wdate += 86400; - } - } - $aday = strftime('%u', $lwst) - 1; - for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) { - $awday = (int) mb_substr($rep['wdays'], $i, 1) - 1; - if ($awday > $aday) { - $lwst = $lwst + ($awday - $aday) * 86400; - $hgst = $lwst + $duration * 86400; - $wdate = $lwst; - while ($wdate >= $start_ts && $wdate <= $end_ts && $wdate <= $hgst) { - // $md_date = $wdate - date('I', $wdate) * 3600; - $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist); - $wdate += 86400; - } - } - } - } - if ($rep['ts'] < $start) { - $adate = $start_ts - (strftime('%u', $start_ts) - 1) * 86400; - $adate += ( $rep['linterval'] - (($adate - $rep['ts']) / 604800) - % $rep['linterval']) * 604800; - $adate -= $rep['linterval'] * 604800; - } else { - $adate = $rep['ts'] + 604800 * $rep['linterval']; - } - - while ($adate >= $properties['DTSTART'] && $adate <= $rep['expire'] && $adate <= $end) { - // event is repeated on different week days - for ($i = 0; $i < mb_strlen($rep['wdays']); $i++) { - $awday = (int) $rep['wdays'][$i]; - $lwst = $adate + ($awday - 1) * 86400; - $hgst = $lwst + $duration * 86400; - if ($lwst < $start_ts) { - $lwst = $start_ts; - } - $wdate = $lwst; - while ($wdate >= $start_ts && $wdate <= $end_ts && $wdate <= $hgst) { - // $md_date = $wdate - date('I', $wdate) * 3600; - $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist); - $wdate += 86400; - } - } - $adate += 604800 * $rep['linterval']; - } - break; - - case 'MONTHLY' : - $bmonth = ($rep['linterval'] - ((($year - date('Y', $rep['ts'])) * 12) - - date('n', $rep['ts'])) % $rep['linterval']) % $rep['linterval']; - - for ($amonth = $bmonth - $rep['linterval']; $amonth <= $bmonth; $amonth += $rep['linterval']) { - if ($rep['ts'] < $start) { - // is repeated at X. week day of X. month... - if (!$rep['day']) { - $lwst = mktime(12, 0, 0, $amonth - - ((($year - date('Y', $rep['ts'])) * 12 - + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), 1, $year) - + ($rep['sinterval'] - 1) * 604800; - $aday = strftime('%u', $lwst); - $lwst -= ( $aday - $rep['wdays']) * 86400; - if ($rep['sinterval'] == 5) { - if (date('j', $lwst) < 10) { - $lwst -= 604800; - } - if (date('n', $lwst) == date('n', $lwst + 604800)) { - $lwst += 604800; - } - } else { - if ($aday > $rep['wdays']) { - $lwst += 604800; - } - } - } else { - // or at X. day of month ? - $lwst = mktime(12, 0, 0, $amonth - - ((($year - date('Y', $rep['ts'])) * 12 - + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), $rep['day'], $year); - } - } else { - // first recurrence - $lwst = $rep['ts']; - $lwst = mktime(12, 0, 0, $amonth - - ((($year - date('Y', $rep['ts'])) * 12 - + ($amonth - date('n', $rep['ts']))) % $rep['linterval']), $rep['day'], $year); - - } - $hgst = $lwst + $duration * 86400; - $md_date = $lwst; - // events last longer than one day - while ($md_date >= $start_ts && $md_date <= $hgst && $md_date <= $end_ts) { - $this->countListEvent($properties, $md_date, $lwst, $hgst, $daylist); - $md_date += 86400; - } - } - break; - - case 'YEARLY' : - for ($ayear = $year - 1; $ayear <= $year; $ayear++) { - if ($rep['day']) { - $lwst = mktime(12, 0, 0, $rep['month'], $rep['day'], $ayear); - $hgst = $lwst + $duration * 86400; - $wdate = $lwst; - while ($hgst >= $start_ts && $wdate <= $hgst && $wdate <= $end_ts) { - $this->countListEvent($properties, $wdate, $lwst, $hgst, $daylist); - $wdate += 86400; - } - } else { - if ($rep['ts'] < $start) { - $adate = mktime(12, 0, 0, $rep['month'], 1, $ayear) - + ($rep['sinterval'] - 1) * 604800; - $aday = strftime('%u', $adate); - $adate -= ( $aday - $rep['wdays']) * 86400; - if ($rep['sinterval'] == 5) { - if (date('j', $adate) < 10) { - $adate -= 604800; - } - } elseif ($aday > $rep['wdays']) { - $adate += 604800; - } - } else { - $adate = $rep['ts']; - } - $lwst = $adate; - $hgst = $lwst + $duration * 86400; - while ($hgst >= $start_ts && $adate <= $hgst && $adate <= $end_ts) { - $this->countListEvent($properties, $adate, $lwst, $hgst, $daylist); - $adate += 86400; - } - } - } - } - } - return $daylist; - } - - private function countListEvent($properties, $date, $lwst, $hgst, &$daylist) - { - if ($date < $this->getStart() || $date > $this->getEnd()) { - return false; - } - $lwst = mktime(12, 0, 0, date('n', $lwst), date('j', $lwst), date('Y', $lwst)); - $hgst = mktime(12, 0, 0, date('n', $hgst), date('j', $hgst), date('Y', $hgst)); - - // if this date is in the exceptions return false - $exdates = explode(',', $properties['EXDATE']); - foreach ($exdates as $exdate) { - if ($exdate > 0 && $exdate >= $lwst && $exdate <= $hgst) { - return false; - } - } - // is event expired? - if ($properties['RRULE']['expire'] > 0 - && $properties['RRULE']['expire'] <= $hgst) { - return false; - } - $idate = date('Ymd', $date); - $daylist["$idate"]["{$properties['STUDIP_ID']}"] = - $daylist["$idate"]["{$properties['STUDIP_ID']}"] - ? $daylist["$idate"]["{$properties['STUDIP_ID']}"]++ : 1; - return true; - } - - /** - * - * TODO use filter instead - * - * @param Event $event - * @param array $restrictions - * @return boolean - */ - public static function checkRestriction(Event $event, $restrictions) - { - $properties = $event->getProperties(); - if (is_array($restrictions)) { - foreach ($restrictions as $property_name => $restriction) { - if (isset($properties[mb_strtoupper($property_name)])) { - if (is_array($restriction)) { - return in_array($properties[mb_strtoupper($property_name)], $restriction); - } else if ($restriction != '') { - return $properties[mb_strtoupper($property_name)] == $restriction; - } - } - } - } - return true; - } - - /** - * Returns an array with all necessary informations to build the day view. - * - * @param type $start - * @param type $end - * @param type $step - * @param type $params - * @return type - */ - public function createEventMatrix($start, $end, $step) - { - // correction of days where dst starts or ends - $dst_offset = (date('I', $this->getStart()) - date('I', $this->getEnd())) * 3600; - $start += $dst_offset; - $end += $dst_offset; - - $term = []; - $em = $this->adapt_events($start, $end, $step); - $max_cols = 0; - $mapping = []; - // calculate maximum number of columns - $w = 0; - for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) { - $col = 0; - $row = $i - $start / $step; - while ($w < sizeof($em['events']) && $em['events'][$w]->getStart() >= $this->getStart() + $i * $step - && $em['events'][$w]->getStart() < $this->getStart() + ($i + 1) * $step) { - $rows = ceil($em['events'][$w]->getDuration() / $step); - if ($rows < 1) { - $rows = 1; - } - if (empty($term[$row])) { - $term[$row] = []; - } - if (empty($term[$row][$col])) { - $term[$row][$col] = ''; - } - while ($term[$row][$col] != '' && $term[$row][$col] != '#') { - $col++; - } - $term[$row][$col] = $em['events'][$w]; - $mapping[$row][$col] = $em['map'][$w]; - - $count = $rows - 1; - for ($x = $row + 1; $x < $row + $rows; $x++) { - for ($y = 0; $y <= $col; $y++) { - if ($y == $col) { - $term[$x][$y] = $count--; - } elseif ($term[$x][$y] == '') { - $term[$x][$y] = '#'; - } - } - } - if ($max_cols < sizeof($term[$row])) { - $max_cols = sizeof($term[$row]); - } - $w++; - } - } - $row_min = 0; - for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) { - $row = $i - $start / $step; - $row_min = $row; - while ($this->maxValue($term[$row] ?? 0, $step) > 1) { - $row += $this->maxValue($term[$row] ?? 0, $step) - 1; - } - $size = 0; - for ($j = $row_min; $j <= $row; $j++) { - if (isset($term[$j]) && count($term[$j]) > $size) { - $size = count($term[$j]); - } - } - for ($j = $row_min; $j <= $row; $j++) { - $colsp[$j] = $size; - } - $i = $row + $start / $step; - } - $rows = []; - $cspan = []; - for ($i = $start / $step; $i < $end / $step + 3600 / $step; $i++) { - $row = $i - $start / $step; - $cspan_0 = 0; - if (!empty($term[$row])) { - if ($colsp[$row] > 0) { - $cspan_0 = (int) ($max_cols / $colsp[$row]); - } - for ($j = 0; $j < $colsp[$row]; $j++) { - $sp = 0; - $n = 0; - if ($j + 1 == $colsp[$row]) { - $cspan[$row][$j] = $cspan_0 + $max_cols % $colsp[$row]; - } - if (is_object($term[$row][$j])) { - // Wieviele Termine sind zum aktuellen Termin zeitgleich? - $p = 0; - $count = 0; - while (array_key_exists($p, $em['events']) && ($aterm = $em['events'][$p])) { - if ($aterm->getStart() >= $term[$row][$j]->getStart() - && $aterm->getStart() <= $term[$row][$j]->getEnd()) { - $count++; - } - $p++; - } - if ($count == 0) { - for ($n = $j + 1; $n < $colsp[$row]; $n++) { - if (!is_int($term[$row][$n])) { - $sp++; - } else { - break; - } - } - $cspan[$row][$j] += $sp; - } - $rows[$row][$j] = ceil($term[$row][$j]->getDuration() / $step); - if ($rows[$row][$j] < 1) { - $rows[$row][$j] = 1; - } - if ($sp > 0) { - for ($m = $row; $m < $rows + $row; $m++) { - $colsp[$m] = $colsp[$m] - $sp + 1; - $v = $j; - while ($term[$m][$v] == '#') { - $term[$m][$v] = 1; - } - } - $j = $n; - } - } elseif ($term[$row][$j] == '#') { - $csp = 1; - while ($term[$row][$j] == '#') { - $csp += $cspan[$row][$j]; - $j++; - } - $cspan[$row][$j] = $csp; - } elseif ($term[$row][$j] == '') { - $cspan[$row][$j] = $max_cols - $j + 1; - } - } - } - } - if ($max_cols < 1 && isset($em['day_events']) && count($em['day_events'])) { - $max_cols = 1; - } - $em['cspan'] = $cspan; - $em['rows'] = $rows; - $em['colsp'] = $colsp; - $em['term'] = $term; - $em['max_cols'] = $max_cols; - $em['mapping'] = $mapping; - return $em; - } - - /** - * Returns max value of colspan in calendar tables for day view. - * - * @param Array $term Array with table cell content. - * @param int $st Seconds between each row in calendar table. - * @return int Max value of colspan. - */ - private function maxValue($term, $st) - { - $max_value = 0; - - if (is_array($term)) { - for ($i = 0; $i < count($term); $i++) { - if (is_object($term[$i])) { - $max = ceil($term[$i]->getDuration() / $st); - } elseif ($term[$i] == '#') { - continue; - } elseif ($term[$i] > $max_value) { - $max = $term[$i]; - } - if ($max > $max_value) { - $max_value = $max; - } - } - } - - return $max_value; - } - - /** - * Returns array with events and other information to build calendar tables - * for day view. - * - * @param int $start Start time date as unix timestamp - * @param int $end End time date as unix timestamp - * @param int $step Seconds between each row in calendar table. - * @return Array Array with new calculated events and some other things. - */ - public function adapt_events($start, $end, $step = 900) - { - $tmp_events = []; - $map_events = []; - $tmp_day_event = []; - $map_day_events = []; - for ($i = 0; $i < sizeof($this->events); $i++) { - $event = $this->events[$i]; - if (($event->getEnd() > $this->getStart() + $start) - && ($event->getStart() < $this->getStart() + $end + 3600)) { - $cloned_event = clone $event; - if ($event->isDayEvent() - || ($event->getStart() <= $this->getStart() - && $event->getEnd() >= $this->getEnd())) { - $cloned_event->setStart($this->getStart()); - $cloned_event->setEnd($this->getEnd()); - $tmp_day_event[] = $cloned_event; - $map_day_events[] = $i; - } else { - $end_corr = $cloned_event->getEnd() % $step; - if ($end_corr > 0) { - $end_corr = $cloned_event->getEnd() + ($step - $end_corr); - $cloned_event->setEnd($end_corr); - } - if ($cloned_event->getStart() < ($this->getStart() + $start)) { - $cloned_event->setStart($this->getStart() + $start); - } - if ($cloned_event->getEnd() > ($this->getStart() + $end + 3600)) { - $cloned_event->setEnd($this->getStart() + $end + 3600); - } - $tmp_events[$cloned_event->id . $cloned_event->getStart()] = $cloned_event; - $map_events[$cloned_event->id . $cloned_event->getStart()] = $i; - } - } - } - - uasort($tmp_events, function($a, $b) {return $a->start - $b->start;}); - $map = []; - foreach (array_keys($tmp_events) as $key) { - $map[] = $map_events[$key]; - } - - return [ - 'events' => array_values($tmp_events), - 'map' => $map, - 'day_events' => $tmp_day_event, - 'day_map' => $map_day_events]; - } - -} diff --git a/lib/classes/forms/SelectedRangesInput.php b/lib/classes/forms/SelectedRangesInput.php new file mode 100644 index 0000000000000000000000000000000000000000..77b2db22cc357cf2225d1fef8cbc825434947b7e --- /dev/null +++ b/lib/classes/forms/SelectedRangesInput.php @@ -0,0 +1,58 @@ +<?php + +namespace Studip\Forms; + +class SelectedRangesInput extends Input +{ + protected $selectable_items = []; + + protected $selected_items = []; + + protected $search_type = null; + + + public function render() + { + $template = $GLOBALS['template_factory']->open('forms/selected_ranges_input'); + $template->name = $this->name; + $template->selectable_items = $this->selectable_items; + $template->selected_items = []; + foreach ($this->selected_items as $item) { + $item_data = []; + if ($item instanceof \Range) { + $item_data['name'] = $item->getFullname(); + $item_data['id'] = $item->getRangeId(); + } elseif ($item instanceof \StudipItem) { + $item_data['name'] = $item->getItemName(); + $item_data['id'] = $item->id; + } elseif ( + is_array($item) + && array_key_exists('name', $item) + && array_key_exists('id', $item) + ) { + $item_data['name'] = $item['name']; + $item_data['id'] = $item['id']; + } + if ($item_data) { + $template->selected_items[] = $item_data; + } + } + $template->searchtype = $this->search_type; + return $template->render(); + } + + public function getRequestValue() + { + return \Request::getArray($this->name); + } + + public function setSelectedItems(array $items) + { + $this->selected_items = $items; + } + + public function setSearchType(\SearchType $search_type) + { + $this->search_type = $search_type; + } +} diff --git a/lib/classes/searchtypes/StandardSearch.class.php b/lib/classes/searchtypes/StandardSearch.class.php index fc3135a00f38917f8c872ac042ee70fc74003022..837a849c86689d36440da202d2c84581a73f923d 100644 --- a/lib/classes/searchtypes/StandardSearch.class.php +++ b/lib/classes/searchtypes/StandardSearch.class.php @@ -94,24 +94,36 @@ class StandardSearch extends SQLSearch switch ($this->search) { case "username": $this->extendedLayout = true; - return "SELECT DISTINCT auth_user_md5.username, CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms " . - "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " . + $sql = "SELECT DISTINCT auth_user_md5.username"; + if (empty($this->search_settings['simple_name'])) { + $sql .= ", CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms "; + } else { + $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) "; + } + $sql .= "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " . "LEFT JOIN user_visibility ON (user_visibility.user_id = auth_user_md5.user_id) " . "WHERE (CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) LIKE REPLACE(:input, ' ', '% ') " . "OR CONCAT(auth_user_md5.Nachname, ' ', auth_user_md5.Vorname) LIKE REPLACE(:input, ' ', '% ') " . "OR CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname) LIKE :input " . "OR auth_user_md5.username LIKE :input) AND " . get_vis_query('auth_user_md5', 'search') . " ORDER BY Nachname ASC, Vorname ASC"; + return $sql; case "user_id": $this->extendedLayout = true; - return "SELECT DISTINCT auth_user_md5.user_id, CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms " . - "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " . + $sql = "SELECT DISTINCT auth_user_md5.user_id"; + if (empty($this->search_settings['simple_name'])) { + $sql .= ", CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname, ' (', auth_user_md5.username, ')'), auth_user_md5.perms "; + } else { + $sql .= ", CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) "; + } + $sql .= "FROM auth_user_md5 LEFT JOIN user_info ON (user_info.user_id = auth_user_md5.user_id) " . "LEFT JOIN user_visibility ON (user_visibility.user_id = auth_user_md5.user_id) " . "WHERE (CONCAT(auth_user_md5.Vorname, ' ', auth_user_md5.Nachname) LIKE REPLACE(:input, ' ', '% ') " . "OR CONCAT(auth_user_md5.Nachname, ' ', auth_user_md5.Vorname) LIKE REPLACE(:input, ' ', '% ') " . "OR CONCAT(auth_user_md5.Nachname, ', ', auth_user_md5.Vorname) LIKE :input " . "OR auth_user_md5.username LIKE :input) AND " . get_vis_query('auth_user_md5', 'search') . " ORDER BY Nachname ASC, Vorname ASC"; + return $sql; case "Seminar_id": return "SELECT seminare.Seminar_id, CONCAT_WS(' ', seminare.VeranstaltungsNummer, seminare.Name, ".$semester.") " . "FROM seminare " . diff --git a/lib/classes/sidebar/DateSelectWidget.php b/lib/classes/sidebar/DateSelectWidget.php new file mode 100644 index 0000000000000000000000000000000000000000..df341658dbcb88fff817c73f70de17267acfdbd8 --- /dev/null +++ b/lib/classes/sidebar/DateSelectWidget.php @@ -0,0 +1,51 @@ +<?php + +class DateSelectWidget extends SidebarWidget +{ + protected $date = null; + protected $calendar_control = false; + + public function __construct() + { + $this->template = 'sidebar/date-select-widget'; + $this->date = new DateTime(); + parent::__construct(); + } + + public function setCalendarControl(bool $calendar_control = false) : void + { + $this->calendar_control = $calendar_control; + } + + public function setDate(DateTime $date) : void + { + $this->date = $date; + } + + public function getDate() : ?DateTime + { + return $this->date; + } + + public function getCalendarControlStatus() : bool + { + return $this->calendar_control; + } + + public function render($variables = []) : string + { + $template = $GLOBALS['template_factory']->open($this->template); + $template->set_attributes($variables + $this->template_variables); + $template->set_attribute('title', _('Datum auswählen')); + $template->set_attribute('date', $this->date); + $template->set_attribute('calendar_control', $this->calendar_control); + + if ($this->layout) { + $layout = $GLOBALS['template_factory']->open($this->layout); + $layout->layout_css_classes = $this->layout_css_classes; + $template->set_layout($layout); + } + + return $template->render(); + } +} diff --git a/lib/exceptions/FeatureDisabledException.php b/lib/exceptions/FeatureDisabledException.php new file mode 100644 index 0000000000000000000000000000000000000000..16af0bf8f8a35f85c5ef6bc7d87d9bb8e3a395d8 --- /dev/null +++ b/lib/exceptions/FeatureDisabledException.php @@ -0,0 +1,11 @@ +<?php +class FeatureDisabledException extends StudipException +{ + public function __construct($message = '', $code = 0, Exception $previous = null) + { + if (func_num_args() === 0) { + $message = _('Diese Funktion ist ausgeschaltet.'); + } + parent::__construct($message, [], $code, $previous); + } +} diff --git a/lib/models/CalendarEvent.class.php b/lib/models/CalendarEvent.class.php deleted file mode 100644 index 16ca91aab0047e882d430b35f292cdc3c80f836c..0000000000000000000000000000000000000000 --- a/lib/models/CalendarEvent.class.php +++ /dev/null @@ -1,1394 +0,0 @@ -<?php -/** - * EventRange.class.php - model class for table calendar_event - * - * 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 Peter Thienel <thienel@data-quest.de> - * @copyright 2014 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * - * @property array $id alias for pk - * @property string $range_id database column - * @property string $event_id database column - * @property int $group_status database column - * @property int $mkdate database column - * @property int $chdate database column - * @property SimpleORMapCollection|CalendarEvent[] $attendees has_many CalendarEvent - * @property SimpleORMapCollection|ConsultationEvent[] $consultation_events has_many ConsultationEvent - * @property User $user belongs_to User - * @property Course $course belongs_to Course - * @property Institute $institute belongs_to Institute - * @property ConsultationBooking $consultation_booking belongs_to ConsultationBooking - * @property EventData $event has_one EventData - * @property mixed $type additional field - * @property mixed $name additional field - * @property mixed $author_id additional field - * @property mixed $editor_id additional field - * @property mixed $title additional field - * @property mixed $start additional field - * @property mixed $end additional field - * @property-read mixed $owner additional field - */ -class CalendarEvent extends SimpleORMap implements Event, PrivacyObject -{ - const PARTSTAT_TENTATIVE = 1; - const PARTSTAT_ACCEPTED = 2; - const PARTSTAT_DECLINED = 3; - const PARTSTAT_DELEGATED = 4; - const PARTSTAT_NEEDS_ACTION = 5; - - protected static function configure($config = []) - { - $config['db_table'] = 'calendar_event'; - - $config['belongs_to']['user'] = [ - 'class_name' => User::class, - 'foreign_key' => 'range_id', - ]; - $config['belongs_to']['course'] = [ - 'class_name' => Course::class, - 'foreign_key' => 'range_id', - ]; - $config['belongs_to']['institute'] = [ - 'class_name' => Institute::class, - 'foreign_key' => 'range_id', - ]; - $config['has_one']['event'] = [ - 'class_name' => EventData::class, - 'foreign_key' => 'event_id', - 'assoc_foreign_key' => 'event_id', - 'on_delete' => 'delete', - 'on_store' => 'store' - ]; - $config['has_many']['attendees'] = [ - 'class_name' => CalendarEvent::class, - 'foreign_key' => 'event_id', - 'assoc_foreign_key' => 'event_id' - ]; - $config['belongs_to']['consultation_booking'] = [ - 'class_name' => ConsultationBooking::class, - 'foreign_key' => 'event_id', - 'assoc_foreign_key' => 'student_event_id', - ]; - $config['has_many']['consultation_events'] = [ - 'class_name' => ConsultationEvent::class, - 'foreign_key' => 'event_id', - 'assoc_foreign_key' => 'event_id', - ]; - $config['additional_fields']['type'] = true; - $config['additional_fields']['name'] = true; - $config['additional_fields']['author_id'] = true; - $config['additional_fields']['editor_id'] = true; - $config['additional_fields']['title'] = true; - $config['additional_fields']['start'] = true; - $config['additional_fields']['end'] = true; - $config['additional_fields']['owner']['get'] = 'getOwner'; - - $config['registered_callbacks']['after_delete'][] = function ($event) { - if ($event->consultation_booking) { - $event->consultation_booking->student_event_id = null; - $event->consultation_booking->store(); - } - $event->consultation_events->delete(); - }; - - parent::configure($config); - } - - private $properties = null; - private $permission_user_id = null; - - /** - * Returns the owner of this event as an object of type User, Course - * or Institute. - * - * @return object - */ - public function getOwner() - { - if ($this->user) { - return $this->user; - } else if ($this->course) { - return $this->course; - } else if ($this->institute) { - return $this->institute; - } - return null; - } - - public static function deleteBySQL($where, $params = []) - { - $ret = parent::deleteBySQL($where, $params); - EventData::garbageCollect(); - return $ret; - } - - /** - * Finds calendar events by the uid of the event data. - * - * @param string $uid The global unique id of this event. - * @return null|CalendarEvent The calendar event, an array of calendar events or null. - */ - public static function findByUid($uid, $range_id = null) - { - $event_data = EventData::findOneByuid($uid); - if ($event_data) { - if ($range_id) { - return self::find([$range_id, $event_data->getId()]); - } - return self::findByevent_id($event_data->getId()); - } - return null; - } - - /** - * Keeps the event data - */ - public function __clone() - { - if (is_object($this->event)) { - $event = clone $this->event; - parent::__clone(); - $this->event = $event; - } else { - parent::__clone(); - } - } - - /** - * Returns a list of all categories the event belongs to. - * Returns an empty string if no permission. - * - * @return string All categories as list. - */ - public function toStringCategories($as_array = false) - { - global $PERS_TERMIN_KAT; - - $categories = []; - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - if ($this->event->categories) { - $categories = array_map('trim', explode(',', $this->event->categories)); - } - if ($this->event->category_intern) { - array_unshift($categories, - $PERS_TERMIN_KAT[$this->event->category_intern]['name']); - } - } - return $as_array ? $categories : implode(', ', $categories); - } - - /** - * Returns the name of the group status. - * Returns an empty string status is unknown. - * - * @return string All categories as list. - */ - public function toStringGroupStatus($status = null) - { - if (is_null($status)) { - $status = $this->group_status; - } - switch ($status) { - case CalendarEvent::PARTSTAT_TENTATIVE : - return _('Abwartend'); - case CalendarEvent::PARTSTAT_ACCEPTED : - return _('Angenommen'); - case CalendarEvent::PARTSTAT_DECLINED : - return _('Abgelehnt'); - case CalendarEvent::PARTSTAT_DELEGATED : - return _('Angenommen (keine Teilnahme)'); - } - return ''; - } - - /** - * Returns all values that defines a recurrence rule or a single value - * named by $index. - * - * @param string $index Name of the value to retrieve (optional). - * @return string|array The value(s) of the recurrence rule. - * @throws InvalidArgumentException - */ - public function getRecurrence($index = null) - { - $recurrence = [ - 'ts' => $this->event->ts ?: mktime(12, 0, 0, date('n', $this->getStart()), date('j', - $this->getStart()), date('Y', $this->getStart())), - 'linterval' => $this->event->linterval, - 'sinterval' => $this->event->sinterval, - 'wdays' => $this->event->wdays, - 'month' => $this->event->month, - 'day' => $this->event->day, - 'rtype' => $this->event->rtype ?: 'SINGLE', - 'duration' => $this->event->duration, - 'count' => $this->event->count, - 'expire' => $this->event->expire ?: Calendar::CALENDAR_END - ]; - if ($index) { - if (in_array($index, array_keys($recurrence))) { - return $recurrence[$index]; - } else { - throw new InvalidArgumentException('CalendarEvent::getRecurrence ' - . $index . ' is not a field in the recurrence rule.'); - } - } - return $recurrence; - } - - /** - * - * TODO should throw an exception if input values are wrong - * - * @param array $r_rule - * @return array|false The values of the recurrence rule. - */ - function setRecurrence($r_rule) - { - $start = $this->getStart(); - $end = $this->getEnd(); - $duration = (int) ((mktime(12, 0, 0, date('n', $end), - date('j', $end), date('Y', $end)) - - mktime(12, 0, 0, date('n', $start), - date('j', $start), date('Y', $start))) / 86400); - if (!isset($r_rule['count'])) { - $r_rule['count'] = 0; - } - - switch ($r_rule['rtype']) { - case 'SINGLE': - $ts = mktime(12, 0, 0, date('n', $start), - date('j', $start), date('Y', $start)); - $rrule = [$ts, 0, 0, '', 0, 0, 'SINGLE', $duration]; - break; - case 'DAILY': - $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1; - $ts = mktime(12, 0, 0, date('n', $start), - date('j', $start) + $r_rule['linterval'], date('Y', $start)); - if ($r_rule['count']) { - $r_rule['expire'] = mktime(23, 59, 59, date('n', $start), date('j', $start) - + ($r_rule['count'] - 1) * $r_rule['linterval'], date('Y', $start)); - } - $rrule = [$ts, $r_rule['linterval'], 0, '', 0, 0, 'DAILY', $duration]; - break; - case 'WEEKLY': - $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1; - if (!$r_rule['wdays']) { - $ts = mktime(12, 0, 0, date('n', $start), date('j', $start) + - ($r_rule['linterval'] * 7 - (strftime('%u', $start) - 1)), - date('Y', $start)); - if ($r_rule['count']) { - $r_rule['expire'] = mktime(23, 59, 59, date('n', $start), - date('j', $start) + ($r_rule['linterval'] * 7 * ($r_rule['count'] - 1)), - date('Y', $start)); - } - $rrule = [$ts, $r_rule['linterval'], 0, strftime('%u', $start), - 0, 0, 'WEEKLY', $duration]; - } else { - $ts = mktime(12, 0, 0, date('n', $start), - date('j', $start) + (7 - (strftime('%u', $start) - 1)) - - ((strftime('%u', $start) <= substr($r_rule['wdays'], -1)) ? 7 : 0), - date('Y', $start)); - - if ($r_rule['count']) { - $dt_ts = DateTime::createFromFormat('U', $ts); - - // max. length of selected week days must not exceed - // number of recurrences - $r_rule['wdays'] = substr($r_rule['wdays'], 0, $r_rule['count']); - - $start_wday = date('N', $start); - $count_first_week = 0; - for ($i = 0; $i < strlen($r_rule['wdays']); $i++) { - if (isset($r_rule['wdays'][$i]) && $r_rule['wdays'][$i] >= $start_wday) { - $count_first_week++; - } - } - - $count_first_week += (date('N', $start) < $r_rule['wdays'][0]) ? 1 : 0; - - $count_complete = $r_rule['count'] - $count_first_week; - $weeks_max = floor($count_complete / strlen($r_rule['wdays'])); - - $dt_expire = $dt_ts->add(new DateInterval('P' . ($weeks_max + 1) . 'W')); - $count_last_week = $count_complete % strlen($r_rule['wdays']); - if ($count_last_week && isset($r_rule['wdays'][$count_last_week - 1])) { - $last_wday = $r_rule['wdays'][$count_last_week - 1]; - $dt_expire = $dt_expire->add(new DateInterval('P' . ($last_wday - 1) . 'D')); - } else { - $dt_expire = $dt_expire->sub(new DateInterval('P1D')); - } - - $expire_ts = $dt_expire->format('U'); - $r_rule['expire'] = mktime(23, 59, 59, date('n', $expire_ts), - date('j', $expire_ts), date('Y', $expire_ts)); - } - $rrule = [$ts, $r_rule['linterval'], 0, $r_rule['wdays'], - 0, 0, 'WEEKLY', $duration]; - } - break; - case 'MONTHLY': - if ($r_rule['month']) { - return false; - } - $r_rule['linterval'] = $r_rule['linterval'] ? intval($r_rule['linterval']) : 1; - if (!$r_rule['day'] && !$r_rule['sinterval'] && !$r_rule['wdays']) { - $amonth = date('n', $start) + $r_rule['linterval']; - $ts = mktime(12, 0, 0, $amonth, date('j', $start), date('Y', $start)); - $rrule = [$ts, $r_rule['linterval'], 0, '', 0, - date('j', $start), 'MONTHLY', $duration]; - } else if (!$r_rule['sinterval'] && !$r_rule['wdays']) { - if ($r_rule['day'] < date('j', $start)) { - $amonth = date('n', $start) + $r_rule['linterval']; - } else { - $amonth = date('n', $start); - } - $ts = mktime(12, 0, 0, $amonth, $r_rule['day'], date('Y', $start)); - $rrule = [$ts, $r_rule['linterval'], 0, '', 0, - $r_rule['day'], 'MONTHLY', $duration]; - } else if (!$r_rule['day']) { - $amonth = date('n', $start); - $adate = mktime(12, 0, 0, $amonth, 1, - date('Y', $start)) + ($r_rule['sinterval'] - 1) * 604800; - $awday = strftime('%u', $adate); - $adate -= ( $awday - $r_rule['wdays']) * 86400; - if ($r_rule['sinterval'] == 5) { - if (date('j', $adate) < 10) { - $adate -= 604800; - } - if (date('n', $adate) == date('n', $adate + 604800)) { - $adate += 604800; - } - } else if ($awday > $r_rule['wdays']) { - $adate += 604800; - } - if (date('Ymd', $adate) < date('Ymd', $start)) { - $amonth = date('n', $start) + $r_rule['linterval']; - $adate = mktime(12, 0, 0, $amonth, 1, - date('Y', $start)) + ($r_rule['sinterval'] - 1) * 604800; - $awday = strftime('%u', $adate); - $adate -= ( $awday - $r_rule['wdays']) * 86400; - if ($r_rule['sinterval'] == 5) { - if (date('j', $adate) < 10) { - $adate -= 604800; - } - if (date('n', $adate) == date('n', $adate + 604800)) { - $adate += 604800; - } - } else if ($awday > $r_rule['wdays']) { - $adate += 604800; - } - } - $ts = $adate; - $rrule = [$ts, $r_rule['linterval'], $r_rule['sinterval'], - $r_rule['wdays'], 0, 0, 'MONTHLY', $duration]; - } - - if ($r_rule['count']) { - $r_rule['expire'] = mktime(23, 59, 59, date('n', $ts) + $r_rule['linterval'] - * ($r_rule['count'] - 1), date('j', $ts), date('Y', $ts)); - } - break; - case 'YEARLY': - if (!$r_rule['month'] && !$r_rule['day'] && !$r_rule['sinterval'] && !$r_rule['wdays']) { - $ts = mktime(12, 0, 0, date('n', $start), - date('j', $start), date('Y', $start) + 1); - $rrule = [$ts, 1, 0, '', date('n', $start), - date('j', $start), 'YEARLY', $duration]; - } else if (!$r_rule['sinterval'] && !$r_rule['wdays']) { - if (!$r_rule['day']) { - $r_rule['day'] = date('j', $start); - } - $ts = mktime(12, 0, 0, $r_rule['month'], $r_rule['day'], - date('Y', $start)); - if ($ts <= mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start))) { - $ts = mktime(12, 0, 0, $r_rule['month'], $r_rule['day'], - date('Y', $start) + 1); - } - $rrule = [$ts, 1, 0, '', $r_rule['month'], - $r_rule['day'], 'YEARLY', $duration]; - } else if (!$r_rule['day']) { - $ayear = date('Y', $start); - do { - $adate = mktime(12, 0, 0, $r_rule['month'], - 1 + ($r_rule['sinterval'] - 1) * 7, $ayear); - $aday = strftime('%u', $adate); - $adate -= ( $aday - $r_rule['wdays']) * 86400; - if ($r_rule['sinterval'] == 5) { - if (date('j', $adate) < 10) { - $adate -= 604800; - } - if (date('n', $adate) == date('n', $adate + 604800)) { - $adate += 604800; - } - } else if ($aday > $r_rule['wdays']) { - $adate += 604800; - } - $ts = $adate; - $ayear++; - } while ($ts <= mktime(12, 0, 0, date('n', $start), date('j', $start), date('Y', $start))); - $rrule = [$ts, 1, $r_rule['sinterval'], $r_rule['wdays'], - $r_rule['month'], 0, 'YEARLY', $duration]; - } - - if ($r_rule['count']) { - $r_rule['expire'] = mktime(23, 59, 59, date('n', $ts), - date('j', $ts), date('Y', $ts) + $r_rule['count'] - 1); - } - break; - default : - $ts = mktime(12, 0, 0, date('n', $start), - date('j', $start), date('Y', $start)); - $rrule = [$ts, 0, 0, '', 0, 0, 'SINGLE', $duration]; - $r_rule['count'] = 0; - } - - if (!$r_rule['expire'] || $r_rule['expire'] > Calendar::CALENDAR_END) { - $r_rule['expire'] = Calendar::CALENDAR_END; - } - $this->event->ts = $rrule[0]; - $this->event->linterval = $rrule[1]; - $this->event->sinterval = $rrule[2]; - $this->event->wdays = $rrule[3]; - $this->event->month = $rrule[4]; - $this->event->day = $rrule[5]; - $this->event->rtype = $rrule[6]; - $this->event->duration = $rrule[7]; - $this->event->count = $r_rule['count']; - $this->event->expire = $r_rule['expire']; - - return $r_rule; - } - - /** - * Returns a string representation of the recurrence rule. - * If $only_type is true returns only the type of the recurrence. - * - * @param bool $only_type If true returns only the type of recurrence. - * @return string The recurrence rule - human readable - */ - public function toStringRecurrence($only_type = false) - { - $rrule = $this->getRecurrence(); - $replace = [_('Montag') . ', ', _('Dienstag') . ', ', _('Mittwoch') . ', ', - _('Donnerstag') . ', ', _('Freitag') . ', ', _('Samstag') . ', ', _('Sonntag') . ', ']; - $search = ['1', '2', '3', '4', '5', '6', '7']; - $wdays = str_replace($search, $replace, $rrule['wdays']); - $wdays = mb_substr($wdays, 0, -2); - - switch ($rrule['rtype']) { - case 'DAILY': - if ($rrule['linterval'] > 1) { - $type = 'xdaily'; - $text = sprintf(_('Der Termin wird alle %s Tage wiederholt.'), - $rrule['linterval']); - } else { - $type = 'daily'; - $text = _('Der Termin wird täglich wiederholt'); - } - break; - case 'WEEKLY': - if ($rrule['linterval'] > 1) { - $type = 'xweek_wdaily'; - $text = sprintf(_('Der Termin wird alle %s Wochen am %s wiederholt.'), - $rrule['linterval'], $wdays); - } else { - if ($rrule['wdays'] = '12345') { - $type = 'workdaily'; - } else { - $type = 'wdaily'; - } - $text = sprintf(_('Der Termin wird jeden %s wiederholt.'), $wdays); - } - break; - case 'MONTHLY': - if ($rrule['linterval'] > 1) { - if ($rrule['day']) { - $type = 'mday_xmonthly'; - $text = sprintf(_('Der Termin wird am %s. alle %s Monate wiederholt.'), - $rrule['day'], $rrule['linterval']); - } else { - if ($rrule['sinterval'] != '5') { - $type = 'xwday_xmonthly'; - $text = sprintf(_('Der Termin wird jeden %s. %s alle %s Monate wiederholt.'), - $rrule['sinterval'], $wdays, $rrule['linterval']); - } else { - $type = 'lastwday_xmonthly'; - $text = sprintf(_('Der Termin wird jeden letzten %s alle %s Monate wiederholt.'), - $wdays, $rrule['linterval']); - } - } - } else { - if ($rrule['day']) { - $type = 'mday_monthly'; - $text = sprintf(_('Der Termin wird am %s. jeden Monat wiederholt.'), - $rrule['day'], $rrule['linterval']); - } else { - if ($rrule['sinterval'] != '5') { - $type = 'xwday_monthly'; - $text = sprintf(_('Der Termin wird am %s. %s jeden Monat wiederholt.'), - $rrule['sinterval'], $wdays, $rrule['linterval']); - } else { - $type = 'lastwday_monthly'; - $text = sprintf(_('Der Termin wird jeden letzten %s jeden Monat wiederholt.'), - $wdays, $rrule['linterval']); - } - } - } - break; - case 'YEARLY': - $month_names = [_('Januar'), _('Februar'), _('März'), _('April'), _('Mai'), - _('Juni'), _('Juli'), _('August'), _('September'), _('Oktober'), - _('November'), _('Dezember')]; - if ($rrule['day']) { - $type = 'mday_month_yearly'; - $text = sprintf(_('Der Termin wird jeden %s. %s wiederholt.'), - $rrule['day'], $month_names[$rrule['month'] - 1]); - } else { - if ($rrule['sinterval'] != '5') { - $type = 'xwday_month_yearly'; - $text = sprintf(_('Der Termin wird jeden %s. %s im %s wiederholt.'), - $rrule['sinterval'], $wdays, $month_names[$rrule['month'] - 1]); - } else { - $type = 'lastwday_month_yearly'; - $text = sprintf(_('Der Termin wird jeden letzten %s im %s wiederholt.'), - $wdays, $month_names[$rrule['month'] - 1]); - } - } - break; - default: - $type = 'single'; - $text = _('Der Termin wird nicht wiederholt.'); - } - return $only_type ? $type : $text; - } - - /** - * Returns the priority in a human readable form. - * If the user has no permission an epmty string will be returned. - * - * @return string The priority as a string. - */ - public function toStringPriority() - { - if (!$this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - return ''; - } - switch ($this->event->priority) { - case 1: - return _('Hoch'); - case 2: - return _('Mittel'); - case 3: - return _('Niedrig'); - default: - return _('Keine Angabe'); - } - } - - /** - * Returns the accessibilty in a human readable form. - * If the user has no permission an epmty string will be returned. - * - * @return string The accessibility as string. - */ - public function toStringAccessibility() - { - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - switch ($this->event->class) { - case 'PUBLIC': - return _('Öffentlich'); - case 'CONFIDENTIAL': - return _('Vertraulich'); - default: - return _('Privat'); - } - } - return ''; - } - - /** - * Returns the exceptions as array of unix timestamps. - * - * @return array Array of unix timestamps. - */ - public function getExceptions() - { - $exceptions = []; - if (trim($this->event->exceptions)) { - $exceptions = explode(',', $this->event->exceptions); - } - return $exceptions; - } - - /** - * Sets proper timestamps as exceptions for given unix timestamps. - * - * @param array $exceptions Array of exceptions as unix timestamps. - */ - public function setExceptions($exceptions) - { - $exc = []; - if (is_array($exceptions)) { - $exc = array_map(function ($exception) { - $exception = intval($exception); - return mktime(12, 0, 0, date('n', $exception), - date('j', $exception), date('Y', $exception)); - }, $exceptions); - } - $this->event->exceptions = implode(',', $exc); - } - - /** - * Returns the title of this event. - * If the user has not the permission Event::PERMISSION_READABLE, - * the title is "Keine Berechtigung.". - * - * @return string - */ - public function getTitle() - { - if (!$this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - return _('Keine Berechtigung.'); - } - if ($this->event->summary == '') { - return _('Kein Titel'); - } - return $this->event->summary; - } - - /** - * Sets the title of this event. - * - * @param type $title The title of this event. - */ - public function setTitle($title) - { - $this->event->summary = $title; - } - - /** - * Returns the starttime as unix timestamp of this event. - * - * @return int The starttime of this event as a unix timestamp - */ - public function getStart() - { - return $this->event->start; - } - - /** - * Sets the start date time with given unix timestamp. - * - * @param string $timestamp Unix timestamp. - */ - public function setStart($timestamp) - { - $this->event->start = $timestamp; - } - - /** - * Returns the endtime as unix timestamp of this event. - * - * @return int the endtime of this event as a unix timestamp - */ - public function getEnd() - { - return $this->event->end; - } - - /** - * Sets the end date time by given unix timestamp. - * - * @param string $timestamp Unix timestamp. - */ - public function setEnd($timestamp) - { - $this->event->end = $timestamp; - } - - /** - * Returns the user id of the author. - * - * @return string User id of the author. - */ - public function getAuthorId() - { - return $this->event->author_id; - } - - /** - * Sets the author by given user id. - * - * @param string $author_id User id of the author. - */ - public function setAuthorId($author_id) - { - $this->event->author_id = $author_id; - } - - /** - * Sets the editor id by given user id. - * - * @param string $editor_id User id of the editor. - */ - public function setEditorId($editor_id) - { - $this->event->editor_id = $editor_id; - } - - /** - * Returns the duration of this event in seconds. - * - * @return int the duration of this event in seconds - */ - function getDuration() - { - return $this->event->end - $this->event->start; - } - - /** - * Returns the location. - * Without permission or the location is not set an empty string is returned. - * - * @return string The location - */ - public function getLocation() - { - $location = ''; - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - if (trim($this->event->location) != '') { - $location = $this->event->location; - } - } - return $location; - } - - /** - * Returns the global unique id of this event. - * - * @return string The global unique id. - */ - public function getUid() - { - return $this->event->uid !== '' - ? $this->event->uid - : 'Stud.IP-' . $this->event_id . '@' . $_SERVER['SERVER_NAME']; - } - - /** - * Returns the description of the topic. - * If the user has no permission or the event has no topic - * or the topics have no descritopn an empty string is returned. - * - * @return String the description - */ - public function getDescription() - { - $description = ''; - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - $description = trim($this->event->description); - } - return $description; - } - - /** - * Returns the index of the category. - * If the user has no permission, 255 is returned. - * - * @see config/config.inc.php $TERMIN_TYP - * @return int The index of the category - */ - public function getCategory() - { - global $PERS_TERMIN_KAT; - - $category = 0; - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - if ($this->event->category_intern) { - $category = $this->event->category_intern; - } - - if ($category == 0 && trim($this->event->categories)) { - $categories = []; - $i = 1; - foreach ($PERS_TERMIN_KAT as $pers_cat) { - $categories[mb_strtolower($pers_cat['name'])] = $i++; - } - $cat_event = explode(',', $this->event->categories); - foreach ($cat_event as $cat) { - $index = mb_strtolower(trim($cat)); - if ($categories[$index]) { - $category = $categories[$index]; - break; - } - } - } - } else { - $category = 255; - } - return $category; - } - - /** - * Returns a csv list of categories. If no categories are stated or the user - * has no permission an empty string will be returned. - * - * @return string csv list of categories or empty string - */ - public function getUserDefinedCategories() - { - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - return trim((string) $this->event->categories); - } - return ''; - } - - /** - * Stores user defined categories as a csv list. - * - * @param array|string $categories An array or csv list of user defined categories. - */ - public function setUserDefinedCategories($categories) - { - if (!is_array($categories)) { - $categories = explode(',', $categories); - } - $cat_list = implode(',', array_map('trim', $categories)); - $this->event->categories = $cat_list; - } - - /** - * Sets the accessibility (class). Possible classes are 'PUBLIC', 'PRIVATE' - * and 'CONFIDENTIAL'. - * If the given class is unknown, the event gets the class 'PRIVATE'. - * - * @param string $class The name of the class. - */ - public function setAccessibility($class) - { - $class = mb_strtoupper($class); - if (in_array($class, ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'])) { - $this->event->class = $class; - } else { - $this->event->class = 'PRIVATE'; - } - } - - /** - * Sets the priority. Possible values are - * 0: not specified - * 1: high - * 2: middle - * 3: low - * Default is 0. - * - * @param int $priority The priority between 0 and 3. - */ - public function setPriority($priority) - { - if ($priority >= 0 && $priority < 4) - { - $this->event->priority = $priority; - } else { - $this->event->priority = 0; - } - } - - /** - * Returns the user id of the editor. - * - * @return string User id of the editor - */ - public function getEditorId() - { - return $this->event->editor_id; - } - - /** - * Returns whether this event is an all day event. - * - * @return boolean true if all day event - */ - public function isDayEvent() - { - return (date('His', $this->getStart()) == '000000' && - (date('His', $this->getEnd()) == '235959' - || date('His', $this->getEnd() - 1) == '235959')); - } - - /** - * Returns the state of accessibility as string. - * Possible values: - * PUBLIC, PRIVATE, CONFIDENTIAL - * The default is CONFIDENTIAL. - * - * @return string - */ - public function getAccessibility() - { - if ($this->event->class) { - return $this->event->class; - } - return 'CONFIDENTIAL'; - } - - /** - * Returns an array with options for accessibility depending on the permission - * of the given calendar permission. - * - * @param int $permission The calendar permission - * @return array The accessibility options. - */ - public function getAccessibilityOptions($permission) - { - switch ($permission) { - case Calendar::PERMISSION_OWN : - case Calendar::PERMISSION_ADMIN : - $options = [ - // SEMBBS nur private und vertrauliche Termine - 'PUBLIC' => _('Öffentlich'), - 'PRIVATE' => _('Privat'), - 'CONFIDENTIAL' => _('Vertraulich') - ]; - break; - case Calendar::PERMISSION_WRITABLE : - $options = [ - 'PRIVATE' => _('Privat'), - 'CONFIDENTIAL' => _('Vertraulich') - ]; - break; - default : - $options = []; - } - return $options; - } - - /** - * - * @return type - */ - public function getChangeDate() - { - return $this->event->chdate; - } - - /** - * - */ - public function getImportDate() - { - return $this->event->importdate; - } - - - /** - * Returns the object type this event belongs to. - * Possible values are 'user', 'sem', 'inst', 'fak'. - * - * @return string The object type. - */ - public function getType() - { - return get_object_type($this->range_id, ['user', 'sem', 'inst', 'fak']); - } - - /** - * Returns the priority: - * 0 means priority is not stated - * 1 means "high" - * 2 means "middle" - * 3 means "low" - * If the user has no permission it returns 0. - * - * @return int The priority. - */ - public function getPriority() - { - if ($this->havePermission(Event::PERMISSION_READABLE, - $this->permission_user_id)) { - return $this->event->priority ?: 0; - } - return 0; - } - - /** - * @return string - */ - public function getName() - { - switch ($this->type) { - case 'user': - return (string) $this->user->getFullname(); - case 'sem': - return (string) $this->course->name; - case 'inst': - case 'fak': - return (string) $this->institute->name; - default: - return ''; - } - } - - /** - * Returns all properties of this event. - * The name of the properties correspond to the properties of the - * iCalendar calendar data exchange format. There are a few properties with - * the suffix STUDIP_ which have no eqivalent in the iCalendar format. - * - * DTSTART: The start date-time as unix timestamp. - * DTEND: The end date-time as unix timestamp. - * SUMMARY: The short description (title) that will be displayed in the views. - * DESCRIPTION: The long description. - * UID: The global unique id of this event. - * CLASS: - * CATEGORIES: A comma separated list of categories. - * PRIORITY: The priority. - * LOCATION: The location. - * EXDATE: A comma separated list of unix timestamps. - * CREATED: The creation date-time as unix timestamp. - * LAST-MODIFIED: The date-time of last modification as unix timestamp. - * DTSTAMP: The cration date-time of this instance of the event as unix - * timestamp. - * RRULE: All data for the recurrence rule for this event as array. - * EVENT_TYPE: - * - * - * @return array The properties of this event. - */ - public function getProperties() - { - if ($this->properties === null) { - $this->properties = [ - 'DTSTART' => $this->getStart(), - 'DTEND' => $this->getEnd(), - 'SUMMARY' => stripslashes($this->getTitle()), - 'DESCRIPTION' => stripslashes($this->getDescription()), - 'UID' => $this->getUid(), - 'CLASS' => $this->getAccessibility(), - 'CATEGORIES' => $this->toStringCategories(), - 'STUDIP_CATEGORY' => $this->getCategory(), - 'PRIORITY' => $this->getPriority(), - 'LOCATION' => stripslashes($this->getLocation()), - 'RRULE' => $this->getRecurrence(), - 'EXDATE' => (string) $this->event->exceptions, - 'CREATED' => $this->event->mkdate, - 'LAST-MODIFIED' => $this->event->chdate, - 'STUDIP_ID' => $this->event->getId(), - 'DTSTAMP' => time(), - 'EVENT_TYPE' => 'cal', - 'STUDIP_AUTHOR_ID' => $this->event->author_id, - 'STUDIP_EDITOR_ID' => $this->event->editor_id, - 'STUDIP_GROUP_STATUS' => $this->group_status]; - } - return $this->properties; - } - - /** - * Returns the value of property with given name. - * - * @param type $name See CalendarEvent::getProperties() for accepted values. - * @return mixed The value of the property. - * @throws InvalidArgumentException - */ - public function getProperty($name) - { - if ($this->properties === null) { - $this->getProperties(); - } - - if (isset($this->properties[$name])) { - return $this->properties[$name]; - } - throw new InvalidArgumentException(get_class($this) - . ': Property ' . $name . ' does not exist.'); - } - - /** - * Returns all CalendarEvents in the given time range for the given range_id. - * - * @param string $range_id Id of Stud.IP object from type user, course, inst - * @param DateTime $start The start date time. - * @param DateTime $end The end date time. - * @return SimpleORMapCollection Collection of found CalendarEvents. - */ - public static function getEventsByInterval($range_id, DateTime $start, DateTime $end) - { - $query = "SELECT * - FROM calendar_event - INNER JOIN event_data USING (event_id) - WHERE range_id = :range_id - AND ( - start BETWEEN :start AND :end - OR ( - start <= :end - AND CAST(expire AS SIGNED) + CAST(end AS SIGNED) - CAST(start AS SIGNED) >= :start - AND rtype != 'SINGLE' - ) - OR :start BETWEEN start AND end - ) - ORDER BY start ASC"; - $stmt = DBManager::get()->prepare($query); - $stmt->execute([ - ':range_id' => $range_id, - ':start' => $start->getTimestamp(), - ':end' => $end->getTimestamp(), - ]); - $i = 0; - $event_collection = new SimpleORMapCollection(); - $event_collection->setClassName('Event'); - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $event_collection[$i] = new CalendarEvent(); - $event_collection[$i]->setData($row); - $event_collection[$i]->setNew(false); - $event = new EventData(); - $event->setData($row); - $event->setNew(false); - $event_collection[$i]->event = $event; - $i++; - } - return $event_collection; - } - - /** - * Sets the user_id to check his permission. - * - * @param string $user_id The id of the user. - */ - public function setPermissionUser($user_id) - { - $this->permission_user_id = $user_id; - } - - /** - * Checks the permission of the user previously set with - * CalendarEvent::setPermissisonUser or given by second argument. - * Returns true if the user have the at least the given permission. - * - * @param int $permission - * @param string $user_id - * @return boolean - */ - public function havePermission($permission, $user_id = null) - { - $perm = $this->getPermission($user_id); - return $perm >= $permission; - } - - /** - * Returns the permission of the given user or the user set by - * CalendarEvent::setPermssionUser previously. - * - * @staticvar array $permissions - * @param string $user_id The user's id. - * @return int The permission. - */ - public function getPermission($user_id = null) - { - static $permissions = []; - - if (is_null($user_id)) { - $user_id = $this->permission_user_id ?: $GLOBALS['user']->id; - } - if (empty($permissions[$user_id][$this->event_id])) { - if ($user_id == $this->event->author_id) { - $permissions[$user_id][$this->event_id] = Event::PERMISSION_OWN; - } else - - // SEMBBS - // Admins dürfen alle Termine löschen - /* - if ($GLOBALS['perm']->have_perm('admin')) { - $permissions[$user_id][$this->event_id] = Event::PERMISSION_DELETABLE; - } else - * - */ - - if ($user_id == $this->range_id) { - if ($this->group_status) { - $permissions[$user_id][$this->event_id] = Event::PERMISSION_READABLE; - } else { - $permissions[$user_id][$this->event_id] = Event::PERMISSION_DELETABLE; - } - } else { - switch ($this->getType()) { - case 'user': - $permissions[$user_id][$this->event_id] = - $this->getUserCalendarPermission($user_id); - break; - case 'sem': - $permissions[$user_id][$this->event_id] = - $this->getCourseCalendarPermission($user_id); - break; - case 'inst': - case 'fak': - $permissions[$user_id][$this->event_id] = - $this->getInstituteCalendarPermission($user_id); - break; - default: - $permissions[$user_id][$this->event_id] = - Event::PERMISSION_FORBIDDEN; - } - } - } - return $permissions[$user_id][$this->event_id]; - } - - /** - * Get the user's permission for this event in the actual calendar. - * - * @param string $user_id The user id. - * @return int The permission. - */ - private function getUserCalendarPermission($user_id) - { - $permission = Event::PERMISSION_FORBIDDEN; - $accessibility = $this->getAccessibility(); - if ($this->user->id) { - if ($user_id != $this->user->id) { - if ($accessibility == 'PUBLIC') { - $permission = Event::PERMISSION_READABLE; - } - $calendar_user = CalendarUser::find( - [$this->user->getId(), $user_id]); - if ($calendar_user) { - if ($accessibility == 'CONFIDENTIAL') { - if ($this->event->calendars->findOneBy('range_id', $user_id)) { - if ($calendar_user->permission == Calendar::PERMISSION_WRITABLE) { - $permission = Event::PERMISSION_WRITABLE; - } else { - $permission = Event::PERMISSION_READABLE; - } - } else { - $permission = Event::PERMISSION_CONFIDENTIAL; - } - } else { - if ($calendar_user->permission == Calendar::PERMISSION_WRITABLE) { - $permission = Event::PERMISSION_WRITABLE; - } else { - $permission = Event::PERMISSION_READABLE; - } - } - } - } else { - $permission = Event::PERMISSION_WRITABLE; - } - } - return $permission; - } - - /** - * Get the user's permission for this event in the actual calendar if the - * owner is a course. - * - * @param string $user_id The user's id. - * @return int The permission. - */ - private function getCourseCalendarPermission($user_id) - { - global $perm; - - $permission = Event::PERMISSION_FORBIDDEN; - if ($this->course->id) { - $course_perm = $perm->get_studip_perm($this->course->id, $user_id); - switch ($course_perm) { - case 'user': - case 'autor': - $permission = Event::PERMISSION_READABLE; - break; - case 'tutor': - case 'dozent': - case 'admin': - $permission = Event::PERMISSION_WRITABLE; - break; - default: - $permission = Event::PERMISSION_FORBIDDEN; - } - } - return $permission; - } - - /** - * Get the user's permission for this event in the actual calendar if the - * owner is an institute. - * - * @param string $user_id The user's id. - * @return int The permssion. - */ - private function getInstituteCalendarPermission($user_id) - { - global $perm; - $permission = Event::PERMISSION_FORBIDDEN; - if ($this->institute->id) { - $institute_perm = $perm->get_studip_perm($this->institute->id, $user_id); - switch ($institute_perm) { - case 'user'; - case 'autor': - $permission = Event::PERMISSION_READABLE; - break; - case 'tutor': - case 'dozent': - case 'admin': - $permission = Event::PERMISSION_WRITABLE; - break; - default: - $permission = Event::PERMISSION_FORBIDDEN; - } - } - return $permission; - } - - /** - * Returns the user id of the event's author. - * - * @return string The user id of the author. - */ - public function getAuthor() - { - return $this->event->author; - } - - /** - * Returns teh user id of the event's last editor. - * - * @return string The uder id og the editor. - */ - public function getEditor() - { - return $this->event->editor; - } - - /** - * Export available data of a given user into a storage object - * (an instance of the StoredUserData class) for that user. - * - * @param StoredUserData $storage object to store data into - */ - public static function exportUserData(StoredUserData $storage) - { - $sorm = CalendarEvent::findBySQL("range_id = ?", [$storage->user_id]); - if ($sorm) { - $field_data = []; - foreach ($sorm as $row) { - $field_data[] = $row->toRawArray(); - } - if ($field_data) { - $storage->addTabularData(_('Kalender'), 'calendar_event', $field_data); - } - } - } - -} diff --git a/lib/models/CalendarUser.class.php b/lib/models/CalendarUser.class.php deleted file mode 100644 index f079ac67c881e4f79a6d626b74d82bf712c93df2..0000000000000000000000000000000000000000 --- a/lib/models/CalendarUser.class.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php -/** - * CalendarUser.class.php - Model for users with access to other users 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 3.2 - * - * @property array $id alias for pk - * @property string $owner_id database column - * @property string $user_id database column - * @property int $permission database column - * @property int $mkdate database column - * @property int $chdate database column - * @property User $user belongs_to User - * @property User $owner has_one User - * @property-read mixed $nachname additional field - * @property-read mixed $vorname additional field - */ - -class CalendarUser extends SimpleORMap -{ - protected static function configure($config = []) - { - $config['db_table'] = 'calendar_user'; - - $config['has_one']['owner'] = [ - 'class_name' => User::class, - 'foreign_key' => 'owner_id', - 'assoc_foreign_key' => 'user_id' - ]; - $config['belongs_to']['user'] = [ - 'class_name' => User::class, - 'foreign_key' => 'user_id' - ]; - - $config['additional_fields']['nachname']['get'] = function ($cu) { - return $cu->user->nachname; - }; - $config['additional_fields']['vorname']['get'] = function ($cu) { - return $cu->user->vorname; - }; - - parent::configure($config); - } - - public function setPerm($permission) - { - if ($permission == Calendar::PERMISSION_READABLE) { - $this->permission = Calendar::PERMISSION_READABLE; - } else if ($permission == Calendar::PERMISSION_WRITABLE) { - $this->permission = Calendar::PERMISSION_WRITABLE; - } else { - throw new InvalidArgumentException( - 'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.'); - } - } - - public static function getUsers($user_id, $permission = null) - { - $permission_array = [Calendar::PERMISSION_READABLE, - Calendar::PERMISSION_WRITABLE]; - if (!$permission) { - $permission = $permission_array; - } else if (!in_array($permission, $permission_array)) { - throw new InvalidArgumentException( - 'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.'); - } else { - $permission = [$permission]; - } - return SimpleORMapCollection::createFromArray(CalendarUser::findBySQL( - 'owner_id = ? AND permission IN(?)', - [$user_id, $permission])); - - } - - public static function getOwners($user_id, $permission = null) - { - $permission_array = [Calendar::PERMISSION_READABLE, - Calendar::PERMISSION_WRITABLE]; - if (!$permission) { - $permission = $permission_array; - } else if (!in_array($permission, $permission_array)) { - throw new InvalidArgumentException( - 'Calendar permission must be of type PERMISSION_READABLE or PERMISSION_WRITABLE.'); - } else { - $permission = [$permission]; - } - $statement = DBManager::get()->prepare(" - SELECT * - FROM calendar_user - INNER JOIN auth_user_md5 ON (auth_user_md5.user_id = calendar_user.owner_id) - WHERE calendar_user.user_id = :user_id - AND calendar_user.permission IN (:permission) - ORDER BY auth_user_md5.Nachname, auth_user_md5.Vorname - "); - $statement->execute([ - 'user_id' => $user_id, - 'permission' => $permission - ]); - $calendar_users = []; - foreach ($statement->fetchAll(PDO::FETCH_ASSOC) as $data) { - $calendar_users[] = CalendarUser::buildExisting($data); - } - return SimpleORMapCollection::createFromArray($calendar_users); - - } -} diff --git a/lib/models/ConsultationBooking.php b/lib/models/ConsultationBooking.php index 06e6967c2a52a972afb98bd659a5fb3ef2823e8d..7782681fa45797208533089a43e2fedbfc9f94f7 100644 --- a/lib/models/ConsultationBooking.php +++ b/lib/models/ConsultationBooking.php @@ -16,7 +16,7 @@ * @property int $chdate database column * @property ConsultationSlot $slot belongs_to ConsultationSlot * @property User $user belongs_to User - * @property EventData|null $event has_one EventData + * @property CalendarDate $event has_one CalendarDate */ class ConsultationBooking extends SimpleORMap implements PrivacyObject { @@ -37,9 +37,9 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject 'foreign_key' => 'user_id', ]; $config['has_one']['event'] = [ - 'class_name' => EventData::class, + 'class_name' => CalendarDate::class, 'foreign_key' => 'student_event_id', - 'assoc_foreign_key' => 'event_id', + 'assoc_foreign_key' => 'id', 'on_delete' => 'delete', ]; @@ -48,8 +48,8 @@ class ConsultationBooking extends SimpleORMap implements PrivacyObject setTempLanguage($booking->user_id); $event = $booking->slot->createEvent($booking->user); - $event->category_intern = 1; - $event->summary = sprintf( + $event->category = 1; + $event->title = sprintf( _('Termin bei %s'), $booking->slot->block->range->getFullName() ); diff --git a/lib/models/ConsultationEvent.php b/lib/models/ConsultationEvent.php index 2665c6be1ba1dd19b004ec5c0ad6e134cff2fd9b..e0b6b5962cddb3a58ba8e8bba2d98e88db63d190 100644 --- a/lib/models/ConsultationEvent.php +++ b/lib/models/ConsultationEvent.php @@ -10,7 +10,7 @@ * @property string $event_id database column * @property int $mkdate database column * @property ConsultationSlot $slot belongs_to ConsultationSlot - * @property EventData $event has_one EventData + * @property CalendarDate $event belongs_to CalendarDate */ class ConsultationEvent extends SimpleORMap { @@ -23,9 +23,9 @@ class ConsultationEvent extends SimpleORMap 'foreign_key' => 'slot_id', ]; $config['has_one']['event'] = [ - 'class_name' => EventData::class, + 'class_name' => CalendarDate::class, 'foreign_key' => 'event_id', - 'assoc_foreign_key' => 'event_id', + 'assoc_foreign_key' => 'id', 'on_delete' => 'delete', ]; diff --git a/lib/models/ConsultationSlot.php b/lib/models/ConsultationSlot.php index b9881900b18b930117fa5425af98a88e9a9f7ee3..2a71fbd3a0d8beeb3a38b9e2cb1f3845dccf01b9 100644 --- a/lib/models/ConsultationSlot.php +++ b/lib/models/ConsultationSlot.php @@ -166,26 +166,24 @@ class ConsultationSlot extends SimpleORMap * Creates a Stud.IP calendar event relating to the slot. * * @param User $user User object to create the event for - * @return EventData Created event + * @return CalendarDate Created event */ - public function createEvent(User $user) + public function createEvent(User $user) : CalendarDate { - $event = new EventData(); - $event->uid = $this->createEventId($user); + $event = new CalendarDate(); + $event->unique_id = $this->createEventId($user); $event->author_id = $user->id; $event->editor_id = $user->id; - $event->start = $this->start_time; + $event->begin = $this->start_time; $event->end = $this->end_time; - $event->class = 'PRIVATE'; - $event->priority = 0; + $event->access = 'PRIVATE'; $event->location = $this->block->room; - $event->rtype = 'SINGLE'; + $event->repetition_type = ''; $event->store(); - $calendar_event = new CalendarEvent(); - $calendar_event->range_id = $user->id; - $calendar_event->group_status = 0; - $calendar_event->event_id = $event->id; + $calendar_event = new CalendarDateAssignment(); + $calendar_event->range_id = $user->id; + $calendar_event->calendar_date_id = $event->id; $calendar_event->store(); return $event; @@ -267,12 +265,12 @@ class ConsultationSlot extends SimpleORMap }); if (count($bookings) > 0) { - $event->event->category_intern = 1; + $event->event->category = 1; if (count($bookings) === 1) { $booking = $bookings->first(); - $event->event->summary = sprintf( + $event->event->title = sprintf( _('Termin mit %s'), $booking->user ? $booking->user->getFullName() : _('unbekannt') ); @@ -288,9 +286,9 @@ class ConsultationSlot extends SimpleORMap })); } } else { - $event->event->category_intern = 9; - $event->event->summary = _('Freier Termin'); - $event->event->description = _('Dieser Termin ist noch nicht belegt.'); + $event->event->category = 9; + $event->event->title = _('Freier Termin'); + $event->event->description = _('Dieser Termin ist noch nicht belegt.'); } $event->event->store(); diff --git a/lib/models/Contact.class.php b/lib/models/Contact.class.php index be095de1f12dda49c7ff31fae7fb32923272fdb5..44d8bcdd651517ff6a99ada3fa0d0ef66a84a4fb 100644 --- a/lib/models/Contact.class.php +++ b/lib/models/Contact.class.php @@ -10,9 +10,14 @@ * @property string $owner_id database column * @property string $user_id database column * @property int|null $mkdate database column + * @property string $calendar_permissions database column + * An enum with the possible values "", "READ" and "WRITE". + * The empty string specifies that no calendar permissions are granted. * @property SimpleORMapCollection|StatusgruppeUser[] $group_assignments has_many StatusgruppeUser * @property User $owner belongs_to User * @property User $friend belongs_to User + * @property string $mkdate database column + * @property string $chdate database column */ class Contact extends SimpleORMap { @@ -29,15 +34,16 @@ class Contact extends SimpleORMap 'foreign_key' => 'user_id' ]; - $config['has_many']['group_assignments'] = [ - 'class_name' => 'StatusgruppeUser', + $config['has_many']['groups'] = [ + 'class_name' => ContactGroupItem::class, 'assoc_func' => 'findByContact', 'foreign_key' => function ($me) { return [$me]; }, - 'assoc_foreign_key' => function ($group, $params) { - $group->setValue('user_id', $params[0]->user_id); - }, + 'assoc_foreign_key' => function ($item, $params) { + //Nothing else here. But this has to be present + //so that storing a new contact works. + }, 'on_store' => 'store', 'on_delete' => 'delete' ]; diff --git a/lib/models/ContactGroup.class.php b/lib/models/ContactGroup.class.php new file mode 100644 index 0000000000000000000000000000000000000000..0e60d3b5a5ca6185ed4377194c0b2ef5256fd3ad --- /dev/null +++ b/lib/models/ContactGroup.class.php @@ -0,0 +1,44 @@ +<?php +/** + * The ContactGroup class represents a contact group of a user. + * + * This file is part of Stud.IP + * + * 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 2023 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @package resources + * @since 5.5 + * + * @property string $id The ID of the group. + * @property string $name Name of the group. + * @property string $owner_id The ID of the owner to whom the group belongs to. + * @property string $mkdate The creation date of the group. + * @property string $chdate The modification date of the group. + * @property User $owner The owner of the group. + * @property ContactGroupItem[]|SimpleORMapCollection $items The items (users) that belong to the group. + */ +class ContactGroup extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'contact_groups'; + $config['belongs_to']['owner'] = [ + 'class_name' => User::class, + 'foreign_key' => 'owner_id' + ]; + $config['has_many']['items'] = [ + 'class_name' => ContactGroupItem::class, + 'assoc_foreign_key' => 'group_id', + 'on_store' => 'store', + 'on_delete' => 'delete' + ]; + parent::configure($config); + } +} diff --git a/lib/models/ContactGroupItem.class.php b/lib/models/ContactGroupItem.class.php new file mode 100644 index 0000000000000000000000000000000000000000..0204d0a5a6578a1ba5407b10afeaa516f10595f4 --- /dev/null +++ b/lib/models/ContactGroupItem.class.php @@ -0,0 +1,61 @@ +<?php +/** + * The ContactGroupItem class represents an item in a contact group. + * + * This file is part of Stud.IP + * + * 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 2023 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @package resources + * @since 5.5 + * + * @property string $group_id The ID of the group. + * @property string $user_id The ID of the user that is inside the group. + * @property string $mkdate The creation date of the group. + * @property string $chdate The modification date of the group. + * @property ContactGroup $contact_group The group instance for the item. + * @property User $user The user instance for the item. + */ +class ContactGroupItem extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'contact_group_items'; + $config['belongs_to']['contact_group'] = [ + 'class_name' => ContactGroup::class, + 'foreign_key' => 'group_id' + ]; + $config['belongs_to']['user'] = [ + 'class_name' => User::class, + 'foreign_key' => 'user_id' + ]; + parent::configure($config); + } + + /** + * Finds and returns all group items for a contact. + * + * @param Contact $contact The contact for which to find all contact group items. + * @return ContactGroupItem[] All memberships of the contact. + */ + public static function findByContact(Contact $contact): array + { + return self::findBySQL( + 'JOIN `contact_groups` + ON (`contact_group_items`.`group_id` = `contact_groups`.`id`) + WHERE `contact_groups`.`owner_id` = :owner_id + AND `contact_group_items`.`user_id` = :user_id', + [ + 'owner_id' => $contact->owner_id, + 'user_id' => $contact->user_id + ] + ); + } +} diff --git a/lib/models/Course.class.php b/lib/models/Course.class.php index ab582196a4d32faeeb36475b57f5bf83d5a22247..75dad332fe6fc52e6bad37cd1c7e7500e4f5e939 100644 --- a/lib/models/Course.class.php +++ b/lib/models/Course.class.php @@ -80,7 +80,7 @@ * @property-read mixed $config additional field */ -class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange +class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, FeedbackRange, Studip\Calendar\Owner { protected static function configure($config = []) { @@ -1081,4 +1081,40 @@ class Course extends SimpleORMap implements Range, PrivacyObject, StudipItem, Fe { return $this->getFullName(); } + + /** + * @inheritDoc + */ + public static function getCalendarOwner(string $owner_id): ?\Studip\Calendar\Owner + { + return self::find($owner_id); + } + + /** + * @inheritDoc + */ + public function isCalendarReadable(?string $user_id = null): bool + { + if ($user_id === null) { + $user_id = self::findCurrent()->id; + } + + //Calendar read permissions are granted for all participants + //that have at least user permissions. + return $GLOBALS['perm']->have_studip_perm('user', $this->id, $user_id); + } + + /** + * @inheritDoc + */ + public function isCalendarWritable(string $user_id = null): bool + { + if ($user_id === null) { + $user_id = self::findCurrent()->id; + } + + //Calendar write permissions are granted for all participants + //that have autor permissions or higher. + return $GLOBALS['perm']->have_studip_perm('autor', $this->id, $user_id); + } } diff --git a/lib/models/CourseCancelledEvent.class.php b/lib/models/CourseCancelledEvent.class.php deleted file mode 100644 index 5fed9c53989651b85fa19fca9d27b94aab30adc3..0000000000000000000000000000000000000000 --- a/lib/models/CourseCancelledEvent.class.php +++ /dev/null @@ -1,149 +0,0 @@ -<?php -/** - * 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 Peter Thienel <thienel@data-quest.de> - * @copyright 2014 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * - * @property string $id alias for pk - * @property string $termin_id database column - * @property string $event_id alias column for termin_id - * @property string $range_id database column - * @property string $sem_id alias column for range_id - * @property string $autor_id database column - * @property string $author_id alias column for autor_id - * @property string $content database column - * @property string $ex_description alias column for content - * @property int $date database column - * @property int $start alias column for date - * @property int $end_time database column - * @property int $end alias column for end_time - * @property int $mkdate database column - * @property int $chdate database column - * @property int $date_typ database column - * @property int $category_intern alias column for date_typ - * @property string|null $raum database column - * @property string|null $metadate_id database column - * @property string $resource_id database column - * @property SimpleORMapCollection|Folder[] $folders has_many Folder - * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest - * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment - * @property User $author belongs_to User - * @property Course $course belongs_to Course - * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate - * @property ResourceBooking $room_booking has_one ResourceBooking - * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic - * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen - * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User - * @property-read mixed $location additional field - * @property mixed $type additional field - * @property-read mixed $name additional field - * @property-read mixed $title additional field - * @property-read mixed $editor_id additional field - * @property-read mixed $uid additional field - * @property-read mixed $summary additional field - * @property-read mixed $description additional field - */ - -class CourseCancelledEvent extends CourseEvent -{ - - protected static function configure($config = []) - { - $config['alias_fields']['ex_description'] = 'content'; - - if (!self::TableScheme('ex_termine')) { - throw new Exception('Cannot obtain table meta data for table "ex_termine"'); - } - - $config['db_fields'] = self::$schemes['ex_termine']['db_fields']; - $config['pk'] = self::$schemes['ex_termine']['pk']; - - parent::configure($config); - self::$config['CourseCancelledEvent']['db_table'] = 'ex_termine'; - } - - /** - * Returns all CourseCancelledEvents in the given time range for the given range_id. - * - * @param string $user_id Id of Stud.IP object from type user, course, inst - * @param DateTime $start The start date time. - * @param DateTime $end The end date time. - * @return SimpleORMapCollection Collection of found CourseCancelledEvents. - */ - public static function getEventsByInterval($user_id, DateTime $start, dateTime $end) - { - $stmt = DBManager::get()->prepare('SELECT ex_termine.* FROM seminar_user ' - . 'INNER JOIN ex_termine ON seminar_id = range_id ' - . 'WHERE ex_termine.content <> \'\' AND user_id = :user_id ' - . 'AND date BETWEEN :start AND :end ' - . "AND (IFNULL(metadate_id, '') = '' " - . 'OR metadate_id NOT IN ( ' - . 'SELECT metadate_id FROM schedule_seminare ' - . 'WHERE user_id = :user_id AND visible = 0) ) ' - . 'ORDER BY date ASC'); - $stmt->execute([ - ':user_id' => $user_id, - ':start' => $start->getTimestamp(), - ':end' => $end->getTimestamp() - ]); - $event_collection = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $event = new CourseCancelledEvent(); - $event->setData($row); - $event->setNew(false); - // related persons (dozenten) or groups - if (self::checkRelated($event, $user_id)) { - $event_collection[] = $event; - } - } - $event_collection = SimpleORMapCollection::createFromArray($event_collection, false); - $event_collection->setClassName('Event'); - return $event_collection; - } - - /** - * Returns the title of this event. - * The title of a course event is the name of the course or if a topic is - * assigned, the title of this topic. If the user has not the permission - * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.". - * - * @return string - */ - public function getTitle() - { - $title = parent::getTitle(); - if ($this->havePermission(Event::PERMISSION_READABLE)) { - $title .= ' ' . _('(fällt aus)'); - } - return $title; - } - - /** - * Returns the index of the category. - * If the user has no permission, 255 is returned. - * - * TODO remove? use getStudipCategory instead? - * - * @see config/config.inc.php $TERMIN_TYP - * @return int The index of the category - */ - public function getCategory() - { - return 255; - } - - public function getDescription() - { - if ($this->havePermission(Event::PERMISSION_READABLE)) { - return $this->ex_description; - } - return ''; - } - -} diff --git a/lib/models/CourseDate.class.php b/lib/models/CourseDate.class.php index 5ba3534df03a738b387c6b2136a5de846ef401d5..2a5b77e848a968425757a24ed75352933117fd42 100644 --- a/lib/models/CourseDate.class.php +++ b/lib/models/CourseDate.class.php @@ -34,7 +34,7 @@ * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User */ -class CourseDate extends SimpleORMap implements PrivacyObject +class CourseDate extends SimpleORMap implements PrivacyObject, Event { const FORMAT_DEFAULT = 'default'; const FORMAT_VERBOSE = 'verbose'; @@ -476,4 +476,190 @@ class CourseDate extends SimpleORMap implements PrivacyObject date('H:i', $this->end_time) ); } + + //Start of Event interface implementation. + + public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array + { + return self::findBySQL( + "JOIN `seminar_user` + ON `seminar_user`.`seminar_id` = `termine`.`range_id` + WHERE `seminar_user`.`user_id` = :user_id + AND `termine`.`date` BETWEEN :begin AND :end + AND ( + IFNULL(`termine`.`metadate_id`, '') = '' + OR `termine`.`metadate_id` NOT IN ( + SELECT `metadate_id` + FROM `schedule_seminare` + WHERE `user_id` = :user_id + AND `visible` = 0 + ) + ) + ORDER BY date", + [ + 'begin' => $begin->getTimestamp(), + 'end' => $end->getTimestamp(), + 'user_id' => $range_id + ] + ); + } + + //Event interface implementation: + + public function getObjectId() : string + { + return (string) $this->id; + } + + public function getPrimaryObjectID(): string + { + return $this->range_id; + } + + public function getObjectClass(): string + { + return static::class; + } + + public function getTitle(): string + { + return $this->course->name ?? ''; + } + + public function getBegin(): DateTime + { + $begin = new DateTime(); + $begin->setTimestamp($this->date); + return $begin; + } + + public function getEnd(): DateTime + { + $end = new DateTime(); + $end->setTimestamp($this->end_time); + return $end; + } + + public function getDuration(): DateInterval + { + $begin = $this->getBegin(); + $end = $this->getEnd(); + return $end->diff($begin); + } + + public function getLocation(): string + { + return $this->raum ?? ''; + } + + public function getUniqueId(): string + { + return sprintf('Stud.IP-SEM-%1$s@%2$s', $this->id, $_SERVER['SERVER_NAME']); + } + + public function getDescription(): string + { + $descriptions = $this->topics->map(function ($topic) { + $desc = $topic->title . "\n"; + $desc .= $topic->description; + + return $desc; + }); + return implode("\n\n", $descriptions); + } + + public function getAdditionalDescriptions(): array + { + $descriptions = []; + if (count($this->dozenten) > 0) { + $descriptions[_('Durchführende Lehrende')] = implode(', ', $this->dozenten->getFullname()); + } + if (count($this->statusgruppen) > 0) { + $descriptions[_('Beteiligte Gruppen')] = implode(', ', $this->statusgruppen->getValue('name')); + } + return $descriptions; + } + + public function isAllDayEvent(): bool + { + //Course dates are never all day events. + return false; + } + + public function isWritable(string $user_id): bool + { + return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id); + } + + public function getCreationDate(): DateTime + { + $mkdate = new DateTime(); + $mkdate->setTimestamp($this->mkdate); + return $mkdate; + } + + public function getModificationDate(): DateTime + { + $chdate = new DateTime(); + $chdate->setTimestamp($this->chdate); + return $chdate; + } + + public function getImportDate(): DateTime + { + return $this->getCreationDate(); + } + + public function getAuthor(): ?User + { + return $this->author; + } + + public function getEditor(): ?User + { + return null; + } + + public function toEventData(string $user_id): \Studip\Calendar\EventData + { + $begin = new DateTime(); + $begin->setTimestamp($this->date); + $end = new DateTime(); + $end->setTimestamp($this->end_time); + + $membership = CourseMember::findOneBySQL( + 'seminar_id = :course_id AND user_id = :user_id', + ['course_id' => $this->range_id, 'user_id' => $user_id] + ); + $class_names = []; + if ($membership) { + $class_names[] = sprintf('gruppe%u', $membership->status); + } + $studip_view_urls = []; + if ($GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user_id)) { + $studip_view_urls['show'] = URLHelper::getURL('dispatch.php/course/dates/details/' . $this->id, ['cid' => $this->range_id, 'extra_buttons' => '1']); + } + + return new \Studip\Calendar\EventData( + $begin, + $end, + $this->getTitle(), + $class_names, + '#000000', + '#aaaaaa', + $this->isWritable($user_id), + CourseDate::class, + $this->id, + Course::class, + $this->range_id, + 'course', + $this->range_id, + $studip_view_urls, + [], + 'seminar', + 'rgba(0,0,0,0)' + ); + } + + //End of Event interface implementation. } diff --git a/lib/models/CourseEvent.class.php b/lib/models/CourseEvent.class.php deleted file mode 100644 index fa68b136a1007d9edd891ef0717c78da56d970fb..0000000000000000000000000000000000000000 --- a/lib/models/CourseEvent.class.php +++ /dev/null @@ -1,596 +0,0 @@ -<?php -/** - * 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 Peter Thienel <thienel@data-quest.de> - * @copyright 2014 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * - * @property string $id alias for pk - * @property string $termin_id database column - * @property string $event_id alias column for termin_id - * @property string $range_id database column - * @property string $sem_id alias column for range_id - * @property string $autor_id database column - * @property string $author_id alias column for autor_id - * @property string $content database column - * @property int $date database column - * @property int $start alias column for date - * @property int $end_time database column - * @property int $end alias column for end_time - * @property int $mkdate database column - * @property int $chdate database column - * @property int $date_typ database column - * @property int $category_intern alias column for date_typ - * @property string|null $raum database column - * @property string|null $metadate_id database column - * @property SimpleORMapCollection|Folder[] $folders has_many Folder - * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest - * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment - * @property User $author belongs_to User - * @property Course $course belongs_to Course - * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate - * @property ResourceBooking $room_booking has_one ResourceBooking - * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic - * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen - * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User - * @property-read mixed $location additional field - * @property mixed $type additional field - * @property-read mixed $name additional field - * @property-read mixed $title additional field - * @property-read mixed $editor_id additional field - * @property-read mixed $uid additional field - * @property-read mixed $summary additional field - * @property-read mixed $description additional field - */ - -class CourseEvent extends CourseDate implements Event -{ - protected static function configure($config = []) - { - $config['alias_fields']['event_id'] = 'termin_id'; - $config['alias_fields']['start'] = 'date'; - $config['alias_fields']['end'] = 'end_time'; - $config['alias_fields']['category_intern'] = 'date_typ'; - $config['alias_fields']['author_id'] = 'autor_id'; - $config['alias_fields']['sem_id'] = 'range_id'; - - $config['additional_fields']['location']['get'] = 'getRoomName'; - $config['additional_fields']['type'] = true; - $config['additional_fields']['name']['get'] = function ($event) { - return $event->course->getFullname(); - }; - $config['additional_fields']['title']['get'] = 'getTitle'; - $config['additional_fields']['editor_id']['get'] = function ($date) { - return null; - }; - $config['additional_fields']['uid']['get'] = function ($date) { - $host = $_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'], PHP_URL_HOST); - return 'Stud.IP-SEM-' . $date->getId() . '@' . $host; - }; - $config['additional_fields']['summary']['get'] = function ($date) { - return $date->course->name; - }; - $config['additional_fields']['description']['get'] = function ($date) { - return ''; - }; - parent::configure($config); - } - - private $properties = null; - private $permission_user_id = null; - - public function __construct($id = null) - { - $this->permission_user_id = $GLOBALS['user']->id; - parent::__construct($id); - } - - /** - * Returns all CourseEvents in the given time range for the given range_id. - * - * @param string $user_id Id of Stud.IP object from type user, course, inst - * @param DateTime $start The start date time. - * @param DateTime $end The end date time. - * @return SimpleORMapCollection Collection of found CourseEvents. - */ - public static function getEventsByInterval($user_id, DateTime $start, dateTime $end) - { - $stmt = DBManager::get()->prepare('SELECT termine.* FROM seminar_user ' - . 'INNER JOIN termine ON seminar_id = range_id ' - . 'WHERE user_id = :user_id ' - . 'AND bind_calendar = 1 ' - . 'AND date BETWEEN :start AND :end ' - . "AND (IFNULL(metadate_id, '') = '' " - . 'OR metadate_id NOT IN ( ' - . 'SELECT metadate_id FROM schedule_seminare ' - . 'WHERE user_id = :user_id AND visible = 0) ) ' - . 'ORDER BY date ASC'); - $stmt->execute([ - ':user_id' => $user_id, - ':start' => $start->getTimestamp(), - ':end' => $end->getTimestamp() - ]); - $event_collection = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $event = new CourseEvent(); - $event->setData($row); - $event->setNew(false); - // related persons (dozenten) or groups - if (self::checkRelated($event, $user_id)) { - $event_collection[] = $event; - } - } - $event_collection = SimpleORMapCollection::createFromArray($event_collection, false); - $event_collection->setClassName('Event'); - return $event_collection; - } - - /** - * Checks if given user is the responsible lecturer or is member of a - * related group - * - * @global object $perm The globa perm object. - * @param CourseEvent $event The course event to check against. - * @param string $user_id The id of the user. - * @return boolean - */ - protected static function checkRelated(CourseEvent $event, $user_id) - { - global $perm; - - $check_related = false; - $permission = $perm->get_studip_perm($event->range_id, $user_id); - switch ($permission) { - case 'dozent' : - $related_persons = $event->dozenten->pluck('user_id'); - if (sizeof($related_persons)) { - if (in_array($user_id, $related_persons)) { - $check_related = true; - } - } else { - $check_related = true; - } - break; - case 'tutor' : - $check_related = true; - break; - default : - $group_ids = $event->statusgruppen->pluck('statusgruppe_id'); - if (sizeof($group_ids)) { - $member = StatusgruppeUser::findBySQL( - 'statusgruppe_id IN(?) AND user_id = ?', - [$group_ids, $user_id]); - $check_related = sizeof($member) > 0; - } else { - $check_related = true; - } - } - return $check_related; - } - - /** - * Returns the name of the category. - * - * @return array|string the name of the category - */ - public function toStringCategories($as_array = false) - { - $category = ''; - if ( - $this->havePermission(Event::PERMISSION_READABLE) - && !empty($GLOBALS['TERMIN_TYP'][$this->getCategory()]) - ) { - $category = $GLOBALS['TERMIN_TYP'][$this->getCategory()]['name']; - } - return $as_array ? [$category] : $category; - } - - /** - * Returns the id of the related course - * - * @return string The id of the related course. - */ - public function getSeminarId() - { - if ($this->havePermission(Event::PERMISSION_READABLE)) { - return $this->sem_id; - } - return null; - } - - /** - * Returns an array that represents the recurrence rule for this event. - * If an index is given, returns only this field of the rule. - * - * @return array|string The array with th recurrence rule or only one field. - */ - public function getRecurrence($index = null) - { - $rep = ['ts' => 0, 'linterval' => 0, 'sinterval' => 0, 'wdays' => '', - 'month' => 0, 'day' => 0, 'rtype' => 'SINGLE', 'duration' => 1]; - return $index ? $rep[$index] : $rep; - } - - /** - * Returns the name of the related course. - * - * @return string The name of the related course. - */ - public function getSemName() - { - if ($this->havePermission(Event::PERMISSION_READABLE)) { - return $this->course->name; - } - return ''; - } - - /** - * TODO Wird das noch benötigt? - */ - public function getType() - { - return 1; - } - - /** - * Returns the title of this event. - * The title of a course event is the name of the course or if a topic is - * assigned, the title of this topic. If the user has not the permission - * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.". - * - * @return string - */ - public function getTitle() - { - $title = _('Keine Berechtigung.'); - if ($this->havePermission(Event::PERMISSION_READABLE)) { - $description = $this->cycle ? trim($this->cycle->description) : ''; - if (sizeof($this->topics)) { - $title = $this->course->name.": ".implode(', ', $this->topics->pluck('title')); - } else { - $title = $this->course->name; - } - $title = ($description ? $description . ', ' : '') . $title; - } - return $title; - } - - /** - * Returns the starttime as unix timestamp of this event. - * - * @return int The starttime of this event as a unix timestamp. - */ - public function getStart() - { - return $this->date; - } - - /** - * Sets the start date time with given unix timestamp. - * - * @param string $timestamp Unix timestamp. - */ - public function setStart($timestamp) - { - $this->date = $timestamp; - } - - /** - * Returns the endtime of this event. - * - * @return int The endtime of this event as a unix timestamp. - */ - public function getEnd() - { - return $this->end_time; - } - - /** - * Sets the end date time by given unix timestamp. - * - * @param string $timestamp Unix timestamp. - */ - public function setEnd($timestamp) - { - $this->end_time = $timestamp; - } - - /** - * Returns the duration of this event in seconds. - * - * @return int the duration of this event in seconds - */ - function getDuration() - { - return $this->end - $this->start; - } - - /** - * Returns the location. - * Without permission or the location is not set an empty string is returned. - * - * @see ClendarDate::getRoomName() - * @return string The location - */ - function getLocation() - { - $location = ''; - if ($this->havePermission(Event::PERMISSION_READABLE)) { - $location = $this->getRoomName(); - } - return $location; - } - - /** - * Returns the global unique id of this event. - * - * @return string The global unique id. - */ - public function getUid() - { - return $this->uid; - } - - /** - * Returns the description of the topic. - * If the user has no permission or the event has no topic - * or the topics have no descritopn an empty string is returned. - * - * @return String the description - */ - function getDescription() - { - $description = ''; - if ($this->havePermission(Event::PERMISSION_READABLE)) { - $descriptions = $this->topics->map(function ($topic) { - $desc = $topic->title . "\n"; - $desc .= $topic->description; - }); - $description = implode("\n\n", $descriptions); - } - return $description; - } - - /** - * Returns the Stud.IP build in category as integer value. - * If the user has no permission, 255 is returned. - * - * @See config.inc.php $PERS_TERMIN - * @return int the categories - */ - public function getStudipCategory() - { - if ($this->havePermission(Event::PERMISSION_READABLE)) { - return $this->date_typ; - } - return 255; - } - - /** - * Returns the index of the category. - * If the user has no permission, 255 is returned. - * - * TODO remove? use getStudipCategory instead? - * - * @see config/config.inc.php $TERMIN_TYP - * @return int The index of the category - */ - public function getCategory() - { - if ($this->havePermission(Event::PERMISSION_READABLE)) { - return $this->date_typ; - } - return 255; - } - - /** - * Returns the user id of the last editor. - * Since course events have no editor null is returned. - * - * @return null|int Returns always null. - */ - public function getEditorId() - { - return null; - } - - /** - * Returns whether the event is a all day event. - * - * @return - */ - public function isDayEvent() - { - return (($this->end - $this->start) / 60 / 60) > 23; - } - - /** - * Returns the accessibility of this event. The value is not influenced by - * the permission of the actual user. - * - * According to RFC5545 the accessibility (property CLASS) is represented - * by the 3 state PUBLIC, PRIVATE and CONFIDENTIAL - * - * TODO check this statement: - * An course event is always CONFIDENTIAL - * - * @return string The accessibility as string. - */ - public function getAccessibility() - { - return 'CONFIDENTIAL'; - } - - /** - * Returns the unix timestamp of the last change. - * - * @access public - */ - public function getChangeDate() - { - return $this->chdate; - } - - /** - * Returns the date time the event was imported. - * Since course events are not imported normaly, returns the date time - * of creation. - * - * @return int Date time of import as unix timestamp: - */ - public function getImportDate() - { - return $this->mkdate; - } - - /** - * Returns all related groups. - * - * TODO remove, use direct access to field CourseDate::statusgruppen. - * - * @return SimpleORMapCollection The collection of statusgruppen. - */ - public function getRelatedGroups() - { - return $this->statusgruppen; - } - - public function getProperties() - { - if ($this->properties === null) { - $this->properties = [ - 'DTSTART' => $this->getStart(), - 'DTEND' => $this->getEnd(), - 'SUMMARY' => $this->getTitle(), - 'DESCRIPTION' => $this->getDescription(), - 'LOCATION' => $this->getLocation(), - 'CATEGORIES' => $this->toStringCategories(), - 'STUDIP_CATEGORY' => $this->getStudipCategory(), - 'CREATED' => $this->mkdate, - 'LAST-MODIFIED' => $this->chdate, - 'STUDIP_ID' => $this->termin_id, - 'SEM_ID' => $this->range_id, - 'SEMNAME' => $this->course->name, - 'CLASS' => 'CONFIDENTIAL', - 'UID' => CourseEvent::getUid(), - 'RRULE' => CourseEvent::getRecurrence(), - 'EXDATE' => '', - 'EVENT_TYPE' => 'sem', - 'STATUS' => 'CONFIRMED', - 'DTSTAMP' => time()]; - } - return $this->properties; - } - - /** - * Returns the value of property with given name. - * - * @param type $name See CalendarEvent::getProperties() for accepted values. - * @return mixed The value of the property. - * @throws InvalidArgumentException - */ - public function getProperty($name) - { - if ($this->properties === null) { - $this->getProperties(); - } - - if (isset($this->properties[$name])) { - return $this->properties[$name]; - } - throw new InvalidArgumentException(get_class($this) - . ': Property ' . $name . ' does not exist.'); - } - - public function setPermissionUser($user_id) - { - $this->permission_user_id = $user_id; - } - - public function havePermission($permission, $user_id = null) - { - $perm = $this->getPermission($user_id); - return $perm >= $permission; - } - - public function getPermission($user_id = null) - { - global $perm; - - $user_id = $user_id ?: $this->permission_user_id; - $course_perm = $perm->get_studip_perm($this->range_id, $user_id); - $permission = Event::PERMISSION_FORBIDDEN; - switch ($course_perm) { - case 'tutor': - case 'dozent': - $permission = Event::PERMISSION_WRITABLE; - break; - case 'user': - case 'autor': - $permission = Event::PERMISSION_READABLE; - break; - default: - $permission = Event::PERMISSION_FORBIDDEN; - } - - return $permission; - } - - /** - * Course events have no priority so returns always an empty string. - * - * @return string The priority as a string. - */ - public function toStringPriority() - { - return ''; - } - - /** - * Course events have no accessibility settings so returns always the - * an empty string. - * - * @return string The accessibility as string. - */ - public function toStringAccessibility() - { - return ''; - } - - /** - * Returns a string representation of the recurrence rule. - * Since course events have no recurence defined it returns an empty string. - * - * @param bool $only_type If true returns only the type of recurrence. - * @return string The recurrence rule - human readable - */ - public function toStringRecurrence($only_type = false) - { - return ''; - } - - /** - * Returns the author of this event as user object. - * - * @return User|null User object. - */ - public function getAuthor() - { - return $this->author; - } - - /** - * Course events have no editor so always null is returned. - * - * @return null - */ - public function getEditor() - { - return null; - } -} diff --git a/lib/models/CourseExDate.class.php b/lib/models/CourseExDate.class.php index 993767a37b8eb77c78b207423f9424488e0d002f..ce50e314dd332797c8d810ce20c7420654f7c549 100644 --- a/lib/models/CourseExDate.class.php +++ b/lib/models/CourseExDate.class.php @@ -33,7 +33,7 @@ * @property-read mixed $room_request additional field */ -class CourseExDate extends SimpleORMap implements PrivacyObject +class CourseExDate extends SimpleORMap implements PrivacyObject, Event { /** * Configures this model. @@ -247,4 +247,176 @@ class CourseExDate extends SimpleORMap implements PrivacyObject date('H:i', $this->end_time) ); } + + //Start of Event interface implementation. + + public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array + { + return self::findBySQL( + "JOIN `seminar_user` + ON `seminar_user`.`seminar_id` = `ex_termine`.`range_id` + WHERE `seminar_user`.`user_id` = :user_id + AND `ex_termine`.`date` BETWEEN :begin AND :end + AND ( + IFNULL(`ex_termine`.`metadate_id`, '') = '' + OR `ex_termine`.`metadate_id` NOT IN ( + SELECT `metadate_id` + FROM `schedule_seminare` + WHERE `user_id` = :user_id + AND `visible` = 0 + ) + ) + ORDER BY date", + [ + 'begin' => $begin->getTimestamp(), + 'end' => $end->getTimestamp(), + 'user_id' => $range_id + ] + ); + } + + //Event interface implementation: + + public function getObjectId() : string + { + return (string) $this->id; + } + + public function getPrimaryObjectID(): string + { + return $this->range_id; + } + + public function getObjectClass(): string + { + return static::class; + } + + public function getTitle(): string + { + return sprintf('%s (fällt aus)', $this->course->name ?? ''); + } + + public function getBegin(): DateTime + { + $begin = new DateTime(); + $begin->setTimestamp($this->date); + return $begin; + } + + public function getEnd(): DateTime + { + $end = new DateTime(); + $end->setTimestamp($this->end_time); + return $end; + } + + public function getDuration(): DateInterval + { + $begin = $this->getBegin(); + $end = $this->getEnd(); + return $end->diff($begin); + } + + public function getLocation(): string + { + return ''; + } + + public function getUniqueId(): string + { + return sprintf('Stud.IP-SEM-%1$s@%2$s', $this->id, $_SERVER['SERVER_NAME']); + } + + public function getDescription(): string + { + return trim($this->getValue('content')); + } + + public function getAdditionalDescriptions(): array + { + $descriptions = []; + if (count($this->dozenten) > 0) { + $descriptions[_('Durchführende Lehrende')] = implode(', ', $this->dozenten->getFullname()); + } + if (count($this->statusgruppen) > 0) { + $descriptions[_('Beteiligte Gruppen')] = implode(', ', $this->statusgruppen->getValue('name')); + } + return $descriptions; + } + + public function isAllDayEvent(): bool + { + //Course dates are never all day events. + return false; + } + + public function isWritable(string $user_id): bool + { + return $GLOBALS['perm']->have_studip_perm('dozent', $this->range_id, $user_id); + } + + public function getCreationDate(): DateTime + { + $mkdate = new DateTime(); + $mkdate->setTimestamp($this->mkdate); + return $mkdate; + } + + public function getModificationDate(): DateTime + { + $chdate = new DateTime(); + $chdate->setTimestamp($this->chdate); + return $chdate; + } + + public function getImportDate(): DateTime + { + return $this->getCreationDate(); + } + + public function getAuthor(): ?User + { + return $this->author; + } + + public function getEditor(): ?User + { + return null; + } + + public function toEventData(string $user_id): \Studip\Calendar\EventData + { + $begin = new DateTime(); + $begin->setTimestamp($this->date); + $end = new DateTime(); + $end->setTimestamp($this->end_time); + + $studip_view_urls = []; + if ($GLOBALS['perm']->have_studip_perm('user', $this->range_id, $user_id)) { + $studip_view_urls['show'] = URLHelper::getURL('dispatch.php/course/details', ['cid' => $this->range_id, 'link_to_course' => '1']); + } + + return new \Studip\Calendar\EventData( + $begin, + $end, + $this->getTitle(), + [], + '#000000', + '#aaaaaa', + $this->isWritable($user_id), + CourseExDate::class, + $this->id, + Course::class, + $this->range_id, + 'course', + $this->range_id, + $studip_view_urls, + [], + 'seminar', + 'rgba(0,0,0,0)' + ); + } + + //End of Event interface implementation. } diff --git a/lib/models/CourseMarkedEvent.class.php b/lib/models/CourseMarkedEvent.class.php deleted file mode 100644 index 96f030043d51b7be751b2abda4e0f218493173be..0000000000000000000000000000000000000000 --- a/lib/models/CourseMarkedEvent.class.php +++ /dev/null @@ -1,132 +0,0 @@ -<?php -/** - * 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 Peter Thienel <thienel@data-quest.de> - * @copyright 2015 Stud.IP Core-Group - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * - * @property string $id alias for pk - * @property string $termin_id database column - * @property string $event_id alias column for termin_id - * @property string $range_id database column - * @property string $sem_id alias column for range_id - * @property string $autor_id database column - * @property string $author_id alias column for autor_id - * @property string $content database column - * @property int $date database column - * @property int $start alias column for date - * @property int $end_time database column - * @property int $end alias column for end_time - * @property int $mkdate database column - * @property int $chdate database column - * @property int $date_typ database column - * @property int $category_intern alias column for date_typ - * @property string|null $raum database column - * @property string|null $metadate_id database column - * @property SimpleORMapCollection|Folder[] $folders has_many Folder - * @property SimpleORMapCollection|RoomRequest[] $room_requests has_many RoomRequest - * @property SimpleORMapCollection|ResourceRequestAppointment[] $resource_request_appointments has_many ResourceRequestAppointment - * @property User $author belongs_to User - * @property Course $course belongs_to Course - * @property SeminarCycleDate|null $cycle belongs_to SeminarCycleDate - * @property ResourceBooking $room_booking has_one ResourceBooking - * @property SimpleORMapCollection|CourseTopic[] $topics has_and_belongs_to_many CourseTopic - * @property SimpleORMapCollection|Statusgruppen[] $statusgruppen has_and_belongs_to_many Statusgruppen - * @property SimpleORMapCollection|User[] $dozenten has_and_belongs_to_many User - * @property-read mixed $location additional field - * @property mixed $type additional field - * @property-read mixed $name additional field - * @property-read mixed $title additional field - * @property-read mixed $editor_id additional field - * @property-read mixed $uid additional field - * @property-read mixed $summary additional field - * @property-read mixed $description additional field - */ - -class CourseMarkedEvent extends CourseEvent -{ - - protected static function configure($config= []) - { - parent::configure($config); - } - - /** - * Returns all CourseMarkedEvents in the given time range for the given range_id. - * - * @param string $user_id Id of Stud.IP object from type user, course, inst - * @param DateTime $start The start date time. - * @param DateTime $end The end date time. - * @return SimpleORMapCollection Collection of found CourseMarkedEvents. - */ - public static function getEventsByInterval($user_id, DateTime $start, dateTime $end) - { - $stmt = DBManager::get()->prepare('SELECT DISTINCT termine.* FROM schedule_seminare ' - . 'INNER JOIN termine ON schedule_seminare.seminar_id = range_id ' - . 'LEFT JOIN seminar_user ON seminar_user.seminar_id = range_id AND seminar_user.user_id= :user_id ' - . 'WHERE schedule_seminare.user_id = :user_id AND schedule_seminare.visible = 1 ' - . 'AND seminar_user.seminar_id IS NULL AND date BETWEEN :start AND :end ' - . 'ORDER BY date ASC'); - $stmt->execute([ - ':user_id' => $user_id, - ':start' => $start->getTimestamp(), - ':end' => $end->getTimestamp() - ]); - $event_collection = []; - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $event = new CourseMarkedEvent(); - $event->setData($row); - $event->setNew(false); - $event_collection[] = $event; - } - $event_collection = SimpleORMapCollection::createFromArray($event_collection, false); - $event_collection->setClassName('Event'); - return $event_collection; - } - - public function getPermission($user_id = null) - { - return Event::PERMISSION_READABLE; - } - - /** - * Returns the title of this event. - * The title of a course event is the name of the course or if a topic is - * assigned, the title of this topic. If the user has not the permission - * Event::PERMISSION_READABLE, the title is "Keine Berechtigung.". - * - * @return string - */ - public function getTitle() - { - $title = $this->course->name; - $title .= ' ' . _('(vorgemerkt)'); - - return $title; - } - - /** - * Returns the index of the category. - * If the user has no permission, 255 is returned. - * - * TODO remove? use getStudipCategory instead? - * - * @see config/config.inc.php $TERMIN_TYP - * @return int The index of the category - */ - public function getCategory() - { - return 256; - } - - public function getDescription() - { - return ''; - } - -} diff --git a/lib/models/EventData.class.php b/lib/models/EventData.class.php deleted file mode 100644 index 453272d30fd7de6a29a2b46261d554145fb555cb..0000000000000000000000000000000000000000 --- a/lib/models/EventData.class.php +++ /dev/null @@ -1,144 +0,0 @@ -<?php -/** - * EventData.class.php - Model class for calendar events. - * - * 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 Peter Thienel <thienel@data-quest.de> - * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 - * @category Stud.IP - * @since 3.2 - * - * @property string $id alias column for event_id - * @property string $event_id database column - * @property string $author_id database column - * @property string|null $editor_id database column - * @property string $uid database column - * @property int $start database column - * @property int $end database column - * @property string $summary database column - * @property string|null $description database column - * @property string $class database column - * @property string|null $categories database column - * @property int $category_intern database column - * @property int $priority database column - * @property string|null $location database column - * @property int $ts database column - * @property int|null $linterval database column - * @property int|null $sinterval database column - * @property string|null $wdays database column - * @property int|null $month database column - * @property int|null $day database column - * @property string $rtype database column - * @property int $duration database column - * @property int|null $count database column - * @property int $expire database column - * @property string|null $exceptions database column - * @property int $mkdate database column - * @property int $chdate database column - * @property int $importdate database column - * @property SimpleORMapCollection|CalendarEvent[] $calendars has_many CalendarEvent - * @property User $author belongs_to User - * @property User|null $editor belongs_to User - */ - -class EventData extends SimpleORMap implements PrivacyObject -{ - protected static function configure($config = []) - { - $config['db_table'] = 'event_data'; - - $config['belongs_to']['author'] = [ - 'class_name' => User::class, - 'foreign_key' => 'author_id', - ]; - $config['belongs_to']['editor'] = [ - 'class_name' => User::class, - 'foreign_key' => 'editor_id', - ]; - $config['has_many']['calendars'] = [ - 'class_name' => CalendarEvent::class, - 'foreign_key' => 'event_id' - ]; - - $config['default_values']['linterval'] = 0; - $config['default_values']['sinterval'] = 0; - - $config['registered_callbacks']['before_create'][] = 'cbDefaultValues'; - - parent::configure($config); - - } - - public function delete() - { - // do not delete until one calendar is left - if (sizeof($this->calendars) > 1) { - return false; - } - $calendars = $this->calendars; - $ret = parent::delete(); - // only one calendar is left - if ($ret) { - $calendars->each(function($c) { $c->delete(); }); - } - return $ret; - } - - public static function garbageCollect() - { - DBManager::get()->query('DELETE event_data ' - . 'FROM calendar_event LEFT JOIN event_data USING(event_id)' - . 'WHERE range_id IS NULL'); - } - - public function getDefaultValue($field) - { - if ($field == 'start') { - return time(); - } - if ($field == 'end' && $this->content['start']) { - return $this->content['start'] + 3600; - } - if ($field == 'ts' && $this->content['start']) { - return mktime(12, 0, 0, date('n', $this->content['start']), - date('j', $this->content['start']), date('Y', $this->content['start'])); - } - return parent::getDefaultValue($field); - } - - protected function cbDefaultValues() - { - if (empty($this->content['uid'])) { - $this->content['uid'] = 'Stud.IP-' . $this->event_id . '@' . ($_SERVER['SERVER_NAME'] ?? parse_url($GLOBALS['ABSOLUTE_URI_STUDIP'], PHP_URL_HOST)); - } - } - - /** - * Export available data of a given user into a storage object - * (an instance of the StoredUserData class) for that user. - * - * @param StoredUserData $storage object to store data into - */ - public static function exportUserData(StoredUserData $storage) - { - $sorm = EventData::findThru($storage->user_id, [ - 'thru_table' => 'calendar_event', - 'thru_key' => 'range_id', - 'thru_assoc_key' => 'event_id', - 'assoc_foreign_key' => 'event_id', - ]); - if ($sorm) { - $field_data = []; - foreach ($sorm as $row) { - $field_data[] = $row->toRawArray(); - } - if ($field_data) { - $storage->addTabularData(_('Kalender Einträge'), 'event_data', $field_data); - } - } - } -} diff --git a/lib/models/User.class.php b/lib/models/User.class.php index 7f6a402f44f1a109afab29277ed33a90d6035d2f..5f1d6a0e3f08fdfce326e40f59acbe2ad9dcb444 100644 --- a/lib/models/User.class.php +++ b/lib/models/User.class.php @@ -80,7 +80,7 @@ * @property mixed $lock_rule additional field * @property mixed $oercampus_description additional field */ -class User extends AuthUserMd5 implements Range, PrivacyObject +class User extends AuthUserMd5 implements Range, PrivacyObject, Studip\Calendar\Owner { /** * @@ -1541,4 +1541,56 @@ class User extends AuthUserMd5 implements Range, PrivacyObject return $this->config->EXPIRATION_DATE > 0 && $this->config->EXPIRATION_DATE < time(); } + + /** + * @inheritDoc + */ + public static function getCalendarOwner(string $owner_id): ?\Studip\Calendar\Owner + { + return self::find($owner_id); + } + + /** + * @inheritDoc + */ + public function isCalendarReadable(?string $user_id = null): bool + { + if ($user_id === null) { + $user_id = self::findCurrent()->id; + } + + if ($this->id === $user_id) { + //The owner can always read their own calendar. + return true; + } + return Contact::countBySql( + "`owner_id` = :this_user_id AND `user_id` = :other_user_id + AND `calendar_permissions` <> ''", + ['this_user_id' => $this->id, 'other_user_id' => $user_id] + ) > 0; + } + + /** + * @inheritDoc + */ + public function isCalendarWritable(string $user_id = null): bool + { + if ($user_id === null) { + $user_id = self::findCurrent()->id; + } + + if ($this->id === $user_id) { + //The owner can always write their own calendar. + return true; + } + if (Config::get()->CALENDAR_GRANT_ALL_INSERT) { + //All users can write in all users calendars. + return true; + } + return Contact::countBySql( + "`owner_id` = :this_user_id AND `user_id` = :other_user_id + AND `calendar_permissions` = 'WRITE'", + ['this_user_id' => $this->id, 'other_user_id' => $user_id] + ) > 0; + } } diff --git a/lib/models/calendar/CalendarCourseDate.class.php b/lib/models/calendar/CalendarCourseDate.class.php new file mode 100644 index 0000000000000000000000000000000000000000..49179bd579d1d808e47298f45f56209b2f7fc467 --- /dev/null +++ b/lib/models/calendar/CalendarCourseDate.class.php @@ -0,0 +1,34 @@ +<?php + +/** + * CalendarCourseDate is a specialisation of CourseDate for + * course dates that are displayed in the personal calendar. + */ +class CalendarCourseDate extends CourseDate +{ + public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array + { + return parent::findBySQL( + "JOIN `seminar_user` + ON `seminar_user`.`seminar_id` = `termine`.`range_id` + WHERE `seminar_user`.`user_id` = :user_id + AND `seminar_user`.`bind_calendar` = '1' + AND `termine`.`date` BETWEEN :begin AND :end + AND ( + IFNULL(`termine`.`metadate_id`, '') = '' + OR `termine`.`metadate_id` NOT IN ( + SELECT `metadate_id` + FROM `schedule_seminare` + WHERE `user_id` = :user_id + AND `visible` = 0 + ) + ) + ORDER BY date", + [ + 'begin' => $begin->getTimestamp(), + 'end' => $end->getTimestamp(), + 'user_id' => $range_id + ] + ); + } +} diff --git a/lib/models/calendar/CalendarCourseExDate.class.php b/lib/models/calendar/CalendarCourseExDate.class.php new file mode 100644 index 0000000000000000000000000000000000000000..eb23aeb79d53964681157a1f9d63c9753791d774 --- /dev/null +++ b/lib/models/calendar/CalendarCourseExDate.class.php @@ -0,0 +1,35 @@ +<?php + +/** + * CalendarCourseExDate is a specialisation of CourseExDate for + * cancelled course dates that are displayed in the personal calendar. + */ +class CalendarCourseExDate extends CourseExDate +{ + public static function getEvents(DateTime $begin, DateTime $end, string $range_id): array + { + return parent::findBySQL( + "JOIN `seminar_user` + ON `seminar_user`.`seminar_id` = `ex_termine`.`range_id` + WHERE `seminar_user`.`user_id` = :user_id + AND `seminar_user`.`bind_calendar` = '1' + AND `ex_termine`.`date` BETWEEN :begin AND :end + AND `ex_termine`.`content` <> '' + AND ( + IFNULL(`ex_termine`.`metadate_id`, '') = '' + OR `ex_termine`.`metadate_id` NOT IN ( + SELECT `metadate_id` + FROM `schedule_seminare` + WHERE `user_id` = :user_id + AND `visible` = 0 + ) + ) + ORDER BY date", + [ + 'begin' => $begin->getTimestamp(), + 'end' => $end->getTimestamp(), + 'user_id' => $range_id + ] + ); + } +} diff --git a/lib/models/calendar/CalendarDate.class.php b/lib/models/calendar/CalendarDate.class.php new file mode 100644 index 0000000000000000000000000000000000000000..23ea8af3daa4137b8e1900a03c0bf43cdf989209 --- /dev/null +++ b/lib/models/calendar/CalendarDate.class.php @@ -0,0 +1,944 @@ +<?php +/** + * CalendarDate.class.php - Model class for calendar dates. + * + * CalendarDate represents a date in the personal 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 Peter Thienel <thienel@data-quest.de> + * @author Moritz Strohm <strohm@data-quest.de> + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @since 3.2 + * + * @property string id database column + * @property string author_id database column + * @property string editor_id database column + * @property string unique_id database column + * @property string begin database column + * @property string end database column + * @property string title database column + * @property string description database column + * @property string access database column + * @property string user_category database column + * @property string category database column + * @property string location database column + * @property string interval database column + * @property string offset database column + * @property string days database column + * @property string month database column + * @property string day_offset database column + * @property string repetition_type database column + * @property string number_of_dates database column + * @property string repetition_end database column + * @property string mkdate database column + * @property string chdate database column + * @property string import_date database column + */ +class CalendarDate extends SimpleORMap implements PrivacyObject +{ + /** + * NEVER_ENDING represents the value of the repetition_end field for + * a date that never ends. The value is the result of computing + * 2 ^ 31 - 1. + * + * NOTE: This constant must be changed long before 2038-01-19 03:14:07 UTC + * or else dates that should end at some specific point in time may end + * never. + */ + public const NEVER_ENDING = 2147483647; + + protected static function configure($config = []) + { + $config['db_table'] = 'calendar_dates'; + + $config['belongs_to']['author'] = [ + 'class_name' => User::class, + 'foreign_key' => 'author_id', + ]; + $config['belongs_to']['editor'] = [ + 'class_name' => User::class, + 'foreign_key' => 'editor_id', + ]; + $config['has_many']['calendars'] = [ + 'class_name' => CalendarDateAssignment::class, + 'assoc_foreign_key' => 'calendar_date_id', + 'on_store' => 'store', + 'on_delete' => 'delete' + ]; + $config['has_many']['exceptions'] = [ + 'class_name' => CalendarDateException::class, + 'assoc_foreign_key' => 'calendar_date_id', + 'on_store' => 'store', + 'on_delete' => 'delete' + ]; + + $config['default_values']['interval'] = 0; + $config['default_values']['offset'] = 0; + + $config['registered_callbacks']['before_store'][] = 'calculateExpiration'; + $config['registered_callbacks']['after_store'][] = 'cbSendDateModificationMail'; + $config['registered_callbacks']['before_store'][] = 'cbGenerateUniqueId'; + + parent::configure($config); + + } + + public function delete() + { + // do not delete until one calendar is left + if (count($this->calendars) > 1) { + return false; + } + $calendars = $this->calendars; + $ret = parent::delete(); + // only one calendar is left + if ($ret) { + $calendars->delete(); + } + return $ret; + } + + public static function garbageCollect() + { + DBManager::get()->query( + 'DELETE `calendar_dates` + FROM `calendar_date_assignments` + LEFT JOIN `calendar_dates` ON (`calendar_dates`.`id` = `calendar_date_assignments`.`calendar_date_id`) + WHERE `range_id` IS NULL' + ); + } + + /** + * @deprecated + */ + public function getDefaultValue($field) + { + if ($field == 'begin') { + return time(); + } + if ($field == 'end' && $this->content['begin']) { + return $this->content['begin'] + 3600; + } + return parent::getDefaultValue($field); + } + + public function cbSendDateModificationMail() + { + $template_factory = new Flexi_TemplateFactory($GLOBALS['STUDIP_BASE_PATH'] . '/locale/'); + + foreach ($this->calendars as $calendar) { + if ($calendar->range_id === $this->editor_id) { + //The editor shall not get a mail about the changes they just made. + continue; + } + if (!$calendar->user) { + //Wrong range or not a user. + continue; + } + setTempLanguage($calendar->range_id); + + $lang_path = getUserLanguagePath($calendar->range_id); + $template = $template_factory->open($lang_path . '/LC_MAILS/date_changed.php'); + $template->set_attribute('date', $this); + $template->set_attribute('receiver', $calendar->user); + $template->set_attribute('receiver_date_assignment', $calendar); + $mail_text = $template->render(); + Message::send( + '____%system%____', + [$calendar->user->username], + sprintf(_('Terminänderung durch %s'), $this->editor->getFullName()), + $mail_text + ); + + restoreLanguage(); + } + } + + /** + * Generates an unique id if it isn't present. + * @return void + */ + public function cbGenerateUniqueId() + { + if (!$this->unique_id) { + $this->unique_id = 'Stud.IP-' . $this->id . '@' . ($_SERVER['SERVER_NAME'] ?? ''); + } + } + + /** + * TODO + * + * @param string $range_id + * @return bool + */ + public function isVisible(string $range_id) + { + if (CalendarDateAssignment::exists([$range_id, $this->id])) { + //Users may see the dates in their calendar: + return true; + } + + $assignments = CalendarDateAssignment::findByCalendar_date_id($this->id); + foreach ($assignments as $assignment) { + if ($assignment->course instanceof Course) { + if ($assignment->course->isCalendarReadable($range_id)) { + return true; + } + } elseif ($assignment->user instanceof User) { + if ($assignment->user->isCalendarReadable($range_id)) { + return true; + } + } + } + + //In case the date is not in a calendar of the user or a course + //where the user has access to, it is only visible when it is public. + return $this->access === 'PUBLIC'; + } + + + public function isWritable(string $range_id) + { + if (CalendarDateAssignment::exists([$range_id, $this->id])) { + //The date is in the calendar of the user/course + //and therefore, the user or course administrator (tutor, dozent) + //may change the date. + return true; + } + + //Check contacts: Has the contact of the user that is represented by + //$range_id write permissions to all the calendars of all the users that + //are assigned to the date? + + $contacts_with_write_permissions = Contact::countBySql( + "JOIN `calendar_date_assignments` cda + ON `contact`.`user_id` = cda.`range_id` + WHERE `contact`.`owner_id` = :current_range_id + AND `contact`.`calendar_permissions` = 'WRITE' + AND cda.`calendar_date_id` = :calendar_date_id + AND cda.`range_id` <> :current_range_id", + [ + 'calendar_date_id' => $this->id, + 'current_range_id' => $range_id + ] + ); + $other_participant_count = CalendarDateAssignment::countBySql( + "`calendar_date_id` = :calendar_date_id + AND `range_id` <> :current_range_id", + [ + 'calendar_date_id' => $this->id, + 'current_range_id' => $range_id + ] + ); + + if ($contacts_with_write_permissions === $other_participant_count) { + //The user represented by $range_id has write permissions to all + //calendars of all the other users that are assigned to the date. + return true; + } + + //NOTE: CALENDAR_GRANT_ALL_INSERT MUST NOT be regarded here, because it only + //defines the behavior when inserting calendar dates and not when modifying them. + + //In case it is a course date, we must check if the user has write + //permissions from the course: + $course_assignments = CalendarDateAssignment::findBySql( + "JOIN `seminare` + ON `calendar_date_assignments`.`range_id` = `seminare`.`seminar_id` + WHERE `calendar_date_id` = :calendar_date_id", + ['calendar_date_id' => $this->id] + ); + foreach ($course_assignments as $course_assignment) { + if ($course_assignment->course->calendarWritable($range_id)) { + return true; + } + } + + return false; + } + + /** + * Determines whether the date spans over one whole day. This means that the date takes + * place on one day from 0:00:00 to 23:59:59. + * + * @return bool True, if the date spans over the whole day, false otherwise. + */ + public function isWholeDay() : bool + { + $begin = new DateTime(); + $begin->setTimestamp($this->begin); + $end = new DateTime(); + $end->setTimestamp($this->end); + + if ($begin->format('Ymd') !== $end->format('Ymd')) { + //Beginning and end are on different days. + return false; + } + //If the beginning is on midnight and the end is one second before midnight of the next day, + //the date spans over the whole day. + return $begin->format('His') === '000000' + && $end->format('His') === '235959'; + } + + + /** + * Calculates the value of the "expire" column in case the CalendarDate object + * has a repetition defined. + * + * @return void + */ + public function calculateExpiration() + { + if (!in_array($this->repetition_type, ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'])) { + //No repetition. Nothing to do. + return; + } + if ($this->number_of_dates > 1) { + //There is a certain amount of repetitions, so that the expiration date + //has to be calculated by that. + $expiration = new DateTime(); + $expiration->setTimestamp($this->begin); + $interval_str = ''; + if ($this->repetition_type === 'DAILY') { + $interval_str = sprintf('P%dD', ((int) $this->number_of_dates - 1) * $this->interval); + } elseif ($this->repetition_type === 'WEEKLY') { + $days_length = mb_strlen($this->days); + if ($days_length > 0) { + $wday = $expiration->format('N'); + // set next weekday as first repetition + $expiration->modify($this->getWeekdayName()); + + $rep_offset = ($this->number_of_dates - 1) % $days_length; + + $rep_count = $this->number_of_dates - 1; + + $days_offset = floor($rep_count / $days_length) * 7 * + $this->interval + $rep_offset - 1; + $interval_str = sprintf('P%dD', $days_offset); + } else { + $interval_str = sprintf('P%dW', ($this->number_of_dates - 1) * $this->interval); + } + } elseif ($this->repetition_type === 'MONTHLY') { + $interval_str = sprintf('P%dM', ($this->number_of_dates - 1) * $this->interval); + } elseif ($this->repetition_type === 'YEARLY') { + $interval_str = sprintf('P%dY', ($this->number_of_dates - 1) * $this->interval); + } + try { + $interval = new DateInterval($interval_str); + $expiration->add($interval); + $expiration->setTime(23, 59, 59); + $this->repetition_end = $expiration->getTimestamp(); + } catch (Exception $e) { + //Nothing to do. + } + } elseif (!$this->repetition_end) { + //No expiration date is specified. + //This would mean that the event "never" expires. + $this->repetition_end = self::NEVER_ENDING; + } + } + + + /** + * + * Returns the DateInterval for the repetition of this calendar date. + * + * @return DateInterval|null The DateInterval for this calendar date or null + * in case the date has no repetition. + * @throws Exception In case a DateInterval cannot be constructed. + */ + public function getRepetitionInterval() : ?DateInterval + { + if ($this->repetition_type === 'DAILY') { + return new DateInterval(sprintf('P%uD', $this->interval)); + } elseif ($this->repetition_type === 'WORKDAYS') { + return new DateInterval('P1W'); + } elseif ($this->repetition_type === 'WEEKLY') { + return new DateInterval(sprintf('P%uW', $this->interval)); + } elseif ($this->repetition_type === 'MONTHLY') { + return new DateInterval(sprintf('P%uM', $this->interval)); + } elseif ($this->repetition_type === 'YEARLY') { + return new DateInterval(sprintf('P%uY', $this->interval)); + } + //No repetition: no interval. + return null; + } + + + public function getRepetitionOffset() : ?DateInterval + { + if (!$this->offset) { + return null; + } + + if ($this->repetition_type === 'MONTHLY') { + if ($this->days_offset) { + return new DateInterval(sprintf('P%1$uM%2$uD', $this->offset, $this->days_offset)); + } else { + return new DateInterval(sprintf('P%uM', $this->offset)); + } + } elseif ($this->repetition_type === 'YEARLY') { + return new DateInterval(sprintf('P%uM', $this->offset)); + } + return null; + } + + + /** + * Export available data of a given user into a storage object + * (an instance of the StoredUserData class) for that user. + * + * @param StoredUserData $storage object to store data into + */ + public static function exportUserData(StoredUserData $storage) + { + $sorm = self::findThru($storage->user_id, [ + 'thru_table' => 'calendar_date_assignments', + 'thru_key' => 'range_id', + 'thru_assoc_key' => 'event_id', + 'assoc_foreign_key' => 'event_id', + ]); + if ($sorm) { + $field_data = []; + foreach ($sorm as $row) { + $field_data[] = $row->toRawArray(); + } + if ($field_data) { + $storage->addTabularData(_('Kalendereinträge'), 'calendar_dates', $field_data); + } + } + } + + + /** + * This is a helper method to set all the fields for date repetition to an empty string. + * + * @return void + */ + public function clearRepetitionFields() + { + $this->repetition_type = ''; + $this->interval = ''; + $this->offset = ''; + $this->days = ''; + $this->month = ''; + $this->number_of_dates = '1'; + $this->repetition_end = ''; + } + + public function getAccessAsString() : string + { + if ($this->access === 'PUBLIC') { + return _('Öffentlich'); + } elseif ($this->access === 'PRIVATE') { + return _('Privat'); + } elseif ($this->access === 'CONFIDENTIAL') { + return _('Vertraulich'); + } else { + return _('Keine Angabe'); + } + } + + public function getRepetitionAsString() : string + { + require_once 'lib/dates.inc.php'; + + $repetition_string = ''; + + if ($this->repetition_type === 'SINGLE') { + $repetition_string = _('Keine Wiederholung'); + } elseif ($this->repetition_type === 'DAILY') { + if ($this->interval > 0) { + if ($this->interval == '1') { + //Each day + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Täglich (%u Termine)'), + $this->number_of_dates + ); + } elseif ($this->repetition_end < CalendarDate::NEVER_ENDING) { + $repetition_string = sprintf( + _('Täglich bis zum %1$s'), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = _('Täglich ohne Begrenzung'); + } + } else { + //Every %u day + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jeden %1$u. Tag (%2$u Termine)'), + $this->interval, + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jeden %1$u. Tag bis zum %2$s'), + $this->interval, + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jeden %u. Tag ohne Begrenzung'), + $this->interval + ); + } + } + } + } elseif ($this->repetition_type === 'WEEKLY') { + $weekday_string = ''; + if (strlen($this->days) > 1) { + //Multiple days + $days = []; + foreach (str_split($this->days) as $day_number) { + if ($day_number == '7') { + $day_number = '0'; + } + $days[] = getWeekday($day_number, false); + } + $all_but_last_day = array_slice($days, 0, -1); + $weekday_string = sprintf( + _('%1$s und %2$s'), + implode(', ', $all_but_last_day), + end($days) + ); + } else { + //One day + $weekday_string = getWeekday($this->days[0], false); + } + if ($this->interval == '1') { + //Each week + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + ngettext('Einmal am folgenden %s', 'Jeden %1$s (%2$u Termine)', $this->number_of_dates - 1), + $weekday_string, + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jeden %1$s bis zum %2$s'), + $weekday_string, + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jeden %s ohne Begrenzung'), + $weekday_string + ); + } + } else { + //Every %u week + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jeden %1$u. %2$s (%3$u Termine)'), + $this->interval, + $weekday_string, + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jeden %1$u. %2$s bis zum %3$s'), + $this->interval, + $weekday_string, + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jeden %1$u. %2$s ohne Begrenzung'), + $this->interval, + $weekday_string + ); + } + } + } elseif ($this->repetition_type === 'MONTHLY') { + if ($this->interval == '1') { + //Each month + if ($this->days) { + if ($this->offset < 0) { + //Repetition on one specific day of week in the last week. + $repetition_string = sprintf( + _('Jeden Monat am letzten %s'), + getWeekday($this->days, false) + ); + } else { + //Repetition on one specific day of week in a specific week. + $repetition_string = sprintf( + _('Jeden Monat am %1$u. %2$s'), + $this->offset, + getWeekday($this->days, false) + ); + } + } else { + //Repetition on one specific day of month. + $repetition_string = sprintf( + _('Jeden Monat am %u. Tag'), + $this->offset + ); + } + } else { + //Every %u month + if ($this->days) { + if ($this->offset < 0) { + //Repetition on one specific day of week on the last week. + $repetition_string = sprintf( + _('Jeden %1$u. Monat am letzten %2$s'), + $this->interval, + getWeekday($this->days, false) + ); + } else { + //Repetition on one specific day of week in a specific week. + $repetition_string = sprintf( + _('Jeden %1$u. Monat am %2$u. %3$s'), + $this->interval, + $this->offset, + getWeekday($this->days, false) + ); + } + } else { + //Repetition on one specific day of month. + $repetition_string = sprintf( + _('Jeden %1$u. Monat am %2$u.'), + $this->interval, + $this->offset + ); + } + } + } elseif ($this->repetition_type === 'YEARLY') { + if ($this->interval == '1') { + //Each year + if ($this->days) { + //Repetition on one specific day of week in a specific week + //in a specific month. + if ($this->offset < 0) { + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am letzten %2$s (%3$u Termine)'), + getMonthName($this->month, false), + getWeekday($this->days, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am letzten %2$s bis zum %3$s'), + getMonthName($this->month, false), + getWeekday($this->days, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am letzten %2$s ohne Begrenzung'), + getMonthName($this->month, false), + getWeekday($this->days, false) + ); + } + } else { + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am %2$u. %3$s (%4$u Termine'), + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am %2$u. %3$s bis zum %4$s'), + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes Jahr im %1$s am %2$u. %3$s ohne Begrenzung'), + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false) + ); + } + } + } else { + //Repetition on one specific day of month. + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes Jahr am %1$u. %2$s (%3$u Termine)'), + $this->offset, + getMonthName($this->month, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes Jahr am %1$u. %2$s bis zum %3$s'), + $this->offset, + getMonthName($this->month, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes Jahr am %1$u. %2$s ohne Begrenzung'), + $this->offset, + getMonthName($this->month, false) + ); + } + } + } else { + //Every %u years + if ($this->days) { + //Repetition on one specific day of week in a specific week + //in a specific month. + if ($this->offset < 0) { + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am letzten %3$s (%4$u Termine)'), + $this->interval, + getMonthName($this->month, false), + getWeekday($this->days, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am letzten %3$s bis zum %4$s'), + $this->interval, + getMonthName($this->month, false), + getWeekday($this->days, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am letzten %3$s ohne Begrenzung'), + $this->interval, + getMonthName($this->month, false), + getWeekday($this->days, false) + ); + } + } else { + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s (%5$u Termine)'), + $this->interval, + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s bis zum %5$s'), + $this->interval, + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr im %2$s am %3$u. %4$s ohne Begrenzung'), + $this->interval, + getMonthName($this->month, false), + $this->offset, + getWeekday($this->days, false) + ); + } + } + } else { + //Repetition on one specific day of month. + if ($this->number_of_dates > 1) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr am %2$u. %3$s (%4$u Termine)'), + $this->interval, + $this->offset, + getMonthName($this->month, false), + $this->number_of_dates + ); + } elseif ($this->repetition_end < self::NEVER_ENDING) { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr am %2$u. %3$s bis zum %4$s'), + $this->interval, + $this->offset, + getMonthName($this->month, false), + date('d.m.Y', $this->repetition_end) + ); + } else { + $repetition_string = sprintf( + _('Jedes %1$u. Jahr am %2$u. %3$s ohne Begrenzung'), + $this->interval, + $this->offset, + getMonthName($this->month, false) + ); + } + } + } + } + + return $repetition_string; + } + + + /** + * Creates the HTML for creating a repetition input Vue component instance + * and fills it with the values from the model. + * + * @param string $element_name The name of the element. + * + * @return string The HTML code for creating the repetition input vue instance. + */ + public function getRepetitionInputHtml(string $element_name = 'repetition') : string + { + $repetition_end_type = ''; + $repetition_end_date = ''; + $repetition_dow = '[]'; + $repetition_dow_week = ''; + + if ($this->isNew()) { + $repetition_end_date = htmlReady(date('d.m.Y', $this->end)); + $repetition_dow = sprintf('["%s"]', date('N', $this->begin)); + $repetition_dow_week = '1'; + } else { + + if ($this->repetition_end) { + $repetition_end_date = htmlReady(date('d.m.Y', $this->repetition_end)); + } else { + //Provide a good default value in case the user wants to enable or change the repetition: + $repetition_end_date = htmlReady(date('d.m.Y', $this->end)); + } + if ($this->days) { + $repetition_dow = json_encode(str_split($this->days)); + $repetition_dow_week = $this->offset; + } else { + //The days field is not in use. Use the day of the beginning as a good default. + $repetition_dow = sprintf('["%s"]', date('N', $this->begin)); + //Also set repetition_dow_week to 1 as a good default in case the user + //switches to the monthly repetition type where a specific day of week + //is selected instead of a specific day of month: + $repetition_dow_week = '1'; + } + + if ($this->number_of_dates > 1) { + $repetition_end_type = 'end_count'; + } elseif ($this->repetition_end && intval($this->repetition_end) !== self::NEVER_ENDING) { + //The end date is at some certain date and not on the virtual "never" date. + $repetition_end_type = 'end_date'; + } + } + + $attributes = [ + 'name' => $element_name, + 'default_date' => $this->begin, + 'repetition_type' => $this->isNew() ? '' : $this->repetition_type, + 'repetition_interval' => $this->isNew() ? '1' : $this->interval, + ':repetition_dow' => $repetition_dow, + ':repetition_dow_week' => $repetition_dow_week, + ':repetition_month' => $this->isNew() ? date('m', $this->begin) : $this->month, + ':repetition_month_type' => $this->isNew() ? "'dom'" : ($this->days ? "'dow'" : "'dom'"), + ':repetition_dom' => $this->isNew() ? date('d', $this->begin) : $this->offset, + ':repetition_end_type' => sprintf("'%s'", $repetition_end_type), + ':number_of_dates' => $this->isNew() ? '1' : $this->number_of_dates, + ':repetition_end_date' => sprintf("'%s'", $repetition_end_date) + ]; + return sprintf('<repetition-input %s></repetition-input>', arrayToHtmlAttributes($attributes)); + } + + public function getCategoryAsString() : string + { + if ($this->user_category) { + return $this->user_category; + } + return $GLOBALS['PERS_TERMIN_KAT'][$this->category]['name'] ?? ''; + } + + /** + * Returns the textual ordinal for the offset of a weekday from property offset + * or an empty string if offset is not set. + * + * @return string The textual ordinal. + */ + public function getOrdinalName(): string + { + if (mb_strlen($this->offset)) { + $ordinal_array = [ + '1' => 'first', + '2' => 'second', + '3' => 'third', + '4' => 'fourth', + '5' => 'fifth', + '-1' => 'last' + ]; + return $ordinal_array[$this->offset]; + } + return ''; + } + + /** + * Returns the short name of first weekday from property days or an + * empty string if days is not set. + * + * @param $offset int Offset of days. + * @return string Short name of weekday. + */ + public function getWeekdayName(int $offset = 0): string + { + if (mb_strlen($this->days)) { + $wdays = [ + '1' => 'mon', + '2' => 'tue', + '3' => 'wed', + '4' => 'thu', + '5' => 'fri', + '6' => 'sat', + '7' => 'sun' + ]; + return $wdays[substr($this->days, $offset, 1)]; + } + return ''; + } + + /** + * Returns a string representation of the access field. + * + * @return string A localised string of the access field. + */ + public function getVisibilityAsString() : string + { + if ($this->access === 'PUBLIC') { + return _('Öffentlich'); + } elseif ($this->access === 'CONFIDENTIAL') { + return _('Vertraulich'); + } else { + return _('Privat'); + } + } + + /** + * Returns the names of the participants of the date. This also includes courses + * to which the date is assigned. + * + * @param string $user_id The user for which to generate the participant array. + * The user with that ID is excluded from that list. + * @return array A list with the names of the participants of the date. + */ + public function getParticipantsAsStringArray(string $user_id = '') : array + { + $participant_strings = []; + foreach ($this->calendars as $calendar) { + if ($calendar->range_id === $user_id) { + //Exclude the user for which to generate the list. + continue; + } + if ($calendar->course instanceof Course) { + $participant_strings[] = $calendar->course->getFullName(); + } elseif ($calendar->user instanceof User) { + $participant_strings[] = $calendar->user->getFullName(); + } + } + + asort($participant_strings); + + return $participant_strings; + } +} diff --git a/lib/models/calendar/CalendarDateAssignment.class.php b/lib/models/calendar/CalendarDateAssignment.class.php new file mode 100644 index 0000000000000000000000000000000000000000..ba0f38720efa2583c76167b72824d1a0b694a547 --- /dev/null +++ b/lib/models/calendar/CalendarDateAssignment.class.php @@ -0,0 +1,714 @@ +<?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; + } + 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_deleted.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 gelöscht'), $this->calendar_date->editor->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 + { + $begin->setTime(0, 0); + $end->setTime(23, 59, 59); + + $sql = "JOIN `calendar_dates` + ON calendar_date_id = `calendar_dates`.`id` + WHERE + `calendar_date_assignments`.`range_id` = :range_id "; + if (!$with_declined) { + $sql .= "AND `calendar_date_assignments`.`participation` <> 'DECLINED' "; + } + $sql .= "AND ( + `calendar_dates`.`begin` BETWEEN :begin AND :end + OR + (`calendar_dates`.`begin` <= :end AND `calendar_dates`.`repetition_type` <> '' + AND `calendar_dates`.`repetition_end` > :begin) + OR + :begin BETWEEN `calendar_dates`.`begin` AND `calendar_dates`.`end` + ) + AND + `access` IN ( :access_levels ) + ORDER BY `calendar_dates`.`begin` ASC"; + + $events = self::findBySql($sql, [ + '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(); + + // duration in full days + $duration = $event->getDurationDays(); + + $cal_start = DateTimeImmutable::createFromMutable($m_start); + $cal_end = DateTimeImmutable::createFromMutable($m_start)->setTime(23,59,59); + $cal_noon = $cal_start->setTime(12, 0); + // 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'))) { + $events_created = array_merge($events_created, self::createRecurrentDate($event, $cal_noon)); + } + } 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(sprintf('today %s', $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; + } + $duration = $this->getDuration(); + return $duration->h === 23 && $duration->i === 59 && $duration->s === 59; + } + + 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(); + $duration = $this->getDuration(); + + $all_day = $begin->format('H:i:s') === '00:00:00' + && $duration->h === 23 + && $duration->i === 59 + && $duration->s === 59; + + + $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 ''; + } +} diff --git a/lib/models/calendar/CalendarDateException.class.php b/lib/models/calendar/CalendarDateException.class.php new file mode 100644 index 0000000000000000000000000000000000000000..b53a728d819b53ff62365fdd16155dfd3f594a32 --- /dev/null +++ b/lib/models/calendar/CalendarDateException.class.php @@ -0,0 +1,40 @@ +<?php +/** + * The CalendarDateException class represents one exception for a calendar date. + * + * This file is part of Stud.IP + * + * 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 2023 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @package resources + * @since 5.5 + * + * @property string $id The ID of the exception. + * @property string $calendar_date_id The ID of the calendar date where the exception belongs to. + * @property string $date The date of the exception in the date format YYYY-MM-DD. + * @property string $mkdate The creation date of the exception. + * @property string $chdate The modification date of the exception. + * @property CalendarDate|null $calendar_date The associated calendar date object. + */ +class CalendarDateException extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'calendar_date_exceptions'; + + $config['belongs_to']['calendar_date'] = [ + 'class_name' => CalendarDate::class, + 'foreign_key' => 'calendar_date_id', + 'assoc_func' => 'find' + ]; + + parent::configure($config); + } +} diff --git a/lib/modules/CoreCalendar.class.php b/lib/modules/CoreCalendar.class.php index c0df36736d70a53057fd99c0e33233784921bb4b..318a2ba7da89a9090d7a1a9d1262b9991e90f0bf 100644 --- a/lib/modules/CoreCalendar.class.php +++ b/lib/modules/CoreCalendar.class.php @@ -20,7 +20,7 @@ class CoreCalendar extends CorePlugin implements StudipModule return null; } - $navigation = new Navigation(_('Kalender'), "seminar_main.php?auswahl={$course_id}&redirect_to=dispatch.php/calendar/single/"); + $navigation = new Navigation(_('Kalender'), URLHelper::getURL('dispatch.php/calendar/calendar/course/' . $course_id)); $navigation->setImage(Icon::create('schedule', Icon::ROLE_CLICKABLE)); return $navigation; } @@ -34,7 +34,7 @@ class CoreCalendar extends CorePlugin implements StudipModule return null; } - $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/single/'); + $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar/course/' . $course_id); $navigation->setImage(Icon::create('schedule', Icon::ROLE_INFO_ALT)); $navigation->setActiveImage(Icon::create('schedule', Icon::ROLE_INFO)); return ['calendar' => $navigation]; @@ -49,10 +49,10 @@ class CoreCalendar extends CorePlugin implements StudipModule 'summary' => _('Kalender'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('schedule', Icon::ROLE_INFO), - 'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE), - 'displayname' => _('Planer'), + 'displayname' => _('Kalender'), ]; } + public function isActivatableForContext(Range $context) { return Config::get()->CALENDAR_GROUP_ENABLE && $context->getRangeType() === 'course'; @@ -60,7 +60,6 @@ class CoreCalendar extends CorePlugin implements StudipModule public function getInfoTemplate($course_id) { - // TODO: Implement getInfoTemplate() method. return null; } } diff --git a/lib/modules/CoreOverview.class.php b/lib/modules/CoreOverview.class.php index af1b9604774109bdb10c2a610fb91bba0a819d3e..94005da3edd6fb4d09d8e92a81f1533e3d31c621 100644 --- a/lib/modules/CoreOverview.class.php +++ b/lib/modules/CoreOverview.class.php @@ -90,7 +90,7 @@ class CoreOverview extends CorePlugin implements StudipModule if ($object_type !== 'sem') { $navigation->addSubNavigation('info', new Navigation(_('Kurzinfo'), 'dispatch.php/institute/overview')); $navigation->addSubNavigation('courses', new Navigation(_('Veranstaltungen'), 'show_bereich.php?level=s&id='.$course_id)); - $navigation->addSubNavigation('schedule', new Navigation(_('Veranstaltungs-Stundenplan'), 'dispatch.php/calendar/instschedule?cid='.$course_id)); + $navigation->addSubNavigation('schedule', new Navigation(_('Veranstaltungs-Stundenplan'), 'dispatch.php/institute/schedule/index/' . $course_id)); if ($GLOBALS['perm']->have_studip_perm('admin', $course_id)) { $navigation->addSubNavigation('admin', new Navigation(_('Administration der Einrichtung'), 'dispatch.php/institute/basicdata/index?new_inst=TRUE')); diff --git a/lib/modules/ScheduleWidget.php b/lib/modules/ScheduleWidget.php index 339beb3f87fa45ec83ef5a86406a0845b0876891..0393176827de3d4a79bad079f6f58c13bfd8199f 100644 --- a/lib/modules/ScheduleWidget.php +++ b/lib/modules/ScheduleWidget.php @@ -39,15 +39,45 @@ class ScheduleWidget extends CorePlugin implements PortalPlugin */ public function getPortalTemplate() { - $view = CalendarScheduleModel::getUserCalendarView( - $GLOBALS['user']->id, - false, - false, - $days = array(0,1,2,3,4) + $week_slot_duration = \Studip\Calendar\Helper::getCalendarSlotDuration('week'); + $calendar_settings = $GLOBALS['user']->cfg->CALENDAR_SETTINGS ?? []; + + $semester = Semester::findCurrent(); + $fullcalendar = \Studip\Fullcalendar::create( + '', + [ + 'minTime' => '08:00', + 'maxTime' => '20:00', + 'allDaySlot' => false, + 'header' => [ + 'left' => '', + 'right' => '' + ], + 'views' => [ + 'timeGridWeek' => [ + 'columnHeaderFormat' => ['weekday' => 'long'], + 'weekends' => $calendar_settings['type_week'] === 'LONG', + 'slotDuration' => $week_slot_duration + ] + ], + 'defaultView' => 'timeGridWeek', + 'defaultDate' => date('Y-m-d'), + 'timeGridEventMinHeight' => 20, + 'eventSources' => [ + [ + 'url' => URLHelper::getURL('dispatch.php/calendar/calendar/schedule_data'), + 'method' => 'GET', + 'extraParams' => [ + 'semester_id' => $semester->id, + 'full_semester_time_range' => false + ] + ] + ] + ] ); $template = $GLOBALS['template_factory']->open('shared/string'); - $template->content = CalendarWidgetView::createFromWeekView($view)->render(); + $template->content = $fullcalendar; return $template; } diff --git a/lib/modules/TerminWidget.php b/lib/modules/TerminWidget.php index c9cf85400ef29b36492743d5258b0f15356bcbde..773e6923948d73a028c2a0483d8b3a0877c7c89d 100644 --- a/lib/modules/TerminWidget.php +++ b/lib/modules/TerminWidget.php @@ -20,7 +20,7 @@ class TerminWidget extends CorePlugin implements PortalPlugin public function getMetadata() { return [ - 'description' => _('Mit diesem Widget haben Sie ihre aktuellen Termine im Überlick.') + 'description' => _('Dieses Widget zeigt die eigenen aktuellen Termine an.') ]; } @@ -31,8 +31,9 @@ class TerminWidget extends CorePlugin implements PortalPlugin $template = $GLOBALS['template_factory']->open('shared/string'); $template->content = $response->body; - $navigation = new Navigation('', 'dispatch.php/calendar/single/week', ['self' => true]); - $navigation->setImage(Icon::create('add', 'clickable', ["title" => _('Neuen Termin anlegen')])); + $navigation = new Navigation('', 'dispatch.php/calendar/date/add'); + $navigation->setImage(Icon::create('add', Icon::ROLE_CLICKABLE, ['title' => _('Neuen Termin anlegen')])); + $navigation->setLinkAttributes(['data-dialog' => 'reload-on-close']); $template->icons = [$navigation]; return $template; diff --git a/lib/navigation/CalendarNavigation.php b/lib/navigation/CalendarNavigation.php index 972f3f840e999a43cb65232556ccf82fe6697728..f201d86c78292201e6e4024aa5909ed7b417ff8f 100644 --- a/lib/navigation/CalendarNavigation.php +++ b/lib/navigation/CalendarNavigation.php @@ -20,25 +20,19 @@ class CalendarNavigation extends Navigation */ public function __construct() { - global $perm; - - parent::__construct(_('Planer')); - - if ( - isset($perm) - && !$perm->have_perm('admin') - && Config::get()->SCHEDULE_ENABLE - ) { - $planerinfo = _('Stundenplan'); - } else { - $planerinfo = _('Termine'); + $title = _('Kalender'); + $main_url = URLHelper::getURL('dispatch.php/calendar/calendar'); + if (!$GLOBALS['perm']->have_perm('admin') && Config::get()->SCHEDULE_ENABLE) { + $title = _('Stundenplan'); + $main_url = URLHelper::getURL('dispatch.php/calendar/schedule'); } + parent::__construct($title, $main_url); - $this->setImage(Icon::create('schedule', 'navigation', ["title" => $planerinfo])); + $this->setImage(Icon::create('schedule', 'navigation', ['title' => $title])); } /** - * Initialize the subnavigation of this item. This method + * Initialize the sub-navigation of this item. This method * is called once before the first item is added or removed. */ public function initSubNavigation() @@ -47,16 +41,13 @@ class CalendarNavigation extends Navigation parent::initSubNavigation(); - // schedule if (!$perm->have_perm('admin') && Config::get()->SCHEDULE_ENABLE) { $navigation = new Navigation(_('Stundenplan'), 'dispatch.php/calendar/schedule'); $this->addSubNavigation('schedule', $navigation); } - // calendar - $atime = $atime ? intval($atime) : Request::int($atime); if (Config::get()->CALENDAR_ENABLE) { - $navigation = new Navigation(_('Terminkalender'), 'dispatch.php/calendar/single', ['self' => 1]); + $navigation = new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar'); $this->addSubNavigation('calendar', $navigation); } } diff --git a/lib/navigation/ProfileNavigation.php b/lib/navigation/ProfileNavigation.php index ffff06cbeb40678dab0e7c1cb88c67f25f4e2444..50dcfec2d9be15ea866398994d3ed8913e4a8ece 100644 --- a/lib/navigation/ProfileNavigation.php +++ b/lib/navigation/ProfileNavigation.php @@ -103,7 +103,7 @@ class ProfileNavigation extends Navigation $navigation->addSubNavigation('messaging', new Navigation(_('Nachrichten'), 'dispatch.php/settings/messaging')); if (Config::get()->CALENDAR_ENABLE) { - $navigation->addSubNavigation('calendar_new', new Navigation(_('Terminkalender'), 'dispatch.php/settings/calendar')); + $navigation->addSubNavigation('calendar_new', new Navigation(_('Kalender'), 'dispatch.php/settings/calendar')); } if (!$perm->have_perm('admin') && Config::get()->MAIL_NOTIFICATION_ENABLE) { diff --git a/lib/navigation/StartNavigation.php b/lib/navigation/StartNavigation.php index bc938b693d37b70a46ddd6c7cc07deb5f1d82b38..913f3faad48e996a8834611a3e95c4837901926b 100644 --- a/lib/navigation/StartNavigation.php +++ b/lib/navigation/StartNavigation.php @@ -281,10 +281,10 @@ class StartNavigation extends Navigation $this->addSubNavigation('profile', $navigation); - $navigation = new Navigation(_('Mein Planer')); + $navigation = new Navigation(_('Kalender')); if (Config::get()->CALENDAR_ENABLE) { - $navigation->addSubNavigation('calendar', new Navigation(_('Terminkalender'), 'dispatch.php/calendar/single')); + $navigation->addSubNavigation('calendar', new Navigation(_('Kalender'), 'dispatch.php/calendar/calendar')); } if (Config::get()->SCHEDULE_ENABLE) { diff --git a/lib/seminar_open.php b/lib/seminar_open.php index 0ca99918017a24374a750c9ac62afb968618dad7..45c4df375b0354d79eddd086055f46411d24dba0 100644 --- a/lib/seminar_open.php +++ b/lib/seminar_open.php @@ -44,7 +44,7 @@ function startpage_redirect($page_code) { $jump_page = "dispatch.php/contact"; break; case 5: - $jump_page = "dispatch.php/calendar/single"; + $jump_page = "dispatch.php/calendar"; break; case 6: // redirect to global blubberstream diff --git a/locale/de/LC_MAILS/_date_information.php b/locale/de/LC_MAILS/_date_information.php new file mode 100644 index 0000000000000000000000000000000000000000..5eab14d55df782a0d21bd0dc9b2f2d3fc170bfe2 --- /dev/null +++ b/locale/de/LC_MAILS/_date_information.php @@ -0,0 +1,28 @@ +*Zeiten:* <?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?> + +*Titel:* <?= $date->title ?> + +<?= $date->description ?? '' ?> + +-- + +<? if ($date->category) : ?> +*Kategorie:* <?= $date->getCategoryAsString() ?> +<? endif ?> + +*Zugriff:* <?= $date->getAccessAsString() ?> + +<? if ($date->repetition_type) : ?> +*Wiederholung:* <?= $date->getRepetitionAsString() ?> +<? endif ?> + +<? if (Config::get()->CALENDAR_GROUP_ENABLE && count($date->calendars) > 1) : ?> +*Teilnehmende:* +<? foreach($date->getParticipantsAsStringArray($receiver->user_id) as $participant_string) : ?> +- <?= $participant_string ?> +<? endforeach ?> +<? endif ?> + +<? if ($receiver_date_assignment) : ?> +**Ihre Teilnahme:** <?= $receiver_date_assignment->getParticipationAsString() ?> +<? endif ?> diff --git a/locale/de/LC_MAILS/date_changed.php b/locale/de/LC_MAILS/date_changed.php new file mode 100644 index 0000000000000000000000000000000000000000..aa92b7fd0c32999610952f82b980cc8b441fa414 --- /dev/null +++ b/locale/de/LC_MAILS/date_changed.php @@ -0,0 +1,10 @@ +<?= $date->editor->getFullName() ?> hat einen Termin im Kalender geändert. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> + +-- + +Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?> diff --git a/locale/de/LC_MAILS/date_created.php b/locale/de/LC_MAILS/date_created.php new file mode 100644 index 0000000000000000000000000000000000000000..d323b944c799d7b1534ff98e9a7d681b32627366 --- /dev/null +++ b/locale/de/LC_MAILS/date_created.php @@ -0,0 +1,10 @@ +<?= $date->editor->getFullName() ?> hat einen Termin im Kalender eingetragen. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> + +-- + +Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?> diff --git a/locale/de/LC_MAILS/date_deleted.php b/locale/de/LC_MAILS/date_deleted.php new file mode 100644 index 0000000000000000000000000000000000000000..0ff6ef80a9d7bb33f02b0f5b1aba4c9f00de5f94 --- /dev/null +++ b/locale/de/LC_MAILS/date_deleted.php @@ -0,0 +1,6 @@ +<?= $date->editor->getFullName() ?> hat einen Termin im Kalender gelöscht. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> diff --git a/locale/de/LC_MAILS/date_participation.php b/locale/de/LC_MAILS/date_participation.php new file mode 100644 index 0000000000000000000000000000000000000000..11b0fb29a20b4f97aa592e609adefa11b19f9df3 --- /dev/null +++ b/locale/de/LC_MAILS/date_participation.php @@ -0,0 +1,14 @@ +<? if ($date_assignment->participation === 'ACCEPTED') : ?> +<?= $date_assignment->user->getFullName() ?> hat Ihren Termin angenommen. +<? elseif ($date_assignment->participation === 'DECLINED') : ?> +<?= $date_assignment->user->getFullName() ?> hat Ihren Termin abgelehnt. +<? endif ?> + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date_assignment->calendar_date, + 'receiver' => $date_assignment->calendar_date->author, +]) ?> + +-- + +Direkt zum Termin: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date_assignment->calendar_date->id) ?> diff --git a/locale/en/LC_MAILS/_date_information.php b/locale/en/LC_MAILS/_date_information.php new file mode 100644 index 0000000000000000000000000000000000000000..a0cb470287ca79ecbf607131c29e76773d9a7556 --- /dev/null +++ b/locale/en/LC_MAILS/_date_information.php @@ -0,0 +1,24 @@ +*Time:* <?= date('d.m.Y H:i', $date->begin) ?> - <?= date('d.m.Y H:i', $date->end) ?> + +*Title:* <?= $date->title ?> + +<?= $date->description ?? '' ?> + +-- + +<? if ($date->category) : ?> +*Category:* <?= $date->category ?> +<? endif ?> + +*Access:* <?= $date->getAccessAsString() ?> + +<? if ($date->repetition_type) : ?> +*Repetition:* <?= $date->getRepetitionAsString() ?> +<? endif ?> + +<? if (Config::get()->CALENDAR_GROUP_ENABLE && count($date->calendars) > 1) : ?> +*Participants:* +<? foreach($date->getParticipantsAsStringArray($receiver->user_id) as $participant_string) : ?> +- <?= $participant_string ?> +<? endforeach ?> +<? endif ?> diff --git a/locale/en/LC_MAILS/date_changed.php b/locale/en/LC_MAILS/date_changed.php new file mode 100644 index 0000000000000000000000000000000000000000..ca1d670f83e3da8b51c8288ffd9d4e78357ca17c --- /dev/null +++ b/locale/en/LC_MAILS/date_changed.php @@ -0,0 +1,10 @@ +<?= $date->editor->getFullName() ?> has modified a date in the calendar. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> + +-- + +Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?> diff --git a/locale/en/LC_MAILS/date_created.php b/locale/en/LC_MAILS/date_created.php new file mode 100644 index 0000000000000000000000000000000000000000..bc805aee6baa8e7d03e9035f211d53667d531de6 --- /dev/null +++ b/locale/en/LC_MAILS/date_created.php @@ -0,0 +1,10 @@ +<?= $date->editor->getFullName() ?> has entered a date in the calendar. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> + +-- + +Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date->id) ?> diff --git a/locale/en/LC_MAILS/date_deleted.php b/locale/en/LC_MAILS/date_deleted.php new file mode 100644 index 0000000000000000000000000000000000000000..b4bafd66c40828a77c9581b662356168940f43e0 --- /dev/null +++ b/locale/en/LC_MAILS/date_deleted.php @@ -0,0 +1,6 @@ +<?= $date->editor->getFullName() ?> has deleted a date in the calendar. + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date, + 'receiver' => $receiver, +]) ?> diff --git a/locale/en/LC_MAILS/date_participation.php b/locale/en/LC_MAILS/date_participation.php new file mode 100644 index 0000000000000000000000000000000000000000..f76113440a74a87726299b9bd17ceafc7427149a --- /dev/null +++ b/locale/en/LC_MAILS/date_participation.php @@ -0,0 +1,14 @@ +<? if ($date_assignment->participation === 'ACCEPTED') : ?> +<?= $date_assignment->user->getFullName() ?> has accepted your date. +<? elseif ($date_assignment->participation === 'DECLINED') : ?> +<?= $date_assignment->user->getFullName() ?> has declined your date. +<? endif ?> + +<?= $this->render_partial(__DIR__ . '/_date_information', [ + 'date' => $date_assignment->calendar_date, + 'receiver' => $date_assignment->calendar_date->author, +]) ?> + +-- + +Go to date: <?= URLHelper::getURL('dispatch.php/calendar/date/index/' . $date_assignment->calendar_date->id) ?> diff --git a/resources/assets/javascripts/bootstrap/calendar_dialog.js b/resources/assets/javascripts/bootstrap/calendar_dialog.js deleted file mode 100644 index ee5ab4c76c690a711d29ead7703511ddfdf09ea6..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/bootstrap/calendar_dialog.js +++ /dev/null @@ -1,11 +0,0 @@ -jQuery(document).on('click', 'td.calendar-day-edit, td.calendar-day-event', function(event) { - var elem = jQuery(this) - .find('a') - .first(); - if (_.isString(elem.attr('href'))) { - STUDIP.Dialog.fromURL(elem.attr('href'), { title: elem.attr('title') }); - event.preventDefault(); - } else { - return false; - } -}); diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 1f4937d19c1710903bfff99130adacf767232a27..8f9e5fce6ab68f880befee27d6c1aacd69272dcb 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -427,6 +427,24 @@ STUDIP.ready(function () { }); } + /* + * Form elements with the "simplevue" class are meant for forms that just need some vue components + * to do something fancy inside the form but which do not need the full functionality of the form builder. + */ + let simple_vue_items = document.querySelectorAll('form .simplevue:not(.vueified)'); + if (simple_vue_items.length > 0) { + STUDIP.Vue.load().then(({createApp}) => { + simple_vue_items.forEach(f => { + createApp({ + el: f, + mounted() { + this.$el.classList.add('vueified'); + } + }); + }); + }); + } + // Well, this is really nasty: Select2 can't determine the select // element's width if it is hidden (by itself or by its parent). // This is due to the fact that elements are not rendered when hidden diff --git a/resources/assets/javascripts/bootstrap/fullcalendar.js b/resources/assets/javascripts/bootstrap/fullcalendar.js index 62beaa9e9f2d7fde29ab018ebc63d9fcb138a876..44786d9d02eb20a1a66058719862954b438bfc58 100644 --- a/resources/assets/javascripts/bootstrap/fullcalendar.js +++ b/resources/assets/javascripts/bootstrap/fullcalendar.js @@ -44,4 +44,5 @@ STUDIP.ready(function () { }); } + jQuery(document).on('change', '#date_select[data-calendar-control]', STUDIP.Fullcalendar.submitDatePicker); }); diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js index 25582d43bdf25c66c6484aec55ae8f2be1ff0c32..8c89b7f2e10475b238fef2613711c24440670c92 100644 --- a/resources/assets/javascripts/bootstrap/resources.js +++ b/resources/assets/javascripts/bootstrap/resources.js @@ -483,7 +483,7 @@ STUDIP.ready(function () { } else if ($(this).hasClass('fc-today-button') || $(this).hasClass('fc-prev-button') || $(this).hasClass('fc-next-button')) { - updateDateURL(); + STUDIP.Fullcalendar.updateDateURL(); } } ); @@ -594,71 +594,11 @@ STUDIP.ready(function () { $('.booking-plan-allday_view').attr('href', url.toString()); } - function submitDatePicker() { - var picked = $('#booking-plan-jmpdate').val(); - var iso_date_string = ''; - if(picked) { - if (picked.includes('.')) { - let [day, month, year] = picked.split('.'); - iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0"); - } else if (picked.includes('/')) { - let [day, month, year] = picked.split('/'); - iso_date_string = year.padStart(4, "20") + '-' + month.padStart(2, "0") + '-' + day.padStart(2, "0"); - } else if (picked.includes('-')) { - iso_date_string = picked; - } - } - if (iso_date_string) { - $('*[data-resources-fullcalendar="1"]').each(function () { - this.calendar.gotoDate(iso_date_string); - }); - updateDateURL(); - } - } - - function updateDateURL() { - let changedMoment; - $('[data-resources-fullcalendar="1"]').each(function () { - changedMoment = $(this)[0].calendar.getDate(); - }); - if (changedMoment) { - let changedDate = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0]; - //Get the timestamp: - let timeStamp = changedMoment.getTime() / 1000; - - $('a.resource-bookings-actions').each(function () { - const url = new URL(this.href); - url.searchParams.set('timestamp', timeStamp) - url.searchParams.set('defaultDate', changedDate) - this.href = url.toString(); - }); - - // Now change the URL of the window. - const url = new URL(window.location.href); - url.searchParams.set('defaultDate', changedDate); - - // Update url in history - history.pushState({}, null, url.toString()); - - // Adjust links accordingly - url.searchParams.delete('allday'); - $('.booking-plan-std_view').attr('href', url.toString()); - - url.searchParams.set('allday', 1); - $('.booking-plan-allday_view').attr('href', url.toString()); - - // Update sidebar value - $('#booking-plan-jmpdate').val(changedMoment.toLocaleDateString('de-DE')); - - //Store the date in the sessionStorage: - sessionStorage.setItem('booking_plan_date', changedDate) - } - } jQuery('#booking-plan-jmpdate').datepicker( { dateFormat: 'dd.mm.yy', - onClose: submitDatePicker + onClose: STUDIP.Fullcalendar.submitDatePicker } ); jQuery('.resource-booking-time-fields input[type="date"]').datepicker( diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 37bec894a65f27309104f570e9fd3c4444f46759..bb03231a39358a1fc2b20a02a675631d48f17174 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -37,7 +37,6 @@ import "./bootstrap/multi_person_search.js" import "./bootstrap/skip_links.js" import "./bootstrap/i18n_input.js" import "./bootstrap/forms.js" -import "./bootstrap/calendar_dialog.js" import "./bootstrap/drag_and_drop_upload.js" import "./bootstrap/admin_sem_classes.js" import "./bootstrap/cronjobs.js" diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 8981e950ef3b3e128a172e755fcda3354f3e5bc7..2d592be2ca452e85869394e4a1a26aed4730da90 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -14,13 +14,13 @@ import Blubber from './lib/blubber.js'; import Browse from './lib/browse.js'; import Cache from './lib/cache.js'; import Calendar from './lib/calendar.js'; -import CalendarDialog from './lib/calendar_dialog.js'; import Clipboard from './lib/clipboard.js'; import Cookie from './lib/cookie.js'; import CourseWizard from './lib/course_wizard.js'; import { createURLHelper } from './lib/url_helper.ts'; import CSS from './lib/css.js'; import Dates from './lib/dates.js'; +import DateTime from './lib/datetime.js'; import Dialog from './lib/dialog.js'; import DragAndDropUpload from './lib/drag_and_drop_upload.js'; import enrollment from './lib/enrollment.js'; @@ -31,6 +31,7 @@ import FilesDashboard from './lib/files_dashboard.js'; import Folders from './lib/folders.js'; import Forms from './lib/forms.js'; import Forum from './lib/forum.js'; +import Fullcalendar from './lib/fullcalendar.js'; import Fullscreen from './lib/fullscreen.js'; import GlobalSearch from './lib/global_search.js'; import HeaderMagic from './lib/header_magic.js'; @@ -101,11 +102,11 @@ window.STUDIP = _.assign(window.STUDIP || {}, { Browse, Cache, Calendar, - CalendarDialog, Cookie, CourseWizard, CSS, Dates, + DateTime, Dialog, DragAndDropUpload, enrollment, @@ -116,6 +117,7 @@ window.STUDIP = _.assign(window.STUDIP || {}, { Folders, Forms, Forum, + Fullcalendar, Fullscreen, Gettext, GlobalSearch, diff --git a/resources/assets/javascripts/lib/calendar_dialog.js b/resources/assets/javascripts/lib/calendar_dialog.js deleted file mode 100644 index e42a1490dca9c475c5c4e7bf207cb7c7a869e2d7..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/lib/calendar_dialog.js +++ /dev/null @@ -1,64 +0,0 @@ -import Dialog from './dialog.js'; - -const CalendarDialog = { - closeMps: function(form) { - var added_users = []; - jQuery('#calendar-manage_access_selectbox option:selected').each(function() { - added_users[added_users.length] = jQuery(this).attr('value'); - }); - jQuery.ajax({ - url: STUDIP.ABSOLUTE_URI_STUDIP + 'dispatch.php/calendar/single/add_users/', - data: { - added_users: added_users - }, - type: 'post' - }); - jQuery(form) - .closest('.ui-dialog-content') - .dialog('close'); - Dialog.fromURL(jQuery('#calendar-open-manageaccess').attr('href')); - return false; - }, - - removeUser: function(element) { - var url = jQuery(element).attr('href'); - jQuery(element).removeAttr('href'); - jQuery.ajax({ - url: url, - type: 'get', - success: function() { - var head_tr = jQuery(element) - .closest('tr') - .prev('.calendar-user-head'); - jQuery(element) - .closest('tr') - .remove(); - if (head_tr.nextUntil('.calendar-user-head').length === 0) { - head_tr.remove(); - } - } - }); - return false; - }, - - addException: function() { - var exc_date = jQuery('#exc-date').val(); - var exists = jQuery('#exc-dates input').is("input[value='" + exc_date + "']"); - if (!exists) { - var compiled = _.template( - '<li><label>' + - '<input type="checkbox" name="del_exc_dates[]" value="<%- excdate %>" style="display: none">' + - '<span><%- excdate %><img src="' + - STUDIP.ASSETS_URL + - 'images/icons/blue/trash.svg' + - '"></span></label>' + - '<input type="hidden" name="exc_dates[]" value="<%- excdate %>">' + - '</li>' - ); - jQuery('#exc-dates').append(compiled({ excdate: exc_date, link: '' })); - } - return false; - } -}; - -export default CalendarDialog; diff --git a/resources/assets/javascripts/lib/dates.js b/resources/assets/javascripts/lib/dates.js index 3be67c057582b271a547de18421c123e0a041092..ccb67a842221d01ff982d52d3ec30611300b7578 100644 --- a/resources/assets/javascripts/lib/dates.js +++ b/resources/assets/javascripts/lib/dates.js @@ -4,6 +4,7 @@ const Dates = { termin_id = $('#new_topic') .closest('[data-termin-id]') .data().terminId; + let course_id = jQuery('#new_topic').closest('[data-course-id]').data().courseId; if (!topic_name) { $('#new_topic').focus(); @@ -12,7 +13,8 @@ const Dates = { $.post(STUDIP.URLHelper.getURL('dispatch.php/course/dates/add_topic'), { title: topic_name, - termin_id: termin_id + termin_id: termin_id, + cid: course_id }).done(function(response) { if (response.li !== undefined) { $('#new_topic') diff --git a/resources/assets/javascripts/lib/datetime.js b/resources/assets/javascripts/lib/datetime.js new file mode 100644 index 0000000000000000000000000000000000000000..d58fac85b84434a549075409f5ac974d92c7f4c4 --- /dev/null +++ b/resources/assets/javascripts/lib/datetime.js @@ -0,0 +1,61 @@ +import { $gettext, $gettextInterpolate } from "./gettext.ts"; + + +const DateTime = { + /** + * A helper method for padding strings with leading zeros. + * @param what The date to pad. + * @param length The length of the string to output. + * @returns {string} A padded version of $what. + */ + pad(what, length = 2) { + return `00000000${what}`.substr(-length); + }, + + /** + * Returns an ISO representation of the specified Date object. + * in the format YYYY-MM-DD. + * + * @param date The Date object to format as ISO date. + * @returns {string} The ISO date string of the Date object. + */ + getISODate(date) { + return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + date.getDate(); + }, + + /** + * Returns a formatted version of the specified Date object + * in the Stud.IP date formatting. + * + * @param date The Date object to be formatted. + * @param relative_value Whether to return a relative time value (true) + * or an absolute one (false). Defaults to false. + * @param date_only Whether to return the date only (true) or date and time (false). + * Defaults to false. Only regarded when $relative_value is false. + * @returns {*|string} The date, formatted according to the Stud.IP format for dates. + */ + getStudipDate(date, relative_value = false, date_only = false) { + if (relative_value) { + let now = Date.now(); + if (now - date < 1 * 60 * 1000) { + return $gettext('Jetzt'); + } + if (now - date < 2 * 60 * 60 * 1000) { + return $gettextInterpolate( + $gettext('Vor %{ minutes } Minuten'), + {minutes: Math.floor((now - date) / (1000 * 60))} + ); + } + return this.pad(date.getHours()) + ':' + this.pad(date.getMinutes()); + } + + if (date_only) { + return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear(); + } + + return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + this.pad(date.getHours()) + ':' + this.pad(date.getMinutes()); + } +}; + + +export default DateTime; diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js index ef891507daa392cf3cff8344ea43d266ea3ecc6d..b5bd78d49a709df1455c2772e1fd5bc7ba2f4ef7 100644 --- a/resources/assets/javascripts/lib/fullcalendar.js +++ b/resources/assets/javascripts/lib/fullcalendar.js @@ -162,6 +162,16 @@ class Fullcalendar end: this.toRFC3339String(info.event.end) } }).fail(info.revert); + } else if (info.event.extendedProps.studip_api_urls.resize_dialog) { + STUDIP.Dialog.fromURL( + info.event.extendedProps.studip_api_urls.resize_dialog, + { + data: { + begin: this.toRFC3339String(info.event.start), + end: this.toRFC3339String(info.event.end) + } + } + ); } } @@ -242,40 +252,80 @@ class Fullcalendar var drop_resource_id = info.newResource ? info.newResource.id : info.event.extendedProps.studip_range_id; - if (info.event.extendedProps.studip_api_urls.move) { + if (info.event.extendedProps.studip_api_urls.move || info.event.extendedProps.studip_api_urls.move_dialog) { + let move_dialog = info.event.extendedProps.studip_api_urls.move_dialog; if (info.event.allDay) { - $.post({ - async: false, - url: info.event.extendedProps.studip_api_urls.move, - data: { - resource_id: drop_resource_id, - begin: this.toRFC3339String(info.event.start.setHours(0,0,0)), - end: this.toRFC3339String(info.event.start.setHours(23,59,59)) - } - }).fail(info.revert); + if (move_dialog) { + STUDIP.Dialog.fromURL( + move_dialog, + { + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)), + end: this.toRFC3339String(info.event.start.setHours(23, 59, 59)) + } + } + ); + } else { + jQuery.post({ + async: false, + url: info.event.extendedProps.studip_api_urls.move, + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)), + end: this.toRFC3339String(info.event.start.setHours(23, 59, 59)) + } + }).fail(info.revert); + } } else if (info.event.end === null) { - var real_end = new Date(); + let real_end = new Date(); real_end.setTime(info.event.start.getTime()); real_end.setHours(info.event.start.getHours()+2); - $.post({ - async: false, - url: info.event.extendedProps.studip_api_urls.move, - data: { - resource_id: drop_resource_id, - begin: this.toRFC3339String(info.event.start), - end: this.toRFC3339String(real_end) - } - }).fail(info.revert); + if (move_dialog) { + STUDIP.Dialog.fromURL( + move_dialog, + { + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start), + end: this.toRFC3339String(real_end) + } + } + ); + } else { + jQuery.post({ + async: false, + url: info.event.extendedProps.studip_api_urls.move, + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start), + end: this.toRFC3339String(real_end) + } + }).fail(info.revert); + } } else { - $.post({ - async: false, - url: info.event.extendedProps.studip_api_urls.move, - data: { - resource_id: drop_resource_id, - begin: this.toRFC3339String(info.event.start), - end: this.toRFC3339String(info.event.end) - } - }).fail(info.revert); + if (move_dialog) { + STUDIP.Dialog.fromURL( + move_dialog, + { + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start), + end: this.toRFC3339String(info.event.end) + } + } + ); + } else { + jQuery.post({ + async: false, + url: info.event.extendedProps.studip_api_urls.move, + data: { + resource_id: drop_resource_id, + begin: this.toRFC3339String(info.event.start), + end: this.toRFC3339String(info.event.end) + } + }).fail(info.revert); + } } } } @@ -370,6 +420,12 @@ class Fullcalendar studip_functions: [], resourceAreaWidth: '20%', select (selectionInfo) { + let calendar_config = JSON.parse(selectionInfo.view.context.calendar.el.dataset.config); + let dialog_size = 'auto'; + if (calendar_config.dialog_size !== undefined) { + dialog_size = calendar_config.dialog_size; + } + if (!selectionInfo.view.viewSpec.options.editable || !selectionInfo.view.viewSpec.options.studip_urls) { //The calendar isn't editable. return; @@ -380,15 +436,19 @@ class Fullcalendar data: { begin: selectionInfo.start.getTime()/1000, end: selectionInfo.end.getTime()/1000, - ressource_id: selectionInfo.resource.id - } + ressource_id: selectionInfo.resource.id, + all_day: selectionInfo.allDay ? '1' : '0' + }, + size: dialog_size }); } else { STUDIP.Dialog.fromURL(selectionInfo.view.viewSpec.options.studip_urls.add, { data: { begin: selectionInfo.start.getTime()/1000, - end: selectionInfo.end.getTime()/1000 - } + end: selectionInfo.end.getTime()/1000, + all_day: selectionInfo.allDay ? '1' : '0' + }, + size: dialog_size }); } } @@ -421,15 +481,33 @@ class Fullcalendar if (extended_props.studip_view_urls === undefined) { return; } + let calendar_config = JSON.parse(eventClickInfo.view.context.calendar.el.dataset.config); + let dialog_size = 'auto'; + if (calendar_config.dialog_size !== undefined) { + //Use the configured default dialog size for the fullcalendar instance: + dialog_size = calendar_config.dialog_size; + } + if (extended_props.dialog_size !== undefined) { + //Use the dialog size of the event: + dialog_size = extended_props.dialog_size; + } if (!event.startEditable && extended_props.studip_view_urls.show) { STUDIP.Dialog.fromURL( - STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show) - ); - } else if (event.startEditable && extended_props.studip_view_urls.edit) { - STUDIP.Dialog.fromURL( - STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit), - {'size': 'big'} + STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show), + {size: dialog_size} ); + } else if (event.startEditable) { + if (extended_props.studip_view_urls.edit) { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit), + {size: dialog_size} + ); + } else if (extended_props.studip_view_urls.show) { + STUDIP.Dialog.fromURL( + STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show), + {size: dialog_size} + ); + } } return false; }, @@ -612,6 +690,77 @@ class Fullcalendar return this.init(node, config); } + + static submitDatePicker() { + let picked_date = jQuery('#booking-plan-jmpdate').val(); + let booking_plan_datepicker = true; + if (!picked_date) { + //Not a booking plan date selector. + picked_date = jQuery('#date_select').val(); + booking_plan_datepicker = false; + } + let iso_date_string = ''; + if (picked_date) { + if (picked_date.includes('.')) { + let [day, month, year] = picked_date.split('.'); + iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0'); + } else if (picked_date.includes('/')) { + let [day, month, year] = picked_date.split('/'); + iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0'); + } else if (picked_date.includes('-')) { + iso_date_string = picked_date; + } + } + if (iso_date_string) { + jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () { + this.calendar.gotoDate(iso_date_string); + }); + if (booking_plan_datepicker) { + Fullcalendar.updateDateURL(); + } + } + } + + static updateDateURL() { + let changedMoment; + jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () { + changedMoment = this.calendar.getDate(); + }); + if (changedMoment) { + let changed_date = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0]; + //Get the timestamp: + let timestamp = changedMoment.getTime() / 1000; + + jQuery('a.resource-bookings-actions').each(function () { + const url = new URL(this.href); + url.searchParams.set('timestamp', timestamp) + url.searchParams.set('defaultDate', changed_date) + this.href = url.toString(); + }); + + // Now change the URL of the window. + const url = new URL(window.location.href); + url.searchParams.set('defaultDate', changed_date); + + // Update url in history + history.pushState({}, null, url.toString()); + + // Adjust links accordingly + url.searchParams.delete('allday'); + jQuery('.booking-plan-std_view').attr('href', url.toString()); + + url.searchParams.set('allday', 1); + jQuery('.booking-plan-allday_view').attr('href', url.toString()); + + // Update sidebar value + let element = jQuery('#booking-plan-jmpdate,#date_select').first(); + element.val(changedMoment.toLocaleDateString('de-DE')); + if (element.is('#booking-plan-jmpdate')) { + //Store the date in the sessionStorage: + sessionStorage.setItem('booking_plan_date', changed_date); + } + } + } } export default Fullcalendar; diff --git a/resources/assets/stylesheets/highcontrast.scss b/resources/assets/stylesheets/highcontrast.scss index 6c822b17be500b1bae62685bcaed259bea251faa..47bfbf213b7cd63dfac2e54344f1ebc55ce6978b 100644 --- a/resources/assets/stylesheets/highcontrast.scss +++ b/resources/assets/stylesheets/highcontrast.scss @@ -515,26 +515,6 @@ form.default fieldset.collapsable.collapsed legend { } /* Stundenplan / Terminkalender */ -.celltoday { - background-color: $white; - border: 1px solid $black; - - > a { - color: $black; - font-size: 1.5em; - } -} -a:link.calhead { - color: $contrast-blue; -} - -.calhead label { - color: $contrast-blue !important; - - &:hover { - text-decoration: underline; - } -} a .hidden-tiny-down { color: $contrast-blue !important; @@ -566,40 +546,6 @@ a .hidden-tiny-down { /* Calendar categories */ -span li.calendar-category1, -ul li.calendar-category1, -span li.calendar-category2, -ul li.calendar-category2, -span li.calendar-category3, -ul li.calendar-category3, -span li.calendar-category4, -ul li.calendar-category4, -span li.calendar-category5, -ul li.calendar-category5, -span li.calendar-category6, -ul li.calendar-category6, -span li.calendar-category7, -ul li.calendar-category7, -span li.calendar-category8, -ul li.calendar-category8, -span li.calendar-category9, -ul li.calendar-category9, -span li.calendar-category10, -ul li.calendar-category10, -span li.calendar-category11, -ul li.calendar-category11, -span li.calendar-category12, -ul li.calendar-category12, -span li.calendar-category13, -ul li.calendar-category13, -span li.calendar-category14, -ul li.calendar-category14, -span li.calendar-category15, -ul li.calendar-category15 { - color: $black; - -} - div.schedule_entry { dl { &.hover:hover { opacity: unset; } @@ -772,244 +718,6 @@ div.schedule_entry { } } -table.calendar-week, -table.calendar-day { - tbody tr td { - &.calendar-day-event { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-day-event; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-day-event; - } - &.calendar-category1, - &.calendar-course-category5 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-1; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-1; - } - &.calendar-category2, - &.calendar-course-category1 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-2; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-2; - } - &.calendar-category3, - &.calendar-course-category2 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-3; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-3; - } - &.calendar-category4, - &.calendar-course-category3 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-4; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-4; - } - &.calendar-category5, - &.calendar-course-category4 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-5; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-5; - } - &.calendar-category6, - &.calendar-course-category6 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-6; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-6; - } - &.calendar-category7, - &.calendar-course-category8 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-7; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-7; - } - &.calendar-category8, - &.calendar-course-category9 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-8; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-8; - } - &.calendar-category9, - &.calendar-course-category10 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-9; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-9; - } - &.calendar-category10, - &.calendar-course-category11 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-10; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-10; - } - &.calendar-category11, - &.calendar-course-category12 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-11; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-11; - } - &.calendar-category12, - &.calendar-course-category13 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-12; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-12; - } - &.calendar-category13, - &.calendar-course-category14 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-13; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-13; - } - &.calendar-category14, - &.calendar-course-category15 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-14; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-14; - } - &.calendar-category15, - &.calendar-course-category7 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-15; - overflow: hidden; - color: $black; - } - background: $white; - border: solid 1px $calendar-category-15; - } - &.calendar-category255, - &.calendar-course-category255 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $calendar-category-255; - overflow: hidden; - } - background: $white; - border: solid 1px $calendar-category-255; - } - /* Termin von im Stundenplan vorgemerkter Kurs */ - &.calendar-course-category256 { - a { - color: $contrast-blue !important; - } - div:first-child { - background-color: $contrast-blue-medium; - overflow: hidden; - } - background: $white; - border: solid 1px $contrast-blue-medium; - } - } -} - - -/* links */ -div.index_container div.index_main nav div.login_link a { - text-decoration: underline; - - p { - color: $black !important; - } -} - /* underlined links only in main content,not in navigation */ a, a:link, diff --git a/resources/assets/stylesheets/less/calendar.less b/resources/assets/stylesheets/less/calendar.less deleted file mode 100644 index edb2e570d157c28062deab09d3a0eed3267cd912..0000000000000000000000000000000000000000 --- a/resources/assets/stylesheets/less/calendar.less +++ /dev/null @@ -1,587 +0,0 @@ -// TODO: LESSify - -/* --- Styles fuer Terminkalender ------------------------------------------- */ -a.day { - font-weight: bold; -} - -a.sday { - color: var(--red); - font-weight: bold; -} - -a.hday { - color: var(--red-80); - font-weight: bold; -} - -span.kwmin { - color: var(--dark-gray-color-80); - font-weight: bold; -} - -a.lightday { - color: var(--base-color-40); - font-weight: bold; -} - -a.lightsday { - color: var(--red-40); - font-weight: bold; -} - -.inday { - font-size: 8pt; -} - -.precol1w { - font-size: 12pt; - font-weight: bold; - color: var(--light-gray-color); - text-align: center; - vertical-align: top; -} - -.precol2w { - font-size: 8pt; - font-weight: bold; - color: var(--light-gray-color); - text-align: center; -} - -td.calhead, div.calhead { - font-size: 18pt; - font-weight: bold; - color: var(--light-gray-color); - text-align: center; -} - -a:link.calhead { - color: var(--base-color-60); - white-space: nowrap; - font-weight: bold; -} - -a:hover.calhead { - color: var(--red-60); -} - -.calhead label { - cursor: pointer; - &:hover { - color: var(--base-color-40); - } - - .media-breakpoint-small-down({ - .button(); - - img { - padding-left: 0.5em; - vertical-align: middle; - } - }) -} - -.celltoday { - background-color: var(--red-20); -} - -td.weekend { - background-color: var(--dark-gray-color-15); -} - -td.weekday { - background-color: var(--dark-gray-color-5); -} - -td.current { - padding: 2px; - border: 2px solid var(--red); -} - -table.calendar-week, table.calendar-day { - border-spacing: 0; - table-layout: fixed; - td { - padding: 0; - } -} - -table.calendar-month { - width: 100%; - tr td { - max-width: 90px; - min-width: 90px; - vertical-align: top; - } -} - -td.month { - background-color: fadeout(darken(@dark-gray-color-15, 5), 30); - padding: 3px; - div { - width: 90%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -td.lightmonth { - background-color: fadeout(darken(@dark-gray-color-5, 5), 30); - padding: 3px; - div { - width: 90%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -table.calendar-month td.calendar-month-week { - text-align: center; - vertical-align: middle; - height: 80px; - width: 80px; -} - -td.weekdayevents { - width: 90px; -} - -nav.calendar-nav { - display: flex; - align-items: center; - padding-bottom: 1em; - - > div { - flex: 1 1 auto; - } - - .calhead { - color: var(--base-color); - } -} - -.calendar-day-edit { - text-align: right; - font-size: 0.8em; -} - -.calendar-week tbody tr, .calendar-day tbody tr { - transition: background-color 0.3s; - &:hover { - background-color: fadeout(@dark-gray-color-5, 40%); - } - & td { - padding: 3px; - border-bottom: 1px solid var(--dark-gray-color-5); - } -} - -.calendar-day-event-title { - overflow: hidden; - text-overflow: ellipsis; - a { - color: var(--dark-gray-color); - } -} - -.calendar-category-mixin(@color-bg) { - vertical-align: top; - font-size: 11px; - color: var(--white); - padding: 0; - /* necessary for All-day Events */ - a { - color: contrast(@color-bg, black, white, 60%); - font-weight: 600; - } -} - -.calendar-single-year { - .calendar-single-year--table { - - > thead th { - min-width: 5em; - text-align: left; - } - - .yday { - white-space: nowrap; - } - } -} -/* --- Coloring Styles for Personal TERMIN Categories ---------------------------------------------- */ -select, ul, span { - option, li, span, input[type="radio"] { - &.calendar-category1 { - color: @calendar-category-1; - } - &.calendar-category2 { - color: @calendar-category-2; - } - &.calendar-category3 { - color: @calendar-category-3; - } - &.calendar-category4 { - color: @calendar-category-4; - } - &.calendar-category5 { - color: @calendar-category-5; - } - &.calendar-category6 { - color: @calendar-category-6; - } - &.calendar-category7 { - color: @calendar-category-7; - } - &.calendar-category8 { - color: @calendar-category-8; - } - &.calendar-category9 { - color: @calendar-category-9; - } - &.calendar-category10 { - color: @calendar-category-10; - } - &.calendar-category11 { - color: @calendar-category-11; - } - &.calendar-category12 { - color: @calendar-category-12; - } - &.calendar-category13 { - color: @calendar-category-13; - } - &.calendar-category14 { - color: @calendar-category-14; - } - &.calendar-category15 { - color: @calendar-category-15; - } - &.calendar-category255 { - color: @calendar-category-255; - } - } -} - -table.calendar-week, table.calendar-day { - & tbody tr td { - &.calendar-day-event { - div:first-child { - background-color: @calendar-day-event; - overflow: hidden; - } - background: @calendar-day-event-aux; - border: solid 1px @calendar-day-event; - .calendar-category-mixin(@calendar-day-event-aux); - } - &.calendar-category1, &.calendar-course-category5 { - div:first-child { - background-color: @calendar-category-1; - overflow: hidden; - } - background: @calendar-category-1-aux; - border: solid 1px @calendar-category-1; - .calendar-category-mixin(@calendar-category-1-aux); - } - &.calendar-category2, &.calendar-course-category1 { - div:first-child { - background-color: @calendar-category-2; - overflow: hidden; - } - background: @calendar-category-2-aux; - border: solid 1px @calendar-category-2; - .calendar-category-mixin(@calendar-category-2-aux); - } - &.calendar-category3, &.calendar-course-category2 { - div:first-child { - background-color: @calendar-category-3; - overflow: hidden; - } - background: @calendar-category-3-aux; - border: solid 1px @calendar-category-3; - .calendar-category-mixin(@calendar-category-3-aux); - } - &.calendar-category4, &.calendar-course-category3 { - div:first-child { - background-color: @calendar-category-4; - overflow: hidden; - } - background: @calendar-category-4-aux; - border: solid 1px @calendar-category-4; - .calendar-category-mixin(@calendar-category-4-aux); - } - &.calendar-category5, &.calendar-course-category4 { - div:first-child { - background-color: @calendar-category-5; - overflow: hidden; - } - background: @calendar-category-5-aux; - border: solid 1px @calendar-category-5; - .calendar-category-mixin(@calendar-category-5-aux); - } - &.calendar-category6, &.calendar-course-category6 { - div:first-child { - background-color: @calendar-category-6; - overflow: hidden; - } - background: @calendar-category-6-aux; - border: solid 1px @calendar-category-6; - .calendar-category-mixin(@calendar-category-6-aux); - } - &.calendar-category7, &.calendar-course-category8 { - div:first-child { - background-color: @calendar-category-7; - overflow: hidden; - } - background: @calendar-category-7-aux; - border: solid 1px @calendar-category-7; - .calendar-category-mixin(@calendar-category-7-aux); - } - &.calendar-category8, &.calendar-course-category9 { - div:first-child { - background-color: @calendar-category-8; - overflow: hidden; - } - background: @calendar-category-8-aux; - border: solid 1px @calendar-category-8; - .calendar-category-mixin(@calendar-category-8-aux); - } - &.calendar-category9, &.calendar-course-category10 { - div:first-child { - background-color: @calendar-category-9; - overflow: hidden; - } - background: @calendar-category-9-aux; - border: solid 1px @calendar-category-9; - .calendar-category-mixin(@calendar-category-9-aux); - } - &.calendar-category10, &.calendar-course-category11 { - div:first-child { - background-color: @calendar-category-10; - overflow: hidden; - } - background: @calendar-category-10-aux; - border: solid 1px @calendar-category-10; - .calendar-category-mixin(@calendar-category-10-aux); - } - &.calendar-category11, &.calendar-course-category12 { - div:first-child { - background-color: @calendar-category-11; - overflow: hidden; - } - background: @calendar-category-11-aux; - border: solid 1px @calendar-category-11; - .calendar-category-mixin(@calendar-category-11-aux); - } - &.calendar-category12, &.calendar-course-category13 { - div:first-child { - background-color: @calendar-category-12; - overflow: hidden; - } - background: @calendar-category-12-aux; - border: solid 1px @calendar-category-12; - .calendar-category-mixin(@calendar-category-12-aux); - } - &.calendar-category13, &.calendar-course-category14 { - div:first-child { - background-color: @calendar-category-13; - overflow: hidden; - } - background: @calendar-category-13-aux; - border: solid 1px @calendar-category-13; - .calendar-category-mixin(@calendar-category-13-aux); - } - &.calendar-category14, &.calendar-course-category15 { - div:first-child { - background-color: @calendar-category-14; - overflow: hidden; - } - background: @calendar-category-14-aux; - border: solid 1px @calendar-category-14; - .calendar-category-mixin(@calendar-category-14-aux); - } - &.calendar-category15, &.calendar-course-category7 { - div:first-child { - background-color: @calendar-category-15; - overflow: hidden; - } - background: @calendar-category-15-aux; - border: solid 1px @calendar-category-15; - .calendar-category-mixin(@calendar-category-15-aux); - } - &.calendar-category255, &.calendar-course-category255 { - div:first-child { - background-color: @calendar-category-255; - overflow: hidden; - } - background: @calendar-category-255-aux; - border: solid 1px @calendar-category-255; - .calendar-category-mixin(@calendar-category-255-aux); - } - /* Termin von im Stundenplan vorgemerkter Kurs */ - &.calendar-course-category256 { - div:first-child { - background-color: #2D2C64; - overflow: hidden; - } - background: mix(#2D2C64, #fff, 60%); - border: solid 1px #2D2C64; - .calendar-category-mixin(mix(#2D2C64, #fff, 60%)); - } - } -} - -a.calendar-event-text1, -a.Calendar-course-event-text5 { - color: @calendar-category-1; -} - -a.calendar-event-text2, -a.Calendar-course-event-text1{ - color: @calendar-category-2; -} - -a.calendar-event-text3, -a.Calendar-course-event-text2 { - color: @calendar-category-3; -} - -a.calendar-event-text4, -a.Calendar-course-event-text3 { - color: @calendar-category-4; -} - -a.calendar-event-text5, -a.Calendar-course-event-text4 { - color: @calendar-category-5; -} - -a.calendar-event-text6, -a.Calendar-course-event-text6 { - color: @calendar-category-6; -} - -a.calendar-event-text7 { - color: @calendar-category-7; -} - -a.calendar-event-text8 { - color: @calendar-category-8; -} - -a.calendar-event-text9 { - color: @calendar-category-9; -} - -a.calendar-event-text10 { - color: @calendar-category-10; -} - -a.calendar-event-text11 { - color: @calendar-category-11; -} - -a.calendar-event-text12 { - color: @calendar-category-12; -} - -a.calendar-event-text13 { - color: @calendar-category-13; -} - -a.calendar-event-text14 { - color: @calendar-category-14; -} - -a.calendar-event-text15, -a.Calendar-course-event-text7 { - color: @calendar-category-15; -} - -a.calendar-event-text255, -a.Calendar-course-event-text255 { - color: @calendar-category-255; -} -.calendar-tooltip { - display: none; - font-size: 0.8em; -} - -.calendar-group-events { - background: linear-gradient(to right, var(--base-color-60), var(--content-color-60)) repeat-x var(--base-color-60); - border: solid 1px var(--base-gray); -} - -#exc-dates { - padding: 2px; - list-style-type: none; - width: 7.5em; - min-height: 5em; - max-height: 10em; - overflow: auto; - border: 1px solid var(--dark-gray-color-60); - - img { - vertical-align: text-top; - } - input:checked ~ span { - text-decoration: line-through; - opacity: 0.6; - } -} - -/* --- Styles fuer TerminZeile ---------------------------------------------- */ -table.tabdaterow { - background-color: white; -} - -td.tddaterowp { - border: 1px solid var(--white); - background-color: var(--dark-gray-color-10); - font-weight: bold; - color: var(--dark-green); - font-size: 8pt; -} - -td.tddaterowpx { - border: 1px solid var(--active-color); - background-color: var(--dark-gray-color-10); - font-weight: bold; - color: var(--dark-green); - font-size: 8pt; -} - - -.recurrences { - width: 100%; - float: none; - list-style: none; - text-align: left; - position: relative; - padding: 0; - margin: 0; - li { - display: block; - width: 100%; - } - input.rec-select { - } - .rec-label { - cursor: pointer; - } - .rec-label:hover { - } - .rec-content { - display: none; - position: relative; - padding-left: 3em; - } - [id^="rec"]:checked + label.rec-label { - } - [id^="rec"]:checked ~ [id^="rec-content"] { - display: block; - } -} diff --git a/resources/assets/stylesheets/scss/calendar.scss b/resources/assets/stylesheets/scss/calendar.scss new file mode 100644 index 0000000000000000000000000000000000000000..4ad94b885ce967c5d1fb052ff7343695503413df --- /dev/null +++ b/resources/assets/stylesheets/scss/calendar.scss @@ -0,0 +1,135 @@ +.fc-body { + .fc-event { + + background-color: #fff; + color: #000; + border-width: 2px; + + &:hover { + color: #000; + } + + &.course-color-0 { + border-color: $group-color-0; + background-color: lighten($group-color-0, 45%); + + &:hover { + background-color: lighten($group-color-0, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-0; + } + } + + &.course-color-1 { + border-color: $group-color-1; + background-color: lighten($group-color-1, 45%); + + &:hover { + background-color: lighten($group-color-1, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-1; + } + } + + &.course-color-2 { + border-color: $group-color-2; + background-color: lighten($group-color-2, 45%); + + &:hover { + background-color: lighten($group-color-2, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-2; + } + } + + &.course-color-3 { + border-color: $group-color-3; + background-color: lighten($group-color-3, 45%); + + &:hover { + background-color: lighten($group-color-3, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-3; + } + } + + &.course-color-4 { + border-color: $group-color-4; + background-color: lighten($group-color-4, 45%); + + &:hover { + background-color: lighten($group-color-4, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-4; + } + } + + &.course-color-5 { + border-color: $group-color-5; + background-color: lighten($group-color-5, 45%); + + &:hover { + background-color: lighten($group-color-5, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-5; + } + } + + &.course-color-6 { + border-color: $group-color-6; + background-color: lighten($group-color-6, 45%); + + &:hover { + background-color: lighten($group-color-6, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-6; + } + } + + &.course-color-7 { + border-color: $group-color-7; + background-color: lighten($group-color-7, 45%); + + &:hover { + background-color: lighten($group-color-7, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-7; + } + } + + &.course-color-8 { + border-color: $group-color-8; + background-color: lighten($group-color-8, 45%); + + &:hover { + background-color: lighten($group-color-8, 50%); + } + + .fc-time { + border-bottom: 1px solid $group-color-8; + } + } + } +} + + +/* special rule for the month view: do not underline the time */ +.fc-view.fc-dayGridMonth-view .fc-event .fc-time { + border: none; +} diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index 0b43f044afae4f571df093bacc3b504bacdf7ddd..fc3eb4b41f58d4feafa304c3733e720f73f2cd76 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -37,7 +37,7 @@ form.default { font-style: italic; } - input[type=date], input[type=email], input[type=number], + input[type=date], input[type=datetime-local], input[type=email], input[type=number], input[type=password], input[type=text], input[type=time], input[type=url], input[type=tel], textarea, select { box-sizing: border-box; @@ -86,10 +86,14 @@ form.default { max-width: $max-width-m; } - input[type=date], input[type=number], input[type=time], input[type=tel]:not(.size-m) { + input[type=date].hasDatepicker, input[type=date][data-date-picker], input[type=number], input[type=time], input[type=tel]:not(.size-m) { max-width: $max-width-s; } + input[type=date]:not(.hasDatepicker, [data-date-picker]) { + max-width: $max-width-m; + } + textarea { min-height: 6em; } @@ -533,6 +537,17 @@ form.default { margin-left: 10px; } } + + .input-with-icon { + input { + display: inline; + width: calc(100% - 24px); + } + img.icon { + height: 2em; + margin-top: 0.5ex; + } + } } form.narrow { diff --git a/resources/assets/stylesheets/studip.less b/resources/assets/stylesheets/studip.less index 1a2c7941b20ead9113e59a3541fb6cda30c111b7..aee6779a5c121fd7ab4f088824eaf5ea058f59f6 100644 --- a/resources/assets/stylesheets/studip.less +++ b/resources/assets/stylesheets/studip.less @@ -12,7 +12,6 @@ @import "less/tables.less"; @import "less/buttons.less"; @import "less/messagebox.less"; -@import "less/calendar.less"; @import "less/schedule.less"; @import "less/files.less"; diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index b0e768ea172bf86327d5c2a7361450a94a4b1171..606fa0738d1153e25a3183516dc46bce96db9d7e 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -22,6 +22,7 @@ @import "scss/blockquote.scss"; @import "scss/blubber"; @import "scss/buttons"; +@import "scss/calendar"; @import "scss/clipboard"; @import "scss/consultation"; @import "scss/contacts"; diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index b8cf935e011761cc70d43b7fe7c5df31394bb7f8..2390bb9b1848a4648366640d49f87c49f2c71a01 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -1,6 +1,11 @@ +import CalendarPermissionsTable from "./components/form_inputs/CalendarPermissionsTable.vue"; +import DayOfWeekSelect from './components/form_inputs/DayOfWeekSelect.vue'; +import DateListInput from './components/form_inputs/DateListInput.vue'; import Multiselect from './components/Multiselect.vue'; +import MyCoursesColouredTable from './components/form_inputs/MyCoursesColouredTable.vue'; import EditableList from "./components/EditableList.vue"; import Quicksearch from './components/Quicksearch.vue'; +import RepetitionInput from "./components/form_inputs/RepetitionInput.vue"; import SidebarWidget from './components/SidebarWidget.vue'; import StudipActionMenu from './components/StudipActionMenu.vue'; import StudipAssetImg from './components/StudipAssetImg.vue'; @@ -10,6 +15,7 @@ import StudipFileSize from './components/StudipFileSize.vue'; import StudipFolderSize from './components/StudipFolderSize.vue'; import StudipIcon from './components/StudipIcon.vue'; import RangeInput from './components/RangeInput.vue'; +import Datepicker from './components/Datepicker.vue'; import Datetimepicker from './components/Datetimepicker.vue'; import TextareaWithToolbar from './components/TextareaWithToolbar.vue'; import I18nTextarea from "./components/I18nTextarea.vue"; @@ -23,14 +29,20 @@ import StudipSelect from './components/StudipSelect.vue'; import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue'; const BaseComponents = { + CalendarPermissionsTable, + DayOfWeekSelect, + DateListInput, Multiselect, + MyCoursesColouredTable, EditableList, Quicksearch, RangeInput, + RepetitionInput, SidebarWidget, StudipActionMenu, StudipAssetImg, StudipDateTime, + Datepicker, Datetimepicker, StudipDialog, StudipFileSize, diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue new file mode 100644 index 0000000000000000000000000000000000000000..3db44ceb9712af4ae0c68d55f251ec29f1d62747 --- /dev/null +++ b/resources/vue/components/Datepicker.vue @@ -0,0 +1,76 @@ +<template> + <span> + <input type="hidden" :name="name" :value="value"> + <input type="text" + ref="visibleInput" + class="visible_input" + @change="setUnixTimestamp" + v-bind="$attrs" + v-on="$listeners"> + </span> +</template> + +<script> +export default { + name: "datepicker", + inheritAttrs: false, + props: { + name: { + type: String, + required: false + }, + value: { + required: false + }, + mindate: { + required: false + }, + maxdate: { + required: false + } + }, + methods: { + setUnixTimestamp () { + let formatted_date = this.$refs.visibleInput.value; + let date = formatted_date.match(/(\d+)/g); + date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); + this.$emit('input', Math.floor(date / 1000)); + } + }, + mounted () { + let value = !isNaN(parseInt(this.value, 10)) ? parseInt(this.value, 10) : this.value; + if (Number.isInteger(value)) { + let date = new Date(value * 1000); + let formatted_date = + (date.getDate() < 10 ? "0" : "") + date.getDate() + + "." + + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + + "." + + date.getFullYear(); + this.$refs.visibleInput.value = formatted_date; + } else { + this.$refs.visibleInput.value = value; + } + let params = { + onSelect: () => { + this.setUnixTimestamp(); + } + }; + if (this.mindate) { + params.minDate = new Date(this.mindate * 1000) + } + if (this.maxdate) { + params.maxDate = new Date(this.maxdate * 1000) + } + $(this.$refs.visibleInput).datetimepicker(params); + }, + watch: { + mindate (new_data, old_data) { + $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000)); + }, + maxdate (new_data, old_data) { + $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000)); + } + } +} +</script> diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue index c76b40063dc5a2ad6ac8c65eaeb4a1649a0959d4..cf1716b63576accc5bac9aa56ccdb881d1661ec6 100644 --- a/resources/vue/components/EditableList.vue +++ b/resources/vue/components/EditableList.vue @@ -78,7 +78,7 @@ export default { return { resort: false, //this is just for triggering the computed property sortedItems to be sorted again preventChangeOfQuickselect: false, - allItems: this.items + allItems: this.items ?? [] }; }, methods: { @@ -159,8 +159,8 @@ export default { if (a.icon === b.icon) { return a.name.localeCompare(b.name); } else { - let a_icon = a.icon || ''; - let b_icon = b.icon || ''; + let a_icon = typeof a.icon === 'string' ? a.icon : ''; + let b_icon = typeof b.icon === 'string' ? b.icon : ''; if (this.category_order.indexOf(a_icon) > -1 && this.category_order.indexOf(b_icon) > -1) { return this.category_order.indexOf(a_icon) < this.category_order.indexOf(b_icon) ? -1 : 1; } else { diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue index 1cf852ca1c2d6dfe7e040a752bb9f9985de53753..dfdc0c3f16a13d545f5a5d629a362ce23d8cbbc0 100644 --- a/resources/vue/components/StudipDateTime.vue +++ b/resources/vue/components/StudipDateTime.vue @@ -5,9 +5,6 @@ </template> <script> - function pad(what, length = 2) { - return `00000000${what}`.substr(-length); - } export default { name: 'studip-date-time', @@ -17,6 +14,11 @@ type: Boolean, required: false, default: false + }, + date_only: { + type: Boolean, + required: false, + default: false } }, computed: { @@ -40,18 +42,8 @@ return `Should be integer: ${this.timestamp}`; } let date = new Date(this.timestamp * 1000); - let now = Date.now(); - if (!force_absolute && this.relative && this.display_relative()) { - if (now - date < 1 * 60 * 1000) { - return this.$gettext('Jetzt'); - } - if (now - date < 2 * 60 * 60 * 1000) { - return this.$gettext('Vor %s Minuten').replace('%s', Math.floor((now - date) / (1000 * 60))); - } - return pad(date.getHours()) + ':' + pad(date.getMinutes()); - } else { - return pad(date.getDate()) + '.' + pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes()); - } + let relative_value = !force_absolute && this.relative && this.display_relative(); + return STUDIP.DateTime.getStudipDate(date, relative_value, this.date_only); } }, mounted: function () { diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..a0a76a2897acbd7e7f8c099c57932ced9d0b31ca --- /dev/null +++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue @@ -0,0 +1,86 @@ +<template> + <div class="formpart"> + <quicksearch v-if="searchtype" :searchtype="searchtype" name="qs" @input="addContact" + :placeholder="$gettext('Personen hinzufügen')"></quicksearch> + <table class="default"> + <caption>{{ $gettext('Kontakte, mit denen der Kalender geteilt wird')}}</caption> + <thead> + <tr> + <th>{{ $gettext('Name') }}</th> + <th>{{ $gettext('Schreibzugriff') }}</th> + <th class="actions">{{ $gettext('Nicht mehr teilen') }}</th> + </tr> + </thead> + <tbody> + <tr v-if="this.users.length === 0"> + <td colspan="3"> + <studip-message-box type="info"> + {{ $gettext('Der Kalender wird mit keinem Kontakt geteilt.') }} + </studip-message-box> + </td> + </tr> + <tr v-for="user in this.users" :key="user.id"> + <td> + <input type="hidden" :name="name + '_permissions[]'" + :value="user.id"> + {{ user.name }} + </td> + <td> + <input type="checkbox" :name="name + '_write_permissions[]'" :value="user.id" + v-model="user.write_permissions" + :aria-label="$gettextInterpolate( + $gettext('Schreibzugriff für %{name}'), + {name: user.name} + )"> + </td> + <td class="actions"> + <studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)" + :title="$gettextInterpolate( + $gettext('Kalender nicht mehr mit %{name} teilen'), + {name: user.name} + )"></studip-icon> + </td> + </tr> + </tbody> + </table> + </div> +</template> + +<script> +import StudipMessageBox from "../StudipMessageBox.vue"; + +export default { + name: "calendar-permissions-table", + components: {StudipMessageBox}, + props: { + name: { + type: String, + required: true + }, + selected_users: { + type: Object, + required: false, + default: () => {}, + }, + searchtype: { + type: String, + required: true, + } + }, + data() { + return { + users: {...this.selected_users}, + } + }, + methods: { + addContact(user_id, name) { + this.$set(this.users, user_id, {id: user_id, name: name, write_permissions: false}); + }, + removeContact(user_id) { + if (this.users[user_id] !== undefined) { + this.$delete(this.users, user_id); + } + } + } +} +</script> diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue new file mode 100644 index 0000000000000000000000000000000000000000..05f3c574e93cab6e348c6eff78f82c2e15dbf8f3 --- /dev/null +++ b/resources/vue/components/form_inputs/DateListInput.vue @@ -0,0 +1,98 @@ +<template> + <div class="formpart"> + <div class="sr-only" aria-live="polite" ref="list_message_field"></div> + <ul> + <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="date"> + <input type="hidden" :name="input_name + '[]'" :value="getISODate(date)"> + <studip-date-time :timestamp="Math.floor(date.getTime() / 1000)" :date_only="true"></studip-date-time> + <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate" + class="enter-accessible" aria-role="button" tabindex="0"></studip-icon> + </li> + </ul> + <label> + {{ $gettext('Datum') }} + <div class="flex-row input-with-icon"> + <input type="text" v-model="selected_date_value" ref="date_select_input"> + <studip-icon shape="add" :title="$gettext('Hinzufügen')" @click="addDate" + class="icon enter-accessible button undecorated" aria-role="button" tabindex="0"></studip-icon> + </div> + </label> + </div> +</template> + +<script> +import StudipDateTime from "../StudipDateTime.vue"; +import {$gettext, $gettextInterpolate} from "@/assets/javascripts/lib/gettext"; + +export default { + name: "date-list-input", + components: {StudipDateTime}, + props: { + name: { + type: String, + required: true + }, + selected_dates: { + type: Array, + required: false, + default: () => [], + } + }, + data () { + return { + selected_date_value: STUDIP.DateTime.getStudipDate(new Date(), false, true), + selected_date_list: this.selected_dates.map(date => new Date(date)), + input_name: this.name, + }; + }, + mounted() { + + //Set up the datepicker for the date selector input: + let v = this; + jQuery(this.$refs.date_select_input).datepicker({ + onSelect: () => { + this.selected_date_value = this.$refs.date_select_input.value; + this.addDate(); + }, + }); + }, + watch: { + selected_date_value(new_value) { + this.$emit('selected_date_value', new_value); + }, + selected_date_list: { + handler (new_value) { + this.$emit('selected_date_list', new_value); + }, + deep: true + } + }, + methods: { + addDate() { + if (this.selected_date_value.length < 8) { + //Input too short. + return; + } + let date_parts = this.selected_date_value.split('.'); + if (date_parts.length !== 3) { + //Incorrect input formatting. + return; + } + let reformatted_date = date_parts[2] + '-' + date_parts[1] + '-' + date_parts[0]; + this.selected_date_list.push(new Date(reformatted_date)); + this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} hinzugefügt'), {date: this.selected_date_value}); + }, + removeDate(date_key) { + if (date_key) { + let date = this.selected_date_list.at(date_key); + let formatted_date = STUDIP.DateTime.getStudipDate(date, false, true); + this.selected_date_list.splice(date_key, 1); + this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} entfernt'), {date: formatted_date}); + } + }, + getISODate(date) { + return STUDIP.DateTime.getISODate(date); + } + } +} +</script> diff --git a/resources/vue/components/form_inputs/DayOfWeekSelect.vue b/resources/vue/components/form_inputs/DayOfWeekSelect.vue new file mode 100644 index 0000000000000000000000000000000000000000..c28ddb6db4a435f3c4dbb7c666ebe952dcd9eded --- /dev/null +++ b/resources/vue/components/form_inputs/DayOfWeekSelect.vue @@ -0,0 +1,60 @@ +<template> + <select :name="name" v-model="selected_value"> + <option v-if="with_indeterminate" value="" + :selected="!value"> + {{ $gettext('Bitte wählen') }} + </option> + <option value="1" :selected="value == '1'"> + {{ $gettext('Montag') }} + </option> + <option value="2" :selected="value == '2'"> + {{ $gettext('Dienstag') }} + </option> + <option value="3" :selected="value == '3'"> + {{ $gettext('Mittwoch') }} + </option> + <option value="4" :selected="value == '4'"> + {{ $gettext('Donnerstag') }} + </option> + <option value="5" :selected="value == '5'"> + {{ $gettext('Freitag') }} + </option> + <option value="6" :selected="value == '6'"> + {{ $gettext('Samstag') }} + </option> + <option value="7" :selected="value == '7'"> + {{ $gettext('Sonntag') }} + </option> + </select> +</template> + +<script> +export default { + name: "day-of-week-select", + props: { + name: { + type: String, + required: true + }, + value: { + type: String, + required: false + }, + with_indeterminate: { + type: Boolean, + required: false, + default: false, + } + }, + data () { + return { + selected_value: this.value + }; + }, + watch: { + selected_value(new_value) { + this.$emit('selected_value', new_value); + } + } +} +</script> diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..d2cc2f1aa3f38fb16a533ae08ac1bcfaf00715d4 --- /dev/null +++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue @@ -0,0 +1,166 @@ +<template> + <div class="formpart"> + <label v-if="with_semester_selector"> + {{ $gettext('Semester') }} + <select :name="`${name}_semester_id`" v-model="semester_id"> + <option v-for="semester in available_semesters" + :value="semester.id" + :key="semester.id" + > + {{ semester.name }} + </option> + </select> + </label> + + <table class="default mycourses"> + <caption>{{ semesterName }}</caption> + <colgroup> + <col style="width: 7px"> + <col style="width: 25px"> + <col style="width: 70px"> + <col> + <col> + </colgroup> + <thead> + <tr> + <th></th> + <th></th> + <th>{{ $gettext('Nummer') }}</th> + <th>{{ $gettext('Name') }}</th> + <th class="actions">{{ $gettext('Auswahl') }}</th> + </tr> + </thead> + <tbody> + <tr v-for="course of courses" :key="course.id"> + <td :class="`gruppe${course.group}`"></td> + <td> + <img :src="course.avatar_url" alt="" class="my-courses-avatar course-avatar-small"> + </td> + <td>{{ course.number }}</td> + <td>{{ course.name }}</td> + <td class="actions"> + <input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0"> + <input type="checkbox" :name="`${name}_course_ids[${course.id}]`" + value="1" :checked="selected_course_id_list.includes(course.id)" + :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name})"> + </td> + </tr> + <tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0"> + <td colspan="5"> + <studip-message-box>{{ $gettext('Im gewählten Semester stehen keine Veranstaltungen zur Auswahl zur Verfügung.') }}</studip-message-box> + </td> + </tr> + </tbody> + </table> + </div> +</template> + +<script> +import StudipMessageBox from "../StudipMessageBox.vue"; + +export default { + name: 'my-courses-coloured-table', + components: {StudipMessageBox}, + props: { + default_semester_id: { + type: String, + required: true, + }, + selected_course_ids: { + type: Array, + required: false, + default: () => [], + }, + name: { + type: String, + required: false, + default: 'selected_course_ids', + }, + semester_data: { + type: Object, + required: false, + default: () => {}, + } + }, + data() { + //Retrieve all semesters, if the semester selector is present: + let semester_data = this.semester_data; + return { + available_semesters: semester_data, + semester_id: null, + semester_courses: Object.values(semester_data).reduce( + (carry, current) => { + carry[current.id] = []; + return carry; + }, + {} + ), + selected_course_id_list: [...this.selected_course_ids], + with_semester_selector: Object.keys(semester_data).length > 0, + membershipGroups: {}, + loadedSemesters: [], + }; + }, + created() { + this.semester_id = this.default_semester_id; + + STUDIP.jsonapi.GET(`users/${STUDIP.USER_ID}/course-memberships`, { + data: { + 'page[limit]': 1000, + } + }).done((response) => { + this.membershipGroups = Object.values(response.data).reduce( + (carry, current) => { + carry[current.id.split('_')[0]] = current.attributes.group; + return carry; + }, + {} + ); + }) + }, + methods: { + loadSemesterCourses(semester_id) { + if (this.loadedSemesters.includes(semester_id)) { + return; + } + + // The courses have not yet been retrieved. + STUDIP.jsonapi.GET(`users/${STUDIP.USER_ID}/courses`, { + data: { + 'fields[courses]': 'id,course-number,title,course-type', + 'filter[semester]': semester_id, + 'include': 'memberships', + } + }).done((response) => { + this.semester_courses[semester_id] = response.data + .filter(item => item.type === 'courses') + .map(item => ({ + id: item.id, + name: item.attributes.title, + number: item.attributes['course-number'] ?? '', + group: this.membershipGroups[item.id] ?? item.attributes['course-type'], + avatar_url: item.meta.avatar.small, + })); + + this.loadedSemesters.push(semester_id); + }); + } + }, + computed: { + courses() { + return [...this.semester_courses[this.semester_id]].sort((a, b) => { + return a.name.localeCompare(b.name) + || a.number.localeCompare(b.number); + }); + }, + semesterName() { + return this.available_semesters[this.semester_id].name ?? ''; + }, + }, + watch: { + semester_id(current) { + this.loadSemesterCourses(current); + } + } +} +</script> diff --git a/resources/vue/components/form_inputs/RepetitionInput.vue b/resources/vue/components/form_inputs/RepetitionInput.vue new file mode 100644 index 0000000000000000000000000000000000000000..ef53ffb8cd2dd3963b3aced7bc72ba3fe684819b --- /dev/null +++ b/resources/vue/components/form_inputs/RepetitionInput.vue @@ -0,0 +1,364 @@ +<template> + <div class="formpart"> + <section> + <label>{{ $gettext('Art der Wiederholung') }} + <select :name="name + '_type'" v-model="repetition_type_value"> + <option value="" :selected="!repetition_type_value"> + {{ $gettext('Keine Wiederholung') }} + </option> + <option value="DAILY" :selected="repetition_type_value === 'DAILY'"> + {{ $gettext('Tägliche Wiederholung') }} + </option> + <option value="WORKDAYS" :selected="repetition_type_value === 'WORKDAYS'"> + {{ $gettext('Wiederholung an jedem Werktag') }} + </option> + <option value="WEEKLY" :selected="repetition_type_value === 'WEEKLY'"> + {{ $gettext('Wöchentliche Wiederholung') }} + </option> + <option value="MONTHLY" :selected="repetition_type_value === 'MONTHLY'"> + {{ $gettext('Monatliche Wiederholung') }} + </option> + <option value="YEARLY" :selected="repetition_type_value === 'YEARLY'"> + {{ $gettext('Jährliche Wiederholung') }} + </option> + </select> + </label> + </section> + <section v-if="repetition_type_value === 'DAILY'"> + <label> + {{ $gettext('Abstand in Tagen') }} + <input type="number" min="1" :name="name + '_interval'" + v-model="repetition_interval_value"> + </label> + </section> + <section v-else-if="repetition_type_value === 'WEEKLY'"> + <label> + {{ $gettext('Abstand in Wochen') }} + <input type="number" min="1" :name="name + '_interval'" + v-model="repetition_interval_value"> + </label> + <div> + <p>{{ $gettext('Wiederholung an bestimmten Wochentagen') }}</p> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="1" :checked="repetition_dow_value.includes('1')"> + {{ $gettext('Montag') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="2" :checked="repetition_dow_value.includes('2')"> + {{ $gettext('Dienstag') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="3" :checked="repetition_dow_value.includes('3')"> + {{ $gettext('Mittwoch') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="4" :checked="repetition_dow_value.includes('4')"> + {{ $gettext('Donnerstag') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="5" :checked="repetition_dow_value.includes('5')"> + {{ $gettext('Freitag') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="6" :checked="repetition_dow_value.includes('6')"> + {{ $gettext('Samstag') }} + </label> + <label> + <input type="checkbox" :name="name + '_dow[]'" + value="7" :checked="repetition_dow_value.includes('7')"> + {{ $gettext('Sonntag') }} + </label> + </div> + </section> + <section v-else-if="repetition_type_value === 'YEARLY'"> + <label> + {{ $gettext('Abstand in Jahren') }} + <input type="number" min="1" :name="name + '_interval'" + v-model="repetition_interval_value"> + </label> + <label> + {{ $gettext('Art der jährlichen Wiederholung') }} + <select :name="name + '_month_type'" + v-model="repetition_month_type_value"> + <option value="dom" + :selected="repetition_month_type_value === 'dom'"> + {{ $gettext('Wiederholung an einem bestimmten Datum') }} + </option> + <option value="dow" + :selected="repetition_month_type_value === 'dow'"> + {{ $gettext('Wiederholung an einem bestimmten Wochentag') }} + </option> + </select> + </label> + <label v-if="repetition_month_type_value === 'dom'"> + {{ $gettext('Tag') }} + <input type="number" :name="name + '_dom'" min="1" max="31" v-model="repetition_dom_value"> + </label> + <label> + {{ $gettext('Monat') }} + <select :name="name + '_month'" + v-model="repetition_month_value"> + <option value="1" :selected="repetition_month_value === 1"> + {{ $gettext('Januar') }} + </option> + <option value="2" :selected="repetition_month_value === 2"> + {{ $gettext('Februar') }} + </option> + <option value="3" :selected="repetition_month_value === 3"> + {{ $gettext('März') }} + </option> + <option value="4" :selected="repetition_month_value === 4"> + {{ $gettext('April') }} + </option> + <option value="5" :selected="repetition_month_value === 5"> + {{ $gettext('Mai') }} + </option> + <option value="6" :selected="repetition_month_value === 6"> + {{ $gettext('Juni') }} + </option> + <option value="7" :selected="repetition_month_value === 7"> + {{ $gettext('Juli') }} + </option> + <option value="8" :selected="repetition_month_value === 8"> + {{ $gettext('August') }} + </option> + <option value="9" :selected="repetition_month_value === 9"> + {{ $gettext('September') }} + </option> + <option value="10" :selected="repetition_month_value === 10"> + {{ $gettext('Oktober') }} + </option> + <option value="11" :selected="repetition_month_value === 11"> + {{ $gettext('November') }} + </option> + <option value="12" :selected="repetition_month_value === 12"> + {{ $gettext('Dezember') }} + </option> + </select> + </label> + </section> + <section v-if="repetition_type_value === 'MONTHLY'"> + <label> + {{ $gettext('Abstand in Monaten') }} + <input type="number" min="1" :name="name + '_interval'" + v-model="repetition_interval_value"> + </label> + <label> + {{ $gettext('Art der monatlichen Wiederholung') }} + <select :name="name + '_month_type'" + v-model="repetition_month_type_value"> + <option value="dom" :selected="repetition_month_type_value === 'dom'"> + {{ $gettext('Wiederholung an einem bestimmten Tag des Monats') }} + </option> + <option value="dow" :selected="repetition_month_type_value === 'dow'"> + {{ $gettext('Wiederholung an einem bestimmten Wochentag') }} + </option> + </select> + </label> + </section> + <section v-if="repetition_type_value === 'MONTHLY' && repetition_month_type_value === 'dom'"> + <label> + {{ $gettext('Wiederholung am einem bestimmten Tag des Monats:') }} + <input type="number" min="1" :name="name + '_dom'" + v-model="repetition_dom_value"> + </label> + </section> + <section v-if="['MONTHLY', 'YEARLY'].includes(repetition_type_value) && repetition_month_type_value === 'dow'"> + <label> + {{ $gettext('Wiederholung an einem bestimmten Wochentag:') }} + <day-of-week-select :name="name + '_dow'" v-model="repetition_dow_value[0]" + :with_indeterminate="true"></day-of-week-select> + </label> + <label> + {{ $gettext('Wann im Monat soll die Wiederholung stattfinden?') }} + <select :name="name + '_dow_week'"> + <option value="" :selected="!repetition_dow_week_value"> + {{ $gettext('Bitte wählen') }} + </option> + <option value="1" :selected="repetition_dow_week_value === 1"> + {{ $gettext('Am ersten gewählten Wochentag') }} + </option> + <option value="2" :selected="repetition_dow_week_value === 2"> + {{ $gettext('Am zweiten gewählten Wochentag') }} + </option> + <option value="3" :selected="repetition_dow_week_value === 3"> + {{ $gettext('Am dritten gewählten Wochentag') }} + </option> + <option value="4" :selected="repetition_dow_week_value === 4"> + {{ $gettext('Am vierten gewählten Wochentag') }} + </option> + <option value="-1" :selected="repetition_dow_week_value === -1"> + {{ $gettext('Am letzten gewählten Wochentag') }} + </option> + </select> + </label> + </section> + + <section v-if="repetition_type_value"> + <label> + {{ $gettext('Ende der Wiederholung') }} + <select :name="name + '_rep_end_type'" + v-model="repetition_end_type_value"> + <option value="" :selected="!repetition_end_type_value"> + {{ $gettext('Nie') }} + </option> + <option value="end_date" :selected="repetition_end_type_value === 'end_date'"> + {{ $gettext('An einem bestimmten Datum') }} + </option> + <option value="end_count" :selected="repetition_end_type_value === 'end_count'"> + {{ $gettext('Nach einer Anzahl von Terminen') }} + </option> + </select> + </label> + </section> + <section v-if="repetition_end_type_value === 'end_date'"> + <label> + {{ $gettext('Enddatum') }} + <input type="text" :name="name + '_rep_end_date'" + data-date-picker v-model="repetition_end_date_value"> + </label> + </section> + <section v-else-if="repetition_end_type_value === 'end_count'"> + <label> + {{ $gettext('Anzahl der Termine') }} + <input type="number" min="1" :name="name + '_number_of_dates'" + v-model="number_of_dates_value"> + </label> + </section> + </div> +</template> + +<script> +export default { + name: "repetition-input", + props: { + name: { + type: String, + required: true + }, + default_date: { + type: String, + required: true + }, + repetition_type: { + type: String, + required: true + }, + repetition_interval: { + type: String, + required: true + }, + repetition_dow: { + type: Array, + required: true + }, + repetition_dow_week: { + type: Number, + required: true + }, + repetition_month: { + type: Number, + required: true + }, + repetition_month_type: { + type: String, + required: false + }, + repetition_dom: { + type: Number, + required: true + }, + repetition_end_type: { + type: String, + required: false + }, + repetition_end_date: { + type: String, + required: true + }, + number_of_dates: { + type: Number, + required: true + } + }, + data () { + return { + repetition_type_value: '', + repetition_interval_value: 1, + repetition_dow_value: [], + repetition_dow_week_value: 0, + repetition_month_type_value: '', + repetition_month_value: 0, + repetition_dom_value: 0, + repetition_end_type_value: '', + repetition_end_date_value: '', + number_of_dates_value: 0 + }; + }, + mounted () { + this.repetition_type_value = this.repetition_type; + this.repetition_interval_value = this.repetition_interval; + this.repetition_dow_value = this.repetition_dow; + this.repetition_dow_week_value = this.repetition_dow_week; + if (this.repetition_month_type === undefined) { + this.repetition_month_type_value = this.repetition_dow.length > 0 ? 'dow' : 'dom'; + } else { + this.repetition_month_type_value = this.repetition_month_type; + } + + this.repetition_month_value = this.repetition_month; + this.repetition_dom_value = this.repetition_dom; + this.repetition_end_type_value = ''; + if (this.repetition_end_type !== undefined) { + this.repetition_end_type_value = this.repetition_end_type; + } else if (this.number_of_dates > 1) { + this.repetition_end_type_value = 'end_count'; + } else if (this.repetition_end_date) { + this.repetition_end_type_value = 'end_date'; + } + this.repetition_end_date_value = this.repetition_end_date; + this.number_of_dates_value = this.number_of_dates; + }, + watch: { + repetition_type_value(new_value) { + this.$emit('input_repetition_type', new_value); + }, + repetition_interval_value(new_value) { + this.$emit('input_repetition_interval', new_value); + }, + repetition_dow_value: { + handler(new_value) { + this.$emit('input_repetition_dow', new_value); + }, + deep: true, + }, + repetition_dow_week_value(new_value) { + this.$emit('input_repetition_dow_week', new_value); + }, + repetition_month_type_value(new_value) { + this.$emit('input_repetition_month_type', new_value); + }, + repetition_month_value(new_value) { + this.$emit('input_repetition_month', new_value); + }, + repetition_dom_value(new_value) { + this.$emit('input_repetition_dom', new_value); + }, + repetition_end_type_value(new_value) { + this.$emit('input_repetition_end_type', new_value); + }, + repetition_end_date_value(new_value) { + this.$emit('input_repetition_end_date', new_value); + }, + number_of_dates_value(new_value) { + this.$emit('input_number_of_dates', new_value); + } + } +} +</script> diff --git a/templates/forms/date_list_input.php b/templates/forms/date_list_input.php new file mode 100644 index 0000000000000000000000000000000000000000..2f759ad1d961099b66af2a2e46c1302b31885ac1 --- /dev/null +++ b/templates/forms/date_list_input.php @@ -0,0 +1,3 @@ +<date-list-input + v-model="<?= htmlReady($name) ?>" + <?= arrayToHtmlAttributes($vue_attributes) ?>></date-list-input> diff --git a/templates/forms/selected_ranges_input.php b/templates/forms/selected_ranges_input.php new file mode 100644 index 0000000000000000000000000000000000000000..03ebf5dd25930da248a9cb0917265b79c1523236 --- /dev/null +++ b/templates/forms/selected_ranges_input.php @@ -0,0 +1,7 @@ +<editable-list name="<?= htmlReady($this->name) ?>" + quicksearch="<?= htmlReady((string) $searchtype) ?>" + :items="<?= htmlReady(json_encode($selected_items)) ?>" + :selectable="<?= htmlReady(json_encode($selectable)) ?>" + :category_order="<?= htmlReady(json_encode($category_order)) ?>" + @input="output => <?= htmlReady($this->name) ?> = output"> +</editable-list> diff --git a/templates/sidebar/date-select-widget.php b/templates/sidebar/date-select-widget.php new file mode 100644 index 0000000000000000000000000000000000000000..a85c44d11b92d8dc58e67263f26313197a9ba14b --- /dev/null +++ b/templates/sidebar/date-select-widget.php @@ -0,0 +1,13 @@ +<form method="post" name="date_select_form" class="default"> + <input type="text" id="date_select" + name="date_select" + value="<?= $date->format('d.m.Y') ?>" + data-date-picker + <? + if ($calendar_control) { + echo 'data-calendar-control'; + } else { + echo 'onchange="jQuery(this).closest(\'form\').submit()"'; + } + ?>> +</form> diff --git a/tests/jsonapi/UserEventsIcalTest.php b/tests/jsonapi/UserEventsIcalTest.php index 81230d457c65d13c44f6ccb6c5fd2ab55f7ebe4f..c679f498251c019c0c8cdfd8b6c91b8da6164d38 100644 --- a/tests/jsonapi/UserEventsIcalTest.php +++ b/tests/jsonapi/UserEventsIcalTest.php @@ -23,16 +23,18 @@ class UserEventsIcalTest extends \Codeception\Test\Unit { $credentials = $this->tester->getCredentialsForTestAutor(); - $calendar = new \SingleCalendar($credentials['id']); - $event = $calendar->getNewEvent(); - $event->setTitle('blypyp'); - - $oldUser = $GLOBALS['user'] ?? null; - $GLOBALS['user'] = \User::find($credentials['id']); - - $calendar->storeEvent($event, [$credentials['id']]); - - $GLOBALS['user'] = $oldUser; + $event = new \CalendarDate(); + $event->setId($event->getNewId()); + $now = time(); + $event->begin = $now; + $event->end = $now + 3600; + $event->title = 'blypyp'; + $event->store(); + $calendar_date = new \CalendarDateAssignment(); + $calendar_date->setId([$credentials['id'], $event->getId()]); + $calendar_date->calendar_date = $event; + $calendar_date->suppress_mails = true; + $calendar_date->store(); $app = $this->tester->createApp($credentials, 'get', '/users/{id}/events.ics', UserEventsIcal::class); diff --git a/tests/jsonapi/UserEventsIndexTest.php b/tests/jsonapi/UserEventsIndexTest.php index 0941f09b664b45decb9b11032feb16716f395e34..ac0747183b6dca9a658f421811c79387c548df4f 100644 --- a/tests/jsonapi/UserEventsIndexTest.php +++ b/tests/jsonapi/UserEventsIndexTest.php @@ -55,14 +55,16 @@ class UserEventsIndexTest extends \Codeception\Test\Unit private function createEvent($credentials) { - $calendar = new \SingleCalendar($credentials['id']); - $event = $calendar->getNewEvent(); - - $oldUser = $GLOBALS['user']; - $GLOBALS['user'] = \User::find($credentials['id']); - - $calendar->storeEvent($event, [$credentials['id']]); - - $GLOBALS['user'] = $oldUser; + $event = new \CalendarDate(); + $event->setId($event->getNewId()); + $now = time(); + $event->begin = $now; + $event->end = $now + 3600; + $event->store(); + $calendar_date = new \CalendarDateAssignment(); + $calendar_date->setId([$credentials['id'], $event->getId()]); + $calendar_date->calendar_date = $event; + $calendar_date->suppress_mails = true; + $calendar_date->store(); } } diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php index 61325efc82c9cbb0e0bb06a4024c2bf7b05e1c25..82ae54e514aa29dc2c319848c0d9865d855a7b4d 100644 --- a/tests/jsonapi/_bootstrap.php +++ b/tests/jsonapi/_bootstrap.php @@ -41,6 +41,7 @@ StudipAutoloader::register(); // General classes folders StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models'); +StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models/calendar'); StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/models/resources'); StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/classes'); StudipAutoloader::addAutoloadPath($GLOBALS['STUDIP_BASE_PATH'].'/lib/classes', 'Studip');