From e1f112083dd7a9a063a1d828be652f9faeaca4b9 Mon Sep 17 00:00:00 2001 From: Rik Veenboer Date: Tue, 10 Oct 2017 14:30:28 +0100 Subject: [PATCH] initial commit --- .htaccess | 2 + cache.php | 141 ++++ config.php | 88 +++ css/default.css | 84 +++ css/jquery.multiselect.css | 23 + .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes .../images/ui-bg_flat_55_fbec88_40x100.png | Bin 0 -> 182 bytes .../images/ui-bg_glass_75_d0e5f5_1x400.png | Bin 0 -> 124 bytes .../images/ui-bg_glass_85_dfeffc_1x400.png | Bin 0 -> 123 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 119 bytes .../ui-bg_gloss-wave_55_5c9ccc_500x100.png | Bin 0 -> 3457 bytes .../ui-bg_inset-hard_100_f5f8f9_1x100.png | Bin 0 -> 104 bytes .../ui-bg_inset-hard_100_fcfdfd_1x100.png | Bin 0 -> 88 bytes .../images/ui-icons_217bc0_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_469bdd_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_6da8d5_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_d8e7f3_256x240.png | Bin 0 -> 4379 bytes .../images/ui-icons_f9bd01_256x240.png | Bin 0 -> 4379 bytes css/redmond/jquery-ui-1.7.1.custom.css | 404 ++++++++++ css/ui.slider.extras.css | 110 +++ functions.php | 58 ++ index.php | 96 +++ js/form.js | 40 + js/jquery.multiselect.js | 705 ++++++++++++++++++ js/selectToUISlider.jQuery.js | 240 ++++++ rooster.php | 45 ++ style.php | 17 + template.php | 208 ++++++ template/form-group.html | 4 + template/form-optgroup.html | 3 + template/form-option-selected.html | 1 + template/form-option.html | 1 + template/form-week.html | 8 + template/form-weeks.html | 4 + template/form.html | 31 + template/legenda-row.html | 4 + template/legenda.html | 11 + template/page.html | 14 + template/rooster-bar.html | 1 + template/rooster-body-row-data.html | 1 + template/rooster-body-row-hour.html | 1 + template/rooster-body-row-minute.html | 1 + template/rooster-body-row.html | 2 + template/rooster.html | 22 + test.php | 76 ++ uvarooster.php | 77 ++ vurooster.php | 187 +++++ 49 files changed, 2710 insertions(+) create mode 100644 .htaccess create mode 100644 cache.php create mode 100644 config.php create mode 100644 css/default.css create mode 100644 css/jquery.multiselect.css create mode 100644 css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 css/redmond/images/ui-bg_flat_55_fbec88_40x100.png create mode 100644 css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png create mode 100644 css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png create mode 100644 css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png create mode 100644 css/redmond/images/ui-bg_inset-hard_100_f5f8f9_1x100.png create mode 100644 css/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png create mode 100644 css/redmond/images/ui-icons_217bc0_256x240.png create mode 100644 css/redmond/images/ui-icons_2e83ff_256x240.png create mode 100644 css/redmond/images/ui-icons_469bdd_256x240.png create mode 100644 css/redmond/images/ui-icons_6da8d5_256x240.png create mode 100644 css/redmond/images/ui-icons_cd0a0a_256x240.png create mode 100644 css/redmond/images/ui-icons_d8e7f3_256x240.png create mode 100644 css/redmond/images/ui-icons_f9bd01_256x240.png create mode 100644 css/redmond/jquery-ui-1.7.1.custom.css create mode 100644 css/ui.slider.extras.css create mode 100644 functions.php create mode 100644 index.php create mode 100644 js/form.js create mode 100644 js/jquery.multiselect.js create mode 100644 js/selectToUISlider.jQuery.js create mode 100644 rooster.php create mode 100644 style.php create mode 100644 template.php create mode 100644 template/form-group.html create mode 100644 template/form-optgroup.html create mode 100644 template/form-option-selected.html create mode 100644 template/form-option.html create mode 100644 template/form-week.html create mode 100644 template/form-weeks.html create mode 100644 template/form.html create mode 100644 template/legenda-row.html create mode 100644 template/legenda.html create mode 100644 template/page.html create mode 100644 template/rooster-bar.html create mode 100644 template/rooster-body-row-data.html create mode 100644 template/rooster-body-row-hour.html create mode 100644 template/rooster-body-row-minute.html create mode 100644 template/rooster-body-row.html create mode 100644 template/rooster.html create mode 100644 test.php create mode 100644 uvarooster.php create mode 100644 vurooster.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..742a661 --- /dev/null +++ b/.htaccess @@ -0,0 +1,2 @@ +RewriteEngine on +RewriteRule ^style.css$ style.php [nc] \ No newline at end of file diff --git a/cache.php b/cache.php new file mode 100644 index 0000000..258049d --- /dev/null +++ b/cache.php @@ -0,0 +1,141 @@ +oRooster = $oRooster; + } + + function getWeek(&$iYear, &$iWeek) { + if (!isset($iYear)) { + $iYear = $this->oRooster->iYear; + } + if (!isset($iWeek)) { + $iWeek = $this->oRooster->iWeek; + } + } + + function getCached($aFiles) { + global $aConfig; + $sContents = null; + if (count($aFiles) > 0) { + krsort($aFiles); + $iTime = key($aFiles); + $sFile = current($aFiles); + if (time() / 3600 - $iTime <= $aConfig['cache']) { + $sContents = file_get_contents($sFile); + array_shift($aFiles); + } + foreach ($aFiles as $sFile) { + unlink($sFile); + } + } + return $sContents; + } + + function getContents($sDirectory, $sExtension, $sGroup, $iYear = null, $iWeek = null) { + $this->getWeek($iYear, $iWeek); + $sGlob = sprintf('%s/%s-%d-%02d-*.%s', $sDirectory, md5($sGroup), $iYear, $iWeek, $sExtension); + $sRegex = sprintf('~%s/[\da-f]+-[0-9]+-[0-9]+-([0-9]+).%s$~', $sDirectory, $sExtension); + $aFiles = array(); + if (($aGlob = glob($sGlob)) === false) { + return null; + } + foreach ($aGlob as $sFile) { + if (preg_match($sRegex, $sFile, $aMatch)) { + $aFiles[$aMatch[1]] = $sFile; + } + } + return $this->getCached($aFiles); + } + + function getGroups($aFilters) { + $aGroups = $aTodo = array(); + foreach ($aFilters as $sFilter) { + $sGlob = sprintf('group/%s-*.base64', md5($sFilter)); + $aFiles = array(); + $sContents = null; + if (($aGlob = glob($sGlob)) !== false) { + foreach ($aGlob as $sFile) { + if (preg_match('~group/[\da-f]+-([0-9]+).base64$~', $sFile, $aMatch)) { + $aFiles[$aMatch[1]] = $sFile; + } + } + $sContents = $this->getCached($aFiles); + } + if (isset($sContents)) { + $aFilterGroups = unserialize(base64_decode($sContents)); + } else { + $aFilterGroups = $this->oRooster->getGroups(array($sFilter)); + $sFile = sprintf('group/%s-%d.base64', md5($sFilter), time() / 3600); + file_put_contents($sFile, base64_encode(serialize($aFilterGroups))); + } + $aGroups = array_merge($aGroups, $aFilterGroups); + } + return $aGroups; + } + + function getRooster($sGroup, $iYear = null, $iWeek = null) { + $this->getWeek($iYear, $iWeek); + $sRooster = $this->getContents('rooster', 'html', $sGroup, $iYear, $iWeek); + if (!isset($sRooster)) { + $this->oRooster->setWeek($iYear, $iWeek); + $sRooster = $this->oRooster->getPage($sGroup); + $sFile = sprintf('rooster/%s-%d-%02d-%d.html', md5($sGroup), $iYear, $iWeek, time() / 3600); + file_put_contents($sFile, $sRooster); + } + return $sRooster; + } + + function getData($sGroup, $iYear = null, $iWeek = null) { + $this->getWeek($iYear, $iWeek); + $sData = $this->getContents('data', 'base64', $sGroup, $iYear, $iWeek); + if (isset($sData)) { + return unserialize(base64_decode($sData)); + } else { + $sRooster = $this->getContents('rooster', 'html', $sGroup, $iYear, $iWeek); + if (isset($sRooster)) { + $aData = $this->oRooster->getData($sRooster); + } else { + $this->oRooster->setWeek($iYear, $iWeek); + $sRooster = $this->oRooster->getPage($sGroup); + $sFile = sprintf('rooster/%s-%d-%02d-%d.html', md5($sGroup), $iYear, $iWeek, time() / 3600); + file_put_contents($sFile, $sRooster); + $aData = $this->oRooster->getData(); + } + $sFile = sprintf('data/%s-%d-%02d-%d.base64', md5($sGroup), $iYear, $iWeek, time() / 3600); + file_put_contents($sFile, base64_encode(serialize($aData))); + return $aData; + } + } + + function clean($bAll = false) { + global $aConfig; + $aGroupGlob = glob('group/*-*.base64'); + $aRoosterGlob = glob('rooster/*-*-*-*.html'); + $aDataGlob = glob('data/*-*-*-*.base64'); + $aFiles = array_merge( + $aGroupGlob === false ? array() : $aGroupGlob, + $aRoosterGlob === false ? array() : $aRoosterGlob, + $aDataGlob === false ? array() : $aDataGlob); + foreach ($aFiles as $sFile) { + $bDelete = true; + if (!$bAll) { + $aFile = explode('.', $sFile); + if (count($aFile) == 2) { + $aFile = explode('-', current($aFile)); + $iTime = array_pop($aFile); + if (time() / 3600 - $iTime <= $aConfig['cache']) { + $bDelete = false; + } + } + } + if ($bDelete) { + unlink($sFile); + } + } + } +} \ No newline at end of file diff --git a/config.php b/config.php new file mode 100644 index 0000000..4a0dbf5 --- /dev/null +++ b/config.php @@ -0,0 +1,88 @@ + 6, + 'start' => array('hour' => 8, 'minute' => 45), + 'end' => array('hour' => 17, 'minute' => 45), + 'steps' => 4, + 'weeks' => 10, + 'year' => 2011, + 'week' => 36 +); +$aConfig['step'] = 60 / $aConfig['steps']; + +$aConfig['filters'] = array( + 'S' => '([\d]S) \(FEW\)', + '1S' => '(1S) \(FEW\)', + '2S' => '(2S) \(FEW\)', + '3S' => '(3S) \(FEW\)', + 'F' => '([\d]F) \(FEW\)', + '1F' => '(1F) \(FEW\)', + '2F' => '(2F) \(FEW\)', + '3F' => '(3F) \(FEW\)', + 'mCh' => '(mCh-[a-z]+) \(FEW\)', + 'mCh-AS' => '(mCh-AS) \(FEW\)', + 'mCh-MDSC' => '(mCh-MDSC) \(FEW\)', + 'mCh-MSP' => '(mCh-MSP) \(FEW\)', + 'mDDS' => '(mDDS-[a-z]+) \(FEW\)', + 'mDDS-BCCA' => '(mDDS-BCCA) \(FEW\)', + 'mDDS-BDA' => '(mDDS-BDA) \(FEW\)', + 'mDDS-CMCT' => '(mDDS-CMCT) \(FEW\)', + 'mDDS-DDS' => '(mDDS-DDS) \(FEW\)', + 'mDDS-DDSA' => '(mDDS-DDSA) \(FEW\)', + 'mDDS-DDTF' => '(mDDS-DDTF) \(FEW\)'); + +$aConfig['colors'] = array( + 'wc' => array('Werkcollege', '#8064a2'), /* purple */ + 'te' => array('Tentamen', '#c0504d'), /* red */ + 'ht' => array('Hertentamen', '#f79646'), /* orange */ + 'hc' => array('Hoorcollege', '#4f81bd'), /* blue */ + 'pr' => array('Practicum', '#9bbb59'), /* green */ + 'bij' => array('Bijeenkomst', '#4bacc6'), /* aqua */ +); + +$aConfig['types'] = array( + 'wc' => array(), + 'te' => array('tent'), + 'ht' => array(), + 'hc' => array('h', 'h/w'), + 'pr' => array('prac'), + 'bij' => array('pres', 'tutor'), +); + +$aConfig['uva'] = array( + '1S' => 'BSc SK_1', + '2S' => 'BSc SK_2', + '3S' => 'BSc SK_3', + 'mCh-AS' => 'MSc CH-AS', + 'mCh-MDSC' => 'MSc CH-MDSC' +); + +$aConfig['groups'] = array( + 'S' => array('Bachelor Scheikunde', array( + '1S' => '1e jaar', + '2S' => '2e jaar', + '3S' => '3e jaar')), + 'F' => array('Bachelor Farmaceutische wetenschappen', array( + '1F' => '1e jaar', + '2F' => '2e jaar', + '3F' => '3e jaar')), + 'mCh' => array('Master Chemistry', array( + 'mCh-AS' => 'Analytical Sciences', + 'mCh-MDSC' => 'Molecular Design, Synthesis and Catalysis', + 'mCh-MSP' => 'Molecular Simulation & Photonics')), + 'mDDS' => array('Drug Discovery & Safety', array( + 'mDDS-BCCA' => 'Biomarkers and Clinical Chemical Analysis', + 'mDDS-BDA' => 'Biomolecular Drug Analysis', + 'mDDS-CMCT' => 'Computational Medicinal Chemistry & Toxicology', + 'mDDS-DDS' => 'Drug Design & Synthesis', + 'mDDS-DDSA' => 'Drug Disposition & Safety Assessment', + 'mDDS-DDTF' => 'Drug Discovery & Target Finding'))); + +$aDirectories = array('group', 'rooster', 'data'); +foreach ($aDirectories as $sDirectory) { + if (!file_exists($sDirectory)) { + mkdir($sDirectory); + } +} \ No newline at end of file diff --git a/css/default.css b/css/default.css new file mode 100644 index 0000000..1709302 --- /dev/null +++ b/css/default.css @@ -0,0 +1,84 @@ +* { + font-family: "Trebuchet MS"; +} +body { + background: #cccccc; + text-align: center; +} +table { + border: 3px solid #e5eff8; + margin: 0 auto 20px auto; + border-collapse: collapse; + background: #f4f9fe; +} +table caption { + font-style: italic; +} +thead th.week { + padding-left: 5px; + font-size: 1.2em; + text-align: left; + color: #555555; +} +thead th.corner { + background: #e5eff8; +} +thead th.bar { + background: #e5eff8; + font-size: 0.6em; +} +thead th.bar a { + font-weight: bold; + color: #66a3d3; + text-decoration: none; +} +tbody th { + color: #678197; + border: 1px solid #e5eff8; + padding: 0 0.2em; + text-align: center; +} +tbody th.hour { + vertical-align: text-top; + font-weight: bold; + font-size: 1.2em; +} +tbody th.minute { + padding: 0 10px; + font-size: 0.5em; +} +table tbody td { + border-left: 1px solid #cccccc; + font-size: 0; +} +tbody td.border { + border-right: 2px solid #999999; +} +thead th, table tbody td.strong { + font-size: inherit; + background: #f4f9fe; + text-align: center; + font-weight: bold; + color: #66a3d3; +} + +a { + color: inherit; +} +small { + font-size: 0.7em; +} +fieldset { + width: 60%; + margin: 0 auto 0 auto; +} + +div.ui-slider { + margin: 0 auto 50px auto; +} +a.multiSelect { + margin-top: 50px; +} +div.multiSelectOptions { + text-align: left; +} \ No newline at end of file diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css new file mode 100644 index 0000000..898786a --- /dev/null +++ b/css/jquery.multiselect.css @@ -0,0 +1,23 @@ +.ui-multiselect { padding:2px 0 2px 4px; text-align:left } +.ui-multiselect span.ui-icon { float:right } +.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; } +.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important } + +.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px } +.ui-multiselect-header ul { font-size:0.9em } +.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 } +.ui-multiselect-header a { text-decoration:none } +.ui-multiselect-header a:hover { text-decoration:underline } +.ui-multiselect-header span.ui-icon { float:left } +.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 } + +.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left } +.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll } +.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px } +.ui-multiselect-checkboxes label input { position:relative; top:1px } +.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid } +.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none } + +/* remove label borders in IE6 because IE6 does not support transparency */ +* html .ui-multiselect-checkboxes label { border:none } diff --git a/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png b/css/redmond/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..5b5dab2ab7b1c50dea9cfe73dc5a269a92d2d4b4 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*FscKIb$B>N1x91EQ4=4yQ7#`R^ z$vje}bP0l+XkK DSH>_4 literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png b/css/redmond/images/ui-bg_flat_55_fbec88_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..47acaadd737478ddb090f47f618810712163317b GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F!3HG1q!d*Fsaj7L$B>N1x91EQ8x$BA993)+ za~~)OO5|O5sDCi_{N8&XlRv*c;OQ6|AR59NN?mFzWBXJVGojypu|S6~c)I$ztaD0e F0syyrGF|`x literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png b/css/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb564f8d0a117f17aa6b844490309dadbd94821 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouq?|on978O6-=0_GYj6;7zWBfT zzjhI`OjAO{6(N>+Em!s|xjZW|^1EO|(5d{JeUmv{p6fa-GJh;t>KCH4`R~7(L8qj} Y_egNRQF(If70@^aPgg&ebxsLQ0Qgob)Bpeg literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png b/css/redmond/images/ui-bg_glass_85_dfeffc_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..014951529c315d6042e72febc310a4d2db5b4a82 GIT binary patch literal 123 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnouq?|lm978O6-<~(*YA_IRxoBVf zfAX@vsV!R#l$@#*eLnw)_Sv|_?i7P!ORnX)SxaXh+BPpZ!Fw~yjr&#G|Jw^YMHDhV X&EsZx`7bsSXc~j3tDnm{r-UW|&(SK+ literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png b/css/redmond/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100644 index 0000000000000000000000000000000000000000..4443fdc1a156babad4336f004eaf5ca5dfa0f9ab GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j6gJjgAK^akKnour0hIh978O6-<~(*YA|4MzBvER z|7}eQtdCVXoUc2b{PaWeaIKu7gJx>{vDV26o)#~38k_!`W=^oo1w6ixmPC4R1b Tyd6G3lNdZ*{an^LB{Ts5`idse literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png b/css/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png new file mode 100644 index 0000000000000000000000000000000000000000..81ecc362d50ef5abbc0420aacd5345822f1f6098 GIT binary patch literal 3457 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 literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-icons_217bc0_256x240.png b/css/redmond/images/ui-icons_217bc0_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..9c8845833b401876624a0a70169630c27085da88 GIT binary patch literal 4379 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 MdKld*ZEWQK0Hw<@@c;k- literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-icons_2e83ff_256x240.png b/css/redmond/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..b425c446d2444bcf40bd35581d2171c63658634e GIT binary patch literal 4379 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 MT@RyMrHzgFA9(vu;s5{u literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-icons_6da8d5_256x240.png b/css/redmond/images/ui-icons_6da8d5_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..60e20ca1891cb17243a78b1db1aaceaff5ba72f7 GIT binary patch literal 4379 zcmd^?`8(8o7stP|7z`O(wipyrma=3U+gOIkAfh4$$-eKIn6cg>TOmc(tl6_gW++=p zWE+tgL~hylB|N(Sjpucp>vPUe=UnH9_qpDmI1{Yi88{yt0Dv>s^>uCn0O+&?))*kC zFXg(y{%Kk^xnZvBQ8M!X6V~+to~Mgr{cmE;03KFZD1!+pGE@4zxwqy693T4^W+7EB zEqw#zU9qb-C8OnIi62~hzjiC>@V{@ym)F03Qd&_bev~7!3WtG{3hdMJs@$pB7JTczR-z z2%t+}u0moe!Wiw4JzI5p&(#YjpRsc&gLt7R+^-4S&;Z62Vk+PXx|U*#Mr*FQCn;nW zcME7ORJD#>H96%sJYw^;_@H~oH{#Wm-P@|Ya(3D{p7FQqL{v%4)OsyKarRb{9 zz4PPkX{QCLk1v@lS0^B?J)_O{e=A&s@2bK+)@<$*KFBKmH(XdaPtToeuREj6e%UK4 z*quTc&~P4*Vt?Jk=!+tlh<|&b%BiAV*uB$PHZYV|-bY27fQ((5!fly^BXpXbZ`>AY zfmkpILNsL5QorpU_%#BmvlDZuLKK3ak3V(=eZ|Meo+ppU=-Mx5g(U08 zs*NjY%naFz)2?jGljWcN3dQFh`lAU0nq_*Q_ocWllB)o<^OtoV>vr7GwGiaqup&>; zDH0~ZOI&eN;Nuq6-*1F@SXL5~15{ZsWEk356b+n%-YMltx0h3D4gY!#&V5*~S-cra zDGv_fu8E~xHd^60JjUP9`b!7eq0!ifx1>7h)5Cs>Cyvcanp58k&Rh0vpwCUiPO4#J z?$SJ?itU+sn05(X+dL(oLRzUsA4aAR!CF0HE)%ZaPFXUGX0RJkq}PZ26W1WVb_BdK zd0An}^hmoG?I+y~r1nd(4-~%CIrpkO>YLM=xQINL`d3HXYt~B1U7kr~rk-}nO1_oW zgWa27l9L#gKd4Jr1_!)vIKp{HzR0}6I?j89ClzNgwoM?faq-Ps@Nn)EJqW=~ec18X ztn=e;?i?oW$HJ=N{stcA7WuXslDSe-e=SerT1?K{85@HBj)6LREb|TsJ@=)xGWVY-bjWcg%%yU_##wPb1Od=aj+~w zHl}3=BWwKwKl=^$c*xxm+P)`z)5V0K9=&Zp5*q)w9Gyf>YgWnk(R@o(wJC1DcY+lb z9Ies#PpLy%b~91-+Xo03A0n_A(P<)~$QR>^=Zc%8mB7>9-H;6SVVj>N@<9?tAUAWe zeBN|Aj0kUPZCArkc`}U1v^iF~(48Qyf6l~{K{`IIV)k9@;KQpi z?LZeKpTOP=rMmq-TL$5c zh`H#A%&k7Avg5w%nYrKct3*FyQVoglB4&Qe&^&QB7GL_E4&^x%d=)-rq}j!?aN$m1 zY91$SepA^$5#*iEJFw#bULvIfD2DT3>ArDp?k~h4S5{TW`CJYWkbF=9y(Gw%wjbBK zm3Mq6kd2Ira$p*so|_X@;y#Ns$zqN@h;5;JGigM2j6Tp*h}jQrCCD5nu)d4lB2p)~ z8-mD;JXgVlVDiUtL7`lal~OvtX0#!&OPKkX->jnrlj~x+cZexiZ+ZkzczN6%1-wZX z5ej6YZMZT^PJEIA(JfbbR6d5##tG$?VKxvKfX(d4Ziz%X~hOfgWiT2 z)T?oJ4%zi;8KSN!$F ztc=GU0*X5l9|`K?gbMk=o(Jw)?P+t158rY<6MO!7!YS0VDVyeYz)w@Iqe2b*oJetB zCYCZ*S-6Rr5gLnQ7v7lURrwJYpVp&Cff9{Uw{52^YGVCWvD*BNaT;?k=H%bEhG>Gy zPWi{K>Evz~$KqxtfpXq>g_cxkwLi~re0)Y#|CNEarg=vbMtdI__L%shsurbp1CR<@@e^368e_MqHkxQF3rwNQtcl*{>!T7 zQ4nD`JLJ@ih=?qKv{hw1w3M5eZ&BArwOTmY@CWM@AE}?!!x|ZTgLpde;XnK_9|Xe) ztH~-!Yy0tj!Rz3D?k9aaR`Nqr#nBRG+8*NQMH>O??7%0+~=KYIQuCz3*lr3 z#%@;?&)b^0^Sd+n{RHWdz&7tyLh1?S4eeu?mcbnc8gi{5?d6^3?_o}~U>BL%X*&{vMfoyN*38*r(AzAdS_@Tr^P}lKm2QtMr)!*N4E4yVV zBZ&r=!SSi+ez1`X8KUfNxg1rcPcWub%HVEGaxvY;!3b)w#v{&PPq7Et>Z$=&STWBI zgCxR%?FtXDG5Y7>BtI<){hN(Tz=069=!yf2C|n>2H;x^LaL@BXke-DQMxl*DbogiCUN!&X{d`F?Ck6c9jvQ~>LtL@EkvSL6 zRn;Wbs>o!xuD)=#Q)OE@Da%4_!f5?LHR_{LX zo3ifx*`gE;7WHo@{mhqq<0)yS*-95Tn0?5^XAJ+NUH7b zL~m|W4gD61;VylNy1au&@Gpko*vkP)>!QAb@lU#eb{e*w{(S}(A16`sG+DEX^u5DO zGr$W&QGd`+Gl+bNY4u6_$wPRZ96re&YBU-yGgP~_lksTLTfgos7xD~19hpkFP%*cEy#tHke%8e8aPwQZrL!ts-NY-r zH$5AU#3zMvX|qMh_Q?9>rWp5qesuEc(wJWDQ-~<}A))J9+sp&&+cV5ajOpjaObdy$SXTXH063v1iRBBa9ePxCA9?cfg4I*&w|;bTrv zoo&-ECoCbR<0Lh3(E@5g6}mSyCL{ix)UB|zx7Ucqwx;EKo6X;$0viOgAip?=+xb>^mpz~tqqC349!g|SU8kv_}9Vz zo^oOvB-02|8Zf7Hz~ZQQ=JihWXwCv}c+M7KM#$i8xN6}J~(@#He MT^FlUsfCO99~F39u>b%7 literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-icons_cd0a0a_256x240.png b/css/redmond/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..2db88b796a36dc0501745c6f90920cf601bfbccd GIT binary patch literal 4379 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 M>tS>&wXu=^1FE+S0{{R3 literal 0 HcmV?d00001 diff --git a/css/redmond/images/ui-icons_d8e7f3_256x240.png b/css/redmond/images/ui-icons_d8e7f3_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8aac4612416c51f1cefb9e0741232c618e31d8 GIT binary patch literal 4379 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%0D_zCrpuZ5A`ofmiUfD(^_>HsH%8NPkyr<*Y&Sw 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 $aTypes) { + if ($sKey == $sType || in_array($sType, $aTypes)) { + return $sKey; + } + } + return $sKey; +} + +function convertData($aData, &$aTable, &$aInfo) { + global $aConfig; + foreach ($aData as $sGroup => $aDays) { + $aTable[$sGroup] = array(); + foreach ($aDays as $iDay => $aDay) { + foreach ($aDay as $aCourse) { + $sText = isset($aCourse['room']) + ? sprintf('[%s-%s] %s @ %s', $aCourse['start'], $aCourse['end'], $aCourse['course'], $aCourse['room']) + : sprintf('[%s-%s] %s', $aCourse['start'], $aCourse['end'], $aCourse['course']); + $aInfo[] = array( + 'text' => $sText, + 'type' => convertType($aCourse['type'])); + $iId = count($aInfo) - 1; + $iStart = timeToSeconds($aCourse['start']); + $iEnd = timeToSeconds($aCourse['end']); + for ($iHour = $aConfig['start']['hour']; $iHour <= $aConfig['end']['hour']; ++$iHour) { + for ($iMinute = 0; $iMinute < $aConfig['steps']; ++$iMinute) { + $iTime = 60 * (60 * $iHour + $aConfig['step'] * $iMinute); + if ($iTime >= $iStart && $iTime <= $iEnd) { + $aTable[$sGroup][$iDay][$iHour][$iMinute] = $iId; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..346153c --- /dev/null +++ b/index.php @@ -0,0 +1,96 @@ += $aConfig['week'] && $aWeek[1] <= 52) || ($iYear == $aConfig['year'] + 1 && $aWeek[1] > 1 && $aWeek[1] < $aConfig['week'])) { + list($iYear, $iWeek) = $aWeek; + $bNow = false; + } + } + } + if ($bNow === true) { + list($iYear, $iWeek) = now(); + } + + /* Groups */ + if (isset($_POST['group'])) { + $aSelected = array_intersect($_POST['group'], array_keys($aConfig['filters'])); + foreach ($aConfig['groups'] as $sCategory => $aGroups) { + $aGroupSelected = array(); + foreach ($aGroups[1] as $sValue => $sText) { + if (in_array($sValue, $aSelected)) { + $aGroupSelected[] = $sValue; + } + } + if (count($aGroupSelected) == count($aGroups[1])) { + $aSelected = array_diff($aSelected, $aGroupSelected); + $aSelected[] = $sCategory; + } + } + $aGroups = array(); + foreach ($aSelected as $sGroup) { + if (isset($aConfig['filters'][$sGroup])) { + $aGroups[] = $aConfig['filters'][$sGroup]; + } + } + if (count($aGroups) > 0) { + $oVuRooster = new VuRooster; + $oVuCache = new Cache($oVuRooster); + $oVuCache->clean(); + + $oUvaRooster = new UvaRooster; + $oUvaCache = new Cache($oUvaRooster); + + $aWeeks = array(); + try { + $aGroups = $oVuCache->getGroups($aGroups); + $oVuRooster->setWeek($iYear, $iWeek); + $oUvaRooster->setWeek($iYear, $iWeek); + + $aTables = $aInfos = array(); + for ($i = 0; $i < $iWeeks; ++$i) { + $aWeeks[] = $oVuRooster->getWeek(); + $aVuData = $aUvaData = array(); + foreach ($aGroups as $sGroup => $sName) { + try { + $aVuData[$sName] = $oVuCache->getData($sGroup); + if (isset($aConfig['uva'][$sName])) { + $aUvaData[$sName] = $oUvaCache->getData($aConfig['uva'][$sName]); + } + } catch (Exception $e) { + die(printf('
ERROR: %s
', $e->getMessage())); + } + } + convertData($aVuData, $aTable, $aInfo); + convertData($aUvaData, $aTable, $aInfo); + $aTables[] = $aTable; + $aInfos[] = $aInfo; + $oVuRooster->nextWeek(); + $oUvaRooster->nextWeek(); + } + echo Template::getWeeks($aTables, $aInfos, $aWeeks); + } catch (Exception $e) { + die(printf('
ERROR: %s
', $e->getMessage())); + } + } + $bForm = false; + } +} + +if ($bForm === true) { + echo Template::getForm(); +} \ No newline at end of file diff --git a/js/form.js b/js/form.js new file mode 100644 index 0000000..ceb2240 --- /dev/null +++ b/js/form.js @@ -0,0 +1,40 @@ +$(window).ready(function () { + $('select#weeks').selectToUISlider({ + labels : 52, + tooltip : false + }).hide() + + $('select#week-from, select#week-to').selectToUISlider({ + labels : 18, + labelSrc : 'text', + tooltip : false + }).hide() + + $('label[for="week-from"]').hide() + $('label[for="week-to"]').text('Selecteer weken') + + $('select#weeks + div.ui-slider').bind('slide', function(e, ui) { + slider = $('select#week-to + div.ui-slider'); + adjustWeeks(slider, 1, slider.slider('values', 0) + ui.value) + }).css('width', '40%') + + $('select#week-to + div.ui-slider').bind('slide', function(e, ui) { + weeks = parseInt($('select#weeks').val()) + if ($(ui.handle).attr('id').split('handle_')[1] == 'week-from') { + adjustWeeks(this, 1, ui.value + weeks) + } else { + adjustWeeks(this, 0, ui.value - weeks) + } + }).css('width', '80%') + + function adjustWeeks(handle, index, value) { + $(handle).slider('values', index, value) + } + + $('select#group').multiselect({ + checkAllText : 'Selecteer alles', + uncheckAllText : 'Selecteer niets', + selectedText : '# van # geselecteerd', + noneSelectedText : $('label[for="group"]').hide().text() + }) +}) \ No newline at end of file diff --git a/js/jquery.multiselect.js b/js/jquery.multiselect.js new file mode 100644 index 0000000..16ae426 --- /dev/null +++ b/js/jquery.multiselect.js @@ -0,0 +1,705 @@ +/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, boss:true, undef:true, curly:true, browser:true, jquery:true */ +/* + * jQuery MultiSelect UI Widget 1.13 + * Copyright (c) 2012 Eric Hynds + * + * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/ + * + * Depends: + * - jQuery 1.4.2+ + * - jQuery UI 1.8 widget factory + * + * Optional: + * - jQuery UI effects + * - jQuery UI position utility + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * +*/ +(function($, undefined){ + +var multiselectID = 0; + +$.widget("ech.multiselect", { + + // default options + options: { + header: true, + height: 175, + minWidth: 225, + classes: '', + checkAllText: 'Check all', + uncheckAllText: 'Uncheck all', + noneSelectedText: 'Select options', + selectedText: '# selected', + selectedList: 0, + show: null, + hide: null, + autoOpen: false, + multiple: true, + position: {} + }, + + _create: function(){ + var el = this.element.hide(), + o = this.options; + + this.speed = $.fx.speeds._default; // default speed for effects + this._isOpen = false; // assume no + + var + button = (this.button = $('')) + .addClass('ui-multiselect ui-widget ui-state-default ui-corner-all') + .addClass( o.classes ) + .attr({ 'title':el.attr('title'), 'aria-haspopup':true, 'tabIndex':el.attr('tabIndex') }) + .insertAfter( el ), + + buttonlabel = (this.buttonlabel = $('')) + .html( o.noneSelectedText ) + .appendTo( button ), + + menu = (this.menu = $('
')) + .addClass('ui-multiselect-menu ui-widget ui-widget-content ui-corner-all') + .addClass( o.classes ) + .appendTo( document.body ), + + header = (this.header = $('
')) + .addClass('ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix') + .appendTo( menu ), + + headerLinkContainer = (this.headerLinkContainer = $('
    ')) + .addClass('ui-helper-reset') + .html(function(){ + if( o.header === true ){ + return '
  • ' + o.checkAllText + '
  • ' + o.uncheckAllText + '
  • '; + } else if(typeof o.header === "string"){ + return '
  • ' + o.header + '
  • '; + } else { + return ''; + } + }) + .append('
  • ') + .appendTo( header ), + + checkboxContainer = (this.checkboxContainer = $('
      ')) + .addClass('ui-multiselect-checkboxes ui-helper-reset') + .appendTo( menu ); + + // perform event bindings + this._bindEvents(); + + // build menu + this.refresh( true ); + + // some addl. logic for single selects + if( !o.multiple ){ + menu.addClass('ui-multiselect-single'); + } + }, + + _init: function(){ + if( this.options.header === false ){ + this.header.hide(); + } + if( !this.options.multiple ){ + this.headerLinkContainer.find('.ui-multiselect-all, .ui-multiselect-none').hide(); + } + if( this.options.autoOpen ){ + this.open(); + } + if( this.element.is(':disabled') ){ + this.disable(); + } + }, + + refresh: function( init ){ + var el = this.element, + o = this.options, + menu = this.menu, + checkboxContainer = this.checkboxContainer, + optgroups = [], + html = "", + id = el.attr('id') || multiselectID++; // unique ID for the label & option tags + + // build items + el.find('option').each(function( i ){ + var $this = $(this), + parent = this.parentNode, + title = this.innerHTML, + description = this.title, + value = this.value, + inputID = 'ui-multiselect-' + (this.id || id + '-option-' + i), + isDisabled = this.disabled, + isSelected = this.selected, + labelClasses = [ 'ui-corner-all' ], + liClasses = (isDisabled ? 'ui-multiselect-disabled ' : ' ') + this.className, + optLabel; + + // is this an optgroup? + if( parent.tagName === 'OPTGROUP' ){ + optLabel = parent.getAttribute( 'label' ); + + // has this optgroup been added already? + if( $.inArray(optLabel, optgroups) === -1 ){ + html += '
    • ' + optLabel + '
    • '; + optgroups.push( optLabel ); + } + } + + if( isDisabled ){ + labelClasses.push( 'ui-state-disabled' ); + } + + // browsers automatically select the first option + // by default with single selects + if( isSelected && !o.multiple ){ + labelClasses.push( 'ui-state-active' ); + } + + html += '
    • '; + + // create the label + html += '
    • '; + }); + + // insert into the DOM + checkboxContainer.html( html ); + + // cache some moar useful elements + this.labels = menu.find('label'); + this.inputs = this.labels.children('input'); + + // set widths + this._setButtonWidth(); + this._setMenuWidth(); + + // remember default value + this.button[0].defaultValue = this.update(); + + // broadcast refresh event; useful for widgets + if( !init ){ + this._trigger('refresh'); + } + }, + + // updates the button text. call refresh() to rebuild + update: function(){ + var o = this.options, + $inputs = this.inputs, + $checked = $inputs.filter(':checked'), + numChecked = $checked.length, + value; + + if( numChecked === 0 ){ + value = o.noneSelectedText; + } else { + if($.isFunction( o.selectedText )){ + value = o.selectedText.call(this, numChecked, $inputs.length, $checked.get()); + } else if( /\d/.test(o.selectedList) && o.selectedList > 0 && numChecked <= o.selectedList){ + value = $checked.map(function(){ return $(this).next().html(); }).get().join(', '); + } else { + value = o.selectedText.replace('#', numChecked).replace('#', $inputs.length); + } + } + + this.buttonlabel.html( value ); + return value; + }, + + // binds events + _bindEvents: function(){ + var self = this, button = this.button; + + function clickHandler(){ + self[ self._isOpen ? 'close' : 'open' ](); + return false; + } + + // webkit doesn't like it when you click on the span :( + button + .find('span') + .bind('click.multiselect', clickHandler); + + // button events + button.bind({ + click: clickHandler, + keypress: function( e ){ + switch(e.which){ + case 27: // esc + case 38: // up + case 37: // left + self.close(); + break; + case 39: // right + case 40: // down + self.open(); + break; + } + }, + mouseenter: function(){ + if( !button.hasClass('ui-state-disabled') ){ + $(this).addClass('ui-state-hover'); + } + }, + mouseleave: function(){ + $(this).removeClass('ui-state-hover'); + }, + focus: function(){ + if( !button.hasClass('ui-state-disabled') ){ + $(this).addClass('ui-state-focus'); + } + }, + blur: function(){ + $(this).removeClass('ui-state-focus'); + } + }); + + // header links + this.header + .delegate('a', 'click.multiselect', function( e ){ + // close link + if( $(this).hasClass('ui-multiselect-close') ){ + self.close(); + + // check all / uncheck all + } else { + self[ $(this).hasClass('ui-multiselect-all') ? 'checkAll' : 'uncheckAll' ](); + } + + e.preventDefault(); + }); + + // optgroup label toggle support + this.menu + .delegate('li.ui-multiselect-optgroup-label a', 'click.multiselect', function( e ){ + e.preventDefault(); + + var $this = $(this), + $inputs = $this.parent().nextUntil('li.ui-multiselect-optgroup-label').find('input:visible:not(:disabled)'), + nodes = $inputs.get(), + label = $this.parent().text(); + + // trigger event and bail if the return is false + if( self._trigger('beforeoptgrouptoggle', e, { inputs:nodes, label:label }) === false ){ + return; + } + + // toggle inputs + self._toggleChecked( + $inputs.filter(':checked').length !== $inputs.length, + $inputs + ); + + self._trigger('optgrouptoggle', e, { + inputs: nodes, + label: label, + checked: nodes[0].checked + }); + }) + .delegate('label', 'mouseenter.multiselect', function(){ + if( !$(this).hasClass('ui-state-disabled') ){ + self.labels.removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover').find('input').focus(); + } + }) + .delegate('label', 'keydown.multiselect', function( e ){ + e.preventDefault(); + + switch(e.which){ + case 9: // tab + case 27: // esc + self.close(); + break; + case 38: // up + case 40: // down + case 37: // left + case 39: // right + self._traverse(e.which, this); + break; + case 13: // enter + $(this).find('input')[0].click(); + break; + } + }) + .delegate('input[type="checkbox"], input[type="radio"]', 'click.multiselect', function( e ){ + var $this = $(this), + val = this.value, + checked = this.checked, + tags = self.element.find('option'); + + // bail if this input is disabled or the event is cancelled + if( this.disabled || self._trigger('click', e, { value: val, text: this.title, checked: checked }) === false ){ + e.preventDefault(); + return; + } + + // make sure the input has focus. otherwise, the esc key + // won't close the menu after clicking an item. + $this.focus(); + + // toggle aria state + $this.attr('aria-selected', checked); + + // change state on the original option tags + tags.each(function(){ + if( this.value === val ){ + this.selected = checked; + } else if( !self.options.multiple ){ + this.selected = false; + } + }); + + // some additional single select-specific logic + if( !self.options.multiple ){ + self.labels.removeClass('ui-state-active'); + $this.closest('label').toggleClass('ui-state-active', checked ); + + // close menu + self.close(); + } + + // fire change on the select box + self.element.trigger("change"); + + // setTimeout is to fix multiselect issue #14 and #47. caused by jQuery issue #3827 + // http://bugs.jquery.com/ticket/3827 + setTimeout($.proxy(self.update, self), 10); + }); + + // close each widget when clicking on any other element/anywhere else on the page + $(document).bind('mousedown.multiselect', function( e ){ + if(self._isOpen && !$.contains(self.menu[0], e.target) && !$.contains(self.button[0], e.target) && e.target !== self.button[0]){ + self.close(); + } + }); + + // deal with form resets. the problem here is that buttons aren't + // restored to their defaultValue prop on form reset, and the reset + // handler fires before the form is actually reset. delaying it a bit + // gives the form inputs time to clear. + $(this.element[0].form).bind('reset.multiselect', function(){ + setTimeout($.proxy(self.refresh, self), 10); + }); + }, + + // set button width + _setButtonWidth: function(){ + var width = this.element.outerWidth(), + o = this.options; + + if( /\d/.test(o.minWidth) && width < o.minWidth){ + width = o.minWidth; + } + + // set widths + this.button.width( width ); + }, + + // set menu width + _setMenuWidth: function(){ + var m = this.menu, + width = this.button.outerWidth()- + parseInt(m.css('padding-left'),10)- + parseInt(m.css('padding-right'),10)- + parseInt(m.css('border-right-width'),10)- + parseInt(m.css('border-left-width'),10); + + m.width( width || this.button.outerWidth() ); + }, + + // move up or down within the menu + _traverse: function( which, start ){ + var $start = $(start), + moveToLast = which === 38 || which === 37, + + // select the first li that isn't an optgroup label / disabled + $next = $start.parent()[moveToLast ? 'prevAll' : 'nextAll']('li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)')[ moveToLast ? 'last' : 'first'](); + + // if at the first/last element + if( !$next.length ){ + var $container = this.menu.find('ul').last(); + + // move to the first/last + this.menu.find('label')[ moveToLast ? 'last' : 'first' ]().trigger('mouseover'); + + // set scroll position + $container.scrollTop( moveToLast ? $container.height() : 0 ); + + } else { + $next.find('label').trigger('mouseover'); + } + }, + + // This is an internal function to toggle the checked property and + // other related attributes of a checkbox. + // + // The context of this function should be a checkbox; do not proxy it. + _toggleState: function( prop, flag ){ + return function(){ + if( !this.disabled ) { + this[ prop ] = flag; + } + + if( flag ){ + this.setAttribute('aria-selected', true); + } else { + this.removeAttribute('aria-selected'); + } + }; + }, + + _toggleChecked: function( flag, group ){ + var $inputs = (group && group.length) ? group : this.inputs, + self = this; + + // toggle state on inputs + $inputs.each(this._toggleState('checked', flag)); + + // give the first input focus + $inputs.eq(0).focus(); + + // update button text + this.update(); + + // gather an array of the values that actually changed + var values = $inputs.map(function(){ + return this.value; + }).get(); + + // toggle state on original option tags + this.element + .find('option') + .each(function(){ + if( !this.disabled && $.inArray(this.value, values) > -1 ){ + self._toggleState('selected', flag).call( this ); + } + }); + + // trigger the change event on the select + if( $inputs.length ) { + this.element.trigger("change"); + } + }, + + _toggleDisabled: function( flag ){ + this.button + .attr({ 'disabled':flag, 'aria-disabled':flag })[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); + + var inputs = this.menu.find('input'); + var key = "ech-multiselect-disabled"; + + if(flag) { + // remember which elements this widget disabled (not pre-disabled) + // elements, so that they can be restored if the widget is re-enabled. + inputs = inputs.filter(':enabled') + .data(key, true) + } else { + inputs = inputs.filter(function() { + return $.data(this, key) === true; + }).removeData(key); + } + + inputs + .attr({ 'disabled':flag, 'arial-disabled':flag }) + .parent()[ flag ? 'addClass' : 'removeClass' ]('ui-state-disabled'); + + this.element + .attr({ 'disabled':flag, 'aria-disabled':flag }); + }, + + // open the menu + open: function( e ){ + var self = this, + button = this.button, + menu = this.menu, + speed = this.speed, + o = this.options, + args = []; + + // bail if the multiselectopen event returns false, this widget is disabled, or is already open + if( this._trigger('beforeopen') === false || button.hasClass('ui-state-disabled') || this._isOpen ){ + return; + } + + var $container = menu.find('ul').last(), + effect = o.show, + pos = button.offset(); + + // figure out opening effects/speeds + if( $.isArray(o.show) ){ + effect = o.show[0]; + speed = o.show[1] || self.speed; + } + + // if there's an effect, assume jQuery UI is in use + // build the arguments to pass to show() + if( effect ) { + args = [ effect, speed ]; + } + + // set the scroll of the checkbox container + $container.scrollTop(0).height(o.height); + + // position and show menu + if( $.ui.position && !$.isEmptyObject(o.position) ){ + o.position.of = o.position.of || button; + + menu + .show() + .position( o.position ) + .hide(); + + // if position utility is not available... + } else { + menu.css({ + top: pos.top + button.outerHeight(), + left: pos.left + }); + } + + // show the menu, maybe with a speed/effect combo + $.fn.show.apply(menu, args); + + // select the first option + // triggering both mouseover and mouseover because 1.4.2+ has a bug where triggering mouseover + // will actually trigger mouseenter. the mouseenter trigger is there for when it's eventually fixed + this.labels.eq(0).trigger('mouseover').trigger('mouseenter').find('input').trigger('focus'); + + button.addClass('ui-state-active'); + this._isOpen = true; + this._trigger('open'); + }, + + // close the menu + close: function(){ + if(this._trigger('beforeclose') === false){ + return; + } + + var o = this.options, + effect = o.hide, + speed = this.speed, + args = []; + + // figure out opening effects/speeds + if( $.isArray(o.hide) ){ + effect = o.hide[0]; + speed = o.hide[1] || this.speed; + } + + if( effect ) { + args = [ effect, speed ]; + } + + $.fn.hide.apply(this.menu, args); + this.button.removeClass('ui-state-active').trigger('blur').trigger('mouseleave'); + this._isOpen = false; + this._trigger('close'); + }, + + enable: function(){ + this._toggleDisabled(false); + }, + + disable: function(){ + this._toggleDisabled(true); + }, + + checkAll: function( e ){ + this._toggleChecked(true); + this._trigger('checkAll'); + }, + + uncheckAll: function(){ + this._toggleChecked(false); + this._trigger('uncheckAll'); + }, + + getChecked: function(){ + return this.menu.find('input').filter(':checked'); + }, + + destroy: function(){ + // remove classes + data + $.Widget.prototype.destroy.call( this ); + + this.button.remove(); + this.menu.remove(); + this.element.show(); + + return this; + }, + + isOpen: function(){ + return this._isOpen; + }, + + widget: function(){ + return this.menu; + }, + + getButton: function(){ + return this.button; + }, + + // react to option changes after initialization + _setOption: function( key, value ){ + var menu = this.menu; + + switch(key){ + case 'header': + menu.find('div.ui-multiselect-header')[ value ? 'show' : 'hide' ](); + break; + case 'checkAllText': + menu.find('a.ui-multiselect-all span').eq(-1).text(value); + break; + case 'uncheckAllText': + menu.find('a.ui-multiselect-none span').eq(-1).text(value); + break; + case 'height': + menu.find('ul').last().height( parseInt(value,10) ); + break; + case 'minWidth': + this.options[ key ] = parseInt(value,10); + this._setButtonWidth(); + this._setMenuWidth(); + break; + case 'selectedText': + case 'selectedList': + case 'noneSelectedText': + this.options[key] = value; // these all needs to update immediately for the update() call + this.update(); + break; + case 'classes': + menu.add(this.button).removeClass(this.options.classes).addClass(value); + break; + case 'multiple': + menu.toggleClass('ui-multiselect-single', !value); + this.options.multiple = value; + this.element[0].multiple = value; + this.refresh(); + } + + $.Widget.prototype._setOption.apply( this, arguments ); + } +}); + +})(jQuery); diff --git a/js/selectToUISlider.jQuery.js b/js/selectToUISlider.jQuery.js new file mode 100644 index 0000000..18704d7 --- /dev/null +++ b/js/selectToUISlider.jQuery.js @@ -0,0 +1,240 @@ +/* + * -------------------------------------------------------------------- + * jQuery-Plugin - selectToUISlider - creates a UI slider component from a select element(s) + * by Scott Jehl, scott@filamentgroup.com + * http://www.filamentgroup.com + * reference article: http://www.filamentgroup.com/lab/update_jquery_ui_16_slider_from_a_select_element/ + * demo page: http://www.filamentgroup.com/examples/slider_v2/index.html + * + * Copyright (c) 2008 Filament Group, Inc + * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. + * + * Usage Notes: please refer to our article above for documentation + * + * -------------------------------------------------------------------- + */ + + +jQuery.fn.selectToUISlider = function(settings){ + var selects = jQuery(this); + + //accessible slider options + var options = jQuery.extend({ + labels: 3, //number of visible labels + tooltip: true, //show tooltips, boolean + tooltipSrc: 'text',//accepts 'value' as well + labelSrc: 'value',//accepts 'value' as well , + sliderOptions: null + }, settings); + + + //handle ID attrs - selects each need IDs for handles to find them + var handleIds = (function(){ + var tempArr = []; + selects.each(function(){ + tempArr.push('handle_'+jQuery(this).attr('id')); + }); + return tempArr; + })(); + + //array of all option elements in select element (ignores optgroups) + var selectOptions = (function(){ + var opts = []; + selects.eq(0).find('option').each(function(){ + opts.push({ + value: jQuery(this).attr('value'), + text: jQuery(this).text() + }); + }); + return opts; + })(); + + //array of opt groups if present + var groups = (function(){ + if(selects.eq(0).find('optgroup').size()>0){ + var groupedData = []; + selects.eq(0).find('optgroup').each(function(i){ + groupedData[i] = {}; + groupedData[i].label = jQuery(this).attr('label'); + groupedData[i].options = []; + jQuery(this).find('option').each(function(){ + groupedData[i].options.push({text: jQuery(this).text(), value: jQuery(this).attr('value')}); + }); + }); + return groupedData; + } + else return null; + })(); + + //check if obj is array + function isArray(obj) { + return obj.constructor == Array; + } + //return tooltip text from option index + function ttText(optIndex){ + return (options.tooltipSrc == 'text') ? selectOptions[optIndex].text : selectOptions[optIndex].value; + } + + //plugin-generated slider options (can be overridden) + var sliderOptions = { + step: 1, + min: 0, + orientation: 'horizontal', + max: selectOptions.length-1, + range: selects.length > 1,//multiple select elements = true + slide: function(e, ui) {//slide function + var thisHandle = jQuery(ui.handle); + //handle feedback + var textval = ttText(ui.value); + thisHandle + .attr('aria-valuetext', textval) + .attr('aria-valuenow', ui.value) + .find('.ui-slider-tooltip .ttContent') + .text( textval ); + + //control original select menu + var currSelect = jQuery('#' + thisHandle.attr('id').split('handle_')[1]); + currSelect.find('option').eq(ui.value).attr('selected', 'selected'); + }, + values: (function(){ + var values = []; + selects.each(function(){ + values.push( jQuery(this).get(0).selectedIndex ); + }); + return values; + })() + }; + + //slider options from settings + options.sliderOptions = (settings) ? jQuery.extend(sliderOptions, settings.sliderOptions) : sliderOptions; + + //select element change event + selects.bind('change keyup click', function(){ + var thisIndex = jQuery(this).get(0).selectedIndex; + var thisHandle = jQuery('#handle_'+ jQuery(this).attr('id')); + var handleIndex = thisHandle.data('handleNum'); + thisHandle.parents('.ui-slider:eq(0)').slider("values", handleIndex, thisIndex); + }); + + + //create slider component div + var sliderComponent = jQuery('
      '); + + //CREATE HANDLES + selects.each(function(i){ + var hidett = ''; + + //associate label for ARIA + var thisLabel = jQuery('label[for=' + jQuery(this).attr('id') +']'); + //labelled by aria doesn't seem to work on slider handle. Using title attr as backup + var labelText = (thisLabel.size()>0) ? 'Slider control for '+ thisLabel.text()+'' : ''; + var thisLabelId = thisLabel.attr('id') || thisLabel.attr('id', 'label_'+handleIds[i]).attr('id'); + + + if( options.tooltip == false ){hidett = ' style="display: none;"';} + jQuery(''+labelText+''+ + ''+ + ''+ + '') + .data('handleNum',i) + .appendTo(sliderComponent); + }); + + //CREATE SCALE AND TICS + + //write dl if there are optgroups + if(groups) { + var inc = 0; + var scale = sliderComponent.append('').find('.ui-slider-scale:eq(0)'); + jQuery(groups).each(function(h){ + scale.append('
      '+this.label+'
      ');//class name becomes camelCased label + var groupOpts = this.options; + jQuery(this.options).each(function(i){ + var style = (inc == selectOptions.length-1 || inc == 0) ? 'style="display: none;"' : '' ; + var labelText = (options.labelSrc == 'text') ? groupOpts[i].text : groupOpts[i].value; + scale.append('
      '+ labelText +'
      '); + inc++; + }); + }); + } + //write ol + else { + var scale = sliderComponent.append('').find('.ui-slider-scale:eq(0)'); + jQuery(selectOptions).each(function(i){ + var style = (i == selectOptions.length-1 || i == 0) ? 'style="display: none;"' : '' ; + var labelText = (options.labelSrc == 'text') ? this.text : this.value; + scale.append('
    • '+ labelText +'
    • '); + }); + } + + function leftVal(i){ + return (i/(selectOptions.length-1) * 100).toFixed(2) +'%'; + + } + + + + + //show and hide labels depending on labels pref + //show the last one if there are more than 1 specified + if(options.labels > 1) sliderComponent.find('.ui-slider-scale li:last span.ui-slider-label, .ui-slider-scale dd:last span.ui-slider-label').addClass('ui-slider-label-show'); + + //set increment + var increm = Math.max(1, Math.round(selectOptions.length / options.labels)); + //show em based on inc + for(var j=0; j increm){//don't show if it's too close to the end label + sliderComponent.find('.ui-slider-scale li:eq('+ j +') span.ui-slider-label, .ui-slider-scale dd:eq('+ j +') span.ui-slider-label').addClass('ui-slider-label-show'); + } + } + + //style the dt's + sliderComponent.find('.ui-slider-scale dt').each(function(i){ + jQuery(this).css({ + 'left': ((100 /( groups.length))*i).toFixed(2) + '%' + }); + }); + + + //inject and return + sliderComponent + .insertAfter(jQuery(this).eq(this.length-1)) + .slider(options.sliderOptions) + .attr('role','application') + .find('.ui-slider-label') + .each(function(){ + jQuery(this).css('marginLeft', -jQuery(this).width()/2); + }); + + //update tooltip arrow inner color + sliderComponent.find('.ui-tooltip-pointer-down-inner').each(function(){ + var bWidth = jQuery('.ui-tooltip-pointer-down-inner').css('borderTopWidth'); + var bColor = jQuery(this).parents('.ui-slider-tooltip').css('backgroundColor') + jQuery(this).css('border-top', bWidth+' solid '+bColor); + }); + + var values = sliderComponent.slider('values'); + + if(isArray(values)){ + jQuery(values).each(function(i){ + sliderComponent.find('.ui-slider-tooltip .ttContent').eq(i).text( ttText(this) ); + }); + } + else { + sliderComponent.find('.ui-slider-tooltip .ttContent').eq(0).text( ttText(values) ); + } + + return this; +} + + diff --git a/rooster.php b/rooster.php new file mode 100644 index 0000000..d3c1289 --- /dev/null +++ b/rooster.php @@ -0,0 +1,45 @@ +iYear = (int) date('Y'); + $this->iWeek = (int) date('W'); + $this->setWeek($this->iYear, $this->iWeek); + } + + function setWeek($iYear, $iWeek) { + if ($iYear == self::SEMESTER1_YEAR && $iWeek >= self::SEMESTER1_WEEK && $iWeek <= 52) { + $iWeeks = $iWeek - self::SEMESTER1_WEEK + 1; + } else if ($iYear == self::SEMESTER2_YEAR && $iWeek >= 1 && $iWeek < self::SEMESTER2_WEEK) { + $iWeeks = $iWeek - self::SEMESTER2_WEEK + 52 + 1; + } else { + throw new Exception('Given week is out of range'); + } + $this->iYear = $iYear; + $this->iWeek = $iWeek; + $this->sWeek = $iWeeks; + } + + function nextWeek() { + if ($this->iWeek == 52) { + ++$this->iYear; + $this->iWeek = 0; + } + ++$this->iWeek; + } + + function getWeek() { + return array($this->iYear, $this->iWeek); + } + + abstract function getPage($sObject); + abstract function getData($sRooster = null); +} \ No newline at end of file diff --git a/style.php b/style.php new file mode 100644 index 0000000..cc64ef1 --- /dev/null +++ b/style.php @@ -0,0 +1,17 @@ + $aColor) { + printf(<< $sGroup) { + $sBar .= str_replace('{GROUP}', $sGroup, $sBarTemplate); + } + + $sBar = str_repeat($sBar, 5); + $sRooster = str_replace( + array('{COLUMNS}', '{WEEK}', '{GROUPS}', '{BAR}', '{BODY}'), + array(5 * $iGroups, $iWeek, $iGroups, $sBar, self::getRoosterBody($aTable, $aInfo)), + file_get_contents('template/rooster.html')); + + foreach (self::$aDays as $iDay => $sDay) { + $sRooster = str_replace(sprintf('{%s}', $sDay), date(self::DAY, getTime($iYear, $iWeek, $iDay)), $sRooster); + } + + return $sRooster; + } + + static function getRoosterBody($aTable, $aInfo) { + global $aConfig; + $sRowTemplate = file_get_contents('template/rooster-body-row.html'); + $sRowHourTemplate = file_get_contents('template/rooster-body-row-hour.html'); + $sRowMinuteTemplate = file_get_contents('template/rooster-body-row-minute.html'); + $sRowDataTemplate = file_get_contents('template/rooster-body-row-data.html'); + $iGroups = count($aGroups = array_keys($aTable)); + $sRoosterBody = ''; + for ($iHour = $aConfig['start']['hour']; $iHour <= $aConfig['end']['hour']; ++$iHour) { + if ($iHour == $aConfig['start']['hour']) { + $iMinute = $aConfig['start']['minute'] / $aConfig['step']; + $iSteps = $aConfig['steps'] - floor($iMinute); + } else { + $iMinute = 0; + $iSteps = $aConfig['steps']; + } + if ($iHour == $aConfig['end']['hour']) { + $iStop = $aConfig['end']['minute'] / $aConfig['step']; + $iSteps = ceil($iStop); + } else { + $iStop = $aConfig['steps']; + } + $bHour = true; + for (; $iMinute < $iStop; ++$iMinute) { + if ($bHour) { + $sRowHour = str_replace(array('{SPAN}', '{HOUR}'), array($iSteps, $iHour), $sRowHourTemplate); + $bHour = false; + } else { + $sRowHour = null; + } + $sRowMinute = str_replace('{MINUTE}', sprintf('%02d', $aConfig['step'] * $iMinute), $sRowMinuteTemplate); + $sRowData = ''; + for ($iDay = 0; $iDay < 5; ++$iDay) { + foreach ($aGroups as $iGroup => $sGroup) { + if (isset($aTable[$sGroup][$iDay][$iHour][$iMinute])) { + $iId = $aTable[$sGroup][$iDay][$iHour][$iMinute]; + $sText = $aInfo[$iId]['text']; + $sType = $aInfo[$iId]['type']; + } else { + $sText = null; + $sType = 'none'; + } + $sRowData .= str_replace( + array('{TYPE}', '{BORDER}', '{TITLE}'), + array($sType, $iGroup == $iGroups - 1 ? ' border' : null, $sText), + $sRowDataTemplate); + } + } + $sRoosterBody .= str_replace( + array('{HOUR}', '{MINUTE}', '{DATA}'), + array($sRowHour, $sRowMinute, $sRowData), $sRowTemplate); + } + } + return $sRoosterBody; + } + + static function getLegenda() { + global $aConfig; + $sRowTemplate = file_get_contents('template/legenda-row.html'); + $sData = ''; + foreach ($aConfig['colors'] as $sType => $aColor) { + $sData .= str_replace(array('{NAME}', '{TYPE}'), array($aColor[0], $sType), $sRowTemplate); + } + return str_replace('{BODY}', $sData, file_get_contents('template/legenda.html')); + } + + static function getFormWeeks() { + global $aConfig; + + $sOption = file_get_contents('template/form-option-selected.html'); + $aOptions = array(); + $iSelected = ceil($aConfig['weeks'] / 2); + for ($iWeeks = 1; $iWeeks <= $aConfig['weeks']; ++$iWeeks) { + $aOptions[] = str_replace( + array('{VALUE}', '{SELECTED}', '{TEXT}'), + array($iWeeks, $iWeeks == $iSelected ? ' selected="selected"' : null, $iWeeks), + $sOption); + } + return str_replace('{OPTIONS}', implode("\n", $aOptions), file_get_contents('template/form-weeks.html')); + } + + static function getFormWeek() { + global $aConfig; + $sOption = file_get_contents('template/form-option.html'); + + /* From */ + $aNow = now(); + $aOptions = self::getWeekOptions($sOption, $aConfig['year'], $aNow, $aConfig['week'], 53); + $sFrom = vsprintf('%d-%d', $aNow); + $aOptgroups = array(str_replace( + array('{LABEL}', '{OPTIONS}'), + array($aConfig['year'], implode("\n", $aOptions)), + file_get_contents('template/form-optgroup.html'))); + + /* To */ + $iWeeks = ceil($aConfig['weeks'] / 2); + $aNow = array( + $aNow[0] + ($aNow[1] + $iWeeks > 52 ? 1 : 0), + ($aNow[1] + $iWeeks) % 52); + $aOptions = self::getWeekOptions($sOption, $aConfig['year'] + 1, $aNow, 1, $aConfig['week']); + $sTo = vsprintf('%d-%d', $aNow); + + $aOptgroups[] = str_replace( + array('{LABEL}', '{OPTIONS}'), + array($aConfig['year'] + 1, implode("\n", $aOptions)), + file_get_contents('template/form-optgroup.html')); + $sOptions = implode("\n", $aOptgroups); + + return str_replace( + array('{OPTIONS-FROM}', '{OPTIONS-TO}'), + array( + str_replace($sFrom, sprintf('%s" selected="selected', $sFrom), $sOptions), + str_replace($sTo, sprintf('%s" selected="selected', $sTo), $sOptions)), + file_get_contents('template/form-week.html')); + } + + static function getWeekOptions($sOption, $iYear, $aNow, $iFrom, $iTo) { + $aOptions = array(); + for ($iWeek = $iFrom; $iWeek < $iTo; ++$iWeek) { + $aOptions[] = str_replace( + array('{VALUE}', '{TEXT}'), + array($sValue = sprintf('%d-%d', $iYear, $iWeek), $iWeek), + $sOption); + if ($aNow[0] && $iWeek == $aNow[1]) { + $sFrom = $sValue; + } + } + return $aOptions; + } + + static function getFormGroup() { + global $aConfig; + $aOptgroups = array(); + $sOption = file_get_contents('template/form-option.html'); + foreach ($aConfig['groups'] as $sCategory => $aGroups) { + $aOptions = array(); + foreach ($aGroups[1] as $sValue => $sText) { + $aOptions[] = str_replace( + array('{VALUE}', '{TEXT}'), + array($sValue, $sText), + $sOption); + } + $aOptgroups[] = str_replace( + array('{LABEL}', '{OPTIONS}'), + array($aGroups[0], implode("\n", $aOptions)), + file_get_contents('template/form-optgroup.html')); + } + return str_replace('{OPTIONS}', implode("\n", $aOptgroups), file_get_contents('template/form-group.html')); + } + + static function getForm() { + return str_replace( + array('{WEEKS}', '{WEEK}', '{GROUP}'), + array(self::getFormWeeks(), self::getFormWeek(), self::getFormGroup()), + file_get_contents('template/form.html')); + } +} \ No newline at end of file diff --git a/template/form-group.html b/template/form-group.html new file mode 100644 index 0000000..4658117 --- /dev/null +++ b/template/form-group.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/template/form-optgroup.html b/template/form-optgroup.html new file mode 100644 index 0000000..0964f9c --- /dev/null +++ b/template/form-optgroup.html @@ -0,0 +1,3 @@ + +{OPTIONS} + \ No newline at end of file diff --git a/template/form-option-selected.html b/template/form-option-selected.html new file mode 100644 index 0000000..d8188df --- /dev/null +++ b/template/form-option-selected.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/template/form-option.html b/template/form-option.html new file mode 100644 index 0000000..abb170f --- /dev/null +++ b/template/form-option.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/template/form-week.html b/template/form-week.html new file mode 100644 index 0000000..ffdd5bb --- /dev/null +++ b/template/form-week.html @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/template/form-weeks.html b/template/form-weeks.html new file mode 100644 index 0000000..e9527f3 --- /dev/null +++ b/template/form-weeks.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/template/form.html b/template/form.html new file mode 100644 index 0000000..e48783a --- /dev/null +++ b/template/form.html @@ -0,0 +1,31 @@ + + + + Gecombineerde roosters + + + + + + + + + + + + + + +
      + +
      + Instellingen +{WEEKS} +{WEEK} +{GROUP} +
      + +
      + + \ No newline at end of file diff --git a/template/legenda-row.html b/template/legenda-row.html new file mode 100644 index 0000000..12bd44c --- /dev/null +++ b/template/legenda-row.html @@ -0,0 +1,4 @@ + + {NAME} +   + diff --git a/template/legenda.html b/template/legenda.html new file mode 100644 index 0000000..660ee76 --- /dev/null +++ b/template/legenda.html @@ -0,0 +1,11 @@ + + + + + + + + + +{BODY} +
      Legenda
      TypeKleur
      \ No newline at end of file diff --git a/template/page.html b/template/page.html new file mode 100644 index 0000000..29d836d --- /dev/null +++ b/template/page.html @@ -0,0 +1,14 @@ + + + + Gecombineerde roosters + + + + +

      << Terug naar formulier

      +{LEGENDA} +{ROOSTER} + + \ No newline at end of file diff --git a/template/rooster-bar.html b/template/rooster-bar.html new file mode 100644 index 0000000..9829dfd --- /dev/null +++ b/template/rooster-bar.html @@ -0,0 +1 @@ + {GROUP} diff --git a/template/rooster-body-row-data.html b/template/rooster-body-row-data.html new file mode 100644 index 0000000..7da1c65 --- /dev/null +++ b/template/rooster-body-row-data.html @@ -0,0 +1 @@ +   diff --git a/template/rooster-body-row-hour.html b/template/rooster-body-row-hour.html new file mode 100644 index 0000000..b8afaab --- /dev/null +++ b/template/rooster-body-row-hour.html @@ -0,0 +1 @@ + {HOUR} diff --git a/template/rooster-body-row-minute.html b/template/rooster-body-row-minute.html new file mode 100644 index 0000000..08320ff --- /dev/null +++ b/template/rooster-body-row-minute.html @@ -0,0 +1 @@ + {MINUTE} diff --git a/template/rooster-body-row.html b/template/rooster-body-row.html new file mode 100644 index 0000000..ba02062 --- /dev/null +++ b/template/rooster-body-row.html @@ -0,0 +1,2 @@ + +{HOUR}{MINUTE}{DATA} diff --git a/template/rooster.html b/template/rooster.html new file mode 100644 index 0000000..a5c90b8 --- /dev/null +++ b/template/rooster.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + +{BAR} + + +{BODY} +
      Rooster week {WEEK}
      Ma
      ({MONDAY})
      Di
      ({TUESDAY})
      Wo
      ({WEDNESDAY})
      Do
      ({THURSDAY})
      Vr
      ({FRIDAY})
       
      diff --git a/test.php b/test.php new file mode 100644 index 0000000..2fb4597 --- /dev/null +++ b/test.php @@ -0,0 +1,76 @@ +clean(); + +$iYear = 2011; +$iWeek = 36; +$iWeeks = $aConfig['weeks']; +$aGroups = array($aConfig['filters']['S'], $aConfig['filters']['F']); + +$aWeeks = array(); + +try { + $aGroups = $oCache->getGroups($aGroups); + $oRooster->setWeek($iYear, $iWeek); + $aData = array(); + for ($i = 0; $i < $iWeeks; ++$i) { + $aWeeks[] = $oRooster->getWeek(); + foreach ($aGroups as $sGroup => $sName) { + try { + $aData[$sName] = $oCache->getData($sGroup); + } catch (Exception $e) { + die(printf('
      ERROR: %s
      ', $e->getMessage())); + } + } + $oRooster->nextWeek(); + } +} catch (Exception $e) { + die(printf('
      ERROR: %s
      ', $e->getMessage())); +} + +convertData($aData, $aTable, $aInfo); +echo Template::getWeeks($aTable, $aInfo, $aWeeks); + + +$oRooster = new Rooster; +$oCache = new Cache($oRooster); +$oCache->clean(true); + +if (isset($_GET['group'])) { + if (isset($aConfig['filters'][$_GET['group']])) { + $aGroups = array_flip($oCache->getGroups(array($aConfig['filters'][$_GET['group']]))); + if (isset($aGroups[$_GET['group']])) { + $iYear = isset($_GET['year']) ? $_GET['year'] : null; + $iWeek = isset($_GET['week']) ? $_GET['week'] : null; + try { + die($oCache->getRooster($aGroups[$_GET['group']], $iYear, $iWeek)); + } catch (Exception $e) { + die('Ongeldige week!'); + } + } + } + die('Ongeldige groep!'); +} + +try { + $aGroups = $oCache->getGroups(array($aConfig['filters']['S'])); + $oRooster->setWeek(2012, 10); + $aData = array(); + for ($i = 0; $i < $aConfig['weeks']; ++$i) { + foreach ($aGroups as $sGroup => $sName) { + try { + $aData[$sName] = $oCache->getData($sGroup); + } catch (Exception $e) { + printf('
      ERROR: %s
      ', $e->getMessage()); + } + } + $oRooster->nextWeek(); + } + file_put_contents('temp', serialize($aData)); +} catch (Exception $e) { + printf('
      ERROR: %s
      ', $e->getMessage()); +} \ No newline at end of file diff --git a/uvarooster.php b/uvarooster.php new file mode 100644 index 0000000..c8928a1 --- /dev/null +++ b/uvarooster.php @@ -0,0 +1,77 @@ + 'posbydayurl', + 'idstring' => $sObject, + 'weeks' => $this->sWeek))); + + if (($this->sContents = file_get_contents($sUrl)) === false) { + throw new Exception('Failed to load page'); + } + return $this->sContents; + } + + function getData($sRooster = null) { + $sRooster = isset($sRooster) ? $sRooster : $this->sContents; + if (substr_count($sRooster, '') != 7) { + echo $sRooster; + throw new Exception('Page does not contain valid data'); + } + $aDays = explode('', $sRooster); + array_shift($aDays); + array_pop($aDays); + array_pop($aDays); + $aData = array(); + foreach ($aDays as $iDay => $sDay) { + if (strpos($sDay, '') !== false) { + $aColumns = null; + $aRows = explode('', $sDay); + array_shift($aRows); + array_pop($aRows); + $sHeader = array_shift($aRows); + if (!isset($aColumns)) { + foreach (explode("\n", strip_tags($sHeader)) as $iColumn => $sColumn) { + switch (trim($sColumn)) { + case 'Start': + $aColumns[$iColumn] = 'start'; + break; + case 'Eind': + $aColumns[$iColumn] = 'end'; + break; + case 'Vak': + $aColumns[$iColumn] = 'course'; + break; + case 'Type': + $aColumns[$iColumn] = 'type'; + break; + case 'Locatie': + $aColumns[$iColumn] = 'room'; + break; + } + } + } + + $aData[$iDay] = array(); + foreach ($aRows as $sRow) { + $aInfo = array(); + $aRow = explode("\n", strip_tags($sRow)); + foreach ($aColumns as $iColumn => $sColumn) { + if (isset($aRow[$iColumn])) { + $aRow[$iColumn] = trim($aRow[$iColumn]); + $aInfo[$sColumn] = $sColumn == 'course' + ? preg_replace('~[\s]+\([^\)]+\)$~', null, $aRow[$iColumn]) + : $aRow[$iColumn]; + } + } + $aData[$iDay][] = $aInfo; + } + } + } + return $aData; + } +} \ No newline at end of file diff --git a/vurooster.php b/vurooster.php new file mode 100644 index 0000000..46b843e --- /dev/null +++ b/vurooster.php @@ -0,0 +1,187 @@ +sCookieFile = tempnam('tmp', 'curl'); + $this->rCurl = curl_init(); + curl_setopt_array($this->rCurl, array( + CURLOPT_COOKIESESSION => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_REFERER => true, + CURLOPT_COOKIEJAR => $this->sCookieFile, + CURLOPT_COOKIEFILE => $this->sCookieFile, + CURLOPT_URL => self::URL_SEMESTER)); + parent::__construct(); + } + + function __destruct() { + curl_close($this->rCurl); + unlink($this->sCookieFile); + } + + function setWeek($iYear, $iWeek) { + parent::setWeek($iYear, $iWeek); + $this->sWeek = sprintf('+%d', $this->sWeek); + curl_setopt($this->rCurl, CURLOPT_POST, false); + $this->execute(); + $this->bOnGroupsPage = false; + } + + function buildQuery($aFields) { + $aQuery = array(); + foreach ($aFields as $sKey => $mValue) { + $aQuery[] = sprintf('%s=%s', $sKey, $mValue); + } + return implode('&', $aQuery); + } + + function getAspFields() { + if ($this->bOnGroupsPage) { + return $this->aAspFields; + } + preg_match('~id="__VIEWSTATE" value="([^"]+)"~i', $this->sContents, $aViewState); + preg_match('~id="__EVENTVALIDATION" value="([^"]+)"~i', $this->sContents, $aEventValidation); + if (count($aViewState) < 2 || count($aEventValidation) < 2) { + throw new Exception('Failed to get asp fields from contents'); + } + return $this->aAspFields = array( + '__VIEWSTATE' => urlencode($aViewState[1]), + '__EVENTVALIDATION' => urlencode($aEventValidation[1])); + } + + function setPostFields($aPostFields) { + try { + $aAspFields = $this->getAspFields(); + } catch (Exception $e) { + throw new Exception('Failed to get required asp fields from contents'); + } + $sQuery = $this->buildQuery(array_merge($aAspFields, $aPostFields)); + curl_setopt($this->rCurl, CURLOPT_POSTFIELDS, $sQuery); + } + + function execute() { + if (!($sContents = curl_exec($this->rCurl))) { + throw new Exception('Failed to execute request'); + } + $this->sContents = $sContents; + } + + function loadGroupsPage() { + if ($this->bOnGroupsPage) { + return; + } + try { + /* Enter group selection mode */ + $this->setPostFields(array( + '__EVENTTARGET' => 'LinkBtn_studentsets')); + $this->execute(); + + /* Stay on groups page */ + $this->getAspFields(); + $this->bOnGroupsPage = true; + } catch (Exception $e) { + throw new Exception('Failed to navigate to groups page'); + } + } + + function getGroups($aFilters) { + $this->loadGroupsPage(); + preg_match('~', $sPart); + $sPart = $aPart[0]; + $aGroups = array(); + foreach ($aFilters as $sFilter) { + $sRegex = sprintf('~"([^"]+)">%s~i', $sFilter); + preg_match_all($sRegex, $sPart, $aMatches, PREG_SET_ORDER); + foreach ($aMatches as $aMatch) { + $aGroups[$aMatch[1]] = $aMatch[2]; + } + } + return $aGroups; + } + + function getPage($sObject) { + try { + $this->loadGroupsPage(); + + /* Select group and period */ + $this->setPostFields(array( + 'tLinkType' => 'studentsets', + 'dlObject' => urlencode($sObject), + 'lbWeeks' => $this->sWeek, + 'dlType' => urlencode('TextSpreadsheet;SWS_Groep'), + 'bGetTimetable' => null)); + + $this->execute(); + return $this->sContents; + } catch (Exception $e) { + throw new Exception('Failed to load page'); + } + } + + function getData($sRooster = null) { + $aDays = explode('sContents); + if (count($aDays) != 8) { + throw new Exception('Page does not contain valid data'); + } + array_shift($aDays); + array_pop($aDays); + array_pop($aDays); + $aData = array(); + foreach ($aDays as $iDay => $sDay) { + $aColumns = null; + $aRows = explode('', $sDay); + $sHeader = array_shift($aRows); + if (!isset($aColumns)) { + preg_match_all('~~', $sHeader, $aMatches); + foreach ($aMatches[1] as $iColumn => $sColumn) { + switch ($sColumn) { + case 'Start': + $aColumns[$iColumn] = 'start'; + break; + case 'Einde': + $aColumns[$iColumn] = 'end'; + break; + case 'Vaknaam': + $aColumns[$iColumn] = 'course'; + break; + case 'Type': + $aColumns[$iColumn] = 'type'; + break; + case 'Zalen': + $aColumns[$iColumn] = 'room'; + break; + } + } + } + $aData[$iDay] = array(); + foreach ($aRows as $sRow) { + $sRow = str_replace('
      ', null, $sRow); + preg_match_all('~~', $sRow, $aMatches); + $aInfo = array(); + foreach ($aColumns as $iColumn => $sColumn) { + $aInfo[$sColumn] = $aMatches[1][$iColumn]; + } + $aData[$iDay][] = $aInfo; + } + } + return $aData; + } +} \ No newline at end of file
      ([^>]*)([^>]*)