diff --git a/schedule/class.SmartCurl.php b/schedule/class.SmartCurl.php index 17c3f52..455f6c6 100644 --- a/schedule/class.SmartCurl.php +++ b/schedule/class.SmartCurl.php @@ -12,7 +12,7 @@ class SmartCurl { } if (!file_exists($this->cache_dir)) { - mkdir($this->cache_dir); + mkdir($this->cache_dir, 0777, true); } $this->cache_index = $this->cache_dir . '.json'; @@ -71,17 +71,16 @@ class SmartCurl { } if (curl_setopt($this->ch, CURLOPT_URL, $url) === false) { - throw new Exception('set url failed'); + throw new Exception('set url failed: ' . $url); } - $cache_file = $this->cache_dir . DIRECTORY_SEPARATOR . $filename; - + $cache_file = $this->cache_dir . DIRECTORY_SEPARATOR . str_replace('/', '@', $filename); $etag = array_key_exists($url, static::$etags) && file_exists($cache_file) ? static::$etags[$url] : null; if (curl_setopt($this->ch, CURLOPT_HTTPHEADER, [ 'If-None-Match:' . (is_null($etag) ? '' : ' ' . $etag), ]) === false) { - throw new Exception('set etag failed'); + throw new Exception('set etag failed: ' . $url); } $response = curl_exec($this->ch); @@ -128,12 +127,6 @@ class SmartCurl { $body = substr($response, $header_size); if ($http_code === 200) { - $dirname = dirname($filename); - - if ($dirname !== '.') { - mkdir($this->cache_dir . DIRECTORY_SEPARATOR . $dirname, 0777, true); - } - file_put_contents($cache_file, $body); } diff --git a/schedule/config.php b/schedule/config.php new file mode 100644 index 0000000..9149f4d --- /dev/null +++ b/schedule/config.php @@ -0,0 +1,38 @@ + 'bg', + 'cfp_url' => 'https://cfp.openfest.org', + 'cut_len' => 70, + 'hidden_speakers' => [4], + 'hidden_language_tracks' => [], + ]; + + $config = [ + 2014 => [ + 'conferenceId' => 1, + 'eventTypes' => [ + 'lecture' => 1, + 'workshop' => 2, + ], + ], + 2015 => [ + 'conferenceId' => 2, + 'eventTypes' => [ + 'lecture' => 3, + 'workshop' => 4, + ], + 'hidden_language_tracks' => [16], + ], + 2016 => [ + 'conferenceId' => 3, + 'eventTypes' => [ + 'lecture' => 5, + 'workshop' => 6, + ], + 'hidden_language_tracks' => [25], + ], + ]; + + return array_merge($globalConfig, $config[$year]); +} diff --git a/schedule/index.php b/schedule/index.php index 32fc571..b6e78da 100644 --- a/schedule/index.php +++ b/schedule/index.php @@ -1,49 +1,39 @@ - - -Test schedule - - - -
-
-
- - - - - - - - - - - - -
 
-
- + + + Test schedule + + + + + +
+ + + + +
+ + + diff --git a/schedule/load.php b/schedule/load.php index f34503e..a33769a 100644 --- a/schedule/load.php +++ b/schedule/load.php @@ -1,59 +1,4 @@ 'events.json', - 'speakers' => 'speakers.json', - 'tracks' => 'tracks.json', - 'event_types' => 'event_types.json', - 'halls' => 'halls.json', - 'slots' => 'slots.json', -]; - - -$data = []; - -foreach ($filenames as $name => $filename) { - $curl = new SmartCurl($base_url, 'cache' . DIRECTORY_SEPARATOR .$CF['confid']); - $json = $curl->getUrl($filename); - - if ($json === false) { - // echo 'get failed: ', $filename, ' ', $base_url, PHP_EOL; - return null; - } - - $decoded = json_decode($json, true); - - if ($decoded === false) { - echo 'decode failed: ', $filename, PHP_EOL; - exit; - } - - $add = true; - switch ($name) { - case 'halls': - $ret = array(); - foreach($decoded as $id => $hall) { - if (in_array($id, $CF['allowedhallids'])) $ret[$id] = $hall['name']; - } - $decoded = $ret; - break; - case 'slots': - $decoded = array_map(function($el) { - foreach (['starts_at', 'ends_at'] as $key) { - $el[$key] = strtotime($el[$key]); - } - - return $el; - }, $decoded); - break; - } - - $data[$name] = $decoded; -} - function compareKeys($a, $b, $key) { $valA = &$a[$key]; $valB = &$b[$key]; @@ -61,10 +6,59 @@ function compareKeys($a, $b, $key) { return ($valA < $valB) ? -1 : (($valA > $valB) ? 1 : 0); } -uasort($data['slots'], function($a, $b) { - return compareKeys($a, $b, 'starts_at') ?: compareKeys($a, $b, 'hall_id'); -}); +function loadData($config) { + $filenames = [ + 'events' => 'events.json', + 'speakers' => 'speakers.json', + 'tracks' => 'tracks.json', + 'event_types' => 'event_types.json', + 'halls' => 'halls.json', + 'slots' => 'slots.json', + ]; -//array_pop($data['halls']); + $data = []; + $curl = new SmartCurl($config['cfp_url'] . '/api/conferences/'); + + foreach ($filenames as $name => $filename) { + $json = $curl->getUrl($config['conferenceId'] . '/' . $filename); -return $data; + if ($json === false) { + echo 'get failed: ', $filename, PHP_EOL; + exit; + } + + $decoded = json_decode($json, true); + + if ($decoded === false) { + echo 'decode failed: ', $filename, PHP_EOL; + exit; + } + + $add = true; + + switch ($name) { + case 'halls': + $decoded = array_map(function($el) { + return $el['name']; + }, $decoded); + break; + case 'slots': + $decoded = array_map(function($el) { + foreach (['starts_at', 'ends_at'] as $key) { + $el[$key] = strtotime($el[$key]); + } + + return $el; + }, $decoded); + break; + } + + $data[$name] = $decoded; + } + + uasort($data['slots'], function($a, $b) { + return compareKeys($a, $b, 'starts_at') ?: compareKeys($a, $b, 'hall_id'); + }); + + return $data; +} diff --git a/schedule/parse.php b/schedule/parse.php index 6ab9345..4c38906 100644 --- a/schedule/parse.php +++ b/schedule/parse.php @@ -1,169 +1,383 @@ - array('name' => 'English', 'locale' => 'en_US.UTF8'), 'bg' => array ('name' => 'Български', 'locale' => 'bg_BG.UTF8')); - -$cut_len = 70; -$cfp_url = 'http://cfp.openfest.org'; -$time = 0; -$date = 0; -$lines = []; -$fulltalks = []; -$prev_event_id = 0; -$colspan = 1; -$hall_ids = array_keys($data['halls']); -$first_hall_id = min($hall_ids); -$last_hall_id = max($hall_ids); - -/* We need to set these so we actually parse properly the dates. WP fucks up both. */ -date_default_timezone_set('Europe/Sofia'); -setlocale(LC_TIME, $languages[$CF['lang']]['locale']); - -foreach ($data['slots'] as $slot_id => $slot) { - if (! in_array($slot['hall_id'], $CF['allowedhallids'])) continue; - $slotTime = $slot['starts_at']; - $slotDate = date('d', $slotTime); - - if ($slotDate !== $date) { - /* this seems to be the easiest way to localize the date */ - $localdate = strftime('%d %B - %A' ,$slotTime); - $lines[] = ''; - $lines[] = '' . $localdate . ''; - $lines[] = ' '; - $lines[] = ''; - - $date = $slotDate; - } - - if ($slotTime !== $time) { - if ($time !== 0) { - $lines[] = ''; - } - - $lines[] = ''; - $lines[] = '' . date('H:i', $slot['starts_at']) . ' - ' . date('H:i', $slot['ends_at']) . ''; - - $time = $slotTime; - } - - $eid = &$slot['event_id']; - $event = &$data['events'][$eid]; - - if (is_null($eid)) { - $lines[] = 'TBA'; - } - else { - $title = mb_substr($event['title'], 0, $cut_len) . (mb_strlen($event['title']) > $cut_len ? '...' : ''); - $speakers = ''; - - if (count($event['participant_user_ids']) > 0) { - $speakers = json_encode($event['participant_user_ids']) . '
'; - - $spk = array(); - $speaker_name = array(); - foreach ($event['participant_user_ids'] as $uid) { - /* The check for uid==4 is for us not to show the "Opefest Team" as a presenter for lunches, etc. */ - if ($uid == 4 || empty ($data['speakers'][$uid])) { - continue; - } else { - /* TODO: fix the URL */ - $name = $data['speakers'][$uid]['first_name'] . ' ' . $data['speakers'][$uid]['last_name']; - $spk[$uid] = '' . $name . ''; - } - } - $speakers = implode (', ', $spk); - } - - - /* Hack, we don't want language for the misc track. This is the same for all years. */ - if ('misc' !== $data['tracks'][$event['track_id']]['name']['en']) { - $csslang = "schedule-".$event['language']; - } else { - $csslang = ""; - } - $cssclass = &$data['tracks'][$event['track_id']]['css_class']; - $style = ' class="' . $cssclass . ' ' . $csslang . '"'; - $content = '' . htmlspecialchars($title) . '
' . $speakers; - - - /* these are done by $eid, as otherwise we get some talks more than once (for example the lunch) */ - $fulltalks[$eid] = ''; - $fulltalks[$eid] .= '
'; - /* We don't want '()' when we don't have a speaker name */ - $fulltalk_spkr = strlen($speakers)>1 ? ' (' . $speakers . ')' : ''; - $fulltalks[$eid] .= '

' . $event['title'] . ' ' . $fulltalk_spkr . ''; - $fulltalks[$eid] .= '

' . $event['abstract'] . '

'; - $fulltalks[$eid] .= '
'; - - if ($slot['event_id'] === $prev_event_id) { - array_pop($lines); - $lines[] = '' . $content . ''; - } - else { - $lines[] = '' . $content . ''; - $colspan = 1; - } - } - - $prev_event_id = $slot['event_id']; -} - -$lines[] = ''; -/* create the legend */ - -$legend = []; - -foreach($data['tracks'] as $track) { - $legend[] = '' . $track['name'][$CF['lang']] . ''; -} -foreach ($languages as $l => $n) { - $legend[] = '' . $n['name'] . ''; -} - -$gspk = []; -$fspk = []; -$types = []; -$types['twitter']['url']='https://twitter.com/'; -$types['twitter']['class']='fa fa-twitter'; -$types['github']['url']='https://github.com/'; -$types['github']['class']='fa fa-github'; -$types['email']['url']='mailto:'; -$types['email']['class']='fa fa-envelope'; - -$gspk[] = '
'; - -foreach ($data['speakers'] as $speaker) { - $name = $speaker['first_name'] . ' ' . $speaker['last_name']; - - $gspk[] = ''; - - $fspk[] = '
'; - $fspk[] = '' . $name .''; - $fspk[] = '

' . $name . '

'; - $fspk[] = '
'; - foreach ($types as $type => $parm) { - if (!empty($speaker[$type])) { - $fspk[] = ''; - } - } - $fspk[] = '
'; - $fspk[] = '

' . $speaker['biography'] . '

'; - $fspk[] = '
'; -} - -$gspk[] = '
'; - -return array_merge($data, compact('lines', 'fulltalks', 'gspk', 'fspk', 'legend')); + array( + 'name' => 'English', + 'locale' => 'en_US.UTF8' + ), + 'bg' => array( + 'name' => 'Български', + 'locale' => 'bg_BG.UTF8' + ) + ); + + // We need to set these so we actually parse properly the dates. WP fucks up both. + date_default_timezone_set('Europe/Sofia'); + setlocale(LC_TIME, $languages[$config['lang']]['locale']); + + // Filter out invalid slots + $data['slots'] = array_filter($data['slots'], function($slot) { + return isset($slot['starts_at'], $slot['ends_at'], $slot['hall_id'], $slot['event_id']); + }); + + // Collect the slots for each hall, sort them in order of starting + $slots = []; + $timestamps = []; + + foreach ($data['halls'] as $hall_id => $hall) { + $slots[$hall_id] = []; + + foreach ($data['slots'] as $slot_id => $slot) { + if ($slot['hall_id'] !== $hall_id) { + continue; + } + + if (!in_array($slot['starts_at'], $timestamps)) { + $timestamps[] = $slot['starts_at']; + } + + if (!in_array($slot['ends_at'], $timestamps)) { + $timestamps[] = $slot['ends_at']; + } + + $slots[$hall_id][$slot['starts_at']] = $slot; + } + + ksort($slots[$hall_id]); + } + + sort($timestamps); + + // Find all microslots (the smallest time unit) + $microslots = []; + $lastTs = 0; + $first = true; + + foreach ($timestamps as $ts) { + if ($first) { + $lastTs = $ts; + $first = false; + continue; + } + + if (date('d.m', $lastTs) !== date('d.m', $ts)) { + $lastTs = $ts; + continue; + } + + $microslots[] = [$lastTs, $ts]; + $lastTs = $ts; + } + + // Fill in the event ID for each time slot in each hall + $events = []; + $filtered_type_id = + array_key_exists('filterEventType', $config) && + array_key_exists($config['filterEventType'], $config['eventTypes']) ? + $config['eventTypes'][$config['filterEventType']] : + null; + + foreach ($data['halls'] as $hall_id => $hall) { + $hall_data = []; + + foreach ($microslots as $timestamps) { + $found = false; + + foreach ($data['slots'] as $slot_id => $slot) { + if ( + $slot['hall_id'] === $hall_id && + $slot['starts_at'] <= $timestamps[0] && + $slot['ends_at'] >= $timestamps[1] && + array_key_exists($slot['event_id'], $data['events']) + ) { + if (!is_null($filtered_type_id)) { + if ($data['events'][$slot['event_id']]['event_type_id'] !== $filtered_type_id) { + continue; + } + } + + $found = true; + $hall_data[] = [ + 'event_id' => $slot['event_id'], + 'hall_id' => $slot['hall_id'], + 'edge' => $slot['starts_at'] === $timestamps[0] || $slot['ends_at'] === $timestamps[1], + ]; + break; + } + } + + if (!$found) { + $hall_data[] = null; + } + } + + $events[] = $hall_data; + } + + // Remove halls with no events after filtering + $count = count($events); + for ($i = 0; $i < $count; ++$i) { + $hasEvents = false; + foreach ($events[$i] as $event_info) { + if (!is_null($event_info)) { + $hasEvents = true; + break; + } + } + if (!$hasEvents) { + unset($events[$i]); + } + } + + // Transpose the matrix + // rows->halls, cols->timeslots ===> rows->timeslots, cols->halls + $events = array_map(null, ...$events); + + // Filter empty slots + $count = count($events); + for ($i = 0; $i < $count; ++$i) { + $hall_count = count($events[$i]); + $hasEvents = false; + + for ($j = 0; $j < $hall_count; ++$j) { + if (!is_null($events[$i][$j]) && $events[$i][$j]['edge']) { + $hasEvents = true; + continue 2; + } + } + + if (!$hasEvents) { + unset($events[$i]); + } + } + + // Merge events longer than one slot + $prevEventId = []; + $prevEventSlot = []; + $prevSlotIndex = 0; + $first = true; + + foreach ($events as $slot_index => &$events_data) { + if ($first) { + $prevEventId = array_map(function($event_info) { + return is_null($event_info) ? null : $event_info['event_id']; + }, $events_data); + $prevEventSlot = array_fill(0, count($events_data), null); + $prevSlotIndex = $slot_index; + $first = false; + continue; + } + + foreach ($events_data as $hall_index => &$event_info) { + if (is_null($event_info)) { + $prevEventId[$hall_index] = null; + $prevEventSlot[$hall_index] = null; + continue; + } + + if ($event_info['event_id'] !== $prevEventId[$hall_index]) { + $prevEventId[$hall_index] = $event_info['event_id']; + $prevEventSlot[$hall_index] = null; + continue; + } + + // We have a long event + if (is_null($prevEventSlot[$hall_index])) { + $prevEventSlot[$hall_index] = $prevSlotIndex; + } + + $master_slot = &$events[$prevEventSlot[$hall_index]][$hall_index]; + + if (!array_key_exists('rowspan', $master_slot)) { + $master_slot['rowspan'] = 2; + } + else { + ++$master_slot['rowspan']; + } + + unset($master_slot); + + $event_info = false; + } + + unset($event_info); + + $prevSlotIndex = $slot_index; + } + + unset($events_data); + + // Build the HTML + $schedule_body = ''; + $lastTs = 0; + $fulltalks = ''; + $hall_ids = []; + + foreach ($events as $slot_index => $events_data) { + $columns = []; + + if (date('d.m', $microslots[$slot_index][0]) !== date('d.m', $lastTs)) { + $schedule_body .= '' . strftime('%d %B - %A', $microslots[$slot_index][0]) . ''; + } + + $lastTs = $microslots[$slot_index][0]; + $lastEventId = 0; + $colspan = 1; + + foreach ($events_data as $event_info) { + if ($event_info === false) { + continue; + } + + if (is_null($event_info['event_id'])) { + $columns[] = ' '; + continue; + } + + if (!in_array($event_info['hall_id'], $hall_ids)) { + $hall_ids[] = $event_info['hall_id']; + } + + $eid = &$event_info['event_id']; + $event = &$data['events'][$eid]; + + $title = mb_substr($event['title'], 0, $config['cut_len']) . (mb_strlen($event['title']) > $config['cut_len'] ? '...' : ''); + $speakers = ''; + + if (count($event['participant_user_ids']) > 0) { + $spk = []; + + foreach ($event['participant_user_ids'] as $uid) { + if (in_array($uid, $config['hidden_speakers']) || empty($data['speakers'][$uid])) { + continue; + } + + $name = $data['speakers'][$uid]['first_name'] . ' ' . $data['speakers'][$uid]['last_name']; + $spk[] = '' . $name . ''; + } + + $speakers = implode (', ', $spk); + } + + $content = '' . htmlspecialchars($title) . '
' . $speakers; + + // these are done by $eid, as otherwise we get some talks more than once (for example the lunch) + // TODO: fix this, it's broken + $fulltalks .= '
'; + + // We don't want '()' when we don't have a speaker name + $fulltalk_spkr = strlen($speakers) > 0 ? (' (' . $speakers . ')') : ''; + $fulltalks .= '

' . $event['title'] . ' ' . $fulltalk_spkr . '

'; + $fulltalks .= '

' . $event['abstract'] . '

'; + $fulltalks .= '
'; + + if ($eid === $lastEventId) { + array_pop($columns); + ++$colspan; + } + else { + $colspan = 1; + } + + $rowspan = array_key_exists('rowspan', $event_info) ? (' rowspan="' . $event_info['rowspan'] . '"') : ''; + + // CSS + $cssClasses = []; + + if (!in_array($event['track_id'], $config['hidden_language_tracks'])) { + $cssClasses[] = 'schedule-' . $event['language']; + } + + $cssClass = $data['tracks'][$event['track_id']]['css_class']; + + if (strlen($cssClass) > 0) { + $cssClasses[] = $cssClass; + } + + $cssClasses = count($cssClasses) > 0 ? (' class="' . implode(' ', $cssClasses) . '"') : ''; + + // Render cell + $columns[] = ' 1 ? ' colspan="' . $colspan . '"' : $rowspan) . $cssClasses . '>' . $content . ''; + + $lastEventId = $eid; + unset($eid, $event); + } + + $schedule_body .= ''; + $schedule_body .= strftime('%H:%M', $microslots[$slot_index][0]) . ' - ' . strftime('%H:%M', $microslots[$slot_index][1]); + $schedule_body .= ''; + $schedule_body .= implode('', $columns); + $schedule_body .= ''; + } + + $schedule = ''; + + foreach ($data['halls'] as $hall_id => $hall) { + if (!in_array($hall_id, $hall_ids)) { + continue; + } + + $schedule .= ''; + } + + $schedule .= ''; + $schedule .= $schedule_body; + $schedule .= '
' . $hall['bg'] . '
'; + + // Create the legend + $legend = ''; + + foreach($data['tracks'] as $track) { + $legend .= '' . $track['name'][$config['lang']] . ''; + } + + foreach ($languages as $code => $lang) { + $legend .= '' . $lang['name'] . ''; + } + + // Speaker list + $gspk = '
'; + $fspk = ''; + $types = [ + 'twitter' => [ + 'class' => 'twitter', + 'url' => 'https://twitter.com/', + ], + 'github' => [ + 'class' => 'github', + 'url' => 'https://github.com/', + ], + 'email' => [ + 'class' => 'envelope', + 'url' => 'mailto:', + ], + ]; + + foreach ($data['speakers'] as $speaker) { + $name = $speaker['first_name'] . ' ' . $speaker['last_name']; + + $gspk .= ''; + + $fspk .= '
'; + $fspk .= '' . $name . ''; + $fspk .= '

' . $name . '

'; + $fspk .= '
'; + + foreach ($types as $type => $param) { + if (!empty($speaker[$type])) { + $fspk .= ''; + } + } + + $fspk .= '
'; + $fspk .= '

' . $speaker['biography'] . '

'; + $fspk .= '
'; + } + + $gspk .= '
'; + + return compact('schedule', 'fulltalks', 'gspk', 'fspk', 'legend'); +}