diff --git a/application/controllers/LvPlanCSVDownload.php b/application/controllers/LvPlanCSVDownload.php index bffaa8240..42900b5c1 100644 --- a/application/controllers/LvPlanCSVDownload.php +++ b/application/controllers/LvPlanCSVDownload.php @@ -19,6 +19,8 @@ class LvPlanCSVDownload extends Auth_Controller ]); $this->load->model('person/Benutzer_model', 'BenutzerModel'); + $this->load->model('ressource/Ort_model', 'OrtModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); } /** @@ -29,20 +31,31 @@ class LvPlanCSVDownload extends Auth_Controller public function index() { $type = $this->getType(); - $uid = $this->getUid(); + $uid = getAuthUID(); + $ortKurzbz = null; + $verbandFilters = null; + if ($type === 'ort') + $ortKurzbz = $this->getRoom(); + elseif ($type === 'verband') + $verbandFilters = $this->getVerbandFilters(); + else + { + $uid = $this->getUid(); + $this->authorizeUid($uid); + $this->validateUid($uid); + } $target = $this->getTarget(); list($begin, $ende) = $this->getDateRange(); - $this->authorizeUid($uid); - $this->validateUid($uid); - $this->load->library('LvPlanCSVLib'); $content = $this->lvplancsvlib->getContent( $uid, $begin, $ende, $type, - $target + $target, + $ortKurzbz, + $verbandFilters ); $filename = 'FH-Kalender_'.date('m_Y', $begin); @@ -65,7 +78,7 @@ class LvPlanCSVDownload extends Auth_Controller { $type = $this->input->get('type', true); - if (!in_array($type, array('student', 'lektor'), true)) + if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) show_error('Ungueltiger Parameter: type', 400); return $type; @@ -84,6 +97,80 @@ class LvPlanCSVDownload extends Auth_Controller return $uid; } + /** + * @return string Requested room identifier. + */ + private function getRoom() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz'); + $room = $this->OrtModel->load($ortKurzbz); + if (isError($room)) + show_error(getError($room), 500); + if (!hasData($room)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + return $ortKurzbz; + } + + /** + * @return array Requested Lehrverband filters. + */ + private function getVerbandFilters() + { + $stgKz = $this->getInteger('stg_kz', true); + $semester = $this->getInteger('sem', false); + $verband = $this->getOptionalFilter('ver'); + $gruppe = $this->getOptionalFilter('grp'); + + $this->StudiengangModel->addSelect('studiengang_kz'); + $studiengang = $this->StudiengangModel->load($stgKz); + if (isError($studiengang)) + show_error(getError($studiengang), 500); + if (!hasData($studiengang)) + show_error('Ungueltiger Parameter: stg_kz', 400); + + return array( + 'stg_kz' => $stgKz, + 'sem' => $semester, + 'ver' => $verband, + 'grp' => $gruppe + ); + } + + private function getInteger($name, $signed) + { + $value = $this->input->get($name, true); + $pattern = $signed ? '/^-?\d+$/' : '/^\d+$/'; + if (!is_string($value) || !preg_match($pattern, $value)) + show_error('Ungueltiger Parameter: '.$name, 400); + + $integer = filter_var($value, FILTER_VALIDATE_INT); + if ($integer === false || (!$signed && $integer < 0)) + show_error('Ungueltiger Parameter: '.$name, 400); + + return $integer; + } + + private function getOptionalFilter($name) + { + $value = $this->input->get($name, true); + if ($value === null || $value === '' || $value === '0') + return null; + if (!is_string($value) + || !check_utf8($value) + || mb_strlen($value) > 20 + || !preg_match('/^[[:alnum:]_-]+$/u', $value)) + { + show_error('Ungueltiger Parameter: '.$name, 400); + } + + return $value; + } + /** * @return string|null Requested CSV target. */ diff --git a/application/controllers/LvPlanExcelDownload.php b/application/controllers/LvPlanExcelDownload.php index 2796eefaf..fb71a64e3 100644 --- a/application/controllers/LvPlanExcelDownload.php +++ b/application/controllers/LvPlanExcelDownload.php @@ -19,6 +19,8 @@ class LvPlanExcelDownload extends Auth_Controller ]); $this->load->model('person/Benutzer_model', 'BenutzerModel'); + $this->load->model('ressource/Ort_model', 'OrtModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); } /** @@ -29,18 +31,29 @@ class LvPlanExcelDownload extends Auth_Controller public function index() { $type = $this->getType(); - $uid = $this->getUid(); + $uid = getAuthUID(); + $ortKurzbz = null; + $verbandFilters = null; + if ($type === 'ort') + $ortKurzbz = $this->getRoom(); + elseif ($type === 'verband') + $verbandFilters = $this->getVerbandFilters(); + else + { + $uid = $this->getUid(); + $this->authorizeUid($uid); + $this->validateUid($uid); + } list($begin, $ende) = $this->getDateRange(); - $this->authorizeUid($uid); - $this->validateUid($uid); - $this->load->library('LvPlanExcelLib'); $content = $this->lvplanexcellib->getContent( $uid, $begin, $ende, - $type + $type, + $ortKurzbz, + $verbandFilters ); $filename = 'FH-Kalender_'.date('m_Y', $begin).'.xls'; @@ -60,7 +73,7 @@ class LvPlanExcelDownload extends Auth_Controller { $type = $this->input->get('type', true); - if (!in_array($type, array('student', 'lektor'), true)) + if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) show_error('Ungueltiger Parameter: type', 400); return $type; @@ -79,6 +92,80 @@ class LvPlanExcelDownload extends Auth_Controller return $uid; } + /** + * @return string Requested room identifier. + */ + private function getRoom() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz'); + $room = $this->OrtModel->load($ortKurzbz); + if (isError($room)) + show_error(getError($room), 500); + if (!hasData($room)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + return $ortKurzbz; + } + + /** + * @return array Requested Lehrverband filters. + */ + private function getVerbandFilters() + { + $stgKz = $this->getInteger('stg_kz', true); + $semester = $this->getInteger('sem', false); + $verband = $this->getOptionalFilter('ver'); + $gruppe = $this->getOptionalFilter('grp'); + + $this->StudiengangModel->addSelect('studiengang_kz'); + $studiengang = $this->StudiengangModel->load($stgKz); + if (isError($studiengang)) + show_error(getError($studiengang), 500); + if (!hasData($studiengang)) + show_error('Ungueltiger Parameter: stg_kz', 400); + + return array( + 'stg_kz' => $stgKz, + 'sem' => $semester, + 'ver' => $verband, + 'grp' => $gruppe + ); + } + + private function getInteger($name, $signed) + { + $value = $this->input->get($name, true); + $pattern = $signed ? '/^-?\d+$/' : '/^\d+$/'; + if (!is_string($value) || !preg_match($pattern, $value)) + show_error('Ungueltiger Parameter: '.$name, 400); + + $integer = filter_var($value, FILTER_VALIDATE_INT); + if ($integer === false || (!$signed && $integer < 0)) + show_error('Ungueltiger Parameter: '.$name, 400); + + return $integer; + } + + private function getOptionalFilter($name) + { + $value = $this->input->get($name, true); + if ($value === null || $value === '' || $value === '0') + return null; + if (!is_string($value) + || !check_utf8($value) + || mb_strlen($value) > 20 + || !preg_match('/^[[:alnum:]_-]+$/u', $value)) + { + show_error('Ungueltiger Parameter: '.$name, 400); + } + + return $value; + } + /** * Validates and expands the requested range to complete weeks. * diff --git a/application/controllers/LvPlanHTMLPreview.php b/application/controllers/LvPlanHTMLPreview.php index d249951f7..899a25e59 100644 --- a/application/controllers/LvPlanHTMLPreview.php +++ b/application/controllers/LvPlanHTMLPreview.php @@ -19,7 +19,9 @@ class LvPlanHTMLPreview extends Auth_Controller ]); $this->load->model('person/Benutzer_model', 'BenutzerModel'); + $this->load->model('crm/Student_model', 'StudentModel'); $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('ressource/Ort_model', 'OrtModel'); } /** @@ -31,9 +33,12 @@ class LvPlanHTMLPreview extends Auth_Controller { $type = $this->getType(); list($begin, $ende) = $this->getDateRange(); - $filters = $type === 'verband' - ? $this->getVerbandFilters() - : $this->getPersonalFilters(); + if ($type === 'verband') + $filters = $this->getVerbandFilters(); + elseif ($type === 'ort') + $filters = $this->getRoomFilters(); + else + $filters = $this->getPersonalFilters($type); $this->load->library('LvPlanHTMLLib'); $content = $this->lvplanhtmllib->getContent( @@ -56,7 +61,7 @@ class LvPlanHTMLPreview extends Auth_Controller { $type = $this->input->get('type', true); - if (!in_array($type, array('student', 'lektor', 'verband'), true)) + if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true)) show_error('Ungueltiger Parameter: type', 400); return $type; @@ -65,8 +70,10 @@ class LvPlanHTMLPreview extends Auth_Controller /** * @return array Personal preview filters. */ - private function getPersonalFilters() + private function getPersonalFilters($type) { + // Legacy wochenplan calendar links include empty Lehrverband parameters + // for personal plans. They are irrelevant when type is student or lektor. if ($this->hasVerbandParameters()) show_error('Ungueltige Parameterkombination', 400); @@ -80,15 +87,81 @@ class LvPlanHTMLPreview extends Auth_Controller show_error('Forbidden', 403); } - $this->BenutzerModel->addSelect('uid'); + $this->BenutzerModel->addSelect( + 'tbl_benutzer.uid, tbl_person.titelpre, tbl_person.vorname, ' + .'tbl_person.nachname, tbl_person.titelpost' + ); + $this->BenutzerModel->addJoin('public.tbl_person', 'person_id', 'LEFT'); $user = $this->BenutzerModel->load(array($uid)); if (isError($user)) show_error(getError($user), 500); if (!hasData($user)) show_error('Ungueltiger Parameter: pers_uid', 400); + $users = getData($user); + $userData = reset($users); + $nameParts = array(); + foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property) + { + if (isset($userData->{$property}) && trim($userData->{$property}) !== '') + $nameParts[] = trim($userData->{$property}); + } - return array('uid' => $uid); + $filters = array( + 'uid' => $uid, + 'full_name' => implode(' ', $nameParts) + ); + + if ($type === 'student') + { + $this->StudentModel->addSelect( + 'studiengang_kz, semester, verband, gruppe' + ); + $student = $this->StudentModel->load(array($uid)); + + if (isError($student)) + show_error(getError($student), 500); + if (!hasData($student)) + show_error('Ungueltiger Parameter: pers_uid', 400); + + $students = getData($student); + $studentData = reset($students); + $filters['stg_kz'] = $studentData->studiengang_kz; + $filters['sem'] = $studentData->semester; + $filters['ver'] = $studentData->verband; + $filters['grp'] = $studentData->gruppe; + } + + return $filters; + } + + /** + * Legacy room links also carry pers_uid, but the UID does not filter or + * authorize a room plan. Only ort_kurzbz identifies the requested plan. + * + * @return array Room preview filters. + */ + private function getRoomFilters() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz, bezeichnung'); + $room = $this->OrtModel->load($ortKurzbz); + + if (isError($room)) + show_error(getError($room), 500); + if (!hasData($room)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $rooms = getData($room); + $roomData = reset($rooms); + + return array( + 'ort_kurzbz' => $ortKurzbz, + 'bezeichnung' => isset($roomData->bezeichnung) ? $roomData->bezeichnung : '' + ); } /** @@ -96,24 +169,30 @@ class LvPlanHTMLPreview extends Auth_Controller */ private function getVerbandFilters() { - if ($this->input->get('pers_uid', true) !== null) - show_error('Ungueltige Parameterkombination', 400); - + // Legacy wochenplan links always carry pers_uid. It does not filter or + // authorize a Lehrverband plan and is intentionally ignored here. $stgKz = $this->getInteger('stg_kz', true); $semester = $this->getInteger('sem', false); $verband = $this->getOptionalFilter('ver'); $gruppe = $this->getOptionalFilter('grp'); - $this->StudiengangModel->addSelect('studiengang_kz'); + $this->StudiengangModel->addSelect( + 'studiengang_kz, UPPER(typ || kurzbz) AS stg_kurzbz' + ); $studiengang = $this->StudiengangModel->load($stgKz); if (isError($studiengang)) show_error(getError($studiengang), 500); if (!hasData($studiengang)) show_error('Ungueltiger Parameter: stg_kz', 400); + $studiengaenge = getData($studiengang); + $studiengangData = reset($studiengaenge); return array( 'stg_kz' => $stgKz, + 'stg_kurzbz' => isset($studiengangData->stg_kurzbz) + ? $studiengangData->stg_kurzbz + : '', 'sem' => $semester, 'ver' => $verband, 'grp' => $gruppe @@ -121,13 +200,14 @@ class LvPlanHTMLPreview extends Auth_Controller } /** - * @return bool Whether any Lehrverband-only parameters were supplied. + * @return bool Whether any non-empty Lehrverband-only parameters were supplied. */ private function hasVerbandParameters() { foreach (array('stg_kz', 'sem', 'ver', 'grp') as $name) { - if ($this->input->get($name, true) !== null) + $value = $this->input->get($name, true); + if ($value !== null && $value !== '') return true; } diff --git a/application/controllers/LvPlanICALDownload.php b/application/controllers/LvPlanICALDownload.php index d1ca022ca..6a9375220 100644 --- a/application/controllers/LvPlanICALDownload.php +++ b/application/controllers/LvPlanICALDownload.php @@ -19,6 +19,8 @@ class LvPlanICALDownload extends Auth_Controller ]); $this->load->model('person/Benutzer_model', 'BenutzerModel'); + $this->load->model('ressource/Ort_model', 'OrtModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); } /** @@ -29,20 +31,31 @@ class LvPlanICALDownload extends Auth_Controller public function index() { $type = $this->getType(); - $uid = $this->getUid(); + $uid = getAuthUID(); + $ortKurzbz = null; + $verbandFilters = null; + if ($type === 'ort') + $ortKurzbz = $this->getRoom(); + elseif ($type === 'verband') + $verbandFilters = $this->getVerbandFilters(); + else + { + $uid = $this->getUid(); + $this->authorizeUid($uid); + $this->validateUid($uid); + } $version = $this->getVersion(); list($begin, $ende) = $this->getDateRange(); - $this->authorizeUid($uid); - $this->validateUid($uid); - $this->load->library('LvPlanICALLib'); $content = $this->lvplanicallib->getContent( $uid, $begin, $ende, $type, - $version + $version, + $ortKurzbz, + $verbandFilters ); $filename = 'FH-Kalender_'.date('m_Y', $begin).'_ical'.$version.'.ics'; @@ -62,7 +75,7 @@ class LvPlanICALDownload extends Auth_Controller { $type = $this->input->get('type', true); - if (!in_array($type, array('student', 'lektor'), true)) + if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) show_error('Ungueltiger Parameter: type', 400); return $type; @@ -81,6 +94,80 @@ class LvPlanICALDownload extends Auth_Controller return $uid; } + /** + * @return string Requested room identifier. + */ + private function getRoom() + { + $ortKurzbz = $this->input->get('ort_kurzbz', true); + if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + $this->OrtModel->addSelect('ort_kurzbz'); + $room = $this->OrtModel->load($ortKurzbz); + if (isError($room)) + show_error(getError($room), 500); + if (!hasData($room)) + show_error('Ungueltiger Parameter: ort_kurzbz', 400); + + return $ortKurzbz; + } + + /** + * @return array Requested Lehrverband filters. + */ + private function getVerbandFilters() + { + $stgKz = $this->getInteger('stg_kz', true); + $semester = $this->getInteger('sem', false); + $verband = $this->getOptionalFilter('ver'); + $gruppe = $this->getOptionalFilter('grp'); + + $this->StudiengangModel->addSelect('studiengang_kz'); + $studiengang = $this->StudiengangModel->load($stgKz); + if (isError($studiengang)) + show_error(getError($studiengang), 500); + if (!hasData($studiengang)) + show_error('Ungueltiger Parameter: stg_kz', 400); + + return array( + 'stg_kz' => $stgKz, + 'sem' => $semester, + 'ver' => $verband, + 'grp' => $gruppe + ); + } + + private function getInteger($name, $signed) + { + $value = $this->input->get($name, true); + $pattern = $signed ? '/^-?\d+$/' : '/^\d+$/'; + if (!is_string($value) || !preg_match($pattern, $value)) + show_error('Ungueltiger Parameter: '.$name, 400); + + $integer = filter_var($value, FILTER_VALIDATE_INT); + if ($integer === false || (!$signed && $integer < 0)) + show_error('Ungueltiger Parameter: '.$name, 400); + + return $integer; + } + + private function getOptionalFilter($name) + { + $value = $this->input->get($name, true); + if ($value === null || $value === '' || $value === '0') + return null; + if (!is_string($value) + || !check_utf8($value) + || mb_strlen($value) > 20 + || !preg_match('/^[[:alnum:]_-]+$/u', $value)) + { + show_error('Ungueltiger Parameter: '.$name, 400); + } + + return $value; + } + /** * @return int Requested iCalendar major version. */ diff --git a/application/controllers/system/MigrateKalender.php b/application/controllers/system/MigrateKalender.php index 2617ba286..49e895dbc 100644 --- a/application/controllers/system/MigrateKalender.php +++ b/application/controllers/system/MigrateKalender.php @@ -460,21 +460,6 @@ class MigrateKalender extends CLI_Controller } } - $rooms = is_array($block->orte_kurzbz) ? $block->orte_kurzbz : explode(',', $block->orte_kurzbz ?? ''); - foreach ($rooms as $room_kurzbz) - { - $room_kurzbz = trim($room_kurzbz); - if (!empty($room_kurzbz)) - { - $this->KalenderOrtModel->insert( - array ( - 'kalender_id' => $kalender_id, - 'ort_kurzbz' => $room_kurzbz - ) - ); - } - } - foreach ($block->svg_kombis as $kombi_str) { $kombi_str = trim($kombi_str, '()'); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index a3afc8c87..5e6d917e8 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -299,6 +299,20 @@ class KalenderLib return $this->_mapEvents($data); } + /** + * Returns the published timetable for one room without collision checks. + */ + public function getPlanForRoom($start_date, $end_date, $ort) + { + $this->_getBasePlan($start_date, $end_date); + + $this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort); + $this->_ci->KalenderModel->db->where('tbl_kalender.status_kurzbz', 'live'); + $data = $this->_ci->KalenderModel->load(); + + return $this->_mapEvents($data, false); + } + public function getForRaumvorschlag($start_date, $end_date, $lektor_uids = [], $gruppen_kurzbz = [], $lehrverband_gruppen = []) diff --git a/application/libraries/LvPlanCSVLib.php b/application/libraries/LvPlanCSVLib.php index 451dfadac..a59972ab9 100644 --- a/application/libraries/LvPlanCSVLib.php +++ b/application/libraries/LvPlanCSVLib.php @@ -62,16 +62,34 @@ class LvPlanCSVLib * @param string $uid User UID. * @param int $begin First included date as Unix timestamp. * @param int $ende Last included date as Unix timestamp. - * @param string $type Timetable type (student or lektor). + * @param string $type Timetable type (student, lektor, ort or verband). * @param string|null $target Optional CSV target (outlook). + * @param string|null $ortKurzbz Room identifier for room plans. + * @param array|null $verbandFilters Lehrverband filters. * @return string */ - public function getContent($uid, $begin, $ende, $type, $target = null) - { - if (!in_array($type, array('student', 'lektor'), true)) + public function getContent( + $uid, + $begin, + $ende, + $type, + $target = null, + $ortKurzbz = null, + $verbandFilters = null + ) { + if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) throw new InvalidArgumentException('Unsupported timetable type'); if ($target !== null && $target !== self::TARGET_OUTLOOK) throw new InvalidArgumentException('Unsupported CSV target'); + if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === '')) + throw new InvalidArgumentException('Missing room timetable filter'); + if ($type === 'verband' + && (!is_array($verbandFilters) + || !array_key_exists('stg_kz', $verbandFilters) + || !array_key_exists('sem', $verbandFilters))) + { + throw new InvalidArgumentException('Missing Lehrverband timetable filters'); + } $isLecturer = $type === 'lektor'; $this->ci->load->library('KalenderLib', array('uid' => $uid)); @@ -79,7 +97,23 @@ class LvPlanCSVLib $startDate = date('Y-m-d', $begin); $endDate = date('Y-m-d', $ende); - if ($isLecturer) + if ($type === 'verband') + { + $events = $this->ci->kalenderlib->getPlanForVerband( + $startDate, + $endDate, + $verbandFilters['stg_kz'], + $verbandFilters['sem'], + isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null, + isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null + ); + } + elseif ($type === 'ort') + { + $events = $this->ci->kalenderlib + ->getPlanForRoom($startDate, $endDate, $ortKurzbz); + } + elseif ($isLecturer) { $events = $this->ci->kalenderlib ->getPlanForLecturerByLecturer($startDate, $endDate, $uid); diff --git a/application/libraries/LvPlanExcelLib.php b/application/libraries/LvPlanExcelLib.php index b18770e3a..4af18dca2 100644 --- a/application/libraries/LvPlanExcelLib.php +++ b/application/libraries/LvPlanExcelLib.php @@ -35,13 +35,30 @@ class LvPlanExcelLib * @param string $uid User UID. * @param int $begin First included date as Unix timestamp. * @param int $ende Last included date as Unix timestamp. - * @param string $type Timetable type (student or lektor). + * @param string $type Timetable type (student, lektor, ort or verband). + * @param string|null $ortKurzbz Room identifier for room plans. + * @param array|null $verbandFilters Lehrverband filters. * @return string Binary workbook content. */ - public function getContent($uid, $begin, $ende, $type) - { - if (!in_array($type, array('student', 'lektor'), true)) + public function getContent( + $uid, + $begin, + $ende, + $type, + $ortKurzbz = null, + $verbandFilters = null + ) { + if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) throw new InvalidArgumentException('Unsupported timetable type'); + if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === '')) + throw new InvalidArgumentException('Missing room timetable filter'); + if ($type === 'verband' + && (!is_array($verbandFilters) + || !array_key_exists('stg_kz', $verbandFilters) + || !array_key_exists('sem', $verbandFilters))) + { + throw new InvalidArgumentException('Missing Lehrverband timetable filters'); + } $isLecturer = $type === 'lektor'; $this->ci->load->library('KalenderLib', array('uid' => $uid)); @@ -50,7 +67,23 @@ class LvPlanExcelLib $startDate = date('Y-m-d', $begin); $endDate = date('Y-m-d', $ende); - if ($isLecturer) + if ($type === 'verband') + { + $events = $this->ci->kalenderlib->getPlanForVerband( + $startDate, + $endDate, + $verbandFilters['stg_kz'], + $verbandFilters['sem'], + isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null, + isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null + ); + } + elseif ($type === 'ort') + { + $events = $this->ci->kalenderlib + ->getPlanForRoom($startDate, $endDate, $ortKurzbz); + } + elseif ($isLecturer) { $events = $this->ci->kalenderlib ->getPlanForLecturerByLecturer($startDate, $endDate, $uid); diff --git a/application/libraries/LvPlanHTMLLib.php b/application/libraries/LvPlanHTMLLib.php index 6912cfc51..267acac77 100644 --- a/application/libraries/LvPlanHTMLLib.php +++ b/application/libraries/LvPlanHTMLLib.php @@ -6,6 +6,8 @@ if (!defined('BASEPATH')) class LvPlanHTMLLib { const TIMEZONE = 'Europe/Vienna'; + const DETAIL_WINDOW_NAME = 'Details'; + const DETAIL_WINDOW_OPTIONS = 'height=320,width=550,left=0,top=0,hotkeys=0,resizable=yes,status=no,scrollbars=no,toolbar=no,location=no,menubar=no,dependent=yes'; private $ci; @@ -25,12 +27,16 @@ class LvPlanHTMLLib */ public function getContent($begin, $ende, $type, array $filters) { - if (!in_array($type, array('student', 'lektor', 'verband'), true)) + if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true)) throw new InvalidArgumentException('Unsupported timetable type'); - $events = $type === 'verband' - ? $this->getVerbandEvents($begin, $ende, $filters) - : $this->getPersonalEvents($begin, $ende, $type, $filters); + if ($type === 'verband') + $events = $this->getVerbandEvents($begin, $ende, $filters); + elseif ($type === 'ort') + $events = $this->getRoomEvents($begin, $ende, $filters); + else + $events = $this->getPersonalEvents($begin, $ende, $type, $filters); + $hourRaster = $this->loadHourRaster(); $normalizedEvents = $this->normalizeEvents($events); @@ -73,6 +79,28 @@ class LvPlanHTMLLib return is_array($events) ? $events : array(); } + /** + * @return array Room plan events. + */ + private function getRoomEvents($begin, $ende, array $filters) + { + if (!isset($filters['ort_kurzbz']) + || !is_string($filters['ort_kurzbz']) + || $filters['ort_kurzbz'] === '') + { + throw new InvalidArgumentException('Missing room timetable filter'); + } + + $this->ci->load->library('KalenderLib', array('uid' => getAuthUID())); + $events = $this->ci->kalenderlib->getPlanForRoom( + $this->formatLocalDate($begin), + $this->formatLocalDate($ende), + $filters['ort_kurzbz'] + ); + + return is_array($events) ? $events : array(); + } + /** * @return array Lehrverband plan events. */ @@ -168,6 +196,9 @@ class LvPlanHTMLLib isset($event->ort_kurzbz) ? $event->ort_kurzbz : '', ' / ' ), + 'detail_ort_kurzbz' => $this->getFirstScalarValue( + isset($event->ort_kurzbz) ? $event->ort_kurzbz : '' + ), 'lecturers' => $this->getLecturers($event), 'groups' => $this->getGroups($event), 'note' => $this->getEventNote($event, $isReservation), @@ -210,7 +241,7 @@ class LvPlanHTMLLib $html .= '

'.$this->escape($title).'

'; foreach ($weeks as $week) - $html .= $this->renderWeek($week, $hourRaster, $type); + $html .= $this->renderWeek($week, $hourRaster, $type, $filters); if (empty($weeks)) $html .= '

Keine Termine im gewählten Zeitraum.

'; @@ -300,7 +331,7 @@ class LvPlanHTMLLib /** * Renders one printable week. */ - private function renderWeek(array $week, array $raster, $type) + private function renderWeek(array $week, array $raster, $type, array $filters) { $weekdayNames = array( 1 => 'Montag', @@ -331,7 +362,7 @@ class LvPlanHTMLLib $html .= ''.$weekdayNames[$weekday].'
'; $html .= $day['date']->format('d.m.Y').'
'; - foreach ($day['cells'] as $events) + foreach ($day['cells'] as $slotIndex => $events) { if (empty($events)) { @@ -340,8 +371,18 @@ class LvPlanHTMLLib } $html .= ''; + + $hourNumber = isset($raster[$slotIndex]['number']) + ? $raster[$slotIndex]['number'] + : null; foreach ($events as $event) - $html .= $this->renderEvent($event, $type, false); + $html .= $this->renderEvent( + $event, + $type, + false, + $filters, + $hourNumber + ); $html .= ''; } @@ -354,7 +395,15 @@ class LvPlanHTMLLib { $html .= '

Termine außerhalb des Stundenrasters

'; foreach ($week['outside'] as $event) - $html .= $this->renderEvent($event, $type, true); + { + $html .= $this->renderEvent( + $event, + $type, + true, + $filters, + $this->getNearestHourNumber($event, $raster) + ); + } $html .= '
'; } @@ -364,26 +413,40 @@ class LvPlanHTMLLib /** * Renders an escaped event card. */ - private function renderEvent(array $event, $type, $showDate) - { + private function renderEvent( + array $event, + $type, + $showDate, + array $filters, + $hourNumber + ) { $style = $event['color'] !== '' ? ' style="background-color: #'.$event['color'].'; margin-bottom: 3px;"' : ''; $html = '
'; - $html .= ''; + $detailUrl = $this->buildDetailUrl($event, $type, $filters, $hourNumber); + $html .= 'encodeJavaScriptString($detailUrl).',' + .$this->encodeJavaScriptString(self::DETAIL_WINDOW_NAME).',' + .$this->encodeJavaScriptString(self::DETAIL_WINDOW_OPTIONS) + .');return false;'; + $html .= ' href="'.$this->escape($detailUrl).'"'; + $html .= ' target="'.self::DETAIL_WINDOW_NAME.'"'; + $html .= ' onclick="'.$this->escape($onclick).'"'; + } + $html .= '>'; if ($showDate) $html .= $event['start']->format('d.m.Y H:i').'–'.$event['end']->format('H:i').'
'; $html .= $this->escape($event['title']); - if ($event['note'] !== '') - { - $html .= 'escape($event['note']).'" alt="">'; - } - if (($type === 'lektor' || $type === 'verband') && $event['groups'] !== '') - $html .= '
'.$this->escape($event['groups']); + if (($type === 'lektor' || $type === 'verband' || $type === 'ort') + && $event['groups'] !== '') + $html .= '
'.$this->renderLines($event['groups']); if ($type !== 'lektor' && $event['lecturers'] !== '') $html .= '
'.$this->renderLines($event['lecturers']); if ($event['location'] !== '') @@ -394,19 +457,107 @@ class LvPlanHTMLLib return $html.'
'; } + /** + * Builds the URL consumed by the legacy timetable detail popup. + */ + private function buildDetailUrl(array $event, $type, array $filters, $hourNumber) + { + if ($hourNumber === null || !preg_match('/^\d+$/', (string)$hourNumber)) + return null; + + $uid = isset($filters['uid']) && $filters['uid'] !== '' + ? $filters['uid'] + : getAuthUID(); + $room = $type === 'ort' && isset($filters['ort_kurzbz']) + ? $filters['ort_kurzbz'] + : $event['detail_ort_kurzbz']; + $parameters = array( + 'type' => $type, + 'datum' => $event['date'], + 'stunde' => (string)$hourNumber, + 'pers_uid' => $uid, + 'stg_kz' => isset($filters['stg_kz']) ? $filters['stg_kz'] : '', + 'sem' => isset($filters['sem']) ? $filters['sem'] : '', + 'ver' => isset($filters['ver']) && $filters['ver'] !== null + ? $filters['ver'] + : '', + 'grp' => isset($filters['grp']) && $filters['grp'] !== null + ? $filters['grp'] + : '', + 'ort_kurzbz' => $room + ); + + return $this->getAppRoot().'cis/private/lvplan/stpl_detail.php?' + .http_build_query($parameters, '', '&', PHP_QUERY_RFC3986); + } + + /** + * Returns the hour whose configured start is closest to the event start. + */ + private function getNearestHourNumber(array $event, array $raster) + { + if (empty($raster)) + return null; + + $eventStart = $this->timeToSeconds($event['start']->format('H:i:s')); + if ($eventStart === null) + return null; + + $nearestNumber = null; + $nearestDistance = null; + foreach ($raster as $hour) + { + $distance = abs($eventStart - $hour['begin']); + if ($nearestDistance === null || $distance < $nearestDistance) + { + $nearestDistance = $distance; + $nearestNumber = $hour['number']; + } + } + + return $nearestNumber; + } + private function getDocumentTitle($type, array $filters) { - if ($type === 'verband') + if ($type === 'ort') { - $title = 'Lehrverband: '.$filters['stg_kz'].'-'.$filters['sem']; - if (isset($filters['ver']) && $filters['ver'] !== null) - $title .= $filters['ver']; - if (isset($filters['grp']) && $filters['grp'] !== null) - $title .= $filters['grp']; + $title = 'Ort: '.$filters['ort_kurzbz']; + if (isset($filters['bezeichnung']) && $filters['bezeichnung'] !== '') + $title .= ' - '.$filters['bezeichnung']; return $title; } - return ($type === 'lektor' ? 'Lektor: ' : 'Student: ').$filters['uid']; + if ($type === 'verband') + { + $studiengang = isset($filters['stg_kurzbz']) + && $filters['stg_kurzbz'] !== '' + ? $filters['stg_kurzbz'] + : $filters['stg_kz']; + $title = 'Lehrverband: '.$studiengang.'-'.$filters['sem']; + + if (isset($filters['ver']) && $filters['ver'] !== null) + $title .= $filters['ver']; + else + $title .= 0; + + if (isset($filters['grp']) && $filters['grp'] !== null) + $title .= $filters['grp']; + else + $title .= 0; + + return $title; + } + + if ($type === 'lektor') + { + $name = isset($filters['full_name']) && $filters['full_name'] !== '' + ? $filters['full_name'] + : $filters['uid']; + return 'Lektor: '.$name; + } + + return 'Student: '.$filters['uid']; } private function getEventDateTime($event, $isoProperty, $dateProperty, $timeProperty) @@ -508,6 +659,22 @@ class LvPlanHTMLLib : ''; } + private function getFirstScalarValue($value) + { + if (is_scalar($value)) + return (string)$value; + if (!is_array($value)) + return ''; + + foreach ($value as $item) + { + if (is_scalar($item) && (string)$item !== '') + return (string)$item; + } + + return ''; + } + private function joinValue($value, $separator = ', ') { if (!is_array($value)) @@ -550,6 +717,14 @@ class LvPlanHTMLLib return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } + private function encodeJavaScriptString($value) + { + return json_encode( + (string)$value, + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT + ); + } + private function renderLines($value) { $lines = array_map('trim', explode(', ', (string)$value)); diff --git a/application/libraries/LvPlanICALLib.php b/application/libraries/LvPlanICALLib.php index 081a0289e..1627e4eb8 100644 --- a/application/libraries/LvPlanICALLib.php +++ b/application/libraries/LvPlanICALLib.php @@ -30,16 +30,34 @@ class LvPlanICALLib * @param string $uid User UID. * @param int $begin First included date as Unix timestamp. * @param int $ende Last included date as Unix timestamp. - * @param string|null $type Timetable type (student or lektor). + * @param string|null $type Timetable type (student, lektor, ort or verband). * @param int $version iCalendar major version (1 or 2). + * @param string|null $ortKurzbz Room identifier for room plans. + * @param array|null $verbandFilters Lehrverband filters. * @return string */ - public function getContent($uid, $begin, $ende, $type = null, $version = 2) - { + public function getContent( + $uid, + $begin, + $ende, + $type = null, + $version = 2, + $ortKurzbz = null, + $verbandFilters = null + ) { if (!in_array($version, array(1, 2), true)) throw new InvalidArgumentException('Unsupported iCalendar version'); - if ($type !== null && !in_array($type, array('student', 'lektor'), true)) + if ($type !== null && !in_array($type, array('student', 'lektor', 'ort', 'verband'), true)) throw new InvalidArgumentException('Unsupported timetable type'); + if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === '')) + throw new InvalidArgumentException('Missing room timetable filter'); + if ($type === 'verband' + && (!is_array($verbandFilters) + || !array_key_exists('stg_kz', $verbandFilters) + || !array_key_exists('sem', $verbandFilters))) + { + throw new InvalidArgumentException('Missing Lehrverband timetable filters'); + } $this->ci->load->library('KalenderLib', array('uid' => $uid)); @@ -58,7 +76,23 @@ class LvPlanICALLib // KalenderLib uses an exclusive end date; $ende is inclusive here. $endDate = date('Y-m-d', strtotime('+1 day', $ende)); - if ($isLecturer) + if ($type === 'verband') + { + $events = $this->ci->kalenderlib->getPlanForVerband( + $startDate, + $endDate, + $verbandFilters['stg_kz'], + $verbandFilters['sem'], + isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null, + isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null + ); + } + elseif ($type === 'ort') + { + $events = $this->ci->kalenderlib + ->getPlanForRoom($startDate, $endDate, $ortKurzbz); + } + elseif ($isLecturer) { $events = $this->ci->kalenderlib ->getPlanForLecturerByLecturer($startDate, $endDate, $uid); diff --git a/include/wochenplan.class.php b/include/wochenplan.class.php index 5274beefb..aa6337d90 100644 --- a/include/wochenplan.class.php +++ b/include/wochenplan.class.php @@ -512,6 +512,69 @@ class wochenplan extends basis_db $this->kal_link.='&pers_uid='.$this->pers_uid.'&ort_kurzbz='.$this->ort_kurzbz.'&stg_kz='.$this->stg_kz.'&sem='.$this->sem.'&ver='.$this->ver.'&grp='.$this->grp.'&gruppe_kurzbz='.$this->gruppe_kurzbz.'&lva='.$this->lva; $kal_link_ws=$this->kal_link.'&begin='.$this->studiensemester_now->start.'&ende='.$this->studiensemester_now->ende; $kal_link_ss=$this->kal_link.'&begin='.$this->studiensemester_next->start.'&ende='.$this->studiensemester_next->ende; + $kal_link_new_ws = null; + $kal_link_new_ss = null; + $kal_link_new_downloads = false; + + // Build only the type-specific parameters accepted by the new endpoints. + if ($this->type == 'student' || $this->type == 'lektor') + { + $kal_link_new_downloads = true; + $kal_link_new_ws = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'begin' => $this->studiensemester_now->start, + 'ende' => $this->studiensemester_now->ende + ), '', '&'); + $kal_link_new_ss = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'begin' => $this->studiensemester_next->start, + 'ende' => $this->studiensemester_next->ende + ), '', '&'); + } + elseif ($this->type == 'ort' && $this->ort_kurzbz != '' && $this->ort_kurzbz != 'all') + { + $kal_link_new_downloads = true; + $kal_link_new_ws = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'ort_kurzbz' => $this->ort_kurzbz, + 'begin' => $this->studiensemester_now->start, + 'ende' => $this->studiensemester_now->ende + ), '', '&'); + $kal_link_new_ss = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'ort_kurzbz' => $this->ort_kurzbz, + 'begin' => $this->studiensemester_next->start, + 'ende' => $this->studiensemester_next->ende + ), '', '&'); + } + elseif ($this->type == 'verband') + { + $kal_link_new_downloads = true; + $kal_link_new_ws = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'stg_kz' => $this->stg_kz, + 'sem' => $this->sem, + 'ver' => $this->ver, + 'grp' => $this->grp, + 'begin' => $this->studiensemester_now->start, + 'ende' => $this->studiensemester_now->ende + ), '', '&'); + $kal_link_new_ss = http_build_query(array( + 'type' => $this->type, + 'pers_uid' => $this->pers_uid, + 'stg_kz' => $this->stg_kz, + 'sem' => $this->sem, + 'ver' => $this->ver, + 'grp' => $this->grp, + 'begin' => $this->studiensemester_next->start, + 'ende' => $this->studiensemester_next->ende + ), '', '&'); + } echo $this->crlf; echo $this->studiensemester_now->name.'
'.$this->crlf; @@ -521,6 +584,19 @@ class wochenplan extends basis_db echo ' CSV-Outlook'.$this->crlf; echo ' vCal Version 1.0'.$this->crlf; echo ' vCal Version 2.0'.$this->crlf; + if ($kal_link_new_ws !== null) + { + echo '
'.$this->crlf; + echo ' HTML-new '.$this->crlf; + if ($kal_link_new_downloads) + { + echo ' excel-new '.$this->crlf; + echo ' CSV-new '.$this->crlf; + echo ' CSV-Outlook-new '.$this->crlf; + echo ' iCal1-new '.$this->crlf; + echo ' iCal2-new'.$this->crlf; + } + } echo ' '.$p->t('lvplan/semesterplaene').''; echo ' '.$this->studiensemester_next->name.'
'.$this->crlf; echo ' HTML'.$this->crlf; @@ -529,6 +605,19 @@ class wochenplan extends basis_db echo ' CSV-Outlook'.$this->crlf; echo ' iCal Version 1.0'.$this->crlf; echo ' iCal Version 2.0'.$this->crlf; + if ($kal_link_new_ss !== null) + { + echo '
'.$this->crlf; + echo ' HTML-new '.$this->crlf; + if ($kal_link_new_downloads) + { + echo ' excel-new '.$this->crlf; + echo ' CSV-new '.$this->crlf; + echo ' CSV-Outlook-new '.$this->crlf; + echo ' iCal1-new '.$this->crlf; + echo ' iCal2-new'.$this->crlf; + } + } echo ' '.$this->crlf; echo ' '.$this->crlf;