Select Git revision
schedule.php
Forked from
Stud.IP / Stud.IP
802 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
schedule.php 26.85 KiB
<?php
/**
* schedule.php - Calender schedule controller
*
* 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
* @package calender
* @since 6.0
*/
class Calendar_ScheduleController extends AuthenticatedController
{
public function before_filter(&$action, &$args)
{
parent::before_filter($action, $args);
if (!Context::isCourse() && Navigation::hasItem('/calendar')) {
Navigation::activateItem('/calendar');
}
}
public function index_action()
{
PageLayout::setTitle(_('Stundenplan'));
if (Navigation::hasItem('/calendar/schedule')) {
Navigation::activateItem('/calendar/schedule');
}
$show_hidden = Request::bool('show_hidden', false);
//Handle the selected semester and create a Fullcalendar instance.
$this->semester = null;
if (Request::submitted('semester_id')) {
$this->semester = Semester::find(Request::option('semester_id'));
if ($this->semester) {
//Store the new semester-ID in the session:
$_SESSION['schedule_semester_id'] = $this->semester->id;
}
}
if (!$this->semester) {
//Load the semester from the session:
$semester_id = $_SESSION['schedule_semester_id'] ?? '';
if ($semester_id) {
$this->semester = Semester::find($semester_id);
} else {
$this->semester = Semester::findCurrent();
}
}
//Build the sidebar:
$sidebar = Sidebar::get();
//Add the semester selector widget first:
$semester_widget = new SemesterSelectorWidget(
$this->indexURL(['show_hidden' => $show_hidden ?: null])
);
$semester_widget->setSelection($semester->id ?? '');
$sidebar->addWidget($semester_widget);
//Then add the actions for the action widget:
$actions = new ActionsWidget();
$actions->addLink(
_('Neuer Termin'),
$this->url_for('calendar/schedule/entry/add'),
Icon::create('add'),
['data-dialog' => 'size=auto']
);
if ($show_hidden) {
$actions->addLink(
_('Ausgeblendete Veranstaltungen verstecken'),
$this->indexURL(['semester_id' => Request::get('semester_id')]),
Icon::create('visibility-visible')
)->asButton();
} else {
$actions->addLink(
_('Ausgeblendete Veranstaltungen anzeigen'),
$this->indexURL([
'show_hidden' => true,
'semester_id' => Request::get('semester_id'),
]),
Icon::create('visibility-invisible')
)->asButton();
}
$actions->addLink(
_('Drucken'),
'#',
Icon::create('print'),
['onclick' => 'window.print(); return false;']
);
$actions->addLink(
_('Einstellungen'),
$this->url_for('calendar/schedule/settings'),
Icon::create('settings'),
['data-dialog' => 'size=auto;reload-on-close']
);
$sidebar->addWidget($actions);
$schedule_settings = UserConfig::get()->getValue('SCHEDULE_SETTINGS');
$size = $schedule_settings['size'] ?? 'medium';
if (Request::submitted('size')) {
$size = Request::option('size');
if (in_array($size, ['small', 'medium', 'large'])) {
//Set the new size in the schedule settings:
$schedule_settings['size'] = $size;
UserConfig::get()->store('SCHEDULE_SETTINGS', $schedule_settings);
} else {
$size = 'medium';
}
}
$views = new ViewsWidget();
$views->setTitle(_('Größe'));
$views->addLink(
_('Klein'),
$this->url_for('calendar/schedule/index', ['size' => 'small'])
)->setActive($size === 'small');
$views->addLink(
_('Mittel'),
$this->url_for('calendar/schedule/index', ['size' => 'medium'])
)->setActive($size === 'medium');
$views->addLink(
_('Groß'),
$this->url_for('calendar/schedule/index', ['size' => 'large'])
)->setActive($size === 'large');
$sidebar->addWidget($views);
$fullcalendar = \Studip\Calendar\Helper::getScheduleFullcalendar(
$semester->id ?? '',
Request::bool('show_hidden', false)
);
$fullcalendar->setResponsiveDefaultView('timeGridDay');
$this->fullcalendar = $fullcalendar->render();
}
public function data_action()
{
//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!');
}
$result = [];
$semester_id = Request::option('semester_id');
$semester = Semester::find($semester_id);
$show_hidden = Request::bool('show_hidden', false);
if ($semester) {
//Get all regular course dates for that semester:
$cycle_dates = SeminarCycleDate::findBySql(
'JOIN `termine` USING (`metadate_id`)
JOIN `seminare` USING (`seminar_id`)
WHERE
`seminar_id` IN (
SELECT `seminar_id` FROM `seminar_user`
WHERE `user_id` = :user_id
UNION
SELECT `course_id` FROM `schedule_courses`
WHERE `user_id` = :user_id
)
AND
(
`termine`.`date` BETWEEN :begin AND :end
OR `termine`.`end_time` BETWEEN :begin AND :end
)
GROUP BY `metadate_id`',
[
'user_id' => $GLOBALS['user']->id,
'begin' => $semester->beginn,
'end' => $semester->ende
]
);
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(
$start_time_parts[0],
$start_time_parts[1],
$start_time_parts[2]
);
$fake_end->setTime(
$end_time_parts[0],
$end_time_parts[1],
$end_time_parts[2]
);
$schedule_course = ScheduleCourseDate::findOneBySQL(
'`course_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $cycle_date->seminar_id,
'user_id' => $GLOBALS['user']->id
]
);
$is_hidden = $schedule_course && !$schedule_course->visible;
if (!$show_hidden && $is_hidden) {
//The regular date belongs to a course that has been hidden in the schedule.
//The flag to include hidden courses is not set which means that the regular
//date shall not be included.
continue;
}
//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 = ['schedule'];
$event_title = $cycle_date->course->getFullName('number-name');
if ($course_membership) {
$event_classes[] = sprintf('course-color-%u', $course_membership->gruppe);
$lecturer_names = array_map(
fn($lecturer) => $lecturer->user->nachname,
CourseMember::findByCourseAndStatus($course_membership->seminar_id, 'dozent')
);
sort($lecturer_names);
$event_title = studip_interpolate(
'%{course_name} (%{lecturer_names})',
[
'course_name' => $cycle_date->course->getFullName('number-name'),
'lecturer_names' => implode(', ', $lecturer_names)
]
);
} elseif ($schedule_course) {
$event_classes[] = 'marked-course';
$event_title = studip_interpolate(
_('%{course_name} (vorgemerkt)'),
['course_name' => $cycle_date->course->getFullName()]
);
}
$event_icon = '';
if ($schedule_course && !$course_membership) {
$event_icon = 'tag';
} elseif ($show_hidden && $is_hidden) {
$event_icon = 'visibility-invisible';
$event_classes[] = 'hidden-course';
}
$event = new \Studip\Calendar\EventData(
$fake_begin,
$fake_end,
$event_title,
$event_classes,
'',
'',
false,
'SeminarCycleDate',
$cycle_date->id,
'',
'',
'course',
$cycle_date->seminar_id,
[
'show' => $this->url_for('calendar/schedule/course_info/' . $cycle_date->seminar_id)
],
[],
$event_icon ?: ''
);
$result[] = $event->toFullcalendarEvent();
}
}
//Add all schedule entries to the result set:
$weekly_dates = ScheduleEntry::findByUser_id($GLOBALS['user']->id);
foreach ($weekly_dates as $date) {
$event_data = $date->toEventData($GLOBALS['user']->id);
//Disable fullcalendar drag & drop actions:
$event_data->editable = false;
$result[] = $event_data->toFullcalendarEvent();
}
$this->render_json($result);
}
/**
* This action handles adding and editing schedule entries.
*
* @param string $entry_id The ID of the entry to be modified. In case the ID is set to "add", a new entry
* will be created. In all other cases, an existing entry will be loaded.
*/
public function entry_action(string $entry_id)
{
$this->entry = null;
if ($entry_id === 'add') {
//Add mode
$this->entry = new ScheduleEntry();
$this->entry->user_id = $GLOBALS['user']->id;
if (!Request::submitted('save')) {
//Provide good default values:
$this->entry->colour_id = 1;
if (Request::submitted('start')) {
//String format
$this->entry->dow = Request::int('dow',date('N'));
$this->entry->setFormattedStart(Request::get('start', date('H:00', strtotime('+1 hour'))));
$this->entry->setFormattedEnd(Request::get('end', date('H:00', strtotime('+2 hours'))));
} elseif (Request::submitted('begin')) {
//Fullcalendar: Timestamps
$begin = Request::get('begin');
$end = Request::get('end');
if ($begin && $end) {
$this->entry->dow = intval(date('N', $begin));
$this->entry->setFormattedStart(date('H:i', $begin));
$this->entry->setFormattedEnd(date('H:i', $end));
}
} else {
$begin = time() + 3600;
$end = $begin + 3600;
$this->entry->dow = intval(date('N', $begin));
$this->entry->setFormattedStart(date('H:00', $begin));
$this->entry->setFormattedEnd(date('H:00', $end));
}
}
PageLayout::setTitle(_('Neuer Termin'));
} else {
//Edit mode
$this->entry = ScheduleEntry::find($entry_id);
if (!$this->entry) {
PageLayout::postError(_('Der Termin wurde nicht gefunden.'));
}
if (!$this->entry->isWritable($GLOBALS['user']->id)) {
throw new AccessDeniedException(_('Sie dürfen diesen Termin nicht bearbeiten!'));
}
PageLayout::setTitle($this->entry->toString());
}
if (Request::submitted('save')) {
CSRFProtection::verifyUnsafeRequest();
$this->saveEntry($entry_id);
} elseif (Request::submitted('delete')) {
CSRFProtection::verifyUnsafeRequest();
$this->deleteEntry();
}
}
/**
* Handles storing a schedule entry.
*/
public function save_entry_action(string $entry_id)
{
$this->entry = null;
if ($entry_id === 'add') {
//Add mode
$this->entry = new ScheduleEntry();
$this->entry->user_id = $GLOBALS['user']->id;
PageLayout::setTitle(_('Neuer Termin'));
} else {
//Edit mode
$this->entry = ScheduleEntry::find($entry_id);
if (!$this->entry) {
PageLayout::postError(_('Der Termin wurde nicht gefunden.'));
}
if (!$this->entry->isWritable($GLOBALS['user']->id)) {
throw new AccessDeniedException(_('Sie dürfen diesen Termin nicht bearbeiten!'));
}
PageLayout::setTitle($this->entry->toString());
}
$this->entry->dow = Request::int('dow', date('N'));
$this->entry->setFormattedStart(Request::get('start'));
$this->entry->setFormattedEnd(Request::get('end'));
$this->entry->colour_id = Request::get('colour_id') ?? '';
$this->entry->label = Request::get('label', '');
$this->entry->content = Request::get('content', '');
if ($this->entry->start_time >= $this->entry->end_time) {
PageLayout::postError(_('Der Startzeitpunkt darf nicht nach dem Endzeitpunkt liegen!'));
$this->redirect('calendar/schedule/entry/' . $entry_id);
return;
}
if ($this->entry->store() !== false) {
if ($entry_id === 'add') {
PageLayout::postSuccess(_('Der Termin wurde hinzugefügt.'));
} else {
PageLayout::postSuccess(_('Der Termin wurde bearbeitet.'));
}
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
} else {
if ($entry_id === 'add') {
PageLayout::postError(_('Der Termin konnte nicht hinzugefügt werden.'));
} else {
PageLayout::postError(_('Der Termin konnte nicht bearbeitet werden.'));
}
$this->redirect('calendar/schedule/entry/' . $entry_id);
}
$this->render_nothing();
}
/**
* Handles deleting a schedule entry.
*/
public function delete_entry_action(string $entry_id)
{
CSRFProtection::verifyUnsafeRequest();
$this->entry = ScheduleEntry::find($entry_id);
if (!$this->entry) {
PageLayout::postError(_('Der Termin wurde nicht gefunden.'));
}
if (!$this->entry->isWritable($GLOBALS['user']->id)) {
throw new AccessDeniedException(_('Sie dürfen diesen Termin nicht bearbeiten!'));
}
if ($this->entry->delete()) {
PageLayout::postSuccess(_('Der Termin wurde gelöscht.'));
} else {
PageLayout::postError(_('Der Termin konnte nicht gelöscht werden.'));
}
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
$this->render_nothing();
}
/**
* Displays information about a course in the schedule.
*
* @param string $course_id The ID of the course.
*/
public function course_info_action(string $course_id)
{
$this->course = Course::find($course_id);
if (!$this->course) {
PageLayout::postError(_('Die Veranstaltung wurde nicht gefunden.'));
return;
}
$this->membership = CourseMember::findOneBySQL(
'`seminar_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $this->course->id,
'user_id' => $GLOBALS['user']->id
]
);
$this->schedule_course_entry = ScheduleCourseDate::findOneBySQL(
'`course_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $this->course->id,
'user_id' => $GLOBALS['user']->id
]
);
PageLayout::setTitle($this->course->getFullName());
}
/**
* Hides a course in the schedule.
*
* @param string $course_id The ID of the course.
*/
public function hide_course_action(string $course_id)
{
CSRFProtection::verifyUnsafeRequest();
$success = false;
$course = Course::find($course_id);
if ($course) {
$this->membership = CourseMember::findOneBySQL(
'`seminar_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $course->id,
'user_id' => $GLOBALS['user']->id
]
);
//Hide the course.
if ($this->membership) {
//Hide the course in the schedule by creating a new schedule course entry
//with the visibility set to 0:
$entry = ScheduleCourseDate::findOneBySQL(
'`user_id` = :user_id AND `course_id` = :course_id',
['user_id' => $GLOBALS['user']->id, 'course_id' => $course->id]
);
if (!$entry) {
$entry = new ScheduleCourseDate();
$entry->user_id = $GLOBALS['user']->id;
$entry->course_id = $course->id;
$entry->metadate_id = '';
}
$entry->visible = false;
$success = $entry->store() !== false;
} else {
//Remove the entry of the marked course from the schedule.
$success = ScheduleCourseDate::deleteBySQL(
'`user_id` = :user_id AND `course_id` = :course_id',
['user_id' => $GLOBALS['user']->id, 'course_id' => $course->id]
) > 0;
}
}
if ($success) {
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
}
$this->render_nothing();
}
/**
* Makes a hidden course visible again in the schedule.
*
* @param string $course_id The ID of the course.
*/
public function show_course_action(string $course_id)
{
CSRFProtection::verifyUnsafeRequest();
$success = false;
$course = Course::find($course_id);
if ($course) {
//Make a hidden course visible again.
$entry = ScheduleCourseDate::findOneBySQL(
'`user_id` = :user_id AND `course_id` = :course_id',
['user_id' => $GLOBALS['user']->id, 'course_id' => $course_id]
);
if ($entry) {
$entry->visible = true;
$success = $entry->store() !== false;
} else {
$success = true;
}
//In case no entry exists, the course is not hidden since an entry in schedule_courses
//must exist with its visible set to zero to make a course disappear from the schedule.
}
if ($success) {
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
}
$this->render_nothing();
}
/**
* Saves the data that are specific to displaying a course in the schedule.
* Currently, this means saving only the colour of the course.
*
* @param string $course_id The ID of the course.
*/
public function save_course_info_action(string $course_id)
{
CSRFProtection::verifyUnsafeRequest();
$success = false;
$course = Course::find($course_id);
if ($course) {
$this->membership = CourseMember::findOneBySQL(
'`seminar_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $course->id,
'user_id' => $GLOBALS['user']->id
]
);
if (!$this->membership) {
throw new AccessDeniedException();
}
//Save the selected group.
$selected_groups = Request::getArray('gruppe');
if (array_key_exists($course->id, $selected_groups)) {
$this->membership->gruppe = $selected_groups[$course->id] ?? '0';
}
$success = $this->membership->store() !== false;
}
if ($success) {
PageLayout::postSuccess(_('Die Farbe der Veranstaltung wurde geändert.'));
} else {
PageLayout::postError(_('Die Farbe der Veranstaltung konnte nicht geändert werden.'));
}
if ($success) {
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
}
$this->render_nothing();
}
public function mark_course_action(string $course_id)
{
$course = Course::find($course_id);
if ($course->isStudygroup()) {
throw new AccessDeniedException();
}
$entry = ScheduleCourseDate::findOneBySQL(
'`course_id` = :course_id AND `user_id` = :user_id',
[
'course_id' => $course_id,
'user_id' => $GLOBALS['user']->id
]
);
if ($entry) {
PageLayout::postInfo(_('Die Veranstaltung wurde bereits zum Stundenplan hinzugefügt.'));
} else {
$entry = new ScheduleCourseDate();
$entry->course_id = $course->id;
$entry->user_id = $GLOBALS['user']->id;
$entry->metadate_id = '';
$entry->visible = true;
if ($entry->store() !== false) {
PageLayout::postSuccess(_('Die Veranstaltung wurde zum Stundenplan hinzugefügt.'));
} else {
PageLayout::postError(_('Die Veranstaltung konnte nicht zum Stundenplan hinzugefügt werden.'));
}
}
$this->redirect('calendar/schedule/index');
}
/**
* Shows the settings dialog for the schedule.
*/
public function settings_action()
{
$user_config = UserConfig::get($GLOBALS['user']->id);
$this->schedule_settings = $user_config->getValue('SCHEDULE_SETTINGS');
//Provide good defaults:
$default_config = [
'start_time' => '08:00',
'end_time' => '20:00',
'weekdays' => 5,
'visible_days' => [1, 2, 3, 4, 5]
];
if (
empty($this->schedule_settings['start_time'])
&& empty($this->schedule_settings['end_time'])
&& empty($this->schedule_settings['weekdays'])
&& empty($this->schedule_settings['visible_days'])
) {
//Use the defaults:
$this->schedule_settings = $default_config;
}
}
/**
* Saves the schedule settings from the settings dialog.
*/
public function save_settings_action()
{
CSRFProtection::verifyUnsafeRequest();
$start_time = Request::get('start_time', '08:00');
$end_time = Request::get('end_time', '20:00');
$weekdays = Request::int('weekdays', 5);
$visible_days = Request::intArray('visible_days');
if ($start_time >= $end_time) {
PageLayout::postError(_('Die Startuhrzeit muss vor der Enduhrzeit liegen.'));
$this->redirect('calendar/schedule/settings');
return;
}
if (!in_array($weekdays, [5, 7])) {
PageLayout::postError(_('Der Stundenplan kann nur 5 oder 7 Tage anzeigen.'));
$this->redirect('calendar/schedule/settings');
return;
}
if (empty($visible_days)) {
PageLayout::postError(_('Es wurde kein Wochentag ausgewählt.'));
$this->redirect('calendar/schedule/settings');
return;
}
$schedule_settings = [
'start_time' => $start_time,
'end_time' => $end_time,
'weekdays' => $weekdays,
'visible_days' => $visible_days
];
UserConfig::get($GLOBALS['user']->id)->store('SCHEDULE_SETTINGS', $schedule_settings);
PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.'));
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', '1');
} else {
$this->redirect('calendar/schedule/index');
}
$this->render_nothing();
}
}