# HG changeset patch # User Nathan Phillip Brink # Date 2011-03-22 16:23:52 # Node ID 9fd95ed08e56196482516a86a8ca82b311436693 # Parent 34aabb706e1834dcff663f5b7bc94ddd48b357da Load the course registration codes dialogue from an AJAX callback. Allow individual schools to override the content of the Registration Codes dialogue. diff --git a/ajax.php b/ajax.php new file mode 100644 --- /dev/null +++ b/ajax.php @@ -0,0 +1,102 @@ + + * + * This file is a part of slate_permutate. + * + * slate_permutate is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * slate_permutate is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with slate_permutate. If not, see . + */ + +/** + * \file + * This file is an endpoint for generic AJAX requests (as opposed to + * autocompletion of course names). + */ + +require_once('inc/school.inc'); +require_once('inc/class.page.php'); +require_once('inc/class.course.inc'); + +page::session_start(); + +/* should the following block of code be moved into a proposed Page::json()? */ +if (isset($_REQUEST['txt'])) { + header('Content-Type: text/plain; encoding=utf-8'); +} +else { + header('Content-Type: application/json; encoding=utf-8'); +} + +/** + * \brief + * Wrap an error message up in JSON and send it. + * + * \param $message + * A valid XHTML fragment which will be wrapped in
+ */ +function slate_permutate_json_error($message) +{ + echo json_encode(array('success' => FALSE, 'message' => '
' . $message . '
')); + exit; +} + +/** + * \brief + * Send a successful JSON response. + * + * \param $data + * A PHP array to be encoded with json_encode() and sent as + * obj.data. + */ +function slate_permutate_json_success(array $data = array()) +{ + echo json_encode(array('success' => TRUE, 'data' => $data)); + exit; +} + +if (isset($_REQUEST['school_registration_html'])) + { + if (!empty($_REQUEST['school'])) + $school = school_load($_REQUEST['school']); + if (empty($school)) + { + $school = school_load_guess(); + if (empty($school)) + slate_permutate_json_error('Unable to load any school.'); + } + + $page = new Page('', array(), FALSE); + + $courses = array(); + if (!empty($_REQUEST['courses']) && is_array($_REQUEST['courses'])) + { + /* + * The below course deserialization blob should be moved into + * the Course object. + */ + foreach ($_REQUEST['courses'] as $course_json) + { + $course = Course::from_json_array($course_json); + if (!empty($course)) + $courses[] = $course; + } + } + + $html = school_registration_html($page, $school, $courses); + if (empty($html)) + slate_permutate_json_error('School\'s registration information producer returned no data.'); + slate_permutate_json_success(array('html' => $html)); + } + +slate_permutate_json_error('Unrecognized command.'); diff --git a/inc/class.course.inc b/inc/class.course.inc --- a/inc/class.course.inc +++ b/inc/class.course.inc @@ -26,7 +26,7 @@ include_once 'class.section.php'; -class Course +class Course implements IteratorAggregate { private $name; // String private $sections; // Array of sections @@ -65,6 +65,15 @@ class Course /** * \brief + * Required function to implement the IteratorAggregate interface. + */ + public function getIterator() + { + return new ArrayIterator($this->sections); + } + + /** + * \brief * Returns the number of sections in the class. */ function getnsections() @@ -202,6 +211,30 @@ class Course /** * \brief + * Produce a Course object based on a JSON array compatible with + * the output of Course::to_json_array(). + * + * \param $json + * The JSON array to parse. + * \return + * A Course. + */ + public static function from_json_array($json) + { + $course = new Course($json['class']); + + if (!empty($json['sections'])) + $course->section_add(Section::from_json_arrays($json['sections'])); + + if (!empty($json['dependencies'])) + foreach ($json['dependencies'] as $dependency) + $course->dependency_add(Course::from_json_array($dependency)); + + return $course; + } + + /** + * \brief * Upgrade a course class to a newer version of that class. */ public function __wakeup() diff --git a/inc/class.schedule.php b/inc/class.schedule.php --- a/inc/class.schedule.php +++ b/inc/class.schedule.php @@ -405,7 +405,7 @@ class Schedule if ($sort_time) sort($time); - echo '

Enter these codes into your school\'s online course registration system to register for classes:

'; + echo '
'; echo '
' . "\n" . '

@@ -442,7 +442,14 @@ class Schedule for($i = $first_permutation; $i < $last_permutation; $i++) { - $syns = array(); + /* + * Store a JSON list of courses, each with only the one + * section rendered in this permutation. This is used for + * the ``Registration Numbers'' dialog which noramlly + * shows users course synonyms. + */ + $permutation_courses = array(); + echo '

\n"; // Beginning of table @@ -518,7 +525,15 @@ class Schedule . '' . htmlentities($current_meeting->getLocation(), ENT_QUOTES) . "\n" . '' . htmlentities($section->getSynonym(), ENT_QUOTES) . "\n" . "\n"; - $syns[$section->getSynonym()] = $section->getSynonym(); + + /* for the ``Registration Codes'' dialogue: */ + if (empty($permutations_courses[$section->getSynonym()])) + { + $singleton_course = new Course($course->getName()); + $singleton_course->section_add($section); + $permutation_courses[$section->getSynonym()] = $singleton_course->to_json_array(); + } + $filled = TRUE; } } @@ -542,11 +557,11 @@ class Schedule echo " \n"; } - - + /* presort */ + ksort($permutation_courses); // End of table echo " \n" - . ' '. json_encode($syns) . "\n" + . ' '. htmlentities(json_encode($permutation_courses)) . "\n" . '
\n"; } diff --git a/inc/class.section.php b/inc/class.section.php --- a/inc/class.section.php +++ b/inc/class.section.php @@ -225,6 +225,42 @@ class Section return $json_arrays; } + /** + * \brief + * Parse a set of JSON arrays into a Section. + * + * When this function was written, the JS frontend did not yet have + * support for directly supporting sections + + * section_meetings. Thus, multiple section meetings were simluated + * by having multiple sections with the same section letter. Thus, + * multiple ``sections'' of JSON are necessary to form together one + * section. + * + * Thus, the caller must ensure that there is only one section in + * the passed-in $json_arrays. + * + * \param $json_arrays + * The JSON array to be parsed into a Section. + * \return + * A Section object. + */ + public static function from_json_arrays(array $json_arrays) + { + $section_meetings = array(); + $letter = ''; + $synonym = NULL; + $prof = NULL; + foreach ($json_arrays as $meeting) + { + $letter = $meeting['section']; + $synonym = $meeting['synonym']; + $prof = $meeting['prof']; + $section_meetings[] = SectionMeeting::from_json_array($meeting); + } + $section = new Section($letter, $section_meetings, $synonym, $prof); + + return $section; + } /* for legacy unserialization */ private $start; diff --git a/inc/class.section_meeting.inc b/inc/class.section_meeting.inc --- a/inc/class.section_meeting.inc +++ b/inc/class.section_meeting.inc @@ -77,7 +77,7 @@ class SectionMeeting * * \param $days_str * The days of the week in a string format. One char per - * day. Mon-Fri is represented with 'm', 't', 'w', 'h', 'f'. + * day. Mon-Sat is represented with 'm', 't', 'w', 'h', 'f', 's'. */ private function days_set($days_str) { @@ -226,4 +226,22 @@ class SectionMeeting return $json_array; } + + /** + * \brief + * Parse a JSON array into a SectionMeeting. + * + * \param $json_array + * The JSON array to parse. + * \return + * A shiny, new SectionMeeting. + */ + public static function from_json_array(array $json_array) + { + $days = ''; + foreach ($json_array['days'] as $day => $meets) + if ($meets) + $days .= $day; + return new SectionMeeting($days, $json_array['time_start'], $json_array['time_end'], $json_array['location'], $json_array['type']); + } } diff --git a/inc/school.inc b/inc/school.inc --- a/inc/school.inc +++ b/inc/school.inc @@ -404,6 +404,48 @@ function school_example_course_id(array /** * \brief + * Populate a ``Registration Codes'' dialog. + * + * A school may override the default output by writing a function with + * the same signature and semantics as this function with a name of + * _registration_html(). + * + * \param $page + * The page object; used to conditionally format code as HTML or + * XHTML. Remember, you are writing an XHTML fragment and should not + * call Page::foot() or Page::head(). + * \param $school + * The school handle. + * \param $courses + * An array of courses, where each course only has one section which + * is the section which the user chose. + * \return + * A string which is a valid XHTML fragment. This fragment should + * direct the user to his school's registration services. It should + * also render the list of sections in a way appropriate to that + * school -- such as a list of fully-qualified section_ids or a + * listing of section synonyms. + */ +function school_registration_html(Page $page, array $school, array $courses) +{ + /* + * The school from which to call the _registration_html() + * function. Used to fall back onto the 'default' school if the + * selected school doesn't have a _registration_html(). + */ + $function_school = $school; + if (!function_exists($function_school['id'] . '_registration_html')) + { + $function_school = school_load('default'); + if (!function_exists($function_school['id'] . '_registration_Html')) + return '
Unable to load generic school_registration_html() function.
'; + } + $school_registration_html = $function_school['id'] . '_registration_html'; + return $school_registration_html($page, $school, $courses); +} + +/** + * \brief * Determine if a school has crawler data stored. * * \param $school diff --git a/school.d/default.inc b/school.d/default.inc --- a/school.d/default.inc +++ b/school.d/default.inc @@ -18,6 +18,9 @@ * along with slate_permutate. If not, see . */ +$inc_dir = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'inc' . DIRECTORY_SEPARATOR; +require_once $inc_dir . 'class.page.php'; + function default_info() { return array('name' => 'Generic College', @@ -51,3 +54,25 @@ function default_instructions_html() EOF; } + +/** + * \brief + * A generic fallback for school_registration_html(). + * + * \see school_registration_html() + */ +function default_registration_html(Page $page, array $school, array $courses) +{ + $html = '' + . '

' . PHP_EOL + . ' Enter these codes into your school\'s online course registration' . PHP_EOL + . ' system (' . htmlentities($school['name']) . '\'s website)' . PHP_EOL + . ' to register for classes:' . PHP_EOL + . '

' . PHP_EOL + . '
    ' . PHP_EOL; + foreach ($courses as $course) + foreach ($course as $section) + $html .= '
  • ' . htmlentities($section->getSynonym()) . '
  • ' . PHP_EOL; + $html .= '
' . PHP_EOL; + return $html; +} diff --git a/scripts/displayTables.js b/scripts/displayTables.js --- a/scripts/displayTables.js +++ b/scripts/displayTables.js @@ -41,6 +41,65 @@ function show_box_change() return false; } +/** + * \brief + * Do an AJAX loading of data with arbitrary error handling. + * + * \param target + * The jQuery object which should be populated with an error message + * or the result of loading. + * \param data + * The data to send as a request. + * \param handler + * A function with the signature handler(target, data) which is called upon + * a successful response. There is a default handler which uses + * .html() to load the data.data.html into target. + * \param error_handler + * A function with the signature handler(target, status_text, data) + * which is called upon an error. The default error_handler will + * store an error message in target, possibly provided by + * data.message if the HTTP request itself was successful but the + * server still claimed there is an error. The third argument, data, + * will be null if the error is at the HTTP level. + */ +function slate_permutate_load(target, data, handler, error_handler) +{ + if (jQuery.type(handler) == 'undefined') + handler = function(target, data) + { + target.html(data.html); + } + + if (jQuery.type(error_handler) == 'undefined') + error_handler = function(target, status_text, data) + { + if (data) + if (data.message) + target.html(data.message); + else + target.html('
Unknown error.
'); + else + target.html('
HTTP error: ' + status_text + '
'); + } + + jQuery.ajax({ + url: 'ajax.php', + data: data, + success: function(data, status_text, xhr) + { + if (data && data.success && jQuery.type(data.data) != 'undefined') + handler(target, data.data); + else + error_handler(target, status_text, data); + }, + dataType: 'json', + error: function(xhr_jq, status_text, error) + { + error_handler(target, status_text, null); + } + }); +} + jQuery(document).ready( function() { jQuery('#show-box input').change(show_box_change); @@ -48,21 +107,21 @@ jQuery(document).ready( function() jQuery("#regDialog").dialog({ modal: true, width: 550, resizable: false, draggable: false, autoOpen: false }); jQuery('#regCodes').click( function() { - jQuery('#regDialogList').empty(); + jQuery('#regDialog').html('

Loading registration information...

'); + + /* hmm... why isn't this information just stored in a global JS variable? */ var tab_i = jQuery('#tabs').tabs('option','selected'); var tab_fragment_i = /-([^-]+)$/.exec(jQuery('#the-tabs li:eq(' + tab_i + ') a').attr('href'))[1]; - var currSec = '.syns' + tab_fragment_i; + var tab_course_data_json_selector = '.course-data-' + tab_fragment_i; - var jHtml = jQuery(currSec).html(); - var secs = eval('(' + jHtml + ')'); - var output = '

'; - for( var i in secs ) { - output = output + secs[i] + '
'; - } - output = output + '

'; - jQuery('#regDialogList').append(output); + var tab_course_data_json = jQuery(tab_course_data_json_selector).text(); + var tab_course_data = eval('(' + tab_course_data_json + ')'); - jQuery("#regDialog").dialog("open"); + slate_permutate_load(jQuery('#regDialog'), {school_registration_html: true, courses: tab_course_data}); + + jQuery("#regDialog").dialog('open'); + + return false; }); } ); diff --git a/styles/general.css b/styles/general.css --- a/styles/general.css +++ b/styles/general.css @@ -256,13 +256,13 @@ a:hover { padding:0; margin:0; } -.syns { +.course-data { display:none; } #regCodes { float: right; } -.synList { +.synonym-list { margin-left: 1em; }