diff --git a/reason_4.0/lib/core/function_libraries/course_functions.php b/reason_4.0/lib/core/function_libraries/course_functions.php index 8dbd6f41a..50eebd75a 100644 --- a/reason_4.0/lib/core/function_libraries/course_functions.php +++ b/reason_4.0/lib/core/function_libraries/course_functions.php @@ -9,12 +9,25 @@ */ include_once(CARL_UTIL_INC.'cache/object_cache.php'); - + +// If you need to extend the course entities for local use (which you probably do), just redefine +// these globals in your extending file with the name of your local classes. +$GLOBALS['course_template_class'] = 'CourseTemplateType'; +$GLOBALS['course_section_class'] = 'CourseSectionType'; +$GLOBALS['catalog_helper_class'] = 'CatalogHelper'; + +// Working with catalog data we need to be always in UTF8, so if we're in a context with a live +// db connection, set the charset. +if (function_exists('get_current_db_connection_name') && get_current_db_connection_name()) +{ + mysql_set_charset('utf8'); +} + class CourseTemplateEntityFactory { public function get_entity(&$row) { - return new CourseTemplateType($row['id']); + return new $GLOBALS['course_template_class']($row['id']); } } @@ -23,6 +36,23 @@ class CourseTemplateType extends Entity protected $sections; protected $limit_to_year = false; protected $external_data; + protected $helper; + + /** + * This is length of time (in minutes) that external data cached on template entities should + * persist before being refreshed. 0 means no refreshing, which is the preferred choice. + * Generally you want your import process to update the external data as needed. However, you + * can import on demand, and this setting will help manage that. + * + * @var int + */ + protected $cache_duration_minutes = 0; + + function CourseTemplateType($id, $cache=true) + { + parent::__construct($id, $cache); + $this->helper = new $GLOBALS['catalog_helper_class'](); + } public function set_academic_year_limit($year) { @@ -45,6 +75,55 @@ public function get_value($col, $refresh = false) } } + /** + * Return the best section value for a particular key, based on whether or not the year limit is set. + * If no year limit is set, you'll get back the value for the most recent term. + * If a year limit is set, and sections occur within that limit, you'll + * get back the value for the last section in that year. If a year limit is set and no sections occur + * in that year, you'll get the value from the most recent year the course was offered. + * + * @param string $key + * @param boolean $refresh + * @return array + */ + function get_best_value_from_sections($key, $refresh = false) + { + if ($refresh || empty($this->{$key}) || ($this->limit_to_year && empty($this->{$key}[$this->limit_to_year]))) + { + if (!isset($this->{$key})) $this->{$key} = array(); + $sections = $this->get_sections(false); + + // If there's no year limit, just return the most recent section + if (!$this->limit_to_year) + { + if ($section = reset($sections)) + { + $year = $this->helper->term_to_academic_year($section->get_value('academic_session')); + $this->{$key}[$year] = $section->get_value($key, $refresh); + return $this->{$key}[$year]; + } + } + + // Otherwise, look for the latest section for the requested year + foreach ( $sections as $section) + { + $year = $this->helper->term_to_academic_year($section->get_value('academic_session')); + if ($year !== $this->limit_to_year) continue; + + $this->{$key}[$year] = $section->get_value($key, $refresh); + return $this->{$key}[$year]; + } + } + else + { + krsort($this->{$key}); + if ($this->limit_to_year) + return $this->{$key}[$this->limit_to_year]; + else + return reset($this->{$key}); + } + } + public function get_sections($honor_limit = true) { if (!is_array($this->sections)) @@ -54,12 +133,31 @@ public function get_sections($honor_limit = true) { foreach ( $sections as $key => $section) { - $this->sections[$section->id()] = new CourseSectionType($section->id()); + $this->sections[$section->id()] = new $GLOBALS['course_section_class']($section->id()); + } + + // Loading all the data for all the sections is potentially a big memory hog, so to start + // with we just populate a few values on each section -- the ones we need to know which + // sections are going to require a full retrieval. + $dbq = new DBSelector; + $dbq->add_table( 's','course_section' ); + $dbq->add_field( 's','id' ); + $dbq->add_field( 's','academic_session' ); + $dbq->add_field( 's','course_number' ); + $dbq->add_field( 's','timeframe_begin' ); + $dbq->add_relation( 'id IN ('.join(',', array_keys($this->sections)).')'); + if ($result = $dbq->run()) + { + foreach ($result as $row) + { + $this->sections[$row['id']]->set_value('academic_session', $row['academic_session']); + $this->sections[$row['id']]->set_value('course_number', $row['course_number']); + $this->sections[$row['id']]->set_value('timeframe_begin', $row['timeframe_begin']); + } } } + uasort($this->sections, array($this->helper, 'sort_courses_by_number_and_date')); } - - uasort($this->sections, 'sort_courses_by_number_and_date'); // If an academic year limit has been set, only return those that match if ($this->limit_to_year && $honor_limit) @@ -67,12 +165,13 @@ public function get_sections($honor_limit = true) $sections = array(); foreach ( $this->sections as $key => $section) { - $year = term_to_academic_year($section->get_value('academic_session')); + $year = $this->helper->term_to_academic_year($section->get_value('academic_session')); if ($year != $this->limit_to_year) { continue; } - $sections[$key] = $section; + $this->sections[$key]->refresh_values(false); + $sections[$key] = $this->sections[$key]; } return $sections; } @@ -84,14 +183,14 @@ public function get_sections($honor_limit = true) * Return the distinct titles associated with this course's sections. Honors year * limits. Used primarily to find courses with sections that have different titles. */ - public function get_value_section_titles($refresh = true) + public function get_value_section_titles($refresh = false) { $titles = array(); if ($sections = $this->get_sections()) { foreach ($sections as $key => $section) { - if (!in_array($section->get_value('title'), $titles)) + if (!in_array($section->get_value('title', $refresh), $titles)) { $titles[$key] = $section->get_value('title'); } @@ -101,20 +200,23 @@ public function get_value_section_titles($refresh = true) } /** - * - * @todo if year limit is in effect, only grab description from that year or older sections - */ - public function get_value_long_description($refresh = true) + * Return the long description for a course. If year limit is in place, load description from + * sections offered that year, otherwise draw the description from the template. If no year limit + * is in place, grab the most recent section description. + * + * @param boolean $refresh + * @return string + */ + public function get_value_long_description($refresh = false) { - $start_date = 0; - foreach ( $this->get_sections(false) as $key => $section) + $limit = !empty($this->limit_to_year); + + foreach ( $this->get_sections($limit) as $section) { - if ($desc = $section->get_value('long_description')) + if ($desc = $section->get_value('long_description', $refresh)) { - if ($section->get_value('timeframe_begin') > $start_date) - { $long_description = $desc; - } + break; } } @@ -124,224 +226,51 @@ public function get_value_long_description($refresh = true) return $long_description; } - /** - * - * @todo if year limit is in effect, only grab description from that year or older sections - */ - public function get_value_credits($refresh = true) - { - $sections = $this->get_sections(false); - array_reverse($sections); - - $max = $min = 0; - foreach ( $sections as $key => $section) - { - if ($credits = $section->get_value('credits')) - { - if (preg_match('/(\d+)-(\d+)/', $credits, $matches)) - { - if (!$min || $matches[1] < $min) - $min = $matches[1]; - if (!$max || $matches[2] > $max) - $max = $matches[2]; - } - else - { - if (!$min || $credits < $min) - $min = $credits; - if (!$max || $credits > $max) - $max = $credits; - } - } - } - - if ($min) - { - if ($min != $max) - return $min .'-'. $max; - else - return $min; - } - - return parent::get_value('credits', $refresh); - } - - /** - * Return the requirements fulfilled by this course. If no year limit is set, - * you'll get back all requirements fulfilled by any section. If a year limit - * is set, and sections occur within that limit, you'll get back all the - * requirements met by that section. If a year limit is set and no sections occur - * in that year, you'll get the requirements met by the sections in the most - * recent year the course was offered. - * - * @param boolean $refresh - * @return array - */ - public function get_value_requirements($refresh = true) - { - $sections = $this->get_sections(false); - array_reverse($sections); - $requirements = array(); - foreach ( $sections as $key => $section) - { - if ($reqs = $section->get_value('requirements')) - { - if ($this->limit_to_year) - { - $year = term_to_academic_year($section->get_value('academic_session')); - if ($year != $this->limit_to_year) - { - if (!isset($latest_reqs[$year])) - $latest_reqs[$year] = $reqs; - else - $latest_reqs[$year] += $reqs; - continue; - } - } - $requirements += $reqs; - } - } - if (!empty($requirements)) - { - if ($this->limit_to_year) - return filter_requirements_by_academic_year($requirements, $this->limit_to_year); - else - return $requirements; - } - else if (isset($latest_reqs)) - { - ksort($latest_reqs); - if ($this->limit_to_year) - return filter_requirements_by_academic_year(end($latest_reqs), $this->limit_to_year); - else - return end($latest_reqs); - } - else - return array(); - } - - /** - * Return how this course is graded. If no year limit is set, you'll get back the value for - * the most recent term. If a year limit is set, and sections occur within that limit, you'll - * get back the value for the last section in that year. If a year limit is set and no sections occur - * in that year, you'll get the value from the most recent year the course was offered. - * - * @param boolean $refresh - * @return array - */ - public function get_value_grading($refresh = true) - { - $sections = $this->get_sections(false); - array_reverse($sections); - foreach ( $sections as $key => $section) - { - if ($grading = $section->get_value('grading', $refresh)) - { - if ($this->limit_to_year) - { - $year = term_to_academic_year($section->get_value('academic_session')); - if ($year > $this->limit_to_year) - continue; - else - return $grading; - } - } - } - } - - /** - * Return the course level for this course. If no year limit is set, you'll get back the value for - * the most recent term. If a year limit is set, and sections occur within that limit, you'll - * get back the value for the last section in that year. If a year limit is set and no sections occur - * in that year, you'll get the value from the most recent year the course was offered. - * +/** + * Return the title for a course. If year limit is in place, load the title from + * sections offered that year, otherwise draw the title from the template. If no year limit + * is in place, grab the most recent section title. + * * @param boolean $refresh - * @return array + * @return string */ - public function get_value_course_level($refresh = true) + public function get_value_title($refresh = false) { - $sections = $this->get_sections(false); - array_reverse($sections); - foreach ( $sections as $key => $section) + $limit = !empty($this->limit_to_year); + + foreach ( $this->get_sections($limit) as $section) { - if ($level = $section->get_value('course_level', $refresh)) + if ($title = $section->get_value('title', $refresh)) { - if ($this->limit_to_year) - { - $year = term_to_academic_year($section->get_value('academic_session')); - if ($year > $this->limit_to_year) - continue; - else - return $level; - } + break; } } + + if (empty($title)) + $title = parent::get_value('title', $refresh); + + return $title; } /** - * Return the gov codes for this course. If no year limit is set, you'll get back the values for - * the most recent term. If a year limit is set, and sections occur within that limit, you'll - * get back the values for the last section in that year. If a year limit is set and no sections occur - * in that year, you'll get the values from the most recent year the course was offered. - * + * Generate a default HTML snippet for the course title. Override this if you + * want to use a diffferent pattern. + * * @param boolean $refresh - * @return array + * @return string */ - public function get_value_gov_codes($refresh = true) - { - $sections = $this->get_sections(false); - array_reverse($sections); - foreach ( $sections as $key => $section) - { - if ($codes = $section->get_value('gov_codes', $refresh)) - { - if ($this->limit_to_year) - { - $year = term_to_academic_year($section->get_value('academic_session')); - if ($year > $this->limit_to_year) - continue; - else - return $codes; - } - } - } - return array(); - } - - /** - * Return an array of faculty who teach sections of this course. - * - * @return array id => name - */ - public function get_value_faculty($refresh = true) - { - $sections = $this->get_sections(); - $faculty = array(); - foreach ( $sections as $key => $section) - { - if ($fac = $section->get_value('faculty', $refresh)) - { - $faculty = $faculty + $fac; - } - } - return $faculty; - } - - public function get_value_start_date($refresh = true) + protected function get_value_display_title($refresh = false) { - $this->fetch_external_data($refresh); - if (isset($this->external_data['CRS_START_DATE'])) - return $this->external_data['CRS_START_DATE']; - } + $html = ''; + $html .= $this->get_value('org_id', $refresh).' '.$this->get_value('course_number', $refresh); + $html .= ' '; + $html .= ''; + $html .= $this->get_value('title', $refresh); + $html .= ' '; - public function get_value_end_date($refresh = true) - { - $this->fetch_external_data($refresh); - if (isset($this->external_data['CRS_END_DATE'])) - return $this->external_data['CRS_END_DATE']; + return $html; } - /** * Returns an array of the academic terms in which this course was offered, formatted as * ( start_date => term_name), sorted by date. Only one element exists per term, even if @@ -360,6 +289,34 @@ public function get_offer_history($honor_limit = true) return $history; } + /** + * Generate a default HTML snippet for displaying details about if and + * when the course is offered in the current academic year. Override this if you want to use a + * different pattern. + * + * @param boolean $honor_limit Whether to honor existing year restriction + * @return string + */ + function get_offer_history_html($honor_limit = true) + { + if ($terms = $this->get_offer_history($honor_limit)) + { + $term_names = array('SU'=>'Summer','FA'=>'Fall','WI'=>'Winter','SP'=>'Spring'); + + foreach ($terms as $term) + { + list(,$termcode) = explode('/', $term); + $offered[$termcode] = $term_names[$termcode]; + } + $history = join(', ', $offered); + } else if ($this->limit_to_year) { + $history = 'Not offered '.$this->helper->get_display_year($this->limit_to_year); + } + + if (!empty($history)) + return ''.$history.''; + } + /** * Returns the academic year in which this course was last offered. The value * is the year that the session started, e.g. courses offered in the 2013-14 academic @@ -371,7 +328,7 @@ public function get_last_offered_academic_year() { if ($history = $this->get_offer_history(false)) { - $year = term_to_academic_year(end($history)); + $year = $this->helper->term_to_academic_year(end($history)); } return (isset($year)) ? $year : 0; @@ -401,61 +358,119 @@ public function get_section_by_id($id) return false; } - protected function fetch_external_data() + /** + * Retrieve and cache course data from an external source. Used for values that are not defined + * in the base course template schema. You'll need to extend this class to provide your own + * retrieval routine. + */ + public function fetch_external_data($refresh = false, $update_cache = true) { - if (empty($this->external_data)) + if (!$this->external_data_is_valid($refresh)) { - // Do we want to check for the empty array here? if ($cache = $this->get_value('cache')) { $this->external_data = json_decode($cache, true); } - if (!isset($this->external_data)) + if ($refresh || !$this->external_data_is_valid()) { - $query = 'SELECT * FROM colleague_data.IDM_CRS WHERE COURSES_ID = '.$this->get_value('sourced_id'); - - if ($result = mysql_query($query)) - { - $this->external_data = mysql_fetch_assoc($result); - $this->external_data['timestamp'] = time(); - } else { - // Indicate that we've tried and failed to retrieve the data, so we don't keep trying - $this->external_data = array(); - } + // Insert your routine for retrieving external data here. + + // Indicate that we've tried and failed to retrieve the data, so we don't keep trying + $this->external_data = array(); - if (!empty($this->external_data)) - { - $this->set_value('cache', json_encode($this->external_data)); - reason_update_entity( - $this->id(), - $this->get_value('last_edited_by'), - array('cache' => $this->get_value('cache')), - false); - } + if ($update_cache && !empty($this->external_data)) + $this->update_cache(); } } + return $this->external_data; + } + + /** + * Update this entity's external data cache from the current value of $this->external_data + */ + protected function update_cache() + { + $this->external_data['timestamp'] = time(); + $encoded = json_encode($this->external_data); + if ($encoded != $this->get_value('cache')) + { + $this->set_value('cache', json_encode($this->external_data)); + reason_update_entity( + $this->id(), + $this->get_value('last_edited_by'), + array('cache' => $this->get_value('cache')), + false); + } + } + + /** + * Determine whether the cached external data needs to be refreshed. + * + * @return boolean + */ + protected function external_data_is_valid($refresh = false) + { + // If it hasn't been defined, it's invalid + if (!is_array($this->external_data)) + return false; + + // If the timestamp is too old, it's invalid + if ($this->cache_duration_minutes && isset($this->external_data['timestamp'])) + { + if ((time() - $this->cache_duration_minutes * 60) > $this->external_data['timestamp']) + return false; + } + // If there is no timestamp, it's invalid + else if ($this->cache_duration_minutes) + { + return false; + } + + // Otherwise, it's probably ok. + return !$refresh; } } + + + + class CourseSectionEntityFactory { public function get_entity(&$row) { - return new CourseSectionType($row['id']); + return new $GLOBALS['course_section_class']($row['id']); } } class CourseSectionType extends Entity { protected $external_data = array(); + protected $helper; + /** + * This is length of time (in minutes) that external data cached on section entities should + * persist before being refreshed. 0 means no refreshing, which is the preferred choice. + * Generally you want your import process to update the external data as needed. However, you + * can import on demand, and this setting will help manage that. + * + * @var int + */ + protected $cache_duration_minutes = 0; + + function CourseSectionType($id=null, $cache=true) + { + parent::__construct($id, $cache); + $this->helper = new $GLOBALS['catalog_helper_class'](); + } + public function get_template() { if ($entities = $this->get_right_relationship('course_template_to_course_section')) { $template = reset($entities); - return new CourseTemplateType($template->id()); + return new $GLOBALS['course_template_class']($template->id()); } } @@ -468,136 +483,41 @@ public function get_value($col, $refresh = false) } else { - return parent::get_value($col, $refresh); - } - } - - public function get_value_requirements($refresh = true) - { - $this->fetch_external_data($refresh); - if (isset($this->external_data['section']['XSEC_COURSE_TYPES_LIST'])) - { - $reqs = explode(' ', $this->external_data['section']['XSEC_COURSE_TYPES_LIST']); - - // Suppress any codes we shouldn't show - $reqs = array_diff($reqs, array('CX','IE','LP')); - - return $reqs; - } - } - - public function get_value_faculty($refresh = true) - { - $this->fetch_faculty_data($refresh); - $faculty = array(); - foreach ($this->external_data['faculty'] as $id => $data) - { - $faculty[$id] = $data['Carleton_Name']; + return parent::get_value($col); } - return $faculty; } /** - * @todo Find out why some 400 courses are marked S/CR/NC but most are S/NC - * - */ - public function get_value_grading($refresh = true) - { - $this->fetch_external_data($refresh); - if (isset($this->external_data['section']['SEC_ONLY_PASS_NOPASS_FLAG'])) - { - if (($this->external_data['section']['SEC_ONLY_PASS_NOPASS_FLAG'] == 'Y') && strpos('10', $this->external_data['section']['XSEC_SEC_COURSE_LEVELS_SV']) === false) - { - if ($this->external_data['section']['SEC_COURSE_NO'] == '400') - return 'S/NC'; - else - return 'S/CR/NC'; - } - } - } - - public function get_value_course_level($refresh = true) - { - $this->fetch_external_data($refresh); - if (isset($this->external_data['section']['XSEC_SEC_COURSE_LEVELS_SV'])) - { - return $this->external_data['section']['XSEC_SEC_COURSE_LEVELS_SV']; - } - } - - public function get_value_gov_codes($refresh = true) - { - $this->fetch_external_data($refresh); - if (isset($this->external_data['section']['XSEC_LOCAL_GOVT_CODES_SV'])) - { - return explode('|',$this->external_data['section']['XSEC_LOCAL_GOVT_CODES_SV']); - } - return array(); - } - - protected function fetch_external_data($refresh = false) + * Retrieve and cache section data from an external source. Used for values that are not defined + * in the base course section schema. You'll need to extend this class to provide your own + * retrieval routine. + */ + public function fetch_external_data($refresh = false, $update_cache = true) { - if (empty($this->external_data['section'])) + if ($refresh || empty($this->external_data['section'])) { if ($cache = $this->get_value('cache')) { $this->external_data = json_decode($cache, true); } - if ($refresh || !isset($this->external_data['section'])) - { - $query = 'SELECT * FROM colleague_data.IDM_COURSE WHERE COURSE_SECTIONS_ID = '.$this->get_value('sourced_id'); - - if ($result = mysql_query($query)) - { - $this->external_data['section'] = mysql_fetch_assoc($result); - $this->external_data['timestamp'] = time(); - } else { - // Indicate that we've tried and failed to retrieve the data, so we don't keep trying - $this->external_data['section'] = array(); - } - - if (!empty($this->external_data['section'])) - $this->update_cache(); - } - } - } - - protected function fetch_faculty_data($refresh = false) - { - if (empty($this->external_data['faculty'])) - { - if ($cache = $this->get_value('cache')) - { - $this->external_data = json_decode($cache, true); - } - - if ($refresh || !isset($this->external_data['faculty'])) + if ($refresh || !$this->external_data_is_valid()) { - $this->external_data['faculty'] = array(); - $query = 'SELECT Id, First_Name, NickName, Last_Name, Carleton_Name, Fac_Catalog_Name FROM colleague_data.IDM_CRS_SEC_FACULTY, colleague_data.EmployeesByPosition_All - WHERE CSF_COURSE_SECTION = '.$this->get_value('sourced_id') .' - AND CSF_FACULTY = EmployeesByPosition_All.Id'; - - mysql_set_charset('utf8'); - if ($result = mysql_query($query)) - { - while ($row = mysql_fetch_assoc($result)) - { - $this->external_data['faculty'][$row['Id']] = $row; - } - $this->external_data['timestamp'] = time(); - } + // Insert your routine for retrieving external data here. - if (!empty($this->external_data['faculty'])) + // Indicate that we've tried and failed to retrieve the data, so we don't keep trying + $this->external_data['section'] = array(); + + if ($update_cache && !empty($this->external_data['section'])) $this->update_cache(); } } - + return $this->external_data; } protected function update_cache() { + $this->external_data['timestamp'] = time(); $encoded = json_encode($this->external_data); if ($encoded != $this->get_value('cache')) { @@ -609,256 +529,620 @@ protected function update_cache() false); } } -} - -/** - * Find courses that have been attached to the specified page. - * - */ -function get_page_courses($page_id) -{ - $courses = array(); - $es = new entity_selector(); - $es->description = 'Selecting courses for this page'; - $factory = new CourseTemplateEntityFactory(); - $es->set_entity_factory($factory); - $es->add_type( id_of('course_template_type') ); - $es->add_left_relationship( $page_id, relationship_id_of('course_template_to_page') ); - $results = $es->run_one(); - foreach ($results as $id => $entity) - { - if (isset($courses[$id])) continue; - $entity->include_source = 'page_courses'; - $courses[$id] = $entity; - } - return $courses; -} + + /** + * Determine whether the cached external data needs to be refreshed. + * + * @return boolean + */ + protected function external_data_is_valid($refresh = false) + { + // If it hasn't been defined, it's invalid + if (!is_array($this->external_data)) + return false; + + // If the timestamp is too old, it's invalid + if ($this->cache_duration_minutes && isset($this->external_data['timestamp'])) + { + if ((time() - $this->cache_duration_minutes * 60) > $this->external_data['timestamp']) + return false; + } + // If there is no timestamp, it's invalid + else if ($this->cache_duration_minutes) + { + return false; + } + + // Otherwise, it's probably ok. + return !$refresh; + } +} + + + + /** - * Find courses that are owned or borrowed by the specified site. - * - */ -function get_site_courses($site) + * A collection of methods for working with catalog and course data. You will probably want to + * extend this class locally to meet your particular needs. + */ +class CatalogHelper { - $courses = array(); - if ($site && (is_int($site) || $site = id_of($site))) + protected $year; + protected $site; + + public function __construct($year = null) + { + if ($year) + { + $this->year = $year; + if (!$this->site = id_of('academic_catalog_'.$year.'_site')) + trigger_error('No catalog site for '.$year.' in CatalogHelper'); + } + else + { + $this->year = $this->get_latest_catalog_year(); + } + } + + /** + * Find courses that have been attached to the specified page. + * + */ + public function get_page_courses($page_id) { - $es = new entity_selector( $site ); - $es->description = 'Selecting courses on site'; + $courses = array(); + $es = new entity_selector(); + $es->description = 'Selecting courses for this page'; $factory = new CourseTemplateEntityFactory(); $es->set_entity_factory($factory); $es->add_type( id_of('course_template_type') ); + $es->add_left_relationship( $page_id, relationship_id_of('course_template_to_page') ); $results = $es->run_one(); foreach ($results as $id => $entity) { if (isset($courses[$id])) continue; - $entity->include_source = 'site_courses'; + $entity->include_source = 'page_courses'; $courses[$id] = $entity; } + return $courses; } - return $courses; -} -/** - * Find courses that use given categories and add them to our collection. - * - * @param $cats array category list (id => name) - */ -function get_courses_by_category($cats, $catalog_site = null) -{ - $courses = array(); - foreach ($cats as $id => $category) + /** + * Find courses that are owned or borrowed by the specified site. + * + */ + public function get_site_courses($site) { + $courses = array(); + if ($site && (is_int($site) || $site = id_of($site))) + { + $es = new entity_selector( $site ); + $es->description = 'Selecting courses on site'; + $factory = new CourseTemplateEntityFactory(); + $es->set_entity_factory($factory); + $es->add_type( id_of('course_template_type') ); + $results = $es->run_one(); + foreach ($results as $id => $entity) + { + if (isset($courses[$id])) continue; + $entity->include_source = 'site_courses'; + $courses[$id] = $entity; + } + } + return $courses; + } + + /** + * Find courses that use given categories and add them to our collection. + * + * @param $cats array category list (id => name) + */ + public function get_courses_by_category($cats, $catalog_site = null) + { + $courses = array(); + foreach ($cats as $id => $category) + { + if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) + $es = new entity_selector($catalog_site); + else + $es = new entity_selector(); + $es->description = 'Selecting courses by category'; + $factory = new CourseTemplateEntityFactory(); + $es->set_entity_factory($factory); + $es->add_type( id_of('course_template_type') ); + $es->add_left_relationship( $id, relationship_id_of('course_template_to_category') ); + $results = $es->run_one(); + foreach ($results as $id => $entity) + { + if (isset($courses[$id])) continue; + $entity->include_source = 'categories'; + $courses[$id] = $entity; + } + } + return $courses; + } + + /** + * Find courses with particular academic subjects and add them to our collection. + * + * @param array $codes Array of subject codes + * @param string $catalog_site Optional unique name of a catalog site. If you pass this, + * only courses associated with that site will be included. + */ + public function get_courses_by_subjects($codes, $catalog_site = null) + { + $courses = array(); if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) $es = new entity_selector($catalog_site); else $es = new entity_selector(); - $es->description = 'Selecting courses by category'; + $es->description = 'Selecting courses by subject'; $factory = new CourseTemplateEntityFactory(); $es->set_entity_factory($factory); $es->add_type( id_of('course_template_type') ); - $es->add_left_relationship( $id, relationship_id_of('course_template_to_category') ); + $es->add_relation('org_id in ("'.join('","', $codes).'")'); + $es->set_order('ABS(course_number), title'); $results = $es->run_one(); foreach ($results as $id => $entity) { if (isset($courses[$id])) continue; - $entity->include_source = 'categories'; + $entity->include_source = 'subjects'; $courses[$id] = $entity; } + return $courses; } - return $courses; -} -/** - * Find courses with particular academic subjects and add them to our collection. - * - * @param array $codes Array of subject codes - * @param string $catalog_site Optional unique name of a catalog site. If you pass this, - * only courses associated with that site will be included. - */ -function get_courses_by_subjects($codes, $catalog_site = null) -{ - $courses = array(); - if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) - $es = new entity_selector($catalog_site); - else - $es = new entity_selector(); - $es->description = 'Selecting courses by subject'; - $factory = new CourseTemplateEntityFactory(); - $es->set_entity_factory($factory); - $es->add_type( id_of('course_template_type') ); - $es->add_relation('org_id in ("'.join('","', $codes).'")'); - $es->set_order('ABS(course_number), title'); - $results = $es->run_one(); - foreach ($results as $id => $entity) - { - if (isset($courses[$id])) continue; - $entity->include_source = 'subjects'; - $courses[$id] = $entity; - } - return $courses; -} + /** + * Find courses with a particular subject and number + * + * @param string $code subject code + * @param string $number course number + * @param string $catalog_site Optional unique name of a catalog site. If you pass this, + * only courses associated with that site will be included. + */ + public function get_courses_by_subject_and_number($code, $number, $catalog_site = null) + { + $courses = array(); + if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) + $es = new entity_selector($catalog_site); + else + $es = new entity_selector(); + $es->description = 'Selecting courses by subject and number'; + $factory = new CourseTemplateEntityFactory(); + $es->set_entity_factory($factory); + $es->add_type( id_of('course_template_type') ); + $es->add_relation('org_id = "'.mysql_real_escape_string($code).'"'); + $es->add_relation('course_number = "'.mysql_real_escape_string($number).'"'); + $es->set_order('title'); + $results = $es->run_one(); + foreach ($results as $id => $entity) + { + if (isset($courses[$id])) continue; + $entity->include_source = 'subject and number'; + $courses[$id] = $entity; + } + return $courses; + } + + public function get_courses_by_org_id($codes, $catalog_site = null) + { + return $this->get_courses_by_subjects($codes, $catalog_site); + } -function get_courses_by_org_id($codes, $catalog_site = null) -{ - return get_courses_by_subjects($codes, $catalog_site); -} + /** + * Find courses by their id in the source data + * + * @param array $ids Array of ids + * @param string or int $catalog_site Optional unique name or id of a catalog site. If you pass this, + * only courses associated with that site will be included. + */ + public function get_courses_by_sourced_id($ids, $catalog_site = null) + { + $courses = array(); + if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) + $es = new entity_selector($catalog_site); + else + $es = new entity_selector(); + $es->description = 'Selecting courses by sourced_id'; + $factory = new CourseTemplateEntityFactory(); + $es->set_entity_factory($factory); + $es->add_type( id_of('course_template_type') ); + $es->add_relation('sourced_id in ("'.join('","', $ids).'")'); + $es->set_order('org_id, course_number'); + $results = $es->run_one(); + foreach ($results as $id => $entity) + { + if (isset($courses[$id])) continue; + $entity->include_source = 'ids'; + $courses[$id] = $entity; + } + return $courses; + } -/** - * Find courses by their id in the source data - * - * @param array $codes Array of ids - * @param string or int $catalog_site Optional unique name or id of a catalog site. If you pass this, - * only courses associated with that site will be included. - */ -function get_courses_by_sourced_id($ids, $catalog_site = null) -{ - $courses = array(); - if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) - $es = new entity_selector($catalog_site); - else - $es = new entity_selector(); - $es->description = 'Selecting courses by sourced_id'; - $factory = new CourseTemplateEntityFactory(); - $es->set_entity_factory($factory); - $es->add_type( id_of('course_template_type') ); - $es->add_relation('sourced_id in ("'.join('","', $ids).'")'); - $es->set_order('org_id, course_number'); - $results = $es->run_one(); - foreach ($results as $id => $entity) - { - if (isset($courses[$id])) continue; - $entity->include_source = 'subjects'; - $courses[$id] = $entity; - } - return $courses; -} + /** + * Find sections by their id in the source data + * + * @param array $ids Array of ids + * @param string or int $catalog_site Optional unique name or id of a catalog site. If you pass this, + * only sections associated with that site will be included. + */ + public function get_sections_by_sourced_id($ids, $catalog_site = null) + { + $sections = array(); + if ($catalog_site && (is_int($catalog_site) || $catalog_site = id_of($catalog_site))) + $es = new entity_selector($catalog_site); + else + $es = new entity_selector(); + $es->description = 'Selecting sections by sourced_id'; + $factory = new CourseSectionEntityFactory(); + $es->set_entity_factory($factory); + $es->add_type( id_of('course_section_type') ); + $es->add_relation('sourced_id in ("'.join('","', $ids).'")'); + $es->set_order('org_id, course_number'); + $results = $es->run_one(); + foreach ($results as $id => $entity) + { + if (isset($sections[$id])) continue; + $entity->include_source = 'ids'; + $sections[$id] = $entity; + } + return $sections; + } + + public function sort_courses_by_name($a, $b) + { + $a_name = $a->get_value('name'); + $b_name = $b->get_value('name'); + if ($a_name == $b_name) { + return 0; + } + return ($a_name < $b_name) ? -1 : 1; + } -function sort_courses_by_name($a, $b) -{ - $a_name = $a->get_value('name'); - $b_name = $b->get_value('name'); - if ($a_name == $b_name) { - return 0; + public function sort_courses_by_date($a, $b) + { + $a_name = $a->get_value('timeframe_begin'); + $b_name = $b->get_value('timeframe_begin'); + if ($a_name == $b_name) { + return 0; + } + return ($a_name > $b_name) ? -1 : 1; } - return ($a_name < $b_name) ? -1 : 1; -} -function sort_courses_by_date($a, $b) -{ - $a_name = $a->get_value('timeframe_begin'); - $b_name = $b->get_value('timeframe_begin'); - if ($a_name == $b_name) { - return 0; + public function sort_courses_by_number_and_date($a, $b) + { + $a_num = $a->get_value('course_number'); + $b_num = $b->get_value('course_number'); + $a_name = $a->get_value('timeframe_begin'); + $b_name = $b->get_value('timeframe_begin'); + if ($a_name.$a_num == $b_name.$b_num) { + return 0; + } + return ($a_name.$a_num > $b_name.$b_num) ? -1 : 1; } - return ($a_name > $b_name) ? -1 : 1; -} -function sort_courses_by_number_and_date($a, $b) -{ - $a_num = $a->get_value('course_number'); - $b_num = $b->get_value('course_number'); - $a_name = $a->get_value('timeframe_begin'); - $b_name = $b->get_value('timeframe_begin'); - if ($a_name.$a_num == $b_name.$b_num) { - return 0; - } - return ($a_name.$a_num > $b_name.$b_num) ? -1 : 1; -} + /** + * Get the list of possible course subjects by looking at the section data. If a year is + * passed, limit the result to those subjects with sections during that academic year. + * + * @param int $year + * + * @todo Generalize timespan query + */ + public function get_course_subjects($year = null) + { + $cache = new ObjectCache('course_subject_cache_'.$year, 60*24); + if ($subjects = $cache->fetch()) return $subjects; -/** - * Get the list of possible course subjects by looking at the section data. If a year is - * passed, limit the result to those subjects with sections during that academic year. - * - * @param int $year - * - * @todo Generalize timespan query - */ -function get_course_subjects($year = null) -{ - $cache = new ObjectCache('course_subject_cache_'.$year, 60*24); - if ($subjects = $cache->fetch()) return $subjects; + // If we're asking about a future academic year, use the previous year's sections, because + // the future year may not be fully populated yet. + if ($year && $this->get_catalog_year_start_date($year) > date('Y-m-d g:i:s')) + $startyear = $year - 1; + else + $startyear = $year; + + $subjects = array(); + $q = 'SELECT distinct org_id FROM course_section'; + if ($year) $q .= ' WHERE timeframe_begin > "'.$this->get_catalog_year_start_date($startyear).'" AND timeframe_end < "'.$this->get_catalog_year_end_date($year).'"'; + $q .= ' ORDER BY org_id'; + if ($result = db_query($q, 'Error selecting course subjects')) + { + while ($row = mysql_fetch_assoc($result)) + $subjects[$row['org_id']] = $row['org_id']; + } + $cache->set($subjects); + return $subjects; + } - $subjects = array(); - $q = 'SELECT distinct org_id FROM course_section'; - if ($year) $q .= ' WHERE timeframe_begin > "'.get_catalog_year_start_date($year).'" AND timeframe_end < "'.get_catalog_year_end_date($year).'"'; - $q .= ' ORDER BY org_id'; - if ($result = db_query($q, 'Error selecting course subjects')) + /** + * Get the list of years for which we have catalog sites. Assumes that catalog sites use the + * naming convention academic_catalog_YEAR_site. + * + * @return array + */ + public function get_catalog_years() { - while ($row = mysql_fetch_assoc($result)) - $subjects[$row['org_id']] = $row['org_id']; + static $catalog_years = array(); + if (empty($catalog_years)) + { + $names = array_flip(reason_get_unique_names()); + if ($catalogs = preg_grep('/^academic_catalog_\d{4}_site$/', $names)) + { + foreach ($catalogs as $catalog) + { + preg_match('/^academic_catalog_(\d{4})_site$/', $catalog, $matches); + $catalog_years[$matches[1]] = $catalog; + } + krsort($catalog_years); + } + } + return $catalog_years; } - $cache->set($subjects); - return $subjects; -} -/** - * Get the list of years for which we have catalog sites. - * - * @todo Finish - */ -function get_catalog_years() -{ - return array(2013=>2013, 2014=>2014, 2015=>2015); -} - -function get_display_year($year) -{ - return $year . '-' . ((int) substr($year, -2) + 1); -} - -function term_to_academic_year($term) -{ - list($year,$term) = explode('/',$term); - $year = 2000 + $year; - if (($term == 'WI') || ($term == 'SP')) $year--; - return $year; -} + /** + * Return the most recent year for which there is a live catalog site + * + * @return integer + */ + public function get_latest_catalog_year() + { + static $latest_year = 0; + + if (empty($latest_year) && $catalog_years = $this->get_catalog_years()) + { + foreach ($catalog_years as $year => $site) + { + $site = new entity(id_of($site)); + if ($site->get_value('site_state') === 'Live') + { + $latest_year = $year; + break; + } + } + } + + return $latest_year; + } + + /** + * Convert a bare year (e.g. 2015) into an academic year display (e.g. 2015-16) + * + * @param mixed $year + * @return string + */ + public function get_display_year($year) + { + return $year . '-' . ((int) substr($year, -2) + 1); + } -function get_catalog_year_start_date($year) -{ - return $year.'-09-01 00:00:00'; -} + /** + * Convert a term code into the catalog year in which it occurs. + * + * @param string $term + * @return integer + */ + public function term_to_academic_year($term) + { + if ($term) + { + list($year,$term) = explode('/',$term); + $year = 2000 + $year; + if (($term == 'WI') || ($term == 'SP')) $year--; + return $year; + } + } -function get_catalog_year_end_date($year) -{ - return ($year + 1).'-07-01 00:00:00'; -} + /** + * Given a catalog year, return a date representing the start of that academic year. Used to + * identify which year a section is offered in. This is a simple default, which you can + * override or extend to do a lookup or something. + * + * @param integer $year + * @return string + */ + public function get_catalog_year_start_date($year) + { + return $year.'-09-01 00:00:00'; + } + /** + * Given a catalog year, return a date representing the end of that academic year. Used to + * identify which year a section is offered in. This is a simple default, which you can + * override or extend to do a lookup or something. + * + * @param integer $year + * @return string + */ + public function get_catalog_year_end_date($year) + { + return ($year + 1).'-07-01 00:00:00'; + } + + public function get_catalog_blocks($year, $org_id, $type) + { + if ($site = id_of('academic_catalog_'.$year.'_site')) + { + $es = new entity_selector( $site ); + $es->description = 'Selecting catalog blocks on site'; + $es->add_type( id_of('course_catalog_block_type') ); + $es->add_relation('org_id = "'.carl_util_sql_string_escape($org_id).'"'); + $es->add_relation('block_type = "'.carl_util_sql_string_escape($type).'"'); + if ($results = $es->run_one()) + return $results; + } + else + { + trigger_error('No catalog site for '.$year.' in get_catalog_block()'); + } + return array(); + } + /** - * Some requirements only apply for some year ranges. This filters a list based on the provided year - * and gives you the ones you want. - */ -function filter_requirements_by_academic_year($reqs, $year) -{ - foreach($reqs as $key => $req) + * Catalog content can contain tags (in {}) that dynamically include lists of courses based on + * values on the course objects. This method detects those tags and calls get_course_list() to + * generate the appropriate list, which is then swapped in for the tag. + * + * @param string $content + * @return string + */ + public function expand_catalog_tags($content) { - if ($year > 2013) - if (in_array($req, array('AL','HU','SS','MS','RAD','WR','ND'))) - unset($reqs[$key]); + if (preg_match_all('/\{([^\}]+)\}/', $content, $tags, PREG_SET_ORDER)) + { + foreach ($tags as $tag) + { + // This is looking for {type key1="value" key2="value"} but it's very forgiving about + // extra spaces or failure to use the right quotes. + if (preg_match('/\s*([^\s=]+)(\s+([^\s=]+)\s*=\s*["\']?([^"\'\b]+)["\'\b])*/', $tag[1], $matches)) + { + $type = strtolower($matches[1]); + for ($i = 2; $i < count($matches); $i = $i + 3) + { + $keys[$matches[$i + 1]] = $matches[$i + 2]; + } + + if (!$courses = $this->get_course_list($type, $keys)) + $courses = 'No courses found for '.$tag[0].''; + + $content = str_replace($tag[0], $courses, $content); + + } + else + { + trigger_error('Badly formed catalog tag: '.$tag[1]); + } + } + } + + return $content; + } + + /** + * Catalog content can contain tags (in {}) that dynamically include lists of courses based on + * values on the course objects. This method takes the values from a tag and generates html for + * the corresponding courses. It uses an extension pattern so that custom methods/functions can + * be defined to handle the generation of course lists for particular keys. + * + * @param string $type What kind of list to generate (titles/descriptions) + * @param array $keys Key value pairs used to select the courses in the list + * @return string + */ + public function get_course_list($type, $keys) + { + $courses = array(); + foreach ($keys as $key => $val) + { + $function = 'get_courses_by_'.$key; + if (method_exists($this, $function)) + $courses = array_merge($courses, $this->$function(array($val), $this->site)); + //else if (method_exists($this->helper, $function)) + // $courses = array_merge($courses, $this->helper->$function(array($val), $this->site_id)); + else + { + trigger_error('No course function found: '.$function); + } + } + + if ($courses) + { + $html = ''; + + if ($type == 'descriptions') + { + foreach ($courses as $course) + { + $html .= $this->get_course_html($course); + } + } + else if ($type == 'titles') + { + $html .= '
]*>/','/<\/p>$/'), '', $course->get_value('long_description')); + if ($prereqs = $course->get_value('list_of_prerequisites')) + $description .= ' Prerequisite: '.trim($prereqs, " .").'. '; + + $html = ''. $description .''; + + if ($credit = $course->get_value('credits')) + { + $details[] = $credit . (($credit == 1) ? ' credit' : ' credits'); + } + + if ($grading = $course->get_value('grading')) + { + $details[] = $grading; + } + if ($requirements = $course->get_value('requirements')) + { + $details[] = join(', ', $requirements); + } + + if ($history = $course->get_offer_history_html()) + { + $details[] = $history; + } + + if ($faculty = $course->get_value('display_faculty')) + { + $details[] = $faculty; + } + + if (isset($details)) + { + $html .= ' '.ucfirst(join('; ', $details)); + } + + return $html; + } + + /** + * Given a course object, generate a default HTML block for displaying it. Override this if you + * want to use a diffferent pattern. + * + * @param object $course + * @return string + */ + public function get_course_html($course) + { + $course->set_academic_year_limit($this->year); + + $html = '
'; + echo "Running...\n"; + } + connectDB(REASON_DB); + mysql_set_charset('utf8'); $this->disable_output_buffering(); - echo "Running\n"; - //$this->import_course_blocks(); - //return; - - //$this->delete_all_course_entities(); - - //foreach ($this->get_section_org_ids() as $org_id) - //{ - if ($raw_data = $this->get_course_template_data()) + if (empty($org_ids) && !$org_ids = $this->get_template_org_ids()) $org_ids = array(null); + foreach ($org_ids as $org_id) + { + if ($raw_data = $this->get_course_template_data($org_id)) { if ($mapped_data = $this->map_course_template_data($raw_data)) { - $this->build_course_template_entities($mapped_data, $this->get_existing_template_ids()); + $this->build_course_template_entities($mapped_data, $this->get_existing_template_ids($org_id), $this->delete_missing_templates); + unset($raw_data); + unset($mapped_data); } else { @@ -55,18 +184,22 @@ public function run() } else { - $this->errors[] = 'No course template data received for '.$org_id.'.'; + if ($this->verbosity == 2) $this->errors[] = 'No course template data received for '.$org_id.'.'; } - //} - //return; - foreach ($this->get_section_org_ids() as $org_id) - { + if ($this->errors) echo join("\n", $this->errors)."\n"; + $this->errors = array(); + } + if (empty($org_ids) && !$org_ids = $this->get_section_org_ids()) $org_ids = array(null); + foreach ($org_ids as $org_id) + { if ($raw_data = $this->get_course_section_data($org_id)) { if ($mapped_data = $this->map_course_section_data($raw_data)) { - $this->build_course_section_entities($mapped_data); + $this->build_course_section_entities($mapped_data, $this->get_existing_section_ids($org_id), $this->delete_missing_sections); + unset($raw_data); + unset($mapped_data); } else { @@ -75,14 +208,105 @@ public function run() } else { - $this->errors[] = 'No course section data received for '.$org_id.'.'; + if ($this->verbosity == 2) $this->errors[] = 'No course section data received for '.$org_id.'.'; } + if ($this->errors) echo join("\n", $this->errors)."\n"; + $this->errors = array(); } - echo join("\n", $this->errors); - echo "Import Complete.\n"; + if ($this->errors) echo join("\n", $this->errors)."\n"; + if ($this->verbosity == 2) echo "Import Complete.\n"; + } + + /** + * Retrieves a list of valid 'org_id' values (your department/subject codes). + * Typically this is the same for templates and sections, but you can define them + * separately if necessary. + */ + protected function get_template_org_ids() + { + return $this->get_section_org_ids(); } + /** + * Retrieves a list of valid 'org_id' values (your department/subject codes). + * Typically this is the same for templates and sections, but you can define them + * separately if necessary. + */ + protected function get_section_org_ids() + { + return array(); + } + + /** + * This method does all the work of retrieving your raw course template data from + * wherever it lives: via a query into your ERP, a query to a local data mirror, + * retrieving data from flat files, etc. The result should be an array with one row + * per course. It doesn't matter how that data is structured -- it will be massaged + * into shape by the mapping step. + * + * You should allow for passing an optional org_id value (a department/subject code) + * to retrieve only a subset of data. + * + * @param mixed $org_id + */ + protected function get_course_template_data($org_id = null) + { + return array(); + } + + /** + * This is a set of rules for excluding course templates from being imported. The method is + * passed a single row of data produced by get_course_template_data(), and based on the values + * in that row you can pass true or false to indicate whether the row should be discarded. + * This is helpful if you need to exclude courses based on values that are difficult to + * restrict in your initial query. + * + * @param type $row + * @return boolean + */ + protected function should_exclude_course_template($row) + { + return false; + } + + /** + * This method does all the work of retrieving your raw course section data from + * wherever it lives: via a query into your ERP, a query to a local data mirror, + * retrieving data from flat files, etc. The result should be an array with one row + * per section. It doesn't matter how that data is structured -- it will be massaged + * into shape by the mapping step. + * + * You should allow for passing an optional org_id value (a department/subject code) + * to retrieve only a subset of data. + * + * @param mixed $org_id + */ + protected function get_course_section_data($org_id = null) + { + return array(); + } + + /** + * This is a set of rules for excluding course templates from being imported. The method is + * passed a single row of data produced by get_course_template_data(), and based on the values + * in that row you can pass true or false to indicate whether the row should be discarded. + * This is helpful if you need to exclude courses based on values that are difficult to + * restrict in your initial query. + * + * @param type $row + * @return boolean + */ + protected function should_exclude_course_section($row) + { + return false; + } + + /** + * Given the 'sourced_id' value of a course template, return the corresponding entity. + * + * @param mixed ID + */ protected function get_section_parent($parent_template_id) { $es = new entity_selector(); @@ -95,14 +319,15 @@ protected function get_section_parent($parent_template_id) return false; } + /** + * Given a course section entity, create a relationship with its parent course template. + * + * @param object + */ protected function link_section_to_parent($section) { - //echo round(memory_get_usage()/1024,2)."K at point A\n"; - if ($template = $this->get_section_parent($section->get_value('parent_template_id'))) { - // echo round(memory_get_usage()/1024,2)."K at point B\n"; - if (!$parents = $section->get_right_relationship('course_template_to_course_section')) { return create_relationship( $template->id(), $section->id(), relationship_id_of('course_template_to_course_section'),false,false); @@ -110,7 +335,7 @@ protected function link_section_to_parent($section) else if (is_array($parents)) { $current_template = reset($parents); - // echo round(memory_get_usage()/1024,2)."K at point C\n"; + // verify that we have the correct parent, and fix if not. if ($current_template->get_value('sourced_id') == $template->get_value('sourced_id')) { @@ -118,142 +343,310 @@ protected function link_section_to_parent($section) } else { - //$this->errors[] = 'Incorrect template attached to '.$section->get_value('name'); - echo 'Incorrect template attached to '.$section->get_value('name'); + $this->errors[] = 'Incorrect template attached to '.$section->get_value('name'); } } else { - //$this->errors[] = 'Non-array '.$parents.' returned from get_right_relationship'; - echo 'Non-array '.$parents.' returned from get_right_relationship'; + $this->errors[] = 'Non-array '.$parents.' returned from get_right_relationship'; } } else { - //$this->errors[] = 'No template found for '.$section->get_value('name'); - echo 'No template found for '.$section->get_value('name'); + $this->errors[] = 'No template found for '.$section->get_value('name'); return false; } } - protected function build_course_template_entities($data, $existing = array()) + /** + * Given the massaged array of data produced by map_course_template_data(), build or + * update the corresponding entities. You can optionally pass an array of all the + * existing course template entity ids, to generate a report of entities that have + * dropped from the source feed. + * + * @param array Course data + * @param array IDs of existing course template entities. + * @param boolean Whether to delete entities dropped from data feed (not recommended) + * + * @todo Have deletion of templates pay attention to the time range selected + */ + protected function build_course_template_entities($data, $existing = array(), $delete = false) { - echo "Building entities\n"; + if ($this->verbosity == 2) echo "Building entities\n"; + $creator = get_user_id($this->entity_creator); foreach ($data as $key => $row) { - $name = sprintf('%s %s %s', $row['org_id'], $row['course_number'], $row['title']); - //echo 'Adding '.$name ."\n"; continue; + $name = $this->build_course_template_entity_name($row); $es = new entity_selector(); + $factory = new CourseTemplateEntityFactory(); + $es->set_entity_factory($factory); $es->add_type(id_of('course_template_type')); $es->add_relation('sourced_id = "'.$row['sourced_id'].'"'); if ($result = $es->run_one()) { $course = reset($result); - // Find all the values that correspond to the data we're importing - $values = array_intersect_key($course->get_values(), $row); - if ($values != $row) - { - echo 'Updating '.$name ."\n"; - reason_update_entity( $course->id(), get_user_id('causal_agent'), $row, false); - } - + $this->update_course_template($row, $course); $key = array_search($course->id(), $existing); if ($key !== false) unset($existing[$key]); } else { - echo 'Adding '.$name ."\n"; - reason_create_entity( id_of($this->current_site), id_of('course_template_type'), get_user_id('causal_agent'), $name, $row); + if ($this->verbosity > 0) $this->errors[] = 'Adding '.$name; + $row['new'] = 0; + $this->process_new_course_template($row); + if (!$this->test_mode) + reason_create_entity( id_of($this->courses_site), id_of('course_template_type'), $creator, $name, $row); } } if (count($existing)) { - $user = get_user_id('causal_agent'); foreach ($existing as $id) { - $course = new CourseTemplateType($id); - echo 'Would Delete: '.$course->get_value('name')."\n"; - //reason_expunge_entity($id, $user); + $course = new $GLOBALS['course_template_class']($id); + if ($this->verbosity == 2) $this->errors[] = 'No longer in feed: '.$course->get_value('name'); + if ($delete && !$this->test_mode) reason_expunge_entity($id, $creator); } } + } + + /** + * Update an existing course template entity with new values. Extend this if you need to perform + * additional actions on the data before or after an update action. + * + * @param array $data Values to apply + * @param object $entity Existing entity object + */ + protected function update_course_template($data, $entity) + { + if ($this->verbosity > 0) $name = $this->build_course_template_entity_name($data); + + // Remove any fields which are managed on the Reason side + foreach ($this->template_data_reason_managed as $key) + { + if (isset($data[$key])) unset($data[$key]); + } + + $current_values = $entity->get_values(); + + // Extract the external data cache so that we can compare it separately with the current + // external data (the timestamp needs to be removed for an accurate comparison). + $current_cache = ''; + if (isset($current_values['cache'])) + { + $current_cache = json_decode($current_values['cache'], true); + if (empty($current_cache)) $current_cache = array(); + unset($current_values['cache']); + if (isset($current_cache['timestamp'])) unset($current_cache['timestamp']); + } + + $external_data = $entity->fetch_external_data(true, false); + + // Find all the values that correspond to the data we're importing + $values = array_intersect_assoc($current_values, $data); + if ($values != $data || $external_data != $current_cache) + { + $external_data['timestamp'] = time(); + $data['cache'] = json_encode($external_data); + if ($this->verbosity > 0) $this->errors[] = 'Updating '.$name; + if (!$this->test_mode) + reason_update_entity( $entity->id(), get_user_id($this->entity_creator), $data, false); + } + else + { + if ($this->verbosity == 2) $this->errors[] = 'Unchanged: '.$name; + } + } + + /** + * This method provides a hook that is run before a new course template is added to Reason. + * If you want to modify values based on existing entities, or do something else when you know + * that a template is new, you can do that here. The data row representing the template is + * passed and updated by reference. + * + * @param type $row + */ + protected function process_new_course_template(&$row) + { } + + /** + * Construct the template entity name (only visible in the Reason admin). You can modify this + * method if you want your names constructed differently. + * + * @param array data row + * @return string + */ + protected function build_course_template_entity_name($row) + { + return sprintf('%s %s %s', $row['org_id'], $row['course_number'], $row['title']); + } - protected function build_course_section_entities($data) + /** + * Given the data assembled by map_course_section_data(), look to see if there's a matching + * entity. If there is, update it if any values have changed. If no entity exists, + * create one and link it to its parent course template entity. + * + * @param array data row + * @return string + * + * @todo Have deletion of sections pay attention to the time range selected + */ + protected function build_course_section_entities($data, $existing = array(), $delete = false) { - echo "Building section entities\n"; + if ($this->verbosity == 2) echo "Building section entities\n"; + $creator = get_user_id($this->entity_creator); foreach ($data as $key => $row) { - //echo round(memory_get_usage()/1024,2)."K at point E\n"; $es = new entity_selector(); $es->add_type(id_of('course_section_type')); - $name = sprintf('%s %s %s', $row['course_number'], $row['academic_session'], $row['title'] ); + $factory = new CourseSectionEntityFactory(); + $es->set_entity_factory($factory); + $name = $this->build_course_section_entity_name($row); $es->relations = array(); $es->add_relation('sourced_id = "'.$row['sourced_id'].'"'); if ($result = $es->run_one()) { $section = reset($result); - // Find all the values that correspond to the data we're importing - $values = array_intersect_key($section->get_values(), $row); - if ($values != $row) - { - echo 'Updating: '.$name ."\n"; - reason_update_entity( $section->id(), get_user_id('causal_agent'), $row, false); - } - else - { - echo 'Unchanged: '.$name ."\n"; - } + $this->update_course_section($row, $section); + $key = array_search($section->id(), $existing); + if ($key !== false) unset($existing[$key]); } else { if ($this->get_section_parent($row['parent_template_id'])) { - echo 'Adding: '.$name ."\n"; - $id = reason_create_entity( id_of($this->current_site), id_of('course_section_type'), get_user_id('causal_agent'), $name, $row); - $section = new entity($id); + if ($this->verbosity > 0) $this->errors[] = 'Adding: '.$name; + $row['new'] = 0; + $this->process_new_course_section($row); + if (!$this->test_mode) + { + $id = reason_create_entity( id_of($this->courses_site), id_of('course_section_type'), $creator, $name, $row); + $section = new entity($id); + } } else { - echo 'No course template found; skipping '.$name ."\n"; + if ($this->verbosity == 2) $this->errors[] = 'No course template found; skipping '.$name; continue; } } if (!empty($section)) $this->link_section_to_parent($section); - //echo round(memory_get_usage()/1024,2)."K at point D\n"; } + + if (count($existing)) + { + foreach ($existing as $id) + { + $course = new $GLOBALS['course_section_class']($id); + if ($this->verbosity == 2) $this->errors[] = 'No longer in feed: '.$course->get_value('course_number').': '.$course->get_value('name'); + if ($delete && !$this->test_mode) reason_expunge_entity($id, $creator); + } + } + } + /** + * Update an existing course section entity with new values. Extend this if you need to perform + * additional actions on the data before or after an update action. + * + * @param array $data Values to apply + * @param object $entity Existing entity object + */ + protected function update_course_section($data, $entity) + { + if ($this->verbosity > 0) $name = $this->build_course_section_entity_name($data); + + // Remove any fields which are managed on the Reason side + foreach ($this->section_data_reason_managed as $key) + { + if (isset($data[$key])) unset($data[$key]); + } + + $current_values = $entity->get_values(); + + // Extract the external data cache so that we can compare it separately with the current + // external data (the timestamp needs to be removed for an accurate comparison). + $current_cache = ''; + if (isset($current_values['cache'])) + { + $current_cache = json_decode($current_values['cache'], true); + if (empty($current_cache)) $current_cache = array(); + unset($current_values['cache']); + if (isset($current_cache['timestamp'])) unset($current_cache['timestamp']); + } + + $external_data = $entity->fetch_external_data(true, false); + + // Find all the values that correspond to the data we're importing + $values = array_intersect_assoc($current_values, $data); + if ($values != $data || $external_data != $current_cache) + { + $external_data['timestamp'] = time(); + $data['cache'] = json_encode($external_data); + if ($this->verbosity > 0) $this->errors[] = 'Updating: '.$name; + if (!$this->test_mode) + reason_update_entity( $entity->id(), get_user_id($this->entity_creator), $data, false); + } + else + { + if ($this->verbosity == 2) $this->errors[] = 'Unchanged: '.$name; + } + + } + + /** + * This method provides a hook that is run before a new course template is added to Reason. + * If you want to modify values based on existing entities, or do something else when you know + * that a template is new, you can do that here. The data row representing the template is + * passed and updated by reference. + * + * @param type $row + */ + protected function process_new_course_section(&$row) + { + + } + + /** + * Construct the section entity name (only visible in the Reason admin). You can modify this + * method if you want your names constructed differently. + * + * @param array data row + * @return string + */ + protected function build_course_section_entity_name($row) + { + return sprintf('%s %s %s', $row['course_number'], $row['academic_session'], $row['title'] ); + } + + /** + * This method accepts the raw data array generated in get_course_template_data() and + * maps it to the Reason course schema based on the rules found in $this->template_data_map. + * Rules can specify a one-to-one mapping between a source field and a schema field, + * specify a function to be called to perform the mapping, or assign a static value. + * + * @param array Raw course template data + */ protected function map_course_template_data($data) { - echo "map_course_template_data\n"; - $map = array( - 'course_number' => 'SEC_COURSE_NO', - 'org_id' => 'SEC_SUBJECT', - 'title' => 'title', - 'short_description' => null, - 'long_description' => 'description', - 'credits' => 'SEC_MAX_CRED', - 'list_of_prerequisites' => 'prereq', - 'status' => 'Active', - 'data_source' => $this->data_source_name, - 'sourced_id' => 'COURSES_ID', - ); + if ($this->verbosity == 2) echo "map_course_template_data\n"; foreach($data as $row) { unset($mapped_row); - foreach ($map as $key => $mapkey) + foreach ($this->template_data_map as $key => $mapkey) { if ($mapkey) { if (method_exists($this, 'template_map_'.$mapkey)) { $method = 'template_map_'.$mapkey; - $mapped_row[$key] = $this->$method($row); + $value = $this->$method($row); + if ($value !== false) $mapped_row[$key] = $value; } else if (array_key_exists($mapkey, $row)) { @@ -279,39 +672,30 @@ protected function map_course_template_data($data) } } + /** + * This method accepts the raw data array generated in get_course_section_data() and + * maps it to the Reason course schema based on the rules found in $this->section_data_map. + * Rules can specify a one-to-one mapping between a source field and a schema field, + * specify a function to be called to perform the mapping, or assign a static value. + * + * @param array Raw course section data + */ protected function map_course_section_data($data) { - echo "map_course_template_data\n"; - $map = array( - 'course_number' => 'SEC_NAME', - 'org_id' => 'SEC_SUBJECT', - 'title' => 'title', - 'short_description' => null, - 'long_description' => 'description', - 'credits' => 'credits', - 'academic_session' => 'SEC_TERM', - 'timeframe_begin' => 'SEC_START_DATE', - 'timeframe_end' => 'SEC_END_DATE', - 'location' => 'location', - 'meeting' => 'meeting', - 'notes' => null, - 'status' => 'Active', - 'data_source' => $this->data_source_name, - 'sourced_id' => 'COURSE_SECTIONS_ID', - 'parent_template_id' => 'SEC_COURSE', - ); - + if ($this->verbosity == 2) echo "map_course_section_data\n"; + foreach($data as $row) { unset($mapped_row); - foreach ($map as $key => $mapkey) + foreach ($this->section_data_map as $key => $mapkey) { if ($mapkey) { if (method_exists($this, 'section_map_'.$mapkey)) { $method = 'section_map_'.$mapkey; - $mapped_row[$key] = $this->$method($row); + $value = $this->$method($row); + if ($value !== false) $mapped_row[$key] = $value; } else if (array_key_exists($mapkey, $row)) { @@ -339,189 +723,50 @@ protected function map_course_section_data($data) return false; } } - - protected function get_course_template_data($org_id = null) - { - echo "get_course_template_data $org_id\n"; - $restore_conn = get_current_db_connection_name(); - connectDB('reg_catalog_new'); - mysql_set_charset('utf8'); - $query = 'SELECT * FROM IDM_CRS WHERE CRS_END_DATE IS NULL OR CRS_END_DATE > NOW() ORDER BY CRS_NAME'; - if ($result = mysql_query($query)) - { - while ($row = mysql_fetch_assoc($result)) - { - if (strpos($row['CRS_NAME'], 'OCP') === 0) continue; - if (strpos($row['CRS_NAME'], 'NORW') === 0) continue; - if (substr($row['CRS_NAME'], -3) == 'SAT') continue; - if (substr($row['CRS_NAME'], -2) == 'WL') continue; - if (substr($row['CRS_NAME'], -2) == 'IB') continue; - if (substr($row['CRS_NAME'], -2) == 'CC') continue; - if (substr($row['CRS_NAME'], -2) == 'MP') continue; - if (substr($row['CRS_NAME'], -2) == 'AP') continue; - if (substr($row['CRS_NAME'], -1) == 'S') continue; - if (substr($row['CRS_NAME'], -1) == 'L') continue; - - $found = false; - $coursetableyear = $this->course_table_year; - while ($coursetableyear > 2009) - { - $coursetable = 'course'.$coursetableyear; - $query = 'SELECT IDM_COURSE.*, description, title, prereq FROM IDM_COURSE - JOIN '.$coursetable.' - ON '.$coursetable.'.course_id = IDM_COURSE.SEC_COURSE - AND ('.$coursetable.'.match_title IS NULL - OR '.$coursetable.'.match_title = IDM_COURSE.SEC_SHORT_TITLE) - WHERE SEC_COURSE = "'.$row['COURSES_ID'].'"'; - - if ($result2 = mysql_query($query)) - { - if (mysql_num_rows($result2) == 1) - { - $row = array_merge($row, mysql_fetch_assoc($result2)); - $found = true; - } - else if (mysql_num_rows($result2) > 1) - { - $row = array_merge($row, mysql_fetch_assoc($result2)); - $found = true; - } - else - { - //echo "No data for $row[COURSES_ID] in $coursetable\n"; - $coursetableyear--; - continue; - } - $data[] = $row; - break; - } - else - { - $this->errors[] = mysql_error(); - } - } - if (!$found) echo "No data for $row[CRS_NAME] in catalog\n"; - } - } - else - { - $this->errors[] = mysql_error(); - } - connectDB($restore_conn); - - if (isset($data)) return $data; - } - - protected function get_course_section_data($org_id = null) - { - echo "get_course_section_data $org_id\n"; - $data = array(); - $coursetable = 'course'.$this->course_table_year; - $restore_conn = get_current_db_connection_name(); - connectDB('reg_catalog_new'); - mysql_set_charset('utf8'); - $found = false; - $coursetableyear = $this->course_table_year; - $org_id_limit = ($org_id) ? ' AND SEC_SUBJECT="'.$org_id.'" ' : ''; - while ($coursetableyear > 2009) - { - $coursetable = 'course'.$coursetableyear; - $query = 'SELECT s.*, description, title FROM IDM_CRS c, IDM_COURSE s - JOIN '.$coursetable.' - ON '.$coursetable.'.course_id = s.SEC_COURSE - AND ('.$coursetable.'.match_title IS NULL - OR '.$coursetable.'.match_title = s.SEC_SHORT_TITLE) - WHERE s.SEC_COURSE = c.COURSES_ID AND - (CRS_END_DATE IS NULL OR CRS_END_DATE > NOW()) - AND SEC_START_DATE > "2009-09-01 00:00:00" '. $org_id_limit .' - ORDER BY SEC_NAME'; - - if ($result = mysql_query($query)) - { - while ($row = mysql_fetch_assoc($result)) - { - if (isset($row['SEC_SUBJECT']) && empty($data[$row['COURSE_SECTIONS_ID']])) - { - if ($row['SEC_SUBJECT'] == 'OCP' || $row['SEC_SUBJECT'] == 'NORW') continue; - if (strpos($row['SEC_NO'], 'WL') !== false) continue; - //if (strpos($row['SEC_TERM'], 'SU') !== false) continue; - - $data[$row['COURSE_SECTIONS_ID']] = $row; - } - } - } - else - { - $this->errors[] = mysql_error(); - } - $coursetableyear--; - } - connectDB($restore_conn); - return $data; - } - - protected function section_map_location($row) - { - $location = array(); - if ($times = explode('|', $row['XSEC_CC_MEETING_TIMES_SV'])) - { - foreach ($times as $time) - { - if (preg_match('/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/', $time, $matches)) - { - $location[] = $matches[1] . ' '. $matches[2]; - } - } - } - return join('|', $location); - } - - protected function section_map_meeting($row) - { - $meeting = array(); - if ($times = explode('|', $row['XSEC_CC_MEETING_TIMES_SV'])) - { - foreach ($times as $time) - { - if (preg_match('/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/', $time, $matches)) - { - $meeting[] = $matches[3] . ' '. $matches[4] . ' '. $matches[5]; - } - } - } - return join('|', $meeting); - } /** - * If both min and max credits are set, return "MIN-MAX" -- otherwise, return - * whichever one is set, or nothing. - */ - protected function section_map_credits($row) - { - $credits = array(); - if ($row['SEC_MIN_CRED']) $credits[] = $row['SEC_MIN_CRED']; - if ($row['SEC_MAX_CRED']) $credits[] = $row['SEC_MAX_CRED']; - return join('-', $credits); - } - - protected function delete_all_course_entities() + * Remove all existing course template entities. Used mostly for catalog setup and testing. + */ + public function delete_all_course_entities() { - $user = get_user_id('causal_agent'); - foreach ($this->get_existing_template_ids() as $id) + $user = get_user_id($this->entity_creator); + $ids = $this->get_existing_template_ids(); + foreach ($ids as $id) { reason_expunge_entity($id, $user); } + unset($ids); } - protected function delete_all_section_entities() + /** + * Remove all existing course section entities. Used mostly for catalog setup and testing. + */ + public function delete_all_section_entities() { - $user = get_user_id('causal_agent'); - foreach ($this->get_existing_section_ids() as $id) + $user = get_user_id($this->entity_creator); + $ids = $this->get_existing_section_ids(); + foreach ($ids as $id) { reason_expunge_entity($id, $user); } + unset($ids); + + // Clear any defunct relationships + $q = 'DELETE FROM relationship WHERE type in ('. + relationship_id_of('course_section_to_category') . ',' . + relationship_id_of('course_template_to_course_section') . ',' . + relationship_id_of('site_owns_course_section_type') . ',' . + relationship_id_of('site_borrows_course_section_type') . ')'; + db_query( $q, 'Unable to delete relationships of entity '.$id ); } + /** + * Get all the ids of course template entities, optionally limited by the org_id value on + * the entity. + * + * @param string $org_id + * @return array + */ protected function get_existing_template_ids($org_id = null) { $es = new entity_selector(); @@ -533,6 +778,13 @@ protected function get_existing_template_ids($org_id = null) return array(); } + /** + * Get all the ids of course section entities, optionally limited by the org_id value on + * the entity. + * + * @param string $org_id + * @return array + */ protected function get_existing_section_ids($org_id = null) { $es = new entity_selector(); @@ -544,35 +796,19 @@ protected function get_existing_section_ids($org_id = null) return array(); } - protected function get_template_org_ids() - { - return $this->get_section_org_ids(); - } - - protected function get_section_org_ids() - { - $org_ids = array(); - $q = 'SELECT DISTINCT SEC_SUBJECT FROM IDM_COURSE ORDER BY SEC_SUBJECT'; - connectDB('reg_catalog_new'); - if ($result = mysql_query($q)) - { - while($row = mysql_fetch_assoc($result)) - $org_ids[] = $row['SEC_SUBJECT']; - } - connectDB(REASON_DB); - return $org_ids; - } - - function disable_output_buffering() + protected function disable_output_buffering() { - @apache_setenv('no-gzip', 1); + // We don't need to (and shouldn't) try to do this in command line mode + if (php_sapi_name() != "cli") + @apache_setenv('no-gzip', 1); + @ini_set('zlib.output_compression', 0); @ini_set('implicit_flush', 1); for ($i = 0; $i < ob_get_level(); $i++) { ob_end_flush(); } ob_implicit_flush(1); } - function get_subjects() + public function get_reason_subjects() { $subjects = array(); $es = new entity_selector(); @@ -587,85 +823,29 @@ function get_subjects() return $subjects; } - function import_course_blocks() + /** + * Remove all existing course blocks in a particular catalog site (designated by academic year). + * Used mostly for catalog setup and testing. + * + * @param int $year + */ + public function delete_all_course_blocks($year) { - $subjects = $this->get_subjects(); - $root_id = root_finder(id_of($this->current_site)); - $rows = array(); - - connectDB('reg_catalog'); - mysql_set_charset('utf8'); - $query = 'SELECT * FROM blocks JOIN block_types ON blocks.type = block_types.id - WHERE YEAR = 2015 ORDER BY dept, sequence'; - if ($result = mysql_query($query)) + if (!$site_id = id_of('academic_catalog_'.$year.'_site')) { - while ($row = mysql_fetch_assoc($result)) - { - $rows[] = $row; - } + echo 'No site found: academic_catalog_'.$year.'_site'; + return; } - connectDB(REASON_DB); - - $subj = ''; - $seq = $page_seq = 1; - foreach ($rows as $row) - { - if ($row['dept'] != $subj) - { - $subj = $row['dept']; - $seq = 1; - if (isset($subjects[$subj])) - $page_name = $subjects[$subj]->get_value('name'); - else - $page_name = $subj; - - $page_values = array( - 'new' => 0, - 'url_fragment' => strtolower($subj), - 'custom_page' => 'catalog_subject_page', - 'link_name' => $page_name, - 'nav_display' => 'Yes', - ); - - $page_id = reason_create_entity( id_of($this->current_site), id_of('minisite_page'), get_user_id('causal_agent'), $page_name, $page_values); - create_relationship( $page_id, $root_id, relationship_id_of('minisite_page_parent'), array('rel_sort_order'=>$page_seq),false); - - $page_seq++; - } - - $block_name = $row['dept'] . ' ' . $row['name']; - - // Fix up include tags - if (preg_match_all('/(\{\{?)([^}]+)\}\}?/', $row['content'], $matches, PREG_SET_ORDER)) + $es = new entity_selector($site_id); + $es->add_type(id_of('course_catalog_block_type')); + if ($result = $es->get_ids()) + { + $user = get_user_id($this->entity_creator); + foreach ($result as $id) { - foreach ($matches as $match) - { - if (preg_match('/(.*) ALL/', $match[2], $submatch)) - $key = 'org_id="'.$submatch[1].'"'; - else - $key = 'gov_code="'.$match[2].'"'; - - if ($match[1] == '{') - $replace = '{descriptions '.$key.'}'; - else - $replace = '{titles '.$key.'}'; - - $row['content'] = str_replace($match[0], $replace, $row['content']); - } + reason_expunge_entity($id, $user); } - - $block_values = array( - 'new' => 0, - 'org_id' => $row['dept'], - 'title' => $row['title'], - 'block_type' => $row['name'], - 'content' => $row['content'], - ); - - $block_id = reason_create_entity( id_of($this->current_site), id_of('course_catalog_block_type'), get_user_id('causal_agent'), $block_name, $block_values); - create_relationship( $page_id, $block_id, relationship_id_of('page_to_course_catalog_block'), array('rel_sort_order'=>$seq), false); - } } } diff --git a/reason_4.0/lib/core/scripts/upgrade/4.4_to_4.5/setup_course_support.php b/reason_4.0/lib/core/scripts/upgrade/4.4_to_4.5/setup_course_support.php index 061d4851a..97d7d3ec9 100644 --- a/reason_4.0/lib/core/scripts/upgrade/4.4_to_4.5/setup_course_support.php +++ b/reason_4.0/lib/core/scripts/upgrade/4.4_to_4.5/setup_course_support.php @@ -55,6 +55,7 @@ class ReasonUpgrader_45_SetupCourseSupport implements reasonUpgraderInterface 'short_description' => array('db_type' => 'text'), 'long_description' => array('db_type' => 'text'), 'credits' => array('db_type' => 'varchar(50)'), + 'list_of_prerequisites' => array('db_type' => 'text'), 'academic_session' => array('db_type' => 'varchar(20)'), 'timeframe_begin' => array('db_type' => 'datetime'), 'timeframe_end' => array('db_type' => 'datetime'), @@ -155,11 +156,6 @@ public function test() $str = ''; if (!$this->course_template_type_exists()) $str .= 'Would create course_template type.
'; if (!$this->course_section_type_exists()) $str .= 'Would create course_section type.
'; - if (empty($str)) - { - $str .= $this->add_indexes(); // only attempt test if both types exist to avoid crash - } - else $str .= 'Would attempt to add indexes to type tables.
'; return $str; } } @@ -320,4 +316,4 @@ protected function course_section_type_exists() } } -?> +?> \ No newline at end of file diff --git a/reason_4.0/lib/core/scripts/upgrade/4.6_to_4.7/relationship_metadata.php b/reason_4.0/lib/core/scripts/upgrade/4.6_to_4.7/relationship_metadata.php index ec5bc1c3b..ecdb541c6 100644 --- a/reason_4.0/lib/core/scripts/upgrade/4.6_to_4.7/relationship_metadata.php +++ b/reason_4.0/lib/core/scripts/upgrade/4.6_to_4.7/relationship_metadata.php @@ -43,10 +43,12 @@ public function description() return "This upgrade: