From 3d54e6763392dc215b361a73eecb9b7fe0cb40a2 Mon Sep 17 00:00:00 2001 From: guillaume pellerin Date: Mon, 16 Sep 2013 16:41:32 +0200 Subject: [PATCH 1/1] init --- category.php | 479 +++ changenumsections.php | 63 + completion.js | 109 + completion.php | 159 + completion_form.php | 222 ++ delete.php | 87 + delete_category_form.php | 151 + dndupload.js | 982 +++++ dndupload.php | 39 + dnduploadlib.php | 696 ++++ edit.php | 173 + edit_form.php | 358 ++ editcategory.php | 162 + editcategory_form.php | 74 + editsection.php | 91 + editsection_form.php | 330 ++ enrol.php | 30 + externallib.php | 1813 +++++++++ format/README.txt | 146 + format/formatlegacy.php | 363 ++ format/lib.php | 941 +++++ format/renderer.php | 797 ++++ format/scorm/format.php | 17 + format/scorm/lang/en/format_scorm.php | 27 + format/scorm/lib.php | 101 + format/scorm/version.php | 30 + format/social/format.php | 35 + format/social/lang/en/format_social.php | 27 + format/social/lib.php | 78 + format/social/version.php | 30 + format/topics/format.js | 73 + format/topics/format.php | 60 + format/topics/lang/en/format_topics.php | 33 + format/topics/lib.php | 299 ++ format/topics/renderer.php | 105 + format/topics/styles.css | 10 + format/topics/version.php | 30 + format/upgrade.txt | 29 + format/weeks/format.js | 73 + format/weeks/format.php | 52 + format/weeks/lang/en/format_weeks.php | 33 + format/weeks/lib.php | 349 ++ format/weeks/renderer.php | 62 + format/weeks/styles.css | 10 + format/weeks/version.php | 30 + index.php | 398 ++ info.php | 61 + jumpto.php | 41 + lib.php | 4755 +++++++++++++++++++++++ loginas.php | 75 + mod.php | 337 ++ modduplicate.php | 151 + modedit.php | 665 ++++ moodleform_mod.php | 862 ++++ pending.php | 149 + publish/backup.php | 124 + publish/forms.php | 392 ++ publish/hubselector.php | 77 + publish/index.php | 180 + publish/lib.php | 299 ++ publish/metadata.php | 274 ++ publish/renderer.php | 218 ++ recent.php | 294 ++ recent_form.php | 169 + renderer.php | 357 ++ report.php | 41 + report/lib.php | 38 + request.php | 73 + request_form.php | 152 + reset.php | 106 + reset_form.php | 135 + resources.php | 143 + rest.php | 239 ++ scales.php | 143 + search.php | 435 +++ style.css | 0 switchrole.php | 87 + tests/courselib_test.php | 438 +++ tests/courserequest_test.php | 148 + tests/externallib_test.php | 640 +++ togglecompletion.php | 176 + user.php | 139 + view.php | 288 ++ yui/coursebase/coursebase.js | 224 ++ yui/dragdrop/dragdrop.js | 428 ++ yui/formatchooser/formatchooser.js | 25 + yui/modchooser/modchooser.js | 166 + yui/toolboxes/toolboxes.js | 800 ++++ 88 files changed, 24800 insertions(+) create mode 100644 category.php create mode 100644 changenumsections.php create mode 100644 completion.js create mode 100644 completion.php create mode 100644 completion_form.php create mode 100644 delete.php create mode 100644 delete_category_form.php create mode 100644 dndupload.js create mode 100644 dndupload.php create mode 100644 dnduploadlib.php create mode 100644 edit.php create mode 100644 edit_form.php create mode 100644 editcategory.php create mode 100644 editcategory_form.php create mode 100644 editsection.php create mode 100644 editsection_form.php create mode 100644 enrol.php create mode 100644 externallib.php create mode 100644 format/README.txt create mode 100644 format/formatlegacy.php create mode 100644 format/lib.php create mode 100644 format/renderer.php create mode 100644 format/scorm/format.php create mode 100644 format/scorm/lang/en/format_scorm.php create mode 100644 format/scorm/lib.php create mode 100644 format/scorm/version.php create mode 100644 format/social/format.php create mode 100644 format/social/lang/en/format_social.php create mode 100644 format/social/lib.php create mode 100644 format/social/version.php create mode 100644 format/topics/format.js create mode 100644 format/topics/format.php create mode 100644 format/topics/lang/en/format_topics.php create mode 100644 format/topics/lib.php create mode 100644 format/topics/renderer.php create mode 100644 format/topics/styles.css create mode 100644 format/topics/version.php create mode 100644 format/upgrade.txt create mode 100644 format/weeks/format.js create mode 100644 format/weeks/format.php create mode 100644 format/weeks/lang/en/format_weeks.php create mode 100644 format/weeks/lib.php create mode 100644 format/weeks/renderer.php create mode 100644 format/weeks/styles.css create mode 100644 format/weeks/version.php create mode 100644 index.php create mode 100644 info.php create mode 100644 jumpto.php create mode 100644 lib.php create mode 100644 loginas.php create mode 100644 mod.php create mode 100644 modduplicate.php create mode 100644 modedit.php create mode 100644 moodleform_mod.php create mode 100644 pending.php create mode 100644 publish/backup.php create mode 100644 publish/forms.php create mode 100644 publish/hubselector.php create mode 100644 publish/index.php create mode 100644 publish/lib.php create mode 100644 publish/metadata.php create mode 100644 publish/renderer.php create mode 100644 recent.php create mode 100644 recent_form.php create mode 100644 renderer.php create mode 100644 report.php create mode 100644 report/lib.php create mode 100644 request.php create mode 100644 request_form.php create mode 100644 reset.php create mode 100644 reset_form.php create mode 100644 resources.php create mode 100644 rest.php create mode 100644 scales.php create mode 100644 search.php create mode 100644 style.css create mode 100644 switchrole.php create mode 100644 tests/courselib_test.php create mode 100644 tests/courserequest_test.php create mode 100644 tests/externallib_test.php create mode 100644 togglecompletion.php create mode 100644 user.php create mode 100644 view.php create mode 100644 yui/coursebase/coursebase.js create mode 100644 yui/dragdrop/dragdrop.js create mode 100644 yui/formatchooser/formatchooser.js create mode 100644 yui/modchooser/modchooser.js create mode 100644 yui/toolboxes/toolboxes.js 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'] +} +); -- 2.39.5