diff --git a/blocks/activity_modules/block_activity_modules.php b/blocks/activity_modules/block_activity_modules.php index adff582bdb7..c44ceb40f9b 100644 --- a/blocks/activity_modules/block_activity_modules.php +++ b/blocks/activity_modules/block_activity_modules.php @@ -27,7 +27,8 @@ class block_activity_modules extends block_list { $archetypes = array(); foreach($modinfo->cms as $cm) { - if (!$cm->uservisible or $cm->modname === 'label') { + // Exclude activities which are not visible or have no link (=label) + if (!$cm->uservisible or !$cm->has_view()) { continue; } if (array_key_exists($cm->modname, $modfullnames)) { diff --git a/blocks/site_main_menu/block_site_main_menu.php b/blocks/site_main_menu/block_site_main_menu.php index 3e139aca9fb..d3434f845b9 100644 --- a/blocks/site_main_menu/block_site_main_menu.php +++ b/blocks/site_main_menu/block_site_main_menu.php @@ -40,21 +40,19 @@ class block_site_main_menu extends block_list { if (!$cm->uservisible) { continue; } - if ($cm->modname == 'label') { - $this->content->items[] = format_text($cm->extra, FORMAT_HTML, $options); + + list($content, $instancename) = + get_print_section_cm_text($cm, $course); + + if (!($url = $cm->get_url())) { + $this->content->items[] = $content; $this->content->icons[] = ''; } else { $linkcss = $cm->visible ? '' : ' class="dimmed" '; - $instancename = format_string($cm->name, true, $course->id); //Accessibility: incidental image - should be empty Alt text - if (!empty($cm->icon)) { - $icon = $OUTPUT->pix_url($cm->icon); - } else { - $icon = $OUTPUT->pix_url('icon', $cm->modname); - } - $icon = ' '; + $icon = ' '; $this->content->items[] = 'extra. - ' href="'.$CFG->wwwroot.'/mod/'.$cm->modname.'/view.php?id='.$cm->id.'">'.$icon.$instancename.''; + ' href="' . $url . '">' . $icon . $instancename . ''; } } } @@ -114,28 +112,18 @@ class block_site_main_menu extends block_list { ''.$strmovehere.''; $this->content->icons[] = ''; } - $instancename = $modinfo->cms[$modnumber]->name; - $instancename = format_string($instancename, true, $course->id); + list($content, $instancename) = + get_print_section_cm_text($modinfo->cms[$modnumber], $course); $linkcss = $mod->visible ? '' : ' class="dimmed" '; - if (!empty($modinfo->cms[$modnumber]->extra)) { - $extra = $modinfo->cms[$modnumber]->extra; - } else { - $extra = ''; - } - if (!empty($modinfo->cms[$modnumber]->icon)) { - $icon = $OUTPUT->pix_url($modinfo->cms[$modnumber]->icon); - } else { - $icon = $OUTPUT->pix_url('icon', $mod->modname); - } - if ($mod->modname == 'label') { - $this->content->items[] = format_text($extra, FORMAT_HTML,$options).$editbuttons; + if (!($url = $mod->get_url())) { + $this->content->items[] = $content . $editbuttons; $this->content->icons[] = ''; } else { //Accessibility: incidental image - should be empty Alt text - $icon = ' '; - $this->content->items[] = ''.$icon.$instancename.''.$editbuttons; + $icon = ' '; + $this->content->items[] = 'extra . + ' href="' . $url . '">' . $icon . $instancename . '' . $editbuttons; } } } diff --git a/blocks/social_activities/block_social_activities.php b/blocks/social_activities/block_social_activities.php index bcf7d7bf315..886e8883951 100644 --- a/blocks/social_activities/block_social_activities.php +++ b/blocks/social_activities/block_social_activities.php @@ -42,21 +42,19 @@ class block_social_activities extends block_list { if (!$cm->uservisible) { continue; } - if ($cm->modname == 'label') { - $this->content->items[] = format_text($cm->extra, FORMAT_HTML, $options); + + list($content, $instancename) = + get_print_section_cm_text($cm, $course); + + if (!($url = $cm->get_url())) { + $this->content->items[] = $content; $this->content->icons[] = ''; } else { $linkcss = $cm->visible ? '' : ' class="dimmed" '; - $instancename = format_string($cm->name, true, $course->id); //Accessibility: incidental image - should be empty Alt text - if (!empty($cm->icon)) { - $icon = $OUTPUT->pix_url($cm->icon); - } else { - $icon = $OUTPUT->pix_url('icon', $cm->modname); - } - $icon = ' '; + $icon = ' '; $this->content->items[] = 'extra. - ' href="'.$CFG->wwwroot.'/mod/'.$cm->modname.'/view.php?id='.$cm->id.'">'.$icon.$instancename.''; + ' href="' . $url . '">' . $icon . $instancename . ''; } } } @@ -123,28 +121,19 @@ class block_social_activities extends block_list { ''.$strmovehere.''; $this->content->icons[] = ''; } - $instancename = $modinfo->cms[$modnumber]->name; - $instancename = format_string($instancename, true, $course->id); - $linkcss = $mod->visible ? '' : ' class="dimmed" '; - if (!empty($modinfo->cms[$modnumber]->extra)) { - $extra = $modinfo->cms[$modnumber]->extra; - } else { - $extra = ''; - } - if (!empty($modinfo->cms[$modnumber]->icon)) { - $icon = $OUTPUT->pix_url($modinfo->cms[$modnumber]->icon); - } else { - $icon = $OUTPUT->pix_url('icon', $mod->modname); - } + list($content, $instancename) = + get_print_section_cm_text($modinfo->cms[$modnumber], $course); - if ($mod->modname == 'label') { - $this->content->items[] = format_text($extra, FORMAT_HTML, $options).$editbuttons; + $linkcss = $mod->visible ? '' : ' class="dimmed" '; + + if (!($url = $mod->get_url())) { + $this->content->items[] = $content . $editbuttons; $this->content->icons[] = ''; } else { //Accessibility: incidental image - should be empty Alt text - $icon = ' '; - $this->content->items[] = ''.$icon.$instancename.''.$editbuttons; + $icon = ' '; + $this->content->items[] = 'extra . + ' href="' . $url . '">' . $icon . $instancename . '' . $editbuttons; } } } diff --git a/course/lib.php b/course/lib.php index d5a5ba5c7f1..492275d7873 100644 --- a/course/lib.php +++ b/course/lib.php @@ -48,10 +48,6 @@ define('FIRSTUSEDEXCELROW', 3); define('MOD_CLASS_ACTIVITY', 0); define('MOD_CLASS_RESOURCE', 1); -if (!defined('MAX_MODINFO_CACHE_SIZE')) { - define('MAX_MODINFO_CACHE_SIZE', 10); -} - function make_log_url($module, $url) { switch ($module) { case 'course': @@ -959,6 +955,9 @@ function print_recent_activity($course) { } $info = explode(' ', $log->info); + // note: in most cases I replaced hardcoding of label with use of + // $cm->has_view() but it was not possible to do this here because + // we don't necessarily have the $cm for it if ($info[0] == 'label') { // Labels are ignored in recent activity continue; } @@ -1112,9 +1111,6 @@ function get_array_of_activities($courseid) { if (function_exists($functionname)) { if ($info = $functionname($rawmods[$seq])) { - if (!empty($info->extra)) { - $mod[$seq]->extra = $info->extra; - } if (!empty($info->icon)) { $mod[$seq]->icon = $info->icon; } @@ -1124,11 +1120,45 @@ function get_array_of_activities($courseid) { if (!empty($info->name)) { $mod[$seq]->name = $info->name; } + if ($info instanceof cached_cm_info) { + // When using cached_cm_info you can include three new fields + // that aren't available for legacy code + if (!empty($info->content)) { + $mod[$seq]->content = $info->content; + } + if (!empty($info->extraclasses)) { + $mod[$seq]->extraclasses = $info->extraclasses; + } + if (!empty($info->onclick)) { + $mod[$seq]->onclick = $info->onclick; + } + if (!empty($info->customdata)) { + $mod[$seq]->customdata = $info->customdata; + } + } else { + // When using a stdclass, the (horrible) deprecated ->extra field + // is available for BC + if (!empty($info->extra)) { + $mod[$seq]->extra = $info->extra; + } + } } } if (!isset($mod[$seq]->name)) { $mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance)); } + + // Minimise the database size by unsetting default options when they are + // 'empty'. This list corresponds to code in the cm_info constructor. + foreach(array('idnumber', 'groupmode', 'groupingid', 'groupmembersonly', + 'indent', 'completion', 'extra', 'extraclasses', 'onclick', 'content', + 'icon', 'iconcomponent', 'customdata', 'availablefrom', 'availableuntil', + 'conditionscompletion', 'conditionsgrade') as $property) { + if (property_exists($mod[$seq], $property) && + empty($mod[$seq]->{$property})) { + unset($mod[$seq]->{$property}); + } + } } } } @@ -1250,6 +1280,44 @@ function set_section_visible($courseid, $sectionnumber, $visibility) { } } +/** + * Obtains shared data that is used in print_section when displaying a + * course-module entry. + * + * Calls format_text or format_string as appropriate, and obtains the correct icon. + * + * This data is also used in other areas of the code. + * @param cm_info $cm Course-module data (must come from get_fast_modinfo) + * @param object $course Moodle course object + * @return array An array with the following values in this order: + * $content (optional extra content for after link), + * $instancename (text of link) + */ +function get_print_section_cm_text(cm_info $cm, $course) { + global $OUTPUT; + + // Get course context + $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); + + // Get content from modinfo if specified. Content displays either + // in addition to the standard link (below), or replaces it if + // the link is turned off by setting ->url to null. + if (($content = $cm->get_content()) !== '') { + $labelformatoptions = new stdClass(); + $labelformatoptions->noclean = true; + $labelformatoptions->overflowdiv = true; + $labelformatoptions->context = $coursecontext; + $content = format_text($content, FORMAT_HTML, $labelformatoptions); + } else { + $content = ''; + } + + $stringoptions = new stdClass; + $stringoptions->context = $coursecontext; + $instancename = format_string($cm->name, true, $stringoptions); + return array($content, $instancename); +} + /** * Prints a section full of activity modules */ @@ -1265,8 +1333,8 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, static $strmovehere; static $strmovefull; static $strunreadpostsone; - static $usetracking; static $groupings; + static $modulenames; if (!isset($initialised)) { $groupbuttons = ($course->groupmode or (!$course->groupmodeforce)); @@ -1277,18 +1345,12 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, $strmovehere = get_string("movehere"); $strmovefull = strip_tags(get_string("movefull", "", "'$USER->activitycopyname'")); } - include_once($CFG->dirroot.'/mod/forum/lib.php'); - if ($usetracking = forum_tp_can_track_forums()) { - $strunreadpostsone = get_string('unreadpostsone', 'forum'); - } + $modulenames = array(); $initialised = true; } - $labelformatoptions = new stdClass(); - $labelformatoptions->noclean = true; - $labelformatoptions->overflowdiv = true; + $tl = textlib_get_instance(); -/// Casting $course->modinfo to string prevents one notice when the field is null $modinfo = get_fast_modinfo($course); $completioninfo = new completion_info($course); @@ -1304,6 +1366,9 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, continue; } + /** + * @var cm_info + */ $mod = $mods[$modnumber]; if ($ismoving and $mod->id == $USER->activitycopy) { @@ -1341,6 +1406,11 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, } } + if (!isset($modulenames[$mod->modname])) { + $modulenames[$mod->modname] = get_string('modulename', $mod->modname); + } + $modulename = $modulenames[$mod->modname]; + // In some cases the activity is visible to user, but it is // dimmed. This is done if viewhiddenactivities is true and if: // 1. the activity is not visible, or @@ -1366,6 +1436,10 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, $liclasses[] = 'activity'; $liclasses[] = $mod->modname; $liclasses[] = 'modtype_'.$mod->modname; + $extraclasses = $mod->get_extra_classes(); + if ($extraclasses) { + $liclasses = array_merge($liclasses, explode(' ', $extraclasses)); + } echo html_writer::start_tag('li', array('class'=>join(' ', $liclasses), 'id'=>'module-'.$modnumber)); if ($ismoving) { echo 'join(' ', $classes))); - $extra = ''; - if (!empty($modinfo->cms[$modnumber]->extra)) { - $extra = $modinfo->cms[$modnumber]->extra; + // Get data about this course-module + list($content, $instancename) = + get_print_section_cm_text($modinfo->cms[$modnumber], $course); + + //Accessibility: for files get description via icon, this is very ugly hack! + $altname = ''; + $altname = $mod->modfullname; + if (!empty($customicon)) { + $archetype = plugin_supports('mod', $mod->modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); + if ($archetype == MOD_ARCHETYPE_RESOURCE) { + $mimetype = mimeinfo_from_icon('type', $customicon); + $altname = get_mimetype_description($mimetype); + } + } + // Avoid unnecessary duplication: if e.g. a forum name already + // includes the word forum (or Forum, etc) then it is unhelpful + // to include that in the accessible description that is added. + if (false !== strpos($tl->strtolower($instancename), + $tl->strtolower($altname))) { + $altname = ''; + } + // File type after name, for alphabetic lists (screen reader). + if ($altname) { + $altname = get_accesshide(' '.$altname); } - if ($mod->modname == "label") { - if ($accessiblebutdim || !$mod->uservisible) { - echo '
'. - get_string('hiddenfromstudents').''; + // We may be displaying this just in order to show information + // about visibility, without the actual link + $contentpart = ''; + if ($mod->uservisible) { + // Nope - in this case the link is fully working for user + $linkclasses = ''; + $textclasses = ''; + if ($accessiblebutdim) { + $linkclasses .= ' dimmed'; + $textclasses .= ' dimmed_text'; + $accesstext = ''. + get_string('hiddenfromstudents').': '; } else { - echo '
'; + $accesstext = ''; } - echo format_text($extra, FORMAT_HTML, $labelformatoptions); - echo "
"; + if ($linkclasses) { + $linkcss = 'class="' . trim($linkclasses) . '" '; + } else { + $linkcss = ''; + } + if ($textclasses) { + $textcss = 'class="' . trim($textclasses) . '" '; + } else { + $textcss = ''; + } + + // Get on-click attribute value if specified + $onclick = $mod->get_on_click(); + if ($onclick) { + $onclick = ' onclick="' . $onclick . '"'; + } + + if ($url = $mod->get_url()) { + // Display link itself + echo '
extra . $onclick . + ' href="' . $url . '">' .
+                            $modulename . ' ' . + $accesstext . '' . + $instancename . $altname . ''; + + // If specified, display extra content after link + if ($content) { + $contentpart = '
' . $content . '
'; + } + } else { + // No link, so display only content + $contentpart = '
extra . '>' . + $accesstext . $content . '
'; + } + if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', get_context_instance(CONTEXT_COURSE, $course->id))) { if (!isset($groupings)) { $groupings = groups_get_all_groupings($course->id); } echo " (".format_string($groupings[$mod->groupingid]->name).')'; } - - } else { // Normal activity - $instancename = format_string($modinfo->cms[$modnumber]->name, true, $course->id); - - $customicon = $modinfo->cms[$modnumber]->icon; - if (!empty($customicon)) { - if (substr($customicon, 0, 4) === 'mod/') { - list($modname, $iconname) = explode('/', substr($customicon, 4), 2); - $icon = $OUTPUT->pix_url($iconname, $modname); - } else { - $icon = $OUTPUT->pix_url($customicon); - } + } else { + $textclasses = $extraclasses; + $textclasses .= ' dimmed_text'; + if ($textclasses) { + $textcss = 'class="' . trim($textclasses) . '" '; } else { - $icon = $OUTPUT->pix_url('icon', $mod->modname); + $textcss = ''; } + $accesstext = '' . + get_string('notavailableyet', 'condition') . + ': '; - //Accessibility: for files get description via icon, this is very ugly hack! - $altname = ''; - $altname = $mod->modfullname; - if (!empty($customicon)) { - $archetype = plugin_supports('mod', $mod->modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); - if ($archetype == MOD_ARCHETYPE_RESOURCE) { - $mimetype = mimeinfo_from_icon('type', $customicon); - $altname = get_mimetype_description($mimetype); - } - } - // Avoid unnecessary duplication. - if (false !== stripos($instancename, $altname)) { - $altname = ''; - } - // File type after name, for alphabetic lists (screen reader). - if ($altname) { - $altname = get_accesshide(' '.$altname); - } - - // We may be displaying this just in order to show information - // about visibility, without the actual link - if ($mod->uservisible) { - // Display normal module link - if (!$accessiblebutdim) { - $linkcss = ''; - $accesstext =''; - } else { - $linkcss = ' class="dimmed" '; - $accesstext = ''. - get_string('hiddenfromstudents').': '; - } - - echo ''. - ''.get_string('modulename',$mod->modname).' '. - $accesstext.''.$instancename.$altname.''; - - if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', get_context_instance(CONTEXT_COURSE, $course->id))) { - if (!isset($groupings)) { - $groupings = groups_get_all_groupings($course->id); - } - echo " (".format_string($groupings[$mod->groupingid]->name).')'; - } - } else { + if ($url = $mod->get_url()) { // Display greyed-out text of link - echo ''. - get_string('notavailableyet','condition').': '. - ''.get_string('modulename', $mod->modname).' '. - $instancename.$altname.''; - } - } - if ($usetracking && $mod->modname == 'forum') { - if ($unread = forum_tp_count_forum_unread_posts($mod, $course)) { - echo ' '; - if ($unread == 1) { - echo $strunreadpostsone; - } else { - print_string('unreadpostsnumber', 'forum', $unread); - } - echo ''; + echo '
extra . + ' >' . '' .
+                            $modulename .
+                            ' '. $instancename . $altname . + '
'; + + // Do not display content after link when it is greyed out like this. + } else { + // No link, so display only content (also greyed) + $contentpart = '
extra . '>' . + $accesstext . $content . '
'; } } + // Module can put text after the link (e.g. forum unread) + echo $mod->get_after_link(); + if ($isediting) { if ($groupbuttons and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) { if (! $mod->groupmodelink = $groupbuttonslink) { @@ -1494,6 +1585,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, } echo '  '; echo make_editing_buttons($mod, $absolute, true, $mod->indent, $section->section); + echo $mod->get_after_edit_icons(); } // Completion @@ -1566,6 +1658,9 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, } } + // Display the content (if any) at this part of the html + echo $contentpart; + // Show availability information (for someone who isn't allowed to // see the activity itself, or for staff) if (!$mod->uservisible) { diff --git a/course/report/outline/index.php b/course/report/outline/index.php index 5c151df0b24..ad4ef64fb56 100644 --- a/course/report/outline/index.php +++ b/course/report/outline/index.php @@ -74,7 +74,7 @@ foreach ($modinfo->sections as $sectionnum=>$section) { foreach ($section as $cmid) { $cm = $modinfo->cms[$cmid]; - if ($cm->modname == 'label') { + if (!$cm->has_view()) { continue; } if (!$cm->uservisible) { diff --git a/course/report/participation/index.php b/course/report/participation/index.php index bb460b8bf65..2ad66bb948a 100644 --- a/course/report/participation/index.php +++ b/course/report/participation/index.php @@ -63,7 +63,7 @@ $modinfo = get_fast_modinfo($course); - $modules = $DB->get_records_select('modules', "visible = 1 AND name <> 'label'", null, 'name ASC'); + $modules = $DB->get_records_select('modules', "visible = 1", null, 'name ASC'); $instanceoptions = array(); foreach ($modules as $module) { @@ -72,8 +72,16 @@ } $instances = array(); foreach ($modinfo->instances[$module->name] as $cm) { + // Skip modules such as label which do not actually have links; + // this means there's nothing to participate in + if (!$cm->has_view()) { + continue; + } $instances[$cm->id] = format_string($cm->name); } + if (count($instances) == 0) { + continue; + } $instanceoptions[] = array(get_string('modulenameplural', $module->name)=>$instances); } diff --git a/course/resources.php b/course/resources.php index b98ced99f85..abf50b28d12 100644 --- a/course/resources.php +++ b/course/resources.php @@ -37,9 +37,6 @@ $allmodules = $DB->get_records('modules', array('visible'=>1)); $modules = array(); foreach ($allmodules as $key=>$module) { $modname = $module->name; - if ($modname === 'label') { - continue; - } $libfile = "$CFG->dirroot/mod/$modname/lib.php"; if (!file_exists($libfile)) { continue; @@ -80,6 +77,10 @@ foreach ($modinfo->cms as $cm) { if (!array_key_exists($cm->modname, $modules)) { continue; } + if (!$cm->has_view()) { + // Exclude label and similar + continue; + } $cms[$cm->id] = $cm; $resources[$cm->modname][] = $cm->instance; } diff --git a/lib/modinfolib.php b/lib/modinfolib.php new file mode 100644 index 00000000000..1265d0ec3a9 --- /dev/null +++ b/lib/modinfolib.php @@ -0,0 +1,1092 @@ +. + +/** + * modinfolib.php - Functions/classes relating to cached information about module instances on + * a course. + * @package core + * @subpackage lib + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author sam marshall + */ + + +// Maximum number of modinfo items to keep in memory cache. Do not increase this to a large +// number because: +// a) modinfo can be big (megabyte range) for some courses +// b) performance of cache will deteriorate if there are very many items in it +if (!defined('MAX_MODINFO_CACHE_SIZE')) { + define('MAX_MODINFO_CACHE_SIZE', 10); +} + + +/** + * Information about a course that is cached in the course table 'modinfo' field (and then in + * memory) in order to reduce the need for other database queries. + * + * This includes information about the course-modules and the sections on the course. It can also + * include dynamic data that has been updated for the current user. + */ +class course_modinfo { + // For convenience we store the course object here as it is needed in other parts of code + private $course; + + // Existing data fields + /////////////////////// + + // These are public for backward compatibility. Note: it is not possible to retain BC + // using PHP magic get methods because behaviour is different with regard to empty(). + + /** + * Course ID + * @var int + * @deprecated For new code, use get_course_id instead. + */ + public $courseid; + + /** + * User ID + * @var int + * @deprecated For new code, use get_user_id instead. + */ + public $userid; + + /** + * Array from int (section num, e.g. 0) => array of int (course-module id); this list only + * includes sections that actually contain at least one course-module + * @var array + * @deprecated For new code, use get_sections instead + */ + public $sections; + + /** + * Array from int (cm id) => cm_info object + * @var array + * @deprecated For new code, use get_cms or get_cm instead. + */ + public $cms; + + /** + * Array from string (modname) => int (instance id) => cm_info object + * @var array + * @deprecated For new code, use get_instances or get_instances_of instead. + */ + public $instances; + + /** + * Groups that the current user belongs to. This value is usually not available (set to null) + * unless the course has activities set to groupmembersonly. When set, it is an array of + * grouping id => array of group id => group id. Includes grouping id 0 for 'all groups'. + * @var array + * @deprecated Don't use this! For new code, use get_groups. + */ + public $groups; + + // Get methods for data + /////////////////////// + + /** + * @return object Moodle course object that was used to construct this data + */ + public function get_course() { + return $this->course; + } + + /** + * @return int Course ID + */ + public function get_course_id() { + return $this->courseid; + } + + /** + * @return int User ID + */ + public function get_user_id() { + return $this->userid; + } + + /** + * @return array Array from section number (e.g. 0) to array of course-module IDs in that + * section; this only includes sections that contain at least one course-module + */ + public function get_sections() { + return $this->sections; + } + + /** + * @return array Array from course-module instance to cm_info object within this course, in + * order of appearance + */ + public function get_cms() { + return $this->cms; + } + + /** + * Obtains a single course-module object (for a course-module that is on this course). + * @param int $cmid Course-module ID + * @return cm_info Information about that course-module + * @throws moodle_exception If the course-module does not exist + */ + public function get_cm($cmid) { + if (empty($this->cms[$cmid])) { + throw new moodle_exception('invalidcoursemodule', 'error'); + } + return $this->cms[$cmid]; + } + + /** + * Obtains all module instances on this course. + * @return array Array from module name => array from instance id => cm_info + */ + public function get_instances() { + return $this->instances; + } + + /** + * Obtains all instances of a particular module on this course. + * @param $modname Name of module (not full frankenstyle) e.g. 'label' + * @return array Array from instance id => cm_info for modules on this course; empty if none + */ + public function get_instances_of($modname) { + if (empty($this->instances[$modname])) { + return array(); + } + return $this->instances[$modname]; + } + + /** + * Returns groups that the current user belongs to on the course. Note: If not already + * available, this may make a database query. + * @param int $groupingid Grouping ID or 0 (default) for all groups + * @return array Array of int (group id) => int (same group id again); empty array if none + */ + public function get_groups($groupingid=0) { + if (is_null($this->groups)) { + // NOTE: Performance could be improved here. The system caches user groups + // in $USER->groupmember[$courseid] => array of groupid=>groupid. Unfortunately this + // structure does not include grouping information. It probably could be changed to + // do so, without a significant performance hit on login, thus saving this one query + // each request. + $this->groups = groups_get_user_groups($this->courseid, $this->userid); + } + if (!isset($this->groups[$groupingid])) { + return array(); + } + return $this->groups[$groupingid]; + } + + /** + * Constructs based on course. + * Note: This constructor should not usually be called directly. + * Use get_fast_modinfo($course) instead as this maintains a cache. + * @param object $course Moodle course object, which may include modinfo + * @param int $userid User ID + */ + public function __construct($course, $userid) { + global $CFG, $DB; + + // Set initial values + $this->courseid = $course->id; + $this->userid = $userid; + $this->sections = array(); + $this->cms = array(); + $this->instances = array(); + $this->groups = null; + $this->course = $course; + + // Check modinfo field is set. If not, build and load it. + if (empty($course->modinfo)) { + rebuild_course_cache($course->id); + $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); + } + + // Load modinfo field into memory as PHP object and check it's valid + $info = unserialize($course->modinfo); + if (!is_array($info)) { + // hmm, something is wrong - lets try to fix it + rebuild_course_cache($course->id); + $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); + $info = unserialize($course->modinfo); + if (!is_array($info)) { + // If it still fails, abort + debugging('Problem with "modinfo" data for this course'); + return; + } + } + + // If we haven't already preloaded contexts for the course, do it now + preload_course_contexts($course->id); + + // Loop through each piece of module data, constructing it + $modexists = array(); + foreach ($info as $mod) { + if (empty($mod->name)) { + // something is wrong here + continue; + } + + // Skip modules which don't exist + if (empty($modexists[$mod->mod])) { + if (!file_exists("$CFG->dirroot/mod/$mod->mod/lib.php")) { + continue; + } + $modexists[$mod->mod] = true; + } + + // Construct info for this module + $cm = new cm_info($this, $course, $mod, $info); + + // Store module in instances and cms array + if (!isset($this->instances[$cm->modname])) { + $this->instances[$cm->modname] = array(); + } + $this->instances[$cm->modname][$cm->instance] = $cm; + $this->cms[$cm->id] = $cm; + + // Reconstruct sections. This works because modules are stored in order + if (!isset($this->sections[$cm->sectionnum])) { + $this->sections[$cm->sectionnum] = array(); + } + $this->sections[$cm->sectionnum][] = $cm->id; + } + + // We need at least 'dynamic' data from each course-module (this is basically the remaining + // data which was always present in previous version of get_fast_modinfo, so it's required + // for BC). Creating it in a second pass is necessary because obtain_dynamic_data sometimes + // needs to be able to refer to a 'complete' (with basic data) modinfo. + foreach ($this->cms as $cm) { + $cm->obtain_dynamic_data(); + } + } +} + + +/** + * Data about a single module on a course. This contains most of the fields in the course_modules + * table, plus additional data when required. + * + * This object has many public fields; code should treat all these fields as read-only and set + * data only using the supplied set functions. Setting the fields directly is not supported + * and may cause problems later. + */ +class cm_info { + /** + * State: Only basic data from modinfo cache is available. + */ + const STATE_BASIC = 0; + + /** + * State: Dynamic data is available too. + */ + const STATE_DYNAMIC = 1; + + /** + * State: View data (for course page) is available. + */ + const STATE_VIEW = 2; + + /** + * Parent object + * @var course_modinfo + */ + private $modinfo; + + /** + * Level of information stored inside this object (STATE_xx constant) + * @var int + */ + private $state; + + // Existing data fields + /////////////////////// + + /** + * Course-module ID - from course_modules table + * @var int + */ + public $id; + + /** + * Module instance (ID within module table) - from course_modules table + * @var int + */ + public $instance; + + /** + * Course ID - from course_modules table + * @var int + */ + public $course; + + /** + * 'ID number' from course-modules table (arbitrary text set by user) - from + * course_modules table + * @var string + */ + public $idnumber; + + /** + * Visible setting (0 or 1; if this is 0, students cannot see/access the activity) - from + * course_modules table + * @var int + */ + public $visible; + + /** + * Group mode (one of the constants NONE, SEPARATEGROUPS, or VISIBLEGROUPS) - from + * course_modules table + * @var int + */ + public $groupmode; + + /** + * Grouping ID (0 = all groupings) + * @var int + */ + public $groupingid; + + /** + * Group members only (if set to 1, only members of a suitable group see this link on the + * course page; 0 = everyone sees it even if they don't belong to a suitable group) - from + * course_modules table + * @var int + */ + public $groupmembersonly; + + /** + * Indent level on course page (0 = no indent) - from course_modules table + * @var int + */ + public $indent; + + /** + * Activity completion setting for this activity, COMPLETION_TRACKING_xx constant - from + * course_modules table + * @var int + */ + public $completion; + + /** + * Available date for this activity (0 if not set, or set to seconds since epoch; before this + * date, activity does not display to students) - from course_modules table + * @var int + */ + public $availablefrom; + + /** + * Available until date for this activity (0 if not set, or set to seconds since epoch; from + * this date, activity does not display to students) - from course_modules table + * @var int + */ + public $availableuntil; + + /** + * When activity is unavailable, this field controls whether it is shown to students (0 = + * hide completely, 1 = show greyed out with information about when it will be available) - + * from course_modules table + * @var int + */ + public $showavailability; + + /** + * Extra HTML that is put in an unhelpful part of the HTML when displaying this module in + * course page - from cached data in modinfo field + * @deprecated This is crazy, don't use it. Replaced by ->extraclasses and ->onclick + * @var string + */ + public $extra; + + /** + * Name of icon to use - from cached data in modinfo field + * @var string + */ + public $icon; + + /** + * Component that contains icon - from cached data in modinfo field + * @var string + */ + public $iconcomponent; + + /** + * Name of module e.g. 'forum' (this is the same name as the module's main database + * table) - from cached data in modinfo field + * @var string + */ + public $modname; + + /** + * Name of module instance for display on page e.g. 'General discussion forum' - from cached + * data in modinfo field + * @var string + */ + public $name; + + /** + * Section number that this course-module is in (section 0 = above the calendar, section 1 + * = week/topic 1, etc) - from cached data in modinfo field + * @var string + */ + public $sectionnum; + + /** + * Availability conditions for this course-module based on the completion of other + * course-modules (array from other course-module id to required completion state for that + * module) - from cached data in modinfo field + * @var array + */ + public $conditionscompletion; + + /** + * Availability conditions for this course-module based on course grades (array from + * grade item id to object with ->min, ->max fields) - from cached data in modinfo field + * @var array + */ + public $conditionsgrade; + + /** + * Plural name of module type, e.g. 'Forums' - from lang file + * @deprecated Do not use this value (you can obtain it by calling get_string instead); it + * will be removed in a future version (see later TODO in this file) + * @var string + */ + public $modplural; + + /** + * True if this course-module is available to students i.e. if all availability conditions + * are met - obtained dynamically + * @var bool + */ + public $available; + + /** + * If course-module is not available to students, this string gives information about + * availability which can be displayed to students and/or staff (e.g. 'Available from 3 + * January 2010') for display on main page - obtained dynamically + * @var string + */ + public $availableinfo; + + /** + * True if this course-module is available to the CURRENT user (for example, if current user + * has viewhiddenactivities capability, they can access the course-module even if it is not + * visible or not available, so this would be true in that case) + * @var bool + */ + public $uservisible; + + // New data available only via functions + //////////////////////////////////////// + + /** + * @var moodle_url + */ + private $url; + + /** + * @var string + */ + private $content; + + /** + * @var string + */ + private $extraclasses; + + /** + * @var string + */ + private $onclick; + + /** + * @var mixed + */ + private $customdata; + + /** + * @var string + */ + private $afterlink; + + /** + * @var string + */ + private $afterediticons; + + /** + * @return bool True if this module has a 'view' page that should be linked to in navigation + * etc (note: modules may still have a view.php file, but return false if this is not + * intended to be linked to from 'normal' parts of the interface; this is what label does). + */ + public function has_view() { + return !is_null($this->url); + } + + /** + * @return moodle_url URL to link to for this module, or null if it doesn't have a view page + */ + public function get_url() { + return $this->url; + } + + /** + * Obtains content to display on main (view) page. + * Note: Will collect view data, if not already obtained. + * @return string Content to display on main page below link, or empty string if none + */ + public function get_content() { + $this->obtain_view_data(); + return $this->content; + } + + /** + * Note: Will collect view data, if not already obtained. + * @return string Extra CSS classes to add to html output for this activity on main page + */ + public function get_extra_classes() { + $this->obtain_view_data(); + return $this->extraclasses; + } + + /** + * @return string Content of HTML on-click attribute. This string will be used literally + * as a string so should be pre-escaped. + */ + public function get_on_click() { + // Does not need view data; may be used by navigation + return $this->onclick; + } + /** + * @return mixed Optional custom data stored in modinfo cache for this activity, or null if none + */ + public function get_custom_data() { + return $this->customdata; + } + + /** + * Note: Will collect view data, if not already obtained. + * @return string Extra HTML code to display after link + */ + public function get_after_link() { + $this->obtain_view_data(); + return $this->afterlink; + } + + /** + * Note: Will collect view data, if not already obtained. + * @return string Extra HTML code to display after editing icons (e.g. more icons) + */ + public function get_after_edit_icons() { + $this->obtain_view_data(); + return $this->afterediticons; + } + + /** + * @param moodle_core_renderer $output Output render to use, or null for default (global) + * @return moodle_url Icon URL for a suitable icon to put beside this cm + */ + public function get_icon_url($output = null) { + global $OUTPUT; + if (!$output) { + $output = $OUTPUT; + } + if (!empty($this->icon)) { + if (substr($this->icon, 0, 4) === 'mod/') { + list($modname, $iconname) = explode('/', substr($this->icon, 4), 2); + $icon = $output->pix_url($iconname, $modname); + } else { + if (!empty($this->iconcomponent)) { + // Icon has specified component + $icon = $output->pix_url($this->icon, $this->iconcomponent); + } else { + // Icon does not have specified component, use default + $icon = $output->pix_url($this->icon); + } + } + } else { + $icon = $output->pix_url('icon', $this->modname); + } + return $icon; + } + + /** + * @return course_modinfo Modinfo object that this came from + */ + public function get_modinfo() { + return $this->modinfo; + } + + /** + * @return object Moodle course object that was used to construct this data + */ + public function get_course() { + return $this->modinfo->get_course(); + } + + // Set functions + //////////////// + + /** + * Sets content to display on course view page below link (if present). + * @param string $content New content as HTML string (empty string if none) + * @return void + */ + public function set_content($content) { + $this->content = $content; + } + + /** + * Sets extra classes to include in CSS. + * @param string $extraclasses Extra classes (empty string if none) + * @return void + */ + public function set_extra_classes($extraclasses) { + $this->extraclasses = $extraclasses; + } + + /** + * Sets value of on-click attribute for JavaScript. + * Note: May not be called from _cm_info_view (only _cm_info_dynamic). + * @param string $onclick New onclick attribute which should be HTML-escaped + * (empty string if none) + * @return void + */ + public function set_on_click($onclick) { + $this->check_not_view_only(); + $this->onclick = $onclick; + } + + /** + * Sets HTML that displays after link on course view page. + * @param string $afterlink HTML string (empty string if none) + * @return void + */ + public function set_after_link($afterlink) { + $this->afterlink = $afterlink; + } + + /** + * Sets HTML that displays after edit icons on course view page. + * @param string $afterediticons HTML string (empty string if none) + * @return void + */ + public function set_after_edit_icons($afterediticons) { + $this->afterediticons = $afterediticons; + } + + /** + * Changes the name (text of link) for this module instance. + * Note: May not be called from _cm_info_view (only _cm_info_dynamic). + * @param string $name Name of activity / link text + * @return void + */ + public function set_name($name) { + $this->update_user_visible(); + $this->name = $name; + } + + /** + * Turns off the view link for this module instance. + * Note: May not be called from _cm_info_view (only _cm_info_dynamic). + * @return void + */ + public function set_no_view_link() { + $this->check_not_view_only(); + $url = null; + } + + /** + * Sets the 'uservisible' flag. This can be used (by setting false) to prevent access and + * display of this module link for the current user. + * Note: May not be called from _cm_info_view (only _cm_info_dynamic). + * @param bool $uservisible + * @return void + */ + public function set_user_visible($uservisible) { + $this->check_not_view_only(); + $this->uservisible = $uservisible; + } + + /** + * Sets the 'available' flag and related details. This flag is normally used to make + * course modules unavailable until a certain date or condition is met. (When a course + * module is unavailable, it is still visible to users who have viewhiddenactivities + * permission.) + * + * When this is function is called, user-visible status is recalculated automatically. + * + * Note: May not be called from _cm_info_view (only _cm_info_dynamic). + * @param bool $available False if this item is not 'available' + * @param int $showavailability 0 = do not show this item at all if it's not available, + * 1 = show this item greyed out with the following message + * @param string $availableinfo Information about why this is not available which displays + * to those who have viewhiddenactivities, and to everyone if showavailability is set; + * note that this function replaces the existing data (if any) + * @return void + */ + public function set_available($available, $showavailability=0, $availableinfo='') { + $this->check_not_view_only(); + $this->available = $available; + $this->showavailability = $showavailability; + $this->availableinfo = $availableinfo; + $this->update_user_visible(); + } + + /** + * Some set functions can only be called from _cm_info_dynamic and not _cm_info_view. + * This is because they may affect parts of this object which are used on pages other + * than the view page (e.g. in the navigation block, or when checking access on + * module pages). + * @return void + */ + private function check_not_view_only() { + if ($this->state >= self::STATE_DYNAMIC) { + throw new coding_exception('Cannot set this data from _cm_info_view because it may ' . + 'affect other pages as well as view'); + } + } + + /** + * Constructor should not be called directly; use get_fast_modinfo. + * @param course_modinfo $modinfo Parent object + * @param object $course Course row + * @param object $mod Module object from the modinfo field of course table + * @param object $info Entire object from modinfo field of course table + */ + public function __construct(course_modinfo $modinfo, $course, $mod, $info) { + global $CFG; + $this->modinfo = $modinfo; + + $this->id = $mod->cm; + $this->instance = $mod->id; + $this->course = $course->id; + $this->modname = $mod->mod; + $this->idnumber = isset($mod->idnumber) ? $mod->idnumber : ''; + $this->name = $mod->name; + $this->visible = $mod->visible; + $this->sectionnum = $mod->section; + $this->groupmode = isset($mod->groupmode) ? $mod->groupmode : 0; + $this->groupingid = isset($mod->groupingid) ? $mod->groupingid : 0; + $this->groupmembersonly = isset($mod->groupmembersonly) ? $mod->groupmembersonly : 0; + $this->indent = isset($mod->indent) ? $mod->indent : 0; + $this->completion = isset($mod->completion) ? $mod->completion : 0; + $this->extra = isset($mod->extra) ? $mod->extra : ''; + $this->extraclasses = isset($mod->extraclasses) ? $mod->extraclasses : ''; + $this->onclick = isset($mod->onclick) ? $mod->onclick : ''; + $this->content = isset($mod->content) ? $mod->content : ''; + $this->icon = isset($mod->icon) ? $mod->icon : ''; + $this->iconcomponent = isset($mod->iconcomponent) ? $mod->iconcomponent : ''; + $this->customdata = isset($mod->customdata) ? $mod->customdata : ''; + $this->state = self::STATE_BASIC; + + // This special case handles old label data. Labels used to use the 'name' field for + // content + if ($this->modname === 'label' && $this->content === '') { + $this->content = $this->extra; + $this->extra = ''; + } + + if (!empty($CFG->enableavailability)) { + // We must have completion information from modinfo. If it's not + // there, cache needs rebuilding + if (!isset($mod->showavailability)) { + throw new modinfo_rebuild_cache_exception( + 'enableavailability option was changed; rebuilding '. + 'cache for course ' . $course->id); + } + $this->showavailability = $mod->showavailability; + $this->availablefrom = isset($mod->availablefrom) ? $mod->availablefrom : 0; + $this->availableuntil = isset($mod->availableuntil) ? $mod->availableuntil : 0; + $this->conditionscompletion = isset($mod->conditionscompletion) + ? $mod->conditionscompletion : array(); + $this->conditionsgrade = isset($mod->conditionsgrade) + ? $mod->conditionsgrade : array(); + } + + // Get module plural name. + // TODO This was a very old performance hack and should now be removed as the information + // certainly doesn't belong in modinfo. On a 'normal' page this is only used in the + // activity_modules block, so if it needs caching, it should be cached there. + static $modplurals; + if (!isset($modplurals[$this->modname])) { + $modplurals[$this->modname] = get_string('modulenameplural', $this->modname); + } + $this->modplural = $modplurals[$this->modname]; + + static $modviews; + if (!isset($modviews[$this->modname])) { + $modviews[$this->modname] = !plugin_supports('mod', $this->modname, + FEATURE_NO_VIEW_LINK); + } + $this->url = $modviews[$this->modname] + ? new moodle_url('/mod/' . $this->modname . '/view.php', array('id'=>$this->id)) + : null; + } + + /** + * If dynamic data for this course-module is not yet available, gets it. + * + * This function is automatically called when constructing course_modinfo, so users don't + * need to call it. + * + * Dynamic data is data which does not come directly from the cache but is calculated at + * runtime based on the current user. Primarily this concerns whether the user can access + * the module or not. + * + * As part of this function, the module's _cm_info_dynamic function from its lib.php will + * be called (if it exists). + * @return void + */ + public function obtain_dynamic_data() { + global $CFG; + if ($this->state >= self::STATE_DYNAMIC) { + return; + } + $userid = $this->modinfo->get_user_id(); + + if (!empty($CFG->enableavailability)) { + // Get availability information + $ci = new condition_info($this); + // Note that the modinfo currently available only includes minimal details (basic data) + // so passing it to this function is a bit dangerous as it would cause infinite + // recursion if it tried to get dynamic data, however we know that this function only + // uses basic data. + $this->available = $ci->is_available($this->availableinfo, true, + $userid, $this->modinfo); + } else { + $this->available = true; + } + + // Update visible state for current user + $this->update_user_visible(); + + // Let module make dynamic changes at this point + $this->call_mod_function('cm_info_dynamic'); + $this->state = self::STATE_DYNAMIC; + } + + /** + * Works out whether activity is visible *for current user* - if this is false, they + * aren't allowed to access it. + * @return void + */ + private function update_user_visible() { + global $CFG; + $modcontext = get_context_instance(CONTEXT_MODULE, $this->id); + $userid = $this->modinfo->get_user_id(); + $this->uservisible = true; + if ((!$this->visible or !$this->available) and + !has_capability('moodle/course:viewhiddenactivities', $modcontext, $userid)) { + // If the activity is hidden or unavailable, and you don't have viewhiddenactivities, + // set it so that user can't see or access it + $this->uservisible = false; + } else if (!empty($CFG->enablegroupmembersonly) and !empty($this->groupmembersonly) + and !has_capability('moodle/site:accessallgroups', $modcontext, $userid)) { + // If the activity has 'group members only' and you don't have accessallgroups... + $groups = $this->modinfo->get_groups(); + if (empty($this->groups[$this->groupingid])) { + // ...and you don't belong to a group, then set it so you can't see/access it + $this->uservisible = false; + } + } + } + + /** + * Calls a module function (if exists), passing in one parameter: this object. + * @param string $type Name of function e.g. if this is 'grooblezorb' and the modname is + * 'forum' then it will try to call 'mod_forum_grooblezorb' or 'forum_grooblezorb' + * @return void + */ + private function call_mod_function($type) { + global $CFG; + $libfile = $CFG->dirroot . '/mod/' . $this->modname . '/lib.php'; + if (file_exists($libfile)) { + include_once($libfile); + $function = 'mod_' . $this->modname . '_' . $type; + if (function_exists($function)) { + $function($this); + } else { + $function = $this->modname . '_' . $type; + if (function_exists($function)) { + $function($this); + } + } + } + } + + /** + * If view data for this course-module is not yet available, obtains it. + * + * This function is automatically called if any of the functions (marked) which require + * view data are called. + * + * View data is data which is needed only for displaying the course main page (& any similar + * functionality on other pages) but is not needed in general. Obtaining view data may have + * a performance cost. + * + * As part of this function, the module's _cm_info_view function from its lib.php will + * be called (if it exists). + * @return void + */ + private function obtain_view_data() { + if ($this->state >= self::STATE_VIEW) { + return; + } + + // Let module make changes at this point + $this->call_mod_function('cm_info_view'); + $this->state = self::STATE_VIEW; + } +} + + +/** + * Special exception that may only be thrown within the constructor for course_modinfo to + * indicate that the cache needs to be rebuilt. Not for use anywhere else. + */ +class modinfo_rebuild_cache_exception extends coding_exception { + function __construct($why) { + // If it ever escapes, that's a code bug + parent::__construct('This exception should be caught by code', $why); + } +} + + +/** + * Returns reference to full info about modules in course (including visibility). + * Cached and as fast as possible (0 or 1 db query). + * + * @global object + * @global object + * @global moodle_database + * @uses MAX_MODINFO_CACHE_SIZE + * @param mixed $course object or 'reset' string to reset caches, modinfo may be updated in db + * @param int $userid Defaults to current user id + * @return course_modinfo Module information for course, or null if resetting + */ +function get_fast_modinfo(&$course, $userid=0) { + global $CFG, $USER, $DB; + require_once($CFG->dirroot.'/course/lib.php'); + + if (!empty($CFG->enableavailability)) { + require_once($CFG->libdir.'/conditionlib.php'); + } + + static $cache = array(); + + if ($course === 'reset') { + $cache = array(); + return null; + } + + if (empty($userid)) { + $userid = $USER->id; + } + + if (array_key_exists($course->id, $cache) and $cache[$course->id]->userid == $userid) { + return $cache[$course->id]; + } + + if (!property_exists($course, 'modinfo')) { + debugging('Coding problem - missing course modinfo property in get_fast_modinfo() call'); + } + + unset($cache[$course->id]); // prevent potential reference problems when switching users + + try { + $cache[$course->id] = new course_modinfo($course, $userid); + } catch (modinfo_rebuild_cache_exception $e) { + debugging($e->debuginfo); + rebuild_course_cache($course->id, true); + $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST); + // This second time we don't catch the exception - if you request cache rebuild twice + // in a row, that's a bug => coding_exception + $cache[$course->id] = new course_modinfo($course, $userid); + } + + // Ensure cache does not use too much RAM + if (count($cache) > MAX_MODINFO_CACHE_SIZE) { + reset($cache); + $key = key($cache); + unset($cache[$key]); + } + + return $cache[$course->id]; +} + + +/** + * Class that is the return value for the _get_coursemodule_info module API function. + * + * Note: For backward compatibility, you can also return a stdclass object from that function. + * The difference is that the stdclass object may contain an 'extra' field (deprecated because + * it was crazy, except for label which uses it differently). The stdclass object may not contain + * the new fields defined here (content, extraclasses, customdata). + */ +class cached_cm_info { + /** + * Name (text of link) for this activity; Leave unset to accept default name + * @var string + */ + public $name; + + /** + * Name of icon for this activity. Normally, this should be used together with $iconcomponent + * to define the icon, as per pix_url function. + * For backward compatibility, if this value is of the form 'mod/forum/icon' then an icon + * within that module will be used. + * @see cm_info::get_icon_url() + * @see renderer_base::pix_url() + * @var string + */ + public $icon; + + /** + * Component for icon for this activity, as per pix_url; leave blank to use default 'moodle' + * component + * @see renderer_base::pix_url() + * @var string + */ + public $iconcomponent; + + /** + * HTML content to be displayed on the main page below the link (if any) for this course-module + * @var string + */ + public $content; + + /** + * Custom data to be stored in modinfo for this activity; useful if there are cases when + * internal information for this activity type needs to be accessible from elsewhere on the + * course without making database queries. May be of any type but should be short. + * @var mixed + */ + public $customdata; + + /** + * Extra CSS class or classes to be added when this activity is displayed on the main page; + * space-separated string + * @var string + */ + public $extraclasses; + + /** + * Content of onclick JavaScript; escaped HTML to be inserted as attribute value + * @var string + */ + public $onclick; +} \ No newline at end of file diff --git a/lib/moodlelib.php b/lib/moodlelib.php index e7fe450c36c..c59449a56c1 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -352,6 +352,8 @@ define('FEATURE_COMPLETION_TRACKS_VIEWS', 'completion_tracks_views'); /** True if module has custom completion rules */ define('FEATURE_COMPLETION_HAS_RULES', 'completion_has_rules'); +/** True if module has no 'view' page (like label) */ +define('FEATURE_NO_VIEW_LINK', 'viewlink'); /** True if module supports outcomes */ define('FEATURE_IDNUMBER', 'idnumber'); /** True if module supports groups */ @@ -2312,6 +2314,14 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $ if ($cm->course != $course->id) { throw new coding_exception('course and cm parameters in require_login() call do not match!!'); } + // make sure we have a $cm from get_fast_modinfo as this contains activity access details + if (!($cm instanceof cm_info)) { + // note: nearly all pages call get_fast_modinfo anyway and it does not make any + // db queries so this is not really a performance concern, however it is obviously + // better if you use get_fast_modinfo to get the cm before calling this. + $modinfo = get_fast_modinfo($course); + $cm = $modinfo->get_cm($cm->id); + } $PAGE->set_cm($cm, $course); // set's up global $COURSE $PAGE->set_pagelayout('incourse'); } else { @@ -2583,47 +2593,15 @@ function require_login($courseorid = NULL, $autologinguest = true, $cm = NULL, $ } } - // test visibility - if ($cm && !$cm->visible && !has_capability('moodle/course:viewhiddenactivities', $cmcontext)) { + // Check visibility of activity to current user; includes visible flag, groupmembersonly, + // conditional availability, etc + if ($cm && !$cm->uservisible) { if ($preventredirect) { throw new require_login_exception('Activity is hidden'); } redirect($CFG->wwwroot, get_string('activityiscurrentlyhidden')); } - // groupmembersonly access control - if (!empty($CFG->enablegroupmembersonly) and $cm and $cm->groupmembersonly and !has_capability('moodle/site:accessallgroups', get_context_instance(CONTEXT_MODULE, $cm->id))) { - if (isguestuser() or !groups_has_membership($cm)) { - if ($preventredirect) { - throw new require_login_exception('Not member of a group'); - } - print_error('groupmembersonlyerror', 'group', $CFG->wwwroot.'/course/view.php?id='.$cm->course); - } - } - - // Conditional activity access control - if (!empty($CFG->enableavailability) and $cm) { - // TODO: this is going to work with login-as-user, sorry! - // We cache conditional access in session - if (!isset($SESSION->conditionaccessok)) { - $SESSION->conditionaccessok = array(); - } - // If you have been allowed into the module once then you are allowed - // in for rest of session, no need to do conditional checks - if (!array_key_exists($cm->id, $SESSION->conditionaccessok)) { - // Get condition info (does a query for the availability table) - require_once($CFG->libdir.'/conditionlib.php'); - $ci = new condition_info($cm, CONDITION_MISSING_EXTRATABLE); - // Check condition for user (this will do a query if the availability - // information depends on grade or completion information) - if ($ci->is_available($junk) || has_capability('moodle/course:viewhiddenactivities', $cmcontext)) { - $SESSION->conditionaccessok[$cm->id] = true; - } else { - print_error('activityiscurrentlyhidden'); - } - } - } - // Finally access granted, update lastaccess times user_accesstime_log($course->id); } @@ -2674,16 +2652,29 @@ function require_logout() { */ function require_course_login($courseorid, $autologinguest = true, $cm = NULL, $setwantsurltome = true, $preventredirect = false) { global $CFG, $PAGE, $SITE; + $issite = (is_object($courseorid) and $courseorid->id == SITEID) + or (!is_object($courseorid) and $courseorid == SITEID); + if ($issite && !empty($cm) && !($cm instanceof cm_info)) { + // note: nearly all pages call get_fast_modinfo anyway and it does not make any + // db queries so this is not really a performance concern, however it is obviously + // better if you use get_fast_modinfo to get the cm before calling this. + if (is_object($courseorid)) { + $course = $courseorid; + } else { + $course = clone($SITE); + } + $modinfo = get_fast_modinfo($course); + $cm = $modinfo->get_cm($cm->id); + } if (!empty($CFG->forcelogin)) { // login required for both SITE and courses require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); - } else if (!empty($cm) and !$cm->visible) { + } else if ($issite && !empty($cm) and !$cm->uservisible) { // always login for hidden activities require_login($courseorid, $autologinguest, $cm, $setwantsurltome, $preventredirect); - } else if ((is_object($courseorid) and $courseorid->id == SITEID) - or (!is_object($courseorid) and $courseorid == SITEID)) { + } else if ($issite) { //login for SITE not required if ($cm and empty($cm->visible)) { // hidden activities are not accessible without login @@ -3006,207 +2997,6 @@ function reset_login_count() { $SESSION->logincount = 0; } -/** - * Returns reference to full info about modules in course (including visibility). - * Cached and as fast as possible (0 or 1 db query). - * - * @global object - * @global object - * @global object - * @uses CONTEXT_MODULE - * @uses MAX_MODINFO_CACHE_SIZE - * @param mixed $course object or 'reset' string to reset caches, modinfo may be updated in db - * @param int $userid Defaults to current user id - * @return mixed courseinfo object or nothing if resetting - */ -function &get_fast_modinfo(&$course, $userid=0) { - global $CFG, $USER, $DB; - require_once($CFG->dirroot.'/course/lib.php'); - - if (!empty($CFG->enableavailability)) { - require_once($CFG->libdir.'/conditionlib.php'); - } - - static $cache = array(); - - if ($course === 'reset') { - $cache = array(); - $nothing = null; - return $nothing; // we must return some reference - } - - if (empty($userid)) { - $userid = $USER->id; - } - - if (array_key_exists($course->id, $cache) and $cache[$course->id]->userid == $userid) { - return $cache[$course->id]; - } - - if (!property_exists($course, 'modinfo')) { - debugging('Coding problem - missing course modinfo property in get_fast_modinfo() call'); - } - - if (empty($course->modinfo)) { - // no modinfo yet - load it - rebuild_course_cache($course->id); - $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); - } - - $modinfo = new stdClass(); - $modinfo->courseid = $course->id; - $modinfo->userid = $userid; - $modinfo->sections = array(); - $modinfo->cms = array(); - $modinfo->instances = array(); - $modinfo->groups = null; // loaded only when really needed - the only one db query - - $info = unserialize($course->modinfo); - if (!is_array($info)) { - // hmm, something is wrong - lets try to fix it - rebuild_course_cache($course->id); - $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); - $info = unserialize($course->modinfo); - if (!is_array($info)) { - return $modinfo; - } - } - - if ($info) { - // detect if upgrade required - $first = reset($info); - if (!isset($first->id)) { - rebuild_course_cache($course->id); - $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); - $info = unserialize($course->modinfo); - if (!is_array($info)) { - return $modinfo; - } - } - } - - $modlurals = array(); - - // If we haven't already preloaded contexts for the course, do it now - preload_course_contexts($course->id); - - foreach ($info as $mod) { - if (empty($mod->name)) { - // something is wrong here - continue; - } - // reconstruct minimalistic $cm - $cm = new stdClass(); - $cm->id = $mod->cm; - $cm->instance = $mod->id; - $cm->course = $course->id; - $cm->modname = $mod->mod; - $cm->idnumber = $mod->idnumber; - $cm->name = $mod->name; - $cm->visible = $mod->visible; - $cm->sectionnum = $mod->section; - $cm->groupmode = $mod->groupmode; - $cm->groupingid = $mod->groupingid; - $cm->groupmembersonly = $mod->groupmembersonly; - $cm->indent = $mod->indent; - $cm->completion = $mod->completion; - $cm->extra = isset($mod->extra) ? $mod->extra : ''; - $cm->icon = isset($mod->icon) ? $mod->icon : ''; - $cm->iconcomponent = isset($mod->iconcomponent) ? $mod->iconcomponent : ''; - $cm->uservisible = true; - if (!empty($CFG->enableavailability)) { - // We must have completion information from modinfo. If it's not - // there, cache needs rebuilding - if(!isset($mod->availablefrom)) { - debugging('enableavailability option was changed; rebuilding '. - 'cache for course '.$course->id); - rebuild_course_cache($course->id,true); - // Re-enter this routine to do it all properly - return get_fast_modinfo($course, $userid); - } - $cm->availablefrom = $mod->availablefrom; - $cm->availableuntil = $mod->availableuntil; - $cm->showavailability = $mod->showavailability; - $cm->conditionscompletion = $mod->conditionscompletion; - $cm->conditionsgrade = $mod->conditionsgrade; - } - - // preload long names plurals and also check module is installed properly - if (!isset($modlurals[$cm->modname])) { - if (!file_exists("$CFG->dirroot/mod/$cm->modname/lib.php")) { - continue; - } - $modlurals[$cm->modname] = get_string('modulenameplural', $cm->modname); - } - $cm->modplural = $modlurals[$cm->modname]; - $modcontext = get_context_instance(CONTEXT_MODULE,$cm->id); - - if (!empty($CFG->enableavailability)) { - // Unfortunately the next call really wants to call - // get_fast_modinfo, but that would be recursive, so we fake up a - // modinfo for it already - if (empty($minimalmodinfo)) { //TODO: this is suspicious (skodak) - $minimalmodinfo = new stdClass(); - $minimalmodinfo->cms = array(); - foreach($info as $mod) { - if (empty($mod->name)) { - // something is wrong here - continue; - } - $minimalcm = new stdClass(); - $minimalcm->id = $mod->cm; - $minimalcm->name = $mod->name; - $minimalmodinfo->cms[$minimalcm->id]=$minimalcm; - } - } - - // Get availability information - $ci = new condition_info($cm); - $cm->available = $ci->is_available($cm->availableinfo, true, $userid, $minimalmodinfo); - } else { - $cm->available = true; - } - if ((!$cm->visible or !$cm->available) and !has_capability('moodle/course:viewhiddenactivities', $modcontext, $userid)) { - $cm->uservisible = false; - - } else if (!empty($CFG->enablegroupmembersonly) and !empty($cm->groupmembersonly) - and !has_capability('moodle/site:accessallgroups', $modcontext, $userid)) { - if (is_null($modinfo->groups)) { - $modinfo->groups = groups_get_user_groups($course->id, $userid); - } - if (empty($modinfo->groups[$cm->groupingid])) { - $cm->uservisible = false; - } - } - - if (!isset($modinfo->instances[$cm->modname])) { - $modinfo->instances[$cm->modname] = array(); - } - $modinfo->instances[$cm->modname][$cm->instance] =& $cm; - $modinfo->cms[$cm->id] =& $cm; - - // reconstruct sections - if (!isset($modinfo->sections[$cm->sectionnum])) { - $modinfo->sections[$cm->sectionnum] = array(); - } - $modinfo->sections[$cm->sectionnum][] = $cm->id; - - unset($cm); - } - - unset($cache[$course->id]); // prevent potential reference problems when switching users - $cache[$course->id] = $modinfo; - - // Ensure cache does not use too much RAM - if (count($cache) > MAX_MODINFO_CACHE_SIZE) { - reset($cache); - $key = key($cache); - unset($cache[$key]); - } - - return $cache[$course->id]; -} - /** * Determines if the currently logged in user is in editing mode. * Note: originally this function had $userid parameter - it was not usable anyway diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 795246fa3d9..1a9f007fced 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1026,10 +1026,14 @@ class global_navigation extends navigation_node { if ($course->id !== SITEID) { // Find the section for the $CM associated with the page and collect // its section number. - foreach ($sections as $section) { - if ($section->id == $cm->section) { - $cm->sectionnumber = $section->section; - break; + if (isset($cm->sectionnum)) { + $cm->sectionnumber = $cm->sectionnum; + } else { + foreach ($sections as $section) { + if ($section->id == $cm->section) { + $cm->sectionnumber = $section->section; + break; + } } } @@ -1429,10 +1433,10 @@ class global_navigation extends navigation_node { * * @param navigation_node $sectionnode * @param int $sectionnumber - * @param stdClass $modinfo Object returned from {@see get_fast_modinfo()} + * @param course_modinfo $modinfo Object returned from {@see get_fast_modinfo()} * @return array Array of activity nodes */ - protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, $modinfo) { + protected function load_section_activities(navigation_node $sectionnode, $sectionnumber, course_modinfo $modinfo) { if (!array_key_exists($sectionnumber, $modinfo->sections)) { return true; } @@ -1449,11 +1453,12 @@ class global_navigation extends navigation_node { } else { $icon = new pix_icon('icon', get_string('modulename', $cm->modname), $cm->modname); } - $url = new moodle_url('/mod/'.$cm->modname.'/view.php', array('id'=>$cm->id)); + $url = $cm->get_url(); $activitynode = $sectionnode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon); $activitynode->title(get_string('modulename', $cm->modname)); $activitynode->hidden = (!$cm->visible); - if ($cm->modname == 'label') { + if (!$url) { + // Do not show activities that don't have links! $activitynode->display = false; } else if ($this->module_extends_navigation($cm->modname)) { $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; @@ -1482,11 +1487,12 @@ class global_navigation extends navigation_node { } else { $icon = new pix_icon('icon', get_string('modulename', $cm->modname), $cm->modname); } - $url = new moodle_url('/mod/'.$cm->modname.'/view.php', array('id'=>$cm->id)); + $url = $cm->get_url(); $activitynode = $coursenode->add(format_string($cm->name), $url, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon); $activitynode->title(get_string('modulename', $cm->modname)); $activitynode->hidden = (!$cm->visible); - if ($cm->modname == 'label') { + if (!$url) { + // Don't show activities that don't have links! $activitynode->display = false; } else if ($this->module_extends_navigation($cm->modname)) { $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; @@ -1509,7 +1515,7 @@ class global_navigation extends navigation_node { * @param navigation_node $activity * @return bool */ - protected function load_activity(stdClass $cm, stdClass $course, navigation_node $activity) { + protected function load_activity($cm, stdClass $course, navigation_node $activity) { global $CFG, $DB; $activity->make_active(); diff --git a/lib/setup.php b/lib/setup.php index e6777541895..de6f9fff90b 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -414,6 +414,7 @@ require_once($CFG->libdir .'/grouplib.php'); // Groups functions require_once($CFG->libdir .'/sessionlib.php'); // All session and cookie related stuff require_once($CFG->libdir .'/editorlib.php'); // All text editor related functions and classes require_once($CFG->libdir .'/messagelib.php'); // Messagelib functions +require_once($CFG->libdir .'/modinfolib.php'); // Cached information on course-module instances // make sure PHP is not severly misconfigured setup_validate_php_configuration(); diff --git a/lib/weblib.php b/lib/weblib.php index 7d2ddc328f8..69b13a4bad7 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -2181,7 +2181,8 @@ function navmenulist($course, $sections, $modinfo, $strsection, $strjumpto, $wid $menu[] = '