From: guillaume pellerin Date: Mon, 16 Sep 2013 14:41:32 +0000 (+0200) Subject: init X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=3d54e6763392dc215b361a73eecb9b7fe0cb40a2;p=teleforma-moodle-course.git init --- 3d54e6763392dc215b361a73eecb9b7fe0cb40a2 diff --git a/category.php b/category.php new file mode 100644 index 0000000..71ea312 --- /dev/null +++ b/category.php @@ -0,0 +1,479 @@ +. + +/** + * Displays the top level category or all courses + * In editing mode, allows the admin to edit a category, + * and rearrange courses + * + * @package core + * @subpackage course + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../config.php"); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->libdir.'/textlib.class.php'); + +$id = required_param('id', PARAM_INT); // Category id +$page = optional_param('page', 0, PARAM_INT); // which page to show +$categoryedit = optional_param('categoryedit', -1, PARAM_BOOL); +$hide = optional_param('hide', 0, PARAM_INT); +$show = optional_param('show', 0, PARAM_INT); +$moveup = optional_param('moveup', 0, PARAM_INT); +$movedown = optional_param('movedown', 0, PARAM_INT); +$moveto = optional_param('moveto', 0, PARAM_INT); +$resort = optional_param('resort', 0, PARAM_BOOL); +$sesskey = optional_param('sesskey', '', PARAM_RAW); + +// MDL-27824 - This is a temporary fix until we have the proper +// way to check/initialize $CFG value. +// @todo MDL-35138 remove this temporary solution +if (!empty($CFG->coursesperpage)) { + $defaultperpage = $CFG->coursesperpage; +} else { + $defaultperpage = 20; +} +$perpage = optional_param('perpage', $defaultperpage, PARAM_INT); // how many per page + +if (empty($id)) { + print_error("unknowcategory"); +} + +$PAGE->set_category_by_id($id); +$PAGE->set_url(new moodle_url('/course/category.php', array('id' => $id))); +// This is sure to be the category context +$context = $PAGE->context; +// And the object has been loaded for us no need for another DB call +$category = $PAGE->category; + +$canedit = can_edit_in_category($category->id); +if ($canedit) { + if ($categoryedit !== -1) { + $USER->editing = $categoryedit; + } + require_login(); + $editingon = $PAGE->user_is_editing(); +} else { + if ($CFG->forcelogin) { + require_login(); + } + $editingon = false; +} + +if (!$category->visible) { + require_capability('moodle/category:viewhiddencategories', $context); +} + +$canmanage = has_capability('moodle/category:manage', $context); +$sesskeyprovided = !empty($sesskey) && confirm_sesskey($sesskey); + +// Process any category actions. +if ($canmanage && $resort && $sesskeyprovided) { + // Resort the category if requested + if ($courses = get_courses($category->id, '', 'c.id,c.fullname,c.sortorder')) { + collatorlib::asort_objects_by_property($courses, 'fullname', collatorlib::SORT_NATURAL); + $i = 1; + foreach ($courses as $course) { + $DB->set_field('course', 'sortorder', $category->sortorder+$i, array('id'=>$course->id)); + $i++; + } + fix_course_sortorder(); // should not be needed + } +} + +// Process any course actions. +if ($editingon && $sesskeyprovided) { + + // Move a specified course to a new category + if (!empty($moveto) and $data = data_submitted()) { + // Some courses are being moved + // user must have category update in both cats to perform this + require_capability('moodle/category:manage', $context); + require_capability('moodle/category:manage', context_coursecat::instance($moveto)); + + if (!$destcategory = $DB->get_record('course_categories', array('id' => $data->moveto))) { + print_error('cannotfindcategory', '', '', $data->moveto); + } + + $courses = array(); + foreach ($data as $key => $value) { + if (preg_match('/^c\d+$/', $key)) { + $courseid = substr($key, 1); + array_push($courses, $courseid); + + // check this course's category + if ($movingcourse = $DB->get_record('course', array('id'=>$courseid))) { + if ($movingcourse->category != $id ) { + print_error('coursedoesnotbelongtocategory'); + } + } else { + print_error('cannotfindcourse'); + } + } + } + move_courses($courses, $data->moveto); + } + + // Hide or show a course + if (!empty($hide) or !empty($show)) { + if (!empty($hide)) { + $course = $DB->get_record('course', array('id' => $hide)); + $visible = 0; + } else { + $course = $DB->get_record('course', array('id' => $show)); + $visible = 1; + } + + if ($course) { + $coursecontext = context_course::instance($course->id); + require_capability('moodle/course:visibility', $coursecontext); + // Set the visibility of the course. we set the old flag when user manually changes visibility of course. + $DB->update_record('course', array('id' => $course->id, 'visible' => $visible, 'visibleold' => $visible, 'timemodified' => time())); + add_to_log($course->id, "course", ($visible ? 'show' : 'hide'), "edit.php?id=$course->id", $course->id); + } + } + + + // Move a course up or down + if (!empty($moveup) or !empty($movedown)) { + require_capability('moodle/category:manage', $context); + + // Ensure the course order has continuous ordering + fix_course_sortorder(); + $swapcourse = NULL; + + if (!empty($moveup)) { + if ($movecourse = $DB->get_record('course', array('id' => $moveup))) { + $swapcourse = $DB->get_record('course', array('sortorder' => $movecourse->sortorder - 1)); + } + } else { + if ($movecourse = $DB->get_record('course', array('id' => $movedown))) { + $swapcourse = $DB->get_record('course', array('sortorder' => $movecourse->sortorder + 1)); + } + } + if ($swapcourse and $movecourse) { + // check course's category + if ($movecourse->category != $id) { + print_error('coursedoesnotbelongtocategory'); + } + $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $movecourse->id)); + $DB->set_field('course', 'sortorder', $movecourse->sortorder, array('id' => $swapcourse->id)); + add_to_log($movecourse->id, "course", "move", "edit.php?id=$movecourse->id", $movecourse->id); + } + } + +} // End of editing stuff + +// Prepare the standard URL params for this page. We'll need them later. +$urlparams = array('id' => $id); +if ($page) { + $urlparams['page'] = $page; +} +if ($perpage) { + $urlparams['perpage'] = $perpage; +} + +// Begin output +if ($editingon && can_edit_in_category()) { + // Integrate into the admin tree only if the user can edit categories at the top level, + // otherwise the admin block does not appear to this user, and you get an error. + require_once($CFG->libdir . '/adminlib.php'); + navigation_node::override_active_url(new moodle_url('/course/category.php', array('id' => $id))); + admin_externalpage_setup('coursemgmt', '', $urlparams, $CFG->wwwroot . '/course/category.php'); + $PAGE->set_context($context); // Ensure that we are actually showing blocks etc for the cat context + + $settingsnode = $PAGE->settingsnav->find_active_node(); + if ($settingsnode) { + $settingsnode->make_inactive(); + $settingsnode->force_open(); + $PAGE->navbar->add($settingsnode->text, $settingsnode->action); + } + echo $OUTPUT->header(); +} else { + $site = get_site(); + $PAGE->set_title("$site->shortname: $category->name"); + $PAGE->set_heading($site->fullname); + $PAGE->set_button(print_course_search('', true, 'navbar')); + $PAGE->set_pagelayout('coursecategory'); + echo $OUTPUT->header(); +} + +/// Print the category selector +$displaylist = array(); +$notused = array(); +make_categories_list($displaylist, $notused); + +echo '
'; +$select = new single_select(new moodle_url('/course/category.php'), 'id', $displaylist, $category->id, null, 'switchcategory'); +$select->set_label(get_string('categories').':'); +echo $OUTPUT->render($select); +echo '
'; + +/// Print current category description +if (!$editingon && $category->description) { + echo $OUTPUT->box_start(); + $options = new stdClass; + $options->noclean = true; + $options->para = false; + $options->overflowdiv = true; + if (!isset($category->descriptionformat)) { + $category->descriptionformat = FORMAT_MOODLE; + } + $text = file_rewrite_pluginfile_urls($category->description, 'pluginfile.php', $context->id, 'coursecat', 'description', null); + echo format_text($text, $category->descriptionformat, $options); + echo $OUTPUT->box_end(); +} + +if ($editingon && $canmanage) { + echo $OUTPUT->container_start('buttons'); + + // Print button to update this category + $url = new moodle_url('/course/editcategory.php', array('id' => $category->id)); + echo $OUTPUT->single_button($url, get_string('editcategorythis'), 'get'); + + // Print button for creating new categories + $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id)); + echo $OUTPUT->single_button($url, get_string('addsubcategory'), 'get'); + + echo $OUTPUT->container_end(); +} + +// Print out all the sub-categories +// In order to view hidden subcategories the user must have the viewhiddencategories +// capability in the current category. +if (has_capability('moodle/category:viewhiddencategories', $context)) { + $categorywhere = ''; +} else { + $categorywhere = 'AND cc.visible = 1'; +} +// We're going to preload the context for the subcategory as we know that we +// need it later on for formatting. + +$ctxselect = context_helper::get_preload_record_columns_sql('ctx'); +$sql = "SELECT cc.*, $ctxselect + FROM {course_categories} cc + JOIN {context} ctx ON cc.id = ctx.instanceid + WHERE cc.parent = :parentid AND + ctx.contextlevel = :contextlevel + $categorywhere + ORDER BY cc.sortorder ASC"; +$subcategories = $DB->get_recordset_sql($sql, array('parentid' => $category->id, 'contextlevel' => CONTEXT_COURSECAT)); +// Prepare a table to display the sub categories. +$table = new html_table; +$table->attributes = array('border' => '0', 'cellspacing' => '2', 'cellpadding' => '4', 'class' => 'generalbox boxaligncenter category_subcategories'); +$table->head = array(new lang_string('subcategories')); +$table->data = array(); +$baseurl = new moodle_url('/course/category.php'); +foreach ($subcategories as $subcategory) { + // Preload the context we will need it to format the category name shortly. + context_helper::preload_from_record($subcategory); + $context = context_coursecat::instance($subcategory->id); + // Prepare the things we need to create a link to the subcategory + $attributes = $subcategory->visible ? array() : array('class' => 'dimmed'); + $text = format_string($subcategory->name, true, array('context' => $context)); + // Add the subcategory to the table + $baseurl->param('id', $subcategory->id); + $table->data[] = array(html_writer::link($baseurl, $text, $attributes)); +} + +$subcategorieswereshown = (count($table->data) > 0); +if ($subcategorieswereshown) { + echo html_writer::table($table); +} + +// Print out all the courses. +$courses = get_courses_page($category->id, 'c.sortorder ASC', + 'c.id,c.sortorder,c.shortname,c.fullname,c.summary,c.visible', + $totalcount, $page*$perpage, $perpage); +$numcourses = count($courses); + +// We can consider that we are using pagination when the total count of courses is different than the one returned. +$pagingmode = $totalcount != $numcourses; + +if (!$courses) { + // There is no course to display. + if (empty($subcategorieswereshown)) { + echo $OUTPUT->heading(get_string("nocoursesyet")); + } +} else if ($numcourses <= $CFG->courseswithsummarieslimit and !$pagingmode and !$editingon) { + // We display courses with their summaries as we have not reached the limit, also we are not + // in paging mode and not allowed to edit either. + echo $OUTPUT->box_start('courseboxes'); + print_courses($category); + echo $OUTPUT->box_end(); +} else { + // The conditions above have failed, we display a basic list of courses with paging/editing options. + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "/course/category.php?id=$category->id&perpage=$perpage"); + + echo '
'; + echo ''; + echo ''; + echo ''; + if ($editingon) { + echo ''; + echo ''; + } else { + echo ''; + } + echo ''; + + $count = 0; + $abletomovecourses = false; // for now + + // Checking if we are at the first or at the last page, to allow courses to + // be moved up and down beyond the paging border + if ($totalcount > $perpage) { + $atfirstpage = ($page == 0); + if ($perpage > 0) { + $atlastpage = (($page + 1) == ceil($totalcount / $perpage)); + } else { + $atlastpage = true; + } + } else { + $atfirstpage = true; + $atlastpage = true; + } + + $baseurl = new moodle_url('/course/category.php', $urlparams + array('sesskey' => sesskey())); + foreach ($courses as $acourse) { + $coursecontext = context_course::instance($acourse->id); + + $count++; + $up = ($count > 1 || !$atfirstpage); + $down = ($count < $numcourses || !$atlastpage); + + $linkcss = $acourse->visible ? '' : ' class="dimmed" '; + echo ''; + $coursename = get_course_display_name_for_list($acourse); + echo ''; + if ($editingon) { + echo ''; + echo ''; + } else { + echo '"; + } + echo ""; + } + + if ($abletomovecourses) { + $movetocategories = array(); + $notused = array(); + make_categories_list($movetocategories, $notused, 'moodle/category:manage'); + $movetocategories[$category->id] = get_string('moveselectedcoursesto'); + echo ''; + } + + echo '
'.get_string('courses').''.get_string('edit').''.get_string('select').' 
'. format_string($coursename) .''; + if (has_capability('moodle/course:update', $coursecontext)) { + $url = new moodle_url('/course/edit.php', array('id' => $acourse->id, 'category' => $id, 'returnto' => 'category')); + echo $OUTPUT->action_icon($url, new pix_icon('t/edit', get_string('settings'))); + } + + // role assignment link + if (has_capability('moodle/course:enrolreview', $coursecontext)) { + $url = new moodle_url('/enrol/users.php', array('id' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/enrolusers', get_string('enrolledusers', 'enrol'))); + } + + if (can_delete_course($acourse->id)) { + $url = new moodle_url('/course/delete.php', array('id' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/delete', get_string('delete'))); + } + + // MDL-8885, users with no capability to view hidden courses, should not be able to lock themselves out + if (has_capability('moodle/course:visibility', $coursecontext) && has_capability('moodle/course:viewhiddencourses', $coursecontext)) { + if (!empty($acourse->visible)) { + $url = new moodle_url($baseurl, array('hide' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/hide', get_string('hide'))); + } else { + $url = new moodle_url($baseurl, array('show' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/show', get_string('show'))); + } + } + + if (has_capability('moodle/backup:backupcourse', $coursecontext)) { + $url = new moodle_url('/backup/backup.php', array('id' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/backup', get_string('backup'))); + } + + if (has_capability('moodle/restore:restorecourse', $coursecontext)) { + $url = new moodle_url('/backup/restorefile.php', array('contextid' => $coursecontext->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/restore', get_string('restore'))); + } + + if ($canmanage) { + if ($up) { + $url = new moodle_url($baseurl, array('moveup' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/up', get_string('moveup'))); + } + + if ($down) { + $url = new moodle_url($baseurl, array('movedown' => $acourse->id)); + echo $OUTPUT->action_icon($url, new pix_icon('t/down', get_string('movedown'))); + } + $abletomovecourses = true; + } + + echo ''; + echo ''; + echo ''; + // print enrol info + if ($icons = enrol_get_course_info_icons($acourse)) { + foreach ($icons as $pix_icon) { + echo $OUTPUT->render($pix_icon); + } + } + if (!empty($acourse->summary)) { + $url = new moodle_url("/course/info.php?id=$acourse->id"); + echo $OUTPUT->action_link($url, ''.get_string('info').'', + new popup_action('click', $url, 'courseinfo'), array('title'=>get_string('summary'))); + } + echo "
'; + echo html_writer::label(get_string('moveselectedcoursesto'), 'movetoid', false, array('class' => 'accesshide')); + echo html_writer::select($movetocategories, 'moveto', $category->id, null, array('id'=>'movetoid', 'class' => 'autosubmit')); + $PAGE->requires->yui_module('moodle-core-formautosubmit', + 'M.core.init_formautosubmit', + array(array('selectid' => 'movetoid', 'nothing' => $category->id)) + ); + echo ''; + echo '
'; + echo '
'; + echo '
'; +} + +echo '
'; +if ($canmanage and $numcourses > 1) { + // Print button to re-sort courses by name + $url = new moodle_url('/course/category.php', array('id' => $category->id, 'resort' => 'name', 'sesskey' => sesskey())); + echo $OUTPUT->single_button($url, get_string('resortcoursesbyname'), 'get'); +} + +if (has_capability('moodle/course:create', $context)) { + // Print button to create a new course + $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'category')); + echo $OUTPUT->single_button($url, get_string('addnewcourse'), 'get'); +} + +if (!empty($CFG->enablecourserequests) && $category->id == $CFG->defaultrequestcategory) { + print_course_request_buttons(context_system::instance()); +} +echo '
'; + +print_course_search(); + +echo $OUTPUT->footer(); diff --git a/changenumsections.php b/changenumsections.php new file mode 100644 index 0000000..deed367 --- /dev/null +++ b/changenumsections.php @@ -0,0 +1,63 @@ +. + +/** + * This script allows the number of sections in a course to be increased + * or decreased, redirecting to the course page. + * + * @package core_course + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.3 + */ + +require_once(dirname(__FILE__).'/../config.php'); +require_once($CFG->dirroot.'/course/lib.php'); + +$courseid = required_param('courseid', PARAM_INT); +$increase = optional_param('increase', true, PARAM_BOOL); +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); +$courseformatoptions = course_get_format($course)->get_format_options(); + +$PAGE->set_url('/course/changenumsections.php', array('courseid' => $courseid)); + +// Authorisation checks. +require_login($course); +require_capability('moodle/course:update', context_course::instance($course->id)); +require_sesskey(); + +if (isset($courseformatoptions['numsections'])) { + if ($increase) { + // Add an additional section. + $courseformatoptions['numsections']++; + } else { + // Remove a section. + $courseformatoptions['numsections']--; + } + + // Don't go less than 0, intentionally redirect silently (for the case of + // double clicks). + if ($courseformatoptions['numsections'] >= 0) { + course_get_format($course)->update_course_format_options( + array('numsections' => $courseformatoptions['numsections'])); + } +} + +$url = course_get_url($course); +$url->set_anchor('changenumsections'); +// Redirect to where we were.. +redirect($url); diff --git a/completion.js b/completion.js new file mode 100644 index 0000000..4d9542a --- /dev/null +++ b/completion.js @@ -0,0 +1,109 @@ + +M.core_completion = {}; + +M.core_completion.init = function(Y) { + // Check the reload-forcing + var changeDetector = Y.one('#completion_dynamic_change'); + if (changeDetector.get('value') > 0) { + changeDetector.set('value', 0); + window.location.reload(); + return; + } + + var handle_success = function(id, o, args) { + Y.one('#completion_dynamic_change').set('value', 1); + + if (o.responseText != 'OK') { + alert('An error occurred when attempting to save your tick mark.\n\n('+o.responseText+'.)'); //TODO: localize + + } else { + var current = args.state.get('value'); + var modulename = args.modulename.get('value'); + if (current == 1) { + var altstr = M.str.completion['completion-alt-manual-y'].replace('{$a}', modulename); + var titlestr = M.str.completion['completion-title-manual-y'].replace('{$a}', modulename); + args.state.set('value', 0); + args.image.set('src', M.util.image_url('i/completion-manual-y', 'moodle')); + args.image.set('alt', altstr); + args.image.set('title', titlestr); + } else { + var altstr = M.str.completion['completion-alt-manual-n'].replace('{$a}', modulename); + var titlestr = M.str.completion['completion-title-manual-n'].replace('{$a}', modulename); + args.state.set('value', 1); + args.image.set('src', M.util.image_url('i/completion-manual-n', 'moodle')); + args.image.set('alt', altstr); + args.image.set('title', titlestr); + } + } + + args.ajax.remove(); + }; + + var handle_failure = function(id, o, args) { + alert('An error occurred when attempting to save your tick mark.\n\n('+o.responseText+'.)'); //TODO: localize + args.ajax.remove(); + }; + + var toggle = function(e) { + e.preventDefault(); + + var form = e.target; + var cmid = 0; + var completionstate = 0; + var state = null; + var image = null; + var modulename = null; + + var inputs = Y.Node.getDOMNode(form).getElementsByTagName('input'); + for (var i=0; i'); + form.append(ajax); + + var cfg = { + method: "POST", + data: 'id='+cmid+'&completionstate='+completionstate+'&fromajax=1&sesskey='+M.cfg.sesskey, + on: { + success: handle_success, + failure: handle_failure + }, + arguments: {state: state, image: image, ajax: ajax, modulename: modulename} + }; + + Y.use('io-base', function(Y) { + Y.io(M.cfg.wwwroot+'/course/togglecompletion.php', cfg); + }); + }; + + // register submit handlers on manual tick completion forms + Y.all('form.togglecompletion').each(function(form) { + if (!form.hasClass('preventjs')) { + Y.on('submit', toggle, form); + } + }); + + // hide the help if there are no completion toggles or icons + var help = Y.one('#completionprogressid'); + if (help && !(Y.one('form.togglecompletion') || Y.one('.autocompletion'))) { + help.setStyle('display', 'none'); + } +}; + + diff --git a/completion.php b/completion.php new file mode 100644 index 0000000..559ccdb --- /dev/null +++ b/completion.php @@ -0,0 +1,159 @@ +libdir.'/completionlib.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_unenrol.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php'); +require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php'); +require_once $CFG->libdir.'/gradelib.php'; +require_once('completion_form.php'); + +$id = required_param('id', PARAM_INT); // course id + +/// basic access control checks +if ($id) { // editing course + + if($id == SITEID){ + // don't allow editing of 'site course' using this from + print_error('cannoteditsiteform'); + } + + if (!$course = $DB->get_record('course', array('id'=>$id))) { + print_error('invalidcourseid'); + } + require_login($course); + require_capability('moodle/course:update', context_course::instance($course->id)); + +} else { + require_login(); + print_error('needcourseid'); +} + +/// Set up the page +$streditcompletionsettings = get_string("editcoursecompletionsettings", 'completion'); +$PAGE->set_course($course); +$PAGE->set_url('/course/completion.php', array('id' => $course->id)); +//$PAGE->navbar->add($streditcompletionsettings); +$PAGE->set_title($course->shortname); +$PAGE->set_heading($course->fullname); +$PAGE->set_pagelayout('standard'); + +/// first create the form +$form = new course_completion_form('completion.php?id='.$id, compact('course')); + +// now override defaults if course already exists +if ($form->is_cancelled()){ + redirect($CFG->wwwroot.'/course/view.php?id='.$course->id); + +} else if ($data = $form->get_data()) { + + $completion = new completion_info($course); + +/// process criteria unlocking if requested + if (!empty($data->settingsunlock)) { + + $completion->delete_course_completion_data(); + + // Return to form (now unlocked) + redirect($CFG->wwwroot."/course/completion.php?id=$course->id"); + } + +/// process data if submitted + // Delete old criteria + $completion->clear_criteria(); + + // Loop through each criteria type and run update_config + global $COMPLETION_CRITERIA_TYPES; + foreach ($COMPLETION_CRITERIA_TYPES as $type) { + $class = 'completion_criteria_'.$type; + $criterion = new $class(); + $criterion->update_config($data); + } + + // Handle aggregation methods + // Overall aggregation + $aggdata = array( + 'course' => $data->id, + 'criteriatype' => null + ); + $aggregation = new completion_aggregation($aggdata); + $aggregation->setMethod($data->overall_aggregation); + $aggregation->save(); + + // Activity aggregation + if (empty($data->activity_aggregation)) { + $data->activity_aggregation = 0; + } + + $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY; + $aggregation = new completion_aggregation($aggdata); + $aggregation->setMethod($data->activity_aggregation); + $aggregation->save(); + + // Course aggregation + if (empty($data->course_aggregation)) { + $data->course_aggregation = 0; + } + + $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_COURSE; + $aggregation = new completion_aggregation($aggdata); + $aggregation->setMethod($data->course_aggregation); + $aggregation->save(); + + // Role aggregation + if (empty($data->role_aggregation)) { + $data->role_aggregation = 0; + } + + $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ROLE; + $aggregation = new completion_aggregation($aggdata); + $aggregation->setMethod($data->role_aggregation); + $aggregation->save(); + + add_to_log($course->id, 'course', 'completion updated', 'completion.php?id='.$course->id); + + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + redirect($url); +} + + +/// Print the form + + +echo $OUTPUT->header(); +echo $OUTPUT->heading($streditcompletionsettings); + +$form->display(); + +echo $OUTPUT->footer(); diff --git a/completion_form.php b/completion_form.php new file mode 100644 index 0000000..1f68101 --- /dev/null +++ b/completion_form.php @@ -0,0 +1,222 @@ +libdir.'/formslib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +class course_completion_form extends moodleform { + + function definition() { + global $USER, $CFG, $DB, $js_enabled; + + $courseconfig = get_config('moodlecourse'); + $mform =& $this->_form; + + $course = $this->_customdata['course']; + $completion = new completion_info($course); + + $params = array( + 'course' => $course->id + ); + + +/// form definition +//-------------------------------------------------------------------------------- + + // Check if there is existing criteria completions + if ($completion->is_course_locked()) { + $mform->addElement('header', '', get_string('completionsettingslocked', 'completion')); + $mform->addElement('static', '', '', get_string('err_settingslocked', 'completion')); + $mform->addElement('submit', 'settingsunlock', get_string('unlockcompletiondelete', 'completion')); + } + + // Get array of all available aggregation methods + $aggregation_methods = $completion->get_aggregation_methods(); + + // Overall criteria aggregation + $mform->addElement('header', 'overallcriteria', get_string('overallcriteriaaggregation', 'completion')); + $mform->addElement('select', 'overall_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods); + $mform->setDefault('overall_aggregation', $completion->get_aggregation_method()); + + // Course prerequisite completion criteria + $mform->addElement('header', 'courseprerequisites', get_string('completiondependencies', 'completion')); + + // Get applicable courses + $courses = $DB->get_records_sql( + " + SELECT DISTINCT + c.id, + c.category, + c.fullname, + cc.id AS selected + FROM + {course} c + LEFT JOIN + {course_completion_criteria} cc + ON cc.courseinstance = c.id + AND cc.course = {$course->id} + INNER JOIN + {course_completion_criteria} ccc + ON ccc.course = c.id + WHERE + c.enablecompletion = ".COMPLETION_ENABLED." + AND c.id <> {$course->id} + " + ); + + if (!empty($courses)) { + if (count($courses) > 1) { + $mform->addElement('select', 'course_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods); + $mform->setDefault('course_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_COURSE)); + } + + // Get category list + $list = array(); + $parents = array(); + make_categories_list($list, $parents); + + // Get course list for select box + $selectbox = array(); + $selected = array(); + foreach ($courses as $c) { + $selectbox[$c->id] = $list[$c->category] . ' / ' . format_string($c->fullname, true, array('context' => context_course::instance($c->id))); + + // If already selected + if ($c->selected) { + $selected[] = $c->id; + } + } + + // Show multiselect box + $mform->addElement('select', 'criteria_course', get_string('coursesavailable', 'completion'), $selectbox, array('multiple' => 'multiple', 'size' => 6)); + + // Select current criteria + $mform->setDefault('criteria_course', $selected); + + // Explain list + $mform->addElement('static', 'criteria_courses_explaination', '', get_string('coursesavailableexplaination', 'completion')); + + } else { + $mform->addElement('static', 'nocourses', '', get_string('err_nocourses', 'completion')); + } + + // Manual self completion + $mform->addElement('header', 'manualselfcompletion', get_string('manualselfcompletion', 'completion')); + $criteria = new completion_criteria_self($params); + $criteria->config_form_display($mform); + + // Role completion criteria + $mform->addElement('header', 'roles', get_string('manualcompletionby', 'completion')); + + $roles = get_roles_with_capability('moodle/course:markcomplete', CAP_ALLOW, context_course::instance($course->id, IGNORE_MISSING)); + + if (!empty($roles)) { + $mform->addElement('select', 'role_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods); + $mform->setDefault('role_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ROLE)); + + foreach ($roles as $role) { + $params_a = array('role' => $role->id); + $criteria = new completion_criteria_role(array_merge($params, $params_a)); + $criteria->config_form_display($mform, $role); + } + } else { + $mform->addElement('static', 'noroles', '', get_string('err_noroles', 'completion')); + } + + // Activity completion criteria + $mform->addElement('header', 'activitiescompleted', get_string('activitiescompleted', 'completion')); + + $activities = $completion->get_activities(); + if (!empty($activities)) { + if (count($activities) > 1) { + $mform->addElement('select', 'activity_aggregation', get_string('aggregationmethod', 'completion'), $aggregation_methods); + $mform->setDefault('activity_aggregation', $completion->get_aggregation_method(COMPLETION_CRITERIA_TYPE_ACTIVITY)); + } + + foreach ($activities as $activity) { + $params_a = array('moduleinstance' => $activity->id); + $criteria = new completion_criteria_activity(array_merge($params, $params_a)); + $criteria->config_form_display($mform, $activity); + } + } else { + $mform->addElement('static', 'noactivities', '', get_string('err_noactivities', 'completion')); + } + + // Completion on date + $mform->addElement('header', 'date', get_string('date')); + $criteria = new completion_criteria_date($params); + $criteria->config_form_display($mform); + + // Completion after enrolment duration + $mform->addElement('header', 'duration', get_string('durationafterenrolment', 'completion')); + $criteria = new completion_criteria_duration($params); + $criteria->config_form_display($mform); + + // Completion on course grade + $mform->addElement('header', 'grade', get_string('coursegrade', 'completion')); + + // Grade enable and passing grade + $course_grade = $DB->get_field('grade_items', 'gradepass', array('courseid' => $course->id, 'itemtype' => 'course')); + if (!$course_grade) { + $course_grade = '0.00000'; + } + $criteria = new completion_criteria_grade($params); + $criteria->config_form_display($mform, $course_grade); + + // Completion on unenrolment + $mform->addElement('header', 'unenrolment', get_string('unenrolment', 'completion')); + $criteria = new completion_criteria_unenrol($params); + $criteria->config_form_display($mform); + + +//-------------------------------------------------------------------------------- + $this->add_action_buttons(); +//-------------------------------------------------------------------------------- + $mform->addElement('hidden', 'id', $course->id); + $mform->setType('id', PARAM_INT); + + // If the criteria are locked, freeze values and submit button + if ($completion->is_course_locked()) { + $except = array('settingsunlock'); + $mform->hardFreezeAllVisibleExcept($except); + $mform->addElement('cancel'); + } + } + + +/// perform some extra moodle validation + function validation($data, $files) { + global $DB, $CFG; + + $errors = parent::validation($data, $files); + + return $errors; + } +} +?> diff --git a/delete.php b/delete.php new file mode 100644 index 0000000..a47822b --- /dev/null +++ b/delete.php @@ -0,0 +1,87 @@ +dirroot . '/course/lib.php'); + + $id = required_param('id', PARAM_INT); // course id + $delete = optional_param('delete', '', PARAM_ALPHANUM); // delete confirmation hash + + $PAGE->set_url('/course/delete.php', array('id' => $id)); + $PAGE->set_context(context_system::instance()); + require_login(); + + $site = get_site(); + + $strdeletecourse = get_string("deletecourse"); + $stradministration = get_string("administration"); + $strcategories = get_string("categories"); + + if (! $course = $DB->get_record("course", array("id"=>$id))) { + print_error("invalidcourseid"); + } + if ($site->id == $course->id) { + // can not delete frontpage! + print_error("invalidcourseid"); + } + + $coursecontext = context_course::instance($course->id); + + if (!can_delete_course($id)) { + print_error('cannotdeletecourse'); + } + + $category = $DB->get_record("course_categories", array("id"=>$course->category)); + $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); + $categoryname = format_string($category->name, true, array('context' => context_coursecat::instance($category->id))); + + $PAGE->navbar->add($stradministration, new moodle_url('/admin/index.php/')); + $PAGE->navbar->add($strcategories, new moodle_url('/course/index.php')); + $PAGE->navbar->add($categoryname, new moodle_url('/course/category.php', array('id'=>$course->category))); + if (! $delete) { + $strdeletecheck = get_string("deletecheck", "", $courseshortname); + $strdeletecoursecheck = get_string("deletecoursecheck"); + + $PAGE->navbar->add($strdeletecheck); + $PAGE->set_title("$site->shortname: $strdeletecheck"); + $PAGE->set_heading($site->fullname); + echo $OUTPUT->header(); + + $message = "$strdeletecoursecheck

" . format_string($course->fullname, true, array('context' => $coursecontext)) . " (" . $courseshortname . ")"; + + echo $OUTPUT->confirm($message, "delete.php?id=$course->id&delete=".md5($course->timemodified), "category.php?id=$course->category"); + + echo $OUTPUT->footer(); + exit; + } + + if ($delete != md5($course->timemodified)) { + print_error("invalidmd5"); + } + + if (!confirm_sesskey()) { + print_error('confirmsesskeybad', 'error'); + } + + // OK checks done, delete the course now. + + add_to_log(SITEID, "course", "delete", "view.php?id=$course->id", "$course->fullname (ID $course->id)"); + + $strdeletingcourse = get_string("deletingcourse", "", $courseshortname); + + $PAGE->navbar->add($strdeletingcourse); + $PAGE->set_title("$site->shortname: $strdeletingcourse"); + $PAGE->set_heading($site->fullname); + echo $OUTPUT->header(); + echo $OUTPUT->heading($strdeletingcourse); + + delete_course($course); + fix_course_sortorder(); //update course count in catagories + + echo $OUTPUT->heading( get_string("deletedcourse", "", $courseshortname) ); + + echo $OUTPUT->continue_button("category.php?id=$course->category"); + + echo $OUTPUT->footer(); + + diff --git a/delete_category_form.php b/delete_category_form.php new file mode 100644 index 0000000..8c32405 --- /dev/null +++ b/delete_category_form.php @@ -0,0 +1,151 @@ +libdir.'/formslib.php'); +require_once($CFG->libdir.'/questionlib.php'); + +class delete_category_form extends moodleform { + + var $_category; + + function definition() { + global $CFG, $DB; + + $mform =& $this->_form; + $category = $this->_customdata; + $categorycontext = context_coursecat::instance($category->id); + $this->_category = $category; + + /// Check permissions, to see if it OK to give the option to delete + /// the contents, rather than move elsewhere. + /// Are there any subcategories of this one, can they be deleted? + $candeletecontent = true; + $tocheck = get_child_categories($category->id); + $containscategories = !empty($tocheck); + $categoryids = array($category->id); + while (!empty($tocheck)) { + $checkcat = array_pop($tocheck); + $childcategoryids[] = $checkcat->id; + $tocheck = $tocheck + get_child_categories($checkcat->id); + $chcontext = context_coursecat::instance($checkcat->id); + if ($candeletecontent && !has_capability('moodle/category:manage', $chcontext)) { + $candeletecontent = false; + } + } + + /// Are there any courses in here, can they be deleted? + list($test, $params) = $DB->get_in_or_equal($categoryids); + $containedcourses = $DB->get_records_sql( + "SELECT id,1 FROM {course} c WHERE c.category $test", $params); + $containscourses = false; + if ($containedcourses) { + $containscourses = true; + foreach ($containedcourses as $courseid => $notused) { + if ($candeletecontent && !can_delete_course($courseid)) { + $candeletecontent = false; + break; + } + } + } + + /// Are there any questions in the question bank here? + $containsquestions = question_context_has_any_questions($categorycontext); + + /// Get the list of categories we might be able to move to. + $testcaps = array(); + if ($containscourses) { + $testcaps[] = 'moodle/course:create'; + } + if ($containscategories || $containsquestions) { + $testcaps[] = 'moodle/category:manage'; + } + $displaylist = array(); + $notused = array(); + if (!empty($testcaps)) { + make_categories_list($displaylist, $notused, $testcaps, $category->id); + } + + /// Now build the options. + $options = array(); + if ($displaylist) { + $options[0] = get_string('movecontentstoanothercategory'); + } + if ($candeletecontent) { + $options[1] = get_string('deleteallcannotundo'); + } + + /// Now build the form. + $mform->addElement('header','general', get_string('categorycurrentcontents', '', format_string($category->name, true, array('context' => $categorycontext)))); + + if ($containscourses || $containscategories || $containsquestions) { + if (empty($options)) { + print_error('youcannotdeletecategory', 'error', 'index.php', format_string($category->name, true, array('context' => $categorycontext))); + } + + /// Describe the contents of this category. + $contents = ''; + $mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), $contents); + + /// Give the options for what to do. + $mform->addElement('select', 'fulldelete', get_string('whattodo'), $options); + if (count($options) == 1) { + $optionkeys = array_keys($options); + $option = reset($optionkeys); + $mform->hardFreeze('fulldelete'); + $mform->setConstant('fulldelete', $option); + } + + if ($displaylist) { + $mform->addElement('select', 'newparent', get_string('movecategorycontentto'), $displaylist); + if (in_array($category->parent, $displaylist)) { + $mform->setDefault('newparent', $category->parent); + } + $mform->disabledIf('newparent', 'fulldelete', 'eq', '1'); + } + } else { + $mform->addElement('hidden', 'fulldelete', 1); + $mform->setType('fulldelete', PARAM_INT); + $mform->addElement('static', 'emptymessage', '', get_string('deletecategoryempty')); + } + + $mform->addElement('hidden', 'delete'); + $mform->setType('delete', PARAM_ALPHANUM); + $mform->addElement('hidden', 'sure'); + $mform->setType('sure', PARAM_ALPHANUM); + $mform->setDefault('sure', md5(serialize($category))); + +//-------------------------------------------------------------------------------- + $this->add_action_buttons(true, get_string('delete')); + + } + +/// perform some extra moodle validation + function validation($data, $files) { + $errors = parent::validation($data, $files); + + if (empty($data['fulldelete']) && empty($data['newparent'])) { + /// When they have chosen the move option, they must specify a destination. + $errors['newparent'] = get_string('required'); + } + + if ($data['sure'] != md5(serialize($this->_category))) { + $errors['categorylabel'] = get_string('categorymodifiedcancel'); + } + + return $errors; + } +} + diff --git a/dndupload.js b/dndupload.js new file mode 100644 index 0000000..1ee8395 --- /dev/null +++ b/dndupload.js @@ -0,0 +1,982 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Javascript library for enableing a drag and drop upload to courses + * + * @package core + * @subpackage course + * @copyright 2012 Davo Smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +M.course_dndupload = { + // YUI object. + Y: null, + // URL for upload requests + url: M.cfg.wwwroot + '/course/dndupload.php', + // maximum size of files allowed in this form + maxbytes: 0, + // ID of the course we are on + courseid: null, + // Data about the different file/data handlers that are available + handlers: null, + // Nasty hack to distinguish between dragenter(first entry), + // dragenter+dragleave(moving between child elements) and dragleave (leaving element) + entercount: 0, + // Used to keep track of the section we are dragging across - to make + // spotting movement between sections more reliable + currentsection: null, + // Used to store the pending uploads whilst the user is being asked for further input + uploadqueue: null, + // True if the there is currently a dialog being shown (asking for a name, or giving a + // choice of file handlers) + uploaddialog: false, + // An array containing the last selected file handler for each file type + lastselected: null, + + // The following are used to identify specific parts of the course page + + // The type of HTML element that is a course section + sectiontypename: 'li', + // The classes that an element must have to be identified as a course section + sectionclasses: ['section', 'main'], + // The ID of the main content area of the page (for adding the 'status' div) + pagecontentid: 'page', + // The selector identifying the list of modules within a section (note changing this may require + // changes to the get_mods_element function) + modslistselector: 'ul.section', + + /** + * Initalise the drag and drop upload interface + * Note: one and only one of options.filemanager and options.formcallback must be defined + * + * @param Y the YUI object + * @param object options { + * courseid: ID of the course we are on + * maxbytes: maximum size of files allowed in this form + * handlers: Data about the different file/data handlers that are available + * } + */ + init: function(Y, options) { + this.Y = Y; + + if (!this.browser_supported()) { + return; // Browser does not support the required functionality + } + + this.maxbytes = options.maxbytes; + this.courseid = options.courseid; + this.handlers = options.handlers; + this.uploadqueue = new Array(); + this.lastselected = new Array(); + + var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.'); + var sections = this.Y.all(sectionselector); + if (sections.isEmpty()) { + return; // No sections - incompatible course format or front page. + } + sections.each( function(el) { + this.add_preview_element(el); + this.init_events(el); + }, this); + + if (options.showstatus) { + this.add_status_div(); + } + }, + + /** + * Add a div element to tell the user that drag and drop upload + * is available (or to explain why it is not available) + */ + add_status_div: function() { + var coursecontents = document.getElementById(this.pagecontentid); + if (!coursecontents) { + return; + } + + var div = document.createElement('div'); + div.id = 'dndupload-status'; + div.style.opacity = 0.0; + coursecontents.insertBefore(div, coursecontents.firstChild); + + var Y = this.Y; + div = Y.one(div); + var handlefile = (this.handlers.filehandlers.length > 0); + var handletext = false; + var handlelink = false; + var i; + for (i=0; i 2) { + this.entercount = 2; + return false; + } + } + + this.show_preview_element(section, type); + + return false; + }, + + /** + * Handle a dragleave event: remove the 'add here' message (if present) + * @param e event data + * @return false to prevent the event from continuing to be processed + */ + drag_leave: function(e) { + if (!this.check_drag(e)) { + return false; + } + + this.entercount--; + if (this.entercount == 1) { + return false; + } + this.entercount = 0; + this.currentsection = null; + + this.hide_preview_element(); + return false; + }, + + /** + * Handle a dragover event: just prevent the browser default (necessary + * to allow drag and drop handling to work) + * @param e event data + * @return false to prevent the event from continuing to be processed + */ + drag_over: function(e) { + this.check_drag(e); + return false; + }, + + /** + * Handle a drop event: hide the 'add here' message, check the attached + * data type and start the upload process + * @param e event data + * @return false to prevent the event from continuing to be processed + */ + drop: function(e) { + if (!(type = this.check_drag(e))) { + return false; + } + + this.hide_preview_element(); + + // Work out the number of the section we are on (from its id) + var section = this.get_section(e.currentTarget); + var sectionnumber = this.get_section_number(section); + + // Process the file or the included data + if (type.type == 'Files') { + var files = e._event.dataTransfer.files; + for (var i=0, f; f=files[i]; i++) { + this.handle_file(f, section, sectionnumber); + } + } else { + var contents = e._event.dataTransfer.getData(type.realtype); + if (contents) { + this.handle_item(type, contents, section, sectionnumber); + } + } + + return false; + }, + + /** + * Find or create the 'ul' element that contains all of the module + * instances in this section + * @param section the DOM element representing the section + * @return false to prevent the event from continuing to be processed + */ + get_mods_element: function(section) { + // Find the 'ul' containing the list of mods + var modsel = section.one(this.modslistselector); + if (!modsel) { + // Create the above 'ul' if it doesn't exist + var modsel = document.createElement('ul'); + modsel.className = 'section img-text'; + var contentel = section.get('children').pop(); + var brel = contentel.get('children').pop(); + contentel.insertBefore(modsel, brel); + modsel = this.Y.one(modsel); + } + + return modsel; + }, + + /** + * Add a new dummy item to the list of mods, to be replaced by a real + * item & link once the AJAX upload call has completed + * @param name the label to show in the element + * @param section the DOM element reperesenting the course section + * @return DOM element containing the new item + */ + add_resource_element: function(name, section) { + var modsel = this.get_mods_element(section); + + var resel = { + parent: modsel, + li: document.createElement('li'), + div: document.createElement('div'), + indentdiv: document.createElement('div'), + a: document.createElement('a'), + icon: document.createElement('img'), + namespan: document.createElement('span'), + groupingspan: document.createElement('span'), + progressouter: document.createElement('span'), + progress: document.createElement('span') + }; + + resel.li.className = 'activity resource modtype_resource'; + + resel.indentdiv.className = 'mod-indent'; + resel.li.appendChild(resel.indentdiv); + + resel.div.className = 'activityinstance'; + resel.indentdiv.appendChild(resel.div); + + resel.a.href = '#'; + resel.div.appendChild(resel.a); + + resel.icon.src = M.util.image_url('i/ajaxloader'); + resel.icon.className = 'activityicon iconlarge'; + resel.a.appendChild(resel.icon); + + resel.namespan.className = 'instancename'; + resel.namespan.innerHTML = name; + resel.a.appendChild(resel.namespan); + + resel.groupingspan.className = 'groupinglabel'; + resel.div.appendChild(resel.groupingspan); + + resel.progressouter.className = 'dndupload-progress-outer'; + resel.progress.className = 'dndupload-progress-inner'; + resel.progress.innerHTML = ' '; + resel.progressouter.appendChild(resel.progress); + resel.div.appendChild(resel.progressouter); + + modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom + + return resel; + }, + + /** + * Hide any visible dndupload-preview elements on the page + */ + hide_preview_element: function() { + this.Y.all('li.dndupload-preview').addClass('dndupload-hidden'); + }, + + /** + * Unhide the preview element for the given section and set it to display + * the correct message + * @param section the YUI node representing the selected course section + * @param type the details of the data type detected in the drag (including the message to display) + */ + show_preview_element: function(section, type) { + this.hide_preview_element(); + var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden'); + preview.one('span').setContent(type.addmessage); + }, + + /** + * Add the preview element to a course section. Note: this needs to be done before 'addEventListener' + * is called, otherwise Firefox will ignore events generated when the mouse is over the preview + * element (instead of passing them up to the parent element) + * @param section the YUI node representing the selected course section + */ + add_preview_element: function(section) { + var modsel = this.get_mods_element(section); + var preview = { + li: document.createElement('li'), + div: document.createElement('div'), + icon: document.createElement('img'), + namespan: document.createElement('span') + }; + + preview.li.className = 'dndupload-preview dndupload-hidden'; + + preview.div.className = 'mod-indent'; + preview.li.appendChild(preview.div); + + preview.icon.src = M.util.image_url('t/addfile'); + preview.icon.className = 'icon'; + preview.div.appendChild(preview.icon); + + preview.div.appendChild(document.createTextNode(' ')); + + preview.namespan.className = 'instancename'; + preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle'); + preview.div.appendChild(preview.namespan); + + modsel.appendChild(preview.li); + }, + + /** + * Find the registered handler for the given file type. If there is more than one, ask the + * user which one to use. Then upload the file to the server + * @param file the details of the file, taken from the FileList in the drop event + * @param section the DOM element representing the selected course section + * @param sectionnumber the number of the selected course section + */ + handle_file: function(file, section, sectionnumber) { + var handlers = new Array(); + var filehandlers = this.handlers.filehandlers; + var extension = ''; + var dotpos = file.name.lastIndexOf('.'); + if (dotpos != -1) { + extension = file.name.substr(dotpos+1, file.name.length); + } + + for (var i=0; i'; + content += '
'; + for (var i=0; i'; + content += '
'; + } + content += '
'; + + var Y = this.Y; + var self = this; + var panel = new Y.Panel({ + bodyContent: content, + width: 350, + zIndex: 5, + centered: true, + modal: true, + visible: true, + render: true, + buttons: [{ + value: M.util.get_string('upload', 'moodle'), + action: function(e) { + e.preventDefault(); + // Find out which module was selected + var module = false; + var div = Y.one('#dndupload_handlers'+uploadid); + div.all('input').each(function(input) { + if (input.get('checked')) { + module = input.get('value'); + } + }); + if (!module) { + return; + } + panel.hide(); + // Remember this selection for next time + self.lastselected[extension] = module; + // Do the upload + self.upload_file(file, section, sectionnumber, module); + }, + section: Y.WidgetStdMod.FOOTER + },{ + value: M.util.get_string('cancel', 'moodle'), + action: function(e) { + e.preventDefault(); + panel.hide(); + }, + section: Y.WidgetStdMod.FOOTER + }] + }); + // When the panel is hidden - destroy it and then check for other pending uploads + panel.after("visibleChange", function(e) { + if (!panel.get('visible')) { + panel.destroy(true); + self.check_upload_queue(); + } + }); + }, + + /** + * Check to see if there are any other dialog boxes to show, now that the current one has + * been dealt with + */ + check_upload_queue: function() { + this.uploaddialog = false; + if (this.uploadqueue.length == 0) { + return; + } + + var details = this.uploadqueue.shift(); + if (details.isfile) { + this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber); + } else { + this.handle_item(details.type, details.contents, details.section, details.sectionnumber); + } + }, + + /** + * Do the file upload: show the dummy element, use an AJAX call to send the data + * to the server, update the progress bar for the file, then replace the dummy + * element with the real information once the AJAX call completes + * @param file the details of the file, taken from the FileList in the drop event + * @param section the DOM element representing the selected course section + * @param sectionnumber the number of the selected course section + */ + upload_file: function(file, section, sectionnumber, module) { + + // This would be an ideal place to use the Y.io function + // however, this does not support data encoded using the + // FormData object, which is needed to transfer data from + // the DataTransfer object into an XMLHTTPRequest + // This can be converted when the YUI issue has been integrated: + // http://yuilibrary.com/projects/yui3/ticket/2531274 + var xhr = new XMLHttpRequest(); + var self = this; + + if (file.size > this.maxbytes) { + alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle')); + return; + } + + // Add the file to the display + var resel = this.add_resource_element(file.name, section); + + // Update the progress bar as the file is uploaded + xhr.upload.addEventListener('progress', function(e) { + if (e.lengthComputable) { + var percentage = Math.round((e.loaded * 100) / e.total); + resel.progress.style.width = percentage + '%'; + } + }, false); + + // Wait for the AJAX call to complete, then update the + // dummy element with the returned details + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var result = JSON.parse(xhr.responseText); + if (result) { + if (result.error == 0) { + // All OK - update the dummy element + resel.icon.src = result.icon; + resel.a.href = result.link; + resel.namespan.innerHTML = result.name; + if (!parseInt(result.visible, 10)) { + resel.a.className = 'dimmed'; + } + + if (result.groupingname) { + resel.groupingspan.innerHTML = '(' + result.groupingname + ')'; + } else { + resel.div.removeChild(resel.groupingspan); + } + + resel.div.removeChild(resel.progressouter); + resel.li.id = result.elementid; + resel.indentdiv.innerHTML += result.commands; + if (result.onclick) { + resel.a.onclick = result.onclick; + } + if (self.Y.UA.gecko > 0) { + // Fix a Firefox bug which makes sites with a '~' in their wwwroot + // log the user out when clicking on the link (before refreshing the page). + resel.div.innerHTML = unescape(resel.div.innerHTML); + } + self.add_editing(result.elementid); + } else { + // Error - remove the dummy element + resel.parent.removeChild(resel.li); + alert(result.error); + } + } + } else { + alert(M.util.get_string('servererror', 'moodle')); + } + } + }; + + // Prepare the data to send + var formData = new FormData(); + formData.append('repo_upload_file', file); + formData.append('sesskey', M.cfg.sesskey); + formData.append('course', this.courseid); + formData.append('section', sectionnumber); + formData.append('module', module); + formData.append('type', 'Files'); + + // Send the AJAX call + xhr.open("POST", this.url, true); + xhr.send(formData); + }, + + /** + * Show a dialog box to gather the name of the resource / activity to be created + * from the uploaded content + * @param type the details of the type of content + * @param contents the contents to be uploaded + * @section the DOM element for the section being uploaded to + * @sectionnumber the number of the section being uploaded to + */ + handle_item: function(type, contents, section, sectionnumber) { + if (type.handlers.length == 0) { + // Nothing to handle this - should not have got here + return; + } + + if (this.uploaddialog) { + var details = new Object(); + details.isfile = false; + details.type = type; + details.contents = contents; + details.section = section; + details.setcionnumber = sectionnumber; + this.uploadqueue.push(details); + return; + } + this.uploaddialog = true; + + var timestamp = new Date().getTime(); + var uploadid = Math.round(Math.random()*100000)+'-'+timestamp; + var nameid = 'dndupload_handler_name'+uploadid; + var content = ''; + content += ''; + content += ' '; + if (type.handlers.length > 1) { + content += '
'; + var sel = type.handlers[0].module; + for (var i=0; i'; + content += '
'; + } + content += '
'; + } + + var Y = this.Y; + var self = this; + var panel = new Y.Panel({ + bodyContent: content, + width: 350, + zIndex: 5, + centered: true, + modal: true, + visible: true, + render: true, + buttons: [{ + value: M.util.get_string('upload', 'moodle'), + action: function(e) { + e.preventDefault(); + var name = Y.one('#dndupload_handler_name'+uploadid).get('value'); + name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim + if (name == '') { + return; + } + var module = false; + if (type.handlers.length > 1) { + // Find out which module was selected + var div = Y.one('#dndupload_handlers'+uploadid); + div.all('input').each(function(input) { + if (input.get('checked')) { + module = input.get('value'); + } + }); + if (!module) { + return; + } + } else { + module = type.handlers[0].module; + } + panel.hide(); + // Do the upload + self.upload_item(name, type.type, contents, section, sectionnumber, module); + }, + section: Y.WidgetStdMod.FOOTER + },{ + value: M.util.get_string('cancel', 'moodle'), + action: function(e) { + e.preventDefault(); + panel.hide(); + }, + section: Y.WidgetStdMod.FOOTER + }] + }); + // When the panel is hidden - destroy it and then check for other pending uploads + panel.after("visibleChange", function(e) { + if (!panel.get('visible')) { + panel.destroy(true); + self.check_upload_queue(); + } + }); + // Focus on the 'name' box + Y.one('#'+nameid).focus(); + }, + + /** + * Upload any data types that are not files: display a dummy resource element, send + * the data to the server, update the progress bar for the file, then replace the + * dummy element with the real information once the AJAX call completes + * @param name the display name for the resource / activity to create + * @param type the details of the data type found in the drop event + * @param contents the actual data that was dropped + * @param section the DOM element representing the selected course section + * @param sectionnumber the number of the selected course section + * @param module the module chosen to handle this upload + */ + upload_item: function(name, type, contents, section, sectionnumber, module) { + + // This would be an ideal place to use the Y.io function + // however, this does not support data encoded using the + // FormData object, which is needed to transfer data from + // the DataTransfer object into an XMLHTTPRequest + // This can be converted when the YUI issue has been integrated: + // http://yuilibrary.com/projects/yui3/ticket/2531274 + var xhr = new XMLHttpRequest(); + var self = this; + + // Add the item to the display + var resel = this.add_resource_element(name, section); + + // Wait for the AJAX call to complete, then update the + // dummy element with the returned details + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + var result = JSON.parse(xhr.responseText); + if (result) { + if (result.error == 0) { + // All OK - update the dummy element + resel.icon.src = result.icon; + resel.a.href = result.link; + resel.namespan.innerHTML = result.name; + if (!parseInt(result.visible, 10)) { + resel.a.className = 'dimmed'; + } + + if (result.groupingname) { + resel.groupingspan.innerHTML = '(' + result.groupingname + ')'; + } else { + resel.div.removeChild(resel.groupingspan); + } + + resel.div.removeChild(resel.progressouter); + resel.li.id = result.elementid; + resel.div.innerHTML += result.commands; + if (result.onclick) { + resel.a.onclick = result.onclick; + } + if (self.Y.UA.gecko > 0) { + // Fix a Firefox bug which makes sites with a '~' in their wwwroot + // log the user out when clicking on the link (before refreshing the page). + resel.div.innerHTML = unescape(resel.div.innerHTML); + } + self.add_editing(result.elementid, sectionnumber); + } else { + // Error - remove the dummy element + resel.parent.removeChild(resel.li); + alert(result.error); + } + } + } else { + alert(M.util.get_string('servererror', 'moodle')); + } + } + }; + + // Prepare the data to send + var formData = new FormData(); + formData.append('contents', contents); + formData.append('displayname', name); + formData.append('sesskey', M.cfg.sesskey); + formData.append('course', this.courseid); + formData.append('section', sectionnumber); + formData.append('type', type); + formData.append('module', module); + + // Send the data + xhr.open("POST", this.url, true); + xhr.send(formData); + }, + + /** + * Call the AJAX course editing initialisation to add the editing tools + * to the newly-created resource link + * @param elementid the id of the DOM element containing the new resource link + * @param sectionnumber the number of the selected course section + */ + add_editing: function(elementid) { + YUI().use('moodle-course-coursebase', function(Y) { + M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid); + }); + } +}; diff --git a/dndupload.php b/dndupload.php new file mode 100644 index 0000000..c4df5a8 --- /dev/null +++ b/dndupload.php @@ -0,0 +1,39 @@ +. + +/** + * Starting point for drag and drop course uploads + * + * @package core + * @subpackage lib + * @copyright 2012 Davo smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('AJAX_SCRIPT', true); + +require_once(dirname(dirname(__FILE__)).'/config.php'); +require_once($CFG->dirroot.'/course/dnduploadlib.php'); + +$courseid = required_param('course', PARAM_INT); +$section = required_param('section', PARAM_INT); +$type = required_param('type', PARAM_TEXT); +$modulename = required_param('module', PARAM_PLUGIN); +$displayname = optional_param('displayname', null, PARAM_TEXT); +$contents = optional_param('contents', null, PARAM_RAW); // It will be up to each plugin to clean this data, before saving it. + +$dndproc = new dndupload_ajax_processor($courseid, $section, $type, $modulename); +$dndproc->process($displayname, $contents); diff --git a/dnduploadlib.php b/dnduploadlib.php new file mode 100644 index 0000000..26e41e8 --- /dev/null +++ b/dnduploadlib.php @@ -0,0 +1,696 @@ +. + +/** + * Library to handle drag and drop course uploads + * + * @package core + * @subpackage lib + * @copyright 2012 Davo smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/repository/lib.php'); +require_once($CFG->dirroot.'/repository/upload/lib.php'); +require_once($CFG->dirroot.'/course/lib.php'); + +/** + * Add the Javascript to enable drag and drop upload to a course page + * + * @param object $course The currently displayed course + * @param array $modnames The list of enabled (visible) modules on this site + * @return void + */ +function dndupload_add_to_course($course, $modnames) { + global $CFG, $PAGE; + + $showstatus = optional_param('notifyeditingon', false, PARAM_BOOL); + + // Get all handlers. + $handler = new dndupload_handler($course, $modnames); + $jsdata = $handler->get_js_data(); + if (empty($jsdata->types) && empty($jsdata->filehandlers)) { + return; // No valid handlers - don't enable drag and drop. + } + + // Add the javascript to the page. + $jsmodule = array( + 'name' => 'coursedndupload', + 'fullpath' => new moodle_url('/course/dndupload.js'), + 'strings' => array( + array('addfilehere', 'moodle'), + array('dndworkingfiletextlink', 'moodle'), + array('dndworkingfilelink', 'moodle'), + array('dndworkingfiletext', 'moodle'), + array('dndworkingfile', 'moodle'), + array('dndworkingtextlink', 'moodle'), + array('dndworkingtext', 'moodle'), + array('dndworkinglink', 'moodle'), + array('filetoolarge', 'moodle'), + array('actionchoice', 'moodle'), + array('servererror', 'moodle'), + array('upload', 'moodle'), + array('cancel', 'moodle') + ), + 'requires' => array('node', 'event', 'panel', 'json', 'anim') + ); + $vars = array( + array('courseid' => $course->id, + 'maxbytes' => get_max_upload_file_size($CFG->maxbytes, $course->maxbytes), + 'handlers' => $handler->get_js_data(), + 'showstatus' => $showstatus) + ); + + $PAGE->requires->js_init_call('M.course_dndupload.init', $vars, true, $jsmodule); +} + + +/** + * Stores all the information about the available dndupload handlers + * + * @package core + * @copyright 2012 Davo Smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dndupload_handler { + + /** + * @var array A list of all registered mime types that can be dropped onto a course + * along with the modules that will handle them. + */ + protected $types = array(); + + /** + * @var array A list of the different file types (extensions) that different modules + * will handle. + */ + protected $filehandlers = array(); + + /** + * Gather a list of dndupload handlers from the different mods + * + * @param object $course The course this is being added to (to check course_allowed_module() ) + */ + public function __construct($course, $modnames = null) { + global $CFG; + + // Add some default types to handle. + // Note: 'Files' type is hard-coded into the Javascript as this needs to be ... + // ... treated a little differently. + $this->add_type('url', array('url', 'text/uri-list', 'text/x-moz-url'), get_string('addlinkhere', 'moodle'), + get_string('nameforlink', 'moodle'), 10); + $this->add_type('text/html', array('text/html'), get_string('addpagehere', 'moodle'), + get_string('nameforpage', 'moodle'), 20); + $this->add_type('text', array('text', 'text/plain'), get_string('addpagehere', 'moodle'), + get_string('nameforpage', 'moodle'), 30); + + // Loop through all modules to find handlers. + $mods = get_plugin_list_with_function('mod', 'dndupload_register'); + foreach ($mods as $component => $funcname) { + list($modtype, $modname) = normalize_component($component); + if ($modnames && !array_key_exists($modname, $modnames)) { + continue; // Module is deactivated (hidden) at the site level. + } + if (!course_allowed_module($course, $modname)) { + continue; // User does not have permission to add this module to the course. + } + $resp = $funcname(); + if (!$resp) { + continue; + } + if (isset($resp['files'])) { + foreach ($resp['files'] as $file) { + $this->add_file_handler($file['extension'], $modname, $file['message']); + } + } + if (isset($resp['addtypes'])) { + foreach ($resp['addtypes'] as $type) { + if (isset($type['priority'])) { + $priority = $type['priority']; + } else { + $priority = 100; + } + $this->add_type($type['identifier'], $type['datatransfertypes'], + $type['addmessage'], $type['namemessage'], $priority); + } + } + if (isset($resp['types'])) { + foreach ($resp['types'] as $type) { + $this->add_type_handler($type['identifier'], $modname, $type['message']); + } + } + } + } + + /** + * Used to add a new mime type that can be drag and dropped onto a + * course displayed in a browser window + * + * @param string $identifier The name that this type will be known as + * @param array $datatransfertypes An array of the different types in the browser + * 'dataTransfer.types' object that will map to this type + * @param string $addmessage The message to display in the browser when this type is being + * dragged onto the page + * @param string $namemessage The message to pop up when asking for the name to give the + * course module instance when it is created + * @param int $priority Controls the order in which types are checked by the browser (mainly + * needed to check for 'text' last as that is usually given as fallback) + */ + public function add_type($identifier, $datatransfertypes, $addmessage, $namemessage, $priority=100) { + if ($this->is_known_type($identifier)) { + throw new coding_exception("Type $identifier is already registered"); + } + + $add = new stdClass; + $add->identifier = $identifier; + $add->datatransfertypes = $datatransfertypes; + $add->addmessage = $addmessage; + $add->namemessage = $namemessage; + $add->priority = $priority; + $add->handlers = array(); + + $this->types[$identifier] = $add; + } + + /** + * Used to declare that a particular module will handle a particular type + * of dropped data + * + * @param string $type The name of the type (as declared in add_type) + * @param string $module The name of the module to handle this type + * @param string $message The message to show the user if more than one handler is registered + * for a type and the user needs to make a choice between them + */ + public function add_type_handler($type, $module, $message) { + if (!$this->is_known_type($type)) { + throw new coding_exception("Trying to add handler for unknown type $type"); + } + + $add = new stdClass; + $add->type = $type; + $add->module = $module; + $add->message = $message; + + $this->types[$type]->handlers[] = $add; + } + + /** + * Used to declare that a particular module will handle a particular type + * of dropped file + * + * @param string $extension The file extension to handle ('*' for all types) + * @param string $module The name of the module to handle this type + * @param string $message The message to show the user if more than one handler is registered + * for a type and the user needs to make a choice between them + */ + public function add_file_handler($extension, $module, $message) { + $extension = strtolower($extension); + + $add = new stdClass; + $add->extension = $extension; + $add->module = $module; + $add->message = $message; + + $this->filehandlers[] = $add; + } + + /** + * Check to see if the type has been registered + * + * @param string $type The identifier of the type you are interested in + * @return bool True if the type is registered + */ + public function is_known_type($type) { + return array_key_exists($type, $this->types); + } + + /** + * Check to see if the module in question has registered to handle the + * type given + * + * @param string $module The name of the module + * @param string $type The identifier of the type + * @return bool True if the module has registered to handle that type + */ + public function has_type_handler($module, $type) { + if (!$this->is_known_type($type)) { + throw new coding_exception("Checking for handler for unknown type $type"); + } + foreach ($this->types[$type]->handlers as $handler) { + if ($handler->module == $module) { + return true; + } + } + return false; + } + + /** + * Check to see if the module in question has registered to handle files + * with the given extension (or to handle all file types) + * + * @param string $module The name of the module + * @param string $extension The extension of the uploaded file + * @return bool True if the module has registered to handle files with + * that extension (or to handle all file types) + */ + public function has_file_handler($module, $extension) { + foreach ($this->filehandlers as $handler) { + if ($handler->module == $module) { + if ($handler->extension == '*' || $handler->extension == $extension) { + return true; + } + } + } + return false; + } + + /** + * Gets a list of the file types that are handled by a particular module + * + * @param string $module The name of the module to check + * @return array of file extensions or string '*' + */ + public function get_handled_file_types($module) { + $types = array(); + foreach ($this->filehandlers as $handler) { + if ($handler->module == $module) { + if ($handler->extension == '*') { + return '*'; + } else { + // Prepending '.' as otherwise mimeinfo fails. + $types[] = '.'.$handler->extension; + } + } + } + return $types; + } + + /** + * Returns an object to pass onto the javascript code with data about all the + * registered file / type handlers + * + * @return object Data to pass on to Javascript code + */ + public function get_js_data() { + global $CFG; + + $ret = new stdClass; + + // Sort the types by priority. + uasort($this->types, array($this, 'type_compare')); + + $ret->types = array(); + if (!empty($CFG->dndallowtextandlinks)) { + foreach ($this->types as $type) { + if (empty($type->handlers)) { + continue; // Skip any types without registered handlers. + } + $ret->types[] = $type; + } + } + + $ret->filehandlers = $this->filehandlers; + $uploadrepo = repository::get_instances(array('type' => 'upload')); + if (empty($uploadrepo)) { + $ret->filehandlers = array(); // No upload repo => no file handlers. + } + + return $ret; + } + + /** + * Comparison function used when sorting types by priority + * @param object $type1 first type to compare + * @param object $type2 second type to compare + * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal + */ + protected function type_compare($type1, $type2) { + if ($type1->priority < $type2->priority) { + return -1; + } + if ($type1->priority > $type2->priority) { + return 1; + } + return 0; + } + +} + +/** + * Processes the upload, creating the course module and returning the result + * + * @package core + * @copyright 2012 Davo Smith + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dndupload_ajax_processor { + + /** Returned when no error has occurred */ + const ERROR_OK = 0; + + /** @var object The course that we are uploading to */ + protected $course = null; + + /** @var context_course The course context for capability checking */ + protected $context = null; + + /** @var int The section number we are uploading to */ + protected $section = null; + + /** @var string The type of upload (e.g. 'Files', 'text/plain') */ + protected $type = null; + + /** @var object The details of the module type that will be created */ + protected $module= null; + + /** @var object The course module that has been created */ + protected $cm = null; + + /** @var dndupload_handler used to check the allowed file types */ + protected $dnduploadhandler = null; + + /** @var string The name to give the new activity instance */ + protected $displayname = null; + + /** + * Set up some basic information needed to handle the upload + * + * @param int $courseid The ID of the course we are uploading to + * @param int $section The section number we are uploading to + * @param string $type The type of upload (as reported by the browser) + * @param string $modulename The name of the module requested to handle this upload + */ + public function __construct($courseid, $section, $type, $modulename) { + global $DB; + + if (!defined('AJAX_SCRIPT')) { + throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests'); + } + + $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + + require_login($this->course, false); + $this->context = context_course::instance($this->course->id); + + if (!is_number($section) || $section < 0) { + throw new coding_exception("Invalid section number $section"); + } + $this->section = $section; + $this->type = $type; + + if (!$this->module = $DB->get_record('modules', array('name' => $modulename))) { + throw new coding_exception("Module $modulename does not exist"); + } + + $this->dnduploadhandler = new dndupload_handler($this->course); + } + + /** + * Check if this upload is a 'file' upload + * + * @return bool true if it is a 'file' upload, false otherwise + */ + protected function is_file_upload() { + return ($this->type == 'Files'); + } + + /** + * Process the upload - creating the module in the course and returning the result to the browser + * + * @param string $displayname optional the name (from the browser) to give the course module instance + * @param string $content optional the content of the upload (for non-file uploads) + */ + public function process($displayname = null, $content = null) { + require_capability('moodle/course:manageactivities', $this->context); + + if ($this->is_file_upload()) { + require_capability('moodle/course:managefiles', $this->context); + if ($content != null) { + throw new moodle_exception('fileuploadwithcontent', 'moodle'); + } + } else { + if (empty($content)) { + throw new moodle_exception('dnduploadwithoutcontent', 'moodle'); + } + } + + require_sesskey(); + + $this->displayname = $displayname; + + if ($this->is_file_upload()) { + $this->handle_file_upload(); + } else { + $this->handle_other_upload($content); + } + } + + /** + * Handle uploads containing files - create the course module, ask the upload repository + * to process the file, ask the mod to set itself up, then return the result to the browser + */ + protected function handle_file_upload() { + global $CFG; + + // Add the file to a draft file area. + $draftitemid = file_get_unused_draft_itemid(); + $maxbytes = get_max_upload_file_size($CFG->maxbytes, $this->course->maxbytes); + $types = $this->dnduploadhandler->get_handled_file_types($this->module->name); + $repo = repository::get_instances(array('type' => 'upload')); + if (empty($repo)) { + throw new moodle_exception('errornouploadrepo', 'moodle'); + } + $repo = reset($repo); // Get the first (and only) upload repo. + $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid); + if (empty($this->displayname)) { + $this->displayname = $this->display_name_from_file($details['file']); + } + + // Create a course module to hold the new instance. + $this->create_course_module(); + + // Ask the module to set itself up. + $moduledata = $this->prepare_module_data($draftitemid); + $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); + if ($instanceid === 'invalidfunction') { + throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); + } + + // Finish setting up the course module. + $this->finish_setup_course_module($instanceid); + } + + /** + * Handle uploads not containing file - create the course module, ask the mod to + * set itself up, then return the result to the browser + * + * @param string $content the content uploaded to the browser + */ + protected function handle_other_upload($content) { + // Check this plugin is registered to handle this type of upload + if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) { + $info = (object)array('modname' => $this->module->name, 'type' => $this->type); + throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info); + } + + // Create a course module to hold the new instance. + $this->create_course_module(); + + // Ask the module to set itself up. + $moduledata = $this->prepare_module_data(null, $content); + $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction'); + if ($instanceid === 'invalidfunction') { + throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function"); + } + + // Finish setting up the course module. + $this->finish_setup_course_module($instanceid); + } + + /** + * Generate the name of the mod instance from the name of the file + * (remove the extension and convert underscore => space + * + * @param string $filename the filename of the uploaded file + * @return string the display name to use + */ + protected function display_name_from_file($filename) { + $pos = textlib::strrpos($filename, '.'); + if ($pos) { // Want to skip if $pos === 0 OR $pos === false. + $filename = textlib::substr($filename, 0, $pos); + } + return str_replace('_', ' ', $filename); + } + + /** + * Create the coursemodule to hold the file/content that has been uploaded + */ + protected function create_course_module() { + if (!course_allowed_module($this->course, $this->module->name)) { + throw new coding_exception("The module {$this->module->name} is not allowed to be added to this course"); + } + + $this->cm = new stdClass(); + $this->cm->course = $this->course->id; + $this->cm->section = $this->section; + $this->cm->module = $this->module->id; + $this->cm->modulename = $this->module->name; + $this->cm->instance = 0; // This will be filled in after we create the instance. + $this->cm->visible = 1; + $this->cm->groupmode = $this->course->groupmode; + $this->cm->groupingid = $this->course->defaultgroupingid; + + // Set the correct default for completion tracking. + $this->cm->completion = COMPLETION_TRACKING_NONE; + $completion = new completion_info($this->course); + if ($completion->is_enabled()) { + if (plugin_supports('mod', $this->cm->modulename, FEATURE_MODEDIT_DEFAULT_COMPLETION, true)) { + $this->cm->completion = COMPLETION_TRACKING_MANUAL; + } + } + + if (!$this->cm->id = add_course_module($this->cm)) { + throw new coding_exception("Unable to create the course module"); + } + // The following are used inside some few core functions, so may as well set them here. + $this->cm->coursemodule = $this->cm->id; + $groupbuttons = ($this->course->groupmode or (!$this->course->groupmodeforce)); + if ($groupbuttons and plugin_supports('mod', $this->module->name, FEATURE_GROUPS, 0)) { + $this->cm->groupmodelink = (!$this->course->groupmodeforce); + } else { + $this->cm->groupmodelink = false; + $this->cm->groupmode = false; + } + } + + /** + * Gather together all the details to pass on to the mod, so that it can initialise it's + * own database tables + * + * @param int $draftitemid optional the id of the draft area containing the file (for file uploads) + * @param string $content optional the content dropped onto the course (for non-file uploads) + * @return object data to pass on to the mod, containing: + * string $type the 'type' as registered with dndupload_handler (or 'Files') + * object $course the course the upload was for + * int $draftitemid optional the id of the draft area containing the files + * int $coursemodule id of the course module that has already been created + * string $displayname the name to use for this activity (can be overriden by the mod) + */ + protected function prepare_module_data($draftitemid = null, $content = null) { + $data = new stdClass(); + $data->type = $this->type; + $data->course = $this->course; + if ($draftitemid) { + $data->draftitemid = $draftitemid; + } else if ($content) { + $data->content = $content; + } + $data->coursemodule = $this->cm->id; + $data->displayname = $this->displayname; + return $data; + } + + /** + * Called after the mod has set itself up, to finish off any course module settings + * (set instance id, add to correct section, set visibility, etc.) and send the response + * + * @param int $instanceid id returned by the mod when it was created + */ + protected function finish_setup_course_module($instanceid) { + global $DB, $USER; + + if (!$instanceid) { + // Something has gone wrong - undo everything we can. + delete_course_module($this->cm->id); + throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); + } + + // Note the section visibility + $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible; + + $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id)); + // Rebuild the course cache after update action + rebuild_course_cache($this->course->id, true); + $this->course->modinfo = null; // Otherwise we will just get the old version back again. + + $sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section); + + set_coursemodule_visible($this->cm->id, $visible); + if (!$visible) { + $DB->set_field('course_modules', 'visibleold', 1, array('id' => $this->cm->id)); + } + + // retrieve the final info about this module. + $info = get_fast_modinfo($this->course); + if (!isset($info->cms[$this->cm->id])) { + // The course module has not been properly created in the course - undo everything. + delete_course_module($this->cm->id); + throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name); + } + $mod = $info->get_cm($this->cm->id); + $mod->groupmodelink = $this->cm->groupmodelink; + $mod->groupmode = $this->cm->groupmode; + + // Trigger mod_created event with information about this module. + $eventdata = new stdClass(); + $eventdata->modulename = $mod->modname; + $eventdata->name = $mod->name; + $eventdata->cmid = $mod->id; + $eventdata->courseid = $this->course->id; + $eventdata->userid = $USER->id; + events_trigger('mod_created', $eventdata); + + add_to_log($this->course->id, "course", "add mod", + "../mod/{$mod->modname}/view.php?id=$mod->id", + "{$mod->modname} $instanceid"); + add_to_log($this->course->id, $mod->modname, "add", + "view.php?id=$mod->id", + "$instanceid", $mod->id); + + $this->send_response($mod); + } + + /** + * Send the details of the newly created activity back to the client browser + * + * @param cm_info $mod details of the mod just created + */ + protected function send_response($mod) { + global $OUTPUT; + + $resp = new stdClass(); + $resp->error = self::ERROR_OK; + $resp->icon = $mod->get_icon_url()->out(); + $resp->name = $mod->name; + $resp->link = $mod->get_url()->out(); + $resp->elementid = 'module-'.$mod->id; + $resp->commands = make_editing_buttons($mod, true, true, 0, $mod->sectionnum); + $resp->onclick = $mod->get_on_click(); + $resp->visible = $mod->visible; + + // if using groupings, then display grouping name + if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', $this->context)) { + $groupings = groups_get_all_groupings($this->course->id); + $resp->groupingname = format_string($groupings[$mod->groupingid]->name); + } + + echo $OUTPUT->header(); + echo json_encode($resp); + die(); + } +} diff --git a/edit.php b/edit.php new file mode 100644 index 0000000..91f28ed --- /dev/null +++ b/edit.php @@ -0,0 +1,173 @@ +. + +/** + * Edit course settings + * + * @package moodlecore + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once('lib.php'); +require_once('edit_form.php'); + +$id = optional_param('id', 0, PARAM_INT); // course id +$categoryid = optional_param('category', 0, PARAM_INT); // course category - can be changed in edit form +$returnto = optional_param('returnto', 0, PARAM_ALPHANUM); // generic navigation return page switch + +$PAGE->set_pagelayout('admin'); +$pageparams = array('id'=>$id); +if (empty($id)) { + $pageparams = array('category'=>$categoryid); +} +$PAGE->set_url('/course/edit.php', $pageparams); + +// basic access control checks +if ($id) { // editing course + if ($id == SITEID){ + // don't allow editing of 'site course' using this from + print_error('cannoteditsiteform'); + } + + $course = course_get_format($id)->get_course(); + require_login($course); + $category = $DB->get_record('course_categories', array('id'=>$course->category), '*', MUST_EXIST); + $coursecontext = context_course::instance($course->id); + require_capability('moodle/course:update', $coursecontext); + +} else if ($categoryid) { // creating new course in this category + $course = null; + require_login(); + $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST); + $catcontext = context_coursecat::instance($category->id); + require_capability('moodle/course:create', $catcontext); + $PAGE->set_context($catcontext); + +} else { + require_login(); + print_error('needcoursecategroyid'); +} + +// Prepare course and the editor +$editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true); +if (!empty($course)) { + //add context for editor + $editoroptions['context'] = $coursecontext; + $course = file_prepare_standard_editor($course, 'summary', $editoroptions, $coursecontext, 'course', 'summary', 0); + + // Inject current aliases + $aliases = $DB->get_records('role_names', array('contextid'=>$coursecontext->id)); + foreach($aliases as $alias) { + $course->{'role_'.$alias->roleid} = $alias->name; + } + +} else { + //editor should respect category context if course context is not set. + $editoroptions['context'] = $catcontext; + $course = file_prepare_standard_editor($course, 'summary', $editoroptions, null, 'course', 'summary', null); +} + +// first create the form +$editform = new course_edit_form(NULL, array('course'=>$course, 'category'=>$category, 'editoroptions'=>$editoroptions, 'returnto'=>$returnto)); +if ($editform->is_cancelled()) { + switch ($returnto) { + case 'category': + $url = new moodle_url($CFG->wwwroot.'/course/category.php', array('id'=>$categoryid)); + break; + case 'topcat': + $url = new moodle_url($CFG->wwwroot.'/course/'); + break; + default: + if (!empty($course->id)) { + $url = new moodle_url($CFG->wwwroot.'/course/view.php', array('id'=>$course->id)); + } else { + $url = new moodle_url($CFG->wwwroot.'/course/'); + } + break; + } + redirect($url); + +} else if ($data = $editform->get_data()) { + // process data if submitted + + if (empty($course->id)) { + // In creating the course + $course = create_course($data, $editoroptions); + + // Get the context of the newly created course + $context = context_course::instance($course->id, MUST_EXIST); + + if (!empty($CFG->creatornewroleid) and !is_viewing($context, NULL, 'moodle/role:assign') and !is_enrolled($context, NULL, 'moodle/role:assign')) { + // deal with course creators - enrol them internally with default role + enrol_try_internal_enrol($course->id, $USER->id, $CFG->creatornewroleid); + + } + if (!is_enrolled($context)) { + // Redirect to manual enrolment page if possible + $instances = enrol_get_instances($course->id, true); + foreach($instances as $instance) { + if ($plugin = enrol_get_plugin($instance->enrol)) { + if ($plugin->get_manual_enrol_link($instance)) { + // we know that the ajax enrol UI will have an option to enrol + redirect(new moodle_url('/enrol/users.php', array('id'=>$course->id))); + } + } + } + } + } else { + // Save any changes to the files used in the editor + update_course($data, $editoroptions); + } + + // Redirect user to newly created/updated course. + redirect(new moodle_url('/course/view.php', array('id' => $course->id))); +} + + +// Print the form + +$site = get_site(); + +$streditcoursesettings = get_string("editcoursesettings"); +$straddnewcourse = get_string("addnewcourse"); +$stradministration = get_string("administration"); +$strcategories = get_string("categories"); + +if (!empty($course->id)) { + $PAGE->navbar->add($streditcoursesettings); + $title = $streditcoursesettings; + $fullname = $course->fullname; +} else { + $PAGE->navbar->add($stradministration, new moodle_url('/admin/index.php')); + $PAGE->navbar->add($strcategories, new moodle_url('/course/index.php')); + $PAGE->navbar->add($straddnewcourse); + $title = "$site->shortname: $straddnewcourse"; + $fullname = $site->fullname; +} + +$PAGE->set_title($title); +$PAGE->set_heading($fullname); + +echo $OUTPUT->header(); +echo $OUTPUT->heading($streditcoursesettings); + +$editform->display(); + +echo $OUTPUT->footer(); + diff --git a/edit_form.php b/edit_form.php new file mode 100644 index 0000000..8f7e875 --- /dev/null +++ b/edit_form.php @@ -0,0 +1,358 @@ +libdir.'/formslib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +class course_edit_form extends moodleform { + protected $course; + protected $context; + + function definition() { + global $USER, $CFG, $DB, $PAGE; + + $mform = $this->_form; + $PAGE->requires->yui_module('moodle-course-formatchooser', 'M.course.init_formatchooser', + array(array('formid' => $mform->getAttribute('id')))); + + $course = $this->_customdata['course']; // this contains the data of this form + $category = $this->_customdata['category']; + $editoroptions = $this->_customdata['editoroptions']; + $returnto = $this->_customdata['returnto']; + + $systemcontext = context_system::instance(); + $categorycontext = context_coursecat::instance($category->id); + + if (!empty($course->id)) { + $coursecontext = context_course::instance($course->id); + $context = $coursecontext; + } else { + $coursecontext = null; + $context = $categorycontext; + } + + $courseconfig = get_config('moodlecourse'); + + $this->course = $course; + $this->context = $context; + +/// form definition with new course defaults +//-------------------------------------------------------------------------------- + $mform->addElement('header','general', get_string('general', 'form')); + + $mform->addElement('hidden', 'returnto', null); + $mform->setType('returnto', PARAM_ALPHANUM); + $mform->setConstant('returnto', $returnto); + + // verify permissions to change course category or keep current + if (empty($course->id)) { + if (has_capability('moodle/course:create', $categorycontext)) { + $displaylist = array(); + $parentlist = array(); + make_categories_list($displaylist, $parentlist, 'moodle/course:create'); + $mform->addElement('select', 'category', get_string('category'), $displaylist); + $mform->addHelpButton('category', 'category'); + $mform->setDefault('category', $category->id); + } else { + $mform->addElement('hidden', 'category', null); + $mform->setType('category', PARAM_INT); + $mform->setConstant('category', $category->id); + } + } else { + if (has_capability('moodle/course:changecategory', $coursecontext)) { + $displaylist = array(); + $parentlist = array(); + make_categories_list($displaylist, $parentlist, 'moodle/course:create'); + if (!isset($displaylist[$course->category])) { + //always keep current + $displaylist[$course->category] = format_string($DB->get_field('course_categories', 'name', array('id'=>$course->category))); + } + $mform->addElement('select', 'category', get_string('category'), $displaylist); + $mform->addHelpButton('category', 'category'); + } else { + //keep current + $mform->addElement('hidden', 'category', null); + $mform->setType('category', PARAM_INT); + $mform->setConstant('category', $course->category); + } + } + + $mform->addElement('text','fullname', get_string('fullnamecourse'),'maxlength="254" size="50"'); + $mform->addHelpButton('fullname', 'fullnamecourse'); + $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); + $mform->setType('fullname', PARAM_TEXT); + if (!empty($course->id) and !has_capability('moodle/course:changefullname', $coursecontext)) { + $mform->hardFreeze('fullname'); + $mform->setConstant('fullname', $course->fullname); + } + + $mform->addElement('text', 'shortname', get_string('shortnamecourse'), 'maxlength="100" size="20"'); + $mform->addHelpButton('shortname', 'shortnamecourse'); + $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); + $mform->setType('shortname', PARAM_TEXT); + if (!empty($course->id) and !has_capability('moodle/course:changeshortname', $coursecontext)) { + $mform->hardFreeze('shortname'); + $mform->setConstant('shortname', $course->shortname); + } + + $mform->addElement('text','idnumber', get_string('idnumbercourse'),'maxlength="100" size="10"'); + $mform->addHelpButton('idnumber', 'idnumbercourse'); + $mform->setType('idnumber', PARAM_RAW); + if (!empty($course->id) and !has_capability('moodle/course:changeidnumber', $coursecontext)) { + $mform->hardFreeze('idnumber'); + $mform->setConstants('idnumber', $course->idnumber); + } + + + $mform->addElement('editor','summary_editor', get_string('coursesummary'), null, $editoroptions); + $mform->addHelpButton('summary_editor', 'coursesummary'); + $mform->setType('summary_editor', PARAM_RAW); + + if (!empty($course->id) and !has_capability('moodle/course:changesummary', $coursecontext)) { + $mform->hardFreeze('summary_editor'); + } + + $courseformats = get_sorted_course_formats(true); + $formcourseformats = array(); + foreach ($courseformats as $courseformat) { + $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat"); + } + if (isset($course->format)) { + $course->format = course_get_format($course)->get_format(); // replace with default if not found + if (!in_array($course->format, $courseformats)) { + // this format is disabled. Still display it in the dropdown + $formcourseformats[$course->format] = get_string('withdisablednote', 'moodle', + get_string('pluginname', 'format_'.$course->format)); + } + } + + $mform->addElement('select', 'format', get_string('format'), $formcourseformats); + $mform->addHelpButton('format', 'format'); + $mform->setDefault('format', $courseconfig->format); + + // button to update format-specific options on format change (will be hidden by JavaScript) + $mform->registerNoSubmitButton('updatecourseformat'); + $mform->addElement('submit', 'updatecourseformat', get_string('courseformatudpate')); + + $mform->addElement('date_selector', 'startdate', get_string('startdate')); + $mform->addHelpButton('startdate', 'startdate'); + $mform->setDefault('startdate', time() + 3600 * 24); + + $options = range(0, 10); + $mform->addElement('select', 'newsitems', get_string('newsitemsnumber'), $options); + $mform->addHelpButton('newsitems', 'newsitemsnumber'); + $mform->setDefault('newsitems', $courseconfig->newsitems); + + $mform->addElement('selectyesno', 'showgrades', get_string('showgrades')); + $mform->addHelpButton('showgrades', 'showgrades'); + $mform->setDefault('showgrades', $courseconfig->showgrades); + + $mform->addElement('selectyesno', 'showreports', get_string('showreports')); + $mform->addHelpButton('showreports', 'showreports'); + $mform->setDefault('showreports', $courseconfig->showreports); + + // Handle non-existing $course->maxbytes on course creation. + $coursemaxbytes = !isset($course->maxbytes) ? null : $course->maxbytes; + + // Let's prepare the maxbytes popup. + $choices = get_max_upload_sizes($CFG->maxbytes, 0, 0, $coursemaxbytes); + $mform->addElement('select', 'maxbytes', get_string('maximumupload'), $choices); + $mform->addHelpButton('maxbytes', 'maximumupload'); + $mform->setDefault('maxbytes', $courseconfig->maxbytes); + + if (!empty($course->legacyfiles) or !empty($CFG->legacyfilesinnewcourses)) { + if (empty($course->legacyfiles)) { + //0 or missing means no legacy files ever used in this course - new course or nobody turned on legacy files yet + $choices = array('0'=>get_string('no'), '2'=>get_string('yes')); + } else { + $choices = array('1'=>get_string('no'), '2'=>get_string('yes')); + } + $mform->addElement('select', 'legacyfiles', get_string('courselegacyfiles'), $choices); + $mform->addHelpButton('legacyfiles', 'courselegacyfiles'); + if (!isset($courseconfig->legacyfiles)) { + // in case this was not initialised properly due to switching of $CFG->legacyfilesinnewcourses + $courseconfig->legacyfiles = 0; + } + $mform->setDefault('legacyfiles', $courseconfig->legacyfiles); + } + + if (!empty($CFG->allowcoursethemes)) { + $themeobjects = get_list_of_themes(); + $themes=array(); + $themes[''] = get_string('forceno'); + foreach ($themeobjects as $key=>$theme) { + if (empty($theme->hidefromselector)) { + $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); + } + } + $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); + } + +//-------------------------------------------------------------------------------- + $mform->addElement('hidden', 'addcourseformatoptionshere'); + +//-------------------------------------------------------------------------------- + enrol_course_edit_form($mform, $course, $context); + +//-------------------------------------------------------------------------------- + $mform->addElement('header','', get_string('groups', 'group')); + + $choices = array(); + $choices[NOGROUPS] = get_string('groupsnone', 'group'); + $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); + $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); + $mform->addElement('select', 'groupmode', get_string('groupmode', 'group'), $choices); + $mform->addHelpButton('groupmode', 'groupmode', 'group'); + $mform->setDefault('groupmode', $courseconfig->groupmode); + + $choices = array(); + $choices['0'] = get_string('no'); + $choices['1'] = get_string('yes'); + $mform->addElement('select', 'groupmodeforce', get_string('groupmodeforce', 'group'), $choices); + $mform->addHelpButton('groupmodeforce', 'groupmodeforce', 'group'); + $mform->setDefault('groupmodeforce', $courseconfig->groupmodeforce); + + //default groupings selector + $options = array(); + $options[0] = get_string('none'); + $mform->addElement('select', 'defaultgroupingid', get_string('defaultgrouping', 'group'), $options); + +//-------------------------------------------------------------------------------- + $mform->addElement('header','', get_string('availability')); + + $choices = array(); + $choices['0'] = get_string('courseavailablenot'); + $choices['1'] = get_string('courseavailable'); + $mform->addElement('select', 'visible', get_string('availability'), $choices); + $mform->addHelpButton('visible', 'availability'); + $mform->setDefault('visible', $courseconfig->visible); + if (!has_capability('moodle/course:visibility', $context)) { + $mform->hardFreeze('visible'); + if (!empty($course->id)) { + $mform->setConstant('visible', $course->visible); + } else { + $mform->setConstant('visible', $courseconfig->visible); + } + } + +//-------------------------------------------------------------------------------- + $mform->addElement('header','', get_string('language')); + + $languages=array(); + $languages[''] = get_string('forceno'); + $languages += get_string_manager()->get_list_of_translations(); + $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); + $mform->setDefault('lang', $courseconfig->lang); + +//-------------------------------------------------------------------------------- + if (completion_info::is_enabled_for_site()) { + $mform->addElement('header','', get_string('progress','completion')); + $mform->addElement('select', 'enablecompletion', get_string('completion','completion'), + array(0=>get_string('completiondisabled','completion'), 1=>get_string('completionenabled','completion'))); + $mform->setDefault('enablecompletion', $courseconfig->enablecompletion); + + $mform->addElement('advcheckbox', 'completionstartonenrol', get_string('completionstartonenrol', 'completion')); + $mform->setDefault('completionstartonenrol', $courseconfig->completionstartonenrol); + $mform->disabledIf('completionstartonenrol', 'enablecompletion', 'eq', 0); + } else { + $mform->addElement('hidden', 'enablecompletion'); + $mform->setType('enablecompletion', PARAM_INT); + $mform->setDefault('enablecompletion',0); + + $mform->addElement('hidden', 'completionstartonenrol'); + $mform->setType('completionstartonenrol', PARAM_INT); + $mform->setDefault('completionstartonenrol',0); + } + +/// customizable role names in this course +//-------------------------------------------------------------------------------- + $mform->addElement('header','rolerenaming', get_string('rolerenaming')); + $mform->addHelpButton('rolerenaming', 'rolerenaming'); + + if ($roles = get_all_roles()) { + $roles = role_fix_names($roles, null, ROLENAME_ORIGINAL); + $assignableroles = get_roles_for_contextlevels(CONTEXT_COURSE); + foreach ($roles as $role) { + $mform->addElement('text', 'role_'.$role->id, get_string('yourwordforx', '', $role->localname)); + $mform->setType('role_'.$role->id, PARAM_TEXT); + if (!in_array($role->id, $assignableroles)) { + $mform->setAdvanced('role_'.$role->id); + } + } + } + +//-------------------------------------------------------------------------------- + $this->add_action_buttons(); +//-------------------------------------------------------------------------------- + $mform->addElement('hidden', 'id', null); + $mform->setType('id', PARAM_INT); + +/// finally set the current form data +//-------------------------------------------------------------------------------- + $this->set_data($course); + } + + function definition_after_data() { + global $DB; + + $mform = $this->_form; + + // add available groupings + if ($courseid = $mform->getElementValue('id') and $mform->elementExists('defaultgroupingid')) { + $options = array(); + if ($groupings = $DB->get_records('groupings', array('courseid'=>$courseid))) { + foreach ($groupings as $grouping) { + $options[$grouping->id] = format_string($grouping->name); + } + } + $gr_el =& $mform->getElement('defaultgroupingid'); + $gr_el->load($options); + } + + // add course format options + $formatvalue = $mform->getElementValue('format'); + if (is_array($formatvalue) && !empty($formatvalue)) { + $courseformat = course_get_format((object)array('format' => $formatvalue[0])); + $newel = $mform->createElement('header', '', get_string('courseformatoptions', 'moodle', + $courseformat->get_format_name())); + $mform->insertElementBefore($newel, 'addcourseformatoptionshere'); + + $elements = $courseformat->create_edit_form_elements($mform); + for ($i = 0; $i < count($elements); $i++) { + $mform->insertElementBefore($mform->removeElement($elements[$i]->getName(), false), + 'addcourseformatoptionshere'); + } + } + } + +/// perform some extra moodle validation + function validation($data, $files) { + global $DB, $CFG; + + $errors = parent::validation($data, $files); + if ($foundcourses = $DB->get_records('course', array('shortname'=>$data['shortname']))) { + if (!empty($data['id'])) { + unset($foundcourses[$data['id']]); + } + if (!empty($foundcourses)) { + foreach ($foundcourses as $foundcourse) { + $foundcoursenames[] = $foundcourse->fullname; + } + $foundcoursenamestring = implode(',', $foundcoursenames); + $errors['shortname']= get_string('shortnametaken', '', $foundcoursenamestring); + } + } + + $errors = array_merge($errors, enrol_course_edit_validation($data, $this->context)); + + $courseformat = course_get_format((object)array('format' => $data['format'])); + $formaterrors = $courseformat->edit_form_validation($data, $files, $errors); + if (!empty($formaterrors) && is_array($formaterrors)) { + $errors = array_merge($errors, $formaterrors); + } + + return $errors; + } +} + diff --git a/editcategory.php b/editcategory.php new file mode 100644 index 0000000..fab0f52 --- /dev/null +++ b/editcategory.php @@ -0,0 +1,162 @@ +. + +/** + * Page for creating or editing course category name/parent/description. + * When called with an id parameter, edits the category with that id. + * Otherwise it creates a new category with default parent from the parent + * parameter, which may be 0. + * + * @package core + * @subpackage course + * @copyright 2007 Nicolas Connault + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once('lib.php'); +require_once('editcategory_form.php'); + +require_login(); + +$id = optional_param('id', 0, PARAM_INT); +$itemid = 0; //initalise itemid, as all files in category description has item id 0 + +if ($id) { + if (!$category = $DB->get_record('course_categories', array('id' => $id))) { + print_error('unknowcategory'); + } + $PAGE->set_url('/course/editcategory.php', array('id' => $id)); + $categorycontext = context_coursecat::instance($id); + $PAGE->set_context($categorycontext); + require_capability('moodle/category:manage', $categorycontext); + $strtitle = get_string('editcategorysettings'); + $editorcontext = $categorycontext; + $title = $strtitle; + $fullname = $category->name; +} else { + $parent = required_param('parent', PARAM_INT); + $PAGE->set_url('/course/editcategory.php', array('parent' => $parent)); + if ($parent) { + if (!$DB->record_exists('course_categories', array('id' => $parent))) { + print_error('unknowcategory'); + } + $context = context_coursecat::instance($parent); + } else { + $context = get_system_context(); + } + $PAGE->set_context($context); + $category = new stdClass(); + $category->id = 0; + $category->parent = $parent; + require_capability('moodle/category:manage', $context); + $strtitle = get_string("addnewcategory"); + $editorcontext = $context; + $itemid = null; //set this explicitly, so files for parent category should not get loaded in draft area. + $title = "$SITE->shortname: ".get_string('addnewcategory'); + $fullname = $SITE->fullname; +} + +$PAGE->set_pagelayout('admin'); + +$editoroptions = array( + 'maxfiles' => EDITOR_UNLIMITED_FILES, + 'maxbytes' => $CFG->maxbytes, + 'trusttext' => true, + 'context' => $editorcontext +); +$category = file_prepare_standard_editor($category, 'description', $editoroptions, $editorcontext, 'coursecat', 'description', $itemid); + +$mform = new editcategory_form('editcategory.php', compact('category', 'editoroptions')); +$mform->set_data($category); + +if ($mform->is_cancelled()) { + if ($id) { + redirect($CFG->wwwroot . '/course/category.php?id=' . $id . '&categoryedit=on'); + } else if ($parent) { + redirect($CFG->wwwroot .'/course/category.php?id=' . $parent . '&categoryedit=on'); + } else { + redirect($CFG->wwwroot .'/course/index.php?categoryedit=on'); + } +} else if ($data = $mform->get_data()) { + $newcategory = new stdClass(); + $newcategory->name = $data->name; + $newcategory->idnumber = $data->idnumber; + $newcategory->description_editor = $data->description_editor; + $newcategory->parent = $data->parent; // if $data->parent = 0, the new category will be a top-level category + + if (isset($data->theme) && !empty($CFG->allowcategorythemes)) { + $newcategory->theme = $data->theme; + } + + $logaction = 'update'; + if ($id) { + // Update an existing category. + $newcategory->id = $category->id; + if ($newcategory->parent != $category->parent) { + // check category manage capability if parent changed + require_capability('moodle/category:manage', get_category_or_system_context((int)$newcategory->parent)); + $parent_cat = $DB->get_record('course_categories', array('id' => $newcategory->parent)); + move_category($newcategory, $parent_cat); + } + } else { + // Create a new category. + $newcategory->description = $data->description_editor['text']; + + // Don't overwrite the $newcategory object as it'll be processed by file_postupdate_standard_editor in a moment + $category = create_course_category($newcategory); + $newcategory->id = $category->id; + $categorycontext = $category->context; + $logaction = 'add'; + } + + $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext, 'coursecat', 'description', 0); + $DB->update_record('course_categories', $newcategory); + add_to_log(SITEID, "category", $logaction, "editcategory.php?id=$newcategory->id", $newcategory->id); + fix_course_sortorder(); + + redirect('category.php?id='.$newcategory->id.'&categoryedit=on'); +} + +// Unfortunately the navigation never generates correctly for this page because technically this page doesn't actually +// exist on the navigation; you get here through the course management page. +// First up we'll try to make the course management page active seeing as that is where the user thinks they are. +// The big prolem here is that the course management page is a common page for both editing users and common users and +// is only added to the admin tree if the user has permission to edit at the system level. +$node = $PAGE->settingsnav->get('root'); +if ($node) { + $node = $node->get('courses'); + if ($node) { + $node = $node->get('coursemgmt'); + } +} +if ($node) { + // The course management page exists so make that active. + $node->make_active(); +} else { + // Failing that we'll override the URL, not as accurate and chances are things + // won't be 100% correct all the time but should work most times. + // A common reason to arrive here is having the management capability within only a particular category (not at system level). + navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryedit' => 'on'))); +} + +$PAGE->set_title($title); +$PAGE->set_heading($fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading($strtitle); +$mform->display(); +echo $OUTPUT->footer(); + diff --git a/editcategory_form.php b/editcategory_form.php new file mode 100644 index 0000000..3e3cfe3 --- /dev/null +++ b/editcategory_form.php @@ -0,0 +1,74 @@ +dirroot.'/course/moodleform_mod.php'); +class editcategory_form extends moodleform { + + // form definition + function definition() { + global $CFG, $DB; + $mform =& $this->_form; + $category = $this->_customdata['category']; + $editoroptions = $this->_customdata['editoroptions']; + + // get list of categories to use as parents, with site as the first one + $options = array(); + if (has_capability('moodle/category:manage', get_system_context()) || $category->parent == 0) { + $options[0] = get_string('top'); + } + $parents = array(); + if ($category->id) { + // Editing an existing category. + make_categories_list($options, $parents, 'moodle/category:manage', $category->id); + if (empty($options[$category->parent])) { + $options[$category->parent] = $DB->get_field('course_categories', 'name', array('id'=>$category->parent)); + } + $strsubmit = get_string('savechanges'); + } else { + // Making a new category + make_categories_list($options, $parents, 'moodle/category:manage'); + $strsubmit = get_string('createcategory'); + } + + $mform->addElement('select', 'parent', get_string('parentcategory'), $options); + $mform->addElement('text', 'name', get_string('categoryname'), array('size'=>'30')); + $mform->addRule('name', get_string('required'), 'required', null); + $mform->addElement('text', 'idnumber', get_string('idnumbercoursecategory'),'maxlength="100" size="10"'); + $mform->addHelpButton('idnumber', 'idnumbercoursecategory'); + $mform->addElement('editor', 'description_editor', get_string('description'), null, $editoroptions); + $mform->setType('description_editor', PARAM_RAW); + if (!empty($CFG->allowcategorythemes)) { + $themes = array(''=>get_string('forceno')); + $allthemes = get_list_of_themes(); + foreach ($allthemes as $key=>$theme) { + if (empty($theme->hidefromselector)) { + $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); + } + } + $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); + } + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + $mform->setDefault('id', $category->id); + + $this->add_action_buttons(true, $strsubmit); + } + + function validation($data, $files) { + global $DB; + $errors = parent::validation($data, $files); + if (!empty($data['idnumber'])) { + if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) { + if (!$data['id'] || $existing->id != $data['id']) { + $errors['idnumber']= get_string('idnumbertaken'); + } + } + } + + return $errors; + } +} + diff --git a/editsection.php b/editsection.php new file mode 100644 index 0000000..c75e3dc --- /dev/null +++ b/editsection.php @@ -0,0 +1,91 @@ +. + +/** + * Edit the section basic information and availability + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once("../config.php"); +require_once("lib.php"); +require_once($CFG->libdir . '/conditionlib.php'); + +$id = required_param('id', PARAM_INT); // course_sections.id +$sectionreturn = optional_param('sr', 0, PARAM_INT); + +$PAGE->set_url('/course/editsection.php', array('id'=>$id, 'sr'=> $sectionreturn)); + +$section = $DB->get_record('course_sections', array('id' => $id), '*', MUST_EXIST); +$course = $DB->get_record('course', array('id' => $section->course), '*', MUST_EXIST); +$sectionnum = $section->section; + +require_login($course); +$context = context_course::instance($course->id); +require_capability('moodle/course:update', $context); + +// get section_info object with all availability options +$sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum); + +$editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true); +$mform = course_get_format($course->id)->editsection_form($PAGE->url, + array('cs' => $sectioninfo, 'editoroptions' => $editoroptions)); +// set current value, make an editable copy of section_info object +// this will retrieve all format-specific options as well +$mform->set_data(convert_to_array($sectioninfo)); + +if ($mform->is_cancelled()){ + // form cancelled, return to course + redirect(course_get_url($course, $section, array('sr' => $sectionreturn))); +} else if ($data = $mform->get_data()) { + // data submitted and validated, update and return to course + $DB->update_record('course_sections', $data); + rebuild_course_cache($course->id, true); + if (isset($data->section)) { + // usually edit form does not change relative section number but just in case + $sectionnum = $data->section; + } + if (!empty($CFG->enableavailability)) { + // Update grade and completion conditions + $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum); + condition_info_section::update_section_from_form($sectioninfo, $data); + rebuild_course_cache($course->id, true); + } + course_get_format($course->id)->update_section_format_options($data); + + add_to_log($course->id, "course", "editsection", "editsection.php?id=$id", "$sectionnum"); + $PAGE->navigation->clear_cache(); + redirect(course_get_url($course, $section, array('sr' => $sectionreturn))); +} + +// the edit form is displayed for the first time or there was a validation +// error on the previous step. Display the edit form: +$sectionname = get_section_name($course, $sectionnum); +$stredit = get_string('edita', '', " $sectionname"); +$strsummaryof = get_string('summaryof', '', " $sectionname"); + +$PAGE->set_title($stredit); +$PAGE->set_heading($course->fullname); +$PAGE->navbar->add($stredit); +echo $OUTPUT->header(); + +echo $OUTPUT->heading($strsummaryof); + +$mform->display(); +echo $OUTPUT->footer(); diff --git a/editsection_form.php b/editsection_form.php new file mode 100644 index 0000000..7fdbdf6 --- /dev/null +++ b/editsection_form.php @@ -0,0 +1,330 @@ +libdir.'/formslib.php'); +require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->libdir.'/completionlib.php'); +require_once($CFG->libdir.'/gradelib.php'); + +/** + * Default form for editing course section + * + * Course format plugins may specify different editing form to use + */ +class editsection_form extends moodleform { + + function definition() { + + $mform = $this->_form; + $course = $this->_customdata['course']; + $mform->addElement('checkbox', 'usedefaultname', get_string('sectionusedefaultname')); + $mform->setDefault('usedefaultname', true); + + $mform->addElement('text', 'name', get_string('sectionname'), array('size'=>'30')); + $mform->setType('name', PARAM_TEXT); + $mform->disabledIf('name','usedefaultname','checked'); + + /// Prepare course and the editor + + $mform->addElement('editor', 'summary_editor', get_string('summary'), null, $this->_customdata['editoroptions']); + $mform->addHelpButton('summary_editor', 'summary'); + $mform->setType('summary_editor', PARAM_RAW); + + $mform->addElement('hidden', 'id'); + $mform->setType('id', PARAM_INT); + + // additional fields that course format has defined + $courseformat = course_get_format($course); + $formatoptions = $courseformat->section_format_options(true); + if (!empty($formatoptions)) { + $elements = $courseformat->create_edit_form_elements($mform, true); + } + + $mform->_registerCancelButton('cancel'); + } + + public function definition_after_data() { + global $CFG, $DB; + + $mform = $this->_form; + $course = $this->_customdata['course']; + + if (!empty($CFG->enableavailability)) { + $mform->addElement('header', '', get_string('availabilityconditions', 'condition')); + // String used by conditions more than once + $strcondnone = get_string('none', 'condition'); + // Grouping conditions - only if grouping is enabled at site level + if (!empty($CFG->enablegroupmembersonly)) { + $options = array(); + $options[0] = get_string('none'); + if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) { + foreach ($groupings as $grouping) { + $context = context_course::instance($course->id); + $options[$grouping->id] = format_string( + $grouping->name, true, array('context' => $context)); + } + } + $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options); + $mform->addHelpButton('groupingid', 'groupingsection', 'group'); + } + + // Available from/to defaults to midnight because then the display + // will be nicer where it tells users when they can access it (it + // shows only the date and not time). + $date = usergetdate(time()); + $midnight = make_timestamp($date['year'], $date['mon'], $date['mday']); + + // Date and time conditions. + $mform->addElement('date_time_selector', 'availablefrom', + get_string('availablefrom', 'condition'), + array('optional' => true, 'defaulttime' => $midnight)); + $mform->addElement('date_time_selector', 'availableuntil', + get_string('availableuntil', 'condition'), + array('optional' => true, 'defaulttime' => $midnight)); + + // Conditions based on grades + $gradeoptions = array(); + $items = grade_item::fetch_all(array('courseid' => $course->id)); + $items = $items ? $items : array(); + foreach ($items as $id => $item) { + $gradeoptions[$id] = $item->get_name(); + } + asort($gradeoptions); + $gradeoptions = array(0 => $strcondnone) + $gradeoptions; + + $grouparray = array(); + $grouparray[] = $mform->createElement('select', 'conditiongradeitemid', '', $gradeoptions); + $grouparray[] = $mform->createElement('static', '', '', + ' ' . get_string('grade_atleast', 'condition').' '); + $grouparray[] = $mform->createElement('text', 'conditiongrademin', '', array('size' => 3)); + $grouparray[] = $mform->createElement('static', '', '', + '% ' . get_string('grade_upto', 'condition') . ' '); + $grouparray[] = $mform->createElement('text', 'conditiongrademax', '', array('size' => 3)); + $grouparray[] = $mform->createElement('static', '', '', '%'); + $group = $mform->createElement('group', 'conditiongradegroup', + get_string('gradecondition', 'condition'), $grouparray); + + // Get full version (including condition info) of section object + $ci = new condition_info_section($this->_customdata['cs']); + $fullcs = $ci->get_full_section(); + $count = count($fullcs->conditionsgrade) + 1; + + // Grade conditions + $this->repeat_elements(array($group), $count, array(), 'conditiongraderepeats', + 'conditiongradeadds', 2, get_string('addgrades', 'condition'), true); + $mform->addHelpButton('conditiongradegroup[0]', 'gradecondition', 'condition'); + + // Conditions based on user fields + $operators = condition_info::get_condition_user_field_operators(); + $useroptions = condition_info::get_condition_user_fields(); + asort($useroptions); + + $useroptions = array(0 => $strcondnone) + $useroptions; + $grouparray = array(); + $grouparray[] =& $mform->createElement('select', 'conditionfield', '', $useroptions); + $grouparray[] =& $mform->createElement('select', 'conditionfieldoperator', '', $operators); + $grouparray[] =& $mform->createElement('text', 'conditionfieldvalue'); + $mform->setType('conditionfieldvalue', PARAM_RAW); + $group = $mform->createElement('group', 'conditionfieldgroup', get_string('userfield', 'condition'), $grouparray); + + $fieldcount = count($fullcs->conditionsfield) + 1; + + $this->repeat_elements(array($group), $fieldcount, array(), 'conditionfieldrepeats', 'conditionfieldadds', 2, + get_string('adduserfields', 'condition'), true); + $mform->addHelpButton('conditionfieldgroup[0]', 'userfield', 'condition'); + + // Conditions based on completion + $completion = new completion_info($course); + if ($completion->is_enabled()) { + $completionoptions = array(); + $modinfo = get_fast_modinfo($course); + foreach ($modinfo->cms as $id => $cm) { + // Add each course-module if it: + // (a) has completion turned on + // (b) does not belong to current course-section + if ($cm->completion && ($fullcs->id != $cm->section)) { + $completionoptions[$id] = $cm->name; + } + } + asort($completionoptions); + $completionoptions = array(0 => $strcondnone) + + $completionoptions; + + $completionvalues = array( + COMPLETION_COMPLETE => get_string('completion_complete', 'condition'), + COMPLETION_INCOMPLETE => get_string('completion_incomplete', 'condition'), + COMPLETION_COMPLETE_PASS => get_string('completion_pass', 'condition'), + COMPLETION_COMPLETE_FAIL => get_string('completion_fail', 'condition')); + + $grouparray = array(); + $grouparray[] = $mform->createElement('select', 'conditionsourcecmid', '', + $completionoptions); + $grouparray[] = $mform->createElement('select', 'conditionrequiredcompletion', '', + $completionvalues); + $group = $mform->createElement('group', 'conditioncompletiongroup', + get_string('completioncondition', 'condition'), $grouparray); + + $count = count($fullcs->conditionscompletion) + 1; + $this->repeat_elements(array($group), $count, array(), + 'conditioncompletionrepeats', 'conditioncompletionadds', 2, + get_string('addcompletions', 'condition'), true); + $mform->addHelpButton('conditioncompletiongroup[0]', + 'completionconditionsection', 'condition'); + } + + // Availability conditions - set up form values + if (!empty($CFG->enableavailability)) { + $num = 0; + foreach ($fullcs->conditionsgrade as $gradeitemid => $minmax) { + $groupelements = $mform->getElement( + 'conditiongradegroup[' . $num . ']')->getElements(); + $groupelements[0]->setValue($gradeitemid); + $groupelements[2]->setValue(is_null($minmax->min) ? '' : + format_float($minmax->min, 5, true, true)); + $groupelements[4]->setValue(is_null($minmax->max) ? '' : + format_float($minmax->max, 5, true, true)); + $num++; + } + + $num = 0; + foreach ($fullcs->conditionsfield as $fieldid => $data) { + $groupelements = $mform->getElement( + 'conditionfieldgroup[' . $num . ']')->getElements(); + $groupelements[0]->setValue($fieldid); + $groupelements[1]->setValue(is_null($data->operator) ? '' : + $data->operator); + $groupelements[2]->setValue(is_null($data->value) ? '' : + $data->value); + $num++; + } + + if ($completion->is_enabled()) { + $num = 0; + foreach ($fullcs->conditionscompletion as $othercmid => $state) { + $groupelements = $mform->getElement('conditioncompletiongroup[' . $num . ']')->getElements(); + $groupelements[0]->setValue($othercmid); + $groupelements[1]->setValue($state); + $num++; + } + } + } + + // Do we display availability info to students? + $showhide = array( + CONDITION_STUDENTVIEW_SHOW => get_string('showavailabilitysection_show', 'condition'), + CONDITION_STUDENTVIEW_HIDE => get_string('showavailabilitysection_hide', 'condition')); + $mform->addElement('select', 'showavailability', + get_string('showavailabilitysection', 'condition'), $showhide); + } + + $this->add_action_buttons(); + } + + public function validation($data, $files) { + $errors = parent::validation($data, $files); + // Conditions: Don't let them set dates which make no sense + if (array_key_exists('availablefrom', $data) && + $data['availablefrom'] && $data['availableuntil'] && + $data['availablefrom'] >= $data['availableuntil']) { + $errors['availablefrom'] = get_string('badavailabledates', 'condition'); + } + + // Conditions: Verify that the grade conditions are numbers, and make sense. + if (array_key_exists('conditiongradegroup', $data)) { + foreach ($data['conditiongradegroup'] as $i => $gradedata) { + if ($gradedata['conditiongrademin'] !== '' && + !is_numeric(unformat_float($gradedata['conditiongrademin']))) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition'); + continue; + } + if ($gradedata['conditiongrademax'] !== '' && + !is_numeric(unformat_float($gradedata['conditiongrademax']))) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition'); + continue; + } + if ($gradedata['conditiongrademin'] !== '' && $gradedata['conditiongrademax'] !== '' && + unformat_float($gradedata['conditiongrademax']) <= unformat_float($gradedata['conditiongrademin'])) { + $errors["conditiongradegroup[{$i}]"] = get_string('badgradelimits', 'condition'); + continue; + } + if ($gradedata['conditiongrademin'] === '' && $gradedata['conditiongrademax'] === '' && + $gradedata['conditiongradeitemid']) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradeitembutnolimits', 'condition'); + continue; + } + if (($gradedata['conditiongrademin'] !== '' || $gradedata['conditiongrademax'] !== '') && + !$gradedata['conditiongradeitemid']) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradelimitsbutnoitem', 'condition'); + continue; + } + } + } + + // Conditions: Verify that the user profile field has not been declared more than once + if (array_key_exists('conditionfieldgroup', $data)) { + // Array to store the existing fields + $arrcurrentfields = array(); + // Error message displayed if any condition is declared more than once. We use lang string because + // this way we don't actually generate the string unless there is an error. + $stralreadydeclaredwarning = new lang_string('fielddeclaredmultipletimes', 'condition'); + foreach ($data['conditionfieldgroup'] as $i => $fielddata) { + if ($fielddata['conditionfield'] == 0) { // Don't need to bother if none is selected + continue; + } + if (in_array($fielddata['conditionfield'], $arrcurrentfields)) { + $errors["conditionfieldgroup[{$i}]"] = $stralreadydeclaredwarning->out(); + } + // Add the field to the array + $arrcurrentfields[] = $fielddata['conditionfield']; + } + } + + return $errors; + } + + /** + * Load in existing data as form defaults + * + * @param stdClass|array $default_values object or array of default values + */ + function set_data($default_values) { + if (!is_object($default_values)) { + // we need object for file_prepare_standard_editor + $default_values = (object)$default_values; + } + $editoroptions = $this->_customdata['editoroptions']; + $default_values = file_prepare_standard_editor($default_values, 'summary', $editoroptions, + $editoroptions['context'], 'course', 'section', $default_values->id); + $default_values->usedefaultname = (is_null($default_values->name)); + parent::set_data($default_values); + } + + /** + * Return submitted data if properly submitted or returns NULL if validation fails or + * if there is no submitted data. + * + * @return object submitted data; NULL if not valid or not submitted or cancelled + */ + function get_data() { + $data = parent::get_data(); + if ($data !== null) { + $editoroptions = $this->_customdata['editoroptions']; + if (!empty($data->usedefaultname)) { + $data->name = null; + } + $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, + $editoroptions['context'], 'course', 'section', $data->id); + $course = $this->_customdata['course']; + foreach (course_get_format($course)->section_format_options() as $option => $unused) { + // fix issue with unset checkboxes not being returned at all + if (!isset($data->$option)) { + $data->$option = null; + } + } + } + return $data; + } +} diff --git a/enrol.php b/enrol.php new file mode 100644 index 0000000..aceb88b --- /dev/null +++ b/enrol.php @@ -0,0 +1,30 @@ +. + +/** + * Redirection of old enrol entry point. + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require('../config.php'); + +$id = required_param('id', PARAM_INT); + +redirect(new moodle_url('/enrol/index.php', array('id'=>$id))); diff --git a/externallib.php b/externallib.php new file mode 100644 index 0000000..b5c82f4 --- /dev/null +++ b/externallib.php @@ -0,0 +1,1813 @@ +. + + +/** + * External course API + * + * @package core_course + * @category external + * @copyright 2009 Petr Skodak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once("$CFG->libdir/externallib.php"); + +/** + * Course external functions + * + * @package core_course + * @category external + * @copyright 2011 Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.2 + */ +class core_course_external extends external_api { + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.2 + */ + public static function get_course_contents_parameters() { + return new external_function_parameters( + array('courseid' => new external_value(PARAM_INT, 'course id'), + 'options' => new external_multiple_structure ( + new external_single_structure( + array('name' => new external_value(PARAM_ALPHANUM, 'option name'), + 'value' => new external_value(PARAM_RAW, 'the value of the option, this param is personaly validated in the external function.') + ) + ), 'Options, not used yet, might be used in later version', VALUE_DEFAULT, array()) + ) + ); + } + + /** + * Get course contents + * + * @param int $courseid course id + * @param array $options These options are not used yet, might be used in later version + * @return array + * @since Moodle 2.2 + */ + public static function get_course_contents($courseid, $options = array()) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + //validate parameter + $params = self::validate_parameters(self::get_course_contents_parameters(), + array('courseid' => $courseid, 'options' => $options)); + + //retrieve the course + $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST); + + //check course format exist + if (!file_exists($CFG->dirroot . '/course/format/' . $course->format . '/lib.php')) { + throw new moodle_exception('cannotgetcoursecontents', 'webservice', '', null, get_string('courseformatnotfound', 'error', '', $course->format)); + } else { + require_once($CFG->dirroot . '/course/format/' . $course->format . '/lib.php'); + } + + // now security checks + $context = context_course::instance($course->id, IGNORE_MISSING); + try { + self::validate_context($context); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $course->id; + throw new moodle_exception('errorcoursecontextnotvalid', 'webservice', '', $exceptionparam); + } + + $canupdatecourse = has_capability('moodle/course:update', $context); + + //create return value + $coursecontents = array(); + + if ($canupdatecourse or $course->visible + or has_capability('moodle/course:viewhiddencourses', $context)) { + + //retrieve sections + $modinfo = get_fast_modinfo($course); + $sections = $modinfo->get_section_info_all(); + + //for each sections (first displayed to last displayed) + foreach ($sections as $key => $section) { + + if (!$section->uservisible) { + continue; + } + + // reset $sectioncontents + $sectionvalues = array(); + $sectionvalues['id'] = $section->id; + $sectionvalues['name'] = get_section_name($course, $section); + $sectionvalues['visible'] = $section->visible; + list($sectionvalues['summary'], $sectionvalues['summaryformat']) = + external_format_text($section->summary, $section->summaryformat, + $context->id, 'course', 'section', $section->id); + $sectioncontents = array(); + + //for each module of the section + foreach ($modinfo->sections[$section->section] as $cmid) { //matching /course/lib.php:print_section() logic + $cm = $modinfo->cms[$cmid]; + + // stop here if the module is not visible to the user + if (!$cm->uservisible) { + continue; + } + + $module = array(); + + //common info (for people being able to see the module or availability dates) + $module['id'] = $cm->id; + $module['name'] = format_string($cm->name, true); + $module['modname'] = $cm->modname; + $module['modplural'] = $cm->modplural; + $module['modicon'] = $cm->get_icon_url()->out(false); + $module['indent'] = $cm->indent; + + $modcontext = context_module::instance($cm->id); + + if (!empty($cm->showdescription)) { + $module['description'] = $cm->get_content(); + } + + //url of the module + $url = $cm->get_url(); + if ($url) { //labels don't have url + $module['url'] = $cm->get_url()->out(); + } + + $canviewhidden = has_capability('moodle/course:viewhiddenactivities', + context_module::instance($cm->id)); + //user that can view hidden module should know about the visibility + $module['visible'] = $cm->visible; + + //availability date (also send to user who can see hidden module when the showavailabilyt is ON) + if ($canupdatecourse or ($CFG->enableavailability && $canviewhidden && $cm->showavailability)) { + $module['availablefrom'] = $cm->availablefrom; + $module['availableuntil'] = $cm->availableuntil; + } + + $baseurl = 'webservice/pluginfile.php'; + + //call $modulename_export_contents + //(each module callback take care about checking the capabilities) + require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php'); + $getcontentfunction = $cm->modname.'_export_contents'; + if (function_exists($getcontentfunction)) { + if ($contents = $getcontentfunction($cm, $baseurl)) { + $module['contents'] = $contents; + } + } + + //assign result to $sectioncontents + $sectioncontents[] = $module; + + } + $sectionvalues['modules'] = $sectioncontents; + + // assign result to $coursecontents + $coursecontents[] = $sectionvalues; + } + } + return $coursecontents; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.2 + */ + public static function get_course_contents_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'Section ID'), + 'name' => new external_value(PARAM_TEXT, 'Section name'), + 'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL), + 'summary' => new external_value(PARAM_RAW, 'Section description'), + 'summaryformat' => new external_format_value('summary'), + 'modules' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'activity id'), + 'url' => new external_value(PARAM_URL, 'activity url', VALUE_OPTIONAL), + 'name' => new external_value(PARAM_RAW, 'activity module name'), + 'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL), + 'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL), + 'modicon' => new external_value(PARAM_URL, 'activity icon url'), + 'modname' => new external_value(PARAM_PLUGIN, 'activity module type'), + 'modplural' => new external_value(PARAM_TEXT, 'activity module plural name'), + 'availablefrom' => new external_value(PARAM_INT, 'module availability start date', VALUE_OPTIONAL), + 'availableuntil' => new external_value(PARAM_INT, 'module availability en date', VALUE_OPTIONAL), + 'indent' => new external_value(PARAM_INT, 'number of identation in the site'), + 'contents' => new external_multiple_structure( + new external_single_structure( + array( + // content info + 'type'=> new external_value(PARAM_TEXT, 'a file or a folder or external link'), + 'filename'=> new external_value(PARAM_FILE, 'filename'), + 'filepath'=> new external_value(PARAM_PATH, 'filepath'), + 'filesize'=> new external_value(PARAM_INT, 'filesize'), + 'fileurl' => new external_value(PARAM_URL, 'downloadable file url', VALUE_OPTIONAL), + 'content' => new external_value(PARAM_RAW, 'Raw content, will be used when type is content', VALUE_OPTIONAL), + 'timecreated' => new external_value(PARAM_INT, 'Time created'), + 'timemodified' => new external_value(PARAM_INT, 'Time modified'), + 'sortorder' => new external_value(PARAM_INT, 'Content sort order'), + + // copyright related info + 'userid' => new external_value(PARAM_INT, 'User who added this content to moodle'), + 'author' => new external_value(PARAM_TEXT, 'Content owner'), + 'license' => new external_value(PARAM_TEXT, 'Content license'), + ) + ), VALUE_DEFAULT, array() + ) + ) + ), 'list of module' + ) + ) + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function get_courses_parameters() { + return new external_function_parameters( + array('options' => new external_single_structure( + array('ids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id') + , 'List of course id. If empty return all courses + except front page course.', + VALUE_OPTIONAL) + ), 'options - operator OR is used', VALUE_DEFAULT, array()) + ) + ); + } + + /** + * Get courses + * + * @param array $options It contains an array (list of ids) + * @return array + * @since Moodle 2.2 + */ + public static function get_courses($options = array()) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + //validate parameter + $params = self::validate_parameters(self::get_courses_parameters(), + array('options' => $options)); + + //retrieve courses + if (!array_key_exists('ids', $params['options']) + or empty($params['options']['ids'])) { + $courses = $DB->get_records('course'); + } else { + $courses = $DB->get_records_list('course', 'id', $params['options']['ids']); + } + + //create return value + $coursesinfo = array(); + foreach ($courses as $course) { + + // now security checks + $context = context_course::instance($course->id, IGNORE_MISSING); + $courseformatoptions = course_get_format($course)->get_format_options(); + try { + self::validate_context($context); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $course->id; + throw new moodle_exception('errorcoursecontextnotvalid', 'webservice', '', $exceptionparam); + } + require_capability('moodle/course:view', $context); + + $courseinfo = array(); + $courseinfo['id'] = $course->id; + $courseinfo['fullname'] = $course->fullname; + $courseinfo['shortname'] = $course->shortname; + $courseinfo['categoryid'] = $course->category; + list($courseinfo['summary'], $courseinfo['summaryformat']) = + external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', 0); + $courseinfo['format'] = $course->format; + $courseinfo['startdate'] = $course->startdate; + if (array_key_exists('numsections', $courseformatoptions)) { + // For backward-compartibility + $courseinfo['numsections'] = $courseformatoptions['numsections']; + } + + //some field should be returned only if the user has update permission + $courseadmin = has_capability('moodle/course:update', $context); + if ($courseadmin) { + $courseinfo['categorysortorder'] = $course->sortorder; + $courseinfo['idnumber'] = $course->idnumber; + $courseinfo['showgrades'] = $course->showgrades; + $courseinfo['showreports'] = $course->showreports; + $courseinfo['newsitems'] = $course->newsitems; + $courseinfo['visible'] = $course->visible; + $courseinfo['maxbytes'] = $course->maxbytes; + if (array_key_exists('hiddensections', $courseformatoptions)) { + // For backward-compartibility + $courseinfo['hiddensections'] = $courseformatoptions['hiddensections']; + } + $courseinfo['groupmode'] = $course->groupmode; + $courseinfo['groupmodeforce'] = $course->groupmodeforce; + $courseinfo['defaultgroupingid'] = $course->defaultgroupingid; + $courseinfo['lang'] = $course->lang; + $courseinfo['timecreated'] = $course->timecreated; + $courseinfo['timemodified'] = $course->timemodified; + $courseinfo['forcetheme'] = $course->theme; + $courseinfo['enablecompletion'] = $course->enablecompletion; + $courseinfo['completionstartonenrol'] = $course->completionstartonenrol; + $courseinfo['completionnotify'] = $course->completionnotify; + $courseinfo['courseformatoptions'] = array(); + foreach ($courseformatoptions as $key => $value) { + $courseinfo['courseformatoptions'][] = array( + 'name' => $key, + 'value' => $value + ); + } + } + + if ($courseadmin or $course->visible + or has_capability('moodle/course:viewhiddencourses', $context)) { + $coursesinfo[] = $courseinfo; + } + } + + return $coursesinfo; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.2 + */ + public static function get_courses_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'course id'), + 'shortname' => new external_value(PARAM_TEXT, 'course short name'), + 'categoryid' => new external_value(PARAM_INT, 'category id'), + 'categorysortorder' => new external_value(PARAM_INT, + 'sort order into the category', VALUE_OPTIONAL), + 'fullname' => new external_value(PARAM_TEXT, 'full name'), + 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), + 'summary' => new external_value(PARAM_RAW, 'summary'), + 'summaryformat' => new external_format_value('summary'), + 'format' => new external_value(PARAM_PLUGIN, + 'course format: weeks, topics, social, site,..'), + 'showgrades' => new external_value(PARAM_INT, + '1 if grades are shown, otherwise 0', VALUE_OPTIONAL), + 'newsitems' => new external_value(PARAM_INT, + 'number of recent items appearing on the course page', VALUE_OPTIONAL), + 'startdate' => new external_value(PARAM_INT, + 'timestamp when the course start'), + 'numsections' => new external_value(PARAM_INT, + '(deprecated, use courseformatoptions) number of weeks/topics', + VALUE_OPTIONAL), + 'maxbytes' => new external_value(PARAM_INT, + 'largest size of file that can be uploaded into the course', + VALUE_OPTIONAL), + 'showreports' => new external_value(PARAM_INT, + 'are activity report shown (yes = 1, no =0)', VALUE_OPTIONAL), + 'visible' => new external_value(PARAM_INT, + '1: available to student, 0:not available', VALUE_OPTIONAL), + 'hiddensections' => new external_value(PARAM_INT, + '(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students', + VALUE_OPTIONAL), + 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', + VALUE_OPTIONAL), + 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', + VALUE_OPTIONAL), + 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', + VALUE_OPTIONAL), + 'timecreated' => new external_value(PARAM_INT, + 'timestamp when the course have been created', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, + 'timestamp when the course have been modified', VALUE_OPTIONAL), + 'enablecompletion' => new external_value(PARAM_INT, + 'Enabled, control via completion and activity settings. Disbaled, + not shown in activity settings.', + VALUE_OPTIONAL), + 'completionstartonenrol' => new external_value(PARAM_INT, + '1: begin tracking a student\'s progress in course completion + after course enrolment. 0: does not', + VALUE_OPTIONAL), + 'completionnotify' => new external_value(PARAM_INT, + '1: yes 0: no', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, + 'forced course language', VALUE_OPTIONAL), + 'forcetheme' => new external_value(PARAM_PLUGIN, + 'name of the force theme', VALUE_OPTIONAL), + 'courseformatoptions' => new external_multiple_structure( + new external_single_structure( + array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'), + 'value' => new external_value(PARAM_RAW, 'course format option value') + )), + 'additional options for particular course format', VALUE_OPTIONAL + ), + ), 'course' + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.2 + */ + public static function create_courses_parameters() { + $courseconfig = get_config('moodlecourse'); //needed for many default values + return new external_function_parameters( + array( + 'courses' => new external_multiple_structure( + new external_single_structure( + array( + 'fullname' => new external_value(PARAM_TEXT, 'full name'), + 'shortname' => new external_value(PARAM_TEXT, 'course short name'), + 'categoryid' => new external_value(PARAM_INT, 'category id'), + 'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL), + 'summary' => new external_value(PARAM_RAW, 'summary', VALUE_OPTIONAL), + 'summaryformat' => new external_format_value('summary', VALUE_DEFAULT), + 'format' => new external_value(PARAM_PLUGIN, + 'course format: weeks, topics, social, site,..', + VALUE_DEFAULT, $courseconfig->format), + 'showgrades' => new external_value(PARAM_INT, + '1 if grades are shown, otherwise 0', VALUE_DEFAULT, + $courseconfig->showgrades), + 'newsitems' => new external_value(PARAM_INT, + 'number of recent items appearing on the course page', + VALUE_DEFAULT, $courseconfig->newsitems), + 'startdate' => new external_value(PARAM_INT, + 'timestamp when the course start', VALUE_OPTIONAL), + 'numsections' => new external_value(PARAM_INT, + '(deprecated, use courseformatoptions) number of weeks/topics', + VALUE_OPTIONAL), + 'maxbytes' => new external_value(PARAM_INT, + 'largest size of file that can be uploaded into the course', + VALUE_DEFAULT, $courseconfig->maxbytes), + 'showreports' => new external_value(PARAM_INT, + 'are activity report shown (yes = 1, no =0)', VALUE_DEFAULT, + $courseconfig->showreports), + 'visible' => new external_value(PARAM_INT, + '1: available to student, 0:not available', VALUE_OPTIONAL), + 'hiddensections' => new external_value(PARAM_INT, + '(deprecated, use courseformatoptions) How the hidden sections in the course are displayed to students', + VALUE_OPTIONAL), + 'groupmode' => new external_value(PARAM_INT, 'no group, separate, visible', + VALUE_DEFAULT, $courseconfig->groupmode), + 'groupmodeforce' => new external_value(PARAM_INT, '1: yes, 0: no', + VALUE_DEFAULT, $courseconfig->groupmodeforce), + 'defaultgroupingid' => new external_value(PARAM_INT, 'default grouping id', + VALUE_DEFAULT, 0), + 'enablecompletion' => new external_value(PARAM_INT, + 'Enabled, control via completion and activity settings. Disabled, + not shown in activity settings.', + VALUE_OPTIONAL), + 'completionstartonenrol' => new external_value(PARAM_INT, + '1: begin tracking a student\'s progress in course completion after + course enrolment. 0: does not', + VALUE_OPTIONAL), + 'completionnotify' => new external_value(PARAM_INT, + '1: yes 0: no', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, + 'forced course language', VALUE_OPTIONAL), + 'forcetheme' => new external_value(PARAM_PLUGIN, + 'name of the force theme', VALUE_OPTIONAL), + 'courseformatoptions' => new external_multiple_structure( + new external_single_structure( + array('name' => new external_value(PARAM_ALPHANUMEXT, 'course format option name'), + 'value' => new external_value(PARAM_RAW, 'course format option value') + )), + 'additional options for particular course format', VALUE_OPTIONAL), + ) + ), 'courses to create' + ) + ) + ); + } + + /** + * Create courses + * + * @param array $courses + * @return array courses (id and shortname only) + * @since Moodle 2.2 + */ + public static function create_courses($courses) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + require_once($CFG->libdir . '/completionlib.php'); + + $params = self::validate_parameters(self::create_courses_parameters(), + array('courses' => $courses)); + + $availablethemes = get_plugin_list('theme'); + $availablelangs = get_string_manager()->get_list_of_translations(); + + $transaction = $DB->start_delegated_transaction(); + + foreach ($params['courses'] as $course) { + + // Ensure the current user is allowed to run this function + $context = context_coursecat::instance($course['categoryid'], IGNORE_MISSING); + try { + self::validate_context($context); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->catid = $course['categoryid']; + throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); + } + require_capability('moodle/course:create', $context); + + // Make sure lang is valid + if (array_key_exists('lang', $course) and empty($availablelangs[$course['lang']])) { + throw new moodle_exception('errorinvalidparam', 'webservice', '', 'lang'); + } + + // Make sure theme is valid + if (array_key_exists('forcetheme', $course)) { + if (!empty($CFG->allowcoursethemes)) { + if (empty($availablethemes[$course['forcetheme']])) { + throw new moodle_exception('errorinvalidparam', 'webservice', '', 'forcetheme'); + } else { + $course['theme'] = $course['forcetheme']; + } + } + } + + //force visibility if ws user doesn't have the permission to set it + $category = $DB->get_record('course_categories', array('id' => $course['categoryid'])); + if (!has_capability('moodle/course:visibility', $context)) { + $course['visible'] = $category->visible; + } + + //set default value for completion + $courseconfig = get_config('moodlecourse'); + if (completion_info::is_enabled_for_site()) { + if (!array_key_exists('enablecompletion', $course)) { + $course['enablecompletion'] = $courseconfig->enablecompletion; + } + if (!array_key_exists('completionstartonenrol', $course)) { + $course['completionstartonenrol'] = $courseconfig->completionstartonenrol; + } + } else { + $course['enablecompletion'] = 0; + $course['completionstartonenrol'] = 0; + } + + $course['category'] = $course['categoryid']; + + // Summary format. + $course['summaryformat'] = external_validate_format($course['summaryformat']); + + if (!empty($course['courseformatoptions'])) { + foreach ($course['courseformatoptions'] as $option) { + $course[$option['name']] = $option['value']; + } + } + + //Note: create_course() core function check shortname, idnumber, category + $course['id'] = create_course((object) $course)->id; + + $resultcourses[] = array('id' => $course['id'], 'shortname' => $course['shortname']); + } + + $transaction->allow_commit(); + + return $resultcourses; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.2 + */ + public static function create_courses_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'course id'), + 'shortname' => new external_value(PARAM_TEXT, 'short name'), + ) + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.2 + */ + public static function delete_courses_parameters() { + return new external_function_parameters( + array( + 'courseids' => new external_multiple_structure(new external_value(PARAM_INT, 'course ID')), + ) + ); + } + + /** + * Delete courses + * + * @param array $courseids A list of course ids + * @since Moodle 2.2 + */ + public static function delete_courses($courseids) { + global $CFG, $DB; + require_once($CFG->dirroot."/course/lib.php"); + + // Parameter validation. + $params = self::validate_parameters(self::delete_courses_parameters(), array('courseids'=>$courseids)); + + $transaction = $DB->start_delegated_transaction(); + + foreach ($params['courseids'] as $courseid) { + $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); + + // Check if the context is valid. + $coursecontext = context_course::instance($course->id); + self::validate_context($coursecontext); + + // Check if the current user has enought permissions. + if (!can_delete_course($courseid)) { + throw new moodle_exception('cannotdeletecategorycourse', 'error', + '', format_string($course->fullname)." (id: $courseid)"); + } + + delete_course($course, false); + } + + $transaction->allow_commit(); + + return null; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.2 + */ + public static function delete_courses_returns() { + return null; + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function duplicate_course_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'course to duplicate id'), + 'fullname' => new external_value(PARAM_TEXT, 'duplicated course full name'), + 'shortname' => new external_value(PARAM_TEXT, 'duplicated course short name'), + 'categoryid' => new external_value(PARAM_INT, 'duplicated course category parent'), + 'visible' => new external_value(PARAM_INT, 'duplicated course visible, default to yes', VALUE_DEFAULT, 1), + 'options' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_ALPHAEXT, 'The backup option name: + "activities" (int) Include course activites (default to 1 that is equal to yes), + "blocks" (int) Include course blocks (default to 1 that is equal to yes), + "filters" (int) Include course filters (default to 1 that is equal to yes), + "users" (int) Include users (default to 0 that is equal to no), + "role_assignments" (int) Include role assignments (default to 0 that is equal to no), + "comments" (int) Include user comments (default to 0 that is equal to no), + "completion_information" (int) Include user course completion information (default to 0 that is equal to no), + "logs" (int) Include course logs (default to 0 that is equal to no), + "histories" (int) Include histories (default to 0 that is equal to no)' + ), + 'value' => new external_value(PARAM_RAW, 'the value for the option 1 (yes) or 0 (no)' + ) + ) + ), VALUE_DEFAULT, array() + ), + ) + ); + } + + /** + * Duplicate a course + * + * @param int $courseid + * @param string $fullname Duplicated course fullname + * @param string $shortname Duplicated course shortname + * @param int $categoryid Duplicated course parent category id + * @param int $visible Duplicated course availability + * @param array $options List of backup options + * @return array New course info + * @since Moodle 2.3 + */ + public static function duplicate_course($courseid, $fullname, $shortname, $categoryid, $visible = 1, $options = array()) { + global $CFG, $USER, $DB; + require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + + // Parameter validation. + $params = self::validate_parameters( + self::duplicate_course_parameters(), + array( + 'courseid' => $courseid, + 'fullname' => $fullname, + 'shortname' => $shortname, + 'categoryid' => $categoryid, + 'visible' => $visible, + 'options' => $options + ) + ); + + // Context validation. + + if (! ($course = $DB->get_record('course', array('id'=>$params['courseid'])))) { + throw new moodle_exception('invalidcourseid', 'error'); + } + + // Category where duplicated course is going to be created. + $categorycontext = context_coursecat::instance($params['categoryid']); + self::validate_context($categorycontext); + + // Course to be duplicated. + $coursecontext = context_course::instance($course->id); + self::validate_context($coursecontext); + + $backupdefaults = array( + 'activities' => 1, + 'blocks' => 1, + 'filters' => 1, + 'users' => 0, + 'role_assignments' => 0, + 'comments' => 0, + 'completion_information' => 0, + 'logs' => 0, + 'histories' => 0 + ); + + $backupsettings = array(); + // Check for backup and restore options. + if (!empty($params['options'])) { + foreach ($params['options'] as $option) { + + // Strict check for a correct value (allways 1 or 0, true or false). + $value = clean_param($option['value'], PARAM_INT); + + if ($value !== 0 and $value !== 1) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + if (!isset($backupdefaults[$option['name']])) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + $backupsettings[$option['name']] = $value; + } + } + + // Capability checking. + + // The backup controller check for this currently, this may be redundant. + require_capability('moodle/course:create', $categorycontext); + require_capability('moodle/restore:restorecourse', $categorycontext); + require_capability('moodle/backup:backupcourse', $coursecontext); + + if (!empty($backupsettings['users'])) { + require_capability('moodle/backup:userinfo', $coursecontext); + require_capability('moodle/restore:userinfo', $categorycontext); + } + + // Check if the shortname is used. + if ($foundcourses = $DB->get_records('course', array('shortname'=>$shortname))) { + foreach ($foundcourses as $foundcourse) { + $foundcoursenames[] = $foundcourse->fullname; + } + + $foundcoursenamestring = implode(',', $foundcoursenames); + throw new moodle_exception('shortnametaken', '', '', $foundcoursenamestring); + } + + // Backup the course. + + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id); + + foreach ($backupsettings as $name => $value) { + $bc->get_plan()->get_setting($name)->set_value($value); + } + + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + + $bc->execute_plan(); + $results = $bc->get_results(); + $file = $results['backup_destination']; + + $bc->destroy(); + + // Restore the backup immediately. + + // Check if we need to unzip the file because the backup temp dir does not contains backup files. + if (!file_exists($backupbasepath . "/moodle_backup.xml")) { + $file->extract_to_pathname(get_file_packer(), $backupbasepath); + } + + // Create new course. + $newcourseid = restore_dbops::create_new_course($params['fullname'], $params['shortname'], $params['categoryid']); + + $rc = new restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE); + + foreach ($backupsettings as $name => $value) { + $setting = $rc->get_plan()->get_setting($name); + if ($setting->get_status() == backup_setting::NOT_LOCKED) { + $setting->set_value($value); + } + } + + if (!$rc->execute_precheck()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + $errorinfo = ''; + + foreach ($precheckresults['errors'] as $error) { + $errorinfo .= $error; + } + + if (array_key_exists('warnings', $precheckresults)) { + foreach ($precheckresults['warnings'] as $warning) { + $errorinfo .= $warning; + } + } + + throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); + } + } + + $rc->execute_plan(); + $rc->destroy(); + + $course = $DB->get_record('course', array('id' => $newcourseid), '*', MUST_EXIST); + $course->fullname = $params['fullname']; + $course->shortname = $params['shortname']; + $course->visible = $params['visible']; + + // Set shortname and fullname back. + $DB->update_record('course', $course); + + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + // Delete the course backup file created by this WebService. Originally located in the course backups area. + $file->delete(); + + return array('id' => $course->id, 'shortname' => $course->shortname); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.3 + */ + public static function duplicate_course_returns() { + return new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'course id'), + 'shortname' => new external_value(PARAM_TEXT, 'short name'), + ) + ); + } + + /** + * Returns description of method parameters for import_course + * + * @return external_function_parameters + * @since Moodle 2.4 + */ + public static function import_course_parameters() { + return new external_function_parameters( + array( + 'importfrom' => new external_value(PARAM_INT, 'the id of the course we are importing from'), + 'importto' => new external_value(PARAM_INT, 'the id of the course we are importing to'), + 'deletecontent' => new external_value(PARAM_INT, 'whether to delete the course content where we are importing to (default to 0 = No)', VALUE_DEFAULT, 0), + 'options' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_ALPHA, 'The backup option name: + "activities" (int) Include course activites (default to 1 that is equal to yes), + "blocks" (int) Include course blocks (default to 1 that is equal to yes), + "filters" (int) Include course filters (default to 1 that is equal to yes)' + ), + 'value' => new external_value(PARAM_RAW, 'the value for the option 1 (yes) or 0 (no)' + ) + ) + ), VALUE_DEFAULT, array() + ), + ) + ); + } + + /** + * Imports a course + * + * @param int $importfrom The id of the course we are importing from + * @param int $importto The id of the course we are importing to + * @param bool $deletecontent Whether to delete the course we are importing to content + * @param array $options List of backup options + * @return null + * @since Moodle 2.4 + */ + public static function import_course($importfrom, $importto, $deletecontent = 0, $options = array()) { + global $CFG, $USER, $DB; + require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); + require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + + // Parameter validation. + $params = self::validate_parameters( + self::import_course_parameters(), + array( + 'importfrom' => $importfrom, + 'importto' => $importto, + 'deletecontent' => $deletecontent, + 'options' => $options + ) + ); + + if ($params['deletecontent'] !== 0 and $params['deletecontent'] !== 1) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['deletecontent']); + } + + // Context validation. + + if (! ($importfrom = $DB->get_record('course', array('id'=>$params['importfrom'])))) { + throw new moodle_exception('invalidcourseid', 'error'); + } + + if (! ($importto = $DB->get_record('course', array('id'=>$params['importto'])))) { + throw new moodle_exception('invalidcourseid', 'error'); + } + + $importfromcontext = context_course::instance($importfrom->id); + self::validate_context($importfromcontext); + + $importtocontext = context_course::instance($importto->id); + self::validate_context($importtocontext); + + $backupdefaults = array( + 'activities' => 1, + 'blocks' => 1, + 'filters' => 1 + ); + + $backupsettings = array(); + + // Check for backup and restore options. + if (!empty($params['options'])) { + foreach ($params['options'] as $option) { + + // Strict check for a correct value (allways 1 or 0, true or false). + $value = clean_param($option['value'], PARAM_INT); + + if ($value !== 0 and $value !== 1) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + if (!isset($backupdefaults[$option['name']])) { + throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']); + } + + $backupsettings[$option['name']] = $value; + } + } + + // Capability checking. + + require_capability('moodle/backup:backuptargetimport', $importfromcontext); + require_capability('moodle/restore:restoretargetimport', $importtocontext); + + $bc = new backup_controller(backup::TYPE_1COURSE, $importfrom->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + + foreach ($backupsettings as $name => $value) { + $bc->get_plan()->get_setting($name)->set_value($value); + } + + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + + $bc->execute_plan(); + $bc->destroy(); + + // Restore the backup immediately. + + // Check if we must delete the contents of the destination course. + if ($params['deletecontent']) { + $restoretarget = backup::TARGET_EXISTING_DELETING; + } else { + $restoretarget = backup::TARGET_EXISTING_ADDING; + } + + $rc = new restore_controller($backupid, $importto->id, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, $restoretarget); + + foreach ($backupsettings as $name => $value) { + $rc->get_plan()->get_setting($name)->set_value($value); + } + + if (!$rc->execute_precheck()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + $errorinfo = ''; + + foreach ($precheckresults['errors'] as $error) { + $errorinfo .= $error; + } + + if (array_key_exists('warnings', $precheckresults)) { + foreach ($precheckresults['warnings'] as $warning) { + $errorinfo .= $warning; + } + } + + throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo); + } + } else { + if ($restoretarget == backup::TARGET_EXISTING_DELETING) { + restore_dbops::delete_course_content($importto->id); + } + } + + $rc->execute_plan(); + $rc->destroy(); + + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + return null; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.4 + */ + public static function import_course_returns() { + return null; + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function get_categories_parameters() { + return new external_function_parameters( + array( + 'criteria' => new external_multiple_structure( + new external_single_structure( + array( + 'key' => new external_value(PARAM_ALPHA, + 'The category column to search, expected keys (value format) are:'. + '"id" (int) the category id,'. + '"name" (string) the category name,'. + '"parent" (int) the parent category id,'. + '"idnumber" (string) category idnumber'. + ' - user must have \'moodle/category:manage\' to search on idnumber,'. + '"visible" (int) whether the returned categories must be visible or hidden. If the key is not passed, + then the function return all categories that the user can see.'. + ' - user must have \'moodle/category:manage\' or \'moodle/category:viewhiddencategories\' to search on visible,'. + '"theme" (string) only return the categories having this theme'. + ' - user must have \'moodle/category:manage\' to search on theme'), + 'value' => new external_value(PARAM_RAW, 'the value to match') + ) + ), 'criteria', VALUE_DEFAULT, array() + ), + 'addsubcategories' => new external_value(PARAM_BOOL, 'return the sub categories infos + (1 - default) otherwise only the category info (0)', VALUE_DEFAULT, 1) + ) + ); + } + + /** + * Get categories + * + * @param array $criteria Criteria to match the results + * @param booln $addsubcategories obtain only the category (false) or its subcategories (true - default) + * @return array list of categories + * @since Moodle 2.3 + */ + public static function get_categories($criteria = array(), $addsubcategories = true) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + // Validate parameters. + $params = self::validate_parameters(self::get_categories_parameters(), + array('criteria' => $criteria, 'addsubcategories' => $addsubcategories)); + + // Retrieve the categories. + $categories = array(); + if (!empty($params['criteria'])) { + + $conditions = array(); + $wheres = array(); + foreach ($params['criteria'] as $crit) { + $key = trim($crit['key']); + + // Trying to avoid duplicate keys. + if (!isset($conditions[$key])) { + + $context = context_system::instance(); + $value = null; + switch ($key) { + case 'id': + $value = clean_param($crit['value'], PARAM_INT); + break; + + case 'idnumber': + if (has_capability('moodle/category:manage', $context)) { + $value = clean_param($crit['value'], PARAM_RAW); + } else { + // We must throw an exception. + // Otherwise the dev client would think no idnumber exists. + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "idnumber" field.'); + } + break; + + case 'name': + $value = clean_param($crit['value'], PARAM_TEXT); + break; + + case 'parent': + $value = clean_param($crit['value'], PARAM_INT); + break; + + case 'visible': + if (has_capability('moodle/category:manage', $context) + or has_capability('moodle/category:viewhiddencategories', + context_system::instance())) { + $value = clean_param($crit['value'], PARAM_INT); + } else { + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "visible" field.'); + } + break; + + case 'theme': + if (has_capability('moodle/category:manage', $context)) { + $value = clean_param($crit['value'], PARAM_THEME); + } else { + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You don\'t have the permissions to search on the "theme" field.'); + } + break; + + default: + throw new moodle_exception('criteriaerror', + 'webservice', '', null, + 'You can not search on this criteria: ' . $key); + } + + if (isset($value)) { + $conditions[$key] = $crit['value']; + $wheres[] = $key . " = :" . $key; + } + } + } + + if (!empty($wheres)) { + $wheres = implode(" AND ", $wheres); + + $categories = $DB->get_records_select('course_categories', $wheres, $conditions); + + // Retrieve its sub subcategories (all levels). + if ($categories and !empty($params['addsubcategories'])) { + $newcategories = array(); + + // Check if we required visible/theme checks. + $additionalselect = ''; + $additionalparams = array(); + if (isset($conditions['visible'])) { + $additionalselect .= ' AND visible = :visible'; + $additionalparams['visible'] = $conditions['visible']; + } + if (isset($conditions['theme'])) { + $additionalselect .= ' AND theme= :theme'; + $additionalparams['theme'] = $conditions['theme']; + } + + foreach ($categories as $category) { + $sqlselect = $DB->sql_like('path', ':path') . $additionalselect; + $sqlparams = array('path' => $category->path.'/%') + $additionalparams; // It will NOT include the specified category. + $subcategories = $DB->get_records_select('course_categories', $sqlselect, $sqlparams); + $newcategories = $newcategories + $subcategories; // Both arrays have integer as keys. + } + $categories = $categories + $newcategories; + } + } + + } else { + // Retrieve all categories in the database. + $categories = $DB->get_records('course_categories'); + } + + // The not returned categories. key => category id, value => reason of exclusion. + $excludedcats = array(); + + // The returned categories. + $categoriesinfo = array(); + + // We need to sort the categories by path. + // The parent cats need to be checked by the algo first. + usort($categories, "core_course_external::compare_categories_by_path"); + + foreach ($categories as $category) { + + // Check if the category is a child of an excluded category, if yes exclude it too (excluded => do not return). + $parents = explode('/', $category->path); + unset($parents[0]); // First key is always empty because path start with / => /1/2/4. + foreach ($parents as $parentid) { + // Note: when the parent exclusion was due to the context, + // the sub category could still be returned. + if (isset($excludedcats[$parentid]) and $excludedcats[$parentid] != 'context') { + $excludedcats[$category->id] = 'parent'; + } + } + + // Check category depth is <= maxdepth (do not check for user who can manage categories). + if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth) + and !has_capability('moodle/category:manage', $context)) { + $excludedcats[$category->id] = 'depth'; + } + + // Check the user can use the category context. + $context = context_coursecat::instance($category->id); + try { + self::validate_context($context); + } catch (Exception $e) { + $excludedcats[$category->id] = 'context'; + + // If it was the requested category then throw an exception. + if (isset($params['categoryid']) && $category->id == $params['categoryid']) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->catid = $category->id; + throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam); + } + } + + // Return the category information. + if (!isset($excludedcats[$category->id])) { + + // Final check to see if the category is visible to the user. + if ($category->visible + or has_capability('moodle/category:viewhiddencategories', context_system::instance()) + or has_capability('moodle/category:manage', $context)) { + + $categoryinfo = array(); + $categoryinfo['id'] = $category->id; + $categoryinfo['name'] = $category->name; + list($categoryinfo['description'], $categoryinfo['descriptionformat']) = + external_format_text($category->description, $category->descriptionformat, + $context->id, 'coursecat', 'description', null); + $categoryinfo['parent'] = $category->parent; + $categoryinfo['sortorder'] = $category->sortorder; + $categoryinfo['coursecount'] = $category->coursecount; + $categoryinfo['depth'] = $category->depth; + $categoryinfo['path'] = $category->path; + + // Some fields only returned for admin. + if (has_capability('moodle/category:manage', $context)) { + $categoryinfo['idnumber'] = $category->idnumber; + $categoryinfo['visible'] = $category->visible; + $categoryinfo['visibleold'] = $category->visibleold; + $categoryinfo['timemodified'] = $category->timemodified; + $categoryinfo['theme'] = $category->theme; + } + + $categoriesinfo[] = $categoryinfo; + } else { + $excludedcats[$category->id] = 'visibility'; + } + } + } + + // Sorting the resulting array so it looks a bit better for the client developer. + usort($categoriesinfo, "core_course_external::compare_categories_by_sortorder"); + + return $categoriesinfo; + } + + /** + * Sort categories array by path + * private function: only used by get_categories + * + * @param array $category1 + * @param array $category2 + * @return int result of strcmp + * @since Moodle 2.3 + */ + private static function compare_categories_by_path($category1, $category2) { + return strcmp($category1->path, $category2->path); + } + + /** + * Sort categories array by sortorder + * private function: only used by get_categories + * + * @param array $category1 + * @param array $category2 + * @return int result of strcmp + * @since Moodle 2.3 + */ + private static function compare_categories_by_sortorder($category1, $category2) { + return strcmp($category1['sortorder'], $category2['sortorder']); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.3 + */ + public static function get_categories_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'category id'), + 'name' => new external_value(PARAM_TEXT, 'category name'), + 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), + 'description' => new external_value(PARAM_RAW, 'category description'), + 'descriptionformat' => new external_format_value('description'), + 'parent' => new external_value(PARAM_INT, 'parent category id'), + 'sortorder' => new external_value(PARAM_INT, 'category sorting order'), + 'coursecount' => new external_value(PARAM_INT, 'number of courses in this category'), + 'visible' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), + 'visibleold' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL), + 'timemodified' => new external_value(PARAM_INT, 'timestamp', VALUE_OPTIONAL), + 'depth' => new external_value(PARAM_INT, 'category depth'), + 'path' => new external_value(PARAM_TEXT, 'category path'), + 'theme' => new external_value(PARAM_THEME, 'category theme', VALUE_OPTIONAL), + ), 'List of categories' + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function create_categories_parameters() { + return new external_function_parameters( + array( + 'categories' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_TEXT, 'new category name'), + 'parent' => new external_value(PARAM_INT, + 'the parent category id inside which the new category will be created + - set to 0 for a root category', + VALUE_DEFAULT, 0), + 'idnumber' => new external_value(PARAM_RAW, + 'the new category idnumber', VALUE_OPTIONAL), + 'description' => new external_value(PARAM_RAW, + 'the new category description', VALUE_OPTIONAL), + 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), + 'theme' => new external_value(PARAM_THEME, + 'the new category theme. This option must be enabled on moodle', + VALUE_OPTIONAL), + ) + ) + ) + ) + ); + } + + /** + * Create categories + * + * @param array $categories - see create_categories_parameters() for the array structure + * @return array - see create_categories_returns() for the array structure + * @since Moodle 2.3 + */ + public static function create_categories($categories) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + $params = self::validate_parameters(self::create_categories_parameters(), + array('categories' => $categories)); + + $transaction = $DB->start_delegated_transaction(); + + $createdcategories = array(); + foreach ($params['categories'] as $category) { + if ($category['parent']) { + if (!$DB->record_exists('course_categories', array('id' => $category['parent']))) { + throw new moodle_exception('unknowcategory'); + } + $context = context_coursecat::instance($category['parent']); + } else { + $context = context_system::instance(); + } + self::validate_context($context); + require_capability('moodle/category:manage', $context); + + // Check name. + if (textlib::strlen($category['name'])>255) { + throw new moodle_exception('categorytoolong'); + } + + $newcategory = new stdClass(); + $newcategory->name = $category['name']; + $newcategory->parent = $category['parent']; + $newcategory->sortorder = 999; // Same as in the course/editcategory.php . + // Format the description. + if (!empty($category['description'])) { + $newcategory->description = $category['description']; + } + $newcategory->descriptionformat = external_validate_format($category['descriptionformat']); + if (isset($category['theme']) and !empty($CFG->allowcategorythemes)) { + $newcategory->theme = $category['theme']; + } + // Check id number. + if (!empty($category['idnumber'])) { // Same as in course/editcategory_form.php . + if (textlib::strlen($category['idnumber'])>100) { + throw new moodle_exception('idnumbertoolong'); + } + if ($existing = $DB->get_record('course_categories', array('idnumber' => $category['idnumber']))) { + if ($existing->id) { + throw new moodle_exception('idnumbertaken'); + } + } + $newcategory->idnumber = $category['idnumber']; + } + + $newcategory = create_course_category($newcategory); + // Populate special fields. + fix_course_sortorder(); + + $createdcategories[] = array('id' => $newcategory->id, 'name' => $newcategory->name); + } + + $transaction->allow_commit(); + + return $createdcategories; + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function create_categories_returns() { + return new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'new category id'), + 'name' => new external_value(PARAM_TEXT, 'new category name'), + ) + ) + ); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function update_categories_parameters() { + return new external_function_parameters( + array( + 'categories' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'course id'), + 'name' => new external_value(PARAM_TEXT, 'category name', VALUE_OPTIONAL), + 'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL), + 'parent' => new external_value(PARAM_INT, 'parent category id', VALUE_OPTIONAL), + 'description' => new external_value(PARAM_RAW, 'category description', VALUE_OPTIONAL), + 'descriptionformat' => new external_format_value('description', VALUE_DEFAULT), + 'theme' => new external_value(PARAM_THEME, + 'the category theme. This option must be enabled on moodle', VALUE_OPTIONAL), + ) + ) + ) + ) + ); + } + + /** + * Update categories + * + * @param array $categories The list of categories to update + * @return null + * @since Moodle 2.3 + */ + public static function update_categories($categories) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + // Validate parameters. + $params = self::validate_parameters(self::update_categories_parameters(), array('categories' => $categories)); + + $transaction = $DB->start_delegated_transaction(); + + foreach ($params['categories'] as $cat) { + if (!$category = $DB->get_record('course_categories', array('id' => $cat['id']))) { + throw new moodle_exception('unknowcategory'); + } + + $categorycontext = context_coursecat::instance($cat['id']); + self::validate_context($categorycontext); + require_capability('moodle/category:manage', $categorycontext); + + if (!empty($cat['name'])) { + if (textlib::strlen($cat['name'])>255) { + throw new moodle_exception('categorytoolong'); + } + $category->name = $cat['name']; + } + if (!empty($cat['idnumber'])) { + if (textlib::strlen($cat['idnumber'])>100) { + throw new moodle_exception('idnumbertoolong'); + } + $category->idnumber = $cat['idnumber']; + } + if (!empty($cat['description'])) { + $category->description = $cat['description']; + $category->descriptionformat = external_validate_format($cat['descriptionformat']); + } + if (!empty($cat['theme'])) { + $category->theme = $cat['theme']; + } + if (!empty($cat['parent']) && ($category->parent != $cat['parent'])) { + // First check if parent exists. + if (!$parent_cat = $DB->get_record('course_categories', array('id' => $cat['parent']))) { + throw new moodle_exception('unknowcategory'); + } + // Then check if we have capability. + self::validate_context(get_category_or_system_context((int)$cat['parent'])); + require_capability('moodle/category:manage', get_category_or_system_context((int)$cat['parent'])); + // Finally move the category. + move_category($category, $parent_cat); + $category->parent = $cat['parent']; + // Get updated path by move_category(). + $category->path = $DB->get_field('course_categories', 'path', + array('id' => $category->id)); + } + $DB->update_record('course_categories', $category); + } + + $transaction->allow_commit(); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.3 + */ + public static function update_categories_returns() { + return null; + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function delete_categories_parameters() { + return new external_function_parameters( + array( + 'categories' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_INT, 'category id to delete'), + 'newparent' => new external_value(PARAM_INT, + 'the parent category to move the contents to, if specified', VALUE_OPTIONAL), + 'recursive' => new external_value(PARAM_BOOL, '1: recursively delete all contents inside this + category, 0 (default): move contents to newparent or current parent category (except if parent is root)', VALUE_DEFAULT, 0) + ) + ) + ) + ) + ); + } + + /** + * Delete categories + * + * @param array $categories A list of category ids + * @return array + * @since Moodle 2.3 + */ + public static function delete_categories($categories) { + global $CFG, $DB; + require_once($CFG->dirroot . "/course/lib.php"); + + // Validate parameters. + $params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories)); + + $transaction = $DB->start_delegated_transaction(); + + foreach ($params['categories'] as $category) { + if (!$deletecat = $DB->get_record('course_categories', array('id' => $category['id']))) { + throw new moodle_exception('unknowcategory'); + } + $context = context_coursecat::instance($deletecat->id); + require_capability('moodle/category:manage', $context); + self::validate_context($context); + self::validate_context(get_category_or_system_context($deletecat->parent)); + + if ($category['recursive']) { + // If recursive was specified, then we recursively delete the category's contents. + category_delete_full($deletecat, false); + } else { + // In this situation, we don't delete the category's contents, we either move it to newparent or parent. + // If the parent is the root, moving is not supported (because a course must always be inside a category). + // We must move to an existing category. + if (!empty($category['newparent'])) { + if (!$DB->record_exists('course_categories', array('id' => $category['newparent']))) { + throw new moodle_exception('unknowcategory'); + } + $newparent = $category['newparent']; + } else { + $newparent = $deletecat->parent; + } + + // This operation is not allowed. We must move contents to an existing category. + if ($newparent == 0) { + throw new moodle_exception('movecatcontentstoroot'); + } + + $parentcontext = get_category_or_system_context($newparent); + require_capability('moodle/category:manage', $parentcontext); + self::validate_context($parentcontext); + category_delete_move($deletecat, $newparent, false); + } + } + + $transaction->allow_commit(); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.3 + */ + public static function delete_categories_returns() { + return null; + } + +} + +/** + * Deprecated course external functions + * + * @package core_course + * @copyright 2009 Petr Skodak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not use this class any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external + */ +class moodle_course_external extends external_api { + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::get_courses_parameters() + */ + public static function get_courses_parameters() { + return core_course_external::get_courses_parameters(); + } + + /** + * Get courses + * + * @param array $options + * @return array + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::get_courses() + */ + public static function get_courses($options) { + return core_course_external::get_courses($options); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::get_courses_returns() + */ + public static function get_courses_returns() { + return core_course_external::get_courses_returns(); + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::create_courses_parameters() + */ + public static function create_courses_parameters() { + return core_course_external::create_courses_parameters(); + } + + /** + * Create courses + * + * @param array $courses + * @return array courses (id and shortname only) + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::create_courses() + */ + public static function create_courses($courses) { + return core_course_external::create_courses($courses); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.0 + * @deprecated Moodle 2.2 MDL-29106 - Please do not call this function any more. + * @todo MDL-31194 This will be deleted in Moodle 2.5. + * @see core_course_external::create_courses_returns() + */ + public static function create_courses_returns() { + return core_course_external::create_courses_returns(); + } + +} diff --git a/format/README.txt b/format/README.txt new file mode 100644 index 0000000..9bb62ab --- /dev/null +++ b/format/README.txt @@ -0,0 +1,146 @@ +Course formats +============== + +To create a new course format, make another folder in here. + +If you want a basic format, you only need to write the 'standard files' listed +below. + +If you want to store information in the database for your format, or control +access to features of your format, you need some of the optional files too. + +All names below assume that your format is called 'yourformat'. + + +Standard files +-------------- + +* yourformat/format.php + + Code that actually displays the course view page. See existing formats for + examples. + +* yourformat/config.php + + Configuration file, mainly controlling default blocks for the format. + See existing formats for examples. + +* yourformat/lang/en/format_yourformat.php + + Language file containing basic language strings for your format. Here + is a minimal language file: + + + + The first string is used in the dropdown menu of course settings. The second + is used when editing an activity within a course of your format. + + Note that existing formats store their language strings in the main + moodle.php, which you can also do, but this separate file is recommended + for contributed formats. + + You can also store other strings in this file if you wish. They can be + accessed as follows, for example to get the section name: + + get_string('nameyourformat','format_yourformat'); + + Of course you can have other folders as well as just English if you want + to provide multiple languages. + + +Optional files (database access) +-------------------------------- + +If these files exist, Moodle will use them to set up database tables when you +visit the admin page. + +* yourformat/db/install.xml + + Database table definitions. Use your format name at the start of the table + names to increase the chance that they are unique. + +* yourformat/db/upgrade.php + + Database upgrade instructions. Similar to other upgrade.php files, so look + at those for modules etc. if you want to see. + + The function must look like: + + function xmldb_format_yourformat_upgrade($oldversion) { + ... + +* yourformat/version.php + + Required if you use database tables. + + version = 2006120100; // Plugin version (update when tables change) + $plugin->requires = 2006092801; // Required Moodle version + ?> + + +Optional files (backup) +----------------------- + +If these files exist, backup and restore run automatically when backing up +the course. You can't back up the course format data independently. + +* yourformat/backuplib.php + + Similar to backup code for other plugins. Must have a function: + + function yourformat_backup_format_data($bf,$preferences) { + ... + +* yourformat/restorelib.php + + Similar to restore code for other plugins. Must have a function: + + function yourformat_restore_format_data($restore,$data) { + ... + + ($data is the xmlized data underneath FORMATDATA in the backup XML file. + Do print_object($data); while testing to see how it looks.) + + +Optional file (capabilities) +---------------------------- + +If this file exists, Moodle refreshes your format's capabilities +(checks that they are all included in the database) whenever you increase +the version in yourformat/version.php. + +* yourformat/db/access.php + + Contains capability entries similar to other access.php files. + + The array definition must look like: + + $format_yourformat_capabilities = array( + ... + + Format names must look like: + + format/yourformat:specialpower + + Capability definitions in your language file must look like: + + $string['yourformat:specialpower']='Revolutionise the world'; + + + +Optional file (styles) +---------------------- + +* yourformat/styles.php + + If this file exists it will be included in the CSS Moodle generates. + + +Optional delete course hook +--------------------------- + +* in your yourformat/lib.php add function format_yourformat_delete_course($courseid) \ No newline at end of file diff --git a/format/formatlegacy.php b/format/formatlegacy.php new file mode 100644 index 0000000..f353a4b --- /dev/null +++ b/format/formatlegacy.php @@ -0,0 +1,363 @@ +. + +/** + * Course format class to allow plugins developed for Moodle 2.3 to work in the new API + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Course format class to allow plugins developed for Moodle 2.3 to work in the new API + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_legacy extends format_base { + + /** + * Returns true if this course format uses sections + * + * This function calls function callback_FORMATNAME_uses_sections() if it exists + * + * @return bool + */ + public function uses_sections() { + global $CFG; + // Note that lib.php in course format folder is already included by now + $featurefunction = 'callback_'.$this->format.'_uses_sections'; + if (function_exists($featurefunction)) { + return $featurefunction(); + } + return false; + } + + /** + * Returns the display name of the given section that the course prefers. + * + * This function calls function callback_FORMATNAME_get_section_name() if it exists + * + * @param int|stdClass $section Section object from database or just field section.section + * @return string Display name that the course format prefers, e.g. "Topic 2" + */ + public function get_section_name($section) { + // Use course formatter callback if it exists + $namingfunction = 'callback_'.$this->format.'_get_section_name'; + if (function_exists($namingfunction) && ($course = $this->get_course())) { + return $namingfunction($course, $this->get_section($section)); + } + + // else, default behavior: + return parent::get_section_name($section); + } + + /** + * The URL to use for the specified course (with section) + * + * This function calls function callback_FORMATNAME_get_section_url() if it exists + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if omitted the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + // Use course formatter callback if it exists + $featurefunction = 'callback_'.$this->format.'_get_section_url'; + if (function_exists($featurefunction) && ($course = $this->get_course())) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + if ($sectionnum) { + $url = $featurefunction($course, $sectionnum); + if ($url || !empty($options['navigation'])) { + return $url; + } + } + } + + // if function is not defined + if (!$this->uses_sections() || + !array_key_exists('coursedisplay', $this->course_format_options())) { + // default behaviour + return parent::get_view_url($section, $options); + } + + $course = $this->get_course(); + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + + $sr = null; + if (array_key_exists('sr', $options)) { + $sr = $options['sr']; + } + if (is_object($section)) { + $sectionno = $section->section; + } else { + $sectionno = $section; + } + if ($sectionno !== null) { + if ($sr !== null) { + if ($sr) { + $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE; + $sectionno = $sr; + } else { + $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; + } + } else { + $usercoursedisplay = $course->coursedisplay; + } + if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { + $url->param('section', $sectionno); + } else { + if (!empty($options['navigation'])) { + return null; + } + $url->set_anchor('section-'.$sectionno); + } + } + return $url; + } + + /** + * Returns the information about the ajax support in the given source format + * + * This function calls function callback_FORMATNAME_ajax_support() if it exists + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * The property (array)testedbrowsers can be used as a parameter for {@link ajaxenabled()}. + * + * @return stdClass + */ + public function supports_ajax() { + // set up default values + $ajaxsupport = parent::supports_ajax(); + + // get the information from the course format library + $featurefunction = 'callback_'.$this->format.'_ajax_support'; + if (function_exists($featurefunction)) { + $formatsupport = $featurefunction(); + if (isset($formatsupport->capable)) { + $ajaxsupport->capable = $formatsupport->capable; + } + if (is_array($formatsupport->testedbrowsers)) { + $ajaxsupport->testedbrowsers = $formatsupport->testedbrowsers; + } + } + return $ajaxsupport; + } + + /** + * Loads all of the course sections into the navigation + * + * First this function calls callback_FORMATNAME_display_content() if it exists to check + * if the navigation should be extended at all + * + * Then it calls function callback_FORMATNAME_load_content() if it exists to actually extend + * navigation + * + * By default the parent method is called + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + global $PAGE; + // if course format displays section on separate pages and we are on course/view.php page + // and the section parameter is specified, make sure this section is expanded in + // navigation + if ($navigation->includesectionnum === false) { + $selectedsection = optional_param('section', null, PARAM_INT); + if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && + $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { + $navigation->includesectionnum = $selectedsection; + } + } + + // check if there are callbacks to extend course navigation + $displayfunc = 'callback_'.$this->format.'_display_content'; + if (function_exists($displayfunc) && !$displayfunc()) { + return; + } + $featurefunction = 'callback_'.$this->format.'_load_content'; + if (function_exists($featurefunction) && ($course = $this->get_course())) { + $featurefunction($navigation, $course, $node); + } else { + parent::extend_course_navigation($navigation, $node); + } + } + + /** + * Custom action after section has been moved in AJAX mode + * + * Used in course/rest.php + * + * This function calls function callback_FORMATNAME_ajax_section_move() if it exists + * + * @return array This will be passed in ajax respose + */ + function ajax_section_move() { + $featurefunction = 'callback_'.$this->format.'_ajax_section_move'; + if (function_exists($featurefunction) && ($course = $this->get_course())) { + return $featurefunction($course); + } else { + return parent::ajax_section_move(); + } + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * This function checks the existence of the file config.php in the course format folder. + * If file exists and contains the code + * $format['defaultblocks'] = 'leftblock1,leftblock2:rightblock1,rightblock2'; + * these blocks are used, otherwise parent function is called + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + global $CFG; + $formatconfig = $CFG->dirroot.'/course/format/'.$this->format.'/config.php'; + $format = array(); // initialize array in external file + if (is_readable($formatconfig)) { + include($formatconfig); + } + if (!empty($format['defaultblocks'])) { + return blocks_parse_default_blocks_list($format['defaultblocks']); + } + return parent::get_default_blocks(); + } + + /** + * Definitions of the additional options that this course format uses for course + * + * By default course formats have the options that existed in Moodle 2.3: + * - coursedisplay + * - numsections + * - hiddensections + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + static $courseformatoptions = false; + if ($courseformatoptions === false) { + $courseconfig = get_config('moodlecourse'); + $courseformatoptions = array( + 'numsections' => array( + 'default' => $courseconfig->numsections, + 'type' => PARAM_INT, + ), + 'hiddensections' => array( + 'default' => $courseconfig->hiddensections, + 'type' => PARAM_INT, + ), + 'coursedisplay' => array( + 'default' => $courseconfig->coursedisplay, + 'type' => PARAM_INT, + ), + ); + } + if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { + $courseconfig = get_config('moodlecourse'); + $sectionmenu = array(); + for ($i = 0; $i <= $courseconfig->maxsections; $i++) { + $sectionmenu[$i] = "$i"; + } + $courseformatoptionsedit = array( + 'numsections' => array( + 'label' => new lang_string('numberweeks'), + 'element_type' => 'select', + 'element_attributes' => array($sectionmenu), + ), + 'hiddensections' => array( + 'label' => new lang_string('hiddensections'), + 'help' => 'hiddensections', + 'help_component' => 'moodle', + 'element_type' => 'select', + 'element_attributes' => array( + array( + 0 => new lang_string('hiddensectionscollapsed'), + 1 => new lang_string('hiddensectionsinvisible') + ) + ), + ), + 'coursedisplay' => array( + 'label' => new lang_string('coursedisplay'), + 'element_type' => 'select', + 'element_attributes' => array( + array( + COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), + COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi') + ) + ), + 'help' => 'coursedisplay', + 'help_component' => 'moodle', + ) + ); + $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); + } + return $courseformatoptions; + } + + /** + * Updates format options for a course + * + * Legacy course formats may assume that course format options + * ('coursedisplay', 'numsections' and 'hiddensections') are shared between formats. + * Therefore we make sure to copy them from the previous format + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @param stdClass $oldcourse if this function is called from {@link update_course()} + * this object contains information about the course before update + * @return bool whether there were any changes to the options values + */ + public function update_course_format_options($data, $oldcourse = null) { + global $DB; + if ($oldcourse !== null) { + $data = (array)$data; + $oldcourse = (array)$oldcourse; + $options = $this->course_format_options(); + foreach ($options as $key => $unused) { + if (!array_key_exists($key, $data)) { + if (array_key_exists($key, $oldcourse)) { + $data[$key] = $oldcourse[$key]; + } else if ($key === 'numsections') { + // If previous format does not have the field 'numsections' and this one does, + // and $data['numsections'] is not set fill it with the maximum section number from the DB + $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections} + WHERE course = ?', array($this->courseid)); + if ($maxsection) { + // If there are no sections, or just default 0-section, 'numsections' will be set to default + $data['numsections'] = $maxsection; + } + } + } + } + } + return $this->update_format_options($data); + } +} \ No newline at end of file diff --git a/format/lib.php b/format/lib.php new file mode 100644 index 0000000..f0ccf9b --- /dev/null +++ b/format/lib.php @@ -0,0 +1,941 @@ +. + +/** + * Base class for course format plugins + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +/** + * Returns an instance of format class (extending format_base) for given course + * + * @param int|stdClass $courseorid either course id or + * an object that has the property 'format' and may contain property 'id' + * @return format_base + */ +function course_get_format($courseorid) { + return format_base::instance($courseorid); +} + +/** + * Base class for course formats + * + * Each course format must declare class + * class format_FORMATNAME extends format_base {} + * in file lib.php + * + * For each course just one instance of this class is created and it will always be returned by + * course_get_format($courseorid). Format may store it's specific course-dependent options in + * variables of this class. + * + * In rare cases instance of child class may be created just for format without course id + * i.e. to check if format supports AJAX. + * + * Also course formats may extend class section_info and overwrite + * format_base::build_section_cache() to return more information about sections. + * + * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming + * it to format_FORMATNAME, then move the code from your callback functions into + * appropriate functions of the class. + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class format_base { + /** @var int Id of the course in this instance (maybe 0) */ + protected $courseid; + /** @var string format used for this course. Please note that it can be different from + * course.format field if course referes to non-existing of disabled format */ + protected $format; + /** @var stdClass data for course object, please use {@link format_base::get_course()} */ + protected $course = false; + /** @var array caches format options, please use {@link format_base::get_format_options()} */ + protected $formatoptions = array(); + /** @var array cached instances */ + private static $instances = array(); + + /** + * Creates a new instance of class + * + * Please use {@link course_get_format($courseorid)} to get an instance of the format class + * + * @param string $format + * @param int $courseid + * @return format_base + */ + protected function __construct($format, $courseid) { + $this->format = $format; + $this->courseid = $courseid; + } + + /** + * Validates that course format exists and enabled and returns either itself or default format + * + * @param string $format + * @return string + */ + protected static final function get_format_or_default($format) { + if ($format === 'site') { + return $format; + } + $plugins = get_sorted_course_formats(); + if (in_array($format, $plugins)) { + return $format; + } + // Else return default format + $defaultformat = get_config('moodlecourse', 'format'); + if (!in_array($defaultformat, $plugins)) { + // when default format is not set correctly, use the first available format + $defaultformat = reset($plugins); + } + static $warningprinted = array(); + if (empty($warningprinted[$format])) { + debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER); + $warningprinted[$format] = true; + } + return $defaultformat; + } + + /** + * Get class name for the format + * + * If course format xxx does not declare class format_xxx, format_legacy will be returned. + * This function also includes lib.php file from corresponding format plugin + * + * @param string $format + * @return string + */ + protected static final function get_class_name($format) { + global $CFG; + static $classnames = array('site' => 'format_site'); + if (!isset($classnames[$format])) { + $plugins = get_plugin_list('format'); + $usedformat = self::get_format_or_default($format); + if (file_exists($plugins[$usedformat].'/lib.php')) { + require_once($plugins[$usedformat].'/lib.php'); + } + $classnames[$format] = 'format_'. $usedformat; + if (!class_exists($classnames[$format])) { + require_once($CFG->dirroot.'/course/format/formatlegacy.php'); + $classnames[$format] = 'format_legacy'; + } + } + return $classnames[$format]; + } + + /** + * Returns an instance of the class + * + * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances + * + * @param int|stdClass $courseorid either course id or + * an object that has the property 'format' and may contain property 'id' + * @return format_base + */ + public static final function instance($courseorid) { + global $DB; + if (!is_object($courseorid)) { + $courseid = (int)$courseorid; + if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) { + $formats = array_keys(self::$instances[$courseid]); + $format = reset($formats); + } else { + $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST); + } + } else { + $format = $courseorid->format; + if (isset($courseorid->id)) { + $courseid = clean_param($courseorid->id, PARAM_INT); + } else { + $courseid = 0; + } + } + // validate that format exists and enabled, use default otherwise + $format = self::get_format_or_default($format); + if (!isset(self::$instances[$courseid][$format])) { + $classname = self::get_class_name($format); + self::$instances[$courseid][$format] = new $classname($format, $courseid); + } + return self::$instances[$courseid][$format]; + } + + /** + * Resets cache for the course (or all caches) + * To be called from {@link rebuild_course_cache()} + * + * @param int $courseid + */ + public static final function reset_course_cache($courseid = 0) { + if ($courseid) { + if (isset(self::$instances[$courseid])) { + foreach (self::$instances[$courseid] as $format => $object) { + // in case somebody keeps the reference to course format object + self::$instances[$courseid][$format]->course = false; + self::$instances[$courseid][$format]->formatoptions = array(); + } + unset(self::$instances[$courseid]); + } + } else { + self::$instances = array(); + } + } + + /** + * Returns the format name used by this course + * + * @return string + */ + public final function get_format() { + return $this->format; + } + + /** + * Returns id of the course (0 if course is not specified) + * + * @return int + */ + public final function get_courseid() { + return $this->courseid; + } + + /** + * Returns a record from course database table plus additional fields + * that course format defines + * + * @return stdClass + */ + public function get_course() { + global $DB; + if (!$this->courseid) { + return null; + } + if ($this->course === false) { + $this->course = $DB->get_record('course', array('id' => $this->courseid)); + $options = $this->get_format_options(); + foreach ($options as $optionname => $optionvalue) { + if (!isset($this->course->$optionname)) { + $this->course->$optionname = $optionvalue; + } else { + debugging('The option name '.$optionname.' in course format '.$this->format. + ' is invalid because the field with the same name exists in {course} table', + DEBUG_DEVELOPER); + } + } + } + return $this->course; + } + + /** + * Returns true if this course format uses sections + * + * This function may be called without specifying the course id + * i.e. in {@link course_format_uses_sections()} + * + * Developers, note that if course format does use sections there should be defined a language + * string with the name 'sectionname' defining what the section relates to in the format, i.e. + * $string['sectionname'] = 'Topic'; + * or + * $string['sectionname'] = 'Week'; + * + * @return bool + */ + public function uses_sections() { + return false; + } + + /** + * Returns a list of sections used in the course + * + * This is a shortcut to get_fast_modinfo()->get_section_info_all() + * @see get_fast_modinfo() + * @see course_modinfo::get_section_info_all() + * + * @return array of section_info objects + */ + public final function get_sections() { + if ($course = $this->get_course()) { + $modinfo = get_fast_modinfo($course); + return $modinfo->get_section_info_all(); + } + return array(); + } + + /** + * Returns information about section used in course + * + * @param int|stdClass $section either section number (field course_section.section) or row from course_section table + * @param int $strictness + * @return section_info + */ + public final function get_section($section, $strictness = IGNORE_MISSING) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + $sections = $this->get_sections(); + if (array_key_exists($sectionnum, $sections)) { + return $sections[$sectionnum]; + } + if ($strictness == MUST_EXIST) { + throw new moodle_exception('sectionnotexist'); + } + return null; + } + + /** + * Returns the display name of the given section that the course prefers. + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * @return Display name that the course format prefers, e.g. "Topic 2" + */ + public function get_section_name($section) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + return get_string('sectionname', 'format_'.$this->format) . ' ' . $sectionnum; + } + + /** + * Returns the information about the ajax support in the given source format + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * The property (array)testedbrowsers can be used as a parameter for {@link ajaxenabled()}. + * + * @return stdClass + */ + public function supports_ajax() { + // no support by default + $ajaxsupport = new stdClass(); + $ajaxsupport->capable = false; + $ajaxsupport->testedbrowsers = array(); + return $ajaxsupport; + } + + /** + * Custom action after section has been moved in AJAX mode + * + * Used in course/rest.php + * + * @return array This will be passed in ajax respose + */ + public function ajax_section_move() { + return null; + } + + /** + * The URL to use for the specified course (with section) + * + * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many + * places in core and contributed modules. If course format wants to change the location + * of the view script, it is not enough to change just this function. Do not forget + * to add proper redirection. + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if null the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + $course = $this->get_course(); + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + + if (array_key_exists('sr', $options)) { + $sectionno = $options['sr']; + } else if (is_object($section)) { + $sectionno = $section->section; + } else { + $sectionno = $section; + } + if (!empty($options['navigation']) && $sectionno !== null) { + // by default assume that sections are never displayed on separate pages + return null; + } + if ($this->uses_sections() && $sectionno !== null) { + $url->set_anchor('section-'.$sectionno); + } + return $url; + } + + /** + * Loads all of the course sections into the navigation + * + * This method is called from {@link global_navigation::load_course_sections()} + * + * By default the method {@link global_navigation::load_generic_course_sections()} is called + * + * When overwriting please note that navigationlib relies on using the correct values for + * arguments $type and $key in {@link navigation_node::add()} + * + * Example of code creating a section node: + * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id); + * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; + * + * Example of code creating an activity node: + * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); + * if (global_navigation::module_extends_navigation($activity->modname)) { + * $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; + * } else { + * $activitynode->nodetype = navigation_node::NODETYPE_LEAF; + * } + * + * Also note that if $navigation->includesectionnum is not null, the section with this relative + * number needs is expected to be loaded + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + if ($course = $this->get_course()) { + $navigation->load_generic_course_sections($course, $node); + } + return array(); + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @see blocks_add_default_course_blocks() + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + global $CFG; + if (!empty($CFG->defaultblocks)){ + return blocks_parse_default_blocks_list($CFG->defaultblocks); + } + $blocknames = array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array('search_forums', 'news_items', 'calendar_upcoming', 'recent_activity') + ); + return $blocknames; + } + + /** + * Returns the localised name of this course format plugin + * + * @return lang_string + */ + public final function get_format_name() { + return new lang_string('pluginname', 'format_'.$this->get_format()); + } + + /** + * Definitions of the additional options that this course format uses for course + * + * This function may be called often, it should be as fast as possible. + * Avoid using get_string() method, use "new lang_string()" instead + * It is not recommended to use dynamic or course-dependant expressions here + * This function may be also called when course does not exist yet. + * + * Option names must be different from fields in the {course} talbe or any form elements on + * course edit form, it may even make sence to use special prefix for them. + * + * Each option must have the option name as a key and the array of properties as a value: + * 'default' - default value for this option (assumed null if not specified) + * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.) + * + * Additional properties used by default implementation of + * {@link format_base::create_edit_form_elements()} (calls this method with $foreditform = true) + * 'label' - localised human-readable label for the edit form + * 'element_type' - type of the form element, default 'text' + * 'element_attributes' - additional attributes for the form element, these are 4th and further + * arguments in the moodleform::addElement() method + * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with + * the name 'myoption_help' must exist in the language file + * 'help_component' - language component to look for help string, by default this the component + * for this course format + * + * This is an interface for creating simple form elements. If format plugin wants to use other + * methods such as disableIf, it can be done by overriding create_edit_form_elements(). + * + * Course format options can be accessed as: + * $this->get_course()->OPTIONNAME (inside the format class) + * course_get_format($course)->get_course()->OPTIONNAME (outside of format class) + * + * All course options are returned by calling: + * $this->get_format_options(); + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + return array(); + } + + /** + * Definitions of the additional options that this course format uses for section + * + * See {@link format_base::course_format_options()} for return array definition. + * + * Additionally section format options may have property 'cache' set to true + * if this option needs to be cached in {@link get_fast_modinfo()}. The 'cache' property + * is recommended to be set only for fields used in {@link format_base::get_section_name()}, + * {@link format_base::extend_course_navigation()} and {@link format_base::get_view_url()} + * + * For better performance cached options are recommended to have 'cachedefault' property + * Unlike 'default', 'cachedefault' should be static and not access get_config(). + * + * Regardless of value of 'cache' all options are accessed in the code as + * $sectioninfo->OPTIONNAME + * where $sectioninfo is instance of section_info, returned by + * get_fast_modinfo($course)->get_section_info($sectionnum) + * or get_fast_modinfo($course)->get_section_info_all() + * + * All format options for particular section are returned by calling: + * $this->get_format_options($section); + * + * @param bool $foreditform + * @return array + */ + public function section_format_options($foreditform = false) { + return array(); + } + + /** + * Returns the format options stored for this course or course section + * + * When overriding please note that this function is called from rebuild_course_cache() + * and section_info object, therefore using of get_fast_modinfo() and/or any function that + * accesses it may lead to recursion. + * + * @param null|int|stdClass|section_info $section if null the course format options will be returned + * otherwise options for specified section will be returned. This can be either + * section object or relative section number (field course_sections.section) + * @return array + */ + public function get_format_options($section = null) { + global $DB; + if ($section === null) { + $options = $this->course_format_options(); + } else { + $options = $this->section_format_options(); + } + if (empty($options)) { + // there are no option for course/sections anyway, no need to go further + return array(); + } + if ($section === null) { + // course format options will be returned + $sectionid = 0; + } else if ($this->courseid && isset($section->id)) { + // course section format options will be returned + $sectionid = $section->id; + } else if ($this->courseid && is_int($section) && + ($sectionobj = $DB->get_record('course_sections', + array('section' => $section, 'courseid' => $this->courseid), 'id'))) { + // course section format options will be returned + $sectionid = $sectionobj->id; + } else { + // non-existing (yet) section was passed as an argument + // default format options for course section will be returned + $sectionid = -1; + } + if (!array_key_exists($sectionid, $this->formatoptions)) { + $this->formatoptions[$sectionid] = array(); + // first fill with default values + foreach ($options as $optionname => $optionparams) { + $this->formatoptions[$sectionid][$optionname] = null; + if (array_key_exists('default', $optionparams)) { + $this->formatoptions[$sectionid][$optionname] = $optionparams['default']; + } + } + if ($this->courseid && $sectionid !== -1) { + // overwrite the default options values with those stored in course_format_options table + // nothing can be stored if we are interested in generic course ($this->courseid == 0) + // or generic section ($sectionid === 0) + $records = $DB->get_records('course_format_options', + array('courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid + ), '', 'id,name,value'); + foreach ($records as $record) { + if (array_key_exists($record->name, $this->formatoptions[$sectionid])) { + $value = $record->value; + if ($value !== null && isset($options[$record->name]['type'])) { + // this will convert string value to number if needed + $value = clean_param($value, $options[$record->name]['type']); + } + $this->formatoptions[$sectionid][$record->name] = $value; + } + } + } + } + return $this->formatoptions[$sectionid]; + } + + /** + * Adds format options elements to the course/section edit form + * + * This function is called from {@link course_edit_form::definition_after_data()} + * + * @param MoodleQuickForm $mform form the elements are added to + * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form + * @return array array of references to the added form elements + */ + public function create_edit_form_elements(&$mform, $forsection = false) { + $elements = array(); + if ($forsection) { + $options = $this->section_format_options(true); + } else { + $options = $this->course_format_options(true); + } + foreach ($options as $optionname => $option) { + if (!isset($option['element_type'])) { + $option['element_type'] = 'text'; + } + $args = array($option['element_type'], $optionname, $option['label']); + if (!empty($option['element_attributes'])) { + $args = array_merge($args, $option['element_attributes']); + } + $elements[] = call_user_func_array(array($mform, 'addElement'), $args); + if (isset($option['help'])) { + $helpcomponent = 'format_'. $this->get_format(); + if (isset($option['help_component'])) { + $helpcomponent = $option['help_component']; + } + $mform->addHelpButton($optionname, $option['help'], $helpcomponent); + } + if (isset($option['type'])) { + $mform->setType($optionname, $option['type']); + } + if (is_null($mform->getElementValue($optionname)) && isset($option['default'])) { + $mform->setDefault($optionname, $option['default']); + } + } + return $elements; + } + + /** + * Override if you need to perform some extra validation of the format options + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @param array $errors errors already discovered in edit form validation + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK. + * Do not repeat errors from $errors param here + */ + public function edit_form_validation($data, $files, $errors) { + return array(); + } + + /** + * Updates format options for a course or section + * + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @param null|int null if these are options for course or section id (course_sections.id) + * if these are options for section + * @return bool whether there were any changes to the options values + */ + protected function update_format_options($data, $sectionid = null) { + global $DB; + if (!$sectionid) { + $allformatoptions = $this->course_format_options(); + $sectionid = 0; + } else { + $allformatoptions = $this->section_format_options(); + } + if (empty($allformatoptions)) { + // nothing to update anyway + return false; + } + $defaultoptions = array(); + $cached = array(); + foreach ($allformatoptions as $key => $option) { + $defaultoptions[$key] = null; + if (array_key_exists('default', $option)) { + $defaultoptions[$key] = $option['default']; + } + $cached[$key] = ($sectionid === 0 || !empty($option['cache'])); + } + $records = $DB->get_records('course_format_options', + array('courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid + ), '', 'name,id,value'); + $changed = $needrebuild = false; + $data = (array)$data; + foreach ($defaultoptions as $key => $value) { + if (isset($records[$key])) { + if (array_key_exists($key, $data) && $records[$key]->value !== $data[$key]) { + $DB->set_field('course_format_options', 'value', + $data[$key], array('id' => $records[$key]->id)); + $changed = true; + $needrebuild = $needrebuild || $cached[$key]; + } + } else { + if (array_key_exists($key, $data) && $data[$key] !== $value) { + $newvalue = $data[$key]; + $changed = true; + $needrebuild = $needrebuild || $cached[$key]; + } else { + $newvalue = $value; + // we still insert entry in DB but there are no changes from user point of + // view and no need to call rebuild_course_cache() + } + $DB->insert_record('course_format_options', array( + 'courseid' => $this->courseid, + 'format' => $this->format, + 'sectionid' => $sectionid, + 'name' => $key, + 'value' => $newvalue + )); + } + } + if ($needrebuild) { + rebuild_course_cache($this->courseid, true); + } + if ($changed) { + // reset internal caches + if (!$sectionid) { + $this->course = false; + } + unset($this->formatoptions[$sectionid]); + } + return $changed; + } + + /** + * Updates format options for a course + * + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @param stdClass $oldcourse if this function is called from {@link update_course()} + * this object contains information about the course before update + * @return bool whether there were any changes to the options values + */ + public function update_course_format_options($data, $oldcourse = null) { + return $this->update_format_options($data); + } + + /** + * Updates format options for a section + * + * Section id is expected in $data->id (or $data['id']) + * If $data does not contain property with the option name, the option will not be updated + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @return bool whether there were any changes to the options values + */ + public function update_section_format_options($data) { + $data = (array)$data; + return $this->update_format_options($data, $data['id']); + } + + /** + * Return an instance of moodleform to edit a specified section + * + * Default implementation returns instance of editsection_form that automatically adds + * additional fields defined in {@link format_base::section_format_options()} + * + * Format plugins may extend editsection_form if they want to have custom edit section form. + * + * @param mixed $action the action attribute for the form. If empty defaults to auto detect the + * current url. If a moodle_url object then outputs params as hidden variables. + * @param array $customdata the array with custom data to be passed to the form + * /course/editsection.php passes section_info object in 'cs' field + * for filling availability fields + * @return moodleform + */ + public function editsection_form($action, $customdata = array()) { + global $CFG; + require_once($CFG->dirroot. '/course/editsection_form.php'); + $context = context_course::instance($this->courseid); + if (!array_key_exists('course', $customdata)) { + $customdata['course'] = $this->get_course(); + } + return new editsection_form($action, $customdata); + } + + /** + * Allows course format to execute code on moodle_page::set_course() + * + * @param moodle_page $page instance of page calling set_course + */ + public function page_set_course(moodle_page $page) { + } + + /** + * Allows course format to execute code on moodle_page::set_cm() + * + * Current module can be accessed as $page->cm (returns instance of cm_info) + * + * @param moodle_page $page instance of page calling set_cm + */ + public function page_set_cm(moodle_page $page) { + } + + /** + * Course-specific information to be output on any course page (usually above navigation bar) + * + * Example of usage: + * define + * class format_FORMATNAME_XXX implements renderable {} + * + * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function: + * class format_FORMATNAME_renderer extends plugin_renderer_base { + * protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) { + * return html_writer::tag('div', 'This is my header/footer'); + * } + * } + * + * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from + * plugin renderer will be called + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_header() { + return null; + } + + /** + * Course-specific information to be output on any course page (usually in the beginning of + * standard footer) + * + * See {@link format_base::course_header()} for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_footer() { + return null; + } + + /** + * Course-specific information to be output immediately above content on any course page + * + * See {@link format_base::course_header()} for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_content_header() { + return null; + } + + /** + * Course-specific information to be output immediately below content on any course page + * + * See {@link format_base::course_header()} for usage + * + * @return null|renderable null for no output or object with data for plugin renderer + */ + public function course_content_footer() { + return null; + } + + /** + * Returns instance of page renderer used by this plugin + * + * @param moodle_page $page + * @return renderer_base + */ + public function get_renderer(moodle_page $page) { + return $page->get_renderer('format_'. $this->get_format()); + } + + /** + * Returns true if the specified section is current + * + * By default we analyze $course->marker + * + * @param int|stdClass|section_info $section + * @return bool + */ + public function is_section_current($section) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); + } +} + +/** + * Pseudo course format used for the site main page + * + * @package core_course + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_site extends format_base { + + /** + * Returns the display name of the given section that the course prefers. + * + * @param int|stdClass $section Section object from database or just field section.section + * @return Display name that the course format prefers, e.g. "Topic 2" + */ + function get_section_name($section) { + return get_string('site'); + } + + /** + * For this fake course referring to the whole site, the site homepage is always returned + * regardless of arguments + * + * @param int|stdClass $section + * @param array $options + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + return new moodle_url('/'); + } + + /** + * Returns the list of blocks to be automatically added on the site frontpage when moodle is installed + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + return blocks_get_default_site_course_blocks(); + } + + /** + * Definitions of the additional options that site uses + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + static $courseformatoptions = false; + if ($courseformatoptions === false) { + $courseformatoptions = array( + 'numsections' => array( + 'default' => 1, + 'type' => PARAM_INT, + ), + ); + } + return $courseformatoptions; + } +} diff --git a/format/renderer.php b/format/renderer.php new file mode 100644 index 0000000..ceb3045 --- /dev/null +++ b/format/renderer.php @@ -0,0 +1,797 @@ +. + +/** + * Base renderer for outputting course formats. + * + * @package core + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.3 + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * This is a convenience renderer which can be used by section based formats + * to reduce code duplication. It is not necessary for all course formats to + * use this and its likely to change in future releases. + * + * @package core + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.3 + */ +abstract class format_section_renderer_base extends plugin_renderer_base { + + /** + * Generate the starting container html for a list of sections + * @return string HTML to output. + */ + abstract protected function start_section_list(); + + /** + * Generate the closing container html for a list of sections + * @return string HTML to output. + */ + abstract protected function end_section_list(); + + /** + * Generate the title for this section page + * @return string the page title + */ + abstract protected function page_title(); + + /** + * Generate the section title + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course The course entry from DB + * @return string HTML to output. + */ + public function section_title($section, $course) { + $title = get_section_name($course, $section); + $url = course_get_url($course, $section->section, array('navigation' => true)); + if ($url) { + $title = html_writer::link($url, $title); + } + return $title; + } + + /** + * Generate the content to displayed on the right part of a section + * before course modules are included + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course The course entry from DB + * @param bool $onsectionpage true if being printed on a section page + * @return string HTML to output. + */ + protected function section_right_content($section, $course, $onsectionpage) { + $o = $this->output->spacer(); + + if ($section->section != 0) { + $controls = $this->section_edit_controls($course, $section, $onsectionpage); + if (!empty($controls)) { + $o = implode('
', $controls); + } + } + + return $o; + } + + /** + * Generate the content to displayed on the left part of a section + * before course modules are included + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course The course entry from DB + * @param bool $onsectionpage true if being printed on a section page + * @return string HTML to output. + */ + protected function section_left_content($section, $course, $onsectionpage) { + $o = $this->output->spacer(); + + if ($section->section != 0) { + // Only in the non-general sections. + if (course_get_format($course)->is_section_current($section)) { + $o = get_accesshide(get_string('currentsection', 'format_'.$course->format)); + } + } + + return $o; + } + + /** + * Generate the display of the header part of a section before + * course modules are included + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course The course entry from DB + * @param bool $onsectionpage true if being printed on a single-section page + * @param int $sectionreturn The section to return to after an action + * @return string HTML to output. + */ + protected function section_header($section, $course, $onsectionpage, $sectionreturn=null) { + global $PAGE; + + $o = ''; + $currenttext = ''; + $sectionstyle = ''; + + if ($section->section != 0) { + // Only in the non-general sections. + if (!$section->visible) { + $sectionstyle = ' hidden'; + } else if (course_get_format($course)->is_section_current($section)) { + $sectionstyle = ' current'; + } + } + + $o.= html_writer::start_tag('li', array('id' => 'section-'.$section->section, + 'class' => 'section main clearfix'.$sectionstyle)); + + $leftcontent = $this->section_left_content($section, $course, $onsectionpage); + $o.= html_writer::tag('div', $leftcontent, array('class' => 'left side')); + + $rightcontent = $this->section_right_content($section, $course, $onsectionpage); + $o.= html_writer::tag('div', $rightcontent, array('class' => 'right side')); + $o.= html_writer::start_tag('div', array('class' => 'content')); + + // When not on a section page, we display the section titles except the general section if null + $hasnamenotsecpg = (!$onsectionpage && ($section->section != 0 || !is_null($section->name))); + + // When on a section page, we only display the general section title, if title is not the default one + $hasnamesecpg = ($onsectionpage && ($section->section == 0 && !is_null($section->name))); + + if ($hasnamenotsecpg || $hasnamesecpg) { + $o.= $this->output->heading($this->section_title($section, $course), 3, 'sectionname'); + } + + $o.= html_writer::start_tag('div', array('class' => 'summary')); + $o.= $this->format_summary_text($section); + + $context = context_course::instance($course->id); + if ($PAGE->user_is_editing() && has_capability('moodle/course:update', $context)) { + $url = new moodle_url('/course/editsection.php', array('id'=>$section->id, 'sr'=>$sectionreturn)); + $o.= html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('t/edit'), + 'class' => 'iconsmall edit', 'alt' => get_string('edit'))), + array('title' => get_string('editsummary'))); + } + $o.= html_writer::end_tag('div'); + + $o .= $this->section_availability_message($section, + has_capability('moodle/course:viewhiddensections', $context)); + + return $o; + } + + /** + * Generate the display of the footer part of a section + * + * @return string HTML to output. + */ + protected function section_footer() { + $o = html_writer::end_tag('div'); + $o.= html_writer::end_tag('li'); + + return $o; + } + + /** + * Generate the edit controls of a section + * + * @param stdClass $course The course entry from DB + * @param stdClass $section The course_section entry from DB + * @param bool $onsectionpage true if being printed on a section page + * @return array of links with edit controls + */ + protected function section_edit_controls($course, $section, $onsectionpage = false) { + global $PAGE; + + if (!$PAGE->user_is_editing()) { + return array(); + } + + $coursecontext = context_course::instance($course->id); + + if ($onsectionpage) { + $baseurl = course_get_url($course, $section->section); + } else { + $baseurl = course_get_url($course); + } + $baseurl->param('sesskey', sesskey()); + + $controls = array(); + + $url = clone($baseurl); + if (has_capability('moodle/course:sectionvisibility', $coursecontext)) { + if ($section->visible) { // Show the hide/show eye. + $strhidefromothers = get_string('hidefromothers', 'format_'.$course->format); + $url->param('hide', $section->section); + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/hide'), + 'class' => 'icon hide', 'alt' => $strhidefromothers)), + array('title' => $strhidefromothers, 'class' => 'editing_showhide')); + } else { + $strshowfromothers = get_string('showfromothers', 'format_'.$course->format); + $url->param('show', $section->section); + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/show'), + 'class' => 'icon hide', 'alt' => $strshowfromothers)), + array('title' => $strshowfromothers, 'class' => 'editing_showhide')); + } + } + + if (!$onsectionpage && has_capability('moodle/course:movesections', $coursecontext)) { + $url = clone($baseurl); + if ($section->section > 1) { // Add a arrow to move section up. + $url->param('section', $section->section); + $url->param('move', -1); + $strmoveup = get_string('moveup'); + + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/up'), + 'class' => 'icon up', 'alt' => $strmoveup)), + array('title' => $strmoveup, 'class' => 'moveup')); + } + + $url = clone($baseurl); + if ($section->section < $course->numsections) { // Add a arrow to move section down. + $url->param('section', $section->section); + $url->param('move', 1); + $strmovedown = get_string('movedown'); + + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/down'), + 'class' => 'icon down', 'alt' => $strmovedown)), + array('title' => $strmovedown, 'class' => 'movedown')); + } + } + + return $controls; + } + + /** + * Generate a summary of a section for display on the 'coruse index page' + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course The course entry from DB + * @param array $mods (argument not used) + * @return string HTML to output. + */ + protected function section_summary($section, $course, $mods) { + $classattr = 'section main section-summary clearfix'; + $linkclasses = ''; + + // If section is hidden then display grey section link + if (!$section->visible) { + $classattr .= ' hidden'; + $linkclasses .= ' dimmed_text'; + } else if (course_get_format($course)->is_section_current($section)) { + $classattr .= ' current'; + } + + $o = ''; + $o .= html_writer::start_tag('li', array('id' => 'section-'.$section->section, 'class' => $classattr)); + + $o .= html_writer::tag('div', '', array('class' => 'left side')); + $o .= html_writer::tag('div', '', array('class' => 'right side')); + $o .= html_writer::start_tag('div', array('class' => 'content')); + + $title = get_section_name($course, $section); + if ($section->uservisible) { + $title = html_writer::tag('a', $title, + array('href' => course_get_url($course, $section->section), 'class' => $linkclasses)); + } + $o .= $this->output->heading($title, 3, 'section-title'); + + $o.= html_writer::start_tag('div', array('class' => 'summarytext')); + $o.= $this->format_summary_text($section); + $o.= html_writer::end_tag('div'); + $o.= $this->section_activity_summary($section, $course, null); + + $context = context_course::instance($course->id); + $o .= $this->section_availability_message($section, + has_capability('moodle/course:viewhiddensections', $context)); + + $o .= html_writer::end_tag('div'); + $o .= html_writer::end_tag('li'); + + return $o; + } + + /** + * Generate a summary of the activites in a section + * + * @param stdClass $section The course_section entry from DB + * @param stdClass $course the course record from DB + * @param array $mods (argument not used) + * @return string HTML to output. + */ + private function section_activity_summary($section, $course, $mods) { + $modinfo = get_fast_modinfo($course); + if (empty($modinfo->sections[$section->section])) { + return ''; + } + + // Generate array with count of activities in this section: + $sectionmods = array(); + $total = 0; + $complete = 0; + $cancomplete = isloggedin() && !isguestuser(); + $completioninfo = new completion_info($course); + foreach ($modinfo->sections[$section->section] as $cmid) { + $thismod = $modinfo->cms[$cmid]; + + if ($thismod->modname == 'label') { + // Labels are special (not interesting for students)! + continue; + } + + if ($thismod->uservisible) { + if (isset($sectionmods[$thismod->modname])) { + $sectionmods[$thismod->modname]['name'] = $thismod->modplural; + $sectionmods[$thismod->modname]['count']++; + } else { + $sectionmods[$thismod->modname]['name'] = $thismod->modfullname; + $sectionmods[$thismod->modname]['count'] = 1; + } + if ($cancomplete && $completioninfo->is_enabled($thismod) != COMPLETION_TRACKING_NONE) { + $total++; + $completiondata = $completioninfo->get_data($thismod, true); + if ($completiondata->completionstate == COMPLETION_COMPLETE) { + $complete++; + } + } + } + } + + if (empty($sectionmods)) { + // No sections + return ''; + } + + // Output section activities summary: + $o = ''; + $o.= html_writer::start_tag('div', array('class' => 'section-summary-activities mdl-right')); + foreach ($sectionmods as $mod) { + $o.= html_writer::start_tag('span', array('class' => 'activity-count')); + $o.= $mod['name'].': '.$mod['count']; + $o.= html_writer::end_tag('span'); + } + $o.= html_writer::end_tag('div'); + + // Output section completion data + if ($total > 0) { + $a = new stdClass; + $a->complete = $complete; + $a->total = $total; + + $o.= html_writer::start_tag('div', array('class' => 'section-summary-activities mdl-right')); + $o.= html_writer::tag('span', get_string('progresstotal', 'completion', $a), array('class' => 'activity-count')); + $o.= html_writer::end_tag('div'); + } + + return $o; + } + + /** + * If section is not visible, display the message about that ('Not available + * until...', that sort of thing). Otherwise, returns blank. + * + * For users with the ability to view hidden sections, it shows the + * information even though you can view the section and also may include + * slightly fuller information (so that teachers can tell when sections + * are going to be unavailable etc). This logic is the same as for + * activities. + * + * @param stdClass $section The course_section entry from DB + * @param bool $canviewhidden True if user can view hidden sections + * @return string HTML to output + */ + protected function section_availability_message($section, $canviewhidden) { + global $CFG; + $o = ''; + if (!$section->uservisible) { + $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo')); + // Note: We only get to this function if availableinfo is non-empty, + // so there is definitely something to print. + $o .= $section->availableinfo; + $o .= html_writer::end_tag('div'); + } else if ($canviewhidden && !empty($CFG->enableavailability) && $section->visible) { + $ci = new condition_info_section($section); + $fullinfo = $ci->get_full_information(); + if ($fullinfo) { + $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo')); + $o .= get_string( + ($section->showavailability ? 'userrestriction_visible' : 'userrestriction_hidden'), + 'condition', $fullinfo); + $o .= html_writer::end_tag('div'); + } + } + return $o; + } + + /** + * Show if something is on on the course clipboard (moving around) + * + * @param stdClass $course The course entry from DB + * @param int $sectionno The section number in the coruse which is being dsiplayed + * @return string HTML to output. + */ + protected function course_activity_clipboard($course, $sectionno = null) { + global $USER; + + $o = ''; + // If currently moving a file then show the current clipboard. + if (ismoving($course->id)) { + $url = new moodle_url('/course/mod.php', + array('sesskey' => sesskey(), + 'cancelcopy' => true, + 'sr' => $sectionno, + ) + ); + + $o.= html_writer::start_tag('div', array('class' => 'clipboard')); + $o.= strip_tags(get_string('activityclipboard', '', $USER->activitycopyname)); + $o.= ' ('.html_writer::link($url, get_string('cancel')).')'; + $o.= html_writer::end_tag('div'); + } + + return $o; + } + + /** + * Generate next/previous section links for naviation + * + * @param stdClass $course The course entry from DB + * @param array $sections The course_sections entries from the DB + * @param int $sectionno The section number in the coruse which is being dsiplayed + * @return array associative array with previous and next section link + */ + protected function get_nav_links($course, $sections, $sectionno) { + // FIXME: This is really evil and should by using the navigation API. + $course = course_get_format($course)->get_course(); + $canviewhidden = has_capability('moodle/course:viewhiddensections', context_course::instance($course->id)) + or !$course->hiddensections; + + $links = array('previous' => '', 'next' => ''); + $back = $sectionno - 1; + while ($back > 0 and empty($links['previous'])) { + if ($canviewhidden || $sections[$back]->uservisible) { + $params = array(); + if (!$sections[$back]->visible) { + $params = array('class' => 'dimmed_text'); + } + $previouslink = html_writer::tag('span', $this->output->larrow(), array('class' => 'larrow')); + $previouslink .= get_section_name($course, $sections[$back]); + $links['previous'] = html_writer::link(course_get_url($course, $back), $previouslink, $params); + } + $back--; + } + + $forward = $sectionno + 1; + while ($forward <= $course->numsections and empty($links['next'])) { + if ($canviewhidden || $sections[$forward]->uservisible) { + $params = array(); + if (!$sections[$forward]->visible) { + $params = array('class' => 'dimmed_text'); + } + $nextlink = get_section_name($course, $sections[$forward]); + $nextlink .= html_writer::tag('span', $this->output->rarrow(), array('class' => 'rarrow')); + $links['next'] = html_writer::link(course_get_url($course, $forward), $nextlink, $params); + } + $forward++; + } + + return $links; + } + + /** + * Generate the header html of a stealth section + * + * @param int $sectionno The section number in the coruse which is being dsiplayed + * @return string HTML to output. + */ + protected function stealth_section_header($sectionno) { + $o = ''; + $o.= html_writer::start_tag('li', array('id' => 'section-'.$sectionno, 'class' => 'section main clearfix orphaned hidden')); + $o.= html_writer::tag('div', '', array('class' => 'left side')); + $o.= html_writer::tag('div', '', array('class' => 'right side')); + $o.= html_writer::start_tag('div', array('class' => 'content')); + $o.= $this->output->heading(get_string('orphanedactivities'), 3, 'sectionname'); + return $o; + } + + /** + * Generate footer html of a stealth section + * + * @return string HTML to output. + */ + protected function stealth_section_footer() { + $o = html_writer::end_tag('div'); + $o.= html_writer::end_tag('li'); + return $o; + } + + /** + * Generate the html for a hidden section + * + * @param int $sectionno The section number in the coruse which is being dsiplayed + * @return string HTML to output. + */ + protected function section_hidden($sectionno) { + $o = ''; + $o.= html_writer::start_tag('li', array('id' => 'section-'.$sectionno, 'class' => 'section main clearfix hidden')); + $o.= html_writer::tag('div', '', array('class' => 'left side')); + $o.= html_writer::tag('div', '', array('class' => 'right side')); + $o.= html_writer::start_tag('div', array('class' => 'content')); + $o.= get_string('notavailable'); + $o.= html_writer::end_tag('div'); + $o.= html_writer::end_tag('li'); + return $o; + } + + /** + * Output the html for a single section page . + * + * @param stdClass $course The course entry from DB + * @param array $sections (argument not used) + * @param array $mods (argument not used) + * @param array $modnames (argument not used) + * @param array $modnamesused (argument not used) + * @param int $displaysection The section number in the course which is being displayed + */ + public function print_single_section_page($course, $sections, $mods, $modnames, $modnamesused, $displaysection) { + global $PAGE; + + $modinfo = get_fast_modinfo($course); + $course = course_get_format($course)->get_course(); + + // Can we view the section in question? + if (!($sectioninfo = $modinfo->get_section_info($displaysection))) { + // This section doesn't exist + print_error('unknowncoursesection', 'error', null, $course->fullname); + return; + } + + if (!$sectioninfo->uservisible) { + if (!$course->hiddensections) { + echo $this->start_section_list(); + echo $this->section_hidden($displaysection); + echo $this->end_section_list(); + } + // Can't view this section. + return; + } + + // Copy activity clipboard.. + echo $this->course_activity_clipboard($course, $displaysection); + $thissection = $modinfo->get_section_info(0); + if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) { + echo $this->start_section_list(); + echo $this->section_header($thissection, $course, true, $displaysection); + print_section($course, $thissection, null, null, true, "100%", false, $displaysection); + if ($PAGE->user_is_editing()) { + print_section_add_menus($course, 0, null, false, false, $displaysection); + } + echo $this->section_footer(); + echo $this->end_section_list(); + } + + // Start single-section div + echo html_writer::start_tag('div', array('class' => 'single-section')); + + // The requested section page. + $thissection = $modinfo->get_section_info($displaysection); + + // Title with section navigation links. + $sectionnavlinks = $this->get_nav_links($course, $modinfo->get_section_info_all(), $displaysection); + $sectiontitle = ''; + $sectiontitle .= html_writer::start_tag('div', array('class' => 'section-navigation header headingblock')); + $sectiontitle .= html_writer::tag('span', $sectionnavlinks['previous'], array('class' => 'mdl-left')); + $sectiontitle .= html_writer::tag('span', $sectionnavlinks['next'], array('class' => 'mdl-right')); + // Title attributes + $titleattr = 'mdl-align title'; + if (!$thissection->visible) { + $titleattr .= ' dimmed_text'; + } + $sectiontitle .= html_writer::tag('div', get_section_name($course, $displaysection), array('class' => $titleattr)); + $sectiontitle .= html_writer::end_tag('div'); + echo $sectiontitle; + + // Now the list of sections.. + echo $this->start_section_list(); + + echo $this->section_header($thissection, $course, true, $displaysection); + // Show completion help icon. + $completioninfo = new completion_info($course); + echo $completioninfo->display_help_icon(); + + print_section($course, $thissection, null, null, true, '100%', false, $displaysection); + if ($PAGE->user_is_editing()) { + print_section_add_menus($course, $displaysection, null, false, false, $displaysection); + } + echo $this->section_footer(); + echo $this->end_section_list(); + + // Display section bottom navigation. + $courselink = html_writer::link(course_get_url($course), get_string('returntomaincoursepage')); + $sectionbottomnav = ''; + $sectionbottomnav .= html_writer::start_tag('div', array('class' => 'section-navigation mdl-bottom')); + $sectionbottomnav .= html_writer::tag('span', $sectionnavlinks['previous'], array('class' => 'mdl-left')); + $sectionbottomnav .= html_writer::tag('span', $sectionnavlinks['next'], array('class' => 'mdl-right')); + $sectionbottomnav .= html_writer::tag('div', $courselink, array('class' => 'mdl-align')); + $sectionbottomnav .= html_writer::end_tag('div'); + echo $sectionbottomnav; + + // close single-section div. + echo html_writer::end_tag('div'); + } + + /** + * Output the html for a multiple section page + * + * @param stdClass $course The course entry from DB + * @param array $sections (argument not used) + * @param array $mods (argument not used) + * @param array $modnames (argument not used) + * @param array $modnamesused (argument not used) + */ + public function print_multiple_section_page($course, $sections, $mods, $modnames, $modnamesused) { + global $PAGE; + + $modinfo = get_fast_modinfo($course); + $course = course_get_format($course)->get_course(); + + $context = context_course::instance($course->id); + // Title with completion help icon. + $completioninfo = new completion_info($course); + echo $completioninfo->display_help_icon(); + echo $this->output->heading($this->page_title(), 2, 'accesshide'); + + // Copy activity clipboard.. + echo $this->course_activity_clipboard($course, 0); + + // Now the list of sections.. + echo $this->start_section_list(); + + foreach ($modinfo->get_section_info_all() as $section => $thissection) { + if ($section == 0) { + // 0-section is displayed a little different then the others + if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) { + echo $this->section_header($thissection, $course, false, 0); + print_section($course, $thissection, null, null, true, "100%", false, 0); + if ($PAGE->user_is_editing()) { + print_section_add_menus($course, 0, null, false, false, 0); + } + echo $this->section_footer(); + } + continue; + } + if ($section > $course->numsections) { + // activities inside this section are 'orphaned', this section will be printed as 'stealth' below + continue; + } + // Show the section if the user is permitted to access it, OR if it's not available + // but showavailability is turned on (and there is some available info text). + $showsection = $thissection->uservisible || + ($thissection->visible && !$thissection->available && $thissection->showavailability + && !empty($thissection->availableinfo)); + if (!$showsection) { + // Hidden section message is overridden by 'unavailable' control + // (showavailability option). + if (!$course->hiddensections && $thissection->available) { + echo $this->section_hidden($section); + } + + continue; + } + + if (!$PAGE->user_is_editing() && $course->coursedisplay == COURSE_DISPLAY_MULTIPAGE) { + // Display section summary only. + echo $this->section_summary($thissection, $course, null); + } else { + echo $this->section_header($thissection, $course, false, 0); + if ($thissection->uservisible) { + print_section($course, $thissection, null, null, true, "100%", false, 0); + if ($PAGE->user_is_editing()) { + print_section_add_menus($course, $section, null, false, false, 0); + } + } + echo $this->section_footer(); + } + } + + if ($PAGE->user_is_editing() and has_capability('moodle/course:update', $context)) { + // Print stealth sections if present. + foreach ($modinfo->get_section_info_all() as $section => $thissection) { + if ($section <= $course->numsections or empty($modinfo->sections[$section])) { + // this is not stealth section or it is empty + continue; + } + echo $this->stealth_section_header($section); + print_section($course, $thissection, null, null, true, "100%", false, 0); + echo $this->stealth_section_footer(); + } + + echo $this->end_section_list(); + + echo html_writer::start_tag('div', array('id' => 'changenumsections', 'class' => 'mdl-right')); + + // Increase number of sections. + $straddsection = get_string('increasesections', 'moodle'); + $url = new moodle_url('/course/changenumsections.php', + array('courseid' => $course->id, + 'increase' => true, + 'sesskey' => sesskey())); + $icon = $this->output->pix_icon('t/switch_plus', $straddsection); + echo html_writer::link($url, $icon.get_accesshide($straddsection), array('class' => 'increase-sections')); + + if ($course->numsections > 0) { + // Reduce number of sections sections. + $strremovesection = get_string('reducesections', 'moodle'); + $url = new moodle_url('/course/changenumsections.php', + array('courseid' => $course->id, + 'increase' => false, + 'sesskey' => sesskey())); + $icon = $this->output->pix_icon('t/switch_minus', $strremovesection); + echo html_writer::link($url, $icon.get_accesshide($strremovesection), array('class' => 'reduce-sections')); + } + + echo html_writer::end_tag('div'); + } else { + echo $this->end_section_list(); + } + + } + + /** + * Generate html for a section summary text + * + * @param stdClass $section The course_section entry from DB + * @return string HTML to output. + */ + protected function format_summary_text($section) { + $context = context_course::instance($section->course); + $summarytext = file_rewrite_pluginfile_urls($section->summary, 'pluginfile.php', + $context->id, 'course', 'section', $section->id); + + $options = new stdClass(); + $options->noclean = true; + $options->overflowdiv = true; + return format_text($summarytext, $section->summaryformat, $options); + } + + /** + * Is the section passed in the current section? + * + * @deprecated since 2.4 + * @see format_base::is_section_current() + * + * @param stdClass $course The course entry from DB + * @param stdClass $section The course_section entry from the DB + * @return bool true if the section is current + */ + protected final function is_section_current($section, $course) { + debugging('Function format_section_renderer_base::is_section_current() is deprecated. '. + 'Use course_get_format($course)->is_section_current($section) instead', DEBUG_DEVELOPER); + return course_get_format($course)->is_section_current($section); + } +} diff --git a/format/scorm/format.php b/format/scorm/format.php new file mode 100644 index 0000000..465f7f7 --- /dev/null +++ b/format/scorm/format.php @@ -0,0 +1,17 @@ +format; + require_once($CFG->dirroot.'/mod/'.$module.'/locallib.php'); + + $strgroups = get_string('groups'); + $strgroupmy = get_string('groupmy'); + $editing = $PAGE->user_is_editing(); + + $moduleformat = $module.'_course_format_display'; + if (function_exists($moduleformat)) { + $moduleformat($USER,$course); + } else { + echo $OUTPUT->notification('The module '. $module. ' does not support single activity course format'); + } diff --git a/format/scorm/lang/en/format_scorm.php b/format/scorm/lang/en/format_scorm.php new file mode 100644 index 0000000..8b684c1 --- /dev/null +++ b/format/scorm/lang/en/format_scorm.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'format_scorm', language 'en', branch 'MOODLE_20_STABLE' + * + * @package format_scorm + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['sectionname'] = 'SCORM'; +$string['pluginname'] = 'SCORM format'; diff --git a/format/scorm/lib.php b/format/scorm/lib.php new file mode 100644 index 0000000..60e9a26 --- /dev/null +++ b/format/scorm/lib.php @@ -0,0 +1,101 @@ +. + +/** + * This file contains main class for the course format SCORM + * + * @since 2.0 + * @package format_scorm + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot. '/course/format/lib.php'); + +/** + * Main class for the Scorm course format + * + * @package format_scorm + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_scorm extends format_base { + + /** + * The URL to use for the specified course + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if null the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + if (!empty($options['navigation']) && $section !== null) { + return null; + } + return new moodle_url('/course/view.php', array('id' => $this->courseid)); + } + + /** + * Loads all of the course sections into the navigation + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + // Scorm course format does not extend course navigation + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + return array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array('news_items', 'recent_activity', 'calendar_upcoming') + ); + } + + /** + * Allows course format to execute code on moodle_page::set_course() + * + * If user is on course view page and there is no scorm module added to the course + * and the user has 'moodle/course:update' capability, redirect to create module + * form. This function is executed before the output starts + * + * @param moodle_page $page instance of page calling set_course + */ + public function page_set_course(moodle_page $page) { + global $PAGE; + if ($PAGE == $page && $page->has_set_url() && + $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { + $modinfo = get_fast_modinfo($this->courseid); + if (empty($modinfo->instances['scorm']) + && has_capability('moodle/course:update', context_course::instance($this->courseid))) { + // Redirect to create a new activity + $url = new moodle_url('/course/modedit.php', + array('course' => $this->courseid, 'section' => 0, 'add' => 'scorm')); + redirect($url); + } + } + } +} diff --git a/format/scorm/version.php b/format/scorm/version.php new file mode 100644 index 0000000..d1641c9 --- /dev/null +++ b/format/scorm/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package format + * @subpackage scorm + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2012112900; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012112900; // Requires this Moodle version +$plugin->component = 'format_scorm'; // Full name of the plugin (used for diagnostics) diff --git a/format/social/format.php b/format/social/format.php new file mode 100644 index 0000000..b661c22 --- /dev/null +++ b/format/social/format.php @@ -0,0 +1,35 @@ +user_is_editing(); + + if ($forum = forum_get_course_forum($course->id, 'social')) { + + $cm = get_coursemodule_from_instance('forum', $forum->id); + $context = context_module::instance($cm->id); + + /// Print forum intro above posts MDL-18483 + if (trim($forum->intro) != '') { + $options = new stdClass(); + $options->para = false; + $introcontent = format_module_intro('forum', $forum, $cm->id); + + if ($PAGE->user_is_editing() && has_capability('moodle/course:update', $context)) { + $streditsummary = get_string('editsummary'); + $introcontent .= ''; + } + echo $OUTPUT->box($introcontent, 'generalbox', 'intro'); + } + + echo ''; + forum_print_latest_discussions($course, $forum, 10, 'plain', '', false); + + } else { + echo $OUTPUT->notification('Could not find or create a social forum here'); + } diff --git a/format/social/lang/en/format_social.php b/format/social/lang/en/format_social.php new file mode 100644 index 0000000..0316353 --- /dev/null +++ b/format/social/lang/en/format_social.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'format_social', language 'en', branch 'MOODLE_20_STABLE' + * + * @package format_social + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['sectionname'] = 'section'; +$string['pluginname'] = 'Social format'; diff --git a/format/social/lib.php b/format/social/lib.php new file mode 100644 index 0000000..3c84a15 --- /dev/null +++ b/format/social/lib.php @@ -0,0 +1,78 @@ +. + +/** + * This file contains main class for the course format Social + * + * @since 2.0 + * @package format_social + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot. '/course/format/lib.php'); + +/** + * Main class for the Social course format + * + * @package format_social + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_social extends format_base { + + /** + * The URL to use for the specified course + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if null the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + if (!empty($options['navigation']) && $section !== null) { + return null; + } + return new moodle_url('/course/view.php', array('id' => $this->courseid)); + } + + /** + * Loads all of the course sections into the navigation + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + // Social course format does not extend navigation, it uses social_activities block instead + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + return array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array('search_forums', 'calendar_upcoming', 'social_activities', + 'recent_activity', 'course_list') + ); + } +} diff --git a/format/social/version.php b/format/social/version.php new file mode 100644 index 0000000..645f02a --- /dev/null +++ b/format/social/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package format + * @subpackage social + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2012112900; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012112900; // Requires this Moodle version +$plugin->component = 'format_social'; // Full name of the plugin (used for diagnostics) diff --git a/format/topics/format.js b/format/topics/format.js new file mode 100644 index 0000000..172d1d9 --- /dev/null +++ b/format/topics/format.js @@ -0,0 +1,73 @@ +// Javascript functions for Topics course format + +M.course = M.course || {}; + +M.course.format = M.course.format || {}; + +/** + * Get sections config for this format + * + * The section structure is: + *
    + *
  • ...
  • + *
  • ...
  • + * ... + *
+ * + * @return {object} section list configuration + */ +M.course.format.get_config = function() { + return { + container_node : 'ul', + container_class : 'topics', + section_node : 'li', + section_class : 'section' + }; +} + +/** + * Swap section + * + * @param {YUI} Y YUI3 instance + * @param {string} node1 node to swap to + * @param {string} node2 node to swap with + * @return {NodeList} section list + */ +M.course.format.swap_sections = function(Y, node1, node2) { + var CSS = { + COURSECONTENT : 'course-content', + SECTIONADDMENUS : 'section_add_menus' + }; + + var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y)); + // Swap menus. + sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS)); +} + +/** + * Process sections after ajax response + * + * @param {YUI} Y YUI3 instance + * @param {array} response ajax response + * @param {string} sectionfrom first affected section + * @param {string} sectionto last affected section + * @return void + */ +M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { + var CSS = { + SECTIONNAME : 'sectionname' + }; + + if (response.action == 'move') { + // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. + if (sectionfrom > sectionto) { + var temp = sectionto; + sectionto = sectionfrom; + sectionfrom = temp; + } + // Update titles in all affected sections. + for (var i = sectionfrom; i <= sectionto; i++) { + sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]); + } + } +} diff --git a/format/topics/format.php b/format/topics/format.php new file mode 100644 index 0000000..cf2c052 --- /dev/null +++ b/format/topics/format.php @@ -0,0 +1,60 @@ +. + +/** + * Topics course format. Display the whole course as "topics" made of modules. + * + * @package format_topics + * @copyright 2006 The Open University + * @author N.D.Freear@open.ac.uk, and others. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +// Horrible backwards compatible parameter aliasing.. +if ($topic = optional_param('topic', 0, PARAM_INT)) { + $url = $PAGE->url; + $url->param('section', $topic); + debugging('Outdated topic param passed to course/view.php', DEBUG_DEVELOPER); + redirect($url); +} +// End backwards-compatible aliasing.. + +$context = context_course::instance($course->id); + +if (($marker >=0) && has_capability('moodle/course:setcurrentsection', $context) && confirm_sesskey()) { + $course->marker = $marker; + course_set_marker($course->id, $marker); +} + +// make sure all sections are created +$course = course_get_format($course)->get_course(); +course_create_sections_if_missing($course, range(0, $course->numsections)); + +$renderer = $PAGE->get_renderer('format_topics'); + +if (!empty($displaysection)) { + $renderer->print_single_section_page($course, null, null, null, null, $displaysection); +} else { + $renderer->print_multiple_section_page($course, null, null, null, null); +} + +// Include course format js module +$PAGE->requires->js('/course/format/topics/format.js'); diff --git a/format/topics/lang/en/format_topics.php b/format/topics/lang/en/format_topics.php new file mode 100644 index 0000000..f9766d9 --- /dev/null +++ b/format/topics/lang/en/format_topics.php @@ -0,0 +1,33 @@ +. + +/** + * Strings for component 'format_topics', language 'en', branch 'MOODLE_20_STABLE' + * + * @package format_topics + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['currentsection'] = 'This topic'; +$string['sectionname'] = 'Topic'; +$string['pluginname'] = 'Topics format'; +$string['section0name'] = 'General'; +$string['page-course-view-topics'] = 'Any course main page in topics format'; +$string['page-course-view-topics-x'] = 'Any course page in topics format'; +$string['hidefromothers'] = 'Hide topic'; +$string['showfromothers'] = 'Show topic'; diff --git a/format/topics/lib.php b/format/topics/lib.php new file mode 100644 index 0000000..debe045 --- /dev/null +++ b/format/topics/lib.php @@ -0,0 +1,299 @@ +. + +/** + * This file contains main class for the course format Topic + * + * @since 2.0 + * @package format_topics + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot. '/course/format/lib.php'); + +/** + * Main class for the Topics course format + * + * @package format_topics + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_topics extends format_base { + + /** + * Returns true if this course format uses sections + * + * @return bool + */ + public function uses_sections() { + return true; + } + + /** + * Returns the display name of the given section that the course prefers. + * + * Use section name is specified by user. Otherwise use default ("Topic #") + * + * @param int|stdClass $section Section object from database or just field section.section + * @return string Display name that the course format prefers, e.g. "Topic 2" + */ + public function get_section_name($section) { + $section = $this->get_section($section); + if ((string)$section->name !== '') { + return format_string($section->name, true, + array('context' => context_course::instance($this->courseid))); + } else if ($section->section == 0) { + return get_string('section0name', 'format_topics'); + } else { + return get_string('topic').' '.$section->section; + } + } + + /** + * The URL to use for the specified course (with section) + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if omitted the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + $course = $this->get_course(); + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + + $sr = null; + if (array_key_exists('sr', $options)) { + $sr = $options['sr']; + } + if (is_object($section)) { + $sectionno = $section->section; + } else { + $sectionno = $section; + } + if ($sectionno !== null) { + if ($sr !== null) { + if ($sr) { + $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE; + $sectionno = $sr; + } else { + $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; + } + } else { + $usercoursedisplay = $course->coursedisplay; + } + if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { + $url->param('section', $sectionno); + } else { + if (!empty($options['navigation'])) { + return null; + } + $url->set_anchor('section-'.$sectionno); + } + } + return $url; + } + + /** + * Returns the information about the ajax support in the given source format + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * The property (array)testedbrowsers can be used as a parameter for {@link ajaxenabled()}. + * + * @return stdClass + */ + public function supports_ajax() { + $ajaxsupport = new stdClass(); + $ajaxsupport->capable = true; + $ajaxsupport->testedbrowsers = array('MSIE' => 6.0, 'Gecko' => 20061111, 'Safari' => 531, 'Chrome' => 6.0); + return $ajaxsupport; + } + + /** + * Loads all of the course sections into the navigation + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + global $PAGE; + // if section is specified in course/view.php, make sure it is expanded in navigation + if ($navigation->includesectionnum === false) { + $selectedsection = optional_param('section', null, PARAM_INT); + if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && + $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { + $navigation->includesectionnum = $selectedsection; + } + } + + // check if there are callbacks to extend course navigation + parent::extend_course_navigation($navigation, $node); + } + + /** + * Custom action after section has been moved in AJAX mode + * + * Used in course/rest.php + * + * @return array This will be passed in ajax respose + */ + function ajax_section_move() { + global $PAGE; + $titles = array(); + $course = $this->get_course(); + $modinfo = get_fast_modinfo($course); + $renderer = $this->get_renderer($PAGE); + if ($renderer && ($sections = $modinfo->get_section_info_all())) { + foreach ($sections as $number => $section) { + $titles[$number] = $renderer->section_title($section, $course); + } + } + return array('sectiontitles' => $titles, 'action' => 'move'); + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + return array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array('search_forums', 'news_items', 'calendar_upcoming', 'recent_activity') + ); + } + + /** + * Definitions of the additional options that this course format uses for course + * + * Topics format uses the following options: + * - coursedisplay + * - numsections + * - hiddensections + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + static $courseformatoptions = false; + if ($courseformatoptions === false) { + $courseconfig = get_config('moodlecourse'); + $courseformatoptions = array( + 'numsections' => array( + 'default' => $courseconfig->numsections, + 'type' => PARAM_INT, + ), + 'hiddensections' => array( + 'default' => $courseconfig->hiddensections, + 'type' => PARAM_INT, + ), + 'coursedisplay' => array( + 'default' => $courseconfig->coursedisplay, + 'type' => PARAM_INT, + ), + ); + } + if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { + $courseconfig = get_config('moodlecourse'); + $max = $courseconfig->maxsections; + if (!isset($max) || !is_numeric($max)) { + $max = 52; + } + $sectionmenu = array(); + for ($i = 0; $i <= $max; $i++) { + $sectionmenu[$i] = "$i"; + } + $courseformatoptionsedit = array( + 'numsections' => array( + 'label' => new lang_string('numberweeks'), + 'element_type' => 'select', + 'element_attributes' => array($sectionmenu), + ), + 'hiddensections' => array( + 'label' => new lang_string('hiddensections'), + 'help' => 'hiddensections', + 'help_component' => 'moodle', + 'element_type' => 'select', + 'element_attributes' => array( + array( + 0 => new lang_string('hiddensectionscollapsed'), + 1 => new lang_string('hiddensectionsinvisible') + ) + ), + ), + 'coursedisplay' => array( + 'label' => new lang_string('coursedisplay'), + 'element_type' => 'select', + 'element_attributes' => array( + array( + COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), + COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi') + ) + ), + 'help' => 'coursedisplay', + 'help_component' => 'moodle', + ) + ); + $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); + } + return $courseformatoptions; + } + + /** + * Updates format options for a course + * + * In case if course format was changed to 'topics', we try to copy options + * 'coursedisplay', 'numsections' and 'hiddensections' from the previous format. + * If previous course format did not have 'numsections' option, we populate it with the + * current number of sections + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @param stdClass $oldcourse if this function is called from {@link update_course()} + * this object contains information about the course before update + * @return bool whether there were any changes to the options values + */ + public function update_course_format_options($data, $oldcourse = null) { + global $DB; + if ($oldcourse !== null) { + $data = (array)$data; + $oldcourse = (array)$oldcourse; + $options = $this->course_format_options(); + foreach ($options as $key => $unused) { + if (!array_key_exists($key, $data)) { + if (array_key_exists($key, $oldcourse)) { + $data[$key] = $oldcourse[$key]; + } else if ($key === 'numsections') { + // If previous format does not have the field 'numsections' + // and $data['numsections'] is not set, + // we fill it with the maximum section number from the DB + $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections} + WHERE course = ?', array($this->courseid)); + if ($maxsection) { + // If there are no sections, or just default 0-section, 'numsections' will be set to default + $data['numsections'] = $maxsection; + } + } + } + } + } + return $this->update_format_options($data); + } +} diff --git a/format/topics/renderer.php b/format/topics/renderer.php new file mode 100644 index 0000000..e293a9d --- /dev/null +++ b/format/topics/renderer.php @@ -0,0 +1,105 @@ +. + +/** + * Renderer for outputting the topics course format. + * + * @package format_topics + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.3 + */ + + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot.'/course/format/renderer.php'); + +/** + * Basic renderer for topics format. + * + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_topics_renderer extends format_section_renderer_base { + + /** + * Generate the starting container html for a list of sections + * @return string HTML to output. + */ + protected function start_section_list() { + return html_writer::start_tag('ul', array('class' => 'topics')); + } + + /** + * Generate the closing container html for a list of sections + * @return string HTML to output. + */ + protected function end_section_list() { + return html_writer::end_tag('ul'); + } + + /** + * Generate the title for this section page + * @return string the page title + */ + protected function page_title() { + return get_string('topicoutline'); + } + + /** + * Generate the edit controls of a section + * + * @param stdClass $course The course entry from DB + * @param stdClass $section The course_section entry from DB + * @param bool $onsectionpage true if being printed on a section page + * @return array of links with edit controls + */ + protected function section_edit_controls($course, $section, $onsectionpage = false) { + global $PAGE; + + if (!$PAGE->user_is_editing()) { + return array(); + } + + $coursecontext = context_course::instance($course->id); + + if ($onsectionpage) { + $url = course_get_url($course, $section->section); + } else { + $url = course_get_url($course); + } + $url->param('sesskey', sesskey()); + + $controls = array(); + if (has_capability('moodle/course:setcurrentsection', $coursecontext)) { + if ($course->marker == $section->section) { // Show the "light globe" on/off. + $url->param('marker', 0); + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marked'), + 'class' => 'icon ', 'alt' => get_string('markedthistopic'))), + array('title' => get_string('markedthistopic'), 'class' => 'editing_highlight')); + } else { + $url->param('marker', $section->section); + $controls[] = html_writer::link($url, + html_writer::empty_tag('img', array('src' => $this->output->pix_url('i/marker'), + 'class' => 'icon', 'alt' => get_string('markthistopic'))), + array('title' => get_string('markthistopic'), 'class' => 'editing_highlight')); + } + } + + return array_merge($controls, parent::section_edit_controls($course, $section, $onsectionpage)); + } +} diff --git a/format/topics/styles.css b/format/topics/styles.css new file mode 100644 index 0000000..432ad96 --- /dev/null +++ b/format/topics/styles.css @@ -0,0 +1,10 @@ +.course-content ul.topics {margin:0;} +.course-content ul.topics li.section {list-style: none;margin:0 0 5px 0;padding:0;} +.course-content ul.topics li.section .content {margin:0 40px;} +.course-content ul.topics li.section .left {float:left;} +.course-content ul.topics li.section .right {float:right;} +.course-content ul.topics li.section .left, +.course-content ul.topics li.section .right {width:40px;text-align:center;padding: 6px 0;} +.course-content ul.topics li.section .right img.icon { padding: 0 0 4px 0;} +.course-content ul.topics li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; } +.jumpmenu {text-align:center;} diff --git a/format/topics/version.php b/format/topics/version.php new file mode 100644 index 0000000..dff32bf --- /dev/null +++ b/format/topics/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package format + * @subpackage topics + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2012112900; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2012112900; // Requires this Moodle version. +$plugin->component = 'format_topics'; // Full name of the plugin (used for diagnostics). diff --git a/format/upgrade.txt b/format/upgrade.txt new file mode 100644 index 0000000..4c5d131 --- /dev/null +++ b/format/upgrade.txt @@ -0,0 +1,29 @@ +This files describes API changes for course formats + +Overview of this plugin type at http://docs.moodle.org/dev/Course_formats + +=== 2.4 === + +Course format API has been changed significantly. Instead of implementing callbacks course formats +may overwrite the class format_base. See format_legacy class for a template for upgrading course +format. + +* Function settings_navigation::add_course_editing_links() is completely removed, course format + functions callback_XXXX_request_key() are no longer used (where XXXX is the course format name) +* functions get_generic_section_name(), get_all_sections(), add_mod_to_section(), get_all_mods() + are deprecated. See their phpdocs in lib/deprecatedlib.php on how to replace them +* Course formats may now have their settings.php file as the most of other plugin types +* Function format_section_renderer_base::is_section_current() is deprecated, overwrite/use + function is_section_current in format class + +=== 2.3 === + +* The new $course->coursedisplay option was introduced, users can now choose to display + a section at a time if the course formats support it: + - COURSE_DISPLAY_SINGLEPAGE indicates the teacher has chosen to display all sections on one page + - COURSE_DISPLAY_MULTIPAGE indicates the teacher has chose to have seperate pages with each section. + +* The parameter for 'currently active section' was standardised in core: + - The course format is passed the currently live section through the $displaysection varaible to format.php + - A 'section' paramter is the standardised way to pass around the current section in a course + - Navigation no longer looks for custom parameters defined in callback_format_request_key diff --git a/format/weeks/format.js b/format/weeks/format.js new file mode 100644 index 0000000..6433702 --- /dev/null +++ b/format/weeks/format.js @@ -0,0 +1,73 @@ +// Javascript functions for Weeks course format + +M.course = M.course || {}; + +M.course.format = M.course.format || {}; + +/** + * Get sections config for this format + * + * The section structure is: + *
    + *
  • ...
  • + *
  • ...
  • + * ... + *
+ * + * @return {object} section list configuration + */ +M.course.format.get_config = function() { + return { + container_node : 'ul', + container_class : 'weeks', + section_node : 'li', + section_class : 'section' + }; +} + +/** + * Swap section + * + * @param {YUI} Y YUI3 instance + * @param {string} node1 node to swap to + * @param {string} node2 node to swap with + * @return {NodeList} section list + */ +M.course.format.swap_sections = function(Y, node1, node2) { + var CSS = { + COURSECONTENT : 'course-content', + SECTIONADDMENUS : 'section_add_menus' + }; + + var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y)); + // Swap menus. + sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS)); +} + +/** + * Process sections after ajax response + * + * @param {YUI} Y YUI3 instance + * @param {array} response ajax response + * @param {string} sectionfrom first affected section + * @param {string} sectionto last affected section + * @return void + */ +M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { + var CSS = { + SECTIONNAME : 'sectionname' + }; + + if (response.action == 'move') { + // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. + if (sectionfrom > sectionto) { + var temp = sectionto; + sectionto = sectionfrom; + sectionfrom = temp; + } + // Update titles in all affected sections. + for (var i = sectionfrom; i <= sectionto; i++) { + sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]); + } + } +} diff --git a/format/weeks/format.php b/format/weeks/format.php new file mode 100644 index 0000000..2f98855 --- /dev/null +++ b/format/weeks/format.php @@ -0,0 +1,52 @@ +. + +/** + * Weeks course format. Display the whole course as "weeks" made of modules. + * + * @package format_weeks + * @copyright 2006 The Open University + * @author N.D.Freear@open.ac.uk, and others. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +// Horrible backwards compatible parameter aliasing.. +if ($week = optional_param('week', 0, PARAM_INT)) { + $url = $PAGE->url; + $url->param('section', $week); + debugging('Outdated week param passed to course/view.php', DEBUG_DEVELOPER); + redirect($url); +} +// End backwards-compatible aliasing.. + +// make sure all sections are created +$course = course_get_format($course)->get_course(); +course_create_sections_if_missing($course, range(0, $course->numsections)); + +$renderer = $PAGE->get_renderer('format_weeks'); + +if (!empty($displaysection)) { + $renderer->print_single_section_page($course, null, null, null, null, $displaysection); +} else { + $renderer->print_multiple_section_page($course, null, null, null, null); +} + +$PAGE->requires->js('/course/format/weeks/format.js'); diff --git a/format/weeks/lang/en/format_weeks.php b/format/weeks/lang/en/format_weeks.php new file mode 100644 index 0000000..fd6c24d --- /dev/null +++ b/format/weeks/lang/en/format_weeks.php @@ -0,0 +1,33 @@ +. + +/** + * Strings for component 'format_weeks', language 'en', branch 'MOODLE_20_STABLE' + * + * @package format_weeks + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['currentsection'] = 'This week'; +$string['sectionname'] = 'Week'; +$string['pluginname'] = 'Weekly format'; +$string['section0name'] = 'General'; +$string['page-course-view-weeks'] = 'Any course main page in weeks format'; +$string['page-course-view-weeks-x'] = 'Any course page in weeks format'; +$string['hidefromothers'] = 'Hide week'; +$string['showfromothers'] = 'Show week'; diff --git a/format/weeks/lib.php b/format/weeks/lib.php new file mode 100644 index 0000000..40686d4 --- /dev/null +++ b/format/weeks/lib.php @@ -0,0 +1,349 @@ +. + +/** + * This file contains main class for the course format Weeks + * + * @since 2.0 + * @package format_weeks + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot. '/course/format/lib.php'); + +/** + * Main class for the Weeks course format + * + * @package format_weeks + * @copyright 2012 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_weeks extends format_base { + + /** + * Returns true if this course format uses sections + * + * @return bool + */ + public function uses_sections() { + return true; + } + + /** + * Returns the display name of the given section that the course prefers. + * + * @param int|stdClass $section Section object from database or just field section.section + * @return string Display name that the course format prefers, e.g. "Topic 2" + */ + public function get_section_name($section) { + $section = $this->get_section($section); + if ((string)$section->name !== '') { + // Return the name the user set. + return format_string($section->name, true, array('context' => context_course::instance($this->courseid))); + } else if ($section->section == 0) { + // Return the general section. + return get_string('section0name', 'format_weeks'); + } else { + $dates = $this->get_section_dates($section); + + // We subtract 24 hours for display purposes. + $dates->end = ($dates->end - 86400); + + $dateformat = ' '.get_string('strftimedateshort'); + $weekday = userdate($dates->start, $dateformat); + $endweekday = userdate($dates->end, $dateformat); + return $weekday.' - '.$endweekday; + } + } + + /** + * The URL to use for the specified course (with section) + * + * @param int|stdClass $section Section object from database or just field course_sections.section + * if omitted the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return null|moodle_url + */ + public function get_view_url($section, $options = array()) { + $course = $this->get_course(); + $url = new moodle_url('/course/view.php', array('id' => $course->id)); + + $sr = null; + if (array_key_exists('sr', $options)) { + $sr = $options['sr']; + } + if (is_object($section)) { + $sectionno = $section->section; + } else { + $sectionno = $section; + } + if ($sectionno !== null) { + if ($sr !== null) { + if ($sr) { + $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE; + $sectionno = $sr; + } else { + $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; + } + } else { + $usercoursedisplay = $course->coursedisplay; + } + if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { + $url->param('section', $sectionno); + } else { + if (!empty($options['navigation'])) { + return null; + } + $url->set_anchor('section-'.$sectionno); + } + } + return $url; + } + + /** + * Returns the information about the ajax support in the given source format + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * The property (array)testedbrowsers can be used as a parameter for {@link ajaxenabled()}. + * + * @return stdClass + */ + public function supports_ajax() { + $ajaxsupport = new stdClass(); + $ajaxsupport->capable = true; + $ajaxsupport->testedbrowsers = array('MSIE' => 6.0, 'Gecko' => 20061111, 'Safari' => 531, 'Chrome' => 6.0); + return $ajaxsupport; + } + + /** + * Loads all of the course sections into the navigation + * + * @param global_navigation $navigation + * @param navigation_node $node The course node within the navigation + */ + public function extend_course_navigation($navigation, navigation_node $node) { + global $PAGE; + // if section is specified in course/view.php, make sure it is expanded in navigation + if ($navigation->includesectionnum === false) { + $selectedsection = optional_param('section', null, PARAM_INT); + if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && + $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { + $navigation->includesectionnum = $selectedsection; + } + } + parent::extend_course_navigation($navigation, $node); + } + + /** + * Custom action after section has been moved in AJAX mode + * + * Used in course/rest.php + * + * @return array This will be passed in ajax respose + */ + function ajax_section_move() { + global $PAGE; + $titles = array(); + $course = $this->get_course(); + $modinfo = get_fast_modinfo($course); + $renderer = $this->get_renderer($PAGE); + if ($renderer && ($sections = $modinfo->get_section_info_all())) { + foreach ($sections as $number => $section) { + $titles[$number] = $renderer->section_title($section, $course); + } + } + return array('sectiontitles' => $titles, 'action' => 'move'); + } + + /** + * Returns the list of blocks to be automatically added for the newly created course + * + * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT + * each of values is an array of block names (for left and right side columns) + */ + public function get_default_blocks() { + return array( + BLOCK_POS_LEFT => array(), + BLOCK_POS_RIGHT => array('search_forums', 'news_items', 'calendar_upcoming', 'recent_activity') + ); + } + + /** + * Definitions of the additional options that this course format uses for course + * + * Weeks format uses the following options: + * - coursedisplay + * - numsections + * - hiddensections + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + static $courseformatoptions = false; + if ($courseformatoptions === false) { + $courseconfig = get_config('moodlecourse'); + $courseformatoptions = array( + 'numsections' => array( + 'default' => $courseconfig->numsections, + 'type' => PARAM_INT, + ), + 'hiddensections' => array( + 'default' => $courseconfig->hiddensections, + 'type' => PARAM_INT, + ), + 'coursedisplay' => array( + 'default' => $courseconfig->coursedisplay, + 'type' => PARAM_INT, + ), + ); + } + if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { + $courseconfig = get_config('moodlecourse'); + $sectionmenu = array(); + $max = $courseconfig->maxsections; + if (!isset($max) || !is_numeric($max)) { + $max = 52; + } + for ($i = 0; $i <= $max; $i++) { + $sectionmenu[$i] = "$i"; + } + $courseformatoptionsedit = array( + 'numsections' => array( + 'label' => new lang_string('numberweeks'), + 'element_type' => 'select', + 'element_attributes' => array($sectionmenu), + ), + 'hiddensections' => array( + 'label' => new lang_string('hiddensections'), + 'help' => 'hiddensections', + 'help_component' => 'moodle', + 'element_type' => 'select', + 'element_attributes' => array( + array( + 0 => new lang_string('hiddensectionscollapsed'), + 1 => new lang_string('hiddensectionsinvisible') + ) + ), + ), + 'coursedisplay' => array( + 'label' => new lang_string('coursedisplay'), + 'element_type' => 'select', + 'element_attributes' => array( + array( + COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), + COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi') + ) + ), + 'help' => 'coursedisplay', + 'help_component' => 'moodle', + ) + ); + $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); + } + return $courseformatoptions; + } + + /** + * Updates format options for a course + * + * In case if course format was changed to 'weeks', we try to copy options + * 'coursedisplay', 'numsections' and 'hiddensections' from the previous format. + * If previous course format did not have 'numsections' option, we populate it with the + * current number of sections + * + * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data + * @param stdClass $oldcourse if this function is called from {@link update_course()} + * this object contains information about the course before update + * @return bool whether there were any changes to the options values + */ + public function update_course_format_options($data, $oldcourse = null) { + global $DB; + if ($oldcourse !== null) { + $data = (array)$data; + $oldcourse = (array)$oldcourse; + $options = $this->course_format_options(); + foreach ($options as $key => $unused) { + if (!array_key_exists($key, $data)) { + if (array_key_exists($key, $oldcourse)) { + $data[$key] = $oldcourse[$key]; + } else if ($key === 'numsections') { + // If previous format does not have the field 'numsections' + // and $data['numsections'] is not set, + // we fill it with the maximum section number from the DB + $maxsection = $DB->get_field_sql('SELECT max(section) from {course_sections} + WHERE course = ?', array($this->courseid)); + if ($maxsection) { + // If there are no sections, or just default 0-section, 'numsections' will be set to default + $data['numsections'] = $maxsection; + } + } + } + } + } + return $this->update_format_options($data); + } + + /** + * Return the start and end date of the passed section + * + * @param int|stdClass|section_info $section section to get the dates for + * @return stdClass property start for startdate, property end for enddate + */ + public function get_section_dates($section) { + $course = $this->get_course(); + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + $oneweekseconds = 604800; + // Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight + // savings and the date changes. + $startdate = $course->startdate + 7200; + + $dates = new stdClass(); + $dates->start = $startdate + ($oneweekseconds * ($sectionnum - 1)); + $dates->end = $dates->start + $oneweekseconds; + + return $dates; + } + + /** + * Returns true if the specified week is current + * + * @param int|stdClass|section_info $section + * @return bool + */ + public function is_section_current($section) { + if (is_object($section)) { + $sectionnum = $section->section; + } else { + $sectionnum = $section; + } + if ($sectionnum < 1) { + return false; + } + $timenow = time(); + $dates = $this->get_section_dates($section); + return (($timenow >= $dates->start) && ($timenow < $dates->end)); + } +} diff --git a/format/weeks/renderer.php b/format/weeks/renderer.php new file mode 100644 index 0000000..33590fd --- /dev/null +++ b/format/weeks/renderer.php @@ -0,0 +1,62 @@ +. + +/** + * Renderer for outputting the weeks course format. + * + * @package format_weeks + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.3 + */ + + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot.'/course/format/renderer.php'); +require_once($CFG->dirroot.'/course/format/weeks/lib.php'); + + +/** + * Basic renderer for weeks format. + * + * @copyright 2012 Dan Poltawski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_weeks_renderer extends format_section_renderer_base { + /** + * Generate the starting container html for a list of sections + * @return string HTML to output. + */ + protected function start_section_list() { + return html_writer::start_tag('ul', array('class' => 'weeks')); + } + + /** + * Generate the closing container html for a list of sections + * @return string HTML to output. + */ + protected function end_section_list() { + return html_writer::end_tag('ul'); + } + + /** + * Generate the title for this section page + * @return string the page title + */ + protected function page_title() { + return get_string('weeklyoutline'); + } +} diff --git a/format/weeks/styles.css b/format/weeks/styles.css new file mode 100644 index 0000000..80d3479 --- /dev/null +++ b/format/weeks/styles.css @@ -0,0 +1,10 @@ +.course-content ul.weeks {margin:0;} +.course-content ul.weeks li.section {list-style: none;margin:0 0 5px 0;padding:0;} +.course-content ul.weeks li.section .content {margin:0 40px;} +.course-content ul.weeks li.section .left {float:left;} +.course-content ul.weeks li.section .right {float:right;} +.course-content ul.weeks li.section .left, +.course-content ul.weeks li.section .right {width:40px;text-align:center;padding: 6px 0;} +.course-content ul.weeks li.section .right img.icon { padding: 0 0 4px 0;} +.course-content ul.weeks li.section .left .section-handle img.icon { padding:0; vertical-align: baseline; } +.jumpmenu {text-align:center;} \ No newline at end of file diff --git a/format/weeks/version.php b/format/weeks/version.php new file mode 100644 index 0000000..16971a6 --- /dev/null +++ b/format/weeks/version.php @@ -0,0 +1,30 @@ +. + +/** + * Version details + * + * @package format + * @subpackage weeks + * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2012112900; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2012112900; // Requires this Moodle version. +$plugin->component = 'format_weeks'; // Full name of the plugin (used for diagnostics). diff --git a/index.php b/index.php new file mode 100644 index 0000000..7253d44 --- /dev/null +++ b/index.php @@ -0,0 +1,398 @@ +. + +/** + * For most people, just lists the course categories + * Allows the admin to create, delete and rename course categories + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once("../config.php"); +require_once("lib.php"); + +$categoryedit = optional_param('categoryedit', -1,PARAM_BOOL); +$delete = optional_param('delete',0,PARAM_INT); +$hide = optional_param('hide',0,PARAM_INT); +$show = optional_param('show',0,PARAM_INT); +$move = optional_param('move',0,PARAM_INT); +$moveto = optional_param('moveto',-1,PARAM_INT); +$moveup = optional_param('moveup',0,PARAM_INT); +$movedown = optional_param('movedown',0,PARAM_INT); + +$site = get_site(); + +$systemcontext = context_system::instance(); + +$PAGE->set_url('/course/index.php'); +$PAGE->set_context($systemcontext); +$PAGE->set_pagelayout('admin'); + +if (can_edit_in_category()) { + if ($categoryedit !== -1) { + $USER->editing = $categoryedit; + } + require_login(); + $adminediting = $PAGE->user_is_editing(); +} else { + if ($CFG->forcelogin) { + require_login(); + } + $adminediting = false; +} + +$stradministration = get_string('administration'); +$strcategories = get_string('categories'); +$strcategory = get_string('category'); +$strcourses = get_string('courses'); +$stredit = get_string('edit'); +$strdelete = get_string('delete'); +$straction = get_string('action'); +$strfulllistofcourses = get_string('fulllistofcourses'); + + +// Unless it's an editing admin, just print the regular listing of courses/categories. +if (!$adminediting) { + $showaddcoursebutton = true; + // Print form for creating new categories. + $countcategories = $DB->count_records('course_categories'); + if ($countcategories > 1 || ($countcategories == 1 && $DB->count_records('course') > 200)) { + $strcourses = get_string('courses'); + $strcategories = get_string('categories'); + + $PAGE->navbar->add($strcategories); + $PAGE->set_title("$site->shortname: $strcategories"); + $PAGE->set_heading($COURSE->fullname); + $PAGE->set_button(update_category_button()); + echo $OUTPUT->header(); + echo $OUTPUT->heading($strcategories); + echo $OUTPUT->skip_link_target(); + echo $OUTPUT->box_start('categorybox'); + print_whole_category_list(); + echo $OUTPUT->box_end(); + print_course_search(); + } else { + $PAGE->navbar->add($strfulllistofcourses); + $PAGE->set_title("$site->shortname: $strfulllistofcourses"); + $PAGE->set_heading($COURSE->fullname); + $PAGE->set_button(update_category_button()); + echo $OUTPUT->header(); + echo $OUTPUT->skip_link_target(); + echo $OUTPUT->box_start('courseboxes'); + $showaddcoursebutton = print_courses(0); + echo $OUTPUT->box_end(); + } + + echo $OUTPUT->container_start('buttons'); + if (has_capability('moodle/course:create', $systemcontext) && $showaddcoursebutton) { + // Print link to create a new course, for the 1st available category. + $options = array('category' => $CFG->defaultrequestcategory); + echo $OUTPUT->single_button(new moodle_url('edit.php', $options), get_string('addnewcourse'), 'get'); + } + print_course_request_buttons($systemcontext); + echo $OUTPUT->container_end(); + echo $OUTPUT->footer(); + exit; +} +/// Everything else is editing on mode. +require_once($CFG->libdir.'/adminlib.php'); +admin_externalpage_setup('coursemgmt'); + +/// Delete a category. +if (!empty($delete) and confirm_sesskey()) { + if (!$deletecat = $DB->get_record('course_categories', array('id'=>$delete))) { + print_error('invalidcategoryid'); + } + $context = context_coursecat::instance($delete); + require_capability('moodle/category:manage', $context); + require_capability('moodle/category:manage', get_category_or_system_context($deletecat->parent)); + + $heading = get_string('deletecategory', 'moodle', format_string($deletecat->name, true, array('context' => $context))); + require_once('delete_category_form.php'); + $mform = new delete_category_form(null, $deletecat); + $mform->set_data(array('delete'=>$delete)); + + if ($mform->is_cancelled()) { + redirect('index.php'); + + } else if (!$data= $mform->get_data()) { + require_once($CFG->libdir . '/questionlib.php'); + echo $OUTPUT->header(); + echo $OUTPUT->heading($heading); + $mform->display(); + echo $OUTPUT->footer(); + exit(); + } + + echo $OUTPUT->header(); + echo $OUTPUT->heading($heading); + + if ($data->fulldelete) { + $deletedcourses = category_delete_full($deletecat, true); + + foreach($deletedcourses as $course) { + echo $OUTPUT->notification(get_string('coursedeleted', '', $course->shortname), 'notifysuccess'); + } + echo $OUTPUT->notification(get_string('coursecategorydeleted', '', format_string($deletecat->name, true, array('context' => $context))), 'notifysuccess'); + + } else { + category_delete_move($deletecat, $data->newparent, true); + } + + // If we deleted $CFG->defaultrequestcategory, make it point somewhere else. + if ($delete == $CFG->defaultrequestcategory) { + set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent'=>0))); + } + + echo $OUTPUT->continue_button('index.php'); + + echo $OUTPUT->footer(); + die; +} + +/// Create a default category if necessary +if (!$categories = get_categories()) { /// No category yet! + // Try and make one + $tempcat = new stdClass(); + $tempcat->name = get_string('miscellaneous'); + $tempcat->id = $DB->insert_record('course_categories', $tempcat); + $tempcat->context = context_coursecat::instance($tempcat->id); + mark_context_dirty('/'.SYSCONTEXTID); + fix_course_sortorder(); // Required to build course_categories.depth and .path. + set_config('defaultrequestcategory', $tempcat->id); +} + +/// Move a category to a new parent if required +if (!empty($move) and ($moveto >= 0) and confirm_sesskey()) { + if ($cattomove = $DB->get_record('course_categories', array('id'=>$move))) { + require_capability('moodle/category:manage', get_category_or_system_context($cattomove->parent)); + if ($cattomove->parent != $moveto) { + $newparent = $DB->get_record('course_categories', array('id'=>$moveto)); + require_capability('moodle/category:manage', get_category_or_system_context($moveto)); + move_category($cattomove, $newparent); + } + } +} + +/// Hide or show a category +if ($hide and confirm_sesskey()) { + if ($tempcat = $DB->get_record('course_categories', array('id'=>$hide))) { + require_capability('moodle/category:manage', get_category_or_system_context($tempcat->parent)); + if ($tempcat->visible == 1) { + course_category_hide($tempcat); + } + } +} else if ($show and confirm_sesskey()) { + if ($tempcat = $DB->get_record('course_categories', array('id'=>$show))) { + require_capability('moodle/category:manage', get_category_or_system_context($tempcat->parent)); + if ($tempcat->visible == 0) { + course_category_show($tempcat); + } + } +} + +/// Move a category up or down +if ((!empty($moveup) or !empty($movedown)) and confirm_sesskey()) { + fix_course_sortorder(); + $swapcategory = NULL; + + if (!empty($moveup)) { + require_capability('moodle/category:manage', context_coursecat::instance($moveup)); + if ($movecategory = $DB->get_record('course_categories', array('id'=>$moveup))) { + if ($swapcategory = $DB->get_records_select('course_categories', "sortordersortorder, $movecategory->parent), 'sortorder DESC', '*', 0, 1)) { + $swapcategory = reset($swapcategory); + } + } + } else { + require_capability('moodle/category:manage', context_coursecat::instance($movedown)); + if ($movecategory = $DB->get_record('course_categories', array('id'=>$movedown))) { + if ($swapcategory = $DB->get_records_select('course_categories', "sortorder>? AND parent=?", array($movecategory->sortorder, $movecategory->parent), 'sortorder ASC', '*', 0, 1)) { + $swapcategory = reset($swapcategory); + } + } + } + if ($swapcategory and $movecategory) { + $DB->set_field('course_categories', 'sortorder', $swapcategory->sortorder, array('id'=>$movecategory->id)); + $DB->set_field('course_categories', 'sortorder', $movecategory->sortorder, array('id'=>$swapcategory->id)); + add_to_log(SITEID, "category", "move", "editcategory.php?id=$movecategory->id", $movecategory->id); + } + + // finally reorder courses + fix_course_sortorder(); +} + +/// Print headings +echo $OUTPUT->header(); +echo $OUTPUT->heading($strcategories); + +/// Print out the categories with all the knobs +$strcategories = get_string('categories'); +$strcourses = get_string('courses'); +$strmovecategoryto = get_string('movecategoryto'); +$stredit = get_string('edit'); + +$displaylist = array(); +$parentlist = array(); + +$displaylist[0] = get_string('top'); +make_categories_list($displaylist, $parentlist); + +echo ''; +echo ''; +echo ''; +echo ''; +echo ''; +echo ''; + +print_category_edit(NULL, $displaylist, $parentlist); +echo '
'.$strcategories.''.$strcourses.''.$stredit.''.$strmovecategoryto.'
'; + +echo '
'; +if (has_capability('moodle/course:create', $systemcontext)) { + // print create course link to first category + $options = array('category' => $CFG->defaultrequestcategory); + $options['returnto'] = 'topcat'; + echo $OUTPUT->single_button(new moodle_url('edit.php', $options), get_string('addnewcourse'), 'get'); +} + +// Print button for creating new categories +if (has_capability('moodle/category:manage', $systemcontext)) { + $options = array('parent'=>0); + echo $OUTPUT->single_button(new moodle_url('editcategory.php', $options), get_string('addnewcategory'), 'get'); +} + +print_course_request_buttons($systemcontext); +echo '
'; + +echo $OUTPUT->footer(); + +function print_category_edit($category, $displaylist, $parentslist, $depth=-1, $up=false, $down=false) { +/// Recursive function to print all the categories ready for editing + + global $CFG, $USER, $OUTPUT; + + static $str = NULL; + + if (is_null($str)) { + $str = new stdClass; + $str->edit = get_string('edit'); + $str->delete = get_string('delete'); + $str->moveup = get_string('moveup'); + $str->movedown = get_string('movedown'); + $str->edit = get_string('editthiscategory'); + $str->hide = get_string('hide'); + $str->show = get_string('show'); + $str->cohorts = get_string('cohorts', 'cohort'); + $str->spacer = $OUTPUT->spacer().' '; + } + + if (!empty($category)) { + + if (!isset($category->context)) { + $category->context = context_coursecat::instance($category->id); + } + + echo ''; + for ($i=0; $i<$depth;$i++) { + echo '      '; + } + $linkcss = $category->visible ? '' : ' class="dimmed" '; + echo ''. + format_string($category->name, true, array('context' => $category->context)).''; + echo ''; + + echo ''.$category->coursecount.''; + + echo ''; /// Print little icons + + if (has_capability('moodle/category:manage', $category->context)) { + echo ' '; + + echo ' '; + + if (!empty($category->visible)) { + echo ' '; + } else { + echo ' '; + } + + if (has_capability('moodle/cohort:manage', $category->context) or has_capability('moodle/cohort:view', $category->context)) { + echo ' '; + } + + if ($up) { + echo ' '; + } else { + echo $str->spacer; + } + if ($down) { + echo ' '; + } else { + echo $str->spacer; + } + } + echo ''; + + echo ''; + if (has_capability('moodle/category:manage', $category->context)) { + $tempdisplaylist = $displaylist; + unset($tempdisplaylist[$category->id]); + foreach ($parentslist as $key => $parents) { + if (in_array($category->id, $parents)) { + unset($tempdisplaylist[$key]); + } + } + $popupurl = new moodle_url("index.php?move=$category->id&sesskey=".sesskey()); + $select = new single_select($popupurl, 'moveto', $tempdisplaylist, $category->parent, null, "moveform$category->id"); + $select->set_label(get_string('frontpagecategorynames'), array('class' => 'accesshide')); + echo $OUTPUT->render($select); + } + echo ''; + echo ''; + } else { + $category = new stdClass(); + $category->id = '0'; + } + + if ($categories = get_categories($category->id)) { // Print all the children recursively + $countcats = count($categories); + $count = 0; + $first = true; + $last = false; + foreach ($categories as $cat) { + $count++; + if ($count == $countcats) { + $last = true; + } + $up = $first ? false : true; + $down = $last ? false : true; + $first = false; + + print_category_edit($cat, $displaylist, $parentslist, $depth+1, $up, $down); + } + } +} diff --git a/info.php b/info.php new file mode 100644 index 0000000..9440168 --- /dev/null +++ b/info.php @@ -0,0 +1,61 @@ +get_record("course", array("shortname"=>$name))) { + print_error("invalidshortname"); + } + } else { + if (!$course = $DB->get_record("course", array("id"=>$id))) { + print_error("invalidcourseid"); + } + } + + $site = get_site(); + + if ($CFG->forcelogin) { + require_login(); + } + + $context = context_course::instance($course->id); + if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) { + print_error('coursehidden', '', $CFG->wwwroot .'/'); + } + + $PAGE->set_context($context); + $PAGE->set_pagelayout('popup'); + $PAGE->set_url('/course/info.php', array('id' => $course->id)); + $PAGE->set_title(get_string("summaryof", "", $course->fullname)); + $PAGE->set_heading(get_string('courseinfo')); + $PAGE->set_course($course); + $PAGE->navbar->add(get_string('summary')); + + echo $OUTPUT->header(); + echo $OUTPUT->heading(''.format_string($course->fullname) . '
(' . format_string($course->shortname, true, array('context' => $context)) . ')'); + + // print enrol info + if ($texts = enrol_get_course_description_texts($course)) { + echo $OUTPUT->box_start('generalbox icons'); + echo implode($texts); + echo $OUTPUT->box_end(); + } + + $courserenderer = $PAGE->get_renderer('core', 'course'); + echo $courserenderer->course_info_box($course); + + echo "
"; + + echo $OUTPUT->footer(); + + diff --git a/jumpto.php b/jumpto.php new file mode 100644 index 0000000..c4d11ef --- /dev/null +++ b/jumpto.php @@ -0,0 +1,41 @@ +. + +/** + * Jumps to a given relative or Moodle absolute URL. + * Mostly used for accessibility. + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require('../config.php'); + +$jump = required_param('jump', PARAM_RAW); + +$PAGE->set_url('/course/jumpto.php'); + +if (!confirm_sesskey()) { + print_error('confirmsesskeybad'); +} + +if (strpos($jump, '/') === 0) { + redirect(new moodle_url($jump)); +} else { + print_error('error'); +} diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..f89572c --- /dev/null +++ b/lib.php @@ -0,0 +1,4755 @@ +. + +/** + * Library of useful functions + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + * @subpackage course + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir.'/completionlib.php'); +require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->dirroot.'/course/dnduploadlib.php'); +require_once($CFG->dirroot.'/course/format/lib.php'); + +define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records +define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds + +/** + * Number of courses to display when summaries are included. + * @var int + * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead. + */ +define('COURSE_MAX_SUMMARIES_PER_PAGE', 10); + +define('COURSE_MAX_COURSES_PER_DROPDOWN',1000); // max courses in log dropdown before switching to optional +define('COURSE_MAX_USERS_PER_DROPDOWN',1000); // max users in log dropdown before switching to optional +define('FRONTPAGENEWS', '0'); +define('FRONTPAGECOURSELIST', '1'); +define('FRONTPAGECATEGORYNAMES', '2'); +define('FRONTPAGETOPICONLY', '3'); +define('FRONTPAGECATEGORYCOMBO', '4'); +define('FRONTPAGECOURSELIMIT', 200); // maximum number of courses displayed on the frontpage +define('EXCELROWS', 65535); +define('FIRSTUSEDEXCELROW', 3); + +define('MOD_CLASS_ACTIVITY', 0); +define('MOD_CLASS_RESOURCE', 1); + +function make_log_url($module, $url) { + switch ($module) { + case 'course': + if (strpos($url, 'report/') === 0) { + // there is only one report type, course reports are deprecated + $url = "/$url"; + break; + } + case 'file': + case 'login': + case 'lib': + case 'admin': + case 'calendar': + case 'category': + case 'mnet course': + if (strpos($url, '../') === 0) { + $url = ltrim($url, '.'); + } else { + $url = "/course/$url"; + } + break; + case 'user': + case 'blog': + $url = "/$module/$url"; + break; + case 'upload': + $url = $url; + break; + case 'coursetags': + $url = '/'.$url; + break; + case 'library': + case '': + $url = '/'; + break; + case 'message': + $url = "/message/$url"; + break; + case 'notes': + $url = "/notes/$url"; + break; + case 'tag': + $url = "/tag/$url"; + break; + case 'role': + $url = '/'.$url; + break; + default: + $url = "/mod/$module/$url"; + break; + } + + //now let's sanitise urls - there might be some ugly nasties:-( + $parts = explode('?', $url); + $script = array_shift($parts); + if (strpos($script, 'http') === 0) { + $script = clean_param($script, PARAM_URL); + } else { + $script = clean_param($script, PARAM_PATH); + } + + $query = ''; + if ($parts) { + $query = implode('', $parts); + $query = str_replace('&', '&', $query); // both & and & are stored in db :-| + $parts = explode('&', $query); + $eq = urlencode('='); + foreach ($parts as $key=>$part) { + $part = urlencode(urldecode($part)); + $part = str_replace($eq, '=', $part); + $parts[$key] = $part; + } + $query = '?'.implode('&', $parts); + } + + return $script.$query; +} + + +function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='', + $modname="", $modid=0, $modaction="", $groupid=0) { + global $CFG, $DB; + + // It is assumed that $date is the GMT time of midnight for that day, + // and so the next 86400 seconds worth of logs are printed. + + /// Setup for group handling. + + // TODO: I don't understand group/context/etc. enough to be able to do + // something interesting with it here + // What is the context of a remote course? + + /// If the group mode is separate, and this user does not have editing privileges, + /// then only the user's group can be viewed. + //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) { + // $groupid = get_current_group($course->id); + //} + /// If this course doesn't have groups, no groupid can be specified. + //else if (!$course->groupmode) { + // $groupid = 0; + //} + + $groupid = 0; + + $joins = array(); + $where = ''; + + $qry = "SELECT l.*, u.firstname, u.lastname, u.picture + FROM {mnet_log} l + LEFT JOIN {user} u ON l.userid = u.id + WHERE "; + $params = array(); + + $where .= "l.hostid = :hostid"; + $params['hostid'] = $hostid; + + // TODO: Is 1 really a magic number referring to the sitename? + if ($course != SITEID || $modid != 0) { + $where .= " AND l.course=:courseid"; + $params['courseid'] = $course; + } + + if ($modname) { + $where .= " AND l.module = :modname"; + $params['modname'] = $modname; + } + + if ('site_errors' === $modid) { + $where .= " AND ( l.action='error' OR l.action='infected' )"; + } else if ($modid) { + //TODO: This assumes that modids are the same across sites... probably + //not true + $where .= " AND l.cmid = :modid"; + $params['modid'] = $modid; + } + + if ($modaction) { + $firstletter = substr($modaction, 0, 1); + if ($firstletter == '-') { + $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true); + $params['modaction'] = '%'.substr($modaction, 1).'%'; + } else { + $where .= " AND ".$DB->sql_like('l.action', ':modaction', false); + $params['modaction'] = '%'.$modaction.'%'; + } + } + + if ($user) { + $where .= " AND l.userid = :user"; + $params['user'] = $user; + } + + if ($date) { + $enddate = $date + 86400; + $where .= " AND l.time > :date AND l.time < :enddate"; + $params['date'] = $date; + $params['enddate'] = $enddate; + } + + $result = array(); + $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params); + if(!empty($result['totalcount'])) { + $where .= " ORDER BY $order"; + $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum); + } else { + $result['logs'] = array(); + } + return $result; +} + +function build_logs_array($course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='', + $modname="", $modid=0, $modaction="", $groupid=0) { + global $DB, $SESSION, $USER; + // It is assumed that $date is the GMT time of midnight for that day, + // and so the next 86400 seconds worth of logs are printed. + + /// Setup for group handling. + + /// If the group mode is separate, and this user does not have editing privileges, + /// then only the user's group can be viewed. + if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) { + if (isset($SESSION->currentgroup[$course->id])) { + $groupid = $SESSION->currentgroup[$course->id]; + } else { + $groupid = groups_get_all_groups($course->id, $USER->id); + if (is_array($groupid)) { + $groupid = array_shift(array_keys($groupid)); + $SESSION->currentgroup[$course->id] = $groupid; + } else { + $groupid = 0; + } + } + } + /// If this course doesn't have groups, no groupid can be specified. + else if (!$course->groupmode) { + $groupid = 0; + } + + $joins = array(); + $params = array(); + + if ($course->id != SITEID || $modid != 0) { + $joins[] = "l.course = :courseid"; + $params['courseid'] = $course->id; + } + + if ($modname) { + $joins[] = "l.module = :modname"; + $params['modname'] = $modname; + } + + if ('site_errors' === $modid) { + $joins[] = "( l.action='error' OR l.action='infected' )"; + } else if ($modid) { + $joins[] = "l.cmid = :modid"; + $params['modid'] = $modid; + } + + if ($modaction) { + $firstletter = substr($modaction, 0, 1); + if ($firstletter == '-') { + $joins[] = $DB->sql_like('l.action', ':modaction', false, true, true); + $params['modaction'] = '%'.substr($modaction, 1).'%'; + } else { + $joins[] = $DB->sql_like('l.action', ':modaction', false); + $params['modaction'] = '%'.$modaction.'%'; + } + } + + + /// Getting all members of a group. + if ($groupid and !$user) { + if ($gusers = groups_get_members($groupid)) { + $gusers = array_keys($gusers); + $joins[] = 'l.userid IN (' . implode(',', $gusers) . ')'; + } else { + $joins[] = 'l.userid = 0'; // No users in groups, so we want something that will always be false. + } + } + else if ($user) { + $joins[] = "l.userid = :userid"; + $params['userid'] = $user; + } + + if ($date) { + $enddate = $date + 86400; + $joins[] = "l.time > :date AND l.time < :enddate"; + $params['date'] = $date; + $params['enddate'] = $enddate; + } + + $selector = implode(' AND ', $joins); + + $totalcount = 0; // Initialise + $result = array(); + $result['logs'] = get_logs($selector, $params, $order, $limitfrom, $limitnum, $totalcount); + $result['totalcount'] = $totalcount; + return $result; +} + + +function print_log($course, $user=0, $date=0, $order="l.time ASC", $page=0, $perpage=100, + $url="", $modname="", $modid=0, $modaction="", $groupid=0) { + + global $CFG, $DB, $OUTPUT; + + if (!$logs = build_logs_array($course, $user, $date, $order, $page*$perpage, $perpage, + $modname, $modid, $modaction, $groupid)) { + echo $OUTPUT->notification("No logs found!"); + echo $OUTPUT->footer(); + exit; + } + + $courses = array(); + + if ($course->id == SITEID) { + $courses[0] = ''; + if ($ccc = get_courses('all', 'c.id ASC', 'c.id,c.shortname')) { + foreach ($ccc as $cc) { + $courses[$cc->id] = $cc->shortname; + } + } + } else { + $courses[$course->id] = $course->shortname; + } + + $totalcount = $logs['totalcount']; + $count=0; + $ldcache = array(); + $tt = getdate(time()); + $today = mktime (0, 0, 0, $tt["mon"], $tt["mday"], $tt["year"]); + + $strftimedatetime = get_string("strftimedatetime"); + + echo "
\n"; + print_string("displayingrecords", "", $totalcount); + echo "
\n"; + + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "$url&perpage=$perpage"); + + $table = new html_table(); + $table->classes = array('logtable','generalbox'); + $table->align = array('right', 'left', 'left'); + $table->head = array( + get_string('time'), + get_string('ip_address'), + get_string('fullnameuser'), + get_string('action'), + get_string('info') + ); + $table->data = array(); + + if ($course->id == SITEID) { + array_unshift($table->align, 'left'); + array_unshift($table->head, get_string('course')); + } + + // Make sure that the logs array is an array, even it is empty, to avoid warnings from the foreach. + if (empty($logs['logs'])) { + $logs['logs'] = array(); + } + + foreach ($logs['logs'] as $log) { + + if (isset($ldcache[$log->module][$log->action])) { + $ld = $ldcache[$log->module][$log->action]; + } else { + $ld = $DB->get_record('log_display', array('module'=>$log->module, 'action'=>$log->action)); + $ldcache[$log->module][$log->action] = $ld; + } + if ($ld && is_numeric($log->info)) { + // ugly hack to make sure fullname is shown correctly + if ($ld->mtable == 'user' && $ld->field == $DB->sql_concat('firstname', "' '" , 'lastname')) { + $log->info = fullname($DB->get_record($ld->mtable, array('id'=>$log->info)), true); + } else { + $log->info = $DB->get_field($ld->mtable, $ld->field, array('id'=>$log->info)); + } + } + + //Filter log->info + $log->info = format_string($log->info); + + // If $log->url has been trimmed short by the db size restriction + // code in add_to_log, keep a note so we don't add a link to a broken url + $brokenurl=(textlib::strlen($log->url)==100 && textlib::substr($log->url,97)=='...'); + + $row = array(); + if ($course->id == SITEID) { + if (empty($log->course)) { + $row[] = get_string('site'); + } else { + $row[] = "wwwroot}/course/view.php?id={$log->course}\">". format_string($courses[$log->course]).""; + } + } + + $row[] = userdate($log->time, '%a').' '.userdate($log->time, $strftimedatetime); + + $link = new moodle_url("/iplookup/index.php?ip=$log->ip&user=$log->userid"); + $row[] = $OUTPUT->action_link($link, $log->ip, new popup_action('click', $link, 'iplookup', array('height' => 440, 'width' => 700))); + + $row[] = html_writer::link(new moodle_url("/user/view.php?id={$log->userid}&course={$log->course}"), fullname($log, has_capability('moodle/site:viewfullnames', context_course::instance($course->id)))); + + $displayaction="$log->module $log->action"; + if ($brokenurl) { + $row[] = $displayaction; + } else { + $link = make_log_url($log->module,$log->url); + $row[] = $OUTPUT->action_link($link, $displayaction, new popup_action('click', $link, 'fromloglive'), array('height' => 440, 'width' => 700)); + } + $row[] = $log->info; + $table->data[] = $row; + } + + echo html_writer::table($table); + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "$url&perpage=$perpage"); +} + + +function print_mnet_log($hostid, $course, $user=0, $date=0, $order="l.time ASC", $page=0, $perpage=100, + $url="", $modname="", $modid=0, $modaction="", $groupid=0) { + + global $CFG, $DB, $OUTPUT; + + if (!$logs = build_mnet_logs_array($hostid, $course, $user, $date, $order, $page*$perpage, $perpage, + $modname, $modid, $modaction, $groupid)) { + echo $OUTPUT->notification("No logs found!"); + echo $OUTPUT->footer(); + exit; + } + + if ($course->id == SITEID) { + $courses[0] = ''; + if ($ccc = get_courses('all', 'c.id ASC', 'c.id,c.shortname,c.visible')) { + foreach ($ccc as $cc) { + $courses[$cc->id] = $cc->shortname; + } + } + } + + $totalcount = $logs['totalcount']; + $count=0; + $ldcache = array(); + $tt = getdate(time()); + $today = mktime (0, 0, 0, $tt["mon"], $tt["mday"], $tt["year"]); + + $strftimedatetime = get_string("strftimedatetime"); + + echo "
\n"; + print_string("displayingrecords", "", $totalcount); + echo "
\n"; + + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "$url&perpage=$perpage"); + + echo "\n"; + echo ""; + if ($course->id == SITEID) { + echo "\n"; + } + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + + if (empty($logs['logs'])) { + echo "
".get_string('course')."".get_string('time')."".get_string('ip_address')."".get_string('fullnameuser')."".get_string('action')."".get_string('info')."
\n"; + return; + } + + $row = 1; + foreach ($logs['logs'] as $log) { + + $log->info = $log->coursename; + $row = ($row + 1) % 2; + + if (isset($ldcache[$log->module][$log->action])) { + $ld = $ldcache[$log->module][$log->action]; + } else { + $ld = $DB->get_record('log_display', array('module'=>$log->module, 'action'=>$log->action)); + $ldcache[$log->module][$log->action] = $ld; + } + if (0 && $ld && !empty($log->info)) { + // ugly hack to make sure fullname is shown correctly + if (($ld->mtable == 'user') and ($ld->field == $DB->sql_concat('firstname', "' '" , 'lastname'))) { + $log->info = fullname($DB->get_record($ld->mtable, array('id'=>$log->info)), true); + } else { + $log->info = $DB->get_field($ld->mtable, $ld->field, array('id'=>$log->info)); + } + } + + //Filter log->info + $log->info = format_string($log->info); + + echo ''; + if ($course->id == SITEID) { + $courseshortname = format_string($courses[$log->course], true, array('context' => context_course::instance(SITEID))); + echo "\n"; + echo " wwwroot}/course/view.php?id={$log->course}\">".$courseshortname."\n"; + echo "\n"; + } + echo "".userdate($log->time, '%a'). + ' '.userdate($log->time, $strftimedatetime)."\n"; + echo "\n"; + $link = new moodle_url("/iplookup/index.php?ip=$log->ip&user=$log->userid"); + echo $OUTPUT->action_link($link, $log->ip, new popup_action('click', $link, 'iplookup', array('height' => 400, 'width' => 700))); + echo "\n"; + $fullname = fullname($log, has_capability('moodle/site:viewfullnames', context_course::instance($course->id))); + echo "\n"; + echo " wwwroot/user/view.php?id={$log->userid}\">$fullname\n"; + echo "\n"; + echo "\n"; + echo $log->action .': '.$log->module; + echo "\n";; + echo "{$log->info}\n"; + echo "\n"; + } + echo "\n"; + + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "$url&perpage=$perpage"); +} + + +function print_log_csv($course, $user, $date, $order='l.time DESC', $modname, + $modid, $modaction, $groupid) { + global $DB, $CFG; + + require_once($CFG->libdir . '/csvlib.class.php'); + + $csvexporter = new csv_export_writer('tab'); + + $header = array(); + $header[] = get_string('course'); + $header[] = get_string('time'); + $header[] = get_string('ip_address'); + $header[] = get_string('fullnameuser'); + $header[] = get_string('action'); + $header[] = get_string('info'); + + if (!$logs = build_logs_array($course, $user, $date, $order, '', '', + $modname, $modid, $modaction, $groupid)) { + return false; + } + + $courses = array(); + + if ($course->id == SITEID) { + $courses[0] = ''; + if ($ccc = get_courses('all', 'c.id ASC', 'c.id,c.shortname')) { + foreach ($ccc as $cc) { + $courses[$cc->id] = $cc->shortname; + } + } + } else { + $courses[$course->id] = $course->shortname; + } + + $count=0; + $ldcache = array(); + $tt = getdate(time()); + $today = mktime (0, 0, 0, $tt["mon"], $tt["mday"], $tt["year"]); + + $strftimedatetime = get_string("strftimedatetime"); + + $csvexporter->set_filename('logs', '.txt'); + $title = array(get_string('savedat').userdate(time(), $strftimedatetime)); + $csvexporter->add_data($title); + $csvexporter->add_data($header); + + if (empty($logs['logs'])) { + return true; + } + + foreach ($logs['logs'] as $log) { + if (isset($ldcache[$log->module][$log->action])) { + $ld = $ldcache[$log->module][$log->action]; + } else { + $ld = $DB->get_record('log_display', array('module'=>$log->module, 'action'=>$log->action)); + $ldcache[$log->module][$log->action] = $ld; + } + if ($ld && is_numeric($log->info)) { + // ugly hack to make sure fullname is shown correctly + if (($ld->mtable == 'user') and ($ld->field == $DB->sql_concat('firstname', "' '" , 'lastname'))) { + $log->info = fullname($DB->get_record($ld->mtable, array('id'=>$log->info)), true); + } else { + $log->info = $DB->get_field($ld->mtable, $ld->field, array('id'=>$log->info)); + } + } + + //Filter log->info + $log->info = format_string($log->info); + $log->info = strip_tags(urldecode($log->info)); // Some XSS protection + + $coursecontext = context_course::instance($course->id); + $firstField = format_string($courses[$log->course], true, array('context' => $coursecontext)); + $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext)); + $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url); + $row = array($firstField, userdate($log->time, $strftimedatetime), $log->ip, $fullname, $log->module.' '.$log->action.' ('.$actionurl.')', $log->info); + $csvexporter->add_data($row); + } + $csvexporter->download_file(); + return true; +} + + +function print_log_xls($course, $user, $date, $order='l.time DESC', $modname, + $modid, $modaction, $groupid) { + + global $CFG, $DB; + + require_once("$CFG->libdir/excellib.class.php"); + + if (!$logs = build_logs_array($course, $user, $date, $order, '', '', + $modname, $modid, $modaction, $groupid)) { + return false; + } + + $courses = array(); + + if ($course->id == SITEID) { + $courses[0] = ''; + if ($ccc = get_courses('all', 'c.id ASC', 'c.id,c.shortname')) { + foreach ($ccc as $cc) { + $courses[$cc->id] = $cc->shortname; + } + } + } else { + $courses[$course->id] = $course->shortname; + } + + $count=0; + $ldcache = array(); + $tt = getdate(time()); + $today = mktime (0, 0, 0, $tt["mon"], $tt["mday"], $tt["year"]); + + $strftimedatetime = get_string("strftimedatetime"); + + $nroPages = ceil(count($logs)/(EXCELROWS-FIRSTUSEDEXCELROW+1)); + $filename = 'logs_'.userdate(time(),get_string('backupnameformat', 'langconfig'),99,false); + $filename .= '.xls'; + + $workbook = new MoodleExcelWorkbook('-'); + $workbook->send($filename); + + $worksheet = array(); + $headers = array(get_string('course'), get_string('time'), get_string('ip_address'), + get_string('fullnameuser'), get_string('action'), get_string('info')); + + // Creating worksheets + for ($wsnumber = 1; $wsnumber <= $nroPages; $wsnumber++) { + $sheettitle = get_string('logs').' '.$wsnumber.'-'.$nroPages; + $worksheet[$wsnumber] = $workbook->add_worksheet($sheettitle); + $worksheet[$wsnumber]->set_column(1, 1, 30); + $worksheet[$wsnumber]->write_string(0, 0, get_string('savedat'). + userdate(time(), $strftimedatetime)); + $col = 0; + foreach ($headers as $item) { + $worksheet[$wsnumber]->write(FIRSTUSEDEXCELROW-1,$col,$item,''); + $col++; + } + } + + if (empty($logs['logs'])) { + $workbook->close(); + return true; + } + + $formatDate =& $workbook->add_format(); + $formatDate->set_num_format(get_string('log_excel_date_format')); + + $row = FIRSTUSEDEXCELROW; + $wsnumber = 1; + $myxls =& $worksheet[$wsnumber]; + foreach ($logs['logs'] as $log) { + if (isset($ldcache[$log->module][$log->action])) { + $ld = $ldcache[$log->module][$log->action]; + } else { + $ld = $DB->get_record('log_display', array('module'=>$log->module, 'action'=>$log->action)); + $ldcache[$log->module][$log->action] = $ld; + } + if ($ld && is_numeric($log->info)) { + // ugly hack to make sure fullname is shown correctly + if (($ld->mtable == 'user') and ($ld->field == $DB->sql_concat('firstname', "' '" , 'lastname'))) { + $log->info = fullname($DB->get_record($ld->mtable, array('id'=>$log->info)), true); + } else { + $log->info = $DB->get_field($ld->mtable, $ld->field, array('id'=>$log->info)); + } + } + + // Filter log->info + $log->info = format_string($log->info); + $log->info = strip_tags(urldecode($log->info)); // Some XSS protection + + if ($nroPages>1) { + if ($row > EXCELROWS) { + $wsnumber++; + $myxls =& $worksheet[$wsnumber]; + $row = FIRSTUSEDEXCELROW; + } + } + + $coursecontext = context_course::instance($course->id); + + $myxls->write($row, 0, format_string($courses[$log->course], true, array('context' => $coursecontext)), ''); + $myxls->write_date($row, 1, $log->time, $formatDate); // write_date() does conversion/timezone support. MDL-14934 + $myxls->write($row, 2, $log->ip, ''); + $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext)); + $myxls->write($row, 3, $fullname, ''); + $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url); + $myxls->write($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')', ''); + $myxls->write($row, 5, $log->info, ''); + + $row++; + } + + $workbook->close(); + return true; +} + +function print_log_ods($course, $user, $date, $order='l.time DESC', $modname, + $modid, $modaction, $groupid) { + + global $CFG, $DB; + + require_once("$CFG->libdir/odslib.class.php"); + + if (!$logs = build_logs_array($course, $user, $date, $order, '', '', + $modname, $modid, $modaction, $groupid)) { + return false; + } + + $courses = array(); + + if ($course->id == SITEID) { + $courses[0] = ''; + if ($ccc = get_courses('all', 'c.id ASC', 'c.id,c.shortname')) { + foreach ($ccc as $cc) { + $courses[$cc->id] = $cc->shortname; + } + } + } else { + $courses[$course->id] = $course->shortname; + } + + $count=0; + $ldcache = array(); + $tt = getdate(time()); + $today = mktime (0, 0, 0, $tt["mon"], $tt["mday"], $tt["year"]); + + $strftimedatetime = get_string("strftimedatetime"); + + $nroPages = ceil(count($logs)/(EXCELROWS-FIRSTUSEDEXCELROW+1)); + $filename = 'logs_'.userdate(time(),get_string('backupnameformat', 'langconfig'),99,false); + $filename .= '.ods'; + + $workbook = new MoodleODSWorkbook('-'); + $workbook->send($filename); + + $worksheet = array(); + $headers = array(get_string('course'), get_string('time'), get_string('ip_address'), + get_string('fullnameuser'), get_string('action'), get_string('info')); + + // Creating worksheets + for ($wsnumber = 1; $wsnumber <= $nroPages; $wsnumber++) { + $sheettitle = get_string('logs').' '.$wsnumber.'-'.$nroPages; + $worksheet[$wsnumber] = $workbook->add_worksheet($sheettitle); + $worksheet[$wsnumber]->set_column(1, 1, 30); + $worksheet[$wsnumber]->write_string(0, 0, get_string('savedat'). + userdate(time(), $strftimedatetime)); + $col = 0; + foreach ($headers as $item) { + $worksheet[$wsnumber]->write(FIRSTUSEDEXCELROW-1,$col,$item,''); + $col++; + } + } + + if (empty($logs['logs'])) { + $workbook->close(); + return true; + } + + $formatDate =& $workbook->add_format(); + $formatDate->set_num_format(get_string('log_excel_date_format')); + + $row = FIRSTUSEDEXCELROW; + $wsnumber = 1; + $myxls =& $worksheet[$wsnumber]; + foreach ($logs['logs'] as $log) { + if (isset($ldcache[$log->module][$log->action])) { + $ld = $ldcache[$log->module][$log->action]; + } else { + $ld = $DB->get_record('log_display', array('module'=>$log->module, 'action'=>$log->action)); + $ldcache[$log->module][$log->action] = $ld; + } + if ($ld && is_numeric($log->info)) { + // ugly hack to make sure fullname is shown correctly + if (($ld->mtable == 'user') and ($ld->field == $DB->sql_concat('firstname', "' '" , 'lastname'))) { + $log->info = fullname($DB->get_record($ld->mtable, array('id'=>$log->info)), true); + } else { + $log->info = $DB->get_field($ld->mtable, $ld->field, array('id'=>$log->info)); + } + } + + // Filter log->info + $log->info = format_string($log->info); + $log->info = strip_tags(urldecode($log->info)); // Some XSS protection + + if ($nroPages>1) { + if ($row > EXCELROWS) { + $wsnumber++; + $myxls =& $worksheet[$wsnumber]; + $row = FIRSTUSEDEXCELROW; + } + } + + $coursecontext = context_course::instance($course->id); + + $myxls->write_string($row, 0, format_string($courses[$log->course], true, array('context' => $coursecontext))); + $myxls->write_date($row, 1, $log->time); + $myxls->write_string($row, 2, $log->ip); + $fullname = fullname($log, has_capability('moodle/site:viewfullnames', $coursecontext)); + $myxls->write_string($row, 3, $fullname); + $actionurl = $CFG->wwwroot. make_log_url($log->module,$log->url); + $myxls->write_string($row, 4, $log->module.' '.$log->action.' ('.$actionurl.')'); + $myxls->write_string($row, 5, $log->info); + + $row++; + } + + $workbook->close(); + return true; +} + + +function print_overview($courses, array $remote_courses=array()) { + global $CFG, $USER, $DB, $OUTPUT; + + $htmlarray = array(); + if ($modules = $DB->get_records('modules')) { + foreach ($modules as $mod) { + if (file_exists(dirname(dirname(__FILE__)).'/mod/'.$mod->name.'/lib.php')) { + include_once(dirname(dirname(__FILE__)).'/mod/'.$mod->name.'/lib.php'); + $fname = $mod->name.'_print_overview'; + if (function_exists($fname)) { + $fname($courses,$htmlarray); + } + } + } + } + foreach ($courses as $course) { + $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); + echo $OUTPUT->box_start('coursebox'); + $attributes = array('title' => s($fullname)); + if (empty($course->visible)) { + $attributes['class'] = 'dimmed'; + } + echo $OUTPUT->heading(html_writer::link( + new moodle_url('/course/view.php', array('id' => $course->id)), $fullname, $attributes), 3); + if (array_key_exists($course->id,$htmlarray)) { + foreach ($htmlarray[$course->id] as $modname => $html) { + echo $html; + } + } + echo $OUTPUT->box_end(); + } + + if (!empty($remote_courses)) { + echo $OUTPUT->heading(get_string('remotecourses', 'mnet')); + } + foreach ($remote_courses as $course) { + echo $OUTPUT->box_start('coursebox'); + $attributes = array('title' => s($course->fullname)); + echo $OUTPUT->heading(html_writer::link( + new moodle_url('/auth/mnet/jump.php', array('hostid' => $course->hostid, 'wantsurl' => '/course/view.php?id='.$course->remoteid)), + format_string($course->shortname), + $attributes) . ' (' . format_string($course->hostname) . ')', 3); + echo $OUTPUT->box_end(); + } +} + + +/** + * This function trawls through the logs looking for + * anything new since the user's last login + */ +function print_recent_activity($course) { + // $course is an object + global $CFG, $USER, $SESSION, $DB, $OUTPUT; + + $context = context_course::instance($course->id); + + $viewfullnames = has_capability('moodle/site:viewfullnames', $context); + + $timestart = round(time() - COURSE_MAX_RECENT_PERIOD, -2); // better db caching for guests - 100 seconds + + if (!isguestuser()) { + if (!empty($USER->lastcourseaccess[$course->id])) { + if ($USER->lastcourseaccess[$course->id] > $timestart) { + $timestart = $USER->lastcourseaccess[$course->id]; + } + } + } + + echo '
'; + echo get_string('activitysince', '', userdate($timestart)); + echo '
'; + echo '
'; + + echo ''.get_string('recentactivityreport').''; + + echo "
\n"; + + $content = false; + +/// Firstly, have there been any new enrolments? + + $users = get_recent_enrolments($course->id, $timestart); + + //Accessibility: new users now appear in an
    list. + if ($users) { + echo '
    '; + echo $OUTPUT->heading(get_string("newusers").':', 3); + $content = true; + echo "
      \n"; + foreach ($users as $user) { + $fullname = fullname($user, $viewfullnames); + echo '
    1. wwwroot/user/view.php?id=$user->id&course=$course->id\">$fullname
    2. \n"; + } + echo "
    \n
    \n"; + } + +/// Next, have there been any modifications to the course structure? + + $modinfo = get_fast_modinfo($course); + + $changelist = array(); + + $logs = $DB->get_records_select('log', "time > ? AND course = ? AND + module = 'course' AND + (action = 'add mod' OR action = 'update mod' OR action = 'delete mod')", + array($timestart, $course->id), "id ASC"); + + if ($logs) { + $actions = array('add mod', 'update mod', 'delete mod'); + $newgones = array(); // added and later deleted items + foreach ($logs as $key => $log) { + if (!in_array($log->action, $actions)) { + continue; + } + $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; + } + + if (count($info) != 2) { + debugging("Incorrect log entry info: id = ".$log->id, DEBUG_DEVELOPER); + continue; + } + + $modname = $info[0]; + $instanceid = $info[1]; + + if ($log->action == 'delete mod') { + // unfortunately we do not know if the mod was visible + if (!array_key_exists($log->info, $newgones)) { + $strdeleted = get_string('deletedactivity', 'moodle', get_string('modulename', $modname)); + $changelist[$log->info] = array ('operation' => 'delete', 'text' => $strdeleted); + } + } else { + if (!isset($modinfo->instances[$modname][$instanceid])) { + if ($log->action == 'add mod') { + // do not display added and later deleted activities + $newgones[$log->info] = true; + } + continue; + } + $cm = $modinfo->instances[$modname][$instanceid]; + if (!$cm->uservisible) { + continue; + } + + if ($log->action == 'add mod') { + $stradded = get_string('added', 'moodle', get_string('modulename', $modname)); + $changelist[$log->info] = array('operation' => 'add', 'text' => "$stradded:
    wwwroot/mod/$cm->modname/view.php?id={$cm->id}\">".format_string($cm->name, true).""); + + } else if ($log->action == 'update mod' and empty($changelist[$log->info])) { + $strupdated = get_string('updated', 'moodle', get_string('modulename', $modname)); + $changelist[$log->info] = array('operation' => 'update', 'text' => "$strupdated:
    wwwroot/mod/$cm->modname/view.php?id={$cm->id}\">".format_string($cm->name, true).""); + } + } + } + } + + if (!empty($changelist)) { + echo $OUTPUT->heading(get_string("courseupdates").':', 3); + $content = true; + foreach ($changelist as $changeinfo => $change) { + echo '

    '.$change['text'].'

    '; + } + } + +/// Now display new things from each module + + $usedmodules = array(); + foreach($modinfo->cms as $cm) { + if (isset($usedmodules[$cm->modname])) { + continue; + } + if (!$cm->uservisible) { + continue; + } + $usedmodules[$cm->modname] = $cm->modname; + } + + foreach ($usedmodules as $modname) { // Each module gets it's own logs and prints them + if (file_exists($CFG->dirroot.'/mod/'.$modname.'/lib.php')) { + include_once($CFG->dirroot.'/mod/'.$modname.'/lib.php'); + $print_recent_activity = $modname.'_print_recent_activity'; + if (function_exists($print_recent_activity)) { + // NOTE: original $isteacher (second parameter below) was replaced with $viewfullnames! + $content = $print_recent_activity($course, $viewfullnames, $timestart) || $content; + } + } else { + debugging("Missing lib.php in lib/{$modname} - please reinstall files or uninstall the module"); + } + } + + if (! $content) { + echo '

    '.get_string('nothingnew').'

    '; + } +} + +/** + * For a given course, returns an array of course activity objects + * Each item in the array contains he following properties: + */ +function get_array_of_activities($courseid) { +// cm - course module id +// mod - name of the module (eg forum) +// section - the number of the section (eg week or topic) +// name - the name of the instance +// visible - is the instance visible or not +// groupingid - grouping id +// groupmembersonly - is this instance visible to group members only +// extra - contains extra string to include in any link + global $CFG, $DB; + if(!empty($CFG->enableavailability)) { + require_once($CFG->libdir.'/conditionlib.php'); + } + + $course = $DB->get_record('course', array('id'=>$courseid)); + + if (empty($course)) { + throw new moodle_exception('courseidnotfound'); + } + + $mod = array(); + + $rawmods = get_course_mods($courseid); + if (empty($rawmods)) { + return $mod; // always return array + } + + if ($sections = $DB->get_records("course_sections", array("course"=>$courseid), "section ASC")) { + foreach ($sections as $section) { + if (!empty($section->sequence)) { + $sequence = explode(",", $section->sequence); + foreach ($sequence as $seq) { + if (empty($rawmods[$seq])) { + continue; + } + $mod[$seq] = new stdClass(); + $mod[$seq]->id = $rawmods[$seq]->instance; + $mod[$seq]->cm = $rawmods[$seq]->id; + $mod[$seq]->mod = $rawmods[$seq]->modname; + + // Oh dear. Inconsistent names left here for backward compatibility. + $mod[$seq]->section = $section->section; + $mod[$seq]->sectionid = $rawmods[$seq]->section; + + $mod[$seq]->module = $rawmods[$seq]->module; + $mod[$seq]->added = $rawmods[$seq]->added; + $mod[$seq]->score = $rawmods[$seq]->score; + $mod[$seq]->idnumber = $rawmods[$seq]->idnumber; + $mod[$seq]->visible = $rawmods[$seq]->visible; + $mod[$seq]->visibleold = $rawmods[$seq]->visibleold; + $mod[$seq]->groupmode = $rawmods[$seq]->groupmode; + $mod[$seq]->groupingid = $rawmods[$seq]->groupingid; + $mod[$seq]->groupmembersonly = $rawmods[$seq]->groupmembersonly; + $mod[$seq]->indent = $rawmods[$seq]->indent; + $mod[$seq]->completion = $rawmods[$seq]->completion; + $mod[$seq]->extra = ""; + $mod[$seq]->completiongradeitemnumber = + $rawmods[$seq]->completiongradeitemnumber; + $mod[$seq]->completionview = $rawmods[$seq]->completionview; + $mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected; + $mod[$seq]->availablefrom = $rawmods[$seq]->availablefrom; + $mod[$seq]->availableuntil = $rawmods[$seq]->availableuntil; + $mod[$seq]->showavailability = $rawmods[$seq]->showavailability; + $mod[$seq]->showdescription = $rawmods[$seq]->showdescription; + if (!empty($CFG->enableavailability)) { + condition_info::fill_availability_conditions($rawmods[$seq]); + $mod[$seq]->conditionscompletion = $rawmods[$seq]->conditionscompletion; + $mod[$seq]->conditionsgrade = $rawmods[$seq]->conditionsgrade; + $mod[$seq]->conditionsfield = $rawmods[$seq]->conditionsfield; + } + + $modname = $mod[$seq]->mod; + $functionname = $modname."_get_coursemodule_info"; + + if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) { + continue; + } + + include_once("$CFG->dirroot/mod/$modname/lib.php"); + + if ($hasfunction = function_exists($functionname)) { + if ($info = $functionname($rawmods[$seq])) { + if (!empty($info->icon)) { + $mod[$seq]->icon = $info->icon; + } + if (!empty($info->iconcomponent)) { + $mod[$seq]->iconcomponent = $info->iconcomponent; + } + 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->iconurl)) { + $mod[$seq]->iconurl = $info->iconurl; + } + 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; + } + } + } + } + // When there is no modname_get_coursemodule_info function, + // but showdescriptions is enabled, then we use the 'intro' + // and 'introformat' fields in the module table + if (!$hasfunction && $rawmods[$seq]->showdescription) { + if ($modvalues = $DB->get_record($rawmods[$seq]->modname, + array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) { + // Set content from intro and introformat. Filters are disabled + // because we filter it with format_text at display time + $mod[$seq]->content = format_module_intro($rawmods[$seq]->modname, + $modvalues, $rawmods[$seq]->id, false); + + // To save making another query just below, put name in here + $mod[$seq]->name = $modvalues->name; + } + } + 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', 'iconurl', 'onclick', 'content', + 'icon', 'iconcomponent', 'customdata', 'showavailability', 'availablefrom', + 'availableuntil', 'conditionscompletion', 'conditionsgrade', + 'completionview', 'completionexpected', 'score', 'showdescription') + as $property) { + if (property_exists($mod[$seq], $property) && + empty($mod[$seq]->{$property})) { + unset($mod[$seq]->{$property}); + } + } + // Special case: this value is usually set to null, but may be 0 + if (property_exists($mod[$seq], 'completiongradeitemnumber') && + is_null($mod[$seq]->completiongradeitemnumber)) { + unset($mod[$seq]->completiongradeitemnumber); + } + } + } + } + } + return $mod; +} + +/** + * Returns the localised human-readable names of all used modules + * + * @param bool $plural if true returns the plural forms of the names + * @return array where key is the module name (component name without 'mod_') and + * the value is the human-readable string. Array sorted alphabetically by value + */ +function get_module_types_names($plural = false) { + static $modnames = null; + global $DB, $CFG; + if ($modnames === null) { + $modnames = array(0 => array(), 1 => array()); + if ($allmods = $DB->get_records("modules")) { + foreach ($allmods as $mod) { + if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) { + $modnames[0][$mod->name] = get_string("modulename", "$mod->name"); + $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name"); + } + } + collatorlib::asort($modnames[0]); + collatorlib::asort($modnames[1]); + } + } + return $modnames[(int)$plural]; +} + +/** + * Set highlighted section. Only one section can be highlighted at the time. + * + * @param int $courseid course id + * @param int $marker highlight section with this number, 0 means remove higlightin + * @return void + */ +function course_set_marker($courseid, $marker) { + global $DB; + $DB->set_field("course", "marker", $marker, array('id' => $courseid)); + format_base::reset_course_cache($courseid); +} + +/** + * For a given course section, marks it visible or hidden, + * and does the same for every activity in that section + * + * @param int $courseid course id + * @param int $sectionnumber The section number to adjust + * @param int $visibility The new visibility + * @return array A list of resources which were hidden in the section + */ +function set_section_visible($courseid, $sectionnumber, $visibility) { + global $DB; + + $resourcestotoggle = array(); + if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) { + $DB->set_field("course_sections", "visible", "$visibility", array("id"=>$section->id)); + if (!empty($section->sequence)) { + $modules = explode(",", $section->sequence); + foreach ($modules as $moduleid) { + if ($cm = $DB->get_record('course_modules', array('id' => $moduleid), 'visible, visibleold')) { + if ($visibility) { + // As we unhide the section, we use the previously saved visibility stored in visibleold. + set_coursemodule_visible($moduleid, $cm->visibleold); + } else { + // We hide the section, so we hide the module but we store the original state in visibleold. + set_coursemodule_visible($moduleid, 0); + $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid)); + } + } + } + } + rebuild_course_cache($courseid, true); + + // Determine which modules are visible for AJAX update + if (!empty($modules)) { + list($insql, $params) = $DB->get_in_or_equal($modules); + $select = 'id ' . $insql . ' AND visible = ?'; + array_push($params, $visibility); + if (!$visibility) { + $select .= ' AND visibleold = 1'; + } + $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params); + } + } + return $resourcestotoggle; +} + +/** + * 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 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()) !== '') { + // Improve filter performance by preloading filter setttings for all + // activities on the course (this does nothing if called multiple + // times) + filter_preload_activities($cm->get_modinfo()); + + // Get module context + $modulecontext = context_module::instance($cm->id); + $labelformatoptions = new stdClass(); + $labelformatoptions->noclean = true; + $labelformatoptions->overflowdiv = true; + $labelformatoptions->context = $modulecontext; + $content = format_text($content, FORMAT_HTML, $labelformatoptions); + } else { + $content = ''; + } + + // Get course context + $coursecontext = context_course::instance($course->id); + $stringoptions = new stdClass; + $stringoptions->context = $coursecontext; + $instancename = format_string($cm->name, true, $stringoptions); + return array($content, $instancename); +} + +/** + * Prints a section full of activity modules + * + * @param stdClass $course The course + * @param stdClass|section_info $section The section object containing properties id and section + * @param array $mods (argument not used) + * @param array $modnamesused (argument not used) + * @param bool $absolute All links are absolute + * @param string $width Width of the container + * @param bool $hidecompletion Hide completion status + * @param int $sectionreturn The section to return to + * @return void + */ +function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%", $hidecompletion=false, $sectionreturn=null) { + global $CFG, $USER, $DB, $PAGE, $OUTPUT; + + static $initialised; + + static $groupbuttons; + static $groupbuttonslink; + static $isediting; + static $ismoving; + static $strmovehere; + static $strmovefull; + static $strunreadpostsone; + + if (!isset($initialised)) { + $groupbuttons = ($course->groupmode or (!$course->groupmodeforce)); + $groupbuttonslink = (!$course->groupmodeforce); + $isediting = $PAGE->user_is_editing(); + $ismoving = $isediting && ismoving($course->id); + if ($ismoving) { + $strmovehere = get_string("movehere"); + $strmovefull = strip_tags(get_string("movefull", "", "'$USER->activitycopyname'")); + } + $initialised = true; + } + + $modinfo = get_fast_modinfo($course); + $completioninfo = new completion_info($course); + + //Accessibility: replace table with list
      , but don't output empty list. + if (!empty($modinfo->sections[$section->section])) { + + // Fix bug #5027, don't want style=\"width:$width\". + echo "
        \n"; + + foreach ($modinfo->sections[$section->section] as $modnumber) { + $mod = $modinfo->cms[$modnumber]; + + if ($ismoving and $mod->id == $USER->activitycopy) { + // do not display moving mod + continue; + } + + // We can continue (because it will not be displayed at all) + // if: + // 1) The activity is not visible to users + // and + // 2a) The 'showavailability' option is not set (if that is set, + // we need to display the activity so we can show + // availability info) + // or + // 2b) The 'availableinfo' is empty, i.e. the activity was + // hidden in a way that leaves no info, such as using the + // eye icon. + if (!$mod->uservisible && + (empty($mod->showavailability) || + empty($mod->availableinfo))) { + // visibility shortcut + continue; + } + + // 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 + // 2. the activity has dates set which do not include current, or + // 3. the activity has any other conditions set (regardless of whether + // current user meets them) + $modcontext = context_module::instance($mod->id); + $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext); + $accessiblebutdim = false; + $conditionalhidden = false; + if ($canviewhidden) { + $accessiblebutdim = !$mod->visible; + if (!empty($CFG->enableavailability)) { + $conditionalhidden = $mod->availablefrom > time() || + ($mod->availableuntil && $mod->availableuntil < time()) || + count($mod->conditionsgrade) > 0 || + count($mod->conditionscompletion) > 0; + } + $accessiblebutdim = $conditionalhidden || $accessiblebutdim; + } + + $liclasses = array(); + $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 ''. + ''.$strmovehere.'
        + '; + } + + $classes = array('mod-indent'); + if (!empty($mod->indent)) { + $classes[] = 'mod-indent-'.$mod->indent; + if ($mod->indent > 15) { + $classes[] = 'mod-indent-huge'; + } + } + echo html_writer::start_tag('div', array('class'=>join(' ', $classes))); + + // 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; + // 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(textlib::strtolower($instancename), + textlib::strtolower($altname))) { + $altname = ''; + } + // File type after name, for alphabetic lists (screen reader). + if ($altname) { + $altname = get_accesshide(' '.$altname); + } + + // Start the div for the activity title, excluding the edit icons. + echo html_writer::start_tag('div', array('class' => 'activityinstance')); + + // 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'; + if ($conditionalhidden) { + $linkclasses .= ' conditionalhidden'; + $textclasses .= ' conditionalhidden'; + } + $accesstext = get_accesshide(get_string('hiddenfromstudents').': '); + } else { + $accesstext = ''; + } + if ($linkclasses) { + $linkcss = trim($linkclasses) . ' '; + } else { + $linkcss = ''; + } + if ($textclasses) { + $textcss = trim($textclasses) . ' '; + } else { + $textcss = ''; + } + + // Get on-click attribute value if specified and decode the onclick - it + // has already been encoded for display (puke). + $onclick = htmlspecialchars_decode($mod->get_on_click(), ENT_QUOTES); + + $groupinglabel = ''; + if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', context_course::instance($course->id))) { + $groupings = groups_get_all_groupings($course->id); + $groupinglabel = html_writer::tag('span', '('.format_string($groupings[$mod->groupingid]->name).')', + array('class' => 'groupinglabel')); + } + + if ($url = $mod->get_url()) { + // Display link itself. + $activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(), + 'class' => 'iconlarge activityicon', 'alt' => $mod->modfullname)) . $accesstext . + html_writer::tag('span', $instancename . $altname, array('class' => 'instancename')); + echo html_writer::link($url, $activitylink, array('class' => $linkcss, 'onclick' => $onclick)) . + $groupinglabel; + + // If specified, display extra content after link. + if ($content) { + $contentpart = html_writer::tag('div', $content, array('class' => + trim('contentafterlink ' . $textclasses))); + } + } else { + // No link, so display only content. + $contentpart = html_writer::tag('div', $accesstext . $content, array('class' => $textcss)); + } + + } else { + $textclasses = $extraclasses; + $textclasses .= ' dimmed_text'; + if ($textclasses) { + $textcss = 'class="' . trim($textclasses) . '" '; + } else { + $textcss = ''; + } + $accesstext = '' . + get_string('notavailableyet', 'condition') . + ': '; + + if ($url = $mod->get_url()) { + // Display greyed-out text of link + echo '
        extra . + ' >' . ' '. $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(); + + // Closing the tag which contains everything but edit icons. $contentpart should not be part of this. + echo html_writer::end_tag('div'); + + // If there is content but NO link (eg label), then display the + // content here (BEFORE any icons). In this case cons must be + // displayed after the content so that it makes more sense visually + // and for accessibility reasons, e.g. if you have a one-line label + // it should work similarly (at least in terms of ordering) to an + // activity. + if (empty($url)) { + echo $contentpart; + } + + if ($isediting) { + if ($groupbuttons and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) { + if (! $mod->groupmodelink = $groupbuttonslink) { + $mod->groupmode = $course->groupmode; + } + + } else { + $mod->groupmode = false; + } + echo make_editing_buttons($mod, $absolute, true, $mod->indent, $sectionreturn); + echo $mod->get_after_edit_icons(); + } + + // Completion + $completion = $hidecompletion + ? COMPLETION_TRACKING_NONE + : $completioninfo->is_enabled($mod); + if ($completion!=COMPLETION_TRACKING_NONE && isloggedin() && + !isguestuser() && $mod->uservisible) { + $completiondata = $completioninfo->get_data($mod,true); + $completionicon = ''; + if ($isediting) { + switch ($completion) { + case COMPLETION_TRACKING_MANUAL : + $completionicon = 'manual-enabled'; break; + case COMPLETION_TRACKING_AUTOMATIC : + $completionicon = 'auto-enabled'; break; + default: // wtf + } + } else if ($completion==COMPLETION_TRACKING_MANUAL) { + switch($completiondata->completionstate) { + case COMPLETION_INCOMPLETE: + $completionicon = 'manual-n'; break; + case COMPLETION_COMPLETE: + $completionicon = 'manual-y'; break; + } + } else { // Automatic + switch($completiondata->completionstate) { + case COMPLETION_INCOMPLETE: + $completionicon = 'auto-n'; break; + case COMPLETION_COMPLETE: + $completionicon = 'auto-y'; break; + case COMPLETION_COMPLETE_PASS: + $completionicon = 'auto-pass'; break; + case COMPLETION_COMPLETE_FAIL: + $completionicon = 'auto-fail'; break; + } + } + if ($completionicon) { + $imgsrc = $OUTPUT->pix_url('i/completion-'.$completionicon); + $formattedname = format_string($mod->name, true, array('context' => $modcontext)); + $imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname); + if ($completion == COMPLETION_TRACKING_MANUAL && !$isediting) { + $imgtitle = get_string('completion-title-' . $completionicon, 'completion', $formattedname); + $newstate = + $completiondata->completionstate==COMPLETION_COMPLETE + ? COMPLETION_INCOMPLETE + : COMPLETION_COMPLETE; + // In manual mode the icon is a toggle form... + + // If this completion state is used by the + // conditional activities system, we need to turn + // off the JS. + if (!empty($CFG->enableavailability) && + condition_info::completion_value_used_as_condition($course, $mod)) { + $extraclass = ' preventjs'; + } else { + $extraclass = ''; + } + echo html_writer::start_tag('form', array( + 'class' => 'togglecompletion' . $extraclass, + 'method' => 'post', + 'action' => $CFG->wwwroot . '/course/togglecompletion.php')); + echo html_writer::start_tag('div'); + echo html_writer::empty_tag('input', array( + 'type' => 'hidden', 'name' => 'id', 'value' => $mod->id)); + echo html_writer::empty_tag('input', array( + 'type' => 'hidden', 'name' => 'modulename', + 'value' => $mod->name)); + echo html_writer::empty_tag('input', array( + 'type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); + echo html_writer::empty_tag('input', array( + 'type' => 'hidden', 'name' => 'completionstate', + 'value' => $newstate)); + echo html_writer::empty_tag('input', array( + 'type' => 'image', 'src' => $imgsrc, 'alt' => $imgalt, 'title' => $imgtitle)); + echo html_writer::end_tag('div'); + echo html_writer::end_tag('form'); + } else { + // In auto mode, or when editing, the icon is just an image + echo ""; + echo "$imgalt"; + } + } + } + + // If there is content AND a link, then display the content here + // (AFTER any icons). Otherwise it was displayed before + if (!empty($url)) { + echo $contentpart; + } + + // Show availability information (for someone who isn't allowed to + // see the activity itself, or for staff) + if (!$mod->uservisible) { + echo '
        '.$mod->availableinfo.'
        '; + } else if ($canviewhidden && !empty($CFG->enableavailability)) { + // Don't add availability information if user is not editing and activity is hidden. + if ($mod->visible || $PAGE->user_is_editing()) { + $hidinfoclass = ''; + if (!$mod->visible) { + $hidinfoclass = 'hide'; + } + $ci = new condition_info($mod); + $fullinfo = $ci->get_full_information(); + if($fullinfo) { + echo '
        '.get_string($mod->showavailability + ? 'userrestriction_visible' + : 'userrestriction_hidden','condition', + $fullinfo).'
        '; + } + } + } + + echo html_writer::end_tag('div'); + echo html_writer::end_tag('li')."\n"; + } + + } elseif ($ismoving) { + echo "
          \n"; + } + + if ($ismoving) { + echo '
        • '. + ''.$strmovehere.'
        • + '; + } + if (!empty($modinfo->sections[$section->section]) || $ismoving) { + echo "
        \n\n"; + } +} + +/** + * Prints the menus to add activities and resources. + * + * @param stdClass $course The course + * @param int $section relative section number (field course_sections.section) + * @param null|array $modnames An array containing the list of modules and their names + * if omitted will be taken from get_module_types_names() + * @param bool $vertical Vertical orientation + * @param bool $return Return the menus or send them to output + * @param int $sectionreturn The section to link back to + * @return void|string depending on $return + */ +function print_section_add_menus($course, $section, $modnames = null, $vertical=false, $return=false, $sectionreturn=null) { + global $CFG, $OUTPUT; + + if ($modnames === null) { + $modnames = get_module_types_names(); + } + + // check to see if user can add menus and there are modules to add + if (!has_capability('moodle/course:manageactivities', context_course::instance($course->id)) + || empty($modnames)) { + if ($return) { + return ''; + } else { + return false; + } + } + + // Retrieve all modules with associated metadata + $modules = get_module_metadata($course, $modnames, $sectionreturn); + + // We'll sort resources and activities into two lists + $resources = array(); + $activities = array(); + + // We need to add the section section to the link for each module + $sectionlink = '§ion=' . $section . '&sr=' . $sectionreturn; + + foreach ($modules as $module) { + if (isset($module->types)) { + // This module has a subtype + // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!! + $subtypes = array(); + foreach ($module->types as $subtype) { + $subtypes[$subtype->link . $sectionlink] = $subtype->title; + } + + // Sort module subtypes into the list + if (!empty($module->title)) { + // This grouping has a name + if ($module->archetype == MOD_CLASS_RESOURCE) { + $resources[] = array($module->title=>$subtypes); + } else { + $activities[] = array($module->title=>$subtypes); + } + } else { + // This grouping does not have a name + if ($module->archetype == MOD_CLASS_RESOURCE) { + $resources = array_merge($resources, $subtypes); + } else { + $activities = array_merge($activities, $subtypes); + } + } + } else { + // This module has no subtypes + if ($module->archetype == MOD_ARCHETYPE_RESOURCE) { + $resources[$module->link . $sectionlink] = $module->title; + } else if ($module->archetype === MOD_ARCHETYPE_SYSTEM) { + // System modules cannot be added by user, do not add to dropdown + } else { + $activities[$module->link . $sectionlink] = $module->title; + } + } + } + + $straddactivity = get_string('addactivity'); + $straddresource = get_string('addresource'); + $sectionname = get_section_name($course, $section); + $strresourcelabel = get_string('addresourcetosection', null, $sectionname); + $stractivitylabel = get_string('addactivitytosection', null, $sectionname); + + $output = html_writer::start_tag('div', array('class' => 'section_add_menus', 'id' => 'add_menus-section-' . $section)); + + if (!$vertical) { + $output .= html_writer::start_tag('div', array('class' => 'horizontal')); + } + + if (!empty($resources)) { + $select = new url_select($resources, '', array(''=>$straddresource), "ressection$section"); + $select->set_help_icon('resources'); + $select->set_label($strresourcelabel, array('class' => 'accesshide')); + $output .= $OUTPUT->render($select); + } + + if (!empty($activities)) { + $select = new url_select($activities, '', array(''=>$straddactivity), "section$section"); + $select->set_help_icon('activities'); + $select->set_label($stractivitylabel, array('class' => 'accesshide')); + $output .= $OUTPUT->render($select); + } + + if (!$vertical) { + $output .= html_writer::end_tag('div'); + } + + $output .= html_writer::end_tag('div'); + + if (course_ajax_enabled($course)) { + $straddeither = get_string('addresourceoractivity'); + // The module chooser link + $modchooser = html_writer::start_tag('div', array('class' => 'mdl-right')); + $modchooser.= html_writer::start_tag('div', array('class' => 'section-modchooser')); + $icon = $OUTPUT->pix_icon('t/add', ''); + $span = html_writer::tag('span', $straddeither, array('class' => 'section-modchooser-text')); + $modchooser .= html_writer::tag('span', $icon . $span, array('class' => 'section-modchooser-link')); + $modchooser.= html_writer::end_tag('div'); + $modchooser.= html_writer::end_tag('div'); + + // Wrap the normal output in a noscript div + $usemodchooser = get_user_preferences('usemodchooser', $CFG->modchooserdefault); + if ($usemodchooser) { + $output = html_writer::tag('div', $output, array('class' => 'hiddenifjs addresourcedropdown')); + $modchooser = html_writer::tag('div', $modchooser, array('class' => 'visibleifjs addresourcemodchooser')); + } else { + // If the module chooser is disabled, we need to ensure that the dropdowns are shown even if javascript is disabled + $output = html_writer::tag('div', $output, array('class' => 'show addresourcedropdown')); + $modchooser = html_writer::tag('div', $modchooser, array('class' => 'hide addresourcemodchooser')); + } + $output = $modchooser . $output; + } + + if ($return) { + return $output; + } else { + echo $output; + } +} + +/** + * Retrieve all metadata for the requested modules + * + * @param object $course The Course + * @param array $modnames An array containing the list of modules and their + * names + * @param int $sectionreturn The section to return to + * @return array A list of stdClass objects containing metadata about each + * module + */ +function get_module_metadata($course, $modnames, $sectionreturn = null) { + global $CFG, $OUTPUT; + + // get_module_metadata will be called once per section on the page and courses may show + // different modules to one another + static $modlist = array(); + if (!isset($modlist[$course->id])) { + $modlist[$course->id] = array(); + } + + $return = array(); + $urlbase = "/course/mod.php?id=$course->id&sesskey=".sesskey().'&sr='.$sectionreturn.'&add='; + foreach($modnames as $modname => $modnamestr) { + if (!course_allowed_module($course, $modname)) { + continue; + } + if (isset($modlist[$course->id][$modname])) { + // This module is already cached + $return[$modname] = $modlist[$course->id][$modname]; + continue; + } + + // Include the module lib + $libfile = "$CFG->dirroot/mod/$modname/lib.php"; + if (!file_exists($libfile)) { + continue; + } + include_once($libfile); + + // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!! + $gettypesfunc = $modname.'_get_types'; + if (function_exists($gettypesfunc)) { + $types = $gettypesfunc(); + if (is_array($types) && count($types) > 0) { + $group = new stdClass(); + $group->name = $modname; + $group->icon = $OUTPUT->pix_icon('icon', '', $modname, array('class' => 'icon')); + foreach($types as $type) { + if ($type->typestr === '--') { + continue; + } + if (strpos($type->typestr, '--') === 0) { + $group->title = str_replace('--', '', $type->typestr); + continue; + } + // Set the Sub Type metadata + $subtype = new stdClass(); + $subtype->title = $type->typestr; + $subtype->type = str_replace('&', '&', $type->type); + $subtype->name = preg_replace('/.*type=/', '', $subtype->type); + $subtype->archetype = $type->modclass; + + // The group archetype should match the subtype archetypes and all subtypes + // should have the same archetype + $group->archetype = $subtype->archetype; + + if (get_string_manager()->string_exists('help' . $subtype->name, $modname)) { + $subtype->help = get_string('help' . $subtype->name, $modname); + } + $subtype->link = $urlbase . $subtype->type; + $group->types[] = $subtype; + } + $modlist[$course->id][$modname] = $group; + } + } else { + $module = new stdClass(); + $module->title = get_string('modulename', $modname); + $module->name = $modname; + $module->link = $urlbase . $modname; + $module->icon = $OUTPUT->pix_icon('icon', '', $module->name, array('class' => 'icon')); + $sm = get_string_manager(); + if ($sm->string_exists('modulename_help', $modname)) { + $module->help = get_string('modulename_help', $modname); + if ($sm->string_exists('modulename_link', $modname)) { // Link to further info in Moodle docs + $link = get_string('modulename_link', $modname); + $linktext = get_string('morehelp'); + $module->help .= html_writer::tag('div', $OUTPUT->doc_link($link, $linktext, true), array('class' => 'helpdoclink')); + } + } + $module->archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); + $modlist[$course->id][$modname] = $module; + } + if (isset($modlist[$course->id][$modname])) { + $return[$modname] = $modlist[$course->id][$modname]; + } else { + debugging("Invalid module metadata configuration for {$modname}"); + } + } + + return $return; +} + +/** + * Return the course category context for the category with id $categoryid, except + * that if $categoryid is 0, return the system context. + * + * @param integer $categoryid a category id or 0. + * @return object the corresponding context + */ +function get_category_or_system_context($categoryid) { + if ($categoryid) { + return context_coursecat::instance($categoryid, IGNORE_MISSING); + } else { + return context_system::instance(); + } +} + +/** + * Gets the child categories of a given courses category. Uses a static cache + * to make repeat calls efficient. + * + * @param int $parentid the id of a course category. + * @return array all the child course categories. + */ +function get_child_categories($parentid) { + static $allcategories = null; + + // only fill in this variable the first time + if (null == $allcategories) { + $allcategories = array(); + + $categories = get_categories(); + foreach ($categories as $category) { + if (empty($allcategories[$category->parent])) { + $allcategories[$category->parent] = array(); + } + $allcategories[$category->parent][] = $category; + } + } + + if (empty($allcategories[$parentid])) { + return array(); + } else { + return $allcategories[$parentid]; + } +} + +/** + * This function recursively travels the categories, building up a nice list + * for display. It also makes an array that list all the parents for each + * category. + * + * For example, if you have a tree of categories like: + * Miscellaneous (id = 1) + * Subcategory (id = 2) + * Sub-subcategory (id = 4) + * Other category (id = 3) + * Then after calling this function you will have + * $list = array(1 => 'Miscellaneous', 2 => 'Miscellaneous / Subcategory', + * 4 => 'Miscellaneous / Subcategory / Sub-subcategory', + * 3 => 'Other category'); + * $parents = array(2 => array(1), 4 => array(1, 2)); + * + * If you specify $requiredcapability, then only categories where the current + * user has that capability will be added to $list, although all categories + * will still be added to $parents, and if you only have $requiredcapability + * in a child category, not the parent, then the child catgegory will still be + * included. + * + * If you specify the option $excluded, then that category, and all its children, + * are omitted from the tree. This is useful when you are doing something like + * moving categories, where you do not want to allow people to move a category + * to be the child of itself. + * + * @param array $list For output, accumulates an array categoryid => full category path name + * @param array $parents For output, accumulates an array categoryid => list of parent category ids. + * @param string/array $requiredcapability if given, only categories where the current + * user has this capability will be added to $list. Can also be an array of capabilities, + * in which case they are all required. + * @param integer $excludeid Omit this category and its children from the lists built. + * @param object $category Build the tree starting at this category - otherwise starts at the top level. + * @param string $path For internal use, as part of recursive calls. + */ +function make_categories_list(&$list, &$parents, $requiredcapability = '', + $excludeid = 0, $category = NULL, $path = "") { + + // initialize the arrays if needed + if (!is_array($list)) { + $list = array(); + } + if (!is_array($parents)) { + $parents = array(); + } + + if (empty($category)) { + // Start at the top level. + $category = new stdClass; + $category->id = 0; + } else { + // This is the excluded category, don't include it. + if ($excludeid > 0 && $excludeid == $category->id) { + return; + } + + $context = context_coursecat::instance($category->id); + $categoryname = format_string($category->name, true, array('context' => $context)); + + // Update $path. + if ($path) { + $path = $path.' / '.$categoryname; + } else { + $path = $categoryname; + } + + // Add this category to $list, if the permissions check out. + if (empty($requiredcapability)) { + $list[$category->id] = $path; + + } else { + $requiredcapability = (array)$requiredcapability; + if (has_all_capabilities($requiredcapability, $context)) { + $list[$category->id] = $path; + } + } + } + + // Add all the children recursively, while updating the parents array. + if ($categories = get_child_categories($category->id)) { + foreach ($categories as $cat) { + if (!empty($category->id)) { + if (isset($parents[$category->id])) { + $parents[$cat->id] = $parents[$category->id]; + } + $parents[$cat->id][] = $category->id; + } + make_categories_list($list, $parents, $requiredcapability, $excludeid, $cat, $path); + } + } +} + +/** + * This function generates a structured array of courses and categories. + * + * The depth of categories is limited by $CFG->maxcategorydepth however there + * is no limit on the number of courses! + * + * Suitable for use with the course renderers course_category_tree method: + * $renderer = $PAGE->get_renderer('core','course'); + * echo $renderer->course_category_tree(get_course_category_tree()); + * + * @global moodle_database $DB + * @param int $id + * @param int $depth + */ +function get_course_category_tree($id = 0, $depth = 0) { + global $DB, $CFG; + $viewhiddencats = has_capability('moodle/category:viewhiddencategories', context_system::instance()); + $categories = get_child_categories($id); + $categoryids = array(); + foreach ($categories as $key => &$category) { + if (!$category->visible && !$viewhiddencats) { + unset($categories[$key]); + continue; + } + $categoryids[$category->id] = $category; + if (empty($CFG->maxcategorydepth) || $depth <= $CFG->maxcategorydepth) { + list($category->categories, $subcategories) = get_course_category_tree($category->id, $depth+1); + foreach ($subcategories as $subid=>$subcat) { + $categoryids[$subid] = $subcat; + } + $category->courses = array(); + } + } + + if ($depth > 0) { + // This is a recursive call so return the required array + return array($categories, $categoryids); + } + + if (empty($categoryids)) { + // No categories available (probably all hidden). + return array(); + } + + // The depth is 0 this function has just been called so we can finish it off + + list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); + list($catsql, $catparams) = $DB->get_in_or_equal(array_keys($categoryids)); + $sql = "SELECT + c.id,c.sortorder,c.visible,c.fullname,c.shortname,c.summary,c.category + $ccselect + FROM {course} c + $ccjoin + WHERE c.category $catsql ORDER BY c.sortorder ASC"; + if ($courses = $DB->get_records_sql($sql, $catparams)) { + // loop throught them + foreach ($courses as $course) { + if ($course->id == SITEID) { + continue; + } + context_instance_preload($course); + if (!empty($course->visible) || has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { + $categoryids[$course->category]->courses[$course->id] = $course; + } + } + } + return $categories; +} + +/** + * Recursive function to print out all the categories in a nice format + * with or without courses included + */ +function print_whole_category_list($category=NULL, $displaylist=NULL, $parentslist=NULL, $depth=-1, $showcourses = true) { + global $CFG; + + // maxcategorydepth == 0 meant no limit + if (!empty($CFG->maxcategorydepth) && $depth >= $CFG->maxcategorydepth) { + return; + } + + if (!$displaylist) { + make_categories_list($displaylist, $parentslist); + } + + if ($category) { + if ($category->visible or has_capability('moodle/category:viewhiddencategories', context_system::instance())) { + print_category_info($category, $depth, $showcourses); + } else { + return; // Don't bother printing children of invisible categories + } + + } else { + $category = new stdClass(); + $category->id = "0"; + } + + if ($categories = get_child_categories($category->id)) { // Print all the children recursively + $countcats = count($categories); + $count = 0; + $first = true; + $last = false; + foreach ($categories as $cat) { + $count++; + if ($count == $countcats) { + $last = true; + } + $up = $first ? false : true; + $down = $last ? false : true; + $first = false; + + print_whole_category_list($cat, $displaylist, $parentslist, $depth + 1, $showcourses); + } + } +} + +/** + * This function will return $options array for html_writer::select(), with whitespace to denote nesting. + */ +function make_categories_options() { + make_categories_list($cats,$parents); + foreach ($cats as $key => $value) { + if (array_key_exists($key,$parents)) { + if ($indent = count($parents[$key])) { + for ($i = 0; $i < $indent; $i++) { + $cats[$key] = ' '.$cats[$key]; + } + } + } + } + return $cats; +} + +/** + * Prints the category info in indented fashion + * This function is only used by print_whole_category_list() above + */ +function print_category_info($category, $depth=0, $showcourses = false) { + global $CFG, $DB, $OUTPUT; + + $strsummary = get_string('summary'); + + $catlinkcss = null; + if (!$category->visible) { + $catlinkcss = array('class'=>'dimmed'); + } + static $coursecount = null; + if (null === $coursecount) { + // only need to check this once + $coursecount = $DB->count_records('course') <= FRONTPAGECOURSELIMIT; + } + + if ($showcourses and $coursecount) { + $catimage = ''; + } else { + $catimage = " "; + } + + $courses = get_courses($category->id, 'c.sortorder ASC', 'c.id,c.sortorder,c.visible,c.fullname,c.shortname,c.summary'); + $context = context_coursecat::instance($category->id); + $fullname = format_string($category->name, true, array('context' => $context)); + + if ($showcourses and $coursecount) { + echo '
        '; + $cat = ''; + $cat .= html_writer::tag('div', $catimage, array('class'=>'image')); + $catlink = html_writer::link(new moodle_url('/course/category.php', array('id'=>$category->id)), $fullname, $catlinkcss); + $cat .= html_writer::tag('div', $catlink, array('class'=>'name')); + + $html = ''; + if ($depth > 0) { + for ($i=0; $i< $depth; $i++) { + $html = html_writer::tag('div', $html . $cat, array('class'=>'indentation')); + $cat = ''; + } + } else { + $html = $cat; + } + echo html_writer::tag('div', $html, array('class'=>'category')); + echo html_writer::tag('div', '', array('class'=>'clearfloat')); + + // does the depth exceed maxcategorydepth + // maxcategorydepth == 0 or unset meant no limit + $limit = !(isset($CFG->maxcategorydepth) && ($depth >= $CFG->maxcategorydepth-1)); + if ($courses && ($limit || $CFG->maxcategorydepth == 0)) { + foreach ($courses as $course) { + $linkcss = null; + if (!$course->visible) { + $linkcss = array('class'=>'dimmed'); + } + + $coursename = get_course_display_name_for_list($course); + $courselink = html_writer::link(new moodle_url('/course/view.php', array('id'=>$course->id)), format_string($coursename), $linkcss); + + // print enrol info + $courseicon = ''; + if ($icons = enrol_get_course_info_icons($course)) { + foreach ($icons as $pix_icon) { + $courseicon = $OUTPUT->render($pix_icon); + } + } + + $coursecontent = html_writer::tag('div', $courseicon.$courselink, array('class'=>'name')); + + if ($course->summary) { + $link = new moodle_url('/course/info.php?id='.$course->id); + $actionlink = $OUTPUT->action_link($link, ''.$strsummary.'', + new popup_action('click', $link, 'courseinfo', array('height' => 400, 'width' => 500)), + array('title'=>$strsummary)); + + $coursecontent .= html_writer::tag('div', $actionlink, array('class'=>'info')); + } + + $html = ''; + for ($i=0; $i <= $depth; $i++) { + $html = html_writer::tag('div', $html . $coursecontent , array('class'=>'indentation')); + $coursecontent = ''; + } + echo html_writer::tag('div', $html, array('class'=>'course clearfloat')); + } + } + echo '
        '; + } else { + echo '
        '; + $html = ''; + $cat = html_writer::link(new moodle_url('/course/category.php', array('id'=>$category->id)), $fullname, $catlinkcss); + if (count($courses) > 0) { + $cat .= html_writer::tag('span', ' ('.count($courses).')', array('title'=>get_string('numberofcourses'), 'class'=>'numberofcourse')); + } + + if ($depth > 0) { + for ($i=0; $i< $depth; $i++) { + $html = html_writer::tag('div', $html .$cat, array('class'=>'indentation')); + $cat = ''; + } + } else { + $html = $cat; + } + + echo html_writer::tag('div', $html, array('class'=>'category')); + echo html_writer::tag('div', '', array('class'=>'clearfloat')); + echo '
        '; + } +} + +/** + * Print the buttons relating to course requests. + * + * @param object $systemcontext the system context. + */ +function print_course_request_buttons($systemcontext) { + global $CFG, $DB, $OUTPUT; + if (empty($CFG->enablecourserequests)) { + return; + } + if (!has_capability('moodle/course:create', $systemcontext) && has_capability('moodle/course:request', $systemcontext)) { + /// Print a button to request a new course + echo $OUTPUT->single_button('request.php', get_string('requestcourse'), 'get'); + } + /// Print a button to manage pending requests + if (has_capability('moodle/site:approvecourse', $systemcontext)) { + $disabled = !$DB->record_exists('course_request', array()); + echo $OUTPUT->single_button('pending.php', get_string('coursespending'), 'get', array('disabled'=>$disabled)); + } +} + +/** + * Does the user have permission to edit things in this category? + * + * @param integer $categoryid The id of the category we are showing, or 0 for system context. + * @return boolean has_any_capability(array(...), ...); in the appropriate context. + */ +function can_edit_in_category($categoryid = 0) { + $context = get_category_or_system_context($categoryid); + return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context); +} + +/** + * Prints the turn editing on/off button on course/index.php or course/category.php. + * + * @param integer $categoryid The id of the category we are showing, or 0 for system context. + * @return string HTML of the editing button, or empty string, if this user is not allowed + * to see it. + */ +function update_category_button($categoryid = 0) { + global $CFG, $PAGE, $OUTPUT; + + // Check permissions. + if (!can_edit_in_category($categoryid)) { + return ''; + } + + // Work out the appropriate action. + if ($PAGE->user_is_editing()) { + $label = get_string('turneditingoff'); + $edit = 'off'; + } else { + $label = get_string('turneditingon'); + $edit = 'on'; + } + + // Generate the button HTML. + $options = array('categoryedit' => $edit, 'sesskey' => sesskey()); + if ($categoryid) { + $options['id'] = $categoryid; + $page = 'category.php'; + } else { + $page = 'index.php'; + } + return $OUTPUT->single_button(new moodle_url('/course/' . $page, $options), $label, 'get'); +} + +/** + * Print courses in category. If category is 0 then all courses are printed. + * @param int|stdClass $category category object or id. + * @return bool true if courses found and printed, else false. + */ +function print_courses($category) { + global $CFG, $OUTPUT; + + if (!is_object($category) && $category==0) { + $categories = get_child_categories(0); // Parent = 0 ie top-level categories only + if (is_array($categories) && count($categories) == 1) { + $category = array_shift($categories); + $courses = get_courses_wmanagers($category->id, + 'c.sortorder ASC', + array('summary','summaryformat')); + } else { + $courses = get_courses_wmanagers('all', + 'c.sortorder ASC', + array('summary','summaryformat')); + } + unset($categories); + } else { + $courses = get_courses_wmanagers($category->id, + 'c.sortorder ASC', + array('summary','summaryformat')); + } + + if ($courses) { + echo html_writer::start_tag('ul', array('class'=>'unlist')); + foreach ($courses as $course) { + $coursecontext = context_course::instance($course->id); + if ($course->visible == 1 || has_capability('moodle/course:viewhiddencourses', $coursecontext)) { + echo html_writer::start_tag('li'); + print_course($course); + echo html_writer::end_tag('li'); + } + } + echo html_writer::end_tag('ul'); + } else { + echo $OUTPUT->heading(get_string("nocoursesyet")); + $context = context_system::instance(); + if (has_capability('moodle/course:create', $context)) { + $options = array(); + if (!empty($category->id)) { + $options['category'] = $category->id; + } else { + $options['category'] = $CFG->defaultrequestcategory; + } + echo html_writer::start_tag('div', array('class'=>'addcoursebutton')); + echo $OUTPUT->single_button(new moodle_url('/course/edit.php', $options), get_string("addnewcourse")); + echo html_writer::end_tag('div'); + return false; + } + } + return true; +} + +/** + * Print a description of a course, suitable for browsing in a list. + * + * @param object $course the course object. + * @param string $highlightterms (optional) some search terms that should be highlighted in the display. + */ +function print_course($course, $highlightterms = '') { + global $CFG, $USER, $DB, $OUTPUT; + + $context = context_course::instance($course->id); + + // Rewrite file URLs so that they are correct + $course->summary = file_rewrite_pluginfile_urls($course->summary, 'pluginfile.php', $context->id, 'course', 'summary', NULL); + + echo html_writer::start_tag('div', array('class'=>'coursebox clearfix')); + echo html_writer::start_tag('div', array('class'=>'info')); + echo html_writer::start_tag('h3', array('class'=>'name')); + + $linkhref = new moodle_url('/course/view.php', array('id'=>$course->id)); + + $coursename = get_course_display_name_for_list($course); + $linktext = highlight($highlightterms, format_string($coursename)); + $linkparams = array('title'=>get_string('entercourse')); + if (empty($course->visible)) { + $linkparams['class'] = 'dimmed'; + } + echo html_writer::link($linkhref, $linktext, $linkparams); + echo html_writer::end_tag('h3'); + + /// first find all roles that are supposed to be displayed + if (!empty($CFG->coursecontact)) { + $managerroles = explode(',', $CFG->coursecontact); + $rusers = array(); + + if (!isset($course->managers)) { + list($sort, $sortparams) = users_order_by_sql('u'); + $rusers = get_role_users($managerroles, $context, true, + 'ra.id AS raid, u.id, u.username, u.firstname, u.lastname, rn.name AS rolecoursealias, + r.name AS rolename, r.sortorder, r.id AS roleid, r.shortname AS roleshortname', + 'r.sortorder ASC, ' . $sort, null, '', '', '', '', $sortparams); + } else { + // use the managers array if we have it for perf reasosn + // populate the datastructure like output of get_role_users(); + foreach ($course->managers as $manager) { + $user = clone($manager->user); + $user->roleid = $manager->roleid; + $user->rolename = $manager->rolename; + $user->roleshortname = $manager->roleshortname; + $user->rolecoursealias = $manager->rolecoursealias; + $rusers[$user->id] = $user; + } + } + + $namesarray = array(); + $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); + foreach ($rusers as $ra) { + if (isset($namesarray[$ra->id])) { + // only display a user once with the higest sortorder role + continue; + } + + $role = new stdClass(); + $role->id = $ra->roleid; + $role->name = $ra->rolename; + $role->shortname = $ra->roleshortname; + $role->coursealias = $ra->rolecoursealias; + $rolename = role_get_name($role, $context, ROLENAME_ALIAS); + + $fullname = fullname($ra, $canviewfullnames); + $namesarray[$ra->id] = $rolename.': '. + html_writer::link(new moodle_url('/user/view.php', array('id'=>$ra->id, 'course'=>SITEID)), $fullname); + } + + if (!empty($namesarray)) { + echo html_writer::start_tag('ul', array('class'=>'teachers')); + foreach ($namesarray as $name) { + echo html_writer::tag('li', $name); + } + echo html_writer::end_tag('ul'); + } + } + echo html_writer::end_tag('div'); // End of info div + +/* CUSTOMICP + echo html_writer::start_tag('div', array('class'=>'summary')); + $options = new stdClass(); + $options->noclean = true; + $options->para = false; + $options->overflowdiv = true; + if (!isset($course->summaryformat)) { + $course->summaryformat = FORMAT_MOODLE; + } + echo highlight($highlightterms, format_text($course->summary, $course->summaryformat, $options, $course->id)); + if ($icons = enrol_get_course_info_icons($course)) { + echo html_writer::start_tag('div', array('class'=>'enrolmenticons')); + foreach ($icons as $icon) { + $icon->attributes["alt"] .= ": ". format_string($coursename, true, array('context'=>$context)); + echo $OUTPUT->render($icon); + } + echo html_writer::end_tag('div'); // End of enrolmenticons div + } + echo html_writer::end_tag('div'); // End of summary div +///*ENDCUSTOMICP */ + echo html_writer::end_tag('div'); // End of coursebox div +} + +/** + * Prints custom user information on the home page. + * Over time this can include all sorts of information + */ +function print_my_moodle() { + global $USER, $CFG, $DB, $OUTPUT; + + if (!isloggedin() or isguestuser()) { + print_error('nopermissions', '', '', 'See My Moodle'); + } + + $courses = enrol_get_my_courses('summary', 'visible DESC,sortorder ASC'); + $rhosts = array(); + $rcourses = array(); + if (!empty($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode==='strict') { + $rcourses = get_my_remotecourses($USER->id); + $rhosts = get_my_remotehosts(); + } + + if (!empty($courses) || !empty($rcourses) || !empty($rhosts)) { + +/* CUSTOMICP désactivation des lignes suivantes + if (!empty($courses)) { + echo '
          '; + foreach ($courses as $course) { + if ($course->id == SITEID) { + continue; + } + echo '
        • '; + print_course($course); + echo "
        • \n"; + } + echo "
        \n"; + } + +///*ENDCUSTOMICP */ + +///* CUSTOMICP insertion des lignes suivantes +if (!empty($courses)) { + echo '
          '; + $teacher_once=0; + $both=0; + foreach ($courses as $course) { + if ($course->id == SITEID) { + continue; + } + + $u_context = get_context_instance(CONTEXT_COURSE, $course->id); + $u_managerroles = explode(',', $CFG->coursecontact); + //CUSTOMICP LIMIT 3 + $u_rusers = get_role_users($u_managerroles, $u_context, true, '', 'r.sortorder ASC, u.lastname ASC LIMIT 3'); + $bool_teacher=0; + foreach ($u_rusers as $u_teacher) { + if ($u_teacher->id == $USER->id) { + $bool_teacher=1; + $teacher_once++; + } + } + if ( $teacher_once == 1) { + ?>

          En tant qu'enseignant toggle

          '; + print_course($course); + echo "\n"; + } + } + if ( $teacher_once != 0) { + echo "
          "; + } + + $student_once=0; + foreach ($courses as $course) { + if ($course->id == SITEID) { + continue; + } + + $u_context = get_context_instance(CONTEXT_COURSE, $course->id); + $u_managerroles = explode(',', $CFG->coursecontact); + //CUSTOMICP LIMIT 3 + $u_rusers = get_role_users($u_managerroles, $u_context, true, '', 'r.sortorder ASC, u.lastname ASC LIMIT 3'); + $bool_teacher=0; + foreach ($u_rusers as $u_teacher) { + if ($u_teacher->id == $USER->id) { + $bool_teacher=1; + } + } + if ( !$bool_teacher) { + + if ( $student_once == 0) { + ?>

          En tant qu'étudiant toggle

          '; + print_course($course); + echo "\n"; + } + + } + if ( $student_once != 0) { + echo "
          "; + } + if ( $both==2 ){ + ?> + + + + \n"; + ?> + + + count_records("course") > (count($courses) + 1) ) { // Some courses not being displayed + echo "
          "; + print_course_search("", false, "short"); + echo ""; + echo $OUTPUT->single_button("$CFG->wwwroot/course/index.php", get_string("fulllistofcourses"), "get"); + echo "
          \n"; + } + + } else { + if ($DB->count_records("course_categories") > 1) { + echo $OUTPUT->box_start("categorybox"); + print_whole_category_list(); + echo $OUTPUT->box_end(); + } else { + print_courses(0); + } + } +} + + +function print_course_search($value="", $return=false, $format="plain") { + global $CFG; + static $count = 0; + + $count++; + + $id = 'coursesearch'; + + if ($count > 1) { + $id .= $count; + } + + $strsearchcourses= get_string("searchcourses"); + + if ($format == 'plain') { + $output = '
          '; + $output .= '
          '; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= '
          '; + } else if ($format == 'short') { + $output = '
          '; + $output .= '
          '; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= '
          '; + } else if ($format == 'navbar') { + $output = '
          '; + $output .= '
          '; + $output .= ''; + $output .= ''; + $output .= ''; + $output .= '
          '; + } + + if ($return) { + return $output; + } + echo $output; +} + +function print_remote_course($course, $width="100%") { + global $CFG, $USER; + + $linkcss = ''; + + $url = "{$CFG->wwwroot}/auth/mnet/jump.php?hostid={$course->hostid}&wantsurl=/course/view.php?id={$course->remoteid}"; + + echo '
          '; + echo '
          '; + echo '
          ' + . format_string($course->fullname) .'
          ' + . format_string($course->hostname) . ' : ' + . format_string($course->cat_name) . ' : ' + . format_string($course->shortname). '
          '; + echo '
          '; + $options = new stdClass(); + $options->noclean = true; + $options->para = false; + $options->overflowdiv = true; + echo format_text($course->summary, $course->summaryformat, $options); + echo '
          '; + echo '
          '; +} + +function print_remote_host($host, $width="100%") { + global $OUTPUT; + + $linkcss = ''; + + echo '
          '; + echo '
          '; + echo '
          '; + echo ''.get_string('course').''; + echo '' + . s($host['name']).' - '; + echo $host['count'] . ' ' . get_string('courses'); + echo '
          '; + echo '
          '; + echo '
          '; +} + + +/// MODULE FUNCTIONS ///////////////////////////////////////////////////////////////// + +function add_course_module($mod) { + global $DB; + + $mod->added = time(); + unset($mod->id); + + $cmid = $DB->insert_record("course_modules", $mod); + rebuild_course_cache($mod->course, true); + return $cmid; +} + +/** + * Creates missing course section(s) and rebuilds course cache + * + * @param int|stdClass $courseorid course id or course object + * @param int|array $sections list of relative section numbers to create + * @return bool if there were any sections created + */ +function course_create_sections_if_missing($courseorid, $sections) { + global $DB; + if (!is_array($sections)) { + $sections = array($sections); + } + $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all()); + if (is_object($courseorid)) { + $courseorid = $courseorid->id; + } + $coursechanged = false; + foreach ($sections as $sectionnum) { + if (!in_array($sectionnum, $existing)) { + $cw = new stdClass(); + $cw->course = $courseorid; + $cw->section = $sectionnum; + $cw->summary = ''; + $cw->summaryformat = FORMAT_HTML; + $cw->sequence = ''; + $id = $DB->insert_record("course_sections", $cw); + $coursechanged = true; + } + } + if ($coursechanged) { + rebuild_course_cache($courseorid, true); + } + return $coursechanged; +} + +/** + * Adds an existing module to the section + * + * Updates both tables {course_sections} and {course_modules} + * + * @param int|stdClass $courseorid course id or course object + * @param int $cmid id of the module already existing in course_modules table + * @param int $sectionnum relative number of the section (field course_sections.section) + * If section does not exist it will be created + * @param int|stdClass $beforemod id or object with field id corresponding to the module + * before which the module needs to be included. Null for inserting in the + * end of the section + * @return int The course_sections ID where the module is inserted + */ +function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) { + global $DB, $COURSE; + if (is_object($beforemod)) { + $beforemod = $beforemod->id; + } + if (is_object($courseorid)) { + $courseid = $courseorid->id; + } else { + $courseid = $courseorid; + } + course_create_sections_if_missing($courseorid, $sectionnum); + // Do not try to use modinfo here, there is no guarantee it is valid! + $section = $DB->get_record('course_sections', array('course'=>$courseid, 'section'=>$sectionnum), '*', MUST_EXIST); + $modarray = explode(",", trim($section->sequence)); + if (empty($section->sequence)) { + $newsequence = "$cmid"; + } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) { + $insertarray = array($cmid, $beforemod); + array_splice($modarray, $key[0], 1, $insertarray); + $newsequence = implode(",", $modarray); + } else { + $newsequence = "$section->sequence,$cmid"; + } + $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id)); + $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid)); + if (is_object($courseorid)) { + rebuild_course_cache($courseorid->id, true); + } else { + rebuild_course_cache($courseorid, true); + } + return $section->id; // Return course_sections ID that was used. +} + +function set_coursemodule_groupmode($id, $groupmode) { + global $DB; + $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST); + if ($cm->groupmode != $groupmode) { + $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id)); + rebuild_course_cache($cm->course, true); + } + return ($cm->groupmode != $groupmode); +} + +function set_coursemodule_idnumber($id, $idnumber) { + global $DB; + $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST); + if ($cm->idnumber != $idnumber) { + $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id)); + rebuild_course_cache($cm->course, true); + } + return ($cm->idnumber != $idnumber); +} + +/** + * Set the visibility of a module and inherent properties. + * + * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered + * has been moved to {@link set_section_visible()} which was the only place from which + * the parameter was used. + * + * @param int $id of the module + * @param int $visible state of the module + * @return bool false when the module was not found, true otherwise + */ +function set_coursemodule_visible($id, $visible) { + global $DB, $CFG; + require_once($CFG->libdir.'/gradelib.php'); + + // Trigger developer's attention when using the previously removed argument. + if (func_num_args() > 2) { + debugging('Wrong number of arguments passed to set_coursemodule_visible(), $prevstateoverrides + has been removed.', DEBUG_DEVELOPER); + } + + if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { + return false; + } + + // Create events and propagate visibility to associated grade items if the value has changed. + // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades. + if ($cm->visible == $visible) { + return true; + } + + if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) { + return false; + } + if ($events = $DB->get_records('event', array('instance'=>$cm->instance, 'modulename'=>$modulename))) { + foreach($events as $event) { + if ($visible) { + show_event($event); + } else { + hide_event($event); + } + } + } + + // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there. + $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course)); + if ($grade_items) { + foreach ($grade_items as $grade_item) { + $grade_item->set_hidden(!$visible); + } + } + + // Updating visible and visibleold to keep them in sync. Only changing a section visibility will + // affect visibleold to allow for an original visibility restore. See set_section_visible(). + $cminfo = new stdClass(); + $cminfo->id = $id; + $cminfo->visible = $visible; + $cminfo->visibleold = $visible; + $DB->update_record('course_modules', $cminfo); + + rebuild_course_cache($cm->course, true); + return true; +} + +/** + * Delete a course module and any associated data at the course level (events) + * Until 1.5 this function simply marked a deleted flag ... now it + * deletes it completely. + * + */ +function delete_course_module($id) { + global $CFG, $DB; + require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->dirroot.'/blog/lib.php'); + + if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { + return true; + } + $modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module)); + //delete events from calendar + if ($events = $DB->get_records('event', array('instance'=>$cm->instance, 'modulename'=>$modulename))) { + foreach($events as $event) { + delete_event($event->id); + } + } + //delete grade items, outcome items and grades attached to modules + if ($grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, + 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course))) { + foreach ($grade_items as $grade_item) { + $grade_item->delete('moddelete'); + } + } + // Delete completion and availability data; it is better to do this even if the + // features are not turned on, in case they were turned on previously (these will be + // very quick on an empty table) + $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id)); + $DB->delete_records('course_modules_availability', array('coursemoduleid'=> $cm->id)); + $DB->delete_records('course_modules_avail_fields', array('coursemoduleid' => $cm->id)); + $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id, + 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); + + delete_context(CONTEXT_MODULE, $cm->id); + $DB->delete_records('course_modules', array('id'=>$cm->id)); + rebuild_course_cache($cm->course, true); + return true; +} + +function delete_mod_from_section($modid, $sectionid) { + global $DB; + + if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) { + + $modarray = explode(",", $section->sequence); + + if ($key = array_keys ($modarray, $modid)) { + array_splice($modarray, $key[0], 1); + $newsequence = implode(",", $modarray); + $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id)); + rebuild_course_cache($section->course, true); + return true; + } else { + return false; + } + + } + return false; +} + +/** + * Moves a section up or down by 1. CANNOT BE USED DIRECTLY BY AJAX! + * + * @param object $course course object + * @param int $section Section number (not id!!!) + * @param int $move (-1 or 1) + * @return boolean true if section moved successfully + * @todo MDL-33379 remove this function in 2.5 + */ +function move_section($course, $section, $move) { + debugging('This function will be removed before 2.5 is released please use move_section_to', DEBUG_DEVELOPER); + +/// Moves a whole course section up and down within the course + global $USER; + + if (!$move) { + return true; + } + + $sectiondest = $section + $move; + + // compartibility with course formats using field 'numsections' + $courseformatoptions = course_get_format($course)->get_format_options(); + if (array_key_exists('numsections', $courseformatoptions) && + $sectiondest > $courseformatoptions['numsections'] or $sectiondest < 1) { + return false; + } + + $retval = move_section_to($course, $section, $sectiondest); + return $retval; +} + +/** + * Moves a section within a course, from a position to another. + * Be very careful: $section and $destination refer to section number, + * not id!. + * + * @param object $course + * @param int $section Section number (not id!!!) + * @param int $destination + * @return boolean Result + */ +function move_section_to($course, $section, $destination) { +/// Moves a whole course section up and down within the course + global $USER, $DB; + + if (!$destination && $destination != 0) { + return true; + } + + // compartibility with course formats using field 'numsections' + $courseformatoptions = course_get_format($course)->get_format_options(); + if ((array_key_exists('numsections', $courseformatoptions) && + ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) { + return false; + } + + // Get all sections for this course and re-order them (2 of them should now share the same section number) + if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id), + 'section ASC, id ASC', 'id, section')) { + return false; + } + + $movedsections = reorder_sections($sections, $section, $destination); + + // Update all sections. Do this in 2 steps to avoid breaking database + // uniqueness constraint + $transaction = $DB->start_delegated_transaction(); + foreach ($movedsections as $id => $position) { + if ($sections[$id] !== $position) { + $DB->set_field('course_sections', 'section', -$position, array('id' => $id)); + } + } + foreach ($movedsections as $id => $position) { + if ($sections[$id] !== $position) { + $DB->set_field('course_sections', 'section', $position, array('id' => $id)); + } + } + + // If we move the highlighted section itself, then just highlight the destination. + // Adjust the higlighted section location if we move something over it either direction. + if ($section == $course->marker) { + course_set_marker($course->id, $destination); + } elseif ($section > $course->marker && $course->marker >= $destination) { + course_set_marker($course->id, $course->marker+1); + } elseif ($section < $course->marker && $course->marker <= $destination) { + course_set_marker($course->id, $course->marker-1); + } + + $transaction->allow_commit(); + rebuild_course_cache($course->id, true); + return true; +} + +/** + * Reordering algorithm for course sections. Given an array of section->section indexed by section->id, + * an original position number and a target position number, rebuilds the array so that the + * move is made without any duplication of section positions. + * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to + * insert a section before the first one, you must give 0 as the target (section 0 can never be moved). + * + * @param array $sections + * @param int $origin_position + * @param int $target_position + * @return array + */ +function reorder_sections($sections, $origin_position, $target_position) { + if (!is_array($sections)) { + return false; + } + + // We can't move section position 0 + if ($origin_position < 1) { + echo "We can't move section position 0"; + return false; + } + + // Locate origin section in sections array + if (!$origin_key = array_search($origin_position, $sections)) { + echo "searched position not in sections array"; + return false; // searched position not in sections array + } + + // Extract origin section + $origin_section = $sections[$origin_key]; + unset($sections[$origin_key]); + + // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!) + $found = false; + $append_array = array(); + foreach ($sections as $id => $position) { + if ($found) { + $append_array[$id] = $position; + unset($sections[$id]); + } + if ($position == $target_position) { + if ($target_position < $origin_position) { + $append_array[$id] = $position; + unset($sections[$id]); + } + $found = true; + } + } + + // Append moved section + $sections[$origin_key] = $origin_section; + + // Append rest of array (if applicable) + if (!empty($append_array)) { + foreach ($append_array as $id => $position) { + $sections[$id] = $position; + } + } + + // Renumber positions + $position = 0; + foreach ($sections as $id => $p) { + $sections[$id] = $position; + $position++; + } + + return $sections; + +} + +/** + * Move the module object $mod to the specified $section + * If $beforemod exists then that is the module + * before which $modid should be inserted + * All parameters are objects + */ +function moveto_module($mod, $section, $beforemod=NULL) { + global $OUTPUT, $DB; + +/// Remove original module from original section + if (! delete_mod_from_section($mod->id, $mod->section)) { + echo $OUTPUT->notification("Could not delete module from existing section"); + } + + // if moving to a hidden section then hide module + if (!$section->visible && $mod->visible) { + // Set this in the object because it is sent as a response to ajax calls. + $mod->visible = 0; + set_coursemodule_visible($mod->id, 0); + // Set visibleold to 1 so module will be visible when section is made visible. + $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id)); + } + if ($section->visible && !$mod->visible) { + set_coursemodule_visible($mod->id, $mod->visibleold); + // Set this in the object because it is sent as a response to ajax calls. + $mod->visible = $mod->visibleold; + } + +/// Add the module into the new section + course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod); + return true; +} + +/** + * Produces the editing buttons for a module + * + * @global core_renderer $OUTPUT + * @staticvar type $str + * @param stdClass $mod The module to produce editing buttons for + * @param bool $absolute_ignored ignored - all links are absolute + * @param bool $moveselect If true a move seleciton process is used (default true) + * @param int $indent The current indenting + * @param int $section The section to link back to + * @return string XHTML for the editing buttons + */ +function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $moveselect = true, $indent=-1, $section=null) { + global $CFG, $OUTPUT, $COURSE; + + static $str; + + $coursecontext = context_course::instance($mod->course); + $modcontext = context_module::instance($mod->id); + + $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign'); + $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'); + + // no permission to edit anything + if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) { + return false; + } + + $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); + + if (!isset($str)) { + $str = new stdClass; + $str->assign = get_string("assignroles", 'role'); + $str->delete = get_string("delete"); + $str->move = get_string("move"); + $str->moveup = get_string("moveup"); + $str->movedown = get_string("movedown"); + $str->moveright = get_string("moveright"); + $str->moveleft = get_string("moveleft"); + $str->update = get_string("update"); + $str->duplicate = get_string("duplicate"); + $str->hide = get_string("hide"); + $str->show = get_string("show"); + $str->groupsnone = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone")); + $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate")); + $str->groupsvisible = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsvisible")); + $str->forcedgroupsnone = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsnone")); + $str->forcedgroupsseparate = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsseparate")); + $str->forcedgroupsvisible = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsvisible")); + $str->edittitle = get_string('edittitle', 'moodle'); + } + + $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); + + if ($section !== null) { + $baseurl->param('sr', $section); + } + $actions = array(); + + // AJAX edit title + if ($mod->modname !== 'label' && $hasmanageactivities && course_ajax_enabled($COURSE)) { + $actions[] = new action_link( + new moodle_url($baseurl, array('update' => $mod->id)), + new pix_icon('t/editstring', $str->edittitle, 'moodle', array('class' => 'iconsmall visibleifjs', 'title' => '')), + null, + array('class' => 'editing_title', 'title' => $str->edittitle) + ); + } + + // leftright + if ($hasmanageactivities) { + if (right_to_left()) { // Exchange arrows on RTL + $rightarrow = 't/left'; + $leftarrow = 't/right'; + } else { + $rightarrow = 't/right'; + $leftarrow = 't/left'; + } + + if ($indent > 0) { + $actions[] = new action_link( + new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')), + new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_moveleft', 'title' => $str->moveleft) + ); + } + if ($indent >= 0) { + $actions[] = new action_link( + new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')), + new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_moveright', 'title' => $str->moveright) + ); + } + } + + // move + if ($hasmanageactivities) { + if ($moveselect) { + $actions[] = new action_link( + new moodle_url($baseurl, array('copy' => $mod->id)), + new pix_icon('t/move', $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_move', 'title' => $str->move) + ); + } else { + $actions[] = new action_link( + new moodle_url($baseurl, array('id' => $mod->id, 'move' => '-1')), + new pix_icon('t/up', $str->moveup, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_moveup', 'title' => $str->moveup) + ); + $actions[] = new action_link( + new moodle_url($baseurl, array('id' => $mod->id, 'move' => '1')), + new pix_icon('t/down', $str->movedown, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_movedown', 'title' => $str->movedown) + ); + } + } + + // Update + if ($hasmanageactivities) { + $actions[] = new action_link( + new moodle_url($baseurl, array('update' => $mod->id)), + new pix_icon('t/edit', $str->update, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_update', 'title' => $str->update) + ); + } + + // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php) + if (has_all_capabilities($dupecaps, $coursecontext) && plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) { + $actions[] = new action_link( + new moodle_url($baseurl, array('duplicate' => $mod->id)), + new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_duplicate', 'title' => $str->duplicate) + ); + } + + // Delete + if ($hasmanageactivities) { + $actions[] = new action_link( + new moodle_url($baseurl, array('delete' => $mod->id)), + new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_delete', 'title' => $str->delete) + ); + } + + // hideshow + if (has_capability('moodle/course:activityvisibility', $modcontext)) { + if ($mod->visible) { + $actions[] = new action_link( + new moodle_url($baseurl, array('hide' => $mod->id)), + new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_hide', 'title' => $str->hide) + ); + } else { + $actions[] = new action_link( + new moodle_url($baseurl, array('show' => $mod->id)), + new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_show', 'title' => $str->show) + ); + } + } + + // groupmode + if ($hasmanageactivities and $mod->groupmode !== false) { + if ($mod->groupmode == SEPARATEGROUPS) { + $groupmode = 0; + $grouptitle = $str->groupsseparate; + $forcedgrouptitle = $str->forcedgroupsseparate; + $groupclass = 'editing_groupsseparate'; + $groupimage = 't/groups'; + } else if ($mod->groupmode == VISIBLEGROUPS) { + $groupmode = 1; + $grouptitle = $str->groupsvisible; + $forcedgrouptitle = $str->forcedgroupsvisible; + $groupclass = 'editing_groupsvisible'; + $groupimage = 't/groupv'; + } else { + $groupmode = 2; + $grouptitle = $str->groupsnone; + $forcedgrouptitle = $str->forcedgroupsnone; + $groupclass = 'editing_groupsnone'; + $groupimage = 't/groupn'; + } + if ($mod->groupmodelink) { + $actions[] = new action_link( + new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $groupmode)), + new pix_icon($groupimage, $grouptitle, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => $groupclass, 'title' => $grouptitle) + ); + } else { + $actions[] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => $forcedgrouptitle, 'class' => 'iconsmall')); + } + } + + // Assign + if (has_capability('moodle/role:assign', $modcontext)){ + $actions[] = new action_link( + new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $modcontext->id)), + new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')), + null, + array('class' => 'editing_assign', 'title' => $str->assign) + ); + } + + // The space added before the is a ugly hack but required to set the CSS property white-space: nowrap + // and having it to work without attaching the preceding text along with it. Hopefully the refactoring of + // the course page HTML will allow this to be removed. + $output = ' ' . html_writer::start_tag('span', array('class' => 'commands')); + foreach ($actions as $action) { + if ($action instanceof renderable) { + $output .= $OUTPUT->render($action); + } else { + $output .= $action; + } + } + $output .= html_writer::end_tag('span'); + return $output; +} + +/** + * given a course object with shortname & fullname, this function will + * truncate the the number of chars allowed and add ... if it was too long + */ +function course_format_name ($course,$max=100) { + + $context = context_course::instance($course->id); + $shortname = format_string($course->shortname, true, array('context' => $context)); + $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); + $str = $shortname.': '. $fullname; + if (textlib::strlen($str) <= $max) { + return $str; + } + else { + return textlib::substr($str,0,$max-3).'...'; + } +} + +/** + * Is the user allowed to add this type of module to this course? + * @param object $course the course settings. Only $course->id is used. + * @param string $modname the module name. E.g. 'forum' or 'quiz'. + * @return bool whether the current user is allowed to add this type of module to this course. + */ +function course_allowed_module($course, $modname) { + if (is_numeric($modname)) { + throw new coding_exception('Function course_allowed_module no longer + supports numeric module ids. Please update your code to pass the module name.'); + } + + $capability = 'mod/' . $modname . ':addinstance'; + if (!get_capability_info($capability)) { + // Debug warning that the capability does not exist, but no more than once per page. + static $warned = array(); + $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); + if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) { + debugging('The module ' . $modname . ' does not define the standard capability ' . + $capability , DEBUG_DEVELOPER); + $warned[$modname] = 1; + } + + // If the capability does not exist, the module can always be added. + return true; + } + + $coursecontext = context_course::instance($course->id); + return has_capability($capability, $coursecontext); +} + +/** + * Recursively delete category including all subcategories and courses. + * @param stdClass $category + * @param boolean $showfeedback display some notices + * @return array return deleted courses + */ +function category_delete_full($category, $showfeedback=true) { + global $CFG, $DB; + require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->libdir.'/questionlib.php'); + require_once($CFG->dirroot.'/cohort/lib.php'); + + if ($children = $DB->get_records('course_categories', array('parent'=>$category->id), 'sortorder ASC')) { + foreach ($children as $childcat) { + category_delete_full($childcat, $showfeedback); + } + } + + $deletedcourses = array(); + if ($courses = $DB->get_records('course', array('category'=>$category->id), 'sortorder ASC')) { + foreach ($courses as $course) { + if (!delete_course($course, false)) { + throw new moodle_exception('cannotdeletecategorycourse','','',$course->shortname); + } + $deletedcourses[] = $course; + } + } + + // move or delete cohorts in this context + cohort_delete_category($category); + + // now delete anything that may depend on course category context + grade_course_category_delete($category->id, 0, $showfeedback); + if (!question_delete_course_category($category, 0, $showfeedback)) { + throw new moodle_exception('cannotdeletecategoryquestions','','',$category->name); + } + + // finally delete the category and it's context + $DB->delete_records('course_categories', array('id'=>$category->id)); + delete_context(CONTEXT_COURSECAT, $category->id); + add_to_log(SITEID, "category", "delete", "index.php", "$category->name (ID $category->id)"); + + events_trigger('course_category_deleted', $category); + + return $deletedcourses; +} + +/** + * Delete category, but move contents to another category. + * @param object $ccategory + * @param int $newparentid category id + * @return bool status + */ +function category_delete_move($category, $newparentid, $showfeedback=true) { + global $CFG, $DB, $OUTPUT; + require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->libdir.'/questionlib.php'); + require_once($CFG->dirroot.'/cohort/lib.php'); + + if (!$newparentcat = $DB->get_record('course_categories', array('id'=>$newparentid))) { + return false; + } + + if ($children = $DB->get_records('course_categories', array('parent'=>$category->id), 'sortorder ASC')) { + foreach ($children as $childcat) { + move_category($childcat, $newparentcat); + } + } + + if ($courses = $DB->get_records('course', array('category'=>$category->id), 'sortorder ASC', 'id')) { + if (!move_courses(array_keys($courses), $newparentid)) { + if ($showfeedback) { + echo $OUTPUT->notification("Error moving courses"); + } + return false; + } + if ($showfeedback) { + echo $OUTPUT->notification(get_string('coursesmovedout', '', format_string($category->name)), 'notifysuccess'); + } + } + + // move or delete cohorts in this context + cohort_delete_category($category); + + // now delete anything that may depend on course category context + grade_course_category_delete($category->id, $newparentid, $showfeedback); + if (!question_delete_course_category($category, $newparentcat, $showfeedback)) { + if ($showfeedback) { + echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $category), 'notifysuccess'); + } + return false; + } + + // finally delete the category and it's context + $DB->delete_records('course_categories', array('id'=>$category->id)); + delete_context(CONTEXT_COURSECAT, $category->id); + add_to_log(SITEID, "category", "delete", "index.php", "$category->name (ID $category->id)"); + + events_trigger('course_category_deleted', $category); + + if ($showfeedback) { + echo $OUTPUT->notification(get_string('coursecategorydeleted', '', format_string($category->name)), 'notifysuccess'); + } + return true; +} + +/** + * Efficiently moves many courses around while maintaining + * sortorder in order. + * + * @param array $courseids is an array of course ids + * @param int $categoryid + * @return bool success + */ +function move_courses($courseids, $categoryid) { + global $CFG, $DB, $OUTPUT; + + if (empty($courseids)) { + // nothing to do + return; + } + + if (!$category = $DB->get_record('course_categories', array('id'=>$categoryid))) { + return false; + } + + $courseids = array_reverse($courseids); + $newparent = context_coursecat::instance($category->id); + $i = 1; + + foreach ($courseids as $courseid) { + if ($course = $DB->get_record('course', array('id'=>$courseid), 'id, category')) { + $course = new stdClass(); + $course->id = $courseid; + $course->category = $category->id; + $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++; + if ($category->visible == 0) { + // hide the course when moving into hidden category, + // do not update the visibleold flag - we want to get to previous state if somebody unhides the category + $course->visible = 0; + } + + $DB->update_record('course', $course); + add_to_log($course->id, "course", "move", "edit.php?id=$course->id", $course->id); + + $context = context_course::instance($course->id); + context_moved($context, $newparent); + } + } + fix_course_sortorder(); + + return true; +} + +/** + * Hide course category and child course and subcategories + * @param stdClass $category + * @return void + */ +function course_category_hide($category) { + global $DB; + + $category->visible = 0; + $DB->set_field('course_categories', 'visible', 0, array('id'=>$category->id)); + $DB->set_field('course_categories', 'visibleold', 0, array('id'=>$category->id)); + $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($category->id)); // store visible flag so that we can return to it if we immediately unhide + $DB->set_field('course', 'visible', 0, array('category' => $category->id)); + // get all child categories and hide too + if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$category->path/%"))) { + foreach ($subcats as $cat) { + $DB->set_field('course_categories', 'visibleold', $cat->visible, array('id'=>$cat->id)); + $DB->set_field('course_categories', 'visible', 0, array('id'=>$cat->id)); + $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($cat->id)); + $DB->set_field('course', 'visible', 0, array('category' => $cat->id)); + } + } + add_to_log(SITEID, "category", "hide", "editcategory.php?id=$category->id", $category->id); +} + +/** + * Show course category and child course and subcategories + * @param stdClass $category + * @return void + */ +function course_category_show($category) { + global $DB; + + $category->visible = 1; + $DB->set_field('course_categories', 'visible', 1, array('id'=>$category->id)); + $DB->set_field('course_categories', 'visibleold', 1, array('id'=>$category->id)); + $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($category->id)); + // get all child categories and unhide too + if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$category->path/%"))) { + foreach ($subcats as $cat) { + if ($cat->visibleold) { + $DB->set_field('course_categories', 'visible', 1, array('id'=>$cat->id)); + } + $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($cat->id)); + } + } + add_to_log(SITEID, "category", "show", "editcategory.php?id=$category->id", $category->id); +} + +/** + * Efficiently moves a category - NOTE that this can have + * a huge impact access-control-wise... + */ +function move_category($category, $newparentcat) { + global $CFG, $DB; + + $context = context_coursecat::instance($category->id); + + $hidecat = false; + if (empty($newparentcat->id)) { + $DB->set_field('course_categories', 'parent', 0, array('id' => $category->id)); + $newparent = context_system::instance(); + } else { + $DB->set_field('course_categories', 'parent', $newparentcat->id, array('id' => $category->id)); + $newparent = context_coursecat::instance($newparentcat->id); + + if (!$newparentcat->visible and $category->visible) { + // better hide category when moving into hidden category, teachers may unhide afterwards and the hidden children will be restored properly + $hidecat = true; + } + } + + context_moved($context, $newparent); + + // now make it last in new category + $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('id'=>$category->id)); + + // Log action. + add_to_log(SITEID, "category", "move", "editcategory.php?id=$category->id", $category->id); + + // and fix the sortorders + fix_course_sortorder(); + + if ($hidecat) { + course_category_hide($category); + } +} + +/** + * Returns the display name of the given section that the course prefers + * + * Implementation of this function is provided by course format + * @see format_base::get_section_name() + * + * @param int|stdClass $courseorid The course to get the section name for (object or just course id) + * @param int|stdClass $section Section object from database or just field course_sections.section + * @return string Display name that the course format prefers, e.g. "Week 2" + */ +function get_section_name($courseorid, $section) { + return course_get_format($courseorid)->get_section_name($section); +} + +/** + * Tells if current course format uses sections + * + * @param string $format Course format ID e.g. 'weeks' $course->format + * @return bool + */ +function course_format_uses_sections($format) { + $course = new stdClass(); + $course->format = $format; + return course_get_format($course)->uses_sections(); +} + +/** + * Returns the information about the ajax support in the given source format + * + * The returned object's property (boolean)capable indicates that + * the course format supports Moodle course ajax features. + * The property (array)testedbrowsers can be used as a parameter for {@see ajaxenabled()}. + * + * @param string $format + * @return stdClass + */ +function course_format_ajax_support($format) { + $course = new stdClass(); + $course->format = $format; + return course_get_format($course)->supports_ajax(); +} + +/** + * Can the current user delete this course? + * Course creators have exception, + * 1 day after the creation they can sill delete the course. + * @param int $courseid + * @return boolean + */ +function can_delete_course($courseid) { + global $USER, $DB; + + $context = context_course::instance($courseid); + + if (has_capability('moodle/course:delete', $context)) { + return true; + } + + // hack: now try to find out if creator created this course recently (1 day) + if (!has_capability('moodle/course:create', $context)) { + return false; + } + + $since = time() - 60*60*24; + + $params = array('userid'=>$USER->id, 'url'=>"view.php?id=$courseid", 'since'=>$since); + $select = "module = 'course' AND action = 'new' AND userid = :userid AND url = :url AND time > :since"; + + return $DB->record_exists_select('log', $select, $params); +} + +/** + * Save the Your name for 'Some role' strings. + * + * @param integer $courseid the id of this course. + * @param array $data the data that came from the course settings form. + */ +function save_local_role_names($courseid, $data) { + global $DB; + $context = context_course::instance($courseid); + + foreach ($data as $fieldname => $value) { + if (strpos($fieldname, 'role_') !== 0) { + continue; + } + list($ignored, $roleid) = explode('_', $fieldname); + + // make up our mind whether we want to delete, update or insert + if (!$value) { + $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid)); + + } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) { + $rolename->name = $value; + $DB->update_record('role_names', $rolename); + + } else { + $rolename = new stdClass; + $rolename->contextid = $context->id; + $rolename->roleid = $roleid; + $rolename->name = $value; + $DB->insert_record('role_names', $rolename); + } + } +} + +/** + * Create a course and either return a $course object + * + * Please note this functions does not verify any access control, + * the calling code is responsible for all validation (usually it is the form definition). + * + * @param array $editoroptions course description editor options + * @param object $data - all the data needed for an entry in the 'course' table + * @return object new course instance + */ +function create_course($data, $editoroptions = NULL) { + global $CFG, $DB; + + //check the categoryid - must be given for all new courses + $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST); + + //check if the shortname already exist + if (!empty($data->shortname)) { + if ($DB->record_exists('course', array('shortname' => $data->shortname))) { + throw new moodle_exception('shortnametaken'); + } + } + + //check if the id number already exist + if (!empty($data->idnumber)) { + if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) { + throw new moodle_exception('idnumbertaken'); + } + } + + $data->timecreated = time(); + $data->timemodified = $data->timecreated; + + // place at beginning of any category + $data->sortorder = 0; + + if ($editoroptions) { + // summary text is updated later, we need context to store the files first + $data->summary = ''; + $data->summary_format = FORMAT_HTML; + } + + if (!isset($data->visible)) { + // data not from form, add missing visibility info + $data->visible = $category->visible; + } + $data->visibleold = $data->visible; + + $newcourseid = $DB->insert_record('course', $data); + $context = context_course::instance($newcourseid, MUST_EXIST); + + if ($editoroptions) { + // Save the files used in the summary editor and store + $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); + $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid)); + $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid)); + } + + // update course format options + course_get_format($newcourseid)->update_course_format_options($data); + + $course = course_get_format($newcourseid)->get_course(); + + // Setup the blocks + blocks_add_default_course_blocks($course); + + // Create a default section. + course_create_sections_if_missing($course, 0); + + fix_course_sortorder(); + + // new context created - better mark it as dirty + mark_context_dirty($context->path); + + // Save any custom role names. + save_local_role_names($course->id, (array)$data); + + // set up enrolments + enrol_course_updated(true, $course, $data); + + add_to_log(SITEID, 'course', 'new', 'view.php?id='.$course->id, $data->fullname.' (ID '.$course->id.')'); + + // Trigger events + events_trigger('course_created', $course); + + return $course; +} + +/** + * Create a new course category and marks the context as dirty + * + * This function does not set the sortorder for the new category and + * @see{fix_course_sortorder} should be called after creating a new course + * category + * + * Please note that this function does not verify access control. + * + * @param object $category All of the data required for an entry in the course_categories table + * @return object new course category + */ +function create_course_category($category) { + global $DB; + + $category->timemodified = time(); + $category->id = $DB->insert_record('course_categories', $category); + $category = $DB->get_record('course_categories', array('id' => $category->id)); + + // We should mark the context as dirty + $category->context = context_coursecat::instance($category->id); + $category->context->mark_dirty(); + + return $category; +} + +/** + * Update a course. + * + * Please note this functions does not verify any access control, + * the calling code is responsible for all validation (usually it is the form definition). + * + * @param object $data - all the data needed for an entry in the 'course' table + * @param array $editoroptions course description editor options + * @return void + */ +function update_course($data, $editoroptions = NULL) { + global $CFG, $DB; + + $data->timemodified = time(); + + $oldcourse = course_get_format($data->id)->get_course(); + $context = context_course::instance($oldcourse->id); + + if ($editoroptions) { + $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); + } + + if (!isset($data->category) or empty($data->category)) { + // prevent nulls and 0 in category field + unset($data->category); + } + $movecat = (isset($data->category) and $oldcourse->category != $data->category); + + if (!isset($data->visible)) { + // data not from form, add missing visibility info + $data->visible = $oldcourse->visible; + } + + if ($data->visible != $oldcourse->visible) { + // reset the visibleold flag when manually hiding/unhiding course + $data->visibleold = $data->visible; + } else { + if ($movecat) { + $newcategory = $DB->get_record('course_categories', array('id'=>$data->category)); + if (empty($newcategory->visible)) { + // make sure when moving into hidden category the course is hidden automatically + $data->visible = 0; + } + } + } + + // Update with the new data + $DB->update_record('course', $data); + // make sure the modinfo cache is reset + rebuild_course_cache($data->id); + + // update course format options with full course data + course_get_format($data->id)->update_course_format_options($data, $oldcourse); + + $course = $DB->get_record('course', array('id'=>$data->id)); + + if ($movecat) { + $newparent = context_coursecat::instance($course->category); + context_moved($context, $newparent); + } + + fix_course_sortorder(); + + // Test for and remove blocks which aren't appropriate anymore + blocks_remove_inappropriate($course); + + // Save any custom role names. + save_local_role_names($course->id, $data); + + // update enrol settings + enrol_course_updated(false, $course, $data); + + add_to_log($course->id, "course", "update", "edit.php?id=$course->id", $course->id); + + // Trigger events + events_trigger('course_updated', $course); + + if ($oldcourse->format !== $course->format) { + // Remove all options stored for the previous format + // We assume that new course format migrated everything it needed watching trigger + // 'course_updated' and in method format_XXX::update_course_format_options() + $DB->delete_records('course_format_options', + array('courseid' => $course->id, 'format' => $oldcourse->format)); + } +} + +/** + * Average number of participants + * @return integer + */ +function average_number_of_participants() { + global $DB, $SITE; + + //count total of enrolments for visible course (except front page) + $sql = 'SELECT COUNT(*) FROM ( + SELECT DISTINCT ue.userid, e.courseid + FROM {user_enrolments} ue, {enrol} e, {course} c + WHERE ue.enrolid = e.id + AND e.courseid <> :siteid + AND c.id = e.courseid + AND c.visible = 1) total'; + $params = array('siteid' => $SITE->id); + $enrolmenttotal = $DB->count_records_sql($sql, $params); + + + //count total of visible courses (minus front page) + $coursetotal = $DB->count_records('course', array('visible' => 1)); + $coursetotal = $coursetotal - 1 ; + + //average of enrolment + if (empty($coursetotal)) { + $participantaverage = 0; + } else { + $participantaverage = $enrolmenttotal / $coursetotal; + } + + return $participantaverage; +} + +/** + * Average number of course modules + * @return integer + */ +function average_number_of_courses_modules() { + global $DB, $SITE; + + //count total of visible course module (except front page) + $sql = 'SELECT COUNT(*) FROM ( + SELECT cm.course, cm.module + FROM {course} c, {course_modules} cm + WHERE c.id = cm.course + AND c.id <> :siteid + AND cm.visible = 1 + AND c.visible = 1) total'; + $params = array('siteid' => $SITE->id); + $moduletotal = $DB->count_records_sql($sql, $params); + + + //count total of visible courses (minus front page) + $coursetotal = $DB->count_records('course', array('visible' => 1)); + $coursetotal = $coursetotal - 1 ; + + //average of course module + if (empty($coursetotal)) { + $coursemoduleaverage = 0; + } else { + $coursemoduleaverage = $moduletotal / $coursetotal; + } + + return $coursemoduleaverage; +} + +/** + * This class pertains to course requests and contains methods associated with + * create, approving, and removing course requests. + * + * Please note we do not allow embedded images here because there is no context + * to store them with proper access control. + * + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.0 + * + * @property-read int $id + * @property-read string $fullname + * @property-read string $shortname + * @property-read string $summary + * @property-read int $summaryformat + * @property-read int $summarytrust + * @property-read string $reason + * @property-read int $requester + */ +class course_request { + + /** + * This is the stdClass that stores the properties for the course request + * and is externally accessed through the __get magic method + * @var stdClass + */ + protected $properties; + + /** + * An array of options for the summary editor used by course request forms. + * This is initially set by {@link summary_editor_options()} + * @var array + * @static + */ + protected static $summaryeditoroptions; + + /** + * Static function to prepare the summary editor for working with a course + * request. + * + * @static + * @param null|stdClass $data Optional, an object containing the default values + * for the form, these may be modified when preparing the + * editor so this should be called before creating the form + * @return stdClass An object that can be used to set the default values for + * an mforms form + */ + public static function prepare($data=null) { + if ($data === null) { + $data = new stdClass; + } + $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); + return $data; + } + + /** + * Static function to create a new course request when passed an array of properties + * for it. + * + * This function also handles saving any files that may have been used in the editor + * + * @static + * @param stdClass $data + * @return course_request The newly created course request + */ + public static function create($data) { + global $USER, $DB, $CFG; + $data->requester = $USER->id; + + // Setting the default category if none set. + if (empty($data->category) || empty($CFG->requestcategoryselection)) { + $data->category = $CFG->defaultrequestcategory; + } + + // Summary is a required field so copy the text over + $data->summary = $data->summary_editor['text']; + $data->summaryformat = $data->summary_editor['format']; + + $data->id = $DB->insert_record('course_request', $data); + + // Create a new course_request object and return it + $request = new course_request($data); + + // Notify the admin if required. + if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { + + $a = new stdClass; + $a->link = "$CFG->wwwroot/course/pending.php"; + $a->user = fullname($USER); + $subject = get_string('courserequest'); + $message = get_string('courserequestnotifyemail', 'admin', $a); + foreach ($users as $user) { + $request->notify($user, $USER, 'courserequested', $subject, $message); + } + } + + return $request; + } + + /** + * Returns an array of options to use with a summary editor + * + * @uses course_request::$summaryeditoroptions + * @return array An array of options to use with the editor + */ + public static function summary_editor_options() { + global $CFG; + if (self::$summaryeditoroptions === null) { + self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0); + } + return self::$summaryeditoroptions; + } + + /** + * Loads the properties for this course request object. Id is required and if + * only id is provided then we load the rest of the properties from the database + * + * @param stdClass|int $properties Either an object containing properties + * or the course_request id to load + */ + public function __construct($properties) { + global $DB; + if (empty($properties->id)) { + if (empty($properties)) { + throw new coding_exception('You must provide a course request id when creating a course_request object'); + } + $id = $properties; + $properties = new stdClass; + $properties->id = (int)$id; + unset($id); + } + if (empty($properties->requester)) { + if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) { + print_error('unknowncourserequest'); + } + } else { + $this->properties = $properties; + } + $this->properties->collision = null; + } + + /** + * Returns the requested property + * + * @param string $key + * @return mixed + */ + public function __get($key) { + return $this->properties->$key; + } + + /** + * Override this to ensure empty($request->blah) calls return a reliable answer... + * + * This is required because we define the __get method + * + * @param mixed $key + * @return bool True is it not empty, false otherwise + */ + public function __isset($key) { + return (!empty($this->properties->$key)); + } + + /** + * Returns the user who requested this course + * + * Uses a static var to cache the results and cut down the number of db queries + * + * @staticvar array $requesters An array of cached users + * @return stdClass The user who requested the course + */ + public function get_requester() { + global $DB; + static $requesters= array(); + if (!array_key_exists($this->properties->requester, $requesters)) { + $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester)); + } + return $requesters[$this->properties->requester]; + } + + /** + * Checks that the shortname used by the course does not conflict with any other + * courses that exist + * + * @param string|null $shortnamemark The string to append to the requests shortname + * should a conflict be found + * @return bool true is there is a conflict, false otherwise + */ + public function check_shortname_collision($shortnamemark = '[*]') { + global $DB; + + if ($this->properties->collision !== null) { + return $this->properties->collision; + } + + if (empty($this->properties->shortname)) { + debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); + $this->properties->collision = false; + } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) { + if (!empty($shortnamemark)) { + $this->properties->shortname .= ' '.$shortnamemark; + } + $this->properties->collision = true; + } else { + $this->properties->collision = false; + } + return $this->properties->collision; + } + + /** + * This function approves the request turning it into a course + * + * This function converts the course request into a course, at the same time + * transferring any files used in the summary to the new course and then removing + * the course request and the files associated with it. + * + * @return int The id of the course that was created from this request + */ + public function approve() { + global $CFG, $DB, $USER; + + $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST); + + $courseconfig = get_config('moodlecourse'); + + // Transfer appropriate settings + $data = clone($this->properties); + unset($data->id); + unset($data->reason); + unset($data->requester); + + // If the category is not set, if the current user does not have the rights to change the category, or if the + // category does not exist, we set the default category to the course to be approved. + // The system level is used because the capability moodle/site:approvecourse is based on a system level. + if (empty($data->category) || !has_capability('moodle/course:changecategory', context_system::instance()) || + (!$category = get_course_category($data->category))) { + $category = get_course_category($CFG->defaultrequestcategory); + } + + // Set category + $data->category = $category->id; + $data->sortorder = $category->sortorder; // place as the first in category + + // Set misc settings + $data->requested = 1; + + // Apply course default settings + $data->format = $courseconfig->format; + $data->newsitems = $courseconfig->newsitems; + $data->showgrades = $courseconfig->showgrades; + $data->showreports = $courseconfig->showreports; + $data->maxbytes = $courseconfig->maxbytes; + $data->groupmode = $courseconfig->groupmode; + $data->groupmodeforce = $courseconfig->groupmodeforce; + $data->visible = $courseconfig->visible; + $data->visibleold = $data->visible; + $data->lang = $courseconfig->lang; + + $course = create_course($data); + $context = context_course::instance($course->id, MUST_EXIST); + + // add enrol instances + if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { + if ($manual = enrol_get_plugin('manual')) { + $manual->add_default_instance($course); + } + } + + // enrol the requester as teacher if necessary + if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) { + enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); + } + + $this->delete(); + + $a = new stdClass(); + $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); + $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id; + $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a)); + + return $course->id; + } + + /** + * Reject a course request + * + * This function rejects a course request, emailing the requesting user the + * provided notice and then removing the request from the database + * + * @param string $notice The message to display to the user + */ + public function reject($notice) { + global $USER, $DB; + $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST); + $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice)); + $this->delete(); + } + + /** + * Deletes the course request and any associated files + */ + public function delete() { + global $DB; + $DB->delete_records('course_request', array('id' => $this->properties->id)); + } + + /** + * Send a message from one user to another using events_trigger + * + * @param object $touser + * @param object $fromuser + * @param string $name + * @param string $subject + * @param string $message + */ + protected function notify($touser, $fromuser, $name='courserequested', $subject, $message) { + $eventdata = new stdClass(); + $eventdata->component = 'moodle'; + $eventdata->name = $name; + $eventdata->userfrom = $fromuser; + $eventdata->userto = $touser; + $eventdata->subject = $subject; + $eventdata->fullmessage = $message; + $eventdata->fullmessageformat = FORMAT_PLAIN; + $eventdata->fullmessagehtml = ''; + $eventdata->smallmessage = ''; + $eventdata->notification = 1; + message_send($eventdata); + } +} + +/** + * Return a list of page types + * @param string $pagetype current page type + * @param stdClass $parentcontext Block's parent context + * @param stdClass $currentcontext Current context of block + */ +function course_page_type_list($pagetype, $parentcontext, $currentcontext) { + // if above course context ,display all course fomats + list($currentcontext, $course, $cm) = get_context_info_array($currentcontext->id); + if ($course->id == SITEID) { + return array('*'=>get_string('page-x', 'pagetype')); + } else { + return array('*'=>get_string('page-x', 'pagetype'), + 'course-*'=>get_string('page-course-x', 'pagetype'), + 'course-view-*'=>get_string('page-course-view-x', 'pagetype') + ); + } +} + +/** + * Determine whether course ajax should be enabled for the specified course + * + * @param stdClass $course The course to test against + * @return boolean Whether course ajax is enabled or note + */ +function course_ajax_enabled($course) { + global $CFG, $PAGE, $SITE; + + // Ajax must be enabled globally + if (!$CFG->enableajax) { + return false; + } + + // The user must be editing for AJAX to be included + if (!$PAGE->user_is_editing()) { + return false; + } + + // Check that the theme suports + if (!$PAGE->theme->enablecourseajax) { + return false; + } + + // Check that the course format supports ajax functionality + // The site 'format' doesn't have information on course format support + if ($SITE->id !== $course->id) { + $courseformatajaxsupport = course_format_ajax_support($course->format); + if (!$courseformatajaxsupport->capable) { + return false; + } + } + + // All conditions have been met so course ajax should be enabled + return true; +} + +/** + * Include the relevant javascript and language strings for the resource + * toolbox YUI module + * + * @param integer $id The ID of the course being applied to + * @param array $usedmodules An array containing the names of the modules in use on the page + * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site + * @param stdClass $config An object containing configuration parameters for ajax modules including: + * * resourceurl The URL to post changes to for resource changes + * * sectionurl The URL to post changes to for section changes + * * pageparams Additional parameters to pass through in the post + * @return bool + */ +function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) { + global $PAGE, $SITE; + + // Ensure that ajax should be included + if (!course_ajax_enabled($course)) { + return false; + } + + if (!$config) { + $config = new stdClass(); + } + + // The URL to use for resource changes + if (!isset($config->resourceurl)) { + $config->resourceurl = '/course/rest.php'; + } + + // The URL to use for section changes + if (!isset($config->sectionurl)) { + $config->sectionurl = '/course/rest.php'; + } + + // Any additional parameters which need to be included on page submission + if (!isset($config->pageparams)) { + $config->pageparams = array(); + } + + // Include toolboxes + $PAGE->requires->yui_module('moodle-course-toolboxes', + 'M.course.init_resource_toolbox', + array(array( + 'courseid' => $course->id, + 'ajaxurl' => $config->resourceurl, + 'config' => $config, + )) + ); + $PAGE->requires->yui_module('moodle-course-toolboxes', + 'M.course.init_section_toolbox', + array(array( + 'courseid' => $course->id, + 'format' => $course->format, + 'ajaxurl' => $config->sectionurl, + 'config' => $config, + )) + ); + + // Include course dragdrop + if ($course->id != $SITE->id) { + $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop', + array(array( + 'courseid' => $course->id, + 'ajaxurl' => $config->sectionurl, + 'config' => $config, + )), null, true); + + $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop', + array(array( + 'courseid' => $course->id, + 'ajaxurl' => $config->resourceurl, + 'config' => $config, + )), null, true); + } + + // Include blocks dragdrop + $params = array( + 'courseid' => $course->id, + 'pagetype' => $PAGE->pagetype, + 'pagelayout' => $PAGE->pagelayout, + 'subpage' => $PAGE->subpage, + 'regions' => $PAGE->blocks->get_regions(), + ); + $PAGE->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', array($params), null, true); + + // Require various strings for the command toolbox + $PAGE->requires->strings_for_js(array( + 'moveleft', + 'deletechecktype', + 'deletechecktypename', + 'edittitle', + 'edittitleinstructions', + 'show', + 'hide', + 'groupsnone', + 'groupsvisible', + 'groupsseparate', + 'clicktochangeinbrackets', + 'markthistopic', + 'markedthistopic', + 'move', + 'movesection', + ), 'moodle'); + + // Include format-specific strings + if ($course->id != $SITE->id) { + $PAGE->requires->strings_for_js(array( + 'showfromothers', + 'hidefromothers', + ), 'format_' . $course->format); + } + + // For confirming resource deletion we need the name of the module in question + foreach ($usedmodules as $module => $modname) { + $PAGE->requires->string_for_js('pluginname', $module); + } + + // Load drag and drop upload AJAX. + dndupload_add_to_course($course, $enabledmodules); + + // Add the module chooser + $PAGE->requires->yui_module('moodle-course-modchooser', + 'M.course.init_chooser', + array(array('courseid' => $course->id, 'closeButtonTitle' => get_string('close', 'editor'))) + ); + $PAGE->requires->strings_for_js(array( + 'addresourceoractivity', + 'modchooserenable', + 'modchooserdisable', + ), 'moodle'); + + return true; +} + +/** + * Returns the sorted list of available course formats, filtered by enabled if necessary + * + * @param bool $enabledonly return only formats that are enabled + * @return array array of sorted format names + */ +function get_sorted_course_formats($enabledonly = false) { + global $CFG; + $formats = get_plugin_list('format'); + + if (!empty($CFG->format_plugins_sortorder)) { + $order = explode(',', $CFG->format_plugins_sortorder); + $order = array_merge(array_intersect($order, array_keys($formats)), + array_diff(array_keys($formats), $order)); + } else { + $order = array_keys($formats); + } + if (!$enabledonly) { + return $order; + } + $sortedformats = array(); + foreach ($order as $formatname) { + if (!get_config('format_'.$formatname, 'disabled')) { + $sortedformats[] = $formatname; + } + } + return $sortedformats; +} + +/** + * The URL to use for the specified course (with section) + * + * @param int|stdClass $courseorid The course to get the section name for (either object or just course id) + * @param int|stdClass $section Section object from database or just field course_sections.section + * if omitted the course view page is returned + * @param array $options options for view URL. At the moment core uses: + * 'navigation' (bool) if true and section has no separate page, the function returns null + * 'sr' (int) used by multipage formats to specify to which section to return + * @return moodle_url The url of course + */ +function course_get_url($courseorid, $section = null, $options = array()) { + return course_get_format($courseorid)->get_view_url($section, $options); +} diff --git a/loginas.php b/loginas.php new file mode 100644 index 0000000..b4e5ea7 --- /dev/null +++ b/loginas.php @@ -0,0 +1,75 @@ +wantsurl = "$CFG->wwwroot/course/view.php?id=".$id; + } else { + $SESSION->wantsurl = "$CFG->wwwroot/"; + } + + redirect(get_login_url()); +} + +///------------------------------------- +/// We are trying to log in as this user in the first place + +$userid = required_param('user', PARAM_INT); // login as this user + +$url = new moodle_url('/course/loginas.php', array('user'=>$userid, 'sesskey'=>sesskey())); +if ($id !== SITEID) { + $url->param('id', $id); +} +$PAGE->set_url($url); + +require_sesskey(); +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); + +/// User must be logged in + +$systemcontext = context_system::instance(); +$coursecontext = context_course::instance($course->id); + +require_login(); + +if (has_capability('moodle/user:loginas', $systemcontext)) { + if (is_siteadmin($userid)) { + print_error('nologinas'); + } + $context = $systemcontext; + $PAGE->set_context($context); +} else { + require_login($course); + require_capability('moodle/user:loginas', $coursecontext); + if (is_siteadmin($userid)) { + print_error('nologinas'); + } + if (!is_enrolled($coursecontext, $userid)) { + print_error('usernotincourse'); + } + $context = $coursecontext; +} + +/// Login as this user and return to course home page. +$oldfullname = fullname($USER, true); +session_loginas($userid, $context); +$newfullname = fullname($USER, true); + +add_to_log($course->id, "course", "loginas", "../user/view.php?id=$course->id&user=$userid", "$oldfullname -> $newfullname"); + +$strloginas = get_string('loginas'); +$strloggedinas = get_string('loggedinas', '', $newfullname); + +$PAGE->set_title($strloggedinas); +$PAGE->set_heading($course->fullname); +$PAGE->navbar->add($strloggedinas); +notice($strloggedinas, "$CFG->wwwroot/course/view.php?id=$course->id"); \ No newline at end of file diff --git a/mod.php b/mod.php new file mode 100644 index 0000000..b1ed617 --- /dev/null +++ b/mod.php @@ -0,0 +1,337 @@ +. + +/** + * Moves, adds, updates, duplicates or deletes modules in a course + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require("../config.php"); +require_once("lib.php"); + +$sectionreturn = optional_param('sr', null, PARAM_INT); +$add = optional_param('add', '', PARAM_ALPHA); +$type = optional_param('type', '', PARAM_ALPHA); +$indent = optional_param('indent', 0, PARAM_INT); +$update = optional_param('update', 0, PARAM_INT); +$duplicate = optional_param('duplicate', 0, PARAM_INT); +$hide = optional_param('hide', 0, PARAM_INT); +$show = optional_param('show', 0, PARAM_INT); +$copy = optional_param('copy', 0, PARAM_INT); +$moveto = optional_param('moveto', 0, PARAM_INT); +$movetosection = optional_param('movetosection', 0, PARAM_INT); +$delete = optional_param('delete', 0, PARAM_INT); +$course = optional_param('course', 0, PARAM_INT); +$groupmode = optional_param('groupmode', -1, PARAM_INT); +$cancelcopy = optional_param('cancelcopy', 0, PARAM_BOOL); +$confirm = optional_param('confirm', 0, PARAM_BOOL); + +// This page should always redirect +$url = new moodle_url('/course/mod.php'); +foreach (compact('indent','update','hide','show','copy','moveto','movetosection','delete','course','cancelcopy','confirm') as $key=>$value) { + if ($value !== 0) { + $url->param($key, $value); + } +} +$url->param('sr', $sectionreturn); +if ($add !== '') { + $url->param('add', $add); +} +if ($type !== '') { + $url->param('type', $type); +} +if ($groupmode !== '') { + $url->param('groupmode', $groupmode); +} +$PAGE->set_url($url); + +require_login(); + +//check if we are adding / editing a module that has new forms using formslib +if (!empty($add)) { + $id = required_param('id', PARAM_INT); + $section = required_param('section', PARAM_INT); + $type = optional_param('type', '', PARAM_ALPHA); + $returntomod = optional_param('return', 0, PARAM_BOOL); + + redirect("$CFG->wwwroot/course/modedit.php?add=$add&type=$type&course=$id§ion=$section&return=$returntomod&sr=$sectionreturn"); + +} else if (!empty($update)) { + $cm = get_coursemodule_from_id('', $update, 0, true, MUST_EXIST); + $returntomod = optional_param('return', 0, PARAM_BOOL); + redirect("$CFG->wwwroot/course/modedit.php?update=$update&return=$returntomod&sr=$sectionreturn"); + +} else if (!empty($duplicate)) { + $cm = get_coursemodule_from_id('', $duplicate, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $coursecontext); + + if (!$confirm or !confirm_sesskey()) { + $PAGE->set_title(get_string('duplicate')); + $PAGE->set_heading($course->fullname); + $PAGE->navbar->add(get_string('duplicatinga', 'core', format_string($cm->name))); + $PAGE->set_pagelayout('incourse'); + + $a = new stdClass(); + $a->modtype = get_string('modulename', $cm->modname); + $a->modname = format_string($cm->name); + $a->modid = $cm->id; + + echo $OUTPUT->header(); + echo $OUTPUT->confirm( + get_string('duplicateconfirm', 'core', $a), + new single_button( + new moodle_url('/course/modduplicate.php', array( + 'cmid' => $cm->id, 'course' => $course->id, 'sr' => $sectionreturn)), + get_string('continue'), + 'post'), + new single_button( + course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)), + get_string('cancel'), + 'get') + ); + echo $OUTPUT->footer(); + die(); + } + +} else if (!empty($delete)) { + $cm = get_coursemodule_from_id('', $delete, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $modcontext); + + $return = course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)); + + if (!$confirm or !confirm_sesskey()) { + $fullmodulename = get_string('modulename', $cm->modname); + + $optionsyes = array('confirm'=>1, 'delete'=>$cm->id, 'sesskey'=>sesskey(), 'sr' => $sectionreturn); + + $strdeletecheck = get_string('deletecheck', '', $fullmodulename); + $strdeletecheckfull = get_string('deletecheckfull', '', "$fullmodulename '$cm->name'"); + + $PAGE->set_pagetype('mod-' . $cm->modname . '-delete'); + $PAGE->set_title($strdeletecheck); + $PAGE->set_heading($course->fullname); + $PAGE->navbar->add($strdeletecheck); + echo $OUTPUT->header(); + + // print_simple_box_start('center', '60%', '#FFAAAA', 20, 'noticebox'); + echo $OUTPUT->box_start('noticebox'); + $formcontinue = new single_button(new moodle_url("$CFG->wwwroot/course/mod.php", $optionsyes), get_string('yes')); + $formcancel = new single_button($return, get_string('no'), 'get'); + echo $OUTPUT->confirm($strdeletecheckfull, $formcontinue, $formcancel); + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); + + exit; + } + + $modlib = "$CFG->dirroot/mod/$cm->modname/lib.php"; + + if (file_exists($modlib)) { + require_once($modlib); + } else { + print_error('modulemissingcode', '', '', $modlib); + } + + $deleteinstancefunction = $cm->modname."_delete_instance"; + + if (!$deleteinstancefunction($cm->instance)) { + echo $OUTPUT->notification("Could not delete the $cm->modname (instance)"); + } + + // remove all module files in case modules forget to do that + $fs = get_file_storage(); + $fs->delete_area_files($modcontext->id); + + if (!delete_course_module($cm->id)) { + echo $OUTPUT->notification("Could not delete the $cm->modname (coursemodule)"); + } + if (!delete_mod_from_section($cm->id, $cm->section)) { + echo $OUTPUT->notification("Could not delete the $cm->modname from that section"); + } + + // Trigger a mod_deleted event with information about this module. + $eventdata = new stdClass(); + $eventdata->modulename = $cm->modname; + $eventdata->cmid = $cm->id; + $eventdata->courseid = $course->id; + $eventdata->userid = $USER->id; + events_trigger('mod_deleted', $eventdata); + + add_to_log($course->id, 'course', "delete mod", + "view.php?id=$cm->course", + "$cm->modname $cm->instance", $cm->id); + + redirect($return); +} + + +if ((!empty($movetosection) or !empty($moveto)) and confirm_sesskey()) { + $cm = get_coursemodule_from_id('', $USER->activitycopy, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $modcontext); + + if (!empty($movetosection)) { + if (!$section = $DB->get_record('course_sections', array('id'=>$movetosection, 'course'=>$cm->course))) { + print_error('sectionnotexist'); + } + $beforecm = NULL; + + } else { // normal moveto + if (!$beforecm = get_coursemodule_from_id('', $moveto, $cm->course, true)) { + print_error('invalidcoursemodule'); + } + if (!$section = $DB->get_record('course_sections', array('id'=>$beforecm->section, 'course'=>$cm->course))) { + print_error('sectionnotexist'); + } + } + + if (!ismoving($section->course)) { + print_error('needcopy', '', "view.php?id=$section->course"); + } + + moveto_module($cm, $section, $beforecm); + + $sectionreturn = $USER->activitycopysectionreturn; + unset($USER->activitycopy); + unset($USER->activitycopycourse); + unset($USER->activitycopyname); + unset($USER->activitycopysectionreturn); + + redirect(course_get_url($course, $section->section, array('sr' => $sectionreturn))); + +} else if (!empty($indent) and confirm_sesskey()) { + $id = required_param('id', PARAM_INT); + + $cm = get_coursemodule_from_id('', $id, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $modcontext); + + $cm->indent += $indent; + + if ($cm->indent < 0) { + $cm->indent = 0; + } + + $DB->set_field('course_modules', 'indent', $cm->indent, array('id'=>$cm->id)); + + rebuild_course_cache($cm->course); + + redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); + +} else if (!empty($hide) and confirm_sesskey()) { + $cm = get_coursemodule_from_id('', $hide, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:activityvisibility', $modcontext); + + set_coursemodule_visible($cm->id, 0); + + redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); + +} else if (!empty($show) and confirm_sesskey()) { + $cm = get_coursemodule_from_id('', $show, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:activityvisibility', $modcontext); + + $section = $DB->get_record('course_sections', array('id'=>$cm->section), '*', MUST_EXIST); + + $module = $DB->get_record('modules', array('id'=>$cm->module), '*', MUST_EXIST); + + if ($module->visible and ($section->visible or (SITEID == $cm->course))) { + set_coursemodule_visible($cm->id, 1); + } + + redirect(course_get_url($course, $section->section, array('sr' => $sectionreturn))); + +} else if ($groupmode > -1 and confirm_sesskey()) { + $id = required_param('id', PARAM_INT); + + $cm = get_coursemodule_from_id('', $id, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $modcontext); + + set_coursemodule_groupmode($cm->id, $groupmode); + + redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); + +} else if (!empty($copy) and confirm_sesskey()) { // value = course module + $cm = get_coursemodule_from_id('', $copy, 0, true, MUST_EXIST); + $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); + $coursecontext = context_course::instance($course->id); + $modcontext = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $modcontext); + + $section = $DB->get_record('course_sections', array('id'=>$cm->section), '*', MUST_EXIST); + + $USER->activitycopy = $copy; + $USER->activitycopycourse = $cm->course; + $USER->activitycopyname = $cm->name; + $USER->activitycopysectionreturn = $sectionreturn; + + redirect(course_get_url($course, $section->section, array('sr' => $sectionreturn))); + +} else if (!empty($cancelcopy) and confirm_sesskey()) { // value = course module + + $courseid = $USER->activitycopycourse; + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + + $cm = get_coursemodule_from_id('', $USER->activitycopy, 0, true, IGNORE_MISSING); + $sectionreturn = $USER->activitycopysectionreturn; + unset($USER->activitycopy); + unset($USER->activitycopycourse); + unset($USER->activitycopyname); + unset($USER->activitycopysectionreturn); + redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); +} else { + print_error('unknowaction'); +} + + diff --git a/modduplicate.php b/modduplicate.php new file mode 100644 index 0000000..d175a87 --- /dev/null +++ b/modduplicate.php @@ -0,0 +1,151 @@ +. + +/** + * Duplicates a given course module + * + * The script backups and restores a single activity as if it was imported + * from the same course, using the default import settings. The newly created + * copy of the activity is then moved right below the original one. + * + * @package core + * @subpackage course + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(dirname(__FILE__)) . '/config.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/filelib.php'); + +$cmid = required_param('cmid', PARAM_INT); +$courseid = required_param('course', PARAM_INT); +$sectionreturn = optional_param('sr', null, PARAM_INT); + +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); +$cm = get_coursemodule_from_id('', $cmid, $course->id, true, MUST_EXIST); +$cmcontext = context_module::instance($cm->id); +$context = context_course::instance($courseid); +$section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course)); + +require_login($course); +require_sesskey(); +require_capability('moodle/course:manageactivities', $context); +// Require both target import caps to be able to duplicate, see make_editing_buttons() +require_capability('moodle/backup:backuptargetimport', $context); +require_capability('moodle/restore:restoretargetimport', $context); + +$PAGE->set_title(get_string('duplicate')); +$PAGE->set_heading($course->fullname); +$PAGE->set_url(new moodle_url('/course/modduplicate.php', array('cmid' => $cm->id, 'courseid' => $course->id))); +$PAGE->set_pagelayout('incourse'); + +$output = $PAGE->get_renderer('core', 'backup'); + +$a = new stdClass(); +$a->modtype = get_string('modulename', $cm->modname); +$a->modname = format_string($cm->name); + +if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) { + $url = course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)); + print_error('duplicatenosupport', 'error', $url, $a); +} + +// backup the activity + +$bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + +$backupid = $bc->get_backupid(); +$backupbasepath = $bc->get_plan()->get_basepath(); + +$bc->execute_plan(); + +$bc->destroy(); + +// restore the backup immediately + +$rc = new restore_controller($backupid, $courseid, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); + +if (!$rc->execute_precheck()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + echo $output->header(); + echo $output->precheck_notices($precheckresults); + $url = course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)); + echo $output->continue_button($url); + echo $output->footer(); + die(); + } +} + +$rc->execute_plan(); + +// now a bit hacky part follows - we try to get the cmid of the newly +// restored copy of the module +$newcmid = null; +$tasks = $rc->get_plan()->get_tasks(); +foreach ($tasks as $task) { + if (is_subclass_of($task, 'restore_activity_task')) { + if ($task->get_old_contextid() == $cmcontext->id) { + $newcmid = $task->get_moduleid(); + break; + } + } +} + +// if we know the cmid of the new course module, let us move it +// right below the original one. otherwise it will stay at the +// end of the section +if ($newcmid) { + $newcm = get_coursemodule_from_id('', $newcmid, $course->id, true, MUST_EXIST); + moveto_module($newcm, $section, $cm); + moveto_module($cm, $section, $newcm); +} + +$rc->destroy(); + +if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); +} + +echo $output->header(); + +if ($newcmid) { + echo $output->confirm( + get_string('duplicatesuccess', 'core', $a), + new single_button( + new moodle_url('/course/modedit.php', array('update' => $newcmid, 'sr' => $sectionreturn)), + get_string('duplicatecontedit'), + 'get'), + new single_button( + course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)), + get_string('duplicatecontcourse'), + 'get') + ); + +} else { + echo $output->notification(get_string('duplicatesuccess', 'core', $a), 'notifysuccess'); + echo $output->continue_button(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn))); +} + +echo $output->footer(); diff --git a/modedit.php b/modedit.php new file mode 100644 index 0000000..c08108a --- /dev/null +++ b/modedit.php @@ -0,0 +1,665 @@ +. + +/** +* Adds or updates modules in a course using new formslib +* +* @package moodlecore +* @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +require_once("../config.php"); +require_once("lib.php"); +require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->libdir.'/gradelib.php'); +require_once($CFG->libdir.'/completionlib.php'); +require_once($CFG->libdir.'/conditionlib.php'); +require_once($CFG->libdir.'/plagiarismlib.php'); + +$add = optional_param('add', '', PARAM_ALPHA); // module name +$update = optional_param('update', 0, PARAM_INT); +$return = optional_param('return', 0, PARAM_BOOL); //return to course/view.php if false or mod/modname/view.php if true +$type = optional_param('type', '', PARAM_ALPHANUM); //TODO: hopefully will be removed in 2.0 +$sectionreturn = optional_param('sr', null, PARAM_INT); + +$url = new moodle_url('/course/modedit.php'); +$url->param('sr', $sectionreturn); +if (!empty($return)) { + $url->param('return', $return); +} + +if (!empty($add)) { + $section = required_param('section', PARAM_INT); + $course = required_param('course', PARAM_INT); + + $url->param('add', $add); + $url->param('section', $section); + $url->param('course', $course); + $PAGE->set_url($url); + + $course = $DB->get_record('course', array('id'=>$course), '*', MUST_EXIST); + $module = $DB->get_record('modules', array('name'=>$add), '*', MUST_EXIST); + + require_login($course); + $context = context_course::instance($course->id); + require_capability('moodle/course:manageactivities', $context); + + course_create_sections_if_missing($course, $section); + $cw = get_fast_modinfo($course)->get_section_info($section); + + if (!course_allowed_module($course, $module->name)) { + print_error('moduledisable'); + } + + $cm = null; + + $data = new stdClass(); + $data->section = $section; // The section number itself - relative!!! (section column in course_sections) + $data->visible = $cw->visible; + $data->course = $course->id; + $data->module = $module->id; + $data->modulename = $module->name; + $data->groupmode = $course->groupmode; + $data->groupingid = $course->defaultgroupingid; + $data->groupmembersonly = 0; + $data->id = ''; + $data->instance = ''; + $data->coursemodule = ''; + $data->add = $add; + $data->return = 0; //must be false if this is an add, go back to course view on cancel + $data->sr = $sectionreturn; + + if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { + $draftid_editor = file_get_submitted_draft_itemid('introeditor'); + file_prepare_draft_area($draftid_editor, null, null, null, null); + $data->introeditor = array('text'=>'', 'format'=>FORMAT_HTML, 'itemid'=>$draftid_editor); // TODO: add better default + } + + if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) + and has_capability('moodle/grade:managegradingforms', $context)) { + require_once($CFG->dirroot.'/grade/grading/lib.php'); + + $data->_advancedgradingdata['methods'] = grading_manager::available_methods(); + $areas = grading_manager::available_areas('mod_'.$module->name); + + foreach ($areas as $areaname => $areatitle) { + $data->_advancedgradingdata['areas'][$areaname] = array( + 'title' => $areatitle, + 'method' => '', + ); + $formfield = 'advancedgradingmethod_'.$areaname; + $data->{$formfield} = ''; + } + } + + if (!empty($type)) { //TODO: hopefully will be removed in 2.0 + $data->type = $type; + } + + $sectionname = get_section_name($course, $cw); + $fullmodulename = get_string('modulename', $module->name); + + if ($data->section && $course->format != 'site') { + $heading = new stdClass(); + $heading->what = $fullmodulename; + $heading->to = $sectionname; + $pageheading = get_string('addinganewto', 'moodle', $heading); + } else { + $pageheading = get_string('addinganew', 'moodle', $fullmodulename); + } + +} else if (!empty($update)) { + + $url->param('update', $update); + $PAGE->set_url($url); + + $cm = get_coursemodule_from_id('', $update, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); + + require_login($course, false, $cm); // needed to setup proper $COURSE + $context = context_module::instance($cm->id); + require_capability('moodle/course:manageactivities', $context); + + $module = $DB->get_record('modules', array('id'=>$cm->module), '*', MUST_EXIST); + $data = $data = $DB->get_record($module->name, array('id'=>$cm->instance), '*', MUST_EXIST); + $cw = $DB->get_record('course_sections', array('id'=>$cm->section), '*', MUST_EXIST); + + $data->coursemodule = $cm->id; + $data->section = $cw->section; // The section number itself - relative!!! (section column in course_sections) + $data->visible = $cm->visible; //?? $cw->visible ? $cm->visible : 0; // section hiding overrides + $data->cmidnumber = $cm->idnumber; // The cm IDnumber + $data->groupmode = groups_get_activity_groupmode($cm); // locked later if forced + $data->groupingid = $cm->groupingid; + $data->groupmembersonly = $cm->groupmembersonly; + $data->course = $course->id; + $data->module = $module->id; + $data->modulename = $module->name; + $data->instance = $cm->instance; + $data->return = $return; + $data->sr = $sectionreturn; + $data->update = $update; + $data->completion = $cm->completion; + $data->completionview = $cm->completionview; + $data->completionexpected = $cm->completionexpected; + $data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1; + $data->showdescription = $cm->showdescription; + if (!empty($CFG->enableavailability)) { + $data->availablefrom = $cm->availablefrom; + $data->availableuntil = $cm->availableuntil; + $data->showavailability = $cm->showavailability; + } + + if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { + $draftid_editor = file_get_submitted_draft_itemid('introeditor'); + $currentintro = file_prepare_draft_area($draftid_editor, $context->id, 'mod_'.$data->modulename, 'intro', 0, array('subdirs'=>true), $data->intro); + $data->introeditor = array('text'=>$currentintro, 'format'=>$data->introformat, 'itemid'=>$draftid_editor); + } + + if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) + and has_capability('moodle/grade:managegradingforms', $context)) { + require_once($CFG->dirroot.'/grade/grading/lib.php'); + $gradingman = get_grading_manager($context, 'mod_'.$data->modulename); + $data->_advancedgradingdata['methods'] = $gradingman->get_available_methods(); + $areas = $gradingman->get_available_areas(); + + foreach ($areas as $areaname => $areatitle) { + $gradingman->set_area($areaname); + $method = $gradingman->get_active_method(); + $data->_advancedgradingdata['areas'][$areaname] = array( + 'title' => $areatitle, + 'method' => $method, + ); + $formfield = 'advancedgradingmethod_'.$areaname; + $data->{$formfield} = $method; + } + } + + if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename, + 'iteminstance'=>$data->instance, 'courseid'=>$course->id))) { + // add existing outcomes + foreach ($items as $item) { + if (!empty($item->outcomeid)) { + $data->{'outcome_'.$item->outcomeid} = 1; + } + } + + // set category if present + $gradecat = false; + foreach ($items as $item) { + if ($gradecat === false) { + $gradecat = $item->categoryid; + continue; + } + if ($gradecat != $item->categoryid) { + //mixed categories + $gradecat = false; + break; + } + } + if ($gradecat !== false) { + // do not set if mixed categories present + $data->gradecat = $gradecat; + } + } + + $sectionname = get_section_name($course, $cw); + $fullmodulename = get_string('modulename', $module->name); + + if ($data->section && $course->format != 'site') { + $heading = new stdClass(); + $heading->what = $fullmodulename; + $heading->in = $sectionname; + $pageheading = get_string('updatingain', 'moodle', $heading); + } else { + $pageheading = get_string('updatinga', 'moodle', $fullmodulename); + } + +} else { + require_login(); + print_error('invalidaction'); +} + +$pagepath = 'mod-' . $module->name . '-'; +if (!empty($type)) { //TODO: hopefully will be removed in 2.0 + $pagepath .= $type; +} else { + $pagepath .= 'mod'; +} +$PAGE->set_pagetype($pagepath); +$PAGE->set_pagelayout('admin'); + +$modmoodleform = "$CFG->dirroot/mod/$module->name/mod_form.php"; +if (file_exists($modmoodleform)) { + require_once($modmoodleform); +} else { + print_error('noformdesc'); +} + +$modlib = "$CFG->dirroot/mod/$module->name/lib.php"; +if (file_exists($modlib)) { + include_once($modlib); +} else { + print_error('modulemissingcode', '', '', $modlib); +} + +$mformclassname = 'mod_'.$module->name.'_mod_form'; +$mform = new $mformclassname($data, $cw->section, $cm, $course); +$mform->set_data($data); + +if ($mform->is_cancelled()) { + if ($return && !empty($cm->id)) { + redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$cm->id"); + } else { + redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn))); + } +} else if ($fromform = $mform->get_data()) { + if (empty($fromform->coursemodule)) { + // Add + $cm = null; + $course = $DB->get_record('course', array('id'=>$fromform->course), '*', MUST_EXIST); + $fromform->instance = ''; + $fromform->coursemodule = ''; + } else { + // Update + $cm = get_coursemodule_from_id('', $fromform->coursemodule, 0, false, MUST_EXIST); + $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); + $fromform->instance = $cm->instance; + $fromform->coursemodule = $cm->id; + } + + if (!empty($fromform->coursemodule)) { + $context = context_module::instance($fromform->coursemodule); + } else { + $context = context_course::instance($course->id); + } + + $fromform->course = $course->id; + $fromform->modulename = clean_param($fromform->modulename, PARAM_PLUGIN); // For safety + + $addinstancefunction = $fromform->modulename."_add_instance"; + $updateinstancefunction = $fromform->modulename."_update_instance"; + + if (!isset($fromform->groupingid)) { + $fromform->groupingid = 0; + } + + if (!isset($fromform->groupmembersonly)) { + $fromform->groupmembersonly = 0; + } + + if (!isset($fromform->name)) { //label + $fromform->name = $fromform->modulename; + } + + if (!isset($fromform->completion)) { + $fromform->completion = COMPLETION_DISABLED; + } + if (!isset($fromform->completionview)) { + $fromform->completionview = COMPLETION_VIEW_NOT_REQUIRED; + } + + // Convert the 'use grade' checkbox into a grade-item number: 0 if + // checked, null if not + if (isset($fromform->completionusegrade) && $fromform->completionusegrade) { + $fromform->completiongradeitemnumber = 0; + } else { + $fromform->completiongradeitemnumber = null; + } + + // the type of event to trigger (mod_created/mod_updated) + $eventname = ''; + + if (!empty($fromform->update)) { + + if (!empty($course->groupmodeforce) or !isset($fromform->groupmode)) { + $fromform->groupmode = $cm->groupmode; // keep original + } + + // update course module first + $cm->groupmode = $fromform->groupmode; + $cm->groupingid = $fromform->groupingid; + $cm->groupmembersonly = $fromform->groupmembersonly; + + $completion = new completion_info($course); + if ($completion->is_enabled()) { + // Update completion settings + $cm->completion = $fromform->completion; + $cm->completiongradeitemnumber = $fromform->completiongradeitemnumber; + $cm->completionview = $fromform->completionview; + $cm->completionexpected = $fromform->completionexpected; + } + if (!empty($CFG->enableavailability)) { + $cm->availablefrom = $fromform->availablefrom; + $cm->availableuntil = $fromform->availableuntil; + $cm->showavailability = $fromform->showavailability; + condition_info::update_cm_from_form($cm,$fromform,true); + } + if (isset($fromform->showdescription)) { + $cm->showdescription = $fromform->showdescription; + } else { + $cm->showdescription = 0; + } + + $DB->update_record('course_modules', $cm); + + $modcontext = context_module::instance($fromform->coursemodule); + + // update embedded links and save files + if (plugin_supports('mod', $fromform->modulename, FEATURE_MOD_INTRO, true)) { + $fromform->intro = file_save_draft_area_files($fromform->introeditor['itemid'], $modcontext->id, + 'mod_'.$fromform->modulename, 'intro', 0, + array('subdirs'=>true), $fromform->introeditor['text']); + $fromform->introformat = $fromform->introeditor['format']; + unset($fromform->introeditor); + } + + if (!$updateinstancefunction($fromform, $mform)) { + print_error('cannotupdatemod', '', course_get_url($course, $cw->section), $fromform->modulename); + } + + // make sure visibility is set correctly (in particular in calendar) + if (has_capability('moodle/course:activityvisibility', $modcontext)) { + set_coursemodule_visible($fromform->coursemodule, $fromform->visible); + } + + if (isset($fromform->cmidnumber)) { //label + // set cm idnumber - uniqueness is already verified by form validation + set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber); + } + + // Now that module is fully updated, also update completion data if + // required (this will wipe all user completion data and recalculate it) + if ($completion->is_enabled() && !empty($fromform->completionunlocked)) { + $completion->reset_all_state($cm); + } + + $eventname = 'mod_updated'; + + add_to_log($course->id, "course", "update mod", + "../mod/$fromform->modulename/view.php?id=$fromform->coursemodule", + "$fromform->modulename $fromform->instance"); + add_to_log($course->id, $fromform->modulename, "update", + "view.php?id=$fromform->coursemodule", + "$fromform->instance", $fromform->coursemodule); + + } else if (!empty($fromform->add)) { + + if (!empty($course->groupmodeforce) or !isset($fromform->groupmode)) { + $fromform->groupmode = 0; // do not set groupmode + } + + if (!course_allowed_module($course, $fromform->modulename)) { + print_error('moduledisable', '', '', $fromform->modulename); + } + + // first add course_module record because we need the context + $newcm = new stdClass(); + $newcm->course = $course->id; + $newcm->module = $fromform->module; + $newcm->instance = 0; // not known yet, will be updated later (this is similar to restore code) + $newcm->visible = $fromform->visible; + $newcm->groupmode = $fromform->groupmode; + $newcm->groupingid = $fromform->groupingid; + $newcm->groupmembersonly = $fromform->groupmembersonly; + $completion = new completion_info($course); + if ($completion->is_enabled()) { + $newcm->completion = $fromform->completion; + $newcm->completiongradeitemnumber = $fromform->completiongradeitemnumber; + $newcm->completionview = $fromform->completionview; + $newcm->completionexpected = $fromform->completionexpected; + } + if(!empty($CFG->enableavailability)) { + $newcm->availablefrom = $fromform->availablefrom; + $newcm->availableuntil = $fromform->availableuntil; + $newcm->showavailability = $fromform->showavailability; + } + if (isset($fromform->showdescription)) { + $newcm->showdescription = $fromform->showdescription; + } else { + $newcm->showdescription = 0; + } + + if (!$fromform->coursemodule = add_course_module($newcm)) { + print_error('cannotaddcoursemodule'); + } + + if (plugin_supports('mod', $fromform->modulename, FEATURE_MOD_INTRO, true)) { + $introeditor = $fromform->introeditor; + unset($fromform->introeditor); + $fromform->intro = $introeditor['text']; + $fromform->introformat = $introeditor['format']; + } + + $returnfromfunc = $addinstancefunction($fromform, $mform); + + if (!$returnfromfunc or !is_number($returnfromfunc)) { + // undo everything we can + $modcontext = context_module::instance($fromform->coursemodule); + delete_context(CONTEXT_MODULE, $fromform->coursemodule); + $DB->delete_records('course_modules', array('id'=>$fromform->coursemodule)); + + if (!is_number($returnfromfunc)) { + print_error('invalidfunction', '', course_get_url($course, $cw->section)); + } else { + print_error('cannotaddnewmodule', '', course_get_url($course, $cw->section), $fromform->modulename); + } + } + + $fromform->instance = $returnfromfunc; + + $DB->set_field('course_modules', 'instance', $returnfromfunc, array('id'=>$fromform->coursemodule)); + + // update embedded links and save files + $modcontext = context_module::instance($fromform->coursemodule); + if (!empty($introeditor)) { + $fromform->intro = file_save_draft_area_files($introeditor['itemid'], $modcontext->id, + 'mod_'.$fromform->modulename, 'intro', 0, + array('subdirs'=>true), $introeditor['text']); + $DB->set_field($fromform->modulename, 'intro', $fromform->intro, array('id'=>$fromform->instance)); + } + + // course_modules and course_sections each contain a reference + // to each other, so we have to update one of them twice. + $sectionid = course_add_cm_to_section($course, $fromform->coursemodule, $fromform->section); + + // make sure visibility is set correctly (in particular in calendar) + // note: allow them to set it even without moodle/course:activityvisibility + set_coursemodule_visible($fromform->coursemodule, $fromform->visible); + $DB->set_field('course_modules', 'visibleold', 1, array('id' => $fromform->coursemodule)); + + if (isset($fromform->cmidnumber)) { //label + // set cm idnumber - uniqueness is already verified by form validation + set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber); + } + + // Set up conditions + if ($CFG->enableavailability) { + condition_info::update_cm_from_form((object)array('id'=>$fromform->coursemodule), $fromform, false); + } + + $eventname = 'mod_created'; + + add_to_log($course->id, "course", "add mod", + "../mod/$fromform->modulename/view.php?id=$fromform->coursemodule", + "$fromform->modulename $fromform->instance"); + add_to_log($course->id, $fromform->modulename, "add", + "view.php?id=$fromform->coursemodule", + "$fromform->instance", $fromform->coursemodule); + } else { + print_error('invaliddata'); + } + + // Trigger mod_created/mod_updated event with information about this module. + $eventdata = new stdClass(); + $eventdata->modulename = $fromform->modulename; + $eventdata->name = $fromform->name; + $eventdata->cmid = $fromform->coursemodule; + $eventdata->courseid = $course->id; + $eventdata->userid = $USER->id; + events_trigger($eventname, $eventdata); + + // sync idnumber with grade_item + if ($grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$fromform->modulename, + 'iteminstance'=>$fromform->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) { + if ($grade_item->idnumber != $fromform->cmidnumber) { + $grade_item->idnumber = $fromform->cmidnumber; + $grade_item->update(); + } + } + + $items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$fromform->modulename, + 'iteminstance'=>$fromform->instance, 'courseid'=>$course->id)); + + // create parent category if requested and move to correct parent category + if ($items and isset($fromform->gradecat)) { + if ($fromform->gradecat == -1) { + $grade_category = new grade_category(); + $grade_category->courseid = $course->id; + $grade_category->fullname = $fromform->name; + $grade_category->insert(); + if ($grade_item) { + $parent = $grade_item->get_parent_category(); + $grade_category->set_parent($parent->id); + } + $fromform->gradecat = $grade_category->id; + } + foreach ($items as $itemid=>$unused) { + $items[$itemid]->set_parent($fromform->gradecat); + if ($itemid == $grade_item->id) { + // use updated grade_item + $grade_item = $items[$itemid]; + } + } + } + + // add outcomes if requested + if ($outcomes = grade_outcome::fetch_all_available($course->id)) { + $grade_items = array(); + + // Outcome grade_item.itemnumber start at 1000, there is nothing above outcomes + $max_itemnumber = 999; + if ($items) { + foreach($items as $item) { + if ($item->itemnumber > $max_itemnumber) { + $max_itemnumber = $item->itemnumber; + } + } + } + + foreach($outcomes as $outcome) { + $elname = 'outcome_'.$outcome->id; + + if (property_exists($fromform, $elname) and $fromform->$elname) { + // so we have a request for new outcome grade item? + if ($items) { + foreach($items as $item) { + if ($item->outcomeid == $outcome->id) { + //outcome aready exists + continue 2; + } + } + } + + $max_itemnumber++; + + $outcome_item = new grade_item(); + $outcome_item->courseid = $course->id; + $outcome_item->itemtype = 'mod'; + $outcome_item->itemmodule = $fromform->modulename; + $outcome_item->iteminstance = $fromform->instance; + $outcome_item->itemnumber = $max_itemnumber; + $outcome_item->itemname = $outcome->fullname; + $outcome_item->outcomeid = $outcome->id; + $outcome_item->gradetype = GRADE_TYPE_SCALE; + $outcome_item->scaleid = $outcome->scaleid; + $outcome_item->insert(); + + // move the new outcome into correct category and fix sortorder if needed + if ($grade_item) { + $outcome_item->set_parent($grade_item->categoryid); + $outcome_item->move_after_sortorder($grade_item->sortorder); + + } else if (isset($fromform->gradecat)) { + $outcome_item->set_parent($fromform->gradecat); + } + } + } + } + + if (plugin_supports('mod', $fromform->modulename, FEATURE_ADVANCED_GRADING, false) + and has_capability('moodle/grade:managegradingforms', $modcontext)) { + require_once($CFG->dirroot.'/grade/grading/lib.php'); + $gradingman = get_grading_manager($modcontext, 'mod_'.$fromform->modulename); + $showgradingmanagement = false; + foreach ($gradingman->get_available_areas() as $areaname => $aretitle) { + $formfield = 'advancedgradingmethod_'.$areaname; + if (isset($fromform->{$formfield})) { + $gradingman->set_area($areaname); + $methodchanged = $gradingman->set_active_method($fromform->{$formfield}); + if (empty($fromform->{$formfield})) { + // going back to the simple direct grading is not a reason + // to open the management screen + $methodchanged = false; + } + $showgradingmanagement = $showgradingmanagement || $methodchanged; + } + } + } + + rebuild_course_cache($course->id); + grade_regrade_final_grades($course->id); + plagiarism_save_form_elements($fromform); //save plagiarism settings + + if (isset($fromform->submitbutton)) { + if (empty($showgradingmanagement)) { + redirect("$CFG->wwwroot/mod/$module->name/view.php?id=$fromform->coursemodule"); + } else { + $returnurl = new moodle_url("/mod/$module->name/view.php", array('id' => $fromform->coursemodule)); + redirect($gradingman->get_management_url($returnurl)); + } + } else { + redirect(course_get_url($course, $cw->section, array('sr' => $sectionreturn))); + } + exit; + +} else { + + $streditinga = get_string('editinga', 'moodle', $fullmodulename); + $strmodulenameplural = get_string('modulenameplural', $module->name); + + if (!empty($cm->id)) { + $context = context_module::instance($cm->id); + } else { + $context = context_course::instance($course->id); + } + + $PAGE->set_heading($course->fullname); + $PAGE->set_title($streditinga); + $PAGE->set_cacheable(false); + echo $OUTPUT->header(); + + if (get_string_manager()->string_exists('modulename_help', $module->name)) { + echo $OUTPUT->heading_with_help($pageheading, 'modulename', $module->name, 'icon'); + } else { + echo $OUTPUT->heading_with_help($pageheading, '', $module->name, 'icon'); + } + + $mform->display(); + + echo $OUTPUT->footer(); +} diff --git a/moodleform_mod.php b/moodleform_mod.php new file mode 100644 index 0000000..b4feb9d --- /dev/null +++ b/moodleform_mod.php @@ -0,0 +1,862 @@ +libdir.'/formslib.php'); +require_once($CFG->libdir.'/completionlib.php'); + +/** + * This class adds extra methods to form wrapper specific to be used for module + * add / update forms mod/{modname}/mod_form.php replaced deprecated mod/{modname}/mod.html + */ +abstract class moodleform_mod extends moodleform { + /** Current data */ + protected $current; + /** + * Instance of the module that is being updated. This is the id of the {prefix}{modulename} + * record. Can be used in form definition. Will be "" if this is an 'add' form and not an + * update one. + * + * @var mixed + */ + protected $_instance; + /** + * Section of course that module instance will be put in or is in. + * This is always the section number itself (column 'section' from 'course_sections' table). + * + * @var mixed + */ + protected $_section; + /** + * Course module record of the module that is being updated. Will be null if this is an 'add' form and not an + * update one. + * + * @var mixed + */ + protected $_cm; + /** + * List of modform features + */ + protected $_features; + /** + * @var array Custom completion-rule elements, if enabled + */ + protected $_customcompletionelements; + /** + * @var string name of module + */ + protected $_modname; + /** current context, course or module depends if already exists*/ + protected $context; + + /** a flag indicating whether outcomes are being used*/ + protected $_outcomesused; + + function moodleform_mod($current, $section, $cm, $course) { + $this->current = $current; + $this->_instance = $current->instance; + $this->_section = $section; + $this->_cm = $cm; + if ($this->_cm) { + $this->context = context_module::instance($this->_cm->id); + } else { + $this->context = context_course::instance($course->id); + } + + // Guess module name + $matches = array(); + if (!preg_match('/^mod_([^_]+)_mod_form$/', get_class($this), $matches)) { + debugging('Use $modname parameter or rename form to mod_xx_mod_form, where xx is name of your module'); + print_error('unknownmodulename'); + } + $this->_modname = $matches[1]; + $this->init_features(); + parent::moodleform('modedit.php'); + } + + protected function init_features() { + global $CFG; + + $this->_features = new stdClass(); + $this->_features->groups = plugin_supports('mod', $this->_modname, FEATURE_GROUPS, true); + $this->_features->groupings = plugin_supports('mod', $this->_modname, FEATURE_GROUPINGS, false); + $this->_features->groupmembersonly = (!empty($CFG->enablegroupmembersonly) and plugin_supports('mod', $this->_modname, FEATURE_GROUPMEMBERSONLY, false)); + $this->_features->outcomes = (!empty($CFG->enableoutcomes) and plugin_supports('mod', $this->_modname, FEATURE_GRADE_OUTCOMES, true)); + $this->_features->hasgrades = plugin_supports('mod', $this->_modname, FEATURE_GRADE_HAS_GRADE, false); + $this->_features->idnumber = plugin_supports('mod', $this->_modname, FEATURE_IDNUMBER, true); + $this->_features->introeditor = plugin_supports('mod', $this->_modname, FEATURE_MOD_INTRO, true); + $this->_features->defaultcompletion = plugin_supports('mod', $this->_modname, FEATURE_MODEDIT_DEFAULT_COMPLETION, true); + $this->_features->rating = plugin_supports('mod', $this->_modname, FEATURE_RATE, false); + $this->_features->showdescription = plugin_supports('mod', $this->_modname, FEATURE_SHOW_DESCRIPTION, false); + + $this->_features->gradecat = ($this->_features->outcomes or $this->_features->hasgrades); + $this->_features->advancedgrading = plugin_supports('mod', $this->_modname, FEATURE_ADVANCED_GRADING, false); + } + + /** + * Only available on moodleform_mod. + * + * @param array $default_values passed by reference + */ + function data_preprocessing(&$default_values){ + if (empty($default_values['scale'])) { + $default_values['assessed'] = 0; + } + + if (empty($default_values['assessed'])){ + $default_values['ratingtime'] = 0; + } else { + $default_values['ratingtime']= + ($default_values['assesstimestart'] && $default_values['assesstimefinish']) ? 1 : 0; + } + } + + /** + * Each module which defines definition_after_data() must call this method using parent::definition_after_data(); + */ + function definition_after_data() { + global $CFG, $COURSE; + $mform =& $this->_form; + + if ($id = $mform->getElementValue('update')) { + $modulename = $mform->getElementValue('modulename'); + $instance = $mform->getElementValue('instance'); + + if ($this->_features->gradecat) { + $gradecat = false; + if (!empty($CFG->enableoutcomes) and $this->_features->outcomes) { + $outcomes = grade_outcome::fetch_all_available($COURSE->id); + if (!empty($outcomes)) { + $gradecat = true; + } + } + + $items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename,'iteminstance'=>$instance, 'courseid'=>$COURSE->id)); + //will be no items if, for example, this activity supports ratings but rating aggregate type == no ratings + if (!empty($items)) { + foreach ($items as $item) { + if (!empty($item->outcomeid)) { + $elname = 'outcome_'.$item->outcomeid; + if ($mform->elementExists($elname)) { + $mform->hardFreeze($elname); // prevent removing of existing outcomes + } + } + } + + foreach ($items as $item) { + if (is_bool($gradecat)) { + $gradecat = $item->categoryid; + continue; + } + if ($gradecat != $item->categoryid) { + //mixed categories + $gradecat = false; + break; + } + } + } + + if ($gradecat === false) { + // items and outcomes in different categories - remove the option + // TODO: add a "Mixed categories" text instead of removing elements with no explanation + if ($mform->elementExists('gradecat')) { + $mform->removeElement('gradecat'); + if ($this->_features->rating) { + //if supports ratings then the max grade dropdown wasnt added so the grade box can be removed entirely + $mform->removeElement('modstandardgrade'); + } + } + } + } + } + + if ($COURSE->groupmodeforce) { + if ($mform->elementExists('groupmode')) { + $mform->hardFreeze('groupmode'); // groupmode can not be changed if forced from course settings + } + } + + // Don't disable/remove groupingid if it is currently set to something, + // otherwise you cannot turn it off at same time as turning off other + // option (MDL-30764) + if (empty($this->_cm) || !$this->_cm->groupingid) { + if ($mform->elementExists('groupmode') and !$mform->elementExists('groupmembersonly') and empty($COURSE->groupmodeforce)) { + $mform->disabledIf('groupingid', 'groupmode', 'eq', NOGROUPS); + + } else if (!$mform->elementExists('groupmode') and $mform->elementExists('groupmembersonly')) { + $mform->disabledIf('groupingid', 'groupmembersonly', 'notchecked'); + + } else if (!$mform->elementExists('groupmode') and !$mform->elementExists('groupmembersonly')) { + // groupings have no use without groupmode or groupmembersonly + if ($mform->elementExists('groupingid')) { + $mform->removeElement('groupingid'); + } + } + } + + // Completion: If necessary, freeze fields + $completion = new completion_info($COURSE); + if ($completion->is_enabled()) { + // If anybody has completed the activity, these options will be 'locked' + $completedcount = empty($this->_cm) + ? 0 + : $completion->count_user_data($this->_cm); + + $freeze = false; + if (!$completedcount) { + if ($mform->elementExists('unlockcompletion')) { + $mform->removeElement('unlockcompletion'); + } + // Automatically set to unlocked (note: this is necessary + // in order to make it recalculate completion once the option + // is changed, maybe someone has completed it now) + $mform->getElement('completionunlocked')->setValue(1); + } else { + // Has the element been unlocked? + if ($mform->exportValue('unlockcompletion')) { + // Yes, add in warning text and set the hidden variable + $mform->insertElementBefore( + $mform->createElement('static', 'completedunlocked', + get_string('completedunlocked', 'completion'), + get_string('completedunlockedtext', 'completion')), + 'unlockcompletion'); + $mform->removeElement('unlockcompletion'); + $mform->getElement('completionunlocked')->setValue(1); + } else { + // No, add in the warning text with the count (now we know + // it) before the unlock button + $mform->insertElementBefore( + $mform->createElement('static', 'completedwarning', + get_string('completedwarning', 'completion'), + get_string('completedwarningtext', 'completion', $completedcount)), + 'unlockcompletion'); + $freeze = true; + } + } + + if ($freeze) { + $mform->freeze('completion'); + if ($mform->elementExists('completionview')) { + $mform->freeze('completionview'); // don't use hardFreeze or checkbox value gets lost + } + if ($mform->elementExists('completionusegrade')) { + $mform->freeze('completionusegrade'); + } + $mform->freeze($this->_customcompletionelements); + } + } + + // Availability conditions + if (!empty($CFG->enableavailability) && $this->_cm) { + $ci = new condition_info($this->_cm); + $fullcm=$ci->get_full_course_module(); + + $num=0; + foreach($fullcm->conditionsgrade as $gradeitemid=>$minmax) { + $groupelements=$mform->getElement('conditiongradegroup['.$num.']')->getElements(); + $groupelements[0]->setValue($gradeitemid); + $groupelements[2]->setValue(is_null($minmax->min) ? '' : + format_float($minmax->min, 5, true, true)); + $groupelements[4]->setValue(is_null($minmax->max) ? '' : + format_float($minmax->max, 5, true, true)); + $num++; + } + + $num = 0; + foreach($fullcm->conditionsfield as $field => $details) { + $groupelements = $mform->getElement('conditionfieldgroup['.$num.']')->getElements(); + $groupelements[0]->setValue($field); + $groupelements[1]->setValue(is_null($details->operator) ? '' : $details->operator); + $groupelements[2]->setValue(is_null($details->value) ? '' : $details->value); + $num++; + } + + if ($completion->is_enabled()) { + $num=0; + foreach($fullcm->conditionscompletion as $othercmid=>$state) { + $groupelements=$mform->getElement('conditioncompletiongroup['.$num.']')->getElements(); + $groupelements[0]->setValue($othercmid); + $groupelements[1]->setValue($state); + $num++; + } + } + } + } + + // form verification + function validation($data, $files) { + global $COURSE, $DB; + $errors = parent::validation($data, $files); + + $mform =& $this->_form; + + $errors = array(); + + if ($mform->elementExists('name')) { + $name = trim($data['name']); + if ($name == '') { + $errors['name'] = get_string('required'); + } + } + + $grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$data['modulename'], + 'iteminstance'=>$data['instance'], 'itemnumber'=>0, 'courseid'=>$COURSE->id)); + if ($data['coursemodule']) { + $cm = $DB->get_record('course_modules', array('id'=>$data['coursemodule'])); + } else { + $cm = null; + } + + if ($mform->elementExists('cmidnumber')) { + // verify the idnumber + if (!grade_verify_idnumber($data['cmidnumber'], $COURSE->id, $grade_item, $cm)) { + $errors['cmidnumber'] = get_string('idnumbertaken'); + } + } + + // Completion: Don't let them choose automatic completion without turning + // on some conditions + if (array_key_exists('completion', $data) && $data['completion']==COMPLETION_TRACKING_AUTOMATIC) { + if (empty($data['completionview']) && empty($data['completionusegrade']) && + !$this->completion_rule_enabled($data)) { + $errors['completion'] = get_string('badautocompletion', 'completion'); + } + } + + // Conditions: Don't let them set dates which make no sense + if (array_key_exists('availablefrom', $data) && + $data['availablefrom'] && $data['availableuntil'] && + $data['availablefrom'] >= $data['availableuntil']) { + $errors['availablefrom'] = get_string('badavailabledates', 'condition'); + } + + // Conditions: Verify that the grade conditions are numbers, and make sense. + if (array_key_exists('conditiongradegroup', $data)) { + foreach ($data['conditiongradegroup'] as $i => $gradedata) { + if ($gradedata['conditiongrademin'] !== '' && + !is_numeric(unformat_float($gradedata['conditiongrademin']))) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition'); + continue; + } + if ($gradedata['conditiongrademax'] !== '' && + !is_numeric(unformat_float($gradedata['conditiongrademax']))) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition'); + continue; + } + if ($gradedata['conditiongrademin'] !== '' && $gradedata['conditiongrademax'] !== '' && + unformat_float($gradedata['conditiongrademax']) <= unformat_float($gradedata['conditiongrademin'])) { + $errors["conditiongradegroup[{$i}]"] = get_string('badgradelimits', 'condition'); + continue; + } + if ($gradedata['conditiongrademin'] === '' && $gradedata['conditiongrademax'] === '' && + $gradedata['conditiongradeitemid']) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradeitembutnolimits', 'condition'); + continue; + } + if (($gradedata['conditiongrademin'] !== '' || $gradedata['conditiongrademax'] !== '') && + !$gradedata['conditiongradeitemid']) { + $errors["conditiongradegroup[{$i}]"] = get_string('gradelimitsbutnoitem', 'condition'); + continue; + } + } + } + + // Conditions: Verify that the user profile field has not been declared more than once + if (array_key_exists('conditionfieldgroup', $data)) { + // Array to store the existing fields + $arrcurrentfields = array(); + // Error message displayed if any condition is declared more than once. We use lang string because + // this way we don't actually generate the string unless there is an error. + $stralreadydeclaredwarning = new lang_string('fielddeclaredmultipletimes', 'condition'); + foreach ($data['conditionfieldgroup'] as $i => $fielddata) { + if ($fielddata['conditionfield'] == 0) { // Don't need to bother if none is selected + continue; + } + if (in_array($fielddata['conditionfield'], $arrcurrentfields)) { + $errors["conditionfieldgroup[{$i}]"] = $stralreadydeclaredwarning->out(); + } + // Add the field to the array + $arrcurrentfields[] = $fielddata['conditionfield']; + } + } + + return $errors; + } + + /** + * Load in existing data as form defaults. Usually new entry defaults are stored directly in + * form definition (new entry form); this function is used to load in data where values + * already exist and data is being edited (edit entry form). + * + * @param mixed $default_values object or array of default values + */ + function set_data($default_values) { + if (is_object($default_values)) { + $default_values = (array)$default_values; + } + + $this->data_preprocessing($default_values); + parent::set_data($default_values); + } + + /** + * Adds all the standard elements to a form to edit the settings for an activity module. + */ + function standard_coursemodule_elements(){ + global $COURSE, $CFG, $DB; + $mform =& $this->_form; + + $this->_outcomesused = false; + if ($this->_features->outcomes) { + if ($outcomes = grade_outcome::fetch_all_available($COURSE->id)) { + $this->_outcomesused = true; + $mform->addElement('header', 'modoutcomes', get_string('outcomes', 'grades')); + foreach($outcomes as $outcome) { + $mform->addElement('advcheckbox', 'outcome_'.$outcome->id, $outcome->get_name()); + } + } + } + + + if ($this->_features->rating) { + require_once($CFG->dirroot.'/rating/lib.php'); + $rm = new rating_manager(); + + $mform->addElement('header', 'modstandardratings', get_string('ratings', 'rating')); + + $permission=CAP_ALLOW; + $rolenamestring = null; + if (!empty($this->_cm)) { + $context = context_module::instance($this->_cm->id); + + $rolenames = get_role_names_with_caps_in_context($context, array('moodle/rating:rate', 'mod/'.$this->_cm->modname.':rate')); + $rolenamestring = implode(', ', $rolenames); + } else { + $rolenamestring = get_string('capabilitychecknotavailable','rating'); + } + $mform->addElement('static', 'rolewarning', get_string('rolewarning','rating'), $rolenamestring); + $mform->addHelpButton('rolewarning', 'rolewarning', 'rating'); + + $mform->addElement('select', 'assessed', get_string('aggregatetype', 'rating') , $rm->get_aggregate_types()); + $mform->setDefault('assessed', 0); + $mform->addHelpButton('assessed', 'aggregatetype', 'rating'); + + $mform->addElement('modgrade', 'scale', get_string('scale'), false); + $mform->disabledIf('scale', 'assessed', 'eq', 0); + + $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating')); + $mform->disabledIf('ratingtime', 'assessed', 'eq', 0); + + $mform->addElement('date_time_selector', 'assesstimestart', get_string('from')); + $mform->disabledIf('assesstimestart', 'assessed', 'eq', 0); + $mform->disabledIf('assesstimestart', 'ratingtime'); + + $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to')); + $mform->disabledIf('assesstimefinish', 'assessed', 'eq', 0); + $mform->disabledIf('assesstimefinish', 'ratingtime'); + } + + //doing this here means splitting up the grade related settings on the lesson settings page + //$this->standard_grading_coursemodule_elements(); + + $mform->addElement('header', 'modstandardelshdr', get_string('modstandardels', 'form')); + if ($this->_features->groups) { + $options = array(NOGROUPS => get_string('groupsnone'), + SEPARATEGROUPS => get_string('groupsseparate'), + VISIBLEGROUPS => get_string('groupsvisible')); + $mform->addElement('select', 'groupmode', get_string('groupmode', 'group'), $options, NOGROUPS); + $mform->addHelpButton('groupmode', 'groupmode', 'group'); + } + + if ($this->_features->groupings or $this->_features->groupmembersonly) { + //groupings selector - used for normal grouping mode or also when restricting access with groupmembersonly + $options = array(); + $options[0] = get_string('none'); + if ($groupings = $DB->get_records('groupings', array('courseid'=>$COURSE->id))) { + foreach ($groupings as $grouping) { + $options[$grouping->id] = format_string($grouping->name); + } + } + $mform->addElement('select', 'groupingid', get_string('grouping', 'group'), $options); + $mform->addHelpButton('groupingid', 'grouping', 'group'); + $mform->setAdvanced('groupingid'); + } + + if ($this->_features->groupmembersonly) { + $mform->addElement('checkbox', 'groupmembersonly', get_string('groupmembersonly', 'group')); + $mform->addHelpButton('groupmembersonly', 'groupmembersonly', 'group'); + $mform->setAdvanced('groupmembersonly'); + } + + $mform->addElement('modvisible', 'visible', get_string('visible')); + if (!empty($this->_cm)) { + $context = context_module::instance($this->_cm->id); + if (!has_capability('moodle/course:activityvisibility', $context)) { + $mform->hardFreeze('visible'); + } + } + + if ($this->_features->idnumber) { + $mform->addElement('text', 'cmidnumber', get_string('idnumbermod')); + $mform->addHelpButton('cmidnumber', 'idnumbermod'); + } + + if (!empty($CFG->enableavailability)) { + // String used by conditions + $strnone = get_string('none','condition'); + // Conditional availability + + // Available from/to defaults to midnight because then the display + // will be nicer where it tells users when they can access it (it + // shows only the date and not time). + $date = usergetdate(time()); + $midnight = make_timestamp($date['year'], $date['mon'], $date['mday']); + + // From/until controls + $mform->addElement('header', 'availabilityconditionsheader', + get_string('availabilityconditions', 'condition')); + $mform->addElement('date_time_selector', 'availablefrom', + get_string('availablefrom', 'condition'), + array('optional' => true, 'defaulttime' => $midnight)); + $mform->addHelpButton('availablefrom', 'availablefrom', 'condition'); + $mform->addElement('date_time_selector', 'availableuntil', + get_string('availableuntil', 'condition'), + array('optional' => true, 'defaulttime' => $midnight)); + + // Conditions based on grades + $gradeoptions = array(); + $items = grade_item::fetch_all(array('courseid'=>$COURSE->id)); + $items = $items ? $items : array(); + foreach($items as $id=>$item) { + // Do not include grades for current item + if (!empty($this->_cm) && $this->_cm->instance == $item->iteminstance + && $this->_cm->modname == $item->itemmodule + && $item->itemtype == 'mod') { + continue; + } + $gradeoptions[$id] = $item->get_name(); + } + asort($gradeoptions); + $gradeoptions = array(0 => $strnone) + $gradeoptions; + + $grouparray = array(); + $grouparray[] =& $mform->createElement('select','conditiongradeitemid','',$gradeoptions); + $grouparray[] =& $mform->createElement('static', '', '',' '.get_string('grade_atleast','condition').' '); + $grouparray[] =& $mform->createElement('text', 'conditiongrademin','',array('size'=>3)); + $grouparray[] =& $mform->createElement('static', '', '','% '.get_string('grade_upto','condition').' '); + $grouparray[] =& $mform->createElement('text', 'conditiongrademax','',array('size'=>3)); + $grouparray[] =& $mform->createElement('static', '', '','%'); + $group = $mform->createElement('group','conditiongradegroup', + get_string('gradecondition', 'condition'),$grouparray); + + // Get version with condition info and store it so we don't ask + // twice + if(!empty($this->_cm)) { + $ci = new condition_info($this->_cm, CONDITION_MISSING_EXTRATABLE); + $this->_cm = $ci->get_full_course_module(); + $count = count($this->_cm->conditionsgrade)+1; + $fieldcount = count($this->_cm->conditionsfield) + 1; + } else { + $count = 1; + $fieldcount = 1; + } + + $this->repeat_elements(array($group), $count, array(), 'conditiongraderepeats', 'conditiongradeadds', 2, + get_string('addgrades', 'condition'), true); + $mform->addHelpButton('conditiongradegroup[0]', 'gradecondition', 'condition'); + + // Conditions based on user fields + $operators = condition_info::get_condition_user_field_operators(); + $useroptions = condition_info::get_condition_user_fields(); + asort($useroptions); + + $useroptions = array(0 => $strnone) + $useroptions; + $grouparray = array(); + $grouparray[] =& $mform->createElement('select', 'conditionfield', '', $useroptions); + $grouparray[] =& $mform->createElement('select', 'conditionfieldoperator', '', $operators); + $grouparray[] =& $mform->createElement('text', 'conditionfieldvalue'); + $mform->setType('conditionfieldvalue', PARAM_RAW); + $group = $mform->createElement('group', 'conditionfieldgroup', get_string('userfield', 'condition'), $grouparray); + + $this->repeat_elements(array($group), $fieldcount, array(), 'conditionfieldrepeats', 'conditionfieldadds', 2, + get_string('adduserfields', 'condition'), true); + $mform->addHelpButton('conditionfieldgroup[0]', 'userfield', 'condition'); + + // Conditions based on completion + $completion = new completion_info($COURSE); + if ($completion->is_enabled()) { + $completionoptions = array(); + $modinfo = get_fast_modinfo($COURSE); + foreach($modinfo->cms as $id=>$cm) { + // Add each course-module if it: + // (a) has completion turned on + // (b) is not the same as current course-module + if ($cm->completion && (empty($this->_cm) || $this->_cm->id != $id)) { + $completionoptions[$id]=$cm->name; + } + } + asort($completionoptions); + $completionoptions = array(0 => $strnone) + $completionoptions; + + $completionvalues=array( + COMPLETION_COMPLETE=>get_string('completion_complete','condition'), + COMPLETION_INCOMPLETE=>get_string('completion_incomplete','condition'), + COMPLETION_COMPLETE_PASS=>get_string('completion_pass','condition'), + COMPLETION_COMPLETE_FAIL=>get_string('completion_fail','condition')); + + $grouparray = array(); + $grouparray[] =& $mform->createElement('select','conditionsourcecmid','',$completionoptions); + $grouparray[] =& $mform->createElement('select','conditionrequiredcompletion','',$completionvalues); + $group = $mform->createElement('group','conditioncompletiongroup', + get_string('completioncondition', 'condition'),$grouparray); + + $count = empty($this->_cm) ? 1 : count($this->_cm->conditionscompletion)+1; + $this->repeat_elements(array($group),$count,array(), + 'conditioncompletionrepeats','conditioncompletionadds',2, + get_string('addcompletions','condition'),true); + $mform->addHelpButton('conditioncompletiongroup[0]', 'completioncondition', 'condition'); + } + + // Do we display availability info to students? + $mform->addElement('select', 'showavailability', get_string('showavailability', 'condition'), + array(CONDITION_STUDENTVIEW_SHOW=>get_string('showavailability_show', 'condition'), + CONDITION_STUDENTVIEW_HIDE=>get_string('showavailability_hide', 'condition'))); + $mform->setDefault('showavailability', CONDITION_STUDENTVIEW_SHOW); + } + + // Conditional activities: completion tracking section + if(!isset($completion)) { + $completion = new completion_info($COURSE); + } + if ($completion->is_enabled()) { + $mform->addElement('header', 'activitycompletionheader', get_string('activitycompletion', 'completion')); + + // Unlock button for if people have completed it (will + // be removed in definition_after_data if they haven't) + $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion')); + $mform->registerNoSubmitButton('unlockcompletion'); + $mform->addElement('hidden', 'completionunlocked', 0); + $mform->setType('completionunlocked', PARAM_INT); + + $mform->addElement('select', 'completion', get_string('completion', 'completion'), + array(COMPLETION_TRACKING_NONE=>get_string('completion_none', 'completion'), + COMPLETION_TRACKING_MANUAL=>get_string('completion_manual', 'completion'))); + $mform->setDefault('completion', $this->_features->defaultcompletion + ? COMPLETION_TRACKING_MANUAL + : COMPLETION_TRACKING_NONE); + $mform->addHelpButton('completion', 'completion', 'completion'); + + // Automatic completion once you view it + $gotcompletionoptions = false; + if (plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) { + $mform->addElement('checkbox', 'completionview', get_string('completionview', 'completion'), + get_string('completionview_desc', 'completion')); + $mform->disabledIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + $gotcompletionoptions = true; + } + + // Automatic completion once it's graded + if (plugin_supports('mod', $this->_modname, FEATURE_GRADE_HAS_GRADE, false)) { + $mform->addElement('checkbox', 'completionusegrade', get_string('completionusegrade', 'completion'), + get_string('completionusegrade_desc', 'completion')); + $mform->disabledIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion'); + $gotcompletionoptions = true; + } + + // Automatic completion according to module-specific rules + $this->_customcompletionelements = $this->add_completion_rules(); + foreach ($this->_customcompletionelements as $element) { + $mform->disabledIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + } + + $gotcompletionoptions = $gotcompletionoptions || + count($this->_customcompletionelements)>0; + + // Automatic option only appears if possible + if ($gotcompletionoptions) { + $mform->getElement('completion')->addOption( + get_string('completion_automatic', 'completion'), + COMPLETION_TRACKING_AUTOMATIC); + } + + // Completion expected at particular date? (For progress tracking) + $mform->addElement('date_selector', 'completionexpected', get_string('completionexpected', 'completion'), array('optional'=>true)); + $mform->addHelpButton('completionexpected', 'completionexpected', 'completion'); + $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE); + } + + $this->standard_hidden_coursemodule_elements(); + } + + /** + * Can be overridden to add custom completion rules if the module wishes + * them. If overriding this, you should also override completion_rule_enabled. + *

          + * Just add elements to the form as needed and return the list of IDs. The + * system will call disabledIf and handle other behaviour for each returned + * ID. + * @return array Array of string IDs of added items, empty array if none + */ + function add_completion_rules() { + return array(); + } + + /** + * Called during validation. Override to indicate, based on the data, whether + * a custom completion rule is enabled (selected). + * + * @param array $data Input data (not yet validated) + * @return bool True if one or more rules is enabled, false if none are; + * default returns false + */ + function completion_rule_enabled($data) { + return false; + } + + function standard_hidden_coursemodule_elements(){ + $mform =& $this->_form; + $mform->addElement('hidden', 'course', 0); + $mform->setType('course', PARAM_INT); + + $mform->addElement('hidden', 'coursemodule', 0); + $mform->setType('coursemodule', PARAM_INT); + + $mform->addElement('hidden', 'section', 0); + $mform->setType('section', PARAM_INT); + + $mform->addElement('hidden', 'module', 0); + $mform->setType('module', PARAM_INT); + + $mform->addElement('hidden', 'modulename', ''); + $mform->setType('modulename', PARAM_PLUGIN); + + $mform->addElement('hidden', 'instance', 0); + $mform->setType('instance', PARAM_INT); + + $mform->addElement('hidden', 'add', 0); + $mform->setType('add', PARAM_ALPHA); + + $mform->addElement('hidden', 'update', 0); + $mform->setType('update', PARAM_INT); + + $mform->addElement('hidden', 'return', 0); + $mform->setType('return', PARAM_BOOL); + + $mform->addElement('hidden', 'sr', 0); + $mform->setType('sr', PARAM_INT); + } + + public function standard_grading_coursemodule_elements() { + global $COURSE, $CFG; + $mform =& $this->_form; + + if ($this->_features->hasgrades) { + + if (!$this->_features->rating || $this->_features->gradecat) { + $mform->addElement('header', 'modstandardgrade', get_string('grade')); + } + + //if supports grades and grades arent being handled via ratings + if (!$this->_features->rating) { + $mform->addElement('modgrade', 'grade', get_string('grade')); + $mform->setDefault('grade', 100); + } + + if ($this->_features->advancedgrading + and !empty($this->current->_advancedgradingdata['methods']) + and !empty($this->current->_advancedgradingdata['areas'])) { + + if (count($this->current->_advancedgradingdata['areas']) == 1) { + // if there is just one gradable area (most cases), display just the selector + // without its name to make UI simplier + $areadata = reset($this->current->_advancedgradingdata['areas']); + $areaname = key($this->current->_advancedgradingdata['areas']); + $mform->addElement('select', 'advancedgradingmethod_'.$areaname, + get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']); + $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading'); + + } else { + // the module defines multiple gradable areas, display a selector + // for each of them together with a name of the area + $areasgroup = array(); + foreach ($this->current->_advancedgradingdata['areas'] as $areaname => $areadata) { + $areasgroup[] = $mform->createElement('select', 'advancedgradingmethod_'.$areaname, + $areadata['title'], $this->current->_advancedgradingdata['methods']); + $areasgroup[] = $mform->createElement('static', 'advancedgradingareaname_'.$areaname, '', $areadata['title']); + } + $mform->addGroup($areasgroup, 'advancedgradingmethodsgroup', get_string('gradingmethods', 'core_grading'), + array(' ', '
          '), false); + } + } + + if ($this->_features->gradecat) { + $mform->addElement('select', 'gradecat', + get_string('gradecategoryonmodform', 'grades'), + grade_get_categories_menu($COURSE->id, $this->_outcomesused)); + $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades'); + } + } + } + + function add_intro_editor($required=false, $customlabel=null) { + if (!$this->_features->introeditor) { + // intro editor not supported in this module + return; + } + + $mform = $this->_form; + $label = is_null($customlabel) ? get_string('moduleintro') : $customlabel; + + $mform->addElement('editor', 'introeditor', $label, null, array('maxfiles'=>EDITOR_UNLIMITED_FILES, 'noclean'=>true, 'context'=>$this->context)); + $mform->setType('introeditor', PARAM_RAW); // no XSS prevention here, users must be trusted + if ($required) { + $mform->addRule('introeditor', get_string('required'), 'required', null, 'client'); + } + + // If the 'show description' feature is enabled, this checkbox appears + // below the intro. + if ($this->_features->showdescription) { + $mform->addElement('checkbox', 'showdescription', get_string('showdescription')); + $mform->addHelpButton('showdescription', 'showdescription'); + } + } + + /** + * Overriding formslib's add_action_buttons() method, to add an extra submit "save changes and return" button. + * + * @param bool $cancel show cancel button + * @param string $submitlabel null means default, false means none, string is label text + * @param string $submit2label null means default, false means none, string is label text + * @return void + */ + function add_action_buttons($cancel=true, $submitlabel=null, $submit2label=null) { + if (is_null($submitlabel)) { + $submitlabel = get_string('savechangesanddisplay'); + } + + if (is_null($submit2label)) { + $submit2label = get_string('savechangesandreturntocourse'); + } + + $mform = $this->_form; + + // elements in a row need a group + $buttonarray = array(); + + if ($submit2label !== false) { + $buttonarray[] = &$mform->createElement('submit', 'submitbutton2', $submit2label); + } + + if ($submitlabel !== false) { + $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel); + } + + if ($cancel) { + $buttonarray[] = &$mform->createElement('cancel'); + } + + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->setType('buttonar', PARAM_RAW); + $mform->closeHeaderBefore('buttonar'); + } +} + + diff --git a/pending.php b/pending.php new file mode 100644 index 0000000..3d6cb18 --- /dev/null +++ b/pending.php @@ -0,0 +1,149 @@ +libdir . '/adminlib.php'); +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/course/request_form.php'); + +require_login(); +require_capability('moodle/site:approvecourse', context_system::instance()); + +$approve = optional_param('approve', 0, PARAM_INT); +$reject = optional_param('reject', 0, PARAM_INT); + +$baseurl = $CFG->wwwroot . '/course/pending.php'; +admin_externalpage_setup('coursespending'); + +/// Process approval of a course. +if (!empty($approve) and confirm_sesskey()) { + /// Load the request. + $course = new course_request($approve); + $courseid = $course->approve(); + + if ($courseid !== false) { + redirect($CFG->wwwroot.'/course/edit.php?id=' . $courseid); + } else { + print_error('courseapprovedfailed'); + } +} + +/// Process rejection of a course. +if (!empty($reject)) { + // Load the request. + $course = new course_request($reject); + + // Prepare the form. + $rejectform = new reject_request_form($baseurl); + $default = new stdClass(); + $default->reject = $course->id; + $rejectform->set_data($default); + +/// Standard form processing if statement. + if ($rejectform->is_cancelled()){ + redirect($baseurl); + + } else if ($data = $rejectform->get_data()) { + + /// Reject the request + $course->reject($data->rejectnotice); + + /// Redirect back to the course listing. + redirect($baseurl, get_string('courserejected')); + } + +/// Display the form for giving a reason for rejecting the request. + echo $OUTPUT->header($rejectform->focus()); + $rejectform->display(); + echo $OUTPUT->footer(); + exit; +} + +/// Print a list of all the pending requests. +echo $OUTPUT->header(); + +$pending = $DB->get_records('course_request'); +if (empty($pending)) { + echo $OUTPUT->heading(get_string('nopendingcourses')); +} else { + echo $OUTPUT->heading(get_string('coursespending')); + +/// Build a table of all the requests. + $table = new html_table(); + $table->attributes['class'] = 'pendingcourserequests generaltable'; + $table->align = array('center', 'center', 'center', 'center', 'center', 'center'); + $table->head = array(get_string('shortnamecourse'), get_string('fullnamecourse'), get_string('requestedby'), + get_string('summary'), get_string('category'), get_string('requestreason'), get_string('action')); + + foreach ($pending as $course) { + $course = new course_request($course); + + // Check here for shortname collisions and warn about them. + $course->check_shortname_collision(); + + // Retreiving category name. + // If the category was not set (can happen after upgrade) or if the user does not have the capability + // to change the category, we fallback on the default one. + // Else, the category proposed is fetched, but we fallback on the default one if we can't find it. + // It is just a matter of displaying the right information because the logic when approving the category + // proceeds the same way. The system context level is used as moodle/site:approvecourse uses it. + if (empty($course->category) || !has_capability('moodle/course:changecategory', context_system::instance()) || + (!$category = get_course_category($course->category))) { + $category = get_course_category($CFG->defaultrequestcategory); + } + + $row = array(); + $row[] = format_string($course->shortname); + $row[] = format_string($course->fullname); + $row[] = fullname($course->get_requester()); + $row[] = $course->summary; + $row[] = format_string($category->name); + $row[] = format_string($course->reason); + $row[] = $OUTPUT->single_button(new moodle_url($baseurl, array('approve' => $course->id, 'sesskey' => sesskey())), get_string('approve'), 'get') . + $OUTPUT->single_button(new moodle_url($baseurl, array('reject' => $course->id)), get_string('rejectdots'), 'get'); + + /// Add the row to the table. + $table->data[] = $row; + } + +/// Display the table. + echo html_writer::table($table); + +/// Message about name collisions, if necessary. + if (!empty($collision)) { + print_string('shortnamecollisionwarning'); + } +} + +/// Finish off the page. +echo $OUTPUT->single_button($CFG->wwwroot . '/course/index.php', get_string('backtocourselisting')); +echo $OUTPUT->footer(); diff --git a/publish/backup.php b/publish/backup.php new file mode 100644 index 0000000..15e091a --- /dev/null +++ b/publish/backup.php @@ -0,0 +1,124 @@ +. // +// // +/////////////////////////////////////////////////////////////////////////// + +/* + * @package course + * @subpackage publish + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * This page display the publication backup form + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php'); +require_once($CFG->dirroot . '/course/publish/lib.php'); +require_once($CFG->libdir . '/filelib.php'); + + +//retrieve initial page parameters +$id = required_param('id', PARAM_INT); +$hubcourseid = required_param('hubcourseid', PARAM_INT); +$huburl = required_param('huburl', PARAM_URL); +$hubname = optional_param('hubname', '', PARAM_TEXT); + +//some permissions and parameters checking +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +require_login($course); +if (!has_capability('moodle/course:publish', context_course::instance($id)) + or !confirm_sesskey()) { + throw new moodle_exception('nopermission'); +} + +//page settings +$PAGE->set_url('/course/publish/backup.php'); +$PAGE->set_pagelayout('course'); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_heading($course->fullname); + +//BEGIN backup processing +$backupid = optional_param('backup', false, PARAM_ALPHANUM); +if (!($bc = backup_ui::load_controller($backupid))) { + $bc = new backup_controller(backup::TYPE_1COURSE, $id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_YES, backup::MODE_HUB, $USER->id); +} +$backup = new backup_ui($bc, + array('id' => $id, 'hubcourseid' => $hubcourseid, 'huburl' => $huburl, 'hubname' => $hubname)); +$backup->process(); +if ($backup->get_stage() == backup_ui::STAGE_FINAL) { + $backup->execute(); +} else { + $backup->save_controller(); +} + +if ($backup->get_stage() !== backup_ui::STAGE_COMPLETE) { + $renderer = $PAGE->get_renderer('core', 'backup'); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishcourseon', 'hub', !empty($hubname)?$hubname:$huburl), 3, 'main'); + if ($backup->enforce_changed_dependencies()) { + debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER); + } + echo $renderer->progress_bar($backup->get_progress_bar()); + echo $backup->display($renderer); + echo $OUTPUT->footer(); + die(); +} + +//$backupfile = $backup->get_stage_results(); +$backupfile = $bc->get_results(); +$backupfile = $backupfile['backup_destination']; +//END backup processing + +//retrieve the token to call the hub +$registrationmanager = new registration_manager(); +$registeredhub = $registrationmanager->get_registeredhub($huburl); + +//display the sending file page +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('sendingcourse', 'hub'), 3, 'main'); +$renderer = $PAGE->get_renderer('core', 'publish'); +echo $renderer->sendingbackupinfo($backupfile); +if (ob_get_level()) { + ob_flush(); +} +flush(); + +//send backup file to the hub +$curl = new curl(); +$params = array(); +$params['filetype'] = HUB_BACKUP_FILE_TYPE; +$params['courseid'] = $hubcourseid; +$params['file'] = $backupfile; +$params['token'] = $registeredhub->token; +$curl->post($huburl . "/local/hub/webservice/upload.php", $params); + +//delete the temp backup file from user_tohub aera +$backupfile->delete(); +$bc->destroy(); + +//Output sending success +echo $renderer->sentbackupinfo($id, $huburl, $hubname); + +echo $OUTPUT->footer(); diff --git a/publish/forms.php b/publish/forms.php new file mode 100644 index 0000000..3b5e8df --- /dev/null +++ b/publish/forms.php @@ -0,0 +1,392 @@ +. // +// // +/////////////////////////////////////////////////////////////////////////// + +/* + * @package course + * @subpackage publish + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * The forms used for course publication + */ + + +require_once($CFG->libdir . '/formslib.php'); +require_once($CFG->dirroot . "/" . $CFG->admin . "/registration/lib.php"); +require_once($CFG->dirroot . "/course/publish/lib.php"); + +/* + * Hub selector to choose on which hub we want to publish. + */ + +class hub_publish_selector_form extends moodleform { + + public function definition() { + global $CFG; + $mform = & $this->_form; + $share = $this->_customdata['share']; + + $mform->addElement('header', 'site', get_string('selecthub', 'hub')); + + $mform->addElement('static', 'info', '', get_string('selecthubinfo', 'hub') . html_writer::empty_tag('br')); + + $registrationmanager = new registration_manager(); + $registeredhubs = $registrationmanager->get_registered_on_hubs(); + + //Public hub list + $options = array(); + foreach ($registeredhubs as $hub) { + + $hubname = $hub->hubname; + $mform->addElement('hidden', clean_param($hub->huburl, PARAM_ALPHANUMEXT), $hubname); + if (empty($hubname)) { + $hubname = $hub->huburl; + } + $mform->addElement('radio', 'huburl', null, ' ' . $hubname, $hub->huburl); + if ($hub->huburl == HUB_MOODLEORGHUBURL) { + $mform->setDefault('huburl', $hub->huburl); + } + } + + $mform->addElement('hidden', 'id', $this->_customdata['id']); + + if ($share) { + $buttonlabel = get_string('shareonhub', 'hub'); + $mform->addElement('hidden', 'share', true); + } else { + $buttonlabel = get_string('advertiseonhub', 'hub'); + $mform->addElement('hidden', 'advertise', true); + } + + $this->add_action_buttons(false, $buttonlabel); + } + +} + +/* + * Course publication form + */ + +class course_publication_form extends moodleform { + + public function definition() { + global $CFG, $DB, $USER, $OUTPUT; + + $strrequired = get_string('required'); + $mform = & $this->_form; + $huburl = $this->_customdata['huburl']; + $hubname = $this->_customdata['hubname']; + $course = $this->_customdata['course']; + $advertise = $this->_customdata['advertise']; + $share = $this->_customdata['share']; + $page = $this->_customdata['page']; + $site = get_site(); + + //hidden parameters + $mform->addElement('hidden', 'huburl', $huburl); + $mform->addElement('hidden', 'hubname', $hubname); + + //check on the hub if the course has already been published + $registrationmanager = new registration_manager(); + $registeredhub = $registrationmanager->get_registeredhub($huburl); + $publicationmanager = new course_publish_manager(); + $publications = $publicationmanager->get_publications($registeredhub->huburl, $course->id, $advertise); + + if (!empty($publications)) { + //get the last publication of this course + $publication = array_pop($publications); + + $function = 'hub_get_courses'; + $options = new stdClass(); + $options->ids = array($publication->hubcourseid); + $options->allsitecourses = 1; + $params = array('search' => '', 'downloadable' => $share, + 'enrollable' => !$share, 'options' => $options); + $serverurl = $huburl . "/local/hub/webservice/webservices.php"; + require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php"); + $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $registeredhub->token); + try { + $result = $xmlrpcclient->call($function, $params); + $publishedcourses = $result['courses']; + } catch (Exception $e) { + $error = $OUTPUT->notification(get_string('errorcourseinfo', 'hub', $e->getMessage())); + $mform->addElement('static', 'errorhub', '', $error); + } + } + + if (!empty($publishedcourses)) { + $publishedcourse = $publishedcourses[0]; + $hubcourseid = $publishedcourse['id']; + $defaultfullname = $publishedcourse['fullname']; + $defaultshortname = $publishedcourse['shortname']; + $defaultsummary = $publishedcourse['description']; + $defaultlanguage = $publishedcourse['language']; + $defaultpublishername = $publishedcourse['publishername']; + $defaultpublisheremail = $publishedcourse['publisheremail']; + $defaultcontributornames = $publishedcourse['contributornames']; + $defaultcoverage = $publishedcourse['coverage']; + $defaultcreatorname = $publishedcourse['creatorname']; + $defaultlicenceshortname = $publishedcourse['licenceshortname']; + $defaultsubject = $publishedcourse['subject']; + $defaultaudience = $publishedcourse['audience']; + $defaulteducationallevel = $publishedcourse['educationallevel']; + $defaultcreatornotes = $publishedcourse['creatornotes']; + $defaultcreatornotesformat = $publishedcourse['creatornotesformat']; + $screenshotsnumber = $publishedcourse['screenshots']; + $privacy = $publishedcourse['privacy']; + if (($screenshotsnumber > 0) and !empty($privacy)) { + $page->requires->yui_module('moodle-block_community-imagegallery', + 'M.blocks_community.init_imagegallery', + array(array('imageids' => array($hubcourseid), + 'imagenumbers' => array($screenshotsnumber), + 'huburl' => $huburl))); + } + } else { + $defaultfullname = $course->fullname; + $defaultshortname = $course->shortname; + $defaultsummary = clean_param($course->summary, PARAM_TEXT); + if (empty($course->lang)) { + $language = get_site()->lang; + if (empty($language)) { + $defaultlanguage = current_language(); + } else { + $defaultlanguage = $language; + } + } else { + $defaultlanguage = $course->lang; + } + $defaultpublishername = $USER->firstname . ' ' . $USER->lastname; + $defaultpublisheremail = $USER->email; + $defaultcontributornames = ''; + $defaultcoverage = ''; + $defaultcreatorname = $USER->firstname . ' ' . $USER->lastname; + $defaultlicenceshortname = 'cc'; + $defaultsubject = 'none'; + $defaultaudience = HUB_AUDIENCE_STUDENTS; + $defaulteducationallevel = HUB_EDULEVEL_TERTIARY; + $defaultcreatornotes = ''; + $defaultcreatornotesformat = FORMAT_HTML; + $screenshotsnumber = 0; + } + + //the input parameters + $mform->addElement('header', 'moodle', get_string('publicationinfo', 'hub')); + + $mform->addElement('text', 'name', get_string('coursename', 'hub'), + array('class' => 'metadatatext')); + $mform->addRule('name', $strrequired, 'required', null, 'client'); + $mform->setType('name', PARAM_TEXT); + $mform->setDefault('name', $defaultfullname); + $mform->addHelpButton('name', 'name', 'hub'); + + $mform->addElement('hidden', 'id', $this->_customdata['id']); + + if ($share) { + $buttonlabel = get_string('shareon', 'hub', !empty($hubname) ? $hubname : $huburl); + + $mform->addElement('hidden', 'share', $share); + + $mform->addElement('text', 'demourl', get_string('demourl', 'hub'), + array('class' => 'metadatatext')); + $mform->setType('demourl', PARAM_URL); + $mform->setDefault('demourl', new moodle_url("/course/view.php?id=" . $course->id)); + $mform->addHelpButton('demourl', 'demourl', 'hub'); + } + + if ($advertise) { + if (empty($publishedcourses)) { + $buttonlabel = get_string('advertiseon', 'hub', !empty($hubname) ? $hubname : $huburl); + } else { + $buttonlabel = get_string('readvertiseon', 'hub', !empty($hubname) ? $hubname : $huburl); + } + $mform->addElement('hidden', 'advertise', $advertise); + $mform->addElement('hidden', 'courseurl', $CFG->wwwroot . "/course/view.php?id=" . $course->id); + $mform->addElement('static', 'courseurlstring', get_string('courseurl', 'hub')); + $mform->setDefault('courseurlstring', new moodle_url("/course/view.php?id=" . $course->id)); + $mform->addHelpButton('courseurlstring', 'courseurl', 'hub'); + } + + $mform->addElement('text', 'courseshortname', get_string('courseshortname', 'hub'), + array('class' => 'metadatatext')); + $mform->setDefault('courseshortname', $defaultshortname); + $mform->addHelpButton('courseshortname', 'courseshortname', 'hub'); + + $mform->addElement('textarea', 'description', get_string('description'), array('rows' => 10, + 'cols' => 57)); + $mform->addRule('description', $strrequired, 'required', null, 'client'); + $mform->setDefault('description', $defaultsummary); + $mform->setType('description', PARAM_TEXT); + $mform->addHelpButton('description', 'description', 'hub'); + + $languages = get_string_manager()->get_list_of_languages(); + collatorlib::asort($languages); + $mform->addElement('select', 'language', get_string('language'), $languages); + $mform->setDefault('language', $defaultlanguage); + $mform->addHelpButton('language', 'language', 'hub'); + + + $mform->addElement('text', 'publishername', get_string('publishername', 'hub'), + array('class' => 'metadatatext')); + $mform->setDefault('publishername', $defaultpublishername); + $mform->addRule('publishername', $strrequired, 'required', null, 'client'); + $mform->addHelpButton('publishername', 'publishername', 'hub'); + + $mform->addElement('text', 'publisheremail', get_string('publisheremail', 'hub'), + array('class' => 'metadatatext')); + $mform->setDefault('publisheremail', $defaultpublisheremail); + $mform->addRule('publisheremail', $strrequired, 'required', null, 'client'); + $mform->addHelpButton('publisheremail', 'publisheremail', 'hub'); + + $mform->addElement('text', 'creatorname', get_string('creatorname', 'hub'), + array('class' => 'metadatatext')); + $mform->addRule('creatorname', $strrequired, 'required', null, 'client'); + $mform->setType('creatorname', PARAM_TEXT); + $mform->setDefault('creatorname', $defaultcreatorname); + $mform->addHelpButton('creatorname', 'creatorname', 'hub'); + + $mform->addElement('text', 'contributornames', get_string('contributornames', 'hub'), + array('class' => 'metadatatext')); + $mform->setDefault('contributornames', $defaultcontributornames); + $mform->addHelpButton('contributornames', 'contributornames', 'hub'); + + $mform->addElement('text', 'coverage', get_string('tags', 'hub'), + array('class' => 'metadatatext')); + $mform->setType('coverage', PARAM_TEXT); + $mform->setDefault('coverage', $defaultcoverage); + $mform->addHelpButton('coverage', 'tags', 'hub'); + + + + require_once($CFG->libdir . "/licenselib.php"); + $licensemanager = new license_manager(); + $licences = $licensemanager->get_licenses(); + $options = array(); + foreach ($licences as $license) { + $options[$license->shortname] = get_string($license->shortname, 'license'); + } + $mform->addElement('select', 'licence', get_string('license'), $options); + $mform->setDefault('licence', $defaultlicenceshortname); + unset($options); + $mform->addHelpButton('licence', 'licence', 'hub'); + + $options = $publicationmanager->get_sorted_subjects(); + + //prepare data for the smartselect + foreach ($options as $key => &$option) { + $keylength = strlen($key); + if ($keylength == 10) { + $option = "  " . $option; + } else if ($keylength == 12) { + $option = "    " . $option; + } + } + + $options = array('none' => get_string('none', 'hub')) + $options; + $mform->addElement('select', 'subject', get_string('subject', 'hub'), $options); + unset($options); + $mform->addHelpButton('subject', 'subject', 'hub'); + $mform->setDefault('subject', $defaultsubject); + $mform->addRule('subject', $strrequired, 'required', null, 'client'); + $this->init_javascript_enhancement('subject', 'smartselect', array('selectablecategories' => false, 'mode' => 'compact')); + + $options = array(); + $options[HUB_AUDIENCE_EDUCATORS] = get_string('audienceeducators', 'hub'); + $options[HUB_AUDIENCE_STUDENTS] = get_string('audiencestudents', 'hub'); + $options[HUB_AUDIENCE_ADMINS] = get_string('audienceadmins', 'hub'); + $mform->addElement('select', 'audience', get_string('audience', 'hub'), $options); + $mform->setDefault('audience', $defaultaudience); + unset($options); + $mform->addHelpButton('audience', 'audience', 'hub'); + + $options = array(); + $options[HUB_EDULEVEL_PRIMARY] = get_string('edulevelprimary', 'hub'); + $options[HUB_EDULEVEL_SECONDARY] = get_string('edulevelsecondary', 'hub'); + $options[HUB_EDULEVEL_TERTIARY] = get_string('eduleveltertiary', 'hub'); + $options[HUB_EDULEVEL_GOVERNMENT] = get_string('edulevelgovernment', 'hub'); + $options[HUB_EDULEVEL_ASSOCIATION] = get_string('edulevelassociation', 'hub'); + $options[HUB_EDULEVEL_CORPORATE] = get_string('edulevelcorporate', 'hub'); + $options[HUB_EDULEVEL_OTHER] = get_string('edulevelother', 'hub'); + $mform->addElement('select', 'educationallevel', get_string('educationallevel', 'hub'), $options); + $mform->setDefault('educationallevel', $defaulteducationallevel); + unset($options); + $mform->addHelpButton('educationallevel', 'educationallevel', 'hub'); + + $editoroptions = array('maxfiles' => 0, 'maxbytes' => 0, 'trusttext' => false, 'forcehttps' => false); + $mform->addElement('editor', 'creatornotes', get_string('creatornotes', 'hub'), '', $editoroptions); + $mform->addRule('creatornotes', $strrequired, 'required', null, 'client'); + $mform->setType('creatornotes', PARAM_CLEANHTML); + $mform->addHelpButton('creatornotes', 'creatornotes', 'hub'); + + if ($advertise) { + if (!empty($screenshotsnumber)) { + + if (!empty($privacy)) { + $baseurl = new moodle_url($huburl . '/local/hub/webservice/download.php', + array('courseid' => $hubcourseid, 'filetype' => HUB_SCREENSHOT_FILE_TYPE)); + $screenshothtml = html_writer::empty_tag('img', + array('src' => $baseurl, 'alt' => $defaultfullname)); + $screenshothtml = html_writer::tag('div', $screenshothtml, + array('class' => 'coursescreenshot', + 'id' => 'image-' . $hubcourseid)); + } else { + $screenshothtml = get_string('existingscreenshotnumber', 'hub', $screenshotsnumber); + } + $mform->addElement('static', 'existingscreenshots', get_string('existingscreenshots', 'hub'), $screenshothtml); + $mform->addHelpButton('existingscreenshots', 'deletescreenshots', 'hub'); + $mform->addElement('checkbox', 'deletescreenshots', '', ' ' . get_string('deletescreenshots', 'hub')); + } + + $mform->addElement('hidden', 'existingscreenshotnumber', $screenshotsnumber); + } + + $mform->addElement('filemanager', 'screenshots', get_string('addscreenshots', 'hub'), null, + array('subdirs' => 0, + 'maxbytes' => 1000000, + 'maxfiles' => 3 + )); + $mform->addHelpButton('screenshots', 'screenshots', 'hub'); + + $this->add_action_buttons(false, $buttonlabel); + + //set default value for creatornotes editor + $data = new stdClass(); + $data->creatornotes = array(); + $data->creatornotes['text'] = $defaultcreatornotes; + $data->creatornotes['format'] = $defaultcreatornotesformat; + $this->set_data($data); + } + + function validation($data, $files) { + global $CFG; + + $errors = array(); + + if ($this->_form->_submitValues['subject'] == 'none') { + $errors['subject'] = get_string('mustselectsubject', 'hub'); + } + + return $errors; + } + +} + diff --git a/publish/hubselector.php b/publish/hubselector.php new file mode 100644 index 0000000..095e33b --- /dev/null +++ b/publish/hubselector.php @@ -0,0 +1,77 @@ +. + +/* + * @package course + * @subpackage publish + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * On this page the user selects where he wants to publish the course +*/ + +require('../../config.php'); + +require_once($CFG->dirroot.'/' . $CFG->admin . '/registration/lib.php'); +require_once($CFG->dirroot.'/course/publish/forms.php'); + +$id = required_param('id', PARAM_INT); +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +require_login($course); + +$PAGE->set_url('/course/publish/hubselector.php', array('id' => $course->id)); +$PAGE->set_pagelayout('course'); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_heading($course->fullname); + +$registrationmanager = new registration_manager(); +$registeredhubs = $registrationmanager->get_registered_on_hubs(); +if (empty($registeredhubs)) { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishon', 'hub'), 3, 'main'); + echo $OUTPUT->box(get_string('notregisteredonhub', 'hub')); + echo $OUTPUT->footer(); + die(); +} + + +$share = optional_param('share', false, PARAM_BOOL); +$advertise = optional_param('advertise', false, PARAM_BOOL); +$hubselectorform = new hub_publish_selector_form('', + array('id' => $id, 'share' => $share, 'advertise' => $advertise)); +$fromform = $hubselectorform->get_data(); + +//// Redirect to the registration form if an URL has been chosen //// +$huburl = optional_param('huburl', false, PARAM_URL); + +//redirect +if (!empty($huburl) and confirm_sesskey()) { + $hubname = optional_param(clean_param($huburl, PARAM_ALPHANUMEXT), '', PARAM_TEXT); + $params = array('sesskey' => sesskey(), 'id' => $id, + 'huburl' => $huburl, 'hubname' => $hubname, 'share' => $share, 'advertise' => $advertise); + redirect(new moodle_url($CFG->wwwroot."/course/publish/metadata.php", + $params)); +} + + +//// OUTPUT //// + + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('publishon', 'hub'), 3, 'main'); +$hubselectorform->display(); +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/publish/index.php b/publish/index.php new file mode 100644 index 0000000..d401e5c --- /dev/null +++ b/publish/index.php @@ -0,0 +1,180 @@ +. + +/* + * @package course + * @subpackage publish + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * The user selects if he wants to publish the course on Moodle.org hub or + * on a specific hub. The site must be registered on a hub to be able to + * publish a course on it. +*/ + +require('../../config.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php'); +require_once($CFG->dirroot . '/course/publish/lib.php'); + +$id = required_param('id', PARAM_INT); +$hubname = optional_param('hubname', 0, PARAM_TEXT); +$huburl = optional_param('huburl', 0, PARAM_URL); + +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); + +require_login($course); +$context = context_course::instance($course->id); +$shortname = format_string($course->shortname, true, array('context' => $context)); + +$PAGE->set_url('/course/publish/index.php', array('id' => $course->id)); +$PAGE->set_pagelayout('course'); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_heading($course->fullname); + +//check that the PHP xmlrpc extension is enabled +if (!extension_loaded('xmlrpc')) { + $notificationerror = $OUTPUT->doc_link('admin/environment/php_extension/xmlrpc', ''); + $notificationerror .= get_string('xmlrpcdisabledpublish', 'hub'); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishcourse', 'hub', $shortname), 3, 'main'); + echo $OUTPUT->notification($notificationerror); + echo $OUTPUT->footer(); + die(); +} + +if (has_capability('moodle/course:publish', context_course::instance($id))) { + + $publicationmanager = new course_publish_manager(); + $confirmmessage = ''; + + //update the courses status + $updatestatusid = optional_param('updatestatusid', false, PARAM_INT); + if (!empty($updatestatusid) and confirm_sesskey()) { + //get the communication token from the publication + $hub = $publicationmanager->get_registeredhub_by_publication($updatestatusid); + if (empty($hub)) { + $confirmmessage = $OUTPUT->notification(get_string('nocheckstatusfromunreghub', 'hub')); + } else { + //get all site courses registered on this hub + $function = 'hub_get_courses'; + $params = array('search' => '', 'downloadable' => 1, + 'enrollable' => 1, 'options' => array( 'allsitecourses' => 1)); + $serverurl = $hub->huburl."/local/hub/webservice/webservices.php"; + require_once($CFG->dirroot."/webservice/xmlrpc/lib.php"); + $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $hub->token); + $result = $xmlrpcclient->call($function, $params); + $sitecourses = $result['courses']; + + //update status for all these course + foreach ($sitecourses as $sitecourse) { + //get the publication from the hub course id + $publication = $publicationmanager->get_publication($sitecourse['id'], $hub->huburl); + if (!empty($publication)) { + $publication->status = $sitecourse['privacy']; + $publication->timechecked = time(); + $publicationmanager->update_publication($publication); + } else { + $msgparams = new stdClass(); + $msgparams->id = $sitecourse['id']; + $msgparams->hubname = html_writer::tag('a', $hub->hubname, array('href' => $hub->huburl)); + $confirmmessage .= $OUTPUT->notification( + get_string('detectednotexistingpublication', 'hub', $msgparams)); + } + } + } + } + + //if the site os registered on no hub display an error page + $registrationmanager = new registration_manager(); + $registeredhubs = $registrationmanager->get_registered_on_hubs(); + if (empty($registeredhubs)) { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishon', 'hub'), 3, 'main'); + echo $OUTPUT->box(get_string('notregisteredonhub', 'hub')); + echo $OUTPUT->footer(); + die(); + } + + $renderer = $PAGE->get_renderer('core', 'publish'); + + /// UNPUBLISH + $cancel = optional_param('cancel', 0, PARAM_BOOL); + if (!empty($cancel) and confirm_sesskey()) { + $confirm = optional_param('confirm', 0, PARAM_BOOL); + $hubcourseid = optional_param('hubcourseid', 0, PARAM_INT); + $publicationid = optional_param('publicationid', 0, PARAM_INT); + $timepublished = optional_param('timepublished', 0, PARAM_INT); + $publication->courseshortname = $course->shortname; + $publication->courseid = $course->id; + $publication->hubname = $hubname; + $publication->huburl = $huburl; + $publication->hubcourseid = $hubcourseid; + $publication->timepublished = $timepublished; + if (empty($publication->hubname)) { + $publication->hubname = $huburl; + } + $publication->id = $publicationid; + if($confirm) { + //unpublish the publication by web service + $registeredhub = $registrationmanager->get_registeredhub($huburl); + $function = 'hub_unregister_courses'; + $params = array('courseids' => array( $publication->hubcourseid)); + $serverurl = $huburl."/local/hub/webservice/webservices.php"; + require_once($CFG->dirroot."/webservice/xmlrpc/lib.php"); + $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $registeredhub->token); + $result = $xmlrpcclient->call($function, $params); + + //delete the publication from the database + $publicationmanager->delete_publication($publicationid); + + //display confirmation message + $confirmmessage = $OUTPUT->notification(get_string('courseunpublished', 'hub', $publication), 'notifysuccess'); + + } else { + //display confirmation page for unpublishing + + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('unpublishcourse', 'hub', $shortname), 3, 'main'); + echo $renderer->confirmunpublishing($publication); + echo $OUTPUT->footer(); + die(); + } + } + + //check if a course was published + if (optional_param('published', 0, PARAM_TEXT)) { + $confirmmessage = $OUTPUT->notification(get_string('coursepublished', 'hub', + empty($hubname)?$huburl:$hubname), 'notifysuccess'); + } + + + /// OUTPUT + echo $OUTPUT->header(); + echo $confirmmessage; + + echo $OUTPUT->heading(get_string('publishcourse', 'hub', $shortname), 3, 'main'); + echo $renderer->publicationselector($course->id); + + $publications = $publicationmanager->get_course_publications($course->id); + if (!empty($publications)) { + echo $OUTPUT->heading(get_string('publishedon', 'hub'), 3, 'main'); + echo $renderer->registeredonhublisting($course->id, $publications); + } + + echo $OUTPUT->footer(); + +} diff --git a/publish/lib.php b/publish/lib.php new file mode 100644 index 0000000..fe13219 --- /dev/null +++ b/publish/lib.php @@ -0,0 +1,299 @@ +. +/// TIME PERIOD /// + +define('HUB_LASTMODIFIED_WEEK', 7); + +define('HUB_LASTMODIFIED_FORTEENNIGHT', 14); + +define('HUB_LASTMODIFIED_MONTH', 30); + + + +//// AUDIENCE //// + +/** + * Audience: educators + */ +define('HUB_AUDIENCE_EDUCATORS', 'educators'); + +/** + * Audience: students + */ +define('HUB_AUDIENCE_STUDENTS', 'students'); + +/** + * Audience: admins + */ +define('HUB_AUDIENCE_ADMINS', 'admins'); + + + +///// EDUCATIONAL LEVEL ///// + +/** + * Educational level: primary + */ +define('HUB_EDULEVEL_PRIMARY', 'primary'); + +/** + * Educational level: secondary + */ +define('HUB_EDULEVEL_SECONDARY', 'secondary'); + +/** + * Educational level: tertiary + */ +define('HUB_EDULEVEL_TERTIARY', 'tertiary'); + +/** + * Educational level: government + */ +define('HUB_EDULEVEL_GOVERNMENT', 'government'); + +/** + * Educational level: association + */ +define('HUB_EDULEVEL_ASSOCIATION', 'association'); + +/** + * Educational level: corporate + */ +define('HUB_EDULEVEL_CORPORATE', 'corporate'); + +/** + * Educational level: other + */ +define('HUB_EDULEVEL_OTHER', 'other'); + + + +///// FILE TYPES ///// + +/** + * FILE TYPE: COURSE SCREENSHOT + */ +define('HUB_SCREENSHOT_FILE_TYPE', 'screenshot'); + +/** + * FILE TYPE: HUB SCREENSHOT + */ +define('HUB_HUBSCREENSHOT_FILE_TYPE', 'hubscreenshot'); + +/** + * FILE TYPE: BACKUP + */ +define('HUB_BACKUP_FILE_TYPE', 'backup'); + +/** + * + * Course publication library + * + * @package course + * @copyright 2010 Moodle Pty Ltd (http://moodle.com) + * @author Jerome Mouneyrac + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_publish_manager { + + /** + * Record a course publication + * @param int $hubid the hub id from the 'registered on hub' table + * @param int $courseid the course id from site point of view + * @param int $enrollable if the course is enrollable = 1, if downloadable = 0 + * @param int $hubcourseid the course id from the hub point of view + */ + public function add_course_publication($huburl, $courseid, $enrollable, $hubcourseid) { + global $DB; + $publication = new stdClass(); + $publication->huburl = $huburl; + $publication->courseid = $courseid; + $publication->hubcourseid = $hubcourseid; + $publication->enrollable = (int) $enrollable; + $publication->timepublished = time(); + $DB->insert_record('course_published', $publication); + } + + /** + * Update a enrollable course publication + * @param int $publicationid + */ + public function update_enrollable_course_publication($publicationid) { + global $DB; + $publication = new stdClass(); + $publication->id = $publicationid; + $publication->timepublished = time(); + $DB->update_record('course_published', $publication); + } + + /** + * Update a course publication + * @param object $publication + */ + public function update_publication($publication) { + global $DB; + $DB->update_record('course_published', $publication); + } + + /** + * Get courses publications + * @param int $hubid specify a hub + * @param int $courseid specify a course + * @param int $enrollable specify type of publication (enrollable or downloadable) + * @return array of publications + */ + public function get_publications($huburl = null, $courseid = null, $enrollable = -1) { + global $DB; + $params = array(); + + if (!empty($huburl)) { + $params['huburl'] = $huburl; + } + + if (!empty($courseid)) { + $params['courseid'] = $courseid; + } + + if ($enrollable != -1) { + $params['enrollable'] = (int) $enrollable; + } + + return $DB->get_records('course_published', $params); + } + + /** + * Get a publication for a course id on a hub + * (which is either the id of the unique possible enrollable publication of a course, + * either an id of one of the downloadable publication) + * @param int $hubcourseid + * @param string $huburl + * @return object publication + */ + public function get_publication($hubcourseid, $huburl) { + global $DB; + return $DB->get_record('course_published', + array('hubcourseid' => $hubcourseid, 'huburl' => $huburl)); + } + + /** + * Get all publication for a course + * @param int $courseid + * @return array of publication + */ + public function get_course_publications($courseid) { + global $DB; + $sql = 'SELECT cp.id, cp.status, cp.timechecked, cp.timepublished, rh.hubname, + rh.huburl, cp.courseid, cp.enrollable, cp.hubcourseid + FROM {course_published} cp, {registration_hubs} rh + WHERE cp.huburl = rh.huburl and cp.courseid = :courseid + ORDER BY cp.enrollable DESC, rh.hubname, cp.timepublished'; + $params = array('courseid' => $courseid); + return $DB->get_records_sql($sql, $params); + } + + /** + * Get the hub concerned by a publication + * @param int $publicationid + * @return object the hub (id, name, url, token) + */ + public function get_registeredhub_by_publication($publicationid) { + global $DB; + $sql = 'SELECT rh.huburl, rh.hubname, rh.token + FROM {course_published} cp, {registration_hubs} rh + WHERE cp.huburl = rh.huburl and cp.id = :publicationid'; + $params = array('publicationid' => $publicationid); + return $DB->get_record_sql($sql, $params); + } + + /** + * Delete a publication + * @param int $publicationid + */ + public function delete_publication($publicationid) { + global $DB; + $DB->delete_records('course_published', array('id' => $publicationid)); + } + + /** + * Delete publications for a hub + * @param string $huburl + * @param int $enrollable + */ + public function delete_hub_publications($huburl, $enrollable = -1) { + global $DB; + + $params = array(); + + if (!empty($huburl)) { + $params['huburl'] = $huburl; + } + + if ($enrollable != -1) { + $params['enrollable'] = (int) $enrollable; + } + + $DB->delete_records('course_published', $params); + } + + /** + * Get an array of all block instances for a given context + * @param int $contextid a context id + * @return array of block instances. + */ + public function get_block_instances_by_context($contextid, $sort = '') { + global $DB; + return $DB->get_records('block_instances', array('parentcontextid' => $contextid), $sort); + } + + /** + * Retrieve all the sorted course subjects + * @return array $subjects + */ + public function get_sorted_subjects() { + $subjects = get_string_manager()->load_component_strings('edufields', current_language()); + + //sort the subjects + asort($subjects); + foreach ($subjects as $key => $option) { + $keylength = strlen($key); + if ($keylength == 8) { + $toplevel[$key] = $option; + } else if ($keylength == 10) { + $sublevel[substr($key, 0, 8)][$key] = $option; + } else if ($keylength == 12) { + $subsublevel[substr($key, 0, 8)][substr($key, 0, 10)][$key] = $option; + } + } + + //recreate the initial structure returned by get_string_manager() + $subjects = array(); + foreach ($toplevel as $key => $name) { + $subjects[$key] = $name; + foreach ($sublevel[$key] as $subkey => $subname) { + $subjects[$subkey] = $subname; + foreach ($subsublevel[$key][$subkey] as $subsubkey => $subsubname) { + $subjects[$subsubkey] = $subsubname; + } + } + } + + return $subjects; + } + +} +?> diff --git a/publish/metadata.php b/publish/metadata.php new file mode 100644 index 0000000..298e5f3 --- /dev/null +++ b/publish/metadata.php @@ -0,0 +1,274 @@ +. // +// // +/////////////////////////////////////////////////////////////////////////// + +/* + * @package course + * @subpackage publish + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL + * @copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com + * + * This page display the publication metadata form + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/course/publish/forms.php'); +require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php'); +require_once($CFG->dirroot . '/course/publish/lib.php'); +require_once($CFG->libdir . '/filelib.php'); + + +//check user access capability to this page +$id = required_param('id', PARAM_INT); + +$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); +require_login($course); + +//page settings +$PAGE->set_url('/course/publish/metadata.php', array('id' => $course->id)); +$PAGE->set_pagelayout('course'); +$PAGE->set_title(get_string('course') . ': ' . $course->fullname); +$PAGE->set_heading($course->fullname); + +//check that the PHP xmlrpc extension is enabled +if (!extension_loaded('xmlrpc')) { + $errornotification = $OUTPUT->doc_link('admin/environment/php_extension/xmlrpc', ''); + $errornotification .= get_string('xmlrpcdisabledpublish', 'hub'); + $context = context_course::instance($course->id); + $shortname = format_string($course->shortname, true, array('context' => $context)); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishcourse', 'hub', $shortname), 3, 'main'); + echo $OUTPUT->notification($errornotification); + echo $OUTPUT->footer(); + die(); +} + +if (has_capability('moodle/course:publish', context_course::instance($id))) { + + //retrieve hub name and hub url + $huburl = optional_param('huburl', '', PARAM_URL); + $hubname = optional_param('hubname', '', PARAM_TEXT); + if (empty($huburl) or !confirm_sesskey()) { + throw new moodle_exception('missingparameter'); + } + + //set the publication form + $advertise = optional_param('advertise', false, PARAM_BOOL); + $share = optional_param('share', false, PARAM_BOOL); + $coursepublicationform = new course_publication_form('', + array('huburl' => $huburl, 'hubname' => $hubname, 'sesskey' => sesskey(), + 'course' => $course, 'advertise' => $advertise, 'share' => $share, + 'id' => $id, 'page' => $PAGE)); + $fromform = $coursepublicationform->get_data(); + + //retrieve the token to call the hub + $registrationmanager = new registration_manager(); + $registeredhub = $registrationmanager->get_registeredhub($huburl); + + //setup web service xml-rpc client + $serverurl = $huburl . "/local/hub/webservice/webservices.php"; + require_once($CFG->dirroot . "/webservice/xmlrpc/lib.php"); + $xmlrpcclient = new webservice_xmlrpc_client($serverurl, $registeredhub->token); + + if (!empty($fromform)) { + + $publicationmanager = new course_publish_manager(); + + //retrieve the course information + $courseinfo = new stdClass(); + $courseinfo->fullname = $fromform->name; + $courseinfo->shortname = $fromform->courseshortname; + $courseinfo->description = $fromform->description; + $courseinfo->language = $fromform->language; + $courseinfo->publishername = $fromform->publishername; + $courseinfo->publisheremail = $fromform->publisheremail; + $courseinfo->contributornames = $fromform->contributornames; + $courseinfo->coverage = $fromform->coverage; + $courseinfo->creatorname = $fromform->creatorname; + $courseinfo->licenceshortname = $fromform->licence; + $courseinfo->subject = $fromform->subject; + $courseinfo->audience = $fromform->audience; + $courseinfo->educationallevel = $fromform->educationallevel; + $creatornotes = $fromform->creatornotes; + $courseinfo->creatornotes = $creatornotes['text']; + $courseinfo->creatornotesformat = $creatornotes['format']; + $courseinfo->sitecourseid = $id; + if (!empty($fromform->deletescreenshots)) { + $courseinfo->deletescreenshots = $fromform->deletescreenshots; + } + if ($share) { + $courseinfo->demourl = $fromform->demourl; + $courseinfo->enrollable = false; + } else { + $courseinfo->courseurl = $fromform->courseurl; + $courseinfo->enrollable = true; + } + + //retrieve the outcomes of this course + require_once($CFG->libdir . '/grade/grade_outcome.php'); + $outcomes = grade_outcome::fetch_all_available($id); + if (!empty($outcomes)) { + foreach ($outcomes as $outcome) { + $sentoutcome = new stdClass(); + $sentoutcome->fullname = $outcome->fullname; + $courseinfo->outcomes[] = $sentoutcome; + } + } + + //retrieve the content information from the course + $coursecontext = context_course::instance($course->id); + $courseblocks = $publicationmanager->get_block_instances_by_context($coursecontext->id, 'blockname'); + + if (!empty($courseblocks)) { + $blockname = ''; + foreach ($courseblocks as $courseblock) { + if ($courseblock->blockname != $blockname) { + if (!empty($blockname)) { + $courseinfo->contents[] = $content; + } + + $blockname = $courseblock->blockname; + $content = new stdClass(); + $content->moduletype = 'block'; + $content->modulename = $courseblock->blockname; + $content->contentcount = 1; + } else { + $content->contentcount = $content->contentcount + 1; + } + } + $courseinfo->contents[] = $content; + } + + $activities = get_fast_modinfo($course, $USER->id); + foreach ($activities->instances as $activityname => $activitydetails) { + $content = new stdClass(); + $content->moduletype = 'activity'; + $content->modulename = $activityname; + $content->contentcount = count($activities->instances[$activityname]); + $courseinfo->contents[] = $content; + } + + //save into screenshots field the references to the screenshot content hash + //(it will be like a unique id from the hub perspective) + if (!empty($fromform->deletescreenshots) or $share) { + $courseinfo->screenshots = 0; + } else { + $courseinfo->screenshots = $fromform->existingscreenshotnumber; + } + if (!empty($fromform->screenshots)) { + $screenshots = $fromform->screenshots; + $fs = get_file_storage(); + $files = $fs->get_area_files(context_user::instance($USER->id)->id, 'user', 'draft', $screenshots); + if (!empty($files)) { + $courseinfo->screenshots = $courseinfo->screenshots + count($files) - 1; //minus the ./ directory + } + } + + // PUBLISH ACTION + + //publish the course information + $function = 'hub_register_courses'; + $params = array('courses' => array($courseinfo)); + try { + $courseids = $xmlrpcclient->call($function, $params); + } catch (Exception $e) { + throw new moodle_exception('errorcoursepublish', 'hub', + new moodle_url('/course/view.php', array('id' => $id)), $e->getMessage()); + } + + if (count($courseids) != 1) { + throw new moodle_exception('errorcoursewronglypublished', 'hub'); + } + + //save the record into the published course table + $publication = $publicationmanager->get_publication($courseids[0], $huburl); + if (empty($publication)) { + //if never been published or if we share, we need to save this new publication record + $publicationmanager->add_course_publication($registeredhub->huburl, $course->id, !$share, $courseids[0]); + } else { + //if we update the enrollable course publication we update the publication record + $publicationmanager->update_enrollable_course_publication($publication->id); + } + + // SEND FILES + $curl = new curl(); + + // send screenshots + if (!empty($fromform->screenshots)) { + + if (!empty($fromform->deletescreenshots) or $share) { + $screenshotnumber = 0; + } else { + $screenshotnumber = $fromform->existingscreenshotnumber; + } + foreach ($files as $file) { + if ($file->is_valid_image()) { + $screenshotnumber = $screenshotnumber + 1; + $params = array(); + $params['filetype'] = HUB_SCREENSHOT_FILE_TYPE; + $params['file'] = $file; + $params['courseid'] = $courseids[0]; + $params['filename'] = $file->get_filename(); + $params['screenshotnumber'] = $screenshotnumber; + $params['token'] = $registeredhub->token; + $curl->post($huburl . "/local/hub/webservice/upload.php", $params); + } + } + } + + //redirect to the backup process page + if ($share) { + $params = array('sesskey' => sesskey(), 'id' => $id, 'hubcourseid' => $courseids[0], + 'huburl' => $huburl, 'hubname' => $hubname); + $backupprocessurl = new moodle_url("/course/publish/backup.php", $params); + redirect($backupprocessurl); + } else { + //redirect to the index publis page + redirect(new moodle_url('/course/publish/index.php', + array('sesskey' => sesskey(), 'id' => $id, 'published' => true, + 'hubname' => $hubname, 'huburl' => $huburl))); + } + } + + /////// OUTPUT SECTION ///////////// + + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('publishcourseon', 'hub', !empty($hubname) ? $hubname : $huburl), 3, 'main'); + + //display hub information (logo, name, description) + $function = 'hub_get_info'; + $params = array(); + try { + $hubinfo = $xmlrpcclient->call($function, $params); + } catch (Exception $e) { + //only print error log in apache (for backward compatibility) + error_log(print_r($e->getMessage(), true)); + } + $renderer = $PAGE->get_renderer('core', 'publish'); + if (!empty($hubinfo)) { + echo $renderer->hubinfo($hubinfo); + } + + //display metadata form + $coursepublicationform->display(); + echo $OUTPUT->footer(); +} diff --git a/publish/renderer.php b/publish/renderer.php new file mode 100644 index 0000000..970fed5 --- /dev/null +++ b/publish/renderer.php @@ -0,0 +1,218 @@ +. // +// // +/////////////////////////////////////////////////////////////////////////// + +/** + * Course publish renderer. + * @package course + * @subpackage publish + * @copyright 2010 Moodle Pty Ltd (http://moodle.com) + * @author Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_publish_renderer extends plugin_renderer_base { + + /** + * Display the selector to advertise or publish a course + */ + public function publicationselector($courseid) { + $text = ''; + + $advertiseurl = new moodle_url("/course/publish/hubselector.php", + array('sesskey' => sesskey(), 'id' => $courseid, 'advertise' => true)); + $advertisebutton = new single_button($advertiseurl, get_string('advertise', 'hub')); + $text .= $this->output->render($advertisebutton); + $text .= html_writer::tag('div', get_string('advertisepublication_help', 'hub'), + array('class' => 'publishhelp')); + + $text .= html_writer::empty_tag('br'); /// TODO Delete + + $uploadurl = new moodle_url("/course/publish/hubselector.php", + array('sesskey' => sesskey(), 'id' => $courseid, 'share' => true)); + $uploadbutton = new single_button($uploadurl, get_string('share', 'hub')); + $text .= $this->output->render($uploadbutton); + $text .= html_writer::tag('div', get_string('sharepublication_help', 'hub'), + array('class' => 'publishhelp')); + + return $text; + } + + /** + * Display the listing of hub where a course is registered on + */ + public function registeredonhublisting($courseid, $publications) { + global $CFG; + $table = new html_table(); + $table->head = array(get_string('type', 'hub'), get_string('hub', 'hub'), + get_string('date'), get_string('status', 'hub'), get_string('operation', 'hub')); + $table->size = array('10%', '40%', '20%', '%10', '%15'); + + $brtag = html_writer::empty_tag('br'); + + foreach ($publications as $publication) { + + $updatebuttonhtml = ''; + + $params = array('sesskey' => sesskey(), 'id' => $publication->courseid, + 'hubcourseid' => $publication->hubcourseid, + 'huburl' => $publication->huburl, 'hubname' => $publication->hubname, + 'cancel' => true, 'publicationid' => $publication->id, + 'timepublished' => $publication->timepublished); + $cancelurl = new moodle_url("/course/publish/index.php", $params); + $cancelbutton = new single_button($cancelurl, get_string('removefromhub', 'hub')); + $cancelbutton->class = 'centeredbutton'; + $cancelbuttonhtml = $this->output->render($cancelbutton); + + if ($publication->enrollable) { + $params = array('sesskey' => sesskey(), 'id' => $publication->courseid, + 'huburl' => $publication->huburl, 'hubname' => $publication->hubname, + 'share' => !$publication->enrollable, 'advertise' => $publication->enrollable); + $updateurl = new moodle_url("/course/publish/metadata.php", $params); + $updatebutton = new single_button($updateurl, get_string('update', 'hub')); + $updatebutton->class = 'centeredbutton'; + $updatebuttonhtml = $this->output->render($updatebutton); + + $operations = $updatebuttonhtml . $brtag . $cancelbuttonhtml; + } else { + $operations = $cancelbuttonhtml; + } + + $hubname = html_writer::tag('a', + $publication->hubname ? $publication->hubname : $publication->huburl, + array('href' => $publication->huburl)); + //if the publication check time if bigger than May 2010, it has been checked + if ($publication->timechecked > 1273127954) { + if ($publication->status == 0) { + $status = get_string('statusunpublished', 'hub'); + } else { + $status = get_string('statuspublished', 'hub'); + } + + $status .= $brtag . html_writer::tag('a', get_string('updatestatus', 'hub'), + array('href' => $CFG->wwwroot . '/course/publish/index.php?id=' + . $courseid . "&updatestatusid=" . $publication->id + . "&sesskey=" . sesskey())) . + $brtag . get_string('lasttimechecked', 'hub') . ": " + . format_time(time() - $publication->timechecked); + } else { + $status = get_string('neverchecked', 'hub') . $brtag + . html_writer::tag('a', get_string('updatestatus', 'hub'), + array('href' => $CFG->wwwroot . '/course/publish/index.php?id=' + . $courseid . "&updatestatusid=" . $publication->id + . "&sesskey=" . sesskey())); + } + //add button cells + $cells = array($publication->enrollable ? + get_string('advertised', 'hub') : get_string('shared', 'hub'), + $hubname, userdate($publication->timepublished, + get_string('strftimedatetimeshort')), $status, $operations); + $row = new html_table_row($cells); + $table->data[] = $row; + } + + $contenthtml = html_writer::table($table); + + return $contenthtml; + } + + /** + * Display unpublishing confirmation page + * @param object $publication + * $publication->courseshortname + $publication->courseid + $publication->hubname + $publication->huburl + $publication->id + */ + public function confirmunpublishing($publication) { + $optionsyes = array('sesskey' => sesskey(), 'id' => $publication->courseid, + 'hubcourseid' => $publication->hubcourseid, + 'huburl' => $publication->huburl, 'hubname' => $publication->hubname, + 'cancel' => true, 'publicationid' => $publication->id, 'confirm' => true); + $optionsno = array('sesskey' => sesskey(), 'id' => $publication->courseid); + $publication->hubname = html_writer::tag('a', $publication->hubname, + array('href' => $publication->huburl)); + $formcontinue = new single_button(new moodle_url("/course/publish/index.php", + $optionsyes), get_string('unpublish', 'hub'), 'post'); + $formcancel = new single_button(new moodle_url("/course/publish/index.php", + $optionsno), get_string('cancel'), 'get'); + return $this->output->confirm(get_string('unpublishconfirmation', 'hub', $publication), + $formcontinue, $formcancel); + } + + /** + * Display waiting information about backup size during uploading backup process + * @param object $backupfile the backup stored_file + * @return $html string + */ + public function sendingbackupinfo($backupfile) { + $sizeinfo = new stdClass(); + $sizeinfo->total = number_format($backupfile->get_filesize() / 1000000, 2); + $html = html_writer::tag('div', get_string('sendingsize', 'hub', $sizeinfo), + array('class' => 'courseuploadtextinfo')); + return $html; + } + + /** + * Display upload successfull message and a button to the publish index page + * @param int $id the course id + * @param string $huburl the hub url where the course is published + * @param string $hubname the hub name where the course is published + * @return $html string + */ + public function sentbackupinfo($id, $huburl, $hubname) { + $html = html_writer::tag('div', get_string('sent', 'hub'), + array('class' => 'courseuploadtextinfo')); + $publishindexurl = new moodle_url('/course/publish/index.php', + array('sesskey' => sesskey(), 'id' => $id, + 'published' => true, 'huburl' => $huburl, 'hubname' => $hubname)); + $continue = $this->output->render( + new single_button($publishindexurl, get_string('continue', 'hub'))); + $html .= html_writer::tag('div', $continue, array('class' => 'sharecoursecontinue')); + return $html; + } + + /** + * Hub information (logo - name - description - link) + * @param object $hubinfo + * @return string html code + */ + public function hubinfo($hubinfo) { + $params = array('filetype' => HUB_HUBSCREENSHOT_FILE_TYPE); + $imgurl = new moodle_url($hubinfo['url'] . + "/local/hub/webservice/download.php", $params); + $screenshothtml = html_writer::empty_tag('img', + array('src' => $imgurl, 'alt' => $hubinfo['name'])); + $hubdescription = html_writer::tag('div', $screenshothtml, + array('class' => 'hubscreenshot')); + + $hubdescription .= html_writer::tag('a', $hubinfo['name'], + array('class' => 'hublink', 'href' => $hubinfo['url'], + 'onclick' => 'this.target="_blank"')); + + $hubdescription .= html_writer::tag('div', format_text($hubinfo['description'], FORMAT_PLAIN), + array('class' => 'hubdescription')); + $hubdescription = html_writer::tag('div', $hubdescription, array('class' => 'hubinfo')); + + return $hubdescription; + } + +} diff --git a/recent.php b/recent.php new file mode 100644 index 0000000..df0f0d5 --- /dev/null +++ b/recent.php @@ -0,0 +1,294 @@ +. + +/** + * Display all recent activity in a flexible way + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once('../config.php'); +require_once('lib.php'); +require_once('recent_form.php'); + +$id = required_param('id', PARAM_INT); + +$PAGE->set_url('/course/recent.php', array('id'=>$id)); + +if (!$course = $DB->get_record('course', array('id'=>$id))) { + print_error("That's an invalid course id"); +} + +require_login($course); + +add_to_log($course->id, "course", "recent", "recent.php?id=$course->id", $course->id); + +$context = context_course::instance($course->id); + +$lastlogin = time() - COURSE_MAX_RECENT_PERIOD; +if (!isguestuser() and !empty($USER->lastcourseaccess[$COURSE->id])) { + if ($USER->lastcourseaccess[$COURSE->id] > $lastlogin) { + $lastlogin = $USER->lastcourseaccess[$COURSE->id]; + } +} + +$param = new stdClass(); +$param->user = 0; +$param->modid = 'all'; +$param->group = 0; +$param->sortby = 'default'; +$param->date = $lastlogin; +$param->id = $COURSE->id; + +$mform = new recent_form(); +$mform->set_data($param); +if ($formdata = $mform->get_data()) { + $param = $formdata; +} + +$userinfo = get_string('allparticipants'); +$dateinfo = get_string('alldays'); + +if (!empty($param->user)) { + if (!$u = $DB->get_record('user', array('id'=>$param->user))) { + print_error("That's an invalid user!"); + } + $userinfo = fullname($u); +} + +$strrecentactivity = get_string('recentactivity'); +$PAGE->navbar->add($strrecentactivity, new moodle_url('/course/recent.php', array('id'=>$course->id))); +$PAGE->navbar->add($userinfo); +$PAGE->set_title("$course->shortname: $strrecentactivity"); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); +echo $OUTPUT->heading(format_string($course->fullname) . ": $userinfo", 2); + +$mform->display(); + +$modinfo = get_fast_modinfo($course); +$modnames = get_module_types_names(); + +if (has_capability('moodle/course:viewhiddensections', $context)) { + $hiddenfilter = ""; +} else { + $hiddenfilter = "AND cs.visible = 1"; +} +$sections = array(); +foreach ($modinfo->get_section_info_all() as $i => $section) { + if (!empty($section->uservisible)) { + $sections[$i] = $section; + } +} + +if ($param->modid === 'all') { + // ok + +} else if (strpos($param->modid, 'mod/') === 0) { + $modname = substr($param->modid, strlen('mod/')); + if (array_key_exists($modname, $modnames) and file_exists("$CFG->dirroot/mod/$modname/lib.php")) { + $filter = $modname; + } + +} else if (strpos($param->modid, 'section/') === 0) { + $sectionid = substr($param->modid, strlen('section/')); + if (isset($sections[$sectionid])) { + $sections = array($sectionid=>$sections[$sectionid]); + } + +} else if (is_numeric($param->modid)) { + $sectionnum = $modinfo->cms[$param->modid]->sectionnum; + $filter_modid = $param->modid; + $sections = array($sectionnum => $sections[$sectionnum]); +} + + +$modinfo->get_groups(); // load all my groups and cache it in modinfo + +$activities = array(); +$index = 0; + +foreach ($sections as $sectionnum => $section) { + + $activity = new stdClass(); + $activity->type = 'section'; + if ($section->section > 0) { + $activity->name = get_section_name($course, $section); + } else { + $activity->name = ''; + } + + $activity->visible = $section->visible; + $activities[$index++] = $activity; + + if (empty($modinfo->sections[$sectionnum])) { + continue; + } + + foreach ($modinfo->sections[$sectionnum] as $cmid) { + $cm = $modinfo->cms[$cmid]; + + if (!$cm->uservisible) { + continue; + } + + if (!empty($filter) and $cm->modname != $filter) { + continue; + } + + if (!empty($filter_modid) and $cmid != $filter_modid) { + continue; + } + + $libfile = "$CFG->dirroot/mod/$cm->modname/lib.php"; + + if (file_exists($libfile)) { + require_once($libfile); + $get_recent_mod_activity = $cm->modname."_get_recent_mod_activity"; + + if (function_exists($get_recent_mod_activity)) { + $activity = new stdClass(); + $activity->type = 'activity'; + $activity->cmid = $cmid; + $activities[$index++] = $activity; + $get_recent_mod_activity($activities, $index, $param->date, $course->id, $cmid, $param->user, $param->group); + } + } + } +} + +$detail = true; + +switch ($param->sortby) { + case 'datedesc' : usort($activities, 'compare_activities_by_time_desc'); break; + case 'dateasc' : usort($activities, 'compare_activities_by_time_asc'); break; + case 'default' : + default : $detail = false; $param->sortby = 'default'; + +} + +if (!empty($activities)) { + + $newsection = true; + $lastsection = ''; + $newinstance = true; + $lastinstance = ''; + $inbox = false; + + $section = 0; + + $activity_count = count($activities); + $viewfullnames = array(); + + foreach ($activities as $key => $activity) { + + if ($activity->type == 'section') { + if ($param->sortby != 'default') { + continue; // no section if ordering by date + } + if ($activity_count == ($key + 1) or $activities[$key+1]->type == 'section') { + // peak at next activity. If it's another section, don't print this one! + // this means there are no activities in the current section + continue; + } + } + + if (($activity->type == 'section') && ($param->sortby == 'default')) { + if ($inbox) { + echo $OUTPUT->box_end(); + echo $OUTPUT->spacer(array('height'=>30, 'br'=>true)); // should be done with CSS instead + } + echo $OUTPUT->box_start(); + if (!empty($activity->name)) { + echo html_writer::tag('h2', $activity->name); + } + $inbox = true; + + } else if ($activity->type == 'activity') { + + if ($param->sortby == 'default') { + $cm = $modinfo->cms[$activity->cmid]; + + if ($cm->visible) { + $class = ''; + } else { + $class = 'dimmed'; + } + $name = format_string($cm->name); + $modfullname = $modnames[$cm->modname]; + + $image = $OUTPUT->pix_icon('icon', $modfullname, $cm->modname, array('class' => 'icon smallicon')); + $link = html_writer::link(new moodle_url("/mod/$cm->modname/view.php", + array("id" => $cm->id)), $name, array('class' => $class)); + echo html_writer::tag('h3', "$image $modfullname $link"); + } + + } else { + + if (!isset($viewfullnames[$activity->cmid])) { + $cm_context = context_module::instance($activity->cmid); + $viewfullnames[$activity->cmid] = has_capability('moodle/site:viewfullnames', $cm_context); + } + + if (!$inbox) { + echo $OUTPUT->box_start(); + $inbox = true; + } + + $print_recent_mod_activity = $activity->type.'_print_recent_mod_activity'; + + if (function_exists($print_recent_mod_activity)) { + $print_recent_mod_activity($activity, $course->id, $detail, $modnames, $viewfullnames[$activity->cmid]); + } + } + } + + if ($inbox) { + echo $OUTPUT->box_end(); + } + + +} else { + + echo html_writer::tag('h3', get_string('norecentactivity'), array('class' => 'mdl-align')); + +} + +echo $OUTPUT->footer(); + +function compare_activities_by_time_desc($a, $b) { + // make sure the activities actually have a timestamp property + if ((!array_key_exists('timestamp', $a)) or (!array_key_exists('timestamp', $b))) { + return 0; + } + if ($a->timestamp == $b->timestamp) + return 0; + return ($a->timestamp > $b->timestamp) ? -1 : 1; +} + +function compare_activities_by_time_asc($a, $b) { + // make sure the activities actually have a timestamp property + if ((!array_key_exists('timestamp', $a)) or (!array_key_exists('timestamp', $b))) { + return 0; + } + if ($a->timestamp == $b->timestamp) + return 0; + return ($a->timestamp < $b->timestamp) ? -1 : 1; +} + diff --git a/recent_form.php b/recent_form.php new file mode 100644 index 0000000..cfdbffa --- /dev/null +++ b/recent_form.php @@ -0,0 +1,169 @@ +. + +/** + * Display all recent activity in a flexible way + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page +} + +require_once($CFG->libdir.'/formslib.php'); + +class recent_form extends moodleform { + function definition() { + global $CFG, $COURSE, $USER; + + $mform =& $this->_form; + $context = context_course::instance($COURSE->id); + $modinfo = get_fast_modinfo($COURSE); + + $mform->addElement('header', 'filters', get_string('managefilters')); //TODO: add better string + + $groupoptions = array(); + if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { + // limited group access + $groups = groups_get_user_groups($COURSE->id); + $allgroups = groups_get_all_groups($COURSE->id); + if (!empty($groups[$COURSE->defaultgroupingid])) { + foreach ($groups[$COURSE->defaultgroupingid] AS $groupid) { + $groupoptions[$groupid] = format_string($allgroups[$groupid]->name, true, array('context'=>$context)); + } + } + } else { + $groupoptions = array('0'=>get_string('allgroups')); + if (has_capability('moodle/site:accessallgroups', $context)) { + // user can see all groups + $allgroups = groups_get_all_groups($COURSE->id); + } else { + // user can see course level groups + $allgroups = groups_get_all_groups($COURSE->id, 0, $COURSE->defaultgroupingid); + } + foreach($allgroups as $group) { + $groupoptions[$group->id] = format_string($group->name, true, array('context'=>$context)); + } + } + + if ($COURSE->id == SITEID) { + $viewparticipants = has_capability('moodle/site:viewparticipants', context_system::instance()); + } else { + $viewparticipants = has_capability('moodle/course:viewparticipants', $context); + } + + if ($viewparticipants) { + $viewfullnames = has_capability('moodle/site:viewfullnames', context_course::instance($COURSE->id)); + + $options = array(); + $options[0] = get_string('allparticipants'); + $options[$CFG->siteguest] = get_string('guestuser'); + + if (isset($groupoptions[0])) { + // can see all enrolled users + if ($enrolled = get_enrolled_users($context, null, 0, user_picture::fields('u'))) { + foreach ($enrolled as $euser) { + $options[$euser->id] = fullname($euser, $viewfullnames); + } + } + } else { + // can see users from some groups only + foreach ($groupoptions as $groupid=>$unused) { + if ($enrolled = get_enrolled_users($context, null, $groupid, user_picture::fields('u'))) { + foreach ($enrolled as $euser) { + if (!array_key_exists($euser->id, $options)) { + $options[$euser->id] = fullname($euser, $viewfullnames); + } + } + } + } + } + + $mform->addElement('select', 'user', get_string('participants'), $options); + $mform->setAdvanced('user'); + } else { + // Default to no user. + $mform->addElement('hidden', 'user', 0); + } + + $options = array(''=>get_string('allactivities')); + $modsused = array(); + + foreach($modinfo->cms as $cm) { + if (!$cm->uservisible) { + continue; + } + $modsused[$cm->modname] = true; + } + + foreach ($modsused as $modname=>$unused) { + $libfile = "$CFG->dirroot/mod/$modname/lib.php"; + if (!file_exists($libfile)) { + unset($modsused[$modname]); + continue; + } + include_once($libfile); + $libfunction = $modname."_get_recent_mod_activity"; + if (!function_exists($libfunction)) { + unset($modsused[$modname]); + continue; + } + $options["mod/$modname"] = get_string('allmods', '', get_string('modulenameplural', $modname)); + } + + foreach ($modinfo->sections as $section=>$cmids) { + $options["section/$section"] = "-- ".get_section_name($COURSE, $section)." --"; + foreach ($cmids as $cmid) { + $cm = $modinfo->cms[$cmid]; + if (empty($modsused[$cm->modname]) or !$cm->uservisible) { + continue; + } + $options[$cm->id] = format_string($cm->name); + } + } + $mform->addElement('select', 'modid', get_string('activities'), $options); + $mform->setAdvanced('modid'); + + + if ($groupoptions) { + $mform->addElement('select', 'group', get_string('groups'), $groupoptions); + $mform->setAdvanced('group'); + } else { + // no access to groups in separate mode + $mform->addElement('hidden','group'); + $mform->setType('group', PARAM_INT); + $mform->setConstants(array('group'=>-1)); + } + + $options = array('default' => get_string('bycourseorder'), + 'dateasc' => get_string('datemostrecentlast'), + 'datedesc' => get_string('datemostrecentfirst')); + $mform->addElement('select', 'sortby', get_string('sortby'), $options); + $mform->setAdvanced('sortby'); + + $mform->addElement('date_time_selector', 'date', get_string('since'), array('optional'=>true)); + + $mform->addElement('hidden','id'); + $mform->setType('id', PARAM_INT); + $mform->setType('courseid', PARAM_INT); + + $this->add_action_buttons(false, get_string('showrecent')); + } +} diff --git a/renderer.php b/renderer.php new file mode 100644 index 0000000..33c15b4 --- /dev/null +++ b/renderer.php @@ -0,0 +1,357 @@ +. + +/** + * Renderer for use with the course section and all the goodness that falls + * within it. + * + * This renderer should contain methods useful to courses, and categories. + * + * @package moodlecore + * @copyright 2010 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The core course renderer + * + * Can be retrieved with the following: + * $renderer = $PAGE->get_renderer('core','course'); + */ +class core_course_renderer extends plugin_renderer_base { + + /** + * A cache of strings + * @var stdClass + */ + protected $strings; + + /** + * Override the constructor so that we can initialise the string cache + * + * @param moodle_page $page + * @param string $target + */ + public function __construct(moodle_page $page, $target) { + $this->strings = new stdClass; + parent::__construct($page, $target); + } + + /** + * Renders course info box. + * + * @param stdClass $course + * @return string + */ + public function course_info_box(stdClass $course) { + global $CFG; + + $context = context_course::instance($course->id); + + $content = ''; + $content .= $this->output->box_start('generalbox info'); + + $summary = file_rewrite_pluginfile_urls($course->summary, 'pluginfile.php', $context->id, 'course', 'summary', null); + $content .= format_text($summary, $course->summaryformat, array('overflowdiv'=>true), $course->id); + if (!empty($CFG->coursecontact)) { + $coursecontactroles = explode(',', $CFG->coursecontact); + foreach ($coursecontactroles as $roleid) { + if ($users = get_role_users($roleid, $context, true)) { + foreach ($users as $teacher) { + $role = new stdClass(); + $role->id = $teacher->roleid; + $role->name = $teacher->rolename; + $role->shortname = $teacher->roleshortname; + $role->coursealias = $teacher->rolecoursealias; + $fullname = fullname($teacher, has_capability('moodle/site:viewfullnames', $context)); + $namesarray[] = role_get_name($role, $context).': '.$fullname.''; + } + } + } + + if (!empty($namesarray)) { + $content .= "

            \n
          • "; + $content .= implode('
          • ', $namesarray); + $content .= "
          "; + } + } + + $content .= $this->output->box_end(); + + return $content; + } + + /** + * Renderers a structured array of courses and categories into a nice + * XHTML tree structure. + * + * This method was designed initially to display the front page course/category + * combo view. The structure can be retrieved by get_course_category_tree() + * + * @param array $structure + * @return string + */ + public function course_category_tree(array $structure) { + $this->strings->summary = get_string('summary'); + + // Generate an id and the required JS call to make this a nice widget + $id = html_writer::random_id('course_category_tree'); + $this->page->requires->js_init_call('M.util.init_toggle_class_on_click', array($id, '.category.with_children .category_label', 'collapsed', '.category.with_children')); + + // Start content generation + $content = html_writer::start_tag('div', array('class'=>'course_category_tree', 'id'=>$id)); + foreach ($structure as $category) { + $content .= $this->course_category_tree_category($category); + } + $content .= html_writer::start_tag('div', array('class'=>'controls')); + $content .= html_writer::tag('div', get_string('collapseall'), array('class'=>'addtoall expandall')); + $content .= html_writer::tag('div', get_string('expandall'), array('class'=>'removefromall collapseall')); + $content .= html_writer::end_tag('div'); + $content .= html_writer::end_tag('div'); + + // Return the course category tree HTML + return $content; + } + + /** + * Renderers a category for use with course_category_tree + * + * @param array $category + * @param int $depth + * @return string + */ + protected function course_category_tree_category(stdClass $category, $depth=1) { + $content = ''; + $hassubcategories = (isset($category->categories) && count($category->categories)>0); + $hascourses = (isset($category->courses) && count($category->courses)>0); + $classes = array('category'); + if ($category->parent != 0) { + $classes[] = 'subcategory'; + } + if (empty($category->visible)) { + $classes[] = 'dimmed_category'; + } + if ($hassubcategories || $hascourses) { + $classes[] = 'with_children'; + if ($depth > 1) { + $classes[] = 'collapsed'; + } + } + $categoryname = format_string($category->name, true, array('context' => context_coursecat::instance($category->id))); + + $content .= html_writer::start_tag('div', array('class'=>join(' ', $classes))); + $content .= html_writer::start_tag('div', array('class'=>'category_label')); + $content .= html_writer::link(new moodle_url('/course/category.php', array('id'=>$category->id)), $categoryname, array('class'=>'category_link')); + $content .= html_writer::end_tag('div'); + if ($hassubcategories) { + $content .= html_writer::start_tag('div', array('class'=>'subcategories')); + foreach ($category->categories as $subcategory) { + $content .= $this->course_category_tree_category($subcategory, $depth+1); + } + $content .= html_writer::end_tag('div'); + } + if ($hascourses) { + $content .= html_writer::start_tag('div', array('class'=>'courses')); + $coursecount = 0; + $strinfo = new lang_string('info'); + foreach ($category->courses as $course) { + $classes = array('course'); + $linkclass = 'course_link'; + if (!$course->visible) { + $linkclass .= ' dimmed'; + } + $coursecount ++; + $classes[] = ($coursecount%2)?'odd':'even'; + $content .= html_writer::start_tag('div', array('class'=>join(' ', $classes))); + $content .= html_writer::link(new moodle_url('/course/view.php', array('id'=>$course->id)), format_string($course->fullname), array('class'=>$linkclass)); + $content .= html_writer::start_tag('div', array('class'=>'course_info clearfix')); + + // print enrol info + if ($icons = enrol_get_course_info_icons($course)) { + foreach ($icons as $pix_icon) { + $content .= $this->render($pix_icon); + } + } + + if ($course->summary) { + $url = new moodle_url('/course/info.php', array('id' => $course->id)); + $image = html_writer::empty_tag('img', array('src'=>$this->output->pix_url('i/info'), 'alt'=>$this->strings->summary)); + $content .= $this->action_link($url, $image, new popup_action('click', $url, 'courseinfo'), array('title' => $this->strings->summary)); + } + $content .= html_writer::end_tag('div'); + $content .= html_writer::end_tag('div'); + } + $content .= html_writer::end_tag('div'); + } + $content .= html_writer::end_tag('div'); + return $content; + } + + /** + * Build the HTML for the module chooser javascript popup + * + * @param array $modules A set of modules as returned form @see + * get_module_metadata + * @param object $course The course that will be displayed + * @return string The composed HTML for the module + */ + public function course_modchooser($modules, $course) { + global $OUTPUT; + + // Add the header + $header = html_writer::tag('div', get_string('addresourceoractivity', 'moodle'), + array('class' => 'hd choosertitle')); + + $formcontent = html_writer::start_tag('form', array('action' => new moodle_url('/course/jumpto.php'), + 'id' => 'chooserform', 'method' => 'post')); + $formcontent .= html_writer::start_tag('div', array('id' => 'typeformdiv')); + $formcontent .= html_writer::tag('input', '', array('type' => 'hidden', 'id' => 'course', + 'name' => 'course', 'value' => $course->id)); + $formcontent .= html_writer::tag('input', '', + array('type' => 'hidden', 'class' => 'jump', 'name' => 'jump', 'value' => '')); + $formcontent .= html_writer::tag('input', '', array('type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey())); + $formcontent .= html_writer::end_tag('div'); + + // Put everything into one tag 'options' + $formcontent .= html_writer::start_tag('div', array('class' => 'options')); + $formcontent .= html_writer::tag('div', get_string('selectmoduletoviewhelp', 'moodle'), + array('class' => 'instruction')); + // Put all options into one tag 'alloptions' to allow us to handle scrolling + $formcontent .= html_writer::start_tag('div', array('class' => 'alloptions')); + + // Activities + $activities = array_filter($modules, function($mod) { + return ($mod->archetype !== MOD_ARCHETYPE_RESOURCE && $mod->archetype !== MOD_ARCHETYPE_SYSTEM); + }); + if (count($activities)) { + $formcontent .= $this->course_modchooser_title('activities'); + $formcontent .= $this->course_modchooser_module_types($activities); + } + + // Resources + $resources = array_filter($modules, function($mod) { + return ($mod->archetype === MOD_ARCHETYPE_RESOURCE); + }); + if (count($resources)) { + $formcontent .= $this->course_modchooser_title('resources'); + $formcontent .= $this->course_modchooser_module_types($resources); + } + + $formcontent .= html_writer::end_tag('div'); // modoptions + $formcontent .= html_writer::end_tag('div'); // types + + $formcontent .= html_writer::start_tag('div', array('class' => 'submitbuttons')); + $formcontent .= html_writer::tag('input', '', + array('type' => 'submit', 'name' => 'submitbutton', 'class' => 'submitbutton', 'value' => get_string('add'))); + $formcontent .= html_writer::tag('input', '', + array('type' => 'submit', 'name' => 'addcancel', 'class' => 'addcancel', 'value' => get_string('cancel'))); + $formcontent .= html_writer::end_tag('div'); + $formcontent .= html_writer::end_tag('form'); + + // Wrap the whole form in a div + $formcontent = html_writer::tag('div', $formcontent, array('id' => 'chooseform')); + + // Put all of the content together + $content = $formcontent; + + $content = html_writer::tag('div', $content, array('class' => 'choosercontainer')); + return $header . html_writer::tag('div', $content, array('class' => 'chooserdialoguebody')); + } + + /** + * Build the HTML for a specified set of modules + * + * @param array $modules A set of modules as used by the + * course_modchooser_module function + * @return string The composed HTML for the module + */ + protected function course_modchooser_module_types($modules) { + $return = ''; + foreach ($modules as $module) { + if (!isset($module->types)) { + $return .= $this->course_modchooser_module($module); + } else { + $return .= $this->course_modchooser_module($module, array('nonoption')); + foreach ($module->types as $type) { + $return .= $this->course_modchooser_module($type, array('option', 'subtype')); + } + } + } + return $return; + } + + /** + * Return the HTML for the specified module adding any required classes + * + * @param object $module An object containing the title, and link. An + * icon, and help text may optionally be specified. If the module + * contains subtypes in the types option, then these will also be + * displayed. + * @param array $classes Additional classes to add to the encompassing + * div element + * @return string The composed HTML for the module + */ + protected function course_modchooser_module($module, $classes = array('option')) { + $output = ''; + $output .= html_writer::start_tag('div', array('class' => implode(' ', $classes))); + $output .= html_writer::start_tag('label', array('for' => 'module_' . $module->name)); + if (!isset($module->types)) { + $output .= html_writer::tag('input', '', array('type' => 'radio', + 'name' => 'jumplink', 'id' => 'module_' . $module->name, 'value' => $module->link)); + } + + $output .= html_writer::start_tag('span', array('class' => 'modicon')); + if (isset($module->icon)) { + // Add an icon if we have one + $output .= $module->icon; + } + $output .= html_writer::end_tag('span'); + + $output .= html_writer::tag('span', $module->title, array('class' => 'typename')); + if (!isset($module->help)) { + // Add help if found + $module->help = get_string('nohelpforactivityorresource', 'moodle'); + } + + // Format the help text using markdown with the following options + $options = new stdClass(); + $options->trusted = false; + $options->noclean = false; + $options->smiley = false; + $options->filter = false; + $options->para = true; + $options->newlines = false; + $options->overflowdiv = false; + $module->help = format_text($module->help, FORMAT_MARKDOWN, $options); + $output .= html_writer::tag('span', $module->help, array('class' => 'typesummary')); + $output .= html_writer::end_tag('label'); + $output .= html_writer::end_tag('div'); + + return $output; + } + + protected function course_modchooser_title($title, $identifier = null) { + $module = new stdClass(); + $module->name = $title; + $module->types = array(); + $module->title = get_string($title, $identifier); + $module->help = ''; + return $this->course_modchooser_module($module, array('moduletypetitle')); + } +} diff --git a/report.php b/report.php new file mode 100644 index 0000000..5cf8d7a --- /dev/null +++ b/report.php @@ -0,0 +1,41 @@ +get_record('course', array('id'=>$id), '*', MUST_EXIST); + + $PAGE->set_pagelayout('standard'); + require_login($course); + + $context = context_course::instance($course->id); + require_capability('moodle/site:viewreports', $context); // basic capability for listing of reports + + $strreports = get_string('reports'); + + $PAGE->set_url(new moodle_url('/course/report.php', array('id'=>$id))); + $PAGE->set_title($course->fullname.': '.$strreports); + $PAGE->set_heading($course->fullname.': '.$strreports); + echo $OUTPUT->header(); + + $reports = get_plugin_list('coursereport'); + + foreach ($reports as $report => $reportdirectory) { + $pluginfile = $reportdirectory.'/mod.php'; + if (file_exists($pluginfile)) { + ob_start(); + include($pluginfile); // Fragment for listing + $html = ob_get_contents(); + ob_end_clean(); + // add div only if plugin accessible + if ($html !== '') { + echo '
          '; + echo $html; + echo '
          '; + } + } + } + + echo $OUTPUT->footer(); + diff --git a/report/lib.php b/report/lib.php new file mode 100644 index 0000000..3ed43f7 --- /dev/null +++ b/report/lib.php @@ -0,0 +1,38 @@ +. + +/** + * This file contains functions used by course reports + * + * @since 2.1 + * @package course-report + * @copyright 2011 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +/** + * Return a list of page types + * @param string $pagetype current page type + * @param stdClass $parentcontext Block's parent context + * @param stdClass $currentcontext Current context of block + */ +function coursereport_page_type_list($pagetype, $parentcontext, $currentcontext) { + $array = array( + '*' => get_string('page-x', 'pagetype'), + 'course-report-*' => get_string('page-course-report-x', 'pagetype') + ); + return $array; +} \ No newline at end of file diff --git a/request.php b/request.php new file mode 100644 index 0000000..bf6fefc --- /dev/null +++ b/request.php @@ -0,0 +1,73 @@ +. + +/** + * Allows a user to request a course be created for them. + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/course/request_form.php'); + +$PAGE->set_url('/course/request.php'); + +/// Where we came from. Used in a number of redirects. +$returnurl = $CFG->wwwroot . '/course/index.php'; + + +/// Check permissions. +require_login(); +if (isguestuser()) { + print_error('guestsarenotallowed', '', $returnurl); +} +if (empty($CFG->enablecourserequests)) { + print_error('courserequestdisabled', '', $returnurl); +} +$context = context_system::instance(); +$PAGE->set_context($context); +require_capability('moodle/course:request', $context); + +/// Set up the form. +$data = course_request::prepare(); +$requestform = new course_request_form($CFG->wwwroot . '/course/request.php', compact('editoroptions')); +$requestform->set_data($data); + +$strtitle = get_string('courserequest'); +$PAGE->set_title($strtitle); +$PAGE->set_heading($strtitle); + +/// Standard form processing if statement. +if ($requestform->is_cancelled()){ + redirect($returnurl); + +} else if ($data = $requestform->get_data()) { + $request = course_request::create($data); + + // and redirect back to the course listing. + notice(get_string('courserequestsuccess'), $returnurl); +} + +$PAGE->navbar->add($strtitle); +echo $OUTPUT->header(); +echo $OUTPUT->heading($strtitle); +// Show the request form. +$requestform->display(); +echo $OUTPUT->footer(); diff --git a/request_form.php b/request_form.php new file mode 100644 index 0000000..867a684 --- /dev/null +++ b/request_form.php @@ -0,0 +1,152 @@ +libdir.'/formslib.php'); + +/** + * A form for a user to request a course. + */ +class course_request_form extends moodleform { + function definition() { + global $CFG, $DB, $USER; + + $mform =& $this->_form; + + if ($pending = $DB->get_records('course_request', array('requester' => $USER->id))) { + $mform->addElement('header', 'pendinglist', get_string('coursespending')); + $list = array(); + foreach ($pending as $cp) { + $list[] = format_string($cp->fullname); + } + $list = implode(', ', $list); + $mform->addElement('static', 'pendingcourses', get_string('courses'), $list); + } + + $mform->addElement('header','coursedetails', get_string('courserequestdetails')); + + $mform->addElement('text', 'fullname', get_string('fullnamecourse'), 'maxlength="254" size="50"'); + $mform->addHelpButton('fullname', 'fullnamecourse'); + $mform->addRule('fullname', get_string('missingfullname'), 'required', null, 'client'); + $mform->setType('fullname', PARAM_TEXT); + + $mform->addElement('text', 'shortname', get_string('shortnamecourse'), 'maxlength="100" size="20"'); + $mform->addHelpButton('shortname', 'shortnamecourse'); + $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client'); + $mform->setType('shortname', PARAM_TEXT); + + if (!empty($CFG->requestcategoryselection)) { + $displaylist = array(); + $parentlist = array(); + make_categories_list($displaylist, $parentlist, ''); + $mform->addElement('select', 'category', get_string('category'), $displaylist); + $mform->setDefault('category', $CFG->defaultrequestcategory); + $mform->addHelpButton('category', 'category'); + } + + $mform->addElement('editor', 'summary_editor', get_string('summary'), null, course_request::summary_editor_options()); + $mform->addHelpButton('summary_editor', 'coursesummary'); + $mform->setType('summary_editor', PARAM_RAW); + + $mform->addElement('header','requestreason', get_string('courserequestreason')); + + $mform->addElement('textarea', 'reason', get_string('courserequestsupport'), array('rows'=>'15', 'cols'=>'50')); + $mform->addRule('reason', get_string('missingreqreason'), 'required', null, 'client'); + $mform->setType('reason', PARAM_TEXT); + + $this->add_action_buttons(true, get_string('requestcourse')); + } + + function validation($data, $files) { + global $DB; + + $errors = parent::validation($data, $files); + $foundcourses = null; + $foundreqcourses = null; + + if (!empty($data['shortname'])) { + $foundcourses = $DB->get_records('course', array('shortname'=>$data['shortname'])); + $foundreqcourses = $DB->get_records('course_request', array('shortname'=>$data['shortname'])); + } + if (!empty($foundreqcourses)) { + if (!empty($foundcourses)) { + $foundcourses = array_merge($foundcourses, $foundreqcourses); + } else { + $foundcourses = $foundreqcourses; + } + } + + if (!empty($foundcourses)) { + foreach ($foundcourses as $foundcourse) { + if (!empty($foundcourse->requester)) { + $pending = 1; + $foundcoursenames[] = $foundcourse->fullname.' [*]'; + } else { + $foundcoursenames[] = $foundcourse->fullname; + } + } + $foundcoursenamestring = implode(',', $foundcoursenames); + + $errors['shortname'] = get_string('shortnametaken', '', $foundcoursenamestring); + if (!empty($pending)) { + $errors['shortname'] .= get_string('starpending'); + } + } + + return $errors; + } +} + +/** + * A form for an administrator to reject a course request. + */ +class reject_request_form extends moodleform { + function definition() { + $mform =& $this->_form; + + $mform->addElement('hidden', 'reject', 0); + $mform->setType('reject', PARAM_INT); + + $mform->addElement('header','coursedetails', get_string('coursereasonforrejecting')); + + $mform->addElement('textarea', 'rejectnotice', get_string('coursereasonforrejectingemail'), array('rows'=>'15', 'cols'=>'50')); + $mform->addRule('rejectnotice', get_string('missingreqreason'), 'required', null, 'client'); + $mform->setType('rejectnotice', PARAM_TEXT); + + $this->add_action_buttons(true, get_string('reject')); + } +} + diff --git a/reset.php b/reset.php new file mode 100644 index 0000000..09a440b --- /dev/null +++ b/reset.php @@ -0,0 +1,106 @@ +. + +/** + * The purpose of this feature is to quickly remove all user related data from a course + * in order to make it available for a new semester. This feature can handle the removal + * of general course data like students, teachers, logs, events and groups as well as module + * specific data. Each module must be modified to take advantage of this new feature. + * The feature will also reset the start date of the course if necessary. + * + * @copyright Mark Flach and moodle.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require('../config.php'); +require_once('reset_form.php'); + +$id = required_param('id', PARAM_INT); + +if (!$course = $DB->get_record('course', array('id'=>$id))) { + print_error("invalidcourseid"); +} + +$PAGE->set_url('/course/reset.php', array('id'=>$id)); + +require_login($course); +require_capability('moodle/course:reset', context_course::instance($course->id)); + +$strreset = get_string('reset'); +$strresetcourse = get_string('resetcourse'); +$strremove = get_string('remove'); + +$PAGE->navbar->add($strresetcourse); +$PAGE->set_title($course->fullname.': '.$strresetcourse); +$PAGE->set_heading($course->fullname.': '.$strresetcourse); + +$mform = new course_reset_form(); + +if ($mform->is_cancelled()) { + redirect($CFG->wwwroot.'/course/view.php?id='.$id); + +} else if ($data = $mform->get_data()) { // no magic quotes + + if (isset($data->selectdefault)) { + $_POST = array(); + $mform = new course_reset_form(); + $mform->load_defaults(); + + } else if (isset($data->deselectall)) { + $_POST = array(); + $mform = new course_reset_form(); + + } else { + echo $OUTPUT->header(); + echo $OUTPUT->heading($strresetcourse); + + $data->reset_start_date_old = $course->startdate; + $status = reset_course_userdata($data); + + $data = array();; + foreach ($status as $item) { + $line = array(); + $line[] = $item['component']; + $line[] = $item['item']; + $line[] = ($item['error']===false) ? get_string('ok') : '
          '.$item['error'].'
          '; + $data[] = $line; + } + + $table = new html_table(); + $table->head = array(get_string('resetcomponent'), get_string('resettask'), get_string('resetstatus')); + $table->size = array('20%', '40%', '40%'); + $table->align = array('left', 'left', 'left'); + $table->width = '80%'; + $table->data = $data; + echo html_writer::table($table); + + echo $OUTPUT->continue_button('view.php?id='.$course->id); // Back to course page + echo $OUTPUT->footer(); + exit; + } +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading($strresetcourse); + +echo $OUTPUT->box(get_string('resetinfo')); + +$mform->display(); +echo $OUTPUT->footer(); + + diff --git a/reset_form.php b/reset_form.php new file mode 100644 index 0000000..ea619fe --- /dev/null +++ b/reset_form.php @@ -0,0 +1,135 @@ +libdir.'/formslib.php'; + +class course_reset_form extends moodleform { + function definition (){ + global $CFG, $COURSE, $DB; + + $mform =& $this->_form; + + $mform->addElement('header', 'generalheader', get_string('general')); + + $mform->addElement('date_selector', 'reset_start_date', get_string('startdate'), array('optional'=>true)); + $mform->addHelpButton('reset_start_date', 'startdate'); + $mform->addElement('checkbox', 'reset_events', get_string('deleteevents', 'calendar')); + $mform->addElement('checkbox', 'reset_logs', get_string('deletelogs')); + $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes')); + $mform->addElement('checkbox', 'reset_comments', get_string('deleteallcomments', 'moodle')); + $mform->addElement('checkbox', 'reset_completion', get_string('deletecompletiondata', 'completion')); + $mform->addElement('checkbox', 'delete_blog_associations', get_string('deleteblogassociations', 'blog')); + $mform->addHelpButton('delete_blog_associations', 'deleteblogassociations', 'blog'); + + + $mform->addElement('header', 'rolesheader', get_string('roles')); + + $roles = get_assignable_roles(context_course::instance($COURSE->id)); + $roles[0] = get_string('noroles', 'role'); + $roles = array_reverse($roles, true); + + $mform->addElement('select', 'unenrol_users', get_string('unenrolroleusers', 'enrol'), $roles, array('multiple' => 'multiple')); + $mform->addElement('checkbox', 'reset_roles_overrides', get_string('deletecourseoverrides', 'role')); + $mform->setAdvanced('reset_roles_overrides'); + $mform->addElement('checkbox', 'reset_roles_local', get_string('deletelocalroles', 'role')); + + + $mform->addElement('header', 'gradebookheader', get_string('gradebook', 'grades')); + + $mform->addElement('checkbox', 'reset_gradebook_items', get_string('removeallcourseitems', 'grades')); + $mform->addElement('checkbox', 'reset_gradebook_grades', get_string('removeallcoursegrades', 'grades')); + $mform->disabledIf('reset_gradebook_grades', 'reset_gradebook_items', 'checked'); + + + $mform->addElement('header', 'groupheader', get_string('groups')); + + $mform->addElement('checkbox', 'reset_groups_remove', get_string('deleteallgroups', 'group')); + $mform->setAdvanced('reset_groups_remove'); + $mform->addElement('checkbox', 'reset_groups_members', get_string('removegroupsmembers', 'group')); + $mform->setAdvanced('reset_groups_members'); + $mform->disabledIf('reset_groups_members', 'reset_groups_remove', 'checked'); + + $mform->addElement('checkbox', 'reset_groupings_remove', get_string('deleteallgroupings', 'group')); + $mform->setAdvanced('reset_groupings_remove'); + $mform->addElement('checkbox', 'reset_groupings_members', get_string('removegroupingsmembers', 'group')); + $mform->setAdvanced('reset_groupings_members'); + $mform->disabledIf('reset_groupings_members', 'reset_groupings_remove', 'checked'); + + $unsupported_mods = array(); + if ($allmods = $DB->get_records('modules') ) { + foreach ($allmods as $mod) { + $modname = $mod->name; + $modfile = $CFG->dirroot."/mod/$modname/lib.php"; + $mod_reset_course_form_definition = $modname.'_reset_course_form_definition'; + $mod_reset__userdata = $modname.'_reset_userdata'; + if (file_exists($modfile)) { + if (!$DB->count_records($modname, array('course'=>$COURSE->id))) { + continue; // Skip mods with no instances + } + include_once($modfile); + if (function_exists($mod_reset_course_form_definition)) { + $mod_reset_course_form_definition($mform); + } else if (!function_exists($mod_reset__userdata)) { + $unsupported_mods[] = $mod; + } + } else { + debugging('Missing lib.php in '.$modname.' module'); + } + } + } + // mention unsupported mods + if (!empty($unsupported_mods)) { + $mform->addElement('header', 'unsupportedheader', get_string('resetnotimplemented')); + foreach($unsupported_mods as $mod) { + $mform->addElement('static', 'unsup'.$mod->name, get_string('modulenameplural', $mod->name)); + $mform->setAdvanced('unsup'.$mod->name); + } + } + + $mform->addElement('hidden', 'id', $COURSE->id); + $mform->setType('id', PARAM_INT); + + $buttonarray = array(); + $buttonarray[] = &$mform->createElement('submit', 'submitbutton', get_string('resetcourse')); + $buttonarray[] = &$mform->createElement('submit', 'selectdefault', get_string('selectdefault')); + $buttonarray[] = &$mform->createElement('submit', 'deselectall', get_string('deselectall')); + $buttonarray[] = &$mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('buttonar'); + } + + function load_defaults() { + global $CFG, $COURSE, $DB; + + $mform =& $this->_form; + + $defaults = array ('reset_events'=>1, 'reset_logs'=>1, 'reset_roles_local'=>1, 'reset_gradebook_grades'=>1, 'reset_notes'=>1); + + // Set student as default in unenrol user list, if role with student archetype exist. + if ($studentrole = get_archetype_roles('student')) { + $defaults['unenrol_users'] = array_keys($studentrole); + } + + if ($allmods = $DB->get_records('modules') ) { + foreach ($allmods as $mod) { + $modname = $mod->name; + $modfile = $CFG->dirroot."/mod/$modname/lib.php"; + $mod_reset_course_form_defaults = $modname.'_reset_course_form_defaults'; + if (file_exists($modfile)) { + @include_once($modfile); + if (function_exists($mod_reset_course_form_defaults)) { + if ($moddefs = $mod_reset_course_form_defaults($COURSE)) { + $defaults = $defaults + $moddefs; + } + } + } + } + } + + foreach ($defaults as $element=>$default) { + $mform->setDefault($element, $default); + } + } +} diff --git a/resources.php b/resources.php new file mode 100644 index 0000000..6debe85 --- /dev/null +++ b/resources.php @@ -0,0 +1,143 @@ +. + +/** + * List of all resource type modules in course + * + * @package moodlecore + * @copyright 2009 Petr Skoda (http://skodak.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once("$CFG->libdir/resourcelib.php"); + +$id = required_param('id', PARAM_INT); // course id + +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +$PAGE->set_pagelayout('course'); +require_course_login($course, true); + +// get list of all resource-like modules +$allmodules = $DB->get_records('modules', array('visible'=>1)); +$modules = array(); +foreach ($allmodules as $key=>$module) { + $modname = $module->name; + $libfile = "$CFG->dirroot/mod/$modname/lib.php"; + if (!file_exists($libfile)) { + continue; + } + $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); + if ($archetype != MOD_ARCHETYPE_RESOURCE) { + continue; + } + + $modules[$modname] = get_string('modulename', $modname); + //some hacky nasic logging + add_to_log($course->id, $modname, 'view all', "index.php?id=$course->id", ''); +} + +$strresources = get_string('resources'); +$strsectionname = get_string('sectionname', 'format_'.$course->format); +$strname = get_string('name'); +$strintro = get_string('moduleintro'); +$strlastmodified = get_string('lastmodified'); + +$PAGE->set_url('/course/resources.php', array('id' => $course->id)); +$PAGE->set_title($course->shortname.': '.$strresources); +$PAGE->set_heading($course->fullname); +$PAGE->navbar->add($strresources); +echo $OUTPUT->header(); + +$modinfo = get_fast_modinfo($course); +$usesections = course_format_uses_sections($course->format); +$cms = array(); +$resources = array(); +foreach ($modinfo->cms as $cm) { + if (!$cm->uservisible) { + continue; + } + 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; +} + +// preload instances +foreach ($resources as $modname=>$instances) { + $resources[$modname] = $DB->get_records_list($modname, 'id', $instances, 'id', 'id,name,intro,introformat,timemodified'); +} + +if (!$cms) { + notice(get_string('thereareno', 'moodle', $strresources), "$CFG->wwwroot/course/view.php?id=$course->id"); + exit; +} + +$table = new html_table(); +$table->attributes['class'] = 'generaltable mod_index'; + +if ($usesections) { + $table->head = array ($strsectionname, $strname, $strintro); + $table->align = array ('center', 'left', 'left'); +} else { + $table->head = array ($strlastmodified, $strname, $strintro); + $table->align = array ('left', 'left', 'left'); +} + +$currentsection = ''; +foreach ($cms as $cm) { + if (!isset($resources[$cm->modname][$cm->instance])) { + continue; + } + $resource = $resources[$cm->modname][$cm->instance]; + if ($usesections) { + $printsection = ''; + if ($cm->sectionnum !== $currentsection) { + if ($cm->sectionnum) { + $printsection = get_section_name($course, $cm->sectionnum); + } + if ($currentsection !== '') { + $table->data[] = 'hr'; + } + $currentsection = $cm->sectionnum; + } + } else { + $printsection = ''.userdate($resource->timemodified).""; + } + + $extra = empty($cm->extra) ? '' : $cm->extra; + if (!empty($cm->icon)) { + $icon = ''.get_string('modulename', $cm->modname).' '; + } else { + $icon = ''.get_string('modulename', $cm->modname).' '; + } + + $class = $cm->visible ? '' : 'class="dimmed"'; // hidden modules are dimmed + $table->data[] = array ( + $printsection, + "wwwroot/mod/$cm->modname/view.php?id=$cm->id\">".$icon.format_string($resource->name)."", + format_module_intro('resource', $resource, $cm->id)); +} + +echo html_writer::table($table); + +echo $OUTPUT->footer(); diff --git a/rest.php b/rest.php new file mode 100644 index 0000000..6272903 --- /dev/null +++ b/rest.php @@ -0,0 +1,239 @@ +. + +/** + * Provide interface for topics AJAX course formats + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +if (!defined('AJAX_SCRIPT')) { + define('AJAX_SCRIPT', true); +} +require_once(dirname(__FILE__) . '/../config.php'); +require_once($CFG->dirroot.'/course/lib.php'); + +// Initialise ALL the incoming parameters here, up front. +$courseid = required_param('courseId', PARAM_INT); +$class = required_param('class', PARAM_ALPHA); +$field = optional_param('field', '', PARAM_ALPHA); +$instanceid = optional_param('instanceId', 0, PARAM_INT); +$sectionid = optional_param('sectionId', 0, PARAM_INT); +$beforeid = optional_param('beforeId', 0, PARAM_INT); +$value = optional_param('value', 0, PARAM_INT); +$column = optional_param('column', 0, PARAM_ALPHA); +$id = optional_param('id', 0, PARAM_INT); +$summary = optional_param('summary', '', PARAM_RAW); +$sequence = optional_param('sequence', '', PARAM_SEQUENCE); +$visible = optional_param('visible', 0, PARAM_INT); +$pageaction = optional_param('action', '', PARAM_ALPHA); // Used to simulate a DELETE command +$title = optional_param('title', '', PARAM_TEXT); + +$PAGE->set_url('/course/rest.php', array('courseId'=>$courseid,'class'=>$class)); + +//NOTE: when making any changes here please make sure it is using the same access control as course/mod.php !! + +$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); +// Check user is logged in and set contexts if we are dealing with resource +if (in_array($class, array('resource'))) { + $cm = get_coursemodule_from_id(null, $id, $course->id, false, MUST_EXIST); + require_login($course, false, $cm); + $modcontext = context_module::instance($cm->id); +} else { + require_login($course); +} +$coursecontext = context_course::instance($course->id); +require_sesskey(); + +echo $OUTPUT->header(); // send headers + +// OK, now let's process the parameters and do stuff +// MDL-10221 the DELETE method is not allowed on some web servers, so we simulate it with the action URL param +$requestmethod = $_SERVER['REQUEST_METHOD']; +if ($pageaction == 'DELETE') { + $requestmethod = 'DELETE'; +} + +switch($requestmethod) { + case 'POST': + + switch ($class) { + case 'section': + + if (!$DB->record_exists('course_sections', array('course'=>$course->id, 'section'=>$id))) { + throw new moodle_exception('AJAX commands.php: Bad Section ID '.$id); + } + + switch ($field) { + case 'visible': + require_capability('moodle/course:sectionvisibility', $coursecontext); + $resourcestotoggle = set_section_visible($course->id, $id, $value); + echo json_encode(array('resourcestotoggle' => $resourcestotoggle)); + break; + + case 'move': + require_capability('moodle/course:movesections', $coursecontext); + move_section_to($course, $id, $value); + // See if format wants to do something about it + $response = course_get_format($course)->ajax_section_move(); + if ($response !== null) { + echo json_encode($response); + } + break; + } + break; + + case 'resource': + switch ($field) { + case 'visible': + require_capability('moodle/course:activityvisibility', $modcontext); + set_coursemodule_visible($cm->id, $value); + break; + + case 'groupmode': + require_capability('moodle/course:manageactivities', $modcontext); + set_coursemodule_groupmode($cm->id, $value); + break; + + case 'indent': + require_capability('moodle/course:manageactivities', $modcontext); + $cm->indent = $value; + if ($cm->indent >= 0) { + $DB->update_record('course_modules', $cm); + rebuild_course_cache($cm->course); + } + break; + + case 'move': + require_capability('moodle/course:manageactivities', $modcontext); + if (!$section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>$sectionid))) { + throw new moodle_exception('AJAX commands.php: Bad section ID '.$sectionid); + } + + if ($beforeid > 0){ + $beforemod = get_coursemodule_from_id('', $beforeid, $course->id); + $beforemod = $DB->get_record('course_modules', array('id'=>$beforeid)); + } else { + $beforemod = NULL; + } + + moveto_module($cm, $section, $beforemod); + echo json_encode(array('visible' => $cm->visible)); + break; + case 'gettitle': + require_capability('moodle/course:manageactivities', $modcontext); + $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST); + $module = new stdClass(); + $module->id = $cm->instance; + + // Don't pass edit strings through multilang filters - we need the entire string + echo json_encode(array('instancename' => $cm->name)); + break; + case 'updatetitle': + require_capability('moodle/course:manageactivities', $modcontext); + require_once($CFG->libdir . '/gradelib.php'); + $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST); + $module = new stdClass(); + $module->id = $cm->instance; + + // Escape strings as they would be by mform + if (!empty($CFG->formatstringstriptags)) { + $module->name = clean_param($title, PARAM_TEXT); + } else { + $module->name = clean_param($title, PARAM_CLEANHTML); + } + + if (!empty($module->name)) { + $DB->update_record($cm->modname, $module); + rebuild_course_cache($cm->course); + } else { + $module->name = $cm->name; + } + + // Attempt to update the grade item if relevant + $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance)); + $grademodule->cmidnumber = $cm->idnumber; + $grademodule->modname = $cm->modname; + grade_update_mod_grades($grademodule); + + // We need to return strings after they've been through filters for multilang + $stringoptions = new stdClass; + $stringoptions->context = $coursecontext; + echo json_encode(array('instancename' => html_entity_decode(format_string($module->name, true, $stringoptions)))); + break; + } + break; + + case 'course': + switch($field) { + case 'marker': + require_capability('moodle/course:setcurrentsection', $coursecontext); + course_set_marker($course->id, $value); + break; + } + break; + } + break; + + case 'DELETE': + switch ($class) { + case 'resource': + require_capability('moodle/course:manageactivities', $modcontext); + $modlib = "$CFG->dirroot/mod/$cm->modname/lib.php"; + + if (file_exists($modlib)) { + include_once($modlib); + } else { + throw new moodle_exception("Ajax rest.php: This module is missing mod/$cm->modname/lib.php"); + } + $deleteinstancefunction = $cm->modname."_delete_instance"; + + // Run the module's cleanup funtion. + if (!$deleteinstancefunction($cm->instance)) { + throw new moodle_exception("Ajax rest.php: Could not delete the $cm->modname $cm->name (instance)"); + die; + } + + // remove all module files in case modules forget to do that + $fs = get_file_storage(); + $fs->delete_area_files($modcontext->id); + + if (!delete_course_module($cm->id)) { + throw new moodle_exception("Ajax rest.php: Could not delete the $cm->modname $cm->name (coursemodule)"); + } + // Remove the course_modules entry. + if (!delete_mod_from_section($cm->id, $cm->section)) { + throw new moodle_exception("Ajax rest.php: Could not delete the $cm->modname $cm->name from section"); + } + + // Trigger a mod_deleted event with information about this module. + $eventdata = new stdClass(); + $eventdata->modulename = $cm->modname; + $eventdata->cmid = $cm->id; + $eventdata->courseid = $course->id; + $eventdata->userid = $USER->id; + events_trigger('mod_deleted', $eventdata); + + add_to_log($courseid, "course", "delete mod", + "view.php?id=$courseid", + "$cm->modname $cm->instance", $cm->id); + break; + } + break; +} diff --git a/scales.php b/scales.php new file mode 100644 index 0000000..d212489 --- /dev/null +++ b/scales.php @@ -0,0 +1,143 @@ +. + +/** + * Allows a creator to edit custom scales, and also display help about scales + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @deprecated - TODO remove this file or replace it with an alternative solution for scales overview + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once("../config.php"); +require_once("lib.php"); + +$id = required_param('id', PARAM_INT); // course id +$scaleid = optional_param('scaleid', 0, PARAM_INT); // scale id (show only this one) + +$url = new moodle_url('/course/scales.php', array('id'=>$id)); +if ($scaleid !== 0) { + $url->param('scaleid', $scaleid); +} +$PAGE->set_url($url); + +$context = null; +if ($course = $DB->get_record('course', array('id'=>$id))) { + require_login($course); + $context = context_course::instance($course->id); +} else { + //$id will be 0 for site level scales + require_login(); + $context = context_system::instance(); +} + +$PAGE->set_context($context); +require_capability('moodle/course:viewscales', $context); + +$strscales = get_string("scales"); +$strcustomscales = get_string("scalescustom"); +$strstandardscales = get_string("scalesstandard"); + +$PAGE->set_title($strscales); +if (!empty($course)) { + $PAGE->set_heading($course->fullname); +} else { + $PAGE->set_heading($SITE->fullname); +} +echo $OUTPUT->header(); + +if ($scaleid) { + if ($scale = $DB->get_record("scale", array('id'=>$scaleid))) { + if ($scale->courseid == 0 || $scale->courseid == $course->id) { + + $scalemenu = make_menu_from_list($scale->scale); + + echo $OUTPUT->box_start(); + echo $OUTPUT->heading($scale->name); + echo "
          "; + echo html_writer::label(get_string('scales'), 'scaleunused'. $scaleid, false, array('class' => 'accesshide')); + echo html_writer::select($scalemenu, 'unused', '', array('' => 'choosedots'), array('id' => 'scaleunused'.$scaleid)); + echo "
          "; + echo text_to_html($scale->description); + echo $OUTPUT->box_end(); + echo $OUTPUT->close_window_button(); + echo $OUTPUT->footer(); + exit; + } + } +} + +$systemcontext = context_system::instance(); + +if ($scales = $DB->get_records("scale", array("courseid"=>$course->id), "name ASC")) { + echo $OUTPUT->heading($strcustomscales); + + if (has_capability('moodle/course:managescales', $context)) { + echo "

          ("; + print_string('scalestip2'); + echo ")

          "; + } + + foreach ($scales as $scale) { + + $scale->description = file_rewrite_pluginfile_urls($scale->description, 'pluginfile.php', $systemcontext->id, 'grade', 'scale', $scale->id); + + $scalemenu = make_menu_from_list($scale->scale); + + echo $OUTPUT->box_start(); + echo $OUTPUT->heading($scale->name); + echo "
          "; + echo html_writer::label(get_string('scales'), 'courseunused' . $scale->id, false, array('class' => 'accesshide')); + echo html_writer::select($scalemenu, 'unused', '', array('' => 'choosedots'), array('id' => 'courseunused' . $scale->id)); + echo "
          "; + echo text_to_html($scale->description); + echo $OUTPUT->box_end(); + echo "
          "; + } + +} else { + if (has_capability('moodle/course:managescales', $context)) { + echo "

          ("; + print_string("scalestip2"); + echo ")

          "; + } +} + +if ($scales = $DB->get_records("scale", array("courseid"=>0), "name ASC")) { + echo $OUTPUT->heading($strstandardscales); + foreach ($scales as $scale) { + + $scale->description = file_rewrite_pluginfile_urls($scale->description, 'pluginfile.php', $systemcontext->id, 'grade', 'scale', $scale->id); + + $scalemenu = make_menu_from_list($scale->scale); + + echo $OUTPUT->box_start(); + echo $OUTPUT->heading($scale->name); + echo "
          "; + echo html_writer::label(get_string('scales'), 'sitescale' . $scale->id, false, array('class' => 'accesshide')); + echo html_writer::select($scalemenu, 'unused', '', array('' => 'choosedots'), array('id' => 'sitescale' . $scale->id)); + echo "
          "; + echo text_to_html($scale->description); + echo $OUTPUT->box_end(); + echo "
          "; + } +} + +echo $OUTPUT->close_window_button(); +echo $OUTPUT->footer(); + diff --git a/search.php b/search.php new file mode 100644 index 0000000..721d968 --- /dev/null +++ b/search.php @@ -0,0 +1,435 @@ +. + +/** + * Displays external information about a course + * @package core + * @category course + * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../config.php"); +require_once($CFG->dirroot.'/course/lib.php'); + +$search = optional_param('search', '', PARAM_RAW); // search words +$page = optional_param('page', 0, PARAM_INT); // which page to show +$perpage = optional_param('perpage', 10, PARAM_INT); // how many per page +$moveto = optional_param('moveto', 0, PARAM_INT); // move to category +$edit = optional_param('edit', -1, PARAM_BOOL); +$hide = optional_param('hide', 0, PARAM_INT); +$show = optional_param('show', 0, PARAM_INT); +$blocklist = optional_param('blocklist', 0, PARAM_INT); +$modulelist= optional_param('modulelist', '', PARAM_PLUGIN); + +// List of minimum capabilities which user need to have for editing/moving course +$capabilities = array('moodle/course:create', 'moodle/category:manage'); + +// List of category id's in which current user has course:create and category:manage capability. +$usercatlist = array(); + +// List of parent category id's +$catparentlist = array(); + +// Populate usercatlist with list of category id's with required capabilities. +make_categories_list($usercatlist, $catparentlist, $capabilities); + +$search = trim(strip_tags($search)); // trim & clean raw searched string +if ($search) { + $searchterms = explode(" ", $search); // Search for words independently + foreach ($searchterms as $key => $searchterm) { + if (strlen($searchterm) < 2) { + unset($searchterms[$key]); + } + } + $search = trim(implode(" ", $searchterms)); +} + +$site = get_site(); + +$urlparams = array(); +foreach (array('search', 'page', 'blocklist', 'modulelist', 'edit') as $param) { + if (!empty($$param)) { + $urlparams[$param] = $$param; + } +} +if ($perpage != 10) { + $urlparams['perpage'] = $perpage; +} +$PAGE->set_url('/course/search.php', $urlparams); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('standard'); + +if ($CFG->forcelogin) { + require_login(); +} + +// Editing is possible if user has system or category level create and manage capability +if (can_edit_in_category() || !empty($usercatlist)) { + if ($edit !== -1) { + $USER->editing = $edit; + } + $adminediting = $PAGE->user_is_editing(); + + // Set perpage if user can edit in category + if ($perpage != 99999) { + $perpage = 30; + } +} else { + $adminediting = false; +} + +// Editing functions +if (has_capability('moodle/course:visibility', context_system::instance())) { + // Hide or show a course + if (($hide || $show) && confirm_sesskey()) { + if ($hide) { + $course = $DB->get_record("course", array("id" => $hide)); + $visible = 0; + } else { + $course = $DB->get_record("course", array("id" => $show)); + $visible = 1; + } + if ($course) { + $DB->set_field("course", "visible", $visible, array("id" => $course->id)); + } + } +} + +$displaylist = array(); +$parentlist = array(); +make_categories_list($displaylist, $parentlist); + +$strcourses = new lang_string("courses"); +$strsearch = new lang_string("search"); +$strsearchresults = new lang_string("searchresults"); +$strcategory = new lang_string("category"); +$strselect = new lang_string("select"); +$strselectall = new lang_string("selectall"); +$strdeselectall = new lang_string("deselectall"); +$stredit = new lang_string("edit"); +$strfrontpage = new lang_string('frontpage', 'admin'); +$strnovalidcourses = new lang_string('novalidcourses'); + +if (empty($search) and empty($blocklist) and empty($modulelist) and empty($moveto) and ($edit != -1)) { + $PAGE->navbar->add($strcourses, new moodle_url('/course/index.php')); + $PAGE->navbar->add($strsearch); + $PAGE->set_title("$site->fullname : $strsearch"); + $PAGE->set_heading($site->fullname); + + echo $OUTPUT->header(); + echo $OUTPUT->box_start(); + echo "
          "; + echo "
          "; + print_course_search("", false, "plain"); + echo "

          "; + print_string("searchhelp"); + echo "

          "; + echo "
          "; + echo $OUTPUT->box_end(); + echo $OUTPUT->footer(); + exit; +} + +$courses = array(); +if (!empty($moveto) and $data = data_submitted() and confirm_sesskey()) { // Some courses are being moved + if (!$destcategory = $DB->get_record("course_categories", array("id" => $moveto))) { + print_error('cannotfindcategory', '', '', $moveto); + } + + // User should have manage and create capablity on destination category. + require_capability('moodle/category:manage', context_coursecat::instance($moveto)); + require_capability('moodle/course:create', context_coursecat::instance($moveto)); + + foreach ( $data as $key => $value ) { + if (preg_match('/^c\d+$/', $key)) { + $courseid = substr($key, 1); + // user must have category:manage and course:create capability for the course to be moved. + $coursecontext = context_course::instance($courseid); + foreach ($capabilities as $capability) { + // Require capability here will result in a fatal error should the user not + // have the requried category ensuring that no moves occur if they are + // trying to move multiple courses. + require_capability($capability, $coursecontext); + array_push($courses, $courseid); + } + } + } + move_courses($courses, $moveto); +} + +// get list of courses containing blocks if required +if (!empty($blocklist) and confirm_sesskey()) { + $blockname = $DB->get_field('block', 'name', array('id' => $blocklist)); + $courses = array(); + list($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); + $sql = "SELECT c.* $select FROM {course} c + $join JOIN {block_instances} bi ON bi.parentcontextid = ctx.id + WHERE bi.blockname = ?"; + $courses = $DB->get_records_sql($sql, array($blockname)); + $totalcount = count($courses); + // Keep only chunk of array which you want to display + if ($totalcount > $perpage) { + $courses = array_chunk($courses, $perpage, true); + $courses = $courses[$page]; + } + foreach ($courses as $course) { + $courses[$course->id] = $course; + } +} elseif (!empty($modulelist) and confirm_sesskey()) { // get list of courses containing modules + $modulename = $modulelist; + list($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx'); + $sql = "SELECT c.* $select FROM {course} c $join + WHERE c.id IN (SELECT DISTINCT cc.id FROM {".$modulelist."} module, {course} cc + WHERE module.course = cc.id)"; + $courselist = $DB->get_records_sql($sql); + $courses = array(); + if (!empty($courselist)) { + $firstcourse = $page*$perpage; + $lastcourse = $page*$perpage + $perpage -1; + $i = 0; + foreach ($courselist as $course) { + if ($i >= $firstcourse && $i <= $lastcourse) { + $courses[$course->id] = $course; + } + $i++; + } + } + $totalcount = count($courselist); +} else if (!empty($searchterm)) { + // Donot do search for empty search request. + $courses = get_courses_search($searchterms, "fullname ASC", $page, $perpage, $totalcount); +} + +$searchform = ''; +// Turn editing should be visible if user have system or category level capability +if (!empty($courses) && (can_edit_in_category() || !empty($usercatlist))) { + if ($PAGE->user_is_editing()) { + $string = new lang_string("turneditingoff"); + $edit = "off"; + } else { + $string = new lang_string("turneditingon"); + $edit = "on"; + } + $params = array_merge($urlparams, array('sesskey' => sesskey(), 'edit' => $edit)); + $aurl = new moodle_url("$CFG->wwwroot/course/search.php", $params); + $searchform = $OUTPUT->single_button($aurl, $string, 'get'); +} else { + $searchform = print_course_search($search, true, "navbar"); +} + +$PAGE->navbar->add($strcourses, new moodle_url('/course/index.php')); +$PAGE->navbar->add($strsearch, new moodle_url('/course/search.php')); +if (!empty($search)) { + $PAGE->navbar->add(s($search)); +} +$PAGE->set_title("$site->fullname : $strsearchresults"); +$PAGE->set_heading($site->fullname); +$PAGE->set_button($searchform); + +echo $OUTPUT->header(); + +$lastcategory = -1; +if ($courses) { + echo $OUTPUT->heading("$strsearchresults: $totalcount"); + $encodedsearch = urlencode($search); + + // add the module/block parameter to the paging bar if they exists + $modulelink = ""; + if (!empty($modulelist) and confirm_sesskey()) { + $modulelink = "&modulelist=".$modulelist."&sesskey=".sesskey(); + } else if (!empty($blocklist) and confirm_sesskey()) { + $modulelink = "&blocklist=".$blocklist."&sesskey=".sesskey(); + } + + print_navigation_bar($totalcount, $page, $perpage, $encodedsearch, $modulelink); + + // Show list of courses + if (!$adminediting) { //Not editing mode + foreach ($courses as $course) { + // front page don't belong to any category and block can exist. + if ($course->category > 0) { + $course->summary .= "

          "; + $course->summary .= "$strcategory: category\">"; + $course->summary .= $displaylist[$course->category]; + $course->summary .= "

          "; + } + print_course($course, $search); + echo $OUTPUT->spacer(array('height'=>5, 'width'=>5, 'br'=>true)); // should be done with CSS instead + } + } else { + // Editing mode + echo "
          \n"; + echo "
          \n"; + echo "\n"; + echo "\n"; + echo "
          \n"; + if (!empty($modulelist) and confirm_sesskey()) { + echo "\n"; + } else if (!empty($blocklist) and confirm_sesskey()) { + echo "\n"; + } + echo "\n\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + + foreach ($courses as $course) { + + context_helper::preload_from_record($course); + $coursecontext = context_course::instance($course->id); + + $linkcss = $course->visible ? "" : " class=\"dimmed\" "; + + // are we displaying the front page (courseid=1)? + if ($course->id == 1) { + echo "\n"; + echo "\n"; + + // can't do anything else with the front page + echo " \n"; // category place + echo " \n"; // select place + echo " \n"; // edit place + echo "\n"; + continue; + } + + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n"; + echo "\n\n"; + } + echo "\n\n\n"; + echo "
          $strcourses$strcategory$strselect$stredit
          wwwroot\">$strfrontpage   
          id\">" + . highlight($search, $coursecontext->get_context_name(false)) . "".$displaylist[$course->category]."\n"; + + // If user has all required capabilities to move course then show selectable checkbox + if (has_all_capabilities($capabilities, $coursecontext)) { + echo "id\" />\n"; + } else { + echo "id\" disabled=\"disabled\" />\n"; + } + + echo "\n"; + + // checks whether user can update course settings + if (has_capability('moodle/course:update', $coursecontext)) { + echo "wwwroot/course/edit.php?id=$course->id\">\npix_url('t/edit') . "\" class=\"iconsmall\" alt=\"".get_string("settings")."\" />\n "; + } + + // checks whether user can do role assignment + if (has_capability('moodle/course:enrolreview', $coursecontext)) { + echo''; + echo ''.get_string('enrolledusers', 'enrol').' ' . "\n"; + } + + // checks whether user can delete course + if (has_capability('moodle/course:delete', $coursecontext)) { + echo "id\">\npix_url('t/delete') . "\" class=\"iconsmall\" alt=\"".get_string("delete")."\" />\n "; + } + + // checks whether user can change visibility + if (has_capability('moodle/course:visibility', $coursecontext)) { + if (!empty($course->visible)) { + echo "id&sesskey=".sesskey()."\">\npix_url('t/hide') . "\" class=\"iconsmall\" alt=\"".get_string("hide")."\" />\n "; + } else { + echo "id&sesskey=".sesskey()."\">\npix_url('t/show') . "\" class=\"iconsmall\" alt=\"".get_string("show")."\" />\n "; + } + } + + // checks whether user can do site backup + if (has_capability('moodle/backup:backupcourse', $coursecontext)) { + $backupurl = new moodle_url('/backup/backup.php', array('id' => $course->id)); + echo "\npix_url('t/backup') . "\" class=\"iconsmall\" alt=\"".get_string("backup")."\" />\n "; + } + + // checks whether user can do restore + if (has_capability('moodle/restore:restorecourse', $coursecontext)) { + $restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $coursecontext->id)); + echo "\npix_url('t/restore') . "\" class=\"iconsmall\" alt=\"".get_string("restore")."\" />\n "; + } + + echo "
          \n"; + echo "
          "; + echo "\n"; + echo "\n"; + // Select box should only show categories in which user has min capability to move course. + echo html_writer::label(get_string('moveselectedcoursesto'), 'movetoid', false, array('class' => 'accesshide')); + echo html_writer::select($usercatlist, 'moveto', '', array(''=>get_string('moveselectedcoursesto')), array('id'=>'movetoid', 'class' => 'autosubmit')); + $PAGE->requires->yui_module('moodle-core-formautosubmit', + 'M.core.init_formautosubmit', + array(array('selectid' => 'movetoid', 'nothing' => false)) + ); + echo "
          \n
          "; + + } + + print_navigation_bar($totalcount,$page,$perpage,$encodedsearch,$modulelink); + +} else { + if (!empty($search)) { + echo $OUTPUT->heading(get_string("nocoursesfound",'', s($search))); + } + else { + echo $OUTPUT->heading($strnovalidcourses); + } +} + +echo "

          "; + +print_course_search($search); + +echo $OUTPUT->footer(); + +/** + * Print a list navigation bar + * Display page numbers, and a link for displaying all entries + * @param int $totalcount number of entry to display + * @param int $page page number + * @param int $perpage number of entry per page + * @param string $encodedsearch + * @param string $modulelink module name + */ +function print_navigation_bar($totalcount, $page, $perpage, $encodedsearch, $modulelink) { + global $OUTPUT; + echo $OUTPUT->paging_bar($totalcount, $page, $perpage, "search.php?search=$encodedsearch".$modulelink."&perpage=$perpage"); + + // display + if ($perpage != 99999 && $totalcount > $perpage) { + echo "

          "; + echo "".get_string("showall", "", $totalcount).""; + echo "

          "; + } else if ($perpage === 99999) { + $defaultperpage = 10; + // If user has course:create or category:manage capability the show 30 records. + $capabilities = array('moodle/course:create', 'moodle/category:manage'); + if (has_any_capability($capabilities, context_system::instance())) { + $defaultperpage = 30; + } + + echo "

          "; + echo "".get_string("showperpage", "", $defaultperpage).""; + echo "

          "; + } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..e69de29 diff --git a/switchrole.php b/switchrole.php new file mode 100644 index 0000000..2b756b6 --- /dev/null +++ b/switchrole.php @@ -0,0 +1,87 @@ +. + +/** + * The purpose of this file is to allow the user to switch roles and be redirected + * back to the page that they were on. + * + * This functionality is also supported in {@link /course/view.php} in order to comply + * with backwards compatibility + * The reason that we created this file was so that user didn't get redirected back + * to the course view page only to be redirected again. + * + * @since 2.0 + * @package course + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->dirroot.'/course/lib.php'); + +$id = required_param('id', PARAM_INT); +$switchrole = optional_param('switchrole',-1, PARAM_INT); +$returnurl = optional_param('returnurl', false, PARAM_LOCALURL); + +$PAGE->set_url('/course/switchrole.php', array('id'=>$id)); + +if (!confirm_sesskey()) { + print_error('confirmsesskeybad', 'error'); +} + +if (! ($course = $DB->get_record('course', array('id'=>$id)))) { + print_error('invalidcourseid', 'error'); +} + +$context = context_course::instance($course->id); + +// Remove any switched roles before checking login +if ($switchrole == 0) { + role_switch($switchrole, $context); +} +require_login($course); + +// Switchrole - sanity check in cost-order... +if ($switchrole > 0 && has_capability('moodle/role:switchroles', $context)) { + // is this role assignable in this context? + // inquiring minds want to know... + $aroles = get_switchable_roles($context); + if (is_array($aroles) && isset($aroles[$switchrole])) { + role_switch($switchrole, $context); + // Double check that this role is allowed here + require_login($course); + } +} + +// TODO: Using SESSION->returnurl is deprecated and should be removed in the future. +// Till then this code remains to support any external applications calling this script. +if (!empty($returnurl) && is_numeric($returnurl)) { + $returnurl = false; + if (!empty($SESSION->returnurl) && strpos($SESSION->returnurl, 'moodle_url')!==false) { + debugging('Code calling switchrole should be passing a URL as a param.', DEBUG_DEVELOPER); + $returnurl = @unserialize($SESSION->returnurl); + if (!($returnurl instanceof moodle_url)) { + $returnurl = false; + } + } +} + +if ($returnurl === false) { + $returnurl = new moodle_url('/course/view.php', array('id' => $course->id)); +} + +redirect($returnurl); diff --git a/tests/courselib_test.php b/tests/courselib_test.php new file mode 100644 index 0000000..a59351b --- /dev/null +++ b/tests/courselib_test.php @@ -0,0 +1,438 @@ +. + +/** + * Course related unit tests + * + * @package core + * @category phpunit + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/course/lib.php'); + +class courselib_testcase extends advanced_testcase { + + public function test_create_course() { + global $DB; + $this->resetAfterTest(true); + $defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); + + $course = new stdClass(); + $course->fullname = 'Apu loves Unit Təsts'; + $course->shortname = 'Spread the lÅ­ve'; + $course->idnumber = '123'; + $course->summary = 'Awesome!'; + $course->summaryformat = FORMAT_PLAIN; + $course->format = 'topics'; + $course->newsitems = 0; + $course->numsections = 5; + $course->category = $defaultcategory; + + $created = create_course($course); + $context = context_course::instance($created->id); + + // Compare original and created. + $original = (array) $course; + $this->assertEquals($original, array_intersect_key((array) $created, $original)); + + // Ensure default section is created. + $sectioncreated = $DB->record_exists('course_sections', array('course' => $created->id, 'section' => 0)); + $this->assertTrue($sectioncreated); + + // Ensure blocks have been associated to the course. + $blockcount = $DB->count_records('block_instances', array('parentcontextid' => $context->id)); + $this->assertGreaterThan(0, $blockcount); + } + + public function test_create_course_with_generator() { + global $DB; + $this->resetAfterTest(true); + $course = $this->getDataGenerator()->create_course(); + + // Ensure default section is created. + $sectioncreated = $DB->record_exists('course_sections', array('course' => $course->id, 'section' => 0)); + $this->assertTrue($sectioncreated); + } + + public function test_create_course_sections() { + global $DB; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course( + array('shortname' => 'GrowingCourse', + 'fullname' => 'Growing Course', + 'numsections' => 5), + array('createsections' => true)); + + // Ensure all 6 (0-5) sections were created and modinfo/sectioninfo cache works properly + $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all()); + $this->assertEquals(range(0, $course->numsections), $sectionscreated); + + // this will do nothing, section already exists + $this->assertFalse(course_create_sections_if_missing($course, $course->numsections)); + + // this will create new section + $this->assertTrue(course_create_sections_if_missing($course, $course->numsections + 1)); + + // Ensure all 7 (0-6) sections were created and modinfo/sectioninfo cache works properly + $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all()); + $this->assertEquals(range(0, $course->numsections + 1), $sectionscreated); + } + + public function test_reorder_sections() { + global $DB; + $this->resetAfterTest(true); + + $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true)); + $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true)); + $oldsections = array(); + $sections = array(); + foreach ($DB->get_records('course_sections', array('course'=>$course->id), 'id') as $section) { + $oldsections[$section->section] = $section->id; + $sections[$section->id] = $section->section; + } + ksort($oldsections); + + $neworder = reorder_sections($sections, 2, 4); + $neworder = array_keys($neworder); + $this->assertEquals($oldsections[0], $neworder[0]); + $this->assertEquals($oldsections[1], $neworder[1]); + $this->assertEquals($oldsections[2], $neworder[4]); + $this->assertEquals($oldsections[3], $neworder[2]); + $this->assertEquals($oldsections[4], $neworder[3]); + $this->assertEquals($oldsections[5], $neworder[5]); + $this->assertEquals($oldsections[6], $neworder[6]); + + $neworder = reorder_sections($sections, 4, 2); + $neworder = array_keys($neworder); + $this->assertEquals($oldsections[0], $neworder[0]); + $this->assertEquals($oldsections[1], $neworder[1]); + $this->assertEquals($oldsections[2], $neworder[3]); + $this->assertEquals($oldsections[3], $neworder[4]); + $this->assertEquals($oldsections[4], $neworder[2]); + $this->assertEquals($oldsections[5], $neworder[5]); + $this->assertEquals($oldsections[6], $neworder[6]); + + $neworder = reorder_sections(1, 2, 4); + $this->assertFalse($neworder); + } + + public function test_move_section_down() { + global $DB; + $this->resetAfterTest(true); + + $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true)); + $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true)); + $oldsections = array(); + foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) { + $oldsections[$section->section] = $section->id; + } + ksort($oldsections); + + // Test move section down.. + move_section_to($course, 2, 4); + $sections = array(); + foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) { + $sections[$section->section] = $section->id; + } + ksort($sections); + + $this->assertEquals($oldsections[0], $sections[0]); + $this->assertEquals($oldsections[1], $sections[1]); + $this->assertEquals($oldsections[2], $sections[4]); + $this->assertEquals($oldsections[3], $sections[2]); + $this->assertEquals($oldsections[4], $sections[3]); + $this->assertEquals($oldsections[5], $sections[5]); + $this->assertEquals($oldsections[6], $sections[6]); + } + + public function test_move_section_up() { + global $DB; + $this->resetAfterTest(true); + + $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true)); + $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true)); + $oldsections = array(); + foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) { + $oldsections[$section->section] = $section->id; + } + ksort($oldsections); + + // Test move section up.. + move_section_to($course, 6, 4); + $sections = array(); + foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) { + $sections[$section->section] = $section->id; + } + ksort($sections); + + $this->assertEquals($oldsections[0], $sections[0]); + $this->assertEquals($oldsections[1], $sections[1]); + $this->assertEquals($oldsections[2], $sections[2]); + $this->assertEquals($oldsections[3], $sections[3]); + $this->assertEquals($oldsections[4], $sections[5]); + $this->assertEquals($oldsections[5], $sections[6]); + $this->assertEquals($oldsections[6], $sections[4]); + } + + public function test_move_section_marker() { + global $DB; + $this->resetAfterTest(true); + + $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true)); + $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true)); + + // Set course marker to the section we are going to move.. + course_set_marker($course->id, 2); + // Verify that the course marker is set correctly. + $course = $DB->get_record('course', array('id' => $course->id)); + $this->assertEquals(2, $course->marker); + + // Test move the marked section down.. + move_section_to($course, 2, 4); + + // Verify that the coruse marker has been moved along with the section.. + $course = $DB->get_record('course', array('id' => $course->id)); + $this->assertEquals(4, $course->marker); + + // Test move the marked section up.. + move_section_to($course, 4, 3); + + // Verify that the course marker has been moved along with the section.. + $course = $DB->get_record('course', array('id' => $course->id)); + $this->assertEquals(3, $course->marker); + + // Test moving a non-marked section above the marked section.. + move_section_to($course, 4, 2); + + // Verify that the course marker has been moved down to accomodate.. + $course = $DB->get_record('course', array('id' => $course->id)); + $this->assertEquals(4, $course->marker); + + // Test moving a non-marked section below the marked section.. + move_section_to($course, 3, 6); + + // Verify that the course marker has been up to accomodate.. + $course = $DB->get_record('course', array('id' => $course->id)); + $this->assertEquals(3, $course->marker); + } + + public function test_get_course_display_name_for_list() { + global $CFG; + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(array('shortname' => 'FROG101', 'fullname' => 'Introduction to pond life')); + + $CFG->courselistshortnames = 0; + $this->assertEquals('Introduction to pond life', get_course_display_name_for_list($course)); + + $CFG->courselistshortnames = 1; + $this->assertEquals('FROG101 Introduction to pond life', get_course_display_name_for_list($course)); + } + + public function test_create_course_category() { + global $CFG, $DB; + $this->resetAfterTest(true); + + // Create the category + $data = new stdClass(); + $data->name = 'aaa'; + $data->description = 'aaa'; + $data->idnumber = ''; + + $category1 = create_course_category($data); + + // Initially confirm that base data was inserted correctly + $this->assertEquals($data->name, $category1->name); + $this->assertEquals($data->description, $category1->description); + $this->assertEquals($data->idnumber, $category1->idnumber); + + // sortorder should be blank initially + $this->assertEmpty($category1->sortorder); + + // Calling fix_course_sortorder() should provide a new sortorder + fix_course_sortorder(); + $category1 = $DB->get_record('course_categories', array('id' => $category1->id)); + + $this->assertGreaterThanOrEqual(1, $category1->sortorder); + + // Create two more categories and test the sortorder worked correctly + $data->name = 'ccc'; + $category2 = create_course_category($data); + $this->assertEmpty($category2->sortorder); + + $data->name = 'bbb'; + $category3 = create_course_category($data); + $this->assertEmpty($category3->sortorder); + + // Calling fix_course_sortorder() should provide a new sortorder to give category1, + // category2, category3. New course categories are ordered by id not name + fix_course_sortorder(); + + $category1 = $DB->get_record('course_categories', array('id' => $category1->id)); + $category2 = $DB->get_record('course_categories', array('id' => $category2->id)); + $category3 = $DB->get_record('course_categories', array('id' => $category3->id)); + + $this->assertGreaterThanOrEqual($category1->sortorder, $category2->sortorder); + $this->assertGreaterThanOrEqual($category2->sortorder, $category3->sortorder); + $this->assertGreaterThanOrEqual($category1->sortorder, $category3->sortorder); + } + + public function test_move_module_in_course() { + $this->resetAfterTest(true); + // Setup fixture + $course = $this->getDataGenerator()->create_course(array('numsections'=>5)); + $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id)); + + $cms = get_fast_modinfo($course)->get_cms(); + $cm = reset($cms); + + course_create_sections_if_missing($course, 3); + $section3 = get_fast_modinfo($course)->get_section_info(3); + + moveto_module($cm, $section3); + + $modinfo = get_fast_modinfo($course); + $this->assertTrue(empty($modinfo->sections[0])); + $this->assertFalse(empty($modinfo->sections[3])); + } + + public function test_module_visibility() { + $this->setAdminUser(); + $this->resetAfterTest(true); + + // Create course and modules. + $course = $this->getDataGenerator()->create_course(array('numsections' => 5)); + $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id)); + $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), 'course' => $course->id)); + $modules = compact('forum', 'assign'); + + // Hiding the modules. + foreach ($modules as $mod) { + set_coursemodule_visible($mod->cmid, 0); + $this->check_module_visibility($mod, 0, 0); + } + + // Showing the modules. + foreach ($modules as $mod) { + set_coursemodule_visible($mod->cmid, 1); + $this->check_module_visibility($mod, 1, 1); + } + } + + public function test_section_visibility() { + $this->setAdminUser(); + $this->resetAfterTest(true); + + // Create course. + $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true)); + + // Testing an empty section. + $sectionnumber = 1; + set_section_visible($course->id, $sectionnumber, 0); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 0); + set_section_visible($course->id, $sectionnumber, 1); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 1); + + // Testing a section with visible modules. + $sectionnumber = 2; + $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('section' => $sectionnumber)); + $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), + 'course' => $course->id), array('section' => $sectionnumber)); + $modules = compact('forum', 'assign'); + set_section_visible($course->id, $sectionnumber, 0); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 0); + foreach ($modules as $mod) { + $this->check_module_visibility($mod, 0, 1); + } + set_section_visible($course->id, $sectionnumber, 1); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 1); + foreach ($modules as $mod) { + $this->check_module_visibility($mod, 1, 1); + } + + // Testing a section with hidden modules, which should stay hidden. + $sectionnumber = 3; + $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('section' => $sectionnumber)); + $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), + 'course' => $course->id), array('section' => $sectionnumber)); + $modules = compact('forum', 'assign'); + foreach ($modules as $mod) { + set_coursemodule_visible($mod->cmid, 0); + $this->check_module_visibility($mod, 0, 0); + } + set_section_visible($course->id, $sectionnumber, 0); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 0); + foreach ($modules as $mod) { + $this->check_module_visibility($mod, 0, 0); + } + set_section_visible($course->id, $sectionnumber, 1); + $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber); + $this->assertEquals($section_info->visible, 1); + foreach ($modules as $mod) { + $this->check_module_visibility($mod, 0, 0); + } + } + + /** + * Helper function to assert that a module has correctly been made visible, or hidden. + * + * @param stdClass $mod module information + * @param int $visibility the current state of the module + * @param int $visibleold the current state of the visibleold property + * @return void + */ + public function check_module_visibility($mod, $visibility, $visibleold) { + global $DB; + $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid); + $this->assertEquals($visibility, $cm->visible); + $this->assertEquals($visibleold, $cm->visibleold); + + // Check the module grade items. + $grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $cm->modname, + 'iteminstance' => $cm->instance, 'courseid' => $cm->course)); + if ($grade_items) { + foreach ($grade_items as $grade_item) { + if ($visibility) { + $this->assertFalse($grade_item->is_hidden(), "$cm->modname grade_item not visible"); + } else { + $this->assertTrue($grade_item->is_hidden(), "$cm->modname grade_item not hidden"); + } + } + } + + // Check the events visibility. + if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $cm->modname))) { + foreach ($events as $event) { + $calevent = new calendar_event($event); + $this->assertEquals($visibility, $calevent->visible, "$cm->modname calendar_event visibility"); + } + } + } + +} diff --git a/tests/courserequest_test.php b/tests/courserequest_test.php new file mode 100644 index 0000000..d376772 --- /dev/null +++ b/tests/courserequest_test.php @@ -0,0 +1,148 @@ +. + +/** + * Course request related unit tests + * + * @package core + * @category phpunit + * @copyright 2012 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot.'/course/lib.php'); + +class courserequest_testcase extends advanced_testcase { + + public function test_create_request() { + global $DB, $USER; + $this->resetAfterTest(true); + + $defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); + set_config('enablecourserequests', 1); + set_config('requestcategoryselection', 0); + set_config('defaultrequestcategory', $defaultcategory); + + // Create some categories. + $cat1 = $this->getDataGenerator()->create_category(); + $cat2 = $this->getDataGenerator()->create_category(); + $cat3 = $this->getDataGenerator()->create_category(); + + // Basic course request. + $data = new stdClass(); + $data->fullname = 'Həllo World!'; + $data->shortname = 'Hi th€re!'; + $data->summary_editor['text'] = 'Lorem Ipsum ©'; + $data->summary_editor['format'] = FORMAT_HTML; + $data->reason = 'Because PHP Unit is cool.'; + $cr = course_request::create($data); + + $this->assertEquals($data->fullname, $cr->fullname); + $this->assertEquals($data->shortname, $cr->shortname); + $this->assertEquals($data->summary_editor['text'], $cr->summary); + $this->assertEquals($data->summary_editor['format'], $cr->summaryformat); + $this->assertEquals($data->reason, $cr->reason); + $this->assertEquals($USER->id, $cr->requester); + $this->assertEquals($defaultcategory, $cr->category); + + // Request with category but category selection not allowed. + set_config('defaultrequestcategory', $cat2->id); + $data->category = $cat1->id; + $cr = course_request::create($data); + $this->assertEquals($cat2->id, $cr->category); + + // Request with category different than default and category selection allowed. + set_config('defaultrequestcategory', $cat3->id); + set_config('requestcategoryselection', 1); + $data->category = $cat1->id; + $cr = course_request::create($data); + $this->assertEquals($cat1->id, $cr->category); + } + + public function test_approve_request() { + global $DB; + $this->resetAfterTest(true); + $this->preventResetByRollback(); + + $this->setAdminUser(); + $defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); + set_config('enablecourserequests', 1); + set_config('requestcategoryselection', 0); + set_config('defaultrequestcategory', $defaultcategory); + + // Create some categories. + $cat1 = $this->getDataGenerator()->create_category(); + $cat2 = $this->getDataGenerator()->create_category(); + + $data = new stdClass(); + $data->fullname = 'Həllo World!'; + $data->shortname = 'Hi th€re!'; + $data->summary_editor['text'] = 'Lorem Ipsum ©'; + $data->summary_editor['format'] = FORMAT_HTML; + $data->reason = 'Because PHP Unit is cool.'; + + // Test without category. + $cr = course_request::create($data); + $id = $cr->approve(); + $this->assertDebuggingCalled(); // Caused by sending of message. + $course = $DB->get_record('course', array('id' => $id)); + $this->assertEquals($data->fullname, $course->fullname); + $this->assertEquals($data->shortname, $course->shortname); + $this->assertEquals($data->summary_editor['text'], $course->summary); + $this->assertEquals($data->summary_editor['format'], $course->summaryformat); + $this->assertEquals(1, $course->requested); + $this->assertEquals($defaultcategory, $course->category); + + // Test with category. + set_config('requestcategoryselection', 1); + set_config('defaultrequestcategory', $cat2->id); + $data->shortname .= ' 2nd'; + $data->category = $cat1->id; + $cr = course_request::create($data); + $id = $cr->approve(); + $this->assertDebuggingCalled(); // Caused by sending of message. + $course = $DB->get_record('course', array('id' => $id)); + $this->assertEquals($data->category, $course->category); + } + + public function test_reject_request() { + global $DB; + $this->resetAfterTest(true); + $this->preventResetByRollback(); + $this->setAdminUser(); + set_config('enablecourserequests', 1); + set_config('requestcategoryselection', 0); + set_config('defaultrequestcategory', $DB->get_field_select('course_categories', "MIN(id)", "parent=0")); + + $data = new stdClass(); + $data->fullname = 'Həllo World!'; + $data->shortname = 'Hi th€re!'; + $data->summary_editor['text'] = 'Lorem Ipsum ©'; + $data->summary_editor['format'] = FORMAT_HTML; + $data->reason = 'Because PHP Unit is cool.'; + + $cr = course_request::create($data); + $this->assertTrue($DB->record_exists('course_request', array('id' => $cr->id))); + $cr->reject('Sorry!'); + $this->assertDebuggingCalled(); // Caused by sending of message. + $this->assertFalse($DB->record_exists('course_request', array('id' => $cr->id))); + } + +} diff --git a/tests/externallib_test.php b/tests/externallib_test.php new file mode 100644 index 0000000..d6f8fa9 --- /dev/null +++ b/tests/externallib_test.php @@ -0,0 +1,640 @@ +. + +/** + * External course functions unit tests + * + * @package core_course + * @category external + * @copyright 2012 Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * External course functions unit tests + * + * @package core_course + * @category external + * @copyright 2012 Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_course_external_testcase extends externallib_advanced_testcase { + + /** + * Tests set up + */ + protected function setUp() { + global $CFG; + require_once($CFG->dirroot . '/course/externallib.php'); + } + + /** + * Test create_categories + */ + public function test_create_categories() { + + global $DB; + + $this->resetAfterTest(true); + + // Set the required capabilities by the external function + $contextid = context_system::instance()->id; + $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); + + // Create base categories. + $category1 = new stdClass(); + $category1->name = 'Root Test Category 1'; + $category2 = new stdClass(); + $category2->name = 'Root Test Category 2'; + $category2->idnumber = 'rootcattest2'; + $category2->desc = 'Description for root test category 1'; + $category2->theme = 'base'; + $categories = array( + array('name' => $category1->name, 'parent' => 0), + array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber, + 'description' => $category2->desc, 'theme' => $category2->theme) + ); + + $createdcats = core_course_external::create_categories($categories); + + // We need to execute the return values cleaning process to simulate the web service server. + $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats); + + // Initially confirm that base data was inserted correctly. + $this->assertEquals($category1->name, $createdcats[0]['name']); + $this->assertEquals($category2->name, $createdcats[1]['name']); + + // Save the ids. + $category1->id = $createdcats[0]['id']; + $category2->id = $createdcats[1]['id']; + + // Create on sub category. + $category3 = new stdClass(); + $category3->name = 'Sub Root Test Category 3'; + $subcategories = array( + array('name' => $category3->name, 'parent' => $category1->id) + ); + + $createdsubcats = core_course_external::create_categories($subcategories); + + // We need to execute the return values cleaning process to simulate the web service server. + $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats); + + // Confirm that sub categories were inserted correctly. + $this->assertEquals($category3->name, $createdsubcats[0]['name']); + + // Save the ids. + $category3->id = $createdsubcats[0]['id']; + + // Calling the ws function should provide a new sortorder to give category1, + // category2, category3. New course categories are ordered by id not name. + $category1 = $DB->get_record('course_categories', array('id' => $category1->id)); + $category2 = $DB->get_record('course_categories', array('id' => $category2->id)); + $category3 = $DB->get_record('course_categories', array('id' => $category3->id)); + + $this->assertGreaterThanOrEqual($category1->sortorder, $category3->sortorder); + $this->assertGreaterThanOrEqual($category2->sortorder, $category3->sortorder); + + // Call without required capability + $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); + $this->setExpectedException('required_capability_exception'); + $createdsubcats = core_course_external::create_categories($subcategories); + + } + + /** + * Test delete categories + */ + public function test_delete_categories() { + global $DB; + + $this->resetAfterTest(true); + + // Set the required capabilities by the external function + $contextid = context_system::instance()->id; + $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); + + $category1 = self::getDataGenerator()->create_category(); + $category2 = self::getDataGenerator()->create_category( + array('parent' => $category1->id)); + $category3 = self::getDataGenerator()->create_category(); + $category4 = self::getDataGenerator()->create_category( + array('parent' => $category3->id)); + $category5 = self::getDataGenerator()->create_category( + array('parent' => $category4->id)); + + //delete category 1 and 2 + delete category 4, category 5 moved under category 3 + core_course_external::delete_categories(array( + array('id' => $category1->id, 'recursive' => 1), + array('id' => $category4->id) + )); + + //check $category 1 and 2 are deleted + $notdeletedcount = $DB->count_records_select('course_categories', + 'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')'); + $this->assertEquals(0, $notdeletedcount); + + //check that $category5 as $category3 for parent + $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id)); + $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id); + + // Call without required capability + $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); + $this->setExpectedException('required_capability_exception'); + $createdsubcats = core_course_external::delete_categories( + array(array('id' => $category3->id))); + } + + /** + * Test get categories + */ + public function test_get_categories() { + global $DB; + + $this->resetAfterTest(true); + + $generatedcats = array(); + $category1data['idnumber'] = 'idnumbercat1'; + $category1data['name'] = 'Category 1 for PHPunit test'; + $category1data['description'] = 'Category 1 description'; + $category1data['descriptionformat'] = FORMAT_MOODLE; + $category1 = self::getDataGenerator()->create_category($category1data); + $generatedcats[$category1->id] = $category1; + $category2 = self::getDataGenerator()->create_category( + array('parent' => $category1->id)); + $generatedcats[$category2->id] = $category2; + $category6 = self::getDataGenerator()->create_category( + array('parent' => $category1->id, 'visible' => 0)); + $generatedcats[$category6->id] = $category6; + $category3 = self::getDataGenerator()->create_category(); + $generatedcats[$category3->id] = $category3; + $category4 = self::getDataGenerator()->create_category( + array('parent' => $category3->id)); + $generatedcats[$category4->id] = $category4; + $category5 = self::getDataGenerator()->create_category( + array('parent' => $category4->id)); + $generatedcats[$category5->id] = $category5; + + // Set the required capabilities by the external function. + $context = context_system::instance(); + $roleid = $this->assignUserCapability('moodle/category:manage', $context->id); + + // Retrieve category1 + sub-categories except not visible ones + $categories = core_course_external::get_categories(array( + array('key' => 'id', 'value' => $category1->id), + array('key' => 'visible', 'value' => 1)), 1); + + // We need to execute the return values cleaning process to simulate the web service server. + $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); + + // Check we retrieve the good total number of categories. + $this->assertEquals(2, count($categories)); + + // Check the return values + foreach ($categories as $category) { + $generatedcat = $generatedcats[$category['id']]; + $this->assertEquals($category['idnumber'], $generatedcat->idnumber); + $this->assertEquals($category['name'], $generatedcat->name); + $this->assertEquals($category['description'], $generatedcat->description); + $this->assertEquals($category['descriptionformat'], FORMAT_HTML); + } + + // Check different params. + $categories = core_course_external::get_categories(array( + array('key' => 'id', 'value' => $category1->id), + array('key' => 'idnumber', 'value' => $category1->idnumber), + array('key' => 'visible', 'value' => 1)), 0); + + // We need to execute the return values cleaning process to simulate the web service server. + $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); + + $this->assertEquals(1, count($categories)); + + // Retrieve categories from parent. + $categories = core_course_external::get_categories(array( + array('key' => 'parent', 'value' => $category3->id)), 1); + $this->assertEquals(2, count($categories)); + + // Retrieve all categories. + $categories = core_course_external::get_categories(); + + // We need to execute the return values cleaning process to simulate the web service server. + $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); + + $this->assertEquals($DB->count_records('course_categories'), count($categories)); + + // Call without required capability (it will fail cause of the search on idnumber). + $this->unassignUserCapability('moodle/category:manage', $context->id, $roleid); + $this->setExpectedException('moodle_exception'); + $categories = core_course_external::get_categories(array( + array('key' => 'id', 'value' => $category1->id), + array('key' => 'idnumber', 'value' => $category1->idnumber), + array('key' => 'visible', 'value' => 1)), 0); + } + + /** + * Test update_categories + */ + public function test_update_categories() { + global $DB; + + $this->resetAfterTest(true); + + // Set the required capabilities by the external function + $contextid = context_system::instance()->id; + $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); + + // Create base categories. + $category1data['idnumber'] = 'idnumbercat1'; + $category1data['name'] = 'Category 1 for PHPunit test'; + $category1data['description'] = 'Category 1 description'; + $category1data['descriptionformat'] = FORMAT_MOODLE; + $category1 = self::getDataGenerator()->create_category($category1data); + $category2 = self::getDataGenerator()->create_category( + array('parent' => $category1->id)); + $category3 = self::getDataGenerator()->create_category(); + $category4 = self::getDataGenerator()->create_category( + array('parent' => $category3->id)); + $category5 = self::getDataGenerator()->create_category( + array('parent' => $category4->id)); + + // We update all category1 attribut. + // Then we move cat4 and cat5 parent: cat3 => cat1 + $categories = array( + array('id' => $category1->id, + 'name' => $category1->name . '_updated', + 'idnumber' => $category1->idnumber . '_updated', + 'description' => $category1->description . '_updated', + 'descriptionformat' => FORMAT_HTML, + 'theme' => $category1->theme), + array('id' => $category4->id, 'parent' => $category1->id)); + + core_course_external::update_categories($categories); + + // Check the values were updated. + $dbcategories = $DB->get_records_select('course_categories', + 'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id + . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')'); + $this->assertEquals($category1->name . '_updated', + $dbcategories[$category1->id]->name); + $this->assertEquals($category1->idnumber . '_updated', + $dbcategories[$category1->id]->idnumber); + $this->assertEquals($category1->description . '_updated', + $dbcategories[$category1->id]->description); + $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat); + + // Check that category4 and category5 have been properly moved. + $this->assertEquals('/' . $category1->id . '/' . $category4->id, + $dbcategories[$category4->id]->path); + $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id, + $dbcategories[$category5->id]->path); + + // Call without required capability. + $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); + $this->setExpectedException('required_capability_exception'); + core_course_external::update_categories($categories); + } + + /** + * Test create_courses + */ + public function test_create_courses() { + global $DB; + + $this->resetAfterTest(true); + + // Enable course completion. + set_config('enablecompletion', 1); + + // Set the required capabilities by the external function + $contextid = context_system::instance()->id; + $roleid = $this->assignUserCapability('moodle/course:create', $contextid); + $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid); + + $category = self::getDataGenerator()->create_category(); + + // Create base categories. + $course1['fullname'] = 'Test course 1'; + $course1['shortname'] = 'Testcourse1'; + $course1['categoryid'] = $category->id; + $course2['fullname'] = 'Test course 2'; + $course2['shortname'] = 'Testcourse2'; + $course2['categoryid'] = $category->id; + $course2['idnumber'] = 'testcourse2idnumber'; + $course2['summary'] = 'Description for course 2'; + $course2['summaryformat'] = FORMAT_MOODLE; + $course2['format'] = 'weeks'; + $course2['showgrades'] = 1; + $course2['newsitems'] = 3; + $course2['startdate'] = 1420092000; // 01/01/2015 + $course2['numsections'] = 4; + $course2['maxbytes'] = 100000; + $course2['showreports'] = 1; + $course2['visible'] = 0; + $course2['hiddensections'] = 0; + $course2['groupmode'] = 0; + $course2['groupmodeforce'] = 0; + $course2['defaultgroupingid'] = 0; + $course2['enablecompletion'] = 1; + $course2['completionstartonenrol'] = 1; + $course2['completionnotify'] = 1; + $course2['lang'] = 'en'; + $course2['forcetheme'] = 'base'; + $course3['fullname'] = 'Test course 3'; + $course3['shortname'] = 'Testcourse3'; + $course3['categoryid'] = $category->id; + $course3['format'] = 'topics'; + $course3options = array('numsections' => 8, + 'hiddensections' => 1, + 'coursedisplay' => 1); + $course3['courseformatoptions'] = array(); + foreach ($course3options as $key => $value) { + $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value); + } + $courses = array($course1, $course2, $course3); + + $createdcourses = core_course_external::create_courses($courses); + + // We need to execute the return values cleaning process to simulate the web service server. + $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses); + + // Check that right number of courses were created. + $this->assertEquals(3, count($createdcourses)); + + // Check that the courses were correctly created. + foreach ($createdcourses as $createdcourse) { + $courseinfo = course_get_format($createdcourse['id'])->get_course(); + + if ($createdcourse['shortname'] == $course2['shortname']) { + $this->assertEquals($courseinfo->fullname, $course2['fullname']); + $this->assertEquals($courseinfo->shortname, $course2['shortname']); + $this->assertEquals($courseinfo->category, $course2['categoryid']); + $this->assertEquals($courseinfo->idnumber, $course2['idnumber']); + $this->assertEquals($courseinfo->summary, $course2['summary']); + $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']); + $this->assertEquals($courseinfo->format, $course2['format']); + $this->assertEquals($courseinfo->showgrades, $course2['showgrades']); + $this->assertEquals($courseinfo->newsitems, $course2['newsitems']); + $this->assertEquals($courseinfo->startdate, $course2['startdate']); + $this->assertEquals($courseinfo->numsections, $course2['numsections']); + $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']); + $this->assertEquals($courseinfo->showreports, $course2['showreports']); + $this->assertEquals($courseinfo->visible, $course2['visible']); + $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']); + $this->assertEquals($courseinfo->groupmode, $course2['groupmode']); + $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']); + $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']); + $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']); + $this->assertEquals($courseinfo->lang, $course2['lang']); + + if (!empty($CFG->allowcoursethemes)) { + $this->assertEquals($courseinfo->theme, $course2['forcetheme']); + } + + // We enabled completion at the beginning of the test. + $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']); + $this->assertEquals($courseinfo->completionstartonenrol, $course2['completionstartonenrol']); + + } else if ($createdcourse['shortname'] == $course1['shortname']) { + $courseconfig = get_config('moodlecourse'); + $this->assertEquals($courseinfo->fullname, $course1['fullname']); + $this->assertEquals($courseinfo->shortname, $course1['shortname']); + $this->assertEquals($courseinfo->category, $course1['categoryid']); + $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML); + $this->assertEquals($courseinfo->format, $courseconfig->format); + $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades); + $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems); + $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes); + $this->assertEquals($courseinfo->showreports, $courseconfig->showreports); + $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode); + $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce); + $this->assertEquals($courseinfo->defaultgroupingid, 0); + } else if ($createdcourse['shortname'] == $course3['shortname']) { + $this->assertEquals($courseinfo->fullname, $course3['fullname']); + $this->assertEquals($courseinfo->shortname, $course3['shortname']); + $this->assertEquals($courseinfo->category, $course3['categoryid']); + $this->assertEquals($courseinfo->format, $course3['format']); + $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']); + $this->assertEquals($courseinfo->numsections, $course3options['numsections']); + $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']); + } else { + throw moodle_exception('Unexpected shortname'); + } + } + + // Call without required capability + $this->unassignUserCapability('moodle/course:create', $contextid, $roleid); + $this->setExpectedException('required_capability_exception'); + $createdsubcats = core_course_external::create_courses($courses); + } + + /** + * Test delete_courses + */ + public function test_delete_courses() { + global $DB, $USER; + + $this->resetAfterTest(true); + + // Admin can delete a course. + $this->setAdminUser(); + // Validate_context() will fail as the email is not set by $this->setAdminUser(). + $USER->email = 'emailtopass@contextvalidation.me'; + + $course1 = self::getDataGenerator()->create_course(); + $course2 = self::getDataGenerator()->create_course(); + $course3 = self::getDataGenerator()->create_course(); + + // Delete courses. + core_course_external::delete_courses(array($course1->id, $course2->id)); + + // Check $course 1 and 2 are deleted. + $notdeletedcount = $DB->count_records_select('course', + 'id IN ( ' . $course1->id . ',' . $course2->id . ')'); + $this->assertEquals(0, $notdeletedcount); + + // Fail when the user is not allow to access the course (enrolled) or is not admin. + $this->setGuestUser(); + $this->setExpectedException('require_login_exception'); + $createdsubcats = core_course_external::delete_courses(array($course3->id)); + } + + /** + * Test get_courses + */ + public function test_get_courses () { + global $DB; + + $this->resetAfterTest(true); + + $generatedcourses = array(); + $coursedata['idnumber'] = 'idnumbercourse1'; + $coursedata['fullname'] = 'Course 1 for PHPunit test'; + $coursedata['summary'] = 'Course 1 description'; + $coursedata['summaryformat'] = FORMAT_MOODLE; + $course1 = self::getDataGenerator()->create_course($coursedata); + $generatedcourses[$course1->id] = $course1; + $course2 = self::getDataGenerator()->create_course(); + $generatedcourses[$course2->id] = $course2; + $course3 = self::getDataGenerator()->create_course(array('format' => 'topics')); + $generatedcourses[$course3->id] = $course3; + + // Set the required capabilities by the external function. + $context = context_system::instance(); + $roleid = $this->assignUserCapability('moodle/course:view', $context->id); + $this->assignUserCapability('moodle/course:update', + context_course::instance($course1->id)->id, $roleid); + $this->assignUserCapability('moodle/course:update', + context_course::instance($course2->id)->id, $roleid); + $this->assignUserCapability('moodle/course:update', + context_course::instance($course3->id)->id, $roleid); + + $courses = core_course_external::get_courses(array('ids' => + array($course1->id, $course2->id))); + + // We need to execute the return values cleaning process to simulate the web service server. + $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses); + + // Check we retrieve the good total number of categories. + $this->assertEquals(2, count($courses)); + + foreach ($courses as $course) { + $dbcourse = $generatedcourses[$course['id']]; + $this->assertEquals($course['idnumber'], $dbcourse->idnumber); + $this->assertEquals($course['fullname'], $dbcourse->fullname); + $this->assertEquals($course['summary'], $dbcourse->summary); + $this->assertEquals($course['summaryformat'], FORMAT_HTML); + $this->assertEquals($course['shortname'], $dbcourse->shortname); + $this->assertEquals($course['categoryid'], $dbcourse->category); + $this->assertEquals($course['format'], $dbcourse->format); + $this->assertEquals($course['showgrades'], $dbcourse->showgrades); + $this->assertEquals($course['newsitems'], $dbcourse->newsitems); + $this->assertEquals($course['startdate'], $dbcourse->startdate); + $this->assertEquals($course['numsections'], $dbcourse->numsections); + $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes); + $this->assertEquals($course['showreports'], $dbcourse->showreports); + $this->assertEquals($course['visible'], $dbcourse->visible); + $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections); + $this->assertEquals($course['groupmode'], $dbcourse->groupmode); + $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce); + $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid); + $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify); + $this->assertEquals($course['lang'], $dbcourse->lang); + $this->assertEquals($course['forcetheme'], $dbcourse->theme); + $this->assertEquals($course['completionstartonenrol'], $dbcourse->completionstartonenrol); + $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion); + $this->assertEquals($course['completionstartonenrol'], $dbcourse->completionstartonenrol); + if ($dbcourse->format === 'topics') { + $this->assertEquals($course['courseformatoptions'], array( + array('name' => 'numsections', 'value' => $dbcourse->numsections), + array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections), + array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay), + )); + } + } + + // Get all courses in the DB + $courses = core_course_external::get_courses(array()); + + // We need to execute the return values cleaning process to simulate the web service server. + $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses); + + $this->assertEquals($DB->count_records('course'), count($courses)); + } + + /** + * Test get_course_contents + */ + public function test_get_course_contents() { + $this->resetAfterTest(true); + + $course = self::getDataGenerator()->create_course(); + $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id)); + $forumcm = get_coursemodule_from_id('forum', $forum->cmid); + $forumcontext = context_module::instance($forum->cmid); + $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id)); + $datacontext = context_module::instance($data->cmid); + $datacm = get_coursemodule_from_instance('page', $data->id); + $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id)); + $pagecontext = context_module::instance($page->cmid); + $pagecm = get_coursemodule_from_instance('page', $page->id); + + // Set the required capabilities by the external function. + $context = context_course::instance($course->id); + $roleid = $this->assignUserCapability('moodle/course:view', $context->id); + $this->assignUserCapability('moodle/course:update', $context->id, $roleid); + + $courses = core_course_external::get_course_contents($course->id, array()); + + // We need to execute the return values cleaning process to simulate the web service server. + $courses = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $courses); + + // Check that the course has the 3 created modules + $this->assertEquals(3, count($courses[0]['modules'])); + } + + /** + * Test duplicate_course + */ + public function test_duplicate_course() { + $this->resetAfterTest(true); + + // Create one course with three modules. + $course = self::getDataGenerator()->create_course(); + $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id)); + $forumcm = get_coursemodule_from_id('forum', $forum->cmid); + $forumcontext = context_module::instance($forum->cmid); + $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id)); + $datacontext = context_module::instance($data->cmid); + $datacm = get_coursemodule_from_instance('page', $data->id); + $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id)); + $pagecontext = context_module::instance($page->cmid); + $pagecm = get_coursemodule_from_instance('page', $page->id); + + // Set the required capabilities by the external function. + $coursecontext = context_course::instance($course->id); + $categorycontext = context_coursecat::instance($course->category); + $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id); + $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid); + $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid); + $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid); + $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid); + // Optional capabilities to copy user data. + $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid); + $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid); + + $newcourse['fullname'] = 'Course duplicate'; + $newcourse['shortname'] = 'courseduplicate'; + $newcourse['categoryid'] = $course->category; + $newcourse['visible'] = true; + $newcourse['options'][] = array('name' => 'users', 'value' => true); + + $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'], + $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']); + + // We need to execute the return values cleaning process to simulate the web service server. + $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate); + + // Check that the course has been duplicated. + $this->assertEquals($newcourse['shortname'], $duplicate['shortname']); + } +} diff --git a/togglecompletion.php b/togglecompletion.php new file mode 100644 index 0000000..0be2108 --- /dev/null +++ b/togglecompletion.php @@ -0,0 +1,176 @@ +. + +/** + * Toggles the manual completion flag for a particular activity or course completion + * and the current user. + * + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once('../config.php'); +require_once($CFG->libdir.'/completionlib.php'); + +// Parameters +$cmid = optional_param('id', 0, PARAM_INT); +$courseid = optional_param('course', 0, PARAM_INT); +$confirm = optional_param('confirm', 0, PARAM_BOOL); + +if (!$cmid && !$courseid) { + print_error('invalidarguments'); +} + +// Process self completion +if ($courseid) { + $PAGE->set_url(new moodle_url('/course/togglecompletion.php', array('course'=>$courseid))); + + // Check user is logged in + $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); + $context = context_course::instance($course->id); + require_login($course); + + $completion = new completion_info($course); + if (!$completion->is_enabled()) { + throw new moodle_exception('completionnotenabled', 'completion'); + } elseif (!$completion->is_tracked_user($USER->id)) { + throw new moodle_exception('nottracked', 'completion'); + } + + // Check if we are marking a user complete via the completion report + $user = optional_param('user', 0, PARAM_INT); + $rolec = optional_param('rolec', 0, PARAM_INT); + + if ($user && $rolec) { + require_sesskey(); + + completion_criteria::factory(array('id'=>$rolec, 'criteriatype'=>COMPLETION_CRITERIA_TYPE_ROLE)); //TODO: this is dumb, because it does not fetch the data?!?! + $criteria = completion_criteria_role::fetch(array('id'=>$rolec)); + + if ($criteria and user_has_role_assignment($USER->id, $criteria->role, $context->id)) { + $criteria_completions = $completion->get_completions($user, COMPLETION_CRITERIA_TYPE_ROLE); + + foreach ($criteria_completions as $criteria_completion) { + if ($criteria_completion->criteriaid == $rolec) { + $criteria->complete($criteria_completion); + break; + } + } + } + + // Return to previous page + if (!empty($_SERVER['HTTP_REFERER'])) { + redirect($_SERVER['HTTP_REFERER']); + } else { + redirect('view.php?id='.$course->id); + } + + } else { + + // Confirm with user + if ($confirm and confirm_sesskey()) { + $completion = $completion->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF); + + if (!$completion) { + print_error('noselfcompletioncriteria', 'completion'); + } + + // Check if the user has already marked themselves as complete + if ($completion->is_complete()) { + print_error('useralreadymarkedcomplete', 'completion'); + } + + $completion->mark_complete(); + + redirect($CFG->wwwroot.'/course/view.php?id='.$courseid); + return; + } + + $strconfirm = get_string('confirmselfcompletion', 'completion'); + $PAGE->set_title($strconfirm); + $PAGE->set_heading($course->fullname); + $PAGE->navbar->add($strconfirm); + echo $OUTPUT->header(); + $buttoncontinue = new single_button(new moodle_url('/course/togglecompletion.php', array('course'=>$courseid, 'confirm'=>1, 'sesskey'=>sesskey())), get_string('yes'), 'post'); + $buttoncancel = new single_button(new moodle_url('/course/view.php', array('id'=>$courseid)), get_string('no'), 'get'); + echo $OUTPUT->confirm($strconfirm, $buttoncontinue, $buttoncancel); + echo $OUTPUT->footer(); + exit; + } +} + + +$targetstate = required_param('completionstate', PARAM_INT); +$fromajax = optional_param('fromajax', 0, PARAM_INT); + +$PAGE->set_url('/course/togglecompletion.php', array('id'=>$cmid, 'completionstate'=>$targetstate)); + +switch($targetstate) { + case COMPLETION_COMPLETE: + case COMPLETION_INCOMPLETE: + break; + default: + print_error('unsupportedstate'); +} + +// Get course-modules entry +$cm = get_coursemodule_from_id(null, $cmid, null, false, MUST_EXIST); +$course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); + +// Check user is logged in +require_login($course, false, $cm); + +if (isguestuser() or !confirm_sesskey()) { + print_error('error'); +} + +// Now change state +$completion = new completion_info($course); +if (!$completion->is_enabled()) { + throw new moodle_exception('completionnotenabled', 'completion'); +} elseif (!$completion->is_tracked_user($USER->id)) { + throw new moodle_exception('nottracked', 'completion'); +} + +// Check completion state is manual +if($cm->completion != COMPLETION_TRACKING_MANUAL) { + error_or_ajax('cannotmanualctrack', $fromajax); +} + +$completion->update_state($cm, $targetstate); + +// And redirect back to course +if ($fromajax) { + print 'OK'; +} else { + // In case of use in other areas of code we allow a 'backto' parameter, + // otherwise go back to course page + $backto = optional_param('backto', 'view.php?id='.$course->id, PARAM_URL); + redirect($backto); +} + +// utility functions + +function error_or_ajax($message, $fromajax) { + if ($fromajax) { + print get_string($message, 'error'); + exit; + } else { + print_error($message); + } +} + diff --git a/user.php b/user.php new file mode 100644 index 0000000..73839e1 --- /dev/null +++ b/user.php @@ -0,0 +1,139 @@ +. + +/** + * Display user activity reports for a course + * + * @copyright 1999 Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package course + */ + +require_once("../config.php"); +require_once("lib.php"); + +$id = required_param('id',PARAM_INT); // course id +$user = required_param('user',PARAM_INT); // user id +$mode = optional_param('mode', "todaylogs", PARAM_ALPHA); + +$url = new moodle_url('/course/user.php', array('id'=>$id,'user'=>$user, 'mode'=>$mode)); + +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +$user = $DB->get_record("user", array("id"=>$user, 'deleted'=>0), '*', MUST_EXIST); + +if ($mode === 'outline' or $mode === 'complete') { + $url = new moodle_url('/report/outline/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>$mode)); + redirect($url); +} +if ($mode === 'todaylogs' or $mode === 'alllogs') { + $logmode = ($mode === 'todaylogs') ? 'today' : 'all'; + $url = new moodle_url('/report/log/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>$logmode)); + redirect($url); +} +if ($mode === 'stats') { + $url = new moodle_url('/report/stats/user.php', array('id'=>$user->id, 'course'=>$course->id)); + redirect($url); +} +if ($mode === 'coursecompletions' or $mode === 'coursecompletion') { + $url = new moodle_url('/report/completion/user.php', array('id'=>$user->id, 'course'=>$course->id)); + redirect($url); +} + +$coursecontext = context_course::instance($course->id); +$personalcontext = context_user::instance($user->id); + +$PAGE->set_url('/course/user.php', array('id'=>$id, 'user'=>$user->id, 'mode'=>$mode)); + +require_login(); +$PAGE->set_pagelayout('admin'); +if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext) and !is_enrolled($coursecontext)) { + // do not require parents to be enrolled in courses ;-) + $PAGE->set_course($course); +} else { + require_login($course); +} + +if ($user->deleted) { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('userdeleted')); + echo $OUTPUT->footer(); + die; +} + +// prepare list of allowed modes +$myreports = ($course->showreports and $USER->id == $user->id); +$anyreport = has_capability('moodle/user:viewuseractivitiesreport', $personalcontext); + +$modes = array(); + +if (has_capability('moodle/grade:viewall', $coursecontext)) { + //ok - can view all course grades + $modes[] = 'grade'; + +} else if ($course->showgrades and $user->id == $USER->id and has_capability('moodle/grade:view', $coursecontext)) { + //ok - can view own grades + $modes[] = 'grade'; + +} else if ($course->showgrades and has_capability('moodle/grade:viewall', $personalcontext)) { + // ok - can view grades of this user - parent most probably + $modes[] = 'grade'; + +} else if ($course->showgrades and $anyreport) { + // ok - can view grades of this user - parent most probably + $modes[] = 'grade'; +} + +if (empty($modes)) { + require_capability('moodle/user:viewuseractivitiesreport', $personalcontext); +} + +if (!in_array($mode, $modes)) { + // forbidden or non-existent mode + $mode = reset($modes); +} + +add_to_log($course->id, "course", "user report", "user.php?id=$course->id&user=$user->id&mode=$mode", "$user->id"); + +$stractivityreport = get_string("activityreport"); + +$PAGE->navigation->extend_for_user($user); +$PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. +$PAGE->set_title("$course->shortname: $stractivityreport ($mode)"); +$PAGE->set_heading($course->fullname); +echo $OUTPUT->header(); + +switch ($mode) { + case "grade": + if (empty($CFG->grade_profilereport) or !file_exists($CFG->dirroot.'/grade/report/'.$CFG->grade_profilereport.'/lib.php')) { + $CFG->grade_profilereport = 'user'; + } + require_once $CFG->libdir.'/gradelib.php'; + require_once $CFG->dirroot.'/grade/lib.php'; + require_once $CFG->dirroot.'/grade/report/'.$CFG->grade_profilereport.'/lib.php'; + + $functionname = 'grade_report_'.$CFG->grade_profilereport.'_profilereport'; + if (function_exists($functionname)) { + $functionname($course, $user); + } + break; + + break; + default: + // can not be reached ;-) +} + + +echo $OUTPUT->footer(); diff --git a/view.php b/view.php new file mode 100644 index 0000000..6f40c36 --- /dev/null +++ b/view.php @@ -0,0 +1,288 @@ +dirroot.'/mod/forum/lib.php'); + require_once($CFG->libdir.'/conditionlib.php'); + require_once($CFG->libdir.'/completionlib.php'); + + $id = optional_param('id', 0, PARAM_INT); + $name = optional_param('name', '', PARAM_RAW); + $edit = optional_param('edit', -1, PARAM_BOOL); + $hide = optional_param('hide', 0, PARAM_INT); + $show = optional_param('show', 0, PARAM_INT); + $idnumber = optional_param('idnumber', '', PARAM_RAW); + $sectionid = optional_param('sectionid', 0, PARAM_INT); + $section = optional_param('section', 0, PARAM_INT); + $move = optional_param('move', 0, PARAM_INT); + $marker = optional_param('marker',-1 , PARAM_INT); + $switchrole = optional_param('switchrole',-1, PARAM_INT); + $modchooser = optional_param('modchooser', -1, PARAM_BOOL); + $return = optional_param('return', 0, PARAM_LOCALURL); + + $params = array(); + if (!empty($name)) { + $params = array('shortname' => $name); + } else if (!empty($idnumber)) { + $params = array('idnumber' => $idnumber); + } else if (!empty($id)) { + $params = array('id' => $id); + }else { + print_error('unspecifycourseid', 'error'); + } + + $course = $DB->get_record('course', $params, '*', MUST_EXIST); + + $urlparams = array('id' => $course->id); + + // Sectionid should get priority over section number + if ($sectionid) { + $section = $DB->get_field('course_sections', 'section', array('id' => $sectionid, 'course' => $course->id), MUST_EXIST); + } + if ($section) { + $urlparams['section'] = $section; + } + + $PAGE->set_url('/course/view.php', $urlparams); // Defined here to avoid notices on errors etc + + // Prevent caching of this page to stop confusion when changing page after making AJAX changes + $PAGE->set_cacheable(false); + + preload_course_contexts($course->id); + $context = context_course::instance($course->id, MUST_EXIST); + + // Remove any switched roles before checking login + if ($switchrole == 0 && confirm_sesskey()) { + role_switch($switchrole, $context); + } + + require_login($course); + + // Switchrole - sanity check in cost-order... + $reset_user_allowed_editing = false; + if ($switchrole > 0 && confirm_sesskey() && + has_capability('moodle/role:switchroles', $context)) { + // is this role assignable in this context? + // inquiring minds want to know... + $aroles = get_switchable_roles($context); + if (is_array($aroles) && isset($aroles[$switchrole])) { + role_switch($switchrole, $context); + // Double check that this role is allowed here + require_login($course); + } + // reset course page state - this prevents some weird problems ;-) + $USER->activitycopy = false; + $USER->activitycopycourse = NULL; + unset($USER->activitycopyname); + unset($SESSION->modform); + $USER->editing = 0; + $reset_user_allowed_editing = true; + } + + //If course is hosted on an external server, redirect to corresponding + //url with appropriate authentication attached as parameter + if (file_exists($CFG->dirroot .'/course/externservercourse.php')) { + include $CFG->dirroot .'/course/externservercourse.php'; + if (function_exists('extern_server_course')) { + if ($extern_url = extern_server_course($course)) { + redirect($extern_url); + } + } + } + + + require_once($CFG->dirroot.'/calendar/lib.php'); /// This is after login because it needs $USER + + $logparam = 'id='. $course->id; + $loglabel = 'view'; + $infoid = $course->id; + if ($section and $section > 0) { + $loglabel = 'view section'; + + // Get section details and check it exists. + $modinfo = get_fast_modinfo($course); + $coursesections = $modinfo->get_section_info($section, MUST_EXIST); + + // Check user is allowed to see it. + if (!$coursesections->uservisible) { + // Note: We actually already know they don't have this capability + // or uservisible would have been true; this is just to get the + // correct error message shown. + require_capability('moodle/course:viewhiddensections', $context); + } + $infoid = $coursesections->id; + $logparam .= '§ionid='. $infoid; + } + add_to_log($course->id, 'course', $loglabel, "view.php?". $logparam, $infoid); + + // Fix course format if it is no longer installed + $course->format = course_get_format($course)->get_format(); + + $PAGE->set_pagelayout('course'); + $PAGE->set_pagetype('course-view-' . $course->format); + $PAGE->set_other_editing_capability('moodle/course:manageactivities'); + + // Preload course format renderer before output starts. + // This is a little hacky but necessary since + // format.php is not included until after output starts + if (file_exists($CFG->dirroot.'/course/format/'.$course->format.'/renderer.php')) { + require_once($CFG->dirroot.'/course/format/'.$course->format.'/renderer.php'); + if (class_exists('format_'.$course->format.'_renderer')) { + // call get_renderer only if renderer is defined in format plugin + // otherwise an exception would be thrown + $PAGE->get_renderer('format_'. $course->format); + } + } + + if ($reset_user_allowed_editing) { + // ugly hack + unset($PAGE->_user_allowed_editing); + } + + if (!isset($USER->editing)) { + $USER->editing = 0; + } + if ($PAGE->user_allowed_editing()) { + if (($edit == 1) and confirm_sesskey()) { + $USER->editing = 1; + // Redirect to site root if Editing is toggled on frontpage + if ($course->id == SITEID) { + redirect($CFG->wwwroot .'/?redirect=0'); + } else if (!empty($return)) { + redirect($CFG->wwwroot . $return); + } else { + $url = new moodle_url($PAGE->url, array('notifyeditingon' => 1)); + redirect($url); + } + } else if (($edit == 0) and confirm_sesskey()) { + $USER->editing = 0; + if(!empty($USER->activitycopy) && $USER->activitycopycourse == $course->id) { + $USER->activitycopy = false; + $USER->activitycopycourse = NULL; + } + // Redirect to site root if Editing is toggled on frontpage + if ($course->id == SITEID) { + redirect($CFG->wwwroot .'/?redirect=0'); + } else if (!empty($return)) { + redirect($CFG->wwwroot . $return); + } else { + redirect($PAGE->url); + } + } + if (($modchooser == 1) && confirm_sesskey()) { + set_user_preference('usemodchooser', $modchooser); + } else if (($modchooser == 0) && confirm_sesskey()) { + set_user_preference('usemodchooser', $modchooser); + } + + if (has_capability('moodle/course:sectionvisibility', $context)) { + if ($hide && confirm_sesskey()) { + set_section_visible($course->id, $hide, '0'); + redirect($PAGE->url); + } + + if ($show && confirm_sesskey()) { + set_section_visible($course->id, $show, '1'); + redirect($PAGE->url); + } + } + + if (has_capability('moodle/course:update', $context)) { + if (!empty($section)) { + if (!empty($move) and has_capability('moodle/course:movesections', $context) and confirm_sesskey()) { + $destsection = $section + $move; + if (move_section_to($course, $section, $destsection)) { + if ($course->id == SITEID) { + redirect($CFG->wwwroot . '/?redirect=0'); + } else { + redirect(course_get_url($course)); + } + } else { + echo $OUTPUT->notification('An error occurred while moving a section'); + } + } + } + } + } else { + $USER->editing = 0; + } + + $SESSION->fromdiscussion = $PAGE->url->out(false); + + + if ($course->id == SITEID) { + // This course is not a real course. + redirect($CFG->wwwroot .'/'); + } + + $completion = new completion_info($course); + if ($completion->is_enabled() && ajaxenabled()) { + $PAGE->requires->string_for_js('completion-title-manual-y', 'completion'); + $PAGE->requires->string_for_js('completion-title-manual-n', 'completion'); + $PAGE->requires->string_for_js('completion-alt-manual-y', 'completion'); + $PAGE->requires->string_for_js('completion-alt-manual-n', 'completion'); + + $PAGE->requires->js_init_call('M.core_completion.init'); + } + + // We are currently keeping the button here from 1.x to help new teachers figure out + // what to do, even though the link also appears in the course admin block. It also + // means you can back out of a situation where you removed the admin block. :) + if ($PAGE->user_allowed_editing()) { + $buttons = $OUTPUT->edit_button($PAGE->url); + $PAGE->set_button($buttons); + } + + $PAGE->set_title(get_string('course') . ': ' . $course->fullname); + $PAGE->set_heading($course->fullname); + echo $OUTPUT->header(); + + if ($completion->is_enabled() && ajaxenabled()) { + // This value tracks whether there has been a dynamic change to the page. + // It is used so that if a user does this - (a) set some tickmarks, (b) + // go to another page, (c) clicks Back button - the page will + // automatically reload. Otherwise it would start with the wrong tick + // values. + echo html_writer::start_tag('form', array('action'=>'.', 'method'=>'get')); + echo html_writer::start_tag('div'); + echo html_writer::empty_tag('input', array('type'=>'hidden', 'id'=>'completion_dynamic_change', 'name'=>'completion_dynamic_change', 'value'=>'0')); + echo html_writer::end_tag('div'); + echo html_writer::end_tag('form'); + } + + // Course wrapper start. + echo html_writer::start_tag('div', array('class'=>'course-content')); + + // make sure that section 0 exists (this function will create one if it is missing) + course_create_sections_if_missing($course, 0); + + // get information about course modules and existing module types + // format.php in course formats may rely on presence of these variables + $modinfo = get_fast_modinfo($course); + $modnames = get_module_types_names(); + $modnamesplural = get_module_types_names(true); + $modnamesused = $modinfo->get_used_module_names(); + $mods = $modinfo->get_cms(); + $sections = $modinfo->get_section_info_all(); + + // CAUTION, hacky fundamental variable defintion to follow! + // Note that because of the way course fromats are constructed though + // inclusion we pass parameters around this way.. + $displaysection = $section; + + // Include the actual course format. + require($CFG->dirroot .'/course/format/'. $course->format .'/format.php'); + // Content wrapper end. + + echo html_writer::end_tag('div'); + + // Include course AJAX + if (include_course_ajax($course, $modnamesused)) { + // Add the module chooser + $renderer = $PAGE->get_renderer('core', 'course'); + echo $renderer->course_modchooser(get_module_metadata($course, $modnames, $displaysection), $course); + } + + echo $OUTPUT->footer(); diff --git a/yui/coursebase/coursebase.js b/yui/coursebase/coursebase.js new file mode 100644 index 0000000..b02c362 --- /dev/null +++ b/yui/coursebase/coursebase.js @@ -0,0 +1,224 @@ +YUI.add('moodle-course-coursebase', function(Y) { + + /** + * The coursebase class + */ + var COURSEBASENAME = 'course-coursebase'; + + var COURSEBASE = function() { + COURSEBASE.superclass.constructor.apply(this, arguments); + } + + Y.extend(COURSEBASE, Y.Base, { + // Registered Modules + registermodules : [], + + /** + * Initialize the coursebase module + */ + initializer : function(config) { + // We don't actually perform any work here + }, + + /** + * Register a new Javascript Module + * + * @param object The instantiated module to call functions on + */ + register_module : function(object) { + this.registermodules.push(object); + }, + + /** + * Invoke the specified function in all registered modules with the given arguments + * + * @param functionname The name of the function to call + * @param args The argument supplied to the function + */ + invoke_function : function(functionname, args) { + for (module in this.registermodules) { + if (functionname in this.registermodules[module]) { + this.registermodules[module][functionname](args); + } + } + } + }, + { + NAME : COURSEBASENAME, + ATTRS : {} + } + ); + + // Ensure that M.course exists and that coursebase is initialised correctly + M.course = M.course || {}; + M.course.coursebase = M.course.coursebase || new COURSEBASE(); + + // Abstract functions that needs to be defined per format (course/format/somename/format.js) + M.course.format = M.course.format || {} + + /** + * Swap section (should be defined in format.js if requred) + * + * @param {YUI} Y YUI3 instance + * @param {string} node1 node to swap to + * @param {string} node2 node to swap with + * @return {NodeList} section list + */ + M.course.format.swap_sections = M.course.format.swap_sections || function(Y, node1, node2) { + return null; + } + + /** + * Process sections after ajax response (should be defined in format.js) + * If some response is expected, we pass it over to format, as it knows better + * hot to process it. + * + * @param {YUI} Y YUI3 instance + * @param {NodeList} list of sections + * @param {array} response ajax response + * @param {string} sectionfrom first affected section + * @param {string} sectionto last affected section + * @return void + */ + M.course.format.process_sections = M.course.format.process_sections || function(Y, sectionlist, response, sectionfrom, sectionto) { + return null; + } + + /** + * Get sections config for this format, for examples see function definition + * in the formats. + * + * @return {object} section list configuration + */ + M.course.format.get_config = M.course.format.get_config || function() { + return { + container_node : null, // compulsory + container_class : null, // compulsory + section_wrapper_node : null, // optional + section_wrapper_class : null, // optional + section_node : null, // compulsory + section_class : null // compulsory + } + } + + /** + * Get section list for this format (usually items inside container_node.container_class selector) + * + * @param {YUI} Y YUI3 instance + * @return {string} section selector + */ + M.course.format.get_section_selector = M.course.format.get_section_selector || function(Y) { + var config = M.course.format.get_config(); + if (config.section_node && config.section_class) { + return config.section_node + '.' + config.section_class; + } + console.log('section_node and section_class are not defined in M.course.format.get_config'); + return null; + } + + /** + * Get section wraper for this format (only used in case when each + * container_node.container_class node is wrapped in some other element). + * + * @param {YUI} Y YUI3 instance + * @return {string} section wrapper selector or M.course.format.get_section_selector + * if section_wrapper_node and section_wrapper_class are not defined in the format config. + */ + M.course.format.get_section_wrapper = M.course.format.get_section_wrapper || function(Y) { + var config = M.course.format.get_config(); + if (config.section_wrapper_node && config.section_wrapper_class) { + return config.section_wrapper_node + '.' + config.section_wrapper_class; + } + return M.course.format.get_section_selector(Y); + } + + /** + * Get the tag of container node + * + * @return {string} tag of container node. + */ + M.course.format.get_containernode = M.course.format.get_containernode || function() { + var config = M.course.format.get_config(); + if (config.container_node) { + return config.container_node; + } else { + console.log('container_node is not defined in M.course.format.get_config'); + } + } + + /** + * Get the class of container node + * + * @return {string} class of the container node. + */ + M.course.format.get_containerclass = M.course.format.get_containerclass || function() { + var config = M.course.format.get_config(); + if (config.container_class) { + return config.container_class; + } else { + console.log('container_class is not defined in M.course.format.get_config'); + } + } + + /** + * Get the tag of draggable node (section wrapper if exists, otherwise section) + * + * @return {string} tag of the draggable node. + */ + M.course.format.get_sectionwrappernode = M.course.format.get_sectionwrappernode || function() { + var config = M.course.format.get_config(); + if (config.section_wrapper_node) { + return config.section_wrapper_node; + } else { + return config.section_node; + } + } + + /** + * Get the class of draggable node (section wrapper if exists, otherwise section) + * + * @return {string} class of the draggable node. + */ + M.course.format.get_sectionwrapperclass = M.course.format.get_sectionwrapperclass || function() { + var config = M.course.format.get_config(); + if (config.section_wrapper_class) { + return config.section_wrapper_class; + } else { + return config.section_class; + } + } + + /** + * Get the tag of section node + * + * @return {string} tag of section node. + */ + M.course.format.get_sectionnode = M.course.format.get_sectionnode || function() { + var config = M.course.format.get_config(); + if (config.section_node) { + return config.section_node; + } else { + console.log('section_node is not defined in M.course.format.get_config'); + } + } + + /** + * Get the class of section node + * + * @return {string} class of the section node. + */ + M.course.format.get_sectionclass = M.course.format.get_sectionclass || function() { + var config = M.course.format.get_config(); + if (config.section_class) { + return config.section_class; + } else { + console.log('section_class is not defined in M.course.format.get_config'); + } + + } + +}, +'@VERSION@', { + requires : ['base', 'node'] +} +); diff --git a/yui/dragdrop/dragdrop.js b/yui/dragdrop/dragdrop.js new file mode 100644 index 0000000..6872ece --- /dev/null +++ b/yui/dragdrop/dragdrop.js @@ -0,0 +1,428 @@ +YUI.add('moodle-course-dragdrop', function(Y) { + + var CSS = { + ACTIVITY : 'activity', + COMMANDSPAN : 'span.commands', + CONTENT : 'content', + COURSECONTENT : 'course-content', + EDITINGMOVE : 'editing_move', + ICONCLASS : 'iconsmall', + JUMPMENU : 'jumpmenu', + LEFT : 'left', + LIGHTBOX : 'lightbox', + MOVEDOWN : 'movedown', + MOVEUP : 'moveup', + PAGECONTENT : 'page-content', + RIGHT : 'right', + SECTION : 'section', + SECTIONADDMENUS : 'section_add_menus', + SECTIONHANDLE : 'section-handle', + SUMMARY : 'summary' + }; + + var DRAGSECTION = function() { + DRAGSECTION.superclass.constructor.apply(this, arguments); + }; + Y.extend(DRAGSECTION, M.core.dragdrop, { + sectionlistselector : null, + + initializer : function(params) { + // Set group for parent class + this.groups = ['section']; + this.samenodeclass = M.course.format.get_sectionwrapperclass(); + this.parentnodeclass = M.course.format.get_containerclass(); + + // Check if we are in single section mode + if (Y.Node.one('.'+CSS.JUMPMENU)) { + return false; + } + // Initialise sections dragging + this.sectionlistselector = M.course.format.get_section_wrapper(Y); + if (this.sectionlistselector) { + this.sectionlistselector = '.'+CSS.COURSECONTENT+' '+this.sectionlistselector; + this.setup_for_section(this.sectionlistselector); + + // Make each li element in the lists of sections draggable + var nodeselector = this.sectionlistselector.slice(CSS.COURSECONTENT.length+2); + var del = new Y.DD.Delegate({ + container: '.'+CSS.COURSECONTENT, + nodes: nodeselector, + target: true, + handles: ['.'+CSS.LEFT], + dragConfig: {groups: this.groups} + }); + del.dd.plug(Y.Plugin.DDProxy, { + // Don't move the node at the end of the drag + moveOnEnd: false + }); + del.dd.plug(Y.Plugin.DDConstrained, { + // Keep it inside the .course-content + constrain: '#'+CSS.PAGECONTENT, + stickY: true + }); + del.dd.plug(Y.Plugin.DDWinScroll); + } + }, + + /** + * Apply dragdrop features to the specified selector or node that refers to section(s) + * + * @param baseselector The CSS selector or node to limit scope to + * @return void + */ + setup_for_section : function(baseselector) { + Y.Node.all(baseselector).each(function(sectionnode) { + // Determine the section ID + var sectionid = this.get_section_id(sectionnode); + + // We skip the top section as it is not draggable + if (sectionid > 0) { + // Remove move icons + var movedown = sectionnode.one('.'+CSS.RIGHT+' a.'+CSS.MOVEDOWN); + var moveup = sectionnode.one('.'+CSS.RIGHT+' a.'+CSS.MOVEUP); + + // Add dragger icon + var title = M.util.get_string('movesection', 'moodle', sectionid); + var cssleft = sectionnode.one('.'+CSS.LEFT); + + if ((movedown || moveup) && cssleft) { + cssleft.setStyle('cursor', 'move'); + cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true)); + + if (moveup) { + moveup.remove(); + } + if (movedown) { + movedown.remove(); + } + } + } + }, this); + }, + + get_section_id : function(node) { + return Number(node.get('id').replace(/section-/i, '')); + }, + + /* + * Drag-dropping related functions + */ + drag_start : function(e) { + // Get our drag object + var drag = e.target; + // Creat a dummy structure of the outer elemnents for clean styles application + var containernode = Y.Node.create('<'+M.course.format.get_containernode()+'>'); + containernode.addClass(M.course.format.get_containerclass()); + var sectionnode = Y.Node.create('<'+ M.course.format.get_sectionwrappernode()+'>'); + sectionnode.addClass( M.course.format.get_sectionwrapperclass()); + sectionnode.setStyle('margin', 0); + sectionnode.setContent(drag.get('node').get('innerHTML')); + containernode.appendChild(sectionnode); + drag.get('dragNode').setContent(containernode); + drag.get('dragNode').addClass(CSS.COURSECONTENT); + }, + + drag_dropmiss : function(e) { + // Missed the target, but we assume the user intended to drop it + // on the last last ghost node location, e.drag and e.drop should be + // prepared by global_drag_dropmiss parent so simulate drop_hit(e). + this.drop_hit(e); + }, + + drop_hit : function(e) { + var drag = e.drag; + // Get a reference to our drag node + var dragnode = drag.get('node'); + var dropnode = e.drop.get('node'); + // Prepare some variables + var dragnodeid = Number(this.get_section_id(dragnode)); + var dropnodeid = Number(this.get_section_id(dropnode)); + + var loopstart = dragnodeid; + var loopend = dropnodeid; + + if (this.goingup) { + loopstart = dropnodeid; + loopend = dragnodeid; + } + + // Get the list of nodes + drag.get('dragNode').removeClass(CSS.COURSECONTENT); + var sectionlist = Y.Node.all(this.sectionlistselector); + + // Add lightbox if it not there + var lightbox = M.util.add_lightbox(Y, dragnode); + + var params = {}; + + // Handle any variables which we must pass back through to + var pageparams = this.get('config').pageparams; + for (varname in pageparams) { + params[varname] = pageparams[varname]; + } + + // Prepare request parameters + params.sesskey = M.cfg.sesskey; + params.courseId = this.get('courseid'); + params['class'] = 'section'; + params.field = 'move'; + params.id = dragnodeid; + params.value = dropnodeid; + + // Do AJAX request + var uri = M.cfg.wwwroot + this.get('ajaxurl'); + + Y.io(uri, { + method: 'POST', + data: params, + on: { + start : function(tid) { + lightbox.show(); + }, + success: function(tid, response) { + // Update section titles, we can't simply swap them as + // they might have custom title + try { + var responsetext = Y.JSON.parse(response.responseText); + if (responsetext.error) { + new M.core.ajaxException(responsetext); + } + M.course.format.process_sections(Y, sectionlist, responsetext, loopstart, loopend); + } catch (e) {} + + // Classic bubble sort algorithm is applied to the section + // nodes between original drag node location and the new one. + do { + var swapped = false; + for (var i = loopstart; i <= loopend; i++) { + if (this.get_section_id(sectionlist.item(i-1)) > this.get_section_id(sectionlist.item(i))) { + // Swap section id + var sectionid = sectionlist.item(i-1).get('id'); + sectionlist.item(i-1).set('id', sectionlist.item(i).get('id')); + sectionlist.item(i).set('id', sectionid); + // See what format needs to swap + M.course.format.swap_sections(Y, i-1, i); + // Update flag + swapped = true; + } + } + loopend = loopend - 1; + } while (swapped); + + // Finally, hide the lightbox + window.setTimeout(function(e) { + lightbox.hide(); + }, 250); + }, + failure: function(tid, response) { + this.ajax_failure(response); + lightbox.hide(); + } + }, + context:this + }); + } + + }, { + NAME : 'course-dragdrop-section', + ATTRS : { + courseid : { + value : null + }, + ajaxurl : { + 'value' : 0 + }, + config : { + 'value' : 0 + } + } + }); + + var DRAGRESOURCE = function() { + DRAGRESOURCE.superclass.constructor.apply(this, arguments); + }; + Y.extend(DRAGRESOURCE, M.core.dragdrop, { + initializer : function(params) { + // Set group for parent class + this.groups = ['resource']; + this.samenodeclass = CSS.ACTIVITY; + this.parentnodeclass = CSS.SECTION; + this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS); + + // Go through all sections + var sectionlistselector = M.course.format.get_section_selector(Y); + if (sectionlistselector) { + sectionlistselector = '.'+CSS.COURSECONTENT+' '+sectionlistselector; + this.setup_for_section(sectionlistselector); + + // Initialise drag & drop for all resources/activities + var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length+2)+' li.'+CSS.ACTIVITY; + var del = new Y.DD.Delegate({ + container: '.'+CSS.COURSECONTENT, + nodes: nodeselector, + target: true, + handles: ['.' + CSS.EDITINGMOVE], + dragConfig: {groups: this.groups} + }); + del.dd.plug(Y.Plugin.DDProxy, { + // Don't move the node at the end of the drag + moveOnEnd: false, + cloneNode: true + }); + del.dd.plug(Y.Plugin.DDConstrained, { + // Keep it inside the .course-content + constrain: '#'+CSS.PAGECONTENT + }); + del.dd.plug(Y.Plugin.DDWinScroll); + + M.course.coursebase.register_module(this); + M.course.dragres = this; + } + }, + + /** + * Apply dragdrop features to the specified selector or node that refers to section(s) + * + * @param baseselector The CSS selector or node to limit scope to + * @return void + */ + setup_for_section : function(baseselector) { + Y.Node.all(baseselector).each(function(sectionnode) { + var resources = sectionnode.one('.'+CSS.CONTENT+' ul.'+CSS.SECTION); + // See if resources ul exists, if not create one + if (!resources) { + var resources = Y.Node.create('
            '); + resources.addClass(CSS.SECTION); + sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after'); + } + // Define empty ul as droptarget, so that item could be moved to empty list + var tar = new Y.DD.Drop({ + node: resources, + groups: this.groups, + padding: '20 0 20 0' + }); + + // Initialise each resource/activity in this section + this.setup_for_resource('#'+sectionnode.get('id')+' li.'+CSS.ACTIVITY); + }, this); + }, + /** + * Apply dragdrop features to the specified selector or node that refers to resource(s) + * + * @param baseselector The CSS selector or node to limit scope to + * @return void + */ + setup_for_resource : function(baseselector) { + Y.Node.all(baseselector).each(function(resourcesnode) { + // Replace move icons + var move = resourcesnode.one('a.'+CSS.EDITINGMOVE); + if (move) { + move.replace(this.resourcedraghandle.cloneNode(true)); + } + }, this); + }, + + get_section_id : function(node) { + return Number(node.get('id').replace(/section-/i, '')); + }, + + get_resource_id : function(node) { + return Number(node.get('id').replace(/module-/i, '')); + }, + + drag_start : function(e) { + // Get our drag object + var drag = e.target; + drag.get('dragNode').setContent(drag.get('node').get('innerHTML')); + drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline'); + }, + + drag_dropmiss : function(e) { + // Missed the target, but we assume the user intended to drop it + // on the last last ghost node location, e.drag and e.drop should be + // prepared by global_drag_dropmiss parent so simulate drop_hit(e). + this.drop_hit(e); + }, + + drop_hit : function(e) { + var drag = e.drag; + // Get a reference to our drag node + var dragnode = drag.get('node'); + var dropnode = e.drop.get('node'); + + // Add spinner if it not there + var spinner = M.util.add_spinner(Y, dragnode.one(CSS.COMMANDSPAN)); + + var params = {}; + + // Handle any variables which we must pass back through to + var pageparams = this.get('config').pageparams; + for (varname in pageparams) { + params[varname] = pageparams[varname]; + } + + // Prepare request parameters + params.sesskey = M.cfg.sesskey; + params.courseId = this.get('courseid'); + params['class'] = 'resource'; + params.field = 'move'; + params.id = Number(this.get_resource_id(dragnode)); + params.sectionId = this.get_section_id(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true)); + + if (dragnode.next()) { + params.beforeId = Number(this.get_resource_id(dragnode.next())); + } + + // Do AJAX request + var uri = M.cfg.wwwroot + this.get('ajaxurl'); + + Y.io(uri, { + method: 'POST', + data: params, + on: { + start : function(tid) { + this.lock_drag_handle(drag, CSS.EDITINGMOVE); + spinner.show(); + }, + success: function(tid, response) { + var responsetext = Y.JSON.parse(response.responseText); + var params = {element: dragnode, visible: responsetext.visible}; + M.course.coursebase.invoke_function('set_visibility_resource_ui', params); + this.unlock_drag_handle(drag, CSS.EDITINGMOVE); + window.setTimeout(function(e) { + spinner.hide(); + }, 250); + }, + failure: function(tid, response) { + this.ajax_failure(response); + this.unlock_drag_handle(drag, CSS.SECTIONHANDLE); + spinner.hide(); + // TODO: revert nodes location + } + }, + context:this + }); + } + }, { + NAME : 'course-dragdrop-resource', + ATTRS : { + courseid : { + value : null + }, + ajaxurl : { + 'value' : 0 + }, + config : { + 'value' : 0 + } + } + }); + + M.course = M.course || {}; + M.course.init_resource_dragdrop = function(params) { + new DRAGRESOURCE(params); + } + M.course.init_section_dragdrop = function(params) { + new DRAGSECTION(params); + } +}, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'dd-scroll', 'moodle-core-dragdrop', 'moodle-core-notification', 'moodle-course-coursebase']}); diff --git a/yui/formatchooser/formatchooser.js b/yui/formatchooser/formatchooser.js new file mode 100644 index 0000000..0597c4b --- /dev/null +++ b/yui/formatchooser/formatchooser.js @@ -0,0 +1,25 @@ +YUI.add('moodle-course-formatchooser', function(Y) { + var FORMATCHOOSER = function() { + FORMATCHOOSER.superclass.constructor.apply(this, arguments); + } + + Y.extend(FORMATCHOOSER, Y.Base, { + initializer : function(params) { + if (params && params.formid) { + var updatebut = Y.one('#'+params.formid+' #id_updatecourseformat'); + var formatselect = Y.one('#'+params.formid+' #id_format'); + if (updatebut && formatselect) { + updatebut.setStyle('display', 'none'); + formatselect.on('change', function() { + updatebut.simulate('click'); + }); + } + } + } + }); + + M.course = M.course || {}; + M.course.init_formatchooser = function(params) { + return new FORMATCHOOSER(params); + } +}, '@VERSION@', {requires:['base', 'node', 'node-event-simulate']}); diff --git a/yui/modchooser/modchooser.js b/yui/modchooser/modchooser.js new file mode 100644 index 0000000..10fdded --- /dev/null +++ b/yui/modchooser/modchooser.js @@ -0,0 +1,166 @@ +YUI.add('moodle-course-modchooser', function(Y) { + var CSS = { + PAGECONTENT : 'div#page-content', + SECTION : 'li.section', + SECTIONMODCHOOSER : 'span.section-modchooser-link', + SITEMENU : 'div.block_site_main_menu', + SITETOPIC : 'div.sitetopic' + }; + + var MODCHOOSERNAME = 'course-modchooser'; + + var MODCHOOSER = function() { + MODCHOOSER.superclass.constructor.apply(this, arguments); + } + + Y.extend(MODCHOOSER, M.core.chooserdialogue, { + // The current section ID + sectionid : null, + + // The hidden element holding the jump param + jumplink : null, + + initializer : function(config) { + var dialogue = Y.one('.chooserdialoguebody'); + var header = Y.one('.choosertitle'); + var params = {}; + this.setup_chooser_dialogue(dialogue, header, params); + + // Initialize existing sections and register for dynamically created sections + this.setup_for_section(); + M.course.coursebase.register_module(this); + + // Catch the page toggle + Y.all('.block_settings #settingsnav .type_course .modchoosertoggle a').on('click', this.toggle_mod_chooser, this); + }, + /** + * Update any section areas within the scope of the specified + * selector with AJAX equivalents + * + * @param baseselector The selector to limit scope to + * @return void + */ + setup_for_section : function(baseselector) { + if (!baseselector) { + var baseselector = CSS.PAGECONTENT; + } + + // Setup for site topics + Y.one(baseselector).all(CSS.SITETOPIC).each(function(section) { + this._setup_for_section(section); + }, this); + + // Setup for standard course topics + Y.one(baseselector).all(CSS.SECTION).each(function(section) { + this._setup_for_section(section); + }, this); + + // Setup for the block site menu + Y.one(baseselector).all(CSS.SITEMENU).each(function(section) { + this._setup_for_section(section); + }, this); + }, + _setup_for_section : function(section, sectionid) { + var chooserspan = section.one(CSS.SECTIONMODCHOOSER); + if (!chooserspan) { + return; + } + var chooserlink = Y.Node.create(""); + chooserspan.get('children').each(function(node) { + chooserlink.appendChild(node); + }); + chooserspan.insertBefore(chooserlink); + chooserlink.on('click', this.display_mod_chooser, this); + }, + /** + * Display the module chooser + * + * @param e Event Triggering Event + * @param secitonid integer The ID of the section triggering the dialogue + * @return void + */ + display_mod_chooser : function (e) { + // Set the section for this version of the dialogue + if (e.target.ancestor(CSS.SITETOPIC)) { + // The site topic has a sectionid of 1 + this.sectionid = 1; + } else if (e.target.ancestor(CSS.SECTION)) { + var section = e.target.ancestor(CSS.SECTION); + this.sectionid = section.get('id').replace('section-', ''); + } else if (e.target.ancestor(CSS.SITEMENU)) { + // The block site menu has a sectionid of 0 + this.sectionid = 0; + } + this.display_chooser(e); + }, + toggle_mod_chooser : function(e) { + // Get the add section link + var modchooserlinks = Y.all('div.addresourcemodchooser'); + + // Get the dropdowns + var dropdowns = Y.all('div.addresourcedropdown'); + + if (modchooserlinks.size() == 0) { + // Continue with non-js action if there are no modchoosers to add + return; + } + + // We need to update the text and link + var togglelink = Y.one('.block_settings #settingsnav .type_course .modchoosertoggle a'); + + // The actual text is in the last child + var toggletext = togglelink.get('lastChild'); + + var usemodchooser; + // Determine whether they're currently hidden + if (modchooserlinks.item(0).hasClass('visibleifjs')) { + // The modchooser is currently visible, hide it + usemodchooser = 0; + modchooserlinks + .removeClass('visibleifjs') + .addClass('hiddenifjs'); + dropdowns + .addClass('visibleifjs') + .removeClass('hiddenifjs'); + toggletext.set('data', M.util.get_string('modchooserenable', 'moodle')); + togglelink.set('href', togglelink.get('href').replace('off', 'on')); + } else { + // The modchooser is currently not visible, show it + usemodchooser = 1; + modchooserlinks + .addClass('visibleifjs') + .removeClass('hiddenifjs'); + dropdowns + .removeClass('visibleifjs') + .addClass('hiddenifjs'); + toggletext.set('data', M.util.get_string('modchooserdisable', 'moodle')); + togglelink.set('href', togglelink.get('href').replace('on', 'off')); + } + + M.util.set_user_preference('usemodchooser', usemodchooser); + + // Prevent the page from reloading + e.preventDefault(); + }, + option_selected : function(thisoption) { + // Add the sectionid to the URL + this.jumplink.set('value', thisoption.get('value') + '§ion=' + this.sectionid); + } + }, + { + NAME : MODCHOOSERNAME, + ATTRS : { + maxheight : { + value : 800 + } + } + }); + M.course = M.course || {}; + M.course.init_chooser = function(config) { + return new MODCHOOSER(config); + } +}, +'@VERSION@', { + requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'transition'] +} +); diff --git a/yui/toolboxes/toolboxes.js b/yui/toolboxes/toolboxes.js new file mode 100644 index 0000000..4e6ad15 --- /dev/null +++ b/yui/toolboxes/toolboxes.js @@ -0,0 +1,800 @@ +YUI.add('moodle-course-toolboxes', function(Y) { + WAITICON = {'pix':"i/loading_small",'component':'moodle'}; + // The CSS selectors we use + var CSS = { + ACTIVITYLI : 'li.activity', + COMMANDSPAN : 'span.commands', + SPINNERCOMMANDSPAN : 'span.commands', + CONTENTAFTERLINK : 'div.contentafterlink', + DELETE : 'a.editing_delete', + DIMCLASS : 'dimmed', + DIMMEDTEXT : 'dimmed_text', + EDITTITLE : 'a.editing_title', + EDITTITLECLASS : 'edittitle', + GENERICICONCLASS : 'iconsmall', + GROUPSNONE : 'a.editing_groupsnone', + GROUPSSEPARATE : 'a.editing_groupsseparate', + GROUPSVISIBLE : 'a.editing_groupsvisible', + HASLABEL : 'label', + HIDE : 'a.editing_hide', + HIGHLIGHT : 'a.editing_highlight', + INSTANCENAME : 'span.instancename', + LIGHTBOX : 'lightbox', + MODINDENTCOUNT : 'mod-indent-', + MODINDENTDIV : 'div.mod-indent', + MODINDENTHUGE : 'mod-indent-huge', + MODULEIDPREFIX : 'module-', + MOVELEFT : 'a.editing_moveleft', + MOVELEFTCLASS : 'editing_moveleft', + MOVERIGHT : 'a.editing_moveright', + PAGECONTENT : 'div#page-content', + RIGHTSIDE : '.right', + SECTIONHIDDENCLASS : 'hidden', + SECTIONIDPREFIX : 'section-', + SECTIONLI : 'li.section', + SHOW : 'a.editing_show', + SHOWHIDE : 'a.editing_showhide', + CONDITIONALHIDDEN : 'conditionalhidden', + AVAILABILITYINFODIV : 'div.availabilityinfo', + SHOWCLASS : 'editing_show', + HIDECLASS : 'hide' + }; + + /** + * The toolbox classes + * + * TOOLBOX is a generic class which should never be directly instantiated + * RESOURCETOOLBOX is a class extending TOOLBOX containing code specific to resources + * SECTIONTOOLBOX is a class extending TOOLBOX containing code specific to sections + */ + var TOOLBOX = function() { + TOOLBOX.superclass.constructor.apply(this, arguments); + } + + Y.extend(TOOLBOX, Y.Base, { + /** + * Toggle the visibility and availability for the specified + * resource show/hide button + */ + toggle_hide_resource_ui : function(button) { + var element = button.ancestor(CSS.ACTIVITYLI); + var hideicon = button.one('img'); + + var dimarea; + var toggle_class; + if (this.is_label(element)) { + toggle_class = CSS.DIMMEDTEXT; + dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1); + } else { + toggle_class = CSS.DIMCLASS; + dimarea = element.one('a'); + } + + var status = ''; + var value; + if (button.hasClass(CSS.SHOWCLASS)) { + status = 'hide'; + value = 1; + } else { + status = 'show'; + value = 0; + } + // Update button info. + var newstring = M.util.get_string(status, 'moodle'); + hideicon.setAttrs({ + 'alt' : newstring, + 'src' : M.util.image_url('t/' + status) + }); + button.set('title', newstring); + button.set('className', 'editing_'+status); + + // If activity is conditionally hidden, then don't toggle. + if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) { + // Change the UI. + dimarea.toggleClass(toggle_class); + // We need to toggle dimming on the description too. + element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT); + } + // Toggle availablity info for conditional activities. + var availabilityinfo = element.one(CSS.AVAILABILITYINFODIV); + + if (availabilityinfo) { + availabilityinfo.toggleClass(CSS.HIDECLASS); + } + return value; + }, + /** + * Send a request using the REST API + * + * @param data The data to submit + * @param statusspinner (optional) A statusspinner which may contain a section loader + * @param optionalconfig (optional) Any additional configuration to submit + * @return response responseText field from responce + */ + send_request : function(data, statusspinner, optionalconfig) { + // Default data structure + if (!data) { + data = {}; + } + // Handle any variables which we must pass back through to + var pageparams = this.get('config').pageparams; + for (varname in pageparams) { + data[varname] = pageparams[varname]; + } + + data.sesskey = M.cfg.sesskey; + data.courseId = this.get('courseid'); + + var uri = M.cfg.wwwroot + this.get('ajaxurl'); + + // Define the configuration to send with the request + var responsetext = []; + var config = { + method: 'POST', + data: data, + on: { + success: function(tid, response) { + try { + responsetext = Y.JSON.parse(response.responseText); + if (responsetext.error) { + new M.core.ajaxException(responsetext); + } + } catch (e) {} + if (statusspinner) { + window.setTimeout(function(e) { + statusspinner.hide(); + }, 400); + } + }, + failure : function(tid, response) { + if (statusspinner) { + statusspinner.hide(); + } + new M.core.ajaxException(response); + } + }, + context: this, + sync: true + } + + // Apply optional config + if (optionalconfig) { + for (varname in optionalconfig) { + config[varname] = optionalconfig[varname]; + } + } + + if (statusspinner) { + statusspinner.show(); + } + + // Send the request + Y.io(uri, config); + return responsetext; + }, + is_label : function(target) { + return target.hasClass(CSS.HASLABEL); + }, + /** + * Return the module ID for the specified element + * + * @param element The
          • element to determine a module-id number for + * @return string The module ID + */ + get_element_id : function(element) { + return element.get('id').replace(CSS.MODULEIDPREFIX, ''); + }, + /** + * Return the module ID for the specified element + * + * @param element The
          • element to determine a module-id number for + * @return string The module ID + */ + get_section_id : function(section) { + return section.get('id').replace(CSS.SECTIONIDPREFIX, ''); + } + }, + { + NAME : 'course-toolbox', + ATTRS : { + // The ID of the current course + courseid : { + 'value' : 0 + }, + ajaxurl : { + 'value' : 0 + }, + config : { + 'value' : 0 + } + } + } + ); + + + var RESOURCETOOLBOX = function() { + RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); + } + + Y.extend(RESOURCETOOLBOX, TOOLBOX, { + // Variables + GROUPS_NONE : 0, + GROUPS_SEPARATE : 1, + GROUPS_VISIBLE : 2, + + /** + * Initialize the resource toolbox + * + * Updates all span.commands with relevant handlers and other required changes + */ + initializer : function(config) { + this.setup_for_resource(); + M.course.coursebase.register_module(this); + + var prefix = CSS.ACTIVITYLI + ' ' + CSS.COMMANDSPAN + ' '; + Y.delegate('click', this.edit_resource_title, CSS.PAGECONTENT, prefix + CSS.EDITTITLE, this); + Y.delegate('click', this.move_left, CSS.PAGECONTENT, prefix + CSS.MOVELEFT, this); + Y.delegate('click', this.move_right, CSS.PAGECONTENT, prefix + CSS.MOVERIGHT, this); + Y.delegate('click', this.delete_resource, CSS.PAGECONTENT, prefix + CSS.DELETE, this); + Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.HIDE, this); + Y.delegate('click', this.toggle_hide_resource, CSS.PAGECONTENT, prefix + CSS.SHOW, this); + Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSNONE, this); + Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSSEPARATE, this); + Y.delegate('click', this.toggle_groupmode, CSS.PAGECONTENT, prefix + CSS.GROUPSVISIBLE, this); + }, + + /** + * Update any span.commands within the scope of the specified + * selector with AJAX equivelants + * + * @param baseselector The selector to limit scope to + * @return void + */ + setup_for_resource : function(baseselector) { + if (!baseselector) { + var baseselector = CSS.PAGECONTENT + ' ' + CSS.ACTIVITYLI; + } + + Y.all(baseselector).each(this._setup_for_resource, this); + }, + _setup_for_resource : function(toolboxtarget) { + toolboxtarget = Y.one(toolboxtarget); + + // Set groupmode attribute for use by this.toggle_groupmode() + var groups; + groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSNONE); + groups.setAttribute('groupmode', this.GROUPS_NONE); + + groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSSEPARATE); + groups.setAttribute('groupmode', this.GROUPS_SEPARATE); + + groups = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.GROUPSVISIBLE); + groups.setAttribute('groupmode', this.GROUPS_VISIBLE); + }, + move_left : function(e) { + this.move_leftright(e, -1); + }, + move_right : function(e) { + this.move_leftright(e, 1); + }, + move_leftright : function(e, direction) { + // Prevent the default button action + e.preventDefault(); + + // Get the element we're working on + var element = e.target.ancestor(CSS.ACTIVITYLI); + + // And we need to determine the current and new indent level + var indentdiv = element.one(CSS.MODINDENTDIV); + var indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/); + + if (indent) { + var oldindent = parseInt(indent[1]); + var newindent = Math.max(0, (oldindent + parseInt(direction))); + indentdiv.removeClass(indent[0]); + } else { + var oldindent = 0; + var newindent = 1; + } + + // Perform the move + indentdiv.addClass(CSS.MODINDENTCOUNT + newindent); + var data = { + 'class' : 'resource', + 'field' : 'indent', + 'value' : newindent, + 'id' : this.get_element_id(element) + }; + var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN)); + this.send_request(data, spinner); + + // Handle removal/addition of the moveleft button + if (newindent == 0) { + element.one(CSS.MOVELEFT).remove(); + } else if (newindent == 1 && oldindent == 0) { + this.add_moveleft(element); + } + + // Handle massive indentation to match non-ajax display + var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE); + if (newindent > 15 && !hashugeclass) { + indentdiv.addClass(CSS.MODINDENTHUGE); + } else if (newindent <= 15 && hashugeclass) { + indentdiv.removeClass(CSS.MODINDENTHUGE); + } + }, + delete_resource : function(e) { + // Prevent the default button action + e.preventDefault(); + + // Get the element we're working on + var element = e.target.ancestor(CSS.ACTIVITYLI); + + var confirmstring = ''; + if (this.is_label(element)) { + // Labels are slightly different to other activities + var plugindata = { + type : M.util.get_string('pluginname', 'label') + } + confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata) + } else { + var plugindata = { + type : M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]), + name : element.one(CSS.INSTANCENAME).get('firstChild').get('data') + } + confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata); + } + + // Confirm element removal + if (!confirm(confirmstring)) { + return false; + } + + // Actually remove the element + element.remove(); + var data = { + 'class' : 'resource', + 'action' : 'DELETE', + 'id' : this.get_element_id(element) + }; + this.send_request(data); + }, + toggle_hide_resource : function(e) { + // Prevent the default button action + e.preventDefault(); + + // Return early if the current section is hidden + var section = e.target.ancestor(M.course.format.get_section_selector(Y)); + if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) { + return; + } + + // Get the element we're working on + var element = e.target.ancestor(CSS.ACTIVITYLI); + + var button = e.target.ancestor('a', true); + + var value = this.toggle_hide_resource_ui(button); + + // Send the request + var data = { + 'class' : 'resource', + 'field' : 'visible', + 'value' : value, + 'id' : this.get_element_id(element) + }; + var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN)); + this.send_request(data, spinner); + return false; // Need to return false to stop the delegate for the new state firing + }, + toggle_groupmode : function(e) { + // Prevent the default button action + e.preventDefault(); + + // Get the element we're working on + var element = e.target.ancestor(CSS.ACTIVITYLI); + + var button = e.target.ancestor('a', true); + var icon = button.one('img'); + + // Current Mode + var groupmode = button.getAttribute('groupmode'); + groupmode++; + if (groupmode > 2) { + groupmode = 0; + } + button.setAttribute('groupmode', groupmode); + + var newtitle = ''; + var iconsrc = ''; + switch (groupmode) { + case this.GROUPS_NONE: + newtitle = 'groupsnone'; + iconsrc = M.util.image_url('t/groupn'); + break; + case this.GROUPS_SEPARATE: + newtitle = 'groupsseparate'; + iconsrc = M.util.image_url('t/groups'); + break; + case this.GROUPS_VISIBLE: + newtitle = 'groupsvisible'; + iconsrc = M.util.image_url('t/groupv'); + break; + } + newtitle = M.util.get_string('clicktochangeinbrackets', 'moodle', + M.util.get_string(newtitle, 'moodle')); + + // Change the UI + icon.setAttrs({ + 'alt' : newtitle, + 'src' : iconsrc + }); + button.setAttribute('title', newtitle); + + // And send the request + var data = { + 'class' : 'resource', + 'field' : 'groupmode', + 'value' : groupmode, + 'id' : this.get_element_id(element) + }; + var spinner = M.util.add_spinner(Y, element.one(CSS.SPINNERCOMMANDSPAN)); + this.send_request(data, spinner); + return false; // Need to return false to stop the delegate for the new state firing + }, + /** + * Add the moveleft button + * This is required after moving left from an initial position of 0 + * + * @param target The encapsulating
          • element + */ + add_moveleft : function(target) { + var left_string = M.util.get_string('moveleft', 'moodle'); + var moveimage = 't/left'; // ltr mode + if ( Y.one(document.body).hasClass('dir-rtl') ) { + moveimage = 't/right'; + } else { + moveimage = 't/left'; + } + var newicon = Y.Node.create('') + .addClass(CSS.GENERICICONCLASS) + .setAttrs({ + 'src' : M.util.image_url(moveimage, 'moodle'), + 'alt' : left_string + }); + var moveright = target.one(CSS.MOVERIGHT); + var newlink = moveright.getAttribute('href').replace('indent=1', 'indent=-1'); + var anchor = new Y.Node.create('') + .setStyle('cursor', 'pointer') + .addClass(CSS.MOVELEFTCLASS) + .setAttribute('href', newlink) + .setAttribute('title', left_string); + anchor.appendChild(newicon); + moveright.insert(anchor, 'before'); + }, + /** + * Edit the title for the resource + */ + edit_resource_title : function(e) { + // Get the element we're working on + var element = e.target.ancestor(CSS.ACTIVITYLI); + var elementdiv = element.one('div'); + var instancename = element.one(CSS.INSTANCENAME); + var currenttitle = instancename.get('firstChild'); + var oldtitle = currenttitle.get('data'); + var titletext = oldtitle; + var editbutton = element.one('a.' + CSS.EDITTITLECLASS + ' img'); + + // Handle events for edit_resource_title + var listenevents = []; + var thisevent; + + // Grab the anchor so that we can swap it with the edit form + var anchor = instancename.ancestor('a'); + + var data = { + 'class' : 'resource', + 'field' : 'gettitle', + 'id' : this.get_element_id(element) + }; + + // Try to retrieve the existing string from the server + var response = this.send_request(data, editbutton); + if (response.instancename) { + titletext = response.instancename; + } + + // Create the editor and submit button + var editor = Y.Node.create('') + .setAttrs({ + 'name' : 'title', + 'value' : titletext, + 'autocomplete' : 'off' + }) + .addClass('titleeditor'); + var editform = Y.Node.create('
            ') + .addClass('activityinstance') + .setAttribute('action', '#'); + var editinstructions = Y.Node.create('') + .addClass('editinstructions') + .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); + var activityicon = element.one('img.activityicon').cloneNode(); + + // Clear the existing content and put the editor in + currenttitle.set('data', ''); + editform.appendChild(activityicon); + editform.appendChild(editor); + anchor.replace(editform); + elementdiv.appendChild(editinstructions); + e.preventDefault(); + + // Focus and select the editor text + editor.focus().select(); + + // Handle removal of the editor + var clear_edittitle = function() { + // Detach all listen events to prevent duplicate triggers + var thisevent; + while (thisevent = listenevents.shift()) { + thisevent.detach(); + } + + if (editinstructions) { + // Convert back to anchor and remove instructions + editform.replace(anchor); + editinstructions.remove(); + editinstructions = null; + } + } + + // Handle cancellation of the editor + var cancel_edittitle = function(e) { + clear_edittitle(); + + // Set the title and anchor back to their previous settings + currenttitle.set('data', oldtitle); + }; + + // Cancel the edit if we lose focus or the escape key is pressed + thisevent = editor.on('blur', cancel_edittitle); + listenevents.push(thisevent); + thisevent = Y.one('document').on('keydown', function(e) { + if (e.keyCode === 27) { + e.preventDefault(); + cancel_edittitle(e); + } + }); + listenevents.push(thisevent); + + // Handle form submission + thisevent = editform.on('submit', function(e) { + // We don't actually want to submit anything + e.preventDefault(); + + // Clear the edit title boxes + clear_edittitle(); + + // We only accept strings which have valid content + var newtitle = Y.Lang.trim(editor.get('value')); + if (newtitle != null && newtitle != "" && newtitle != titletext) { + var data = { + 'class' : 'resource', + 'field' : 'updatetitle', + 'title' : newtitle, + 'id' : this.get_element_id(element) + }; + var response = this.send_request(data, editbutton); + if (response.instancename) { + currenttitle.set('data', response.instancename); + } + } else { + // Invalid content. Set the title back to it's original contents + currenttitle.set('data', oldtitle); + } + }, this); + listenevents.push(thisevent); + }, + /** + * Set the visibility of the current resource (identified by the element) + * to match the hidden parameter (this is not a toggle). + * Only changes the visibility in the browser (no ajax update). + * @param args An object with 'element' being the A node containing the resource + * and 'visible' being the state that the visiblity should be set to. + * @return void + */ + set_visibility_resource_ui: function(args) { + var element = args.element; + var shouldbevisible = args.visible; + var buttonnode = element.one(CSS.SHOW); + var visible = (buttonnode === null); + if (visible) { + buttonnode = element.one(CSS.HIDE); + } + if (visible != shouldbevisible) { + this.toggle_hide_resource_ui(buttonnode); + } + } + }, { + NAME : 'course-resource-toolbox', + ATTRS : { + courseid : { + 'value' : 0 + }, + format : { + 'value' : 'topics' + } + } + }); + + var SECTIONTOOLBOX = function() { + SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); + } + + Y.extend(SECTIONTOOLBOX, TOOLBOX, { + /** + * Initialize the toolboxes module + * + * Updates all span.commands with relevant handlers and other required changes + */ + initializer : function(config) { + this.setup_for_section(); + M.course.coursebase.register_module(this); + + // Section Highlighting + Y.delegate('click', this.toggle_highlight, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.HIGHLIGHT, this); + // Section Visibility + Y.delegate('click', this.toggle_hide_section, CSS.PAGECONTENT, CSS.SECTIONLI + ' ' + CSS.SHOWHIDE, this); + }, + /** + * Update any section areas within the scope of the specified + * selector with AJAX equivelants + * + * @param baseselector The selector to limit scope to + * @return void + */ + setup_for_section : function(baseselector) { + // Left here for potential future use - not currently needed due to YUI delegation in initializer() + /*if (!baseselector) { + var baseselector = CSS.PAGECONTENT; + } + + Y.all(baseselector).each(this._setup_for_section, this);*/ + }, + _setup_for_section : function(toolboxtarget) { + // Left here for potential future use - not currently needed due to YUI delegation in initializer() + }, + toggle_hide_section : function(e) { + // Prevent the default button action + e.preventDefault(); + + // Get the section we're working on + var section = e.target.ancestor(M.course.format.get_section_selector(Y)); + var button = e.target.ancestor('a', true); + var hideicon = button.one('img'); + + // The value to submit + var value; + // The status text for strings and images + var status; + + if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) { + section.addClass(CSS.SECTIONHIDDENCLASS); + value = 0; + status = 'show'; + + } else { + section.removeClass(CSS.SECTIONHIDDENCLASS); + value = 1; + status = 'hide'; + } + + var newstring = M.util.get_string(status + 'fromothers', 'format_' + this.get('format')); + hideicon.setAttrs({ + 'alt' : newstring, + 'src' : M.util.image_url('i/' + status) + }); + button.set('title', newstring); + + // Change the highlight status + var data = { + 'class' : 'section', + 'field' : 'visible', + 'id' : this.get_section_id(section.ancestor(M.course.format.get_section_wrapper(Y), true)), + 'value' : value + }; + + var lightbox = M.util.add_lightbox(Y, section); + lightbox.show(); + + var response = this.send_request(data, lightbox); + + var activities = section.all(CSS.ACTIVITYLI); + activities.each(function(node) { + if (node.one(CSS.SHOW)) { + var button = node.one(CSS.SHOW); + } else { + var button = node.one(CSS.HIDE); + } + var activityid = this.get_element_id(node); + + if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) { + this.toggle_hide_resource_ui(button); + } + }, this); + }, + toggle_highlight : function(e) { + // Prevent the default button action + e.preventDefault(); + + // Get the section we're working on + var section = e.target.ancestor(M.course.format.get_section_selector(Y)); + var button = e.target.ancestor('a', true); + var buttonicon = button.one('img'); + + // Determine whether the marker is currently set + var togglestatus = section.hasClass('current'); + var value = 0; + + // Set the current highlighted item text + var old_string = M.util.get_string('markthistopic', 'moodle'); + Y.one(CSS.PAGECONTENT) + .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT) + .set('title', old_string); + Y.one(CSS.PAGECONTENT) + .all(M.course.format.get_section_selector(Y) + '.current ' + CSS.HIGHLIGHT + ' img') + .set('alt', old_string) + .set('src', M.util.image_url('i/marker')); + + // Remove the highlighting from all sections + var allsections = Y.one(CSS.PAGECONTENT).all(M.course.format.get_section_selector(Y)) + .removeClass('current'); + + // Then add it if required to the selected section + if (!togglestatus) { + section.addClass('current'); + value = this.get_section_id(section.ancestor(M.course.format.get_section_wrapper(Y), true)); + var new_string = M.util.get_string('markedthistopic', 'moodle'); + button + .set('title', new_string); + buttonicon + .set('alt', new_string) + .set('src', M.util.image_url('i/marked')); + } + + // Change the highlight status + var data = { + 'class' : 'course', + 'field' : 'marker', + 'value' : value + }; + var lightbox = M.util.add_lightbox(Y, section); + lightbox.show(); + this.send_request(data, lightbox); + } + }, { + NAME : 'course-section-toolbox', + ATTRS : { + courseid : { + 'value' : 0 + }, + format : { + 'value' : 'topics' + } + } + }); + + M.course = M.course || {}; + + M.course.init_resource_toolbox = function(config) { + return new RESOURCETOOLBOX(config); + }; + + M.course.init_section_toolbox = function(config) { + return new SECTIONTOOLBOX(config); + }; + +}, +'@VERSION@', { + requires : ['base', 'node', 'io', 'moodle-course-coursebase'] +} +);