From 83ea0cc17e54500ea0feeaf9f85f6126f03bed5e Mon Sep 17 00:00:00 2001 From: Adam Olley Date: Wed, 27 Jun 2012 16:48:46 +0800 Subject: [PATCH] MDL-19430 My Moodle: Merged Netspot My Course Overview Block for Moodle 2 --- blocks/course_overview/README | 33 +++ .../course_overview/block_course_overview.php | 112 ++++---- blocks/course_overview/db/install.php | 28 ++ blocks/course_overview/db/upgrade.php | 30 +++ .../lang/en/block_course_overview.php | 26 ++ blocks/course_overview/locallib.php | 167 ++++++++++++ blocks/course_overview/module.js | 226 +++++++++++++++++ blocks/course_overview/move.php | 58 +++++ blocks/course_overview/pix/collapse_all.png | Bin 0 -> 3077 bytes .../pix/colltopics-arrow-current.png | Bin 0 -> 3168 bytes .../pix/colltopics-arrow-down-current.png | Bin 0 -> 3157 bytes .../pix/colltopics-arrow-down.png | Bin 0 -> 3280 bytes .../pix/colltopics-arrow-right.png | Bin 0 -> 3300 bytes blocks/course_overview/pix/expand_all.png | Bin 0 -> 3066 bytes blocks/course_overview/renderer.php | 240 ++++++++++++++++++ blocks/course_overview/save.php | 31 +++ blocks/course_overview/settings.php | 16 ++ blocks/course_overview/styles.css | 83 ++++++ blocks/course_overview/version.php | 4 +- 19 files changed, 984 insertions(+), 70 deletions(-) create mode 100644 blocks/course_overview/README create mode 100644 blocks/course_overview/db/install.php create mode 100644 blocks/course_overview/db/upgrade.php create mode 100644 blocks/course_overview/locallib.php create mode 100644 blocks/course_overview/module.js create mode 100644 blocks/course_overview/move.php create mode 100644 blocks/course_overview/pix/collapse_all.png create mode 100644 blocks/course_overview/pix/colltopics-arrow-current.png create mode 100644 blocks/course_overview/pix/colltopics-arrow-down-current.png create mode 100644 blocks/course_overview/pix/colltopics-arrow-down.png create mode 100644 blocks/course_overview/pix/colltopics-arrow-right.png create mode 100644 blocks/course_overview/pix/expand_all.png create mode 100644 blocks/course_overview/renderer.php create mode 100644 blocks/course_overview/save.php create mode 100644 blocks/course_overview/settings.php create mode 100644 blocks/course_overview/styles.css diff --git a/blocks/course_overview/README b/blocks/course_overview/README new file mode 100644 index 00000000000..2aa3aa5e2ed --- /dev/null +++ b/blocks/course_overview/README @@ -0,0 +1,33 @@ +My Course Overview Block for Moodle 2 + +== Main feautres == + +Provides a user configurable list of the courses a user is enrolled in on their My Moodle page. + +The user can set the number of courses to have visible. + +Courses can be reordered in editing mode via drag and drop (with appropriate fallback when ajax disabled for the user). + +Each course can be expanded to show a list of activity icons alerting the user to updated modules or modules requiring attention. + +Optionally can display the shortnames of child courses under the heading of the course (site-wide option). + +Optionally can display a welcome area above the courses that welcomes the user and alerts them of any pending messages. + +== Installing and configuring == + +Place the code for the block under /blocks/my_course_overview and install the block via the Moodle upgrade process. + +== Credits == + +Coding: NetSpot (http://www.netspot.com.au) + +Jointly Funded and Designed by: + - Flinders University (http://www.flinders.edu.au) + - University of Canberra (http://www.canberra.edu.au) + +== See also == + +[https://github.com/netspotau/moodle-block_my_course_overview Github page] + +[[Category: Contributed code]] diff --git a/blocks/course_overview/block_course_overview.php b/blocks/course_overview/block_course_overview.php index df79e3bf2d4..78f45ca3575 100644 --- a/blocks/course_overview/block_course_overview.php +++ b/blocks/course_overview/block_course_overview.php @@ -1,5 +1,4 @@ dirroot.'/lib/weblib.php'); -require_once($CFG->dirroot . '/lib/formslib.php'); +require_once($CFG->dirroot.'/blocks/course_overview/locallib.php'); class block_course_overview extends block_base { /** * block initializations */ public function init() { - $this->title = get_string('pluginname', 'block_course_overview'); + $this->title = get_string('displaytitle', 'block_course_overview'); } /** @@ -41,82 +38,53 @@ class block_course_overview extends block_base { * @return object */ public function get_content() { - global $USER, $CFG; + global $USER, $CFG, $DB; + require_once($CFG->dirroot.'/user/profile/lib.php'); + if($this->content !== NULL) { return $this->content; } + $config = get_config('block_course_overview'); + $this->content = new stdClass(); $this->content->text = ''; $this->content->footer = ''; $content = array(); - // limits the number of courses showing up - $courses_limit = 21; - // FIXME: this should be a block setting, rather than a global setting - if (isset($CFG->mycoursesperpage)) { - $courses_limit = $CFG->mycoursesperpage; + $moving = optional_param('course_moveid', 0, PARAM_INT); + $updatemynumber = optional_param('mynumber', null, PARAM_INT); + if (!is_null($updatemynumber)) { + block_course_overview_update_mynumber($updatemynumber); } - $morecourses = false; - if ($courses_limit > 0) { - $courses_limit = $courses_limit + 1; + profile_load_custom_fields($USER); + list($courses_sorted, $courses_total) = block_course_overview_get_sorted_courses(); + $overviews = block_course_overview_get_overviews($courses_sorted); + + $renderer = $this->page->get_renderer('block_course_overview'); + if (!isset($config->showwelcomearea) || $config->showwelcomearea) { + $this->content->text = $renderer->welcome_area(); } - $courses = enrol_get_my_courses('id, shortname, modinfo', 'visible DESC,sortorder ASC', $courses_limit); - $site = get_site(); - $course = $site; //just in case we need the old global $course hack - - if (is_enabled_auth('mnet')) { - $remote_courses = get_my_remotecourses(); - } - if (empty($remote_courses)) { - $remote_courses = array(); + //number of sites to display + if ($this->page->user_is_editing()) { + $count = count(enrol_get_my_courses('id')); + $this->content->text .= $renderer->editing_bar_head($count); } - if (($courses_limit > 0) && (count($courses)+count($remote_courses) >= $courses_limit)) { - // get rid of any remote courses that are above the limit - $remote_courses = array_slice($remote_courses, 0, $courses_limit - count($courses), true); - if (count($courses) >= $courses_limit) { - //remove the 'marker' course that we retrieve just to see if we have more than $courses_limit - array_pop($courses); - } - $morecourses = true; - } - - - if (array_key_exists($site->id,$courses)) { - unset($courses[$site->id]); - } - - foreach ($courses as $c) { - if (isset($USER->lastcourseaccess[$c->id])) { - $courses[$c->id]->lastaccess = $USER->lastcourseaccess[$c->id]; - } else { - $courses[$c->id]->lastaccess = 0; - } - } - - if (empty($courses) && empty($remote_courses)) { - $content[] = get_string('nocourses','my'); + if (empty($courses_sorted)) { + $this->content->text .= get_string('nocourses','my'); } else { - ob_start(); - - require_once $CFG->dirroot."/course/lib.php"; - print_overview($courses, $remote_courses); - - $content[] = ob_get_contents(); - ob_end_clean(); + //for each course, build category cache + $this->content->text .= $renderer->course_overview($courses_sorted, $overviews, $moving); + $this->content->text .= $renderer->hidden_courses($courses_total - count($courses_sorted)); + if ($this->page->user_is_editing() && ajaxenabled() && !$moving) { + $this->page->requires->js_init_call('M.block_course_overview.add_handles'); + } } - // if more than 20 courses - if ($morecourses) { - $content[] = '
...'; - } - - $this->content->text = implode($content); - return $this->content; } @@ -126,7 +94,7 @@ class block_course_overview extends block_base { * @return boolean */ public function has_config() { - return false; + return true; } /** @@ -135,7 +103,15 @@ class block_course_overview extends block_base { * @return array */ public function applicable_formats() { - return array('my-index'=>true); + return array('my-index' => true); + } + + public function instance_can_be_hidden() { + return false; + } + + public function hide_header() { + return true; } } ?> diff --git a/blocks/course_overview/db/install.php b/blocks/course_overview/db/install.php new file mode 100644 index 00000000000..67d6914cf8f --- /dev/null +++ b/blocks/course_overview/db/install.php @@ -0,0 +1,28 @@ +get_record('user_info_field', array('shortname' => 'mynumber'))) { + $field = new stdClass(); + $field->shortname = 'mynumber'; + $field->name = 'Number of courses on My Moodle'; + $field->datatype = 'text'; + $field->visible = 1; + $field->categoryid = 1; + + $DB->insert_record('user_info_field', $field); + } + + if (!$DB->get_record('user_info_field', array('shortname' => 'myorder'))) { + $field = new stdClass(); + $field->shortname = 'myorder'; + $field->name = 'Order of courses on My Moodle'; + $field->datatype = 'text'; + $field->visible = 0; + $field->categoryid = 1; + + $DB->insert_record('user_info_field', $field); + } +} \ No newline at end of file diff --git a/blocks/course_overview/db/upgrade.php b/blocks/course_overview/db/upgrade.php new file mode 100644 index 00000000000..f59f7a52468 --- /dev/null +++ b/blocks/course_overview/db/upgrade.php @@ -0,0 +1,30 @@ +get_record('user_info_field', array('shortname' => 'mynumber'))) { + $field = new stdClass(); + $field->shortname = 'mynumber'; + $field->name = 'Number of courses on My Moodle'; + $field->datatype = 'text'; + $field->visible = 1; + $field->categoryid = 1; + + $DB->insert_record('user_info_field', $field); + } + + if (!$DB->get_record('user_info_field', array('shortname' => 'myorder'))) { + $field = new stdClass(); + $field->shortname = 'myorder'; + $field->name = 'Order of courses on My Moodle'; + $field->datatype = 'text'; + $field->visible = 0; + $field->categoryid = 1; + + $DB->insert_record('user_info_field', $field); + } + upgrade_block_savepoint(true, 2012062800, 'course_overview'); + } + return true; +} diff --git a/blocks/course_overview/lang/en/block_course_overview.php b/blocks/course_overview/lang/en/block_course_overview.php index c103d8e2543..f7b2063b9aa 100644 --- a/blocks/course_overview/lang/en/block_course_overview.php +++ b/blocks/course_overview/lang/en/block_course_overview.php @@ -1,2 +1,28 @@ . + +function block_course_overview_get_overviews($courses) { + global $CFG, $USER, $DB, $OUTPUT; + $htmlarray = array(); + if ($modules = $DB->get_records('modules')) { + foreach ($modules as $mod) { + if (file_exists(dirname(__FILE__).'/../../mod/'.$mod->name.'/lib.php')) { + include_once(dirname(__FILE__).'/../../mod/'.$mod->name.'/lib.php'); + $fname = $mod->name.'_print_overview'; + if (function_exists($fname)) { + $fname($courses,$htmlarray); + } + } + } + } + return $htmlarray; +} + +function block_course_overview_update_mynumber($number) { + global $DB, $USER; + if ($field = $DB->get_record('user_info_field', array('shortname' => 'mynumber'))) { + if ($data = $DB->get_record('user_info_data', array('fieldid' => $field->id, 'userid' => $USER->id))) { + $data->data = $number; + $DB->update_record('user_info_data', $data); + } else { + $data = new stdClass(); + $data->fieldid = $field->id; + $data->userid = $USER->id; + $data->data = $number; + $DB->insert_record('user_info_data', $data); + } + } +} + +function block_course_overview_update_myorder($sortorder) { + global $DB, $USER; + if ($field = $DB->get_record('user_info_field', array('shortname' => 'myorder'))) { + if ($data = $DB->get_record('user_info_data', array('fieldid' => $field->id, 'userid' => $USER->id))) { + + $oldlist = explode(',', $data->data); + $newlist = explode(',', $sortorder); + foreach ($oldlist as $oldentry) { + if (!in_array($oldentry, $newlist)) { + $newlist[] = $oldentry; + } + } + $sortorder = implode(',', $newlist); + + $data->data = $sortorder; + $DB->update_record('user_info_data', $data); + } else { + $data = new stdClass(); + $data->fieldid = $field->id; + $data->userid = $USER->id; + $data->data = $sortorder; + $DB->insert_record('user_info_data', $data); + } + } +} + +function block_course_overview_get_child_shortnames($courseid) { + global $COURSE, $DB, $OUTPUT; + + $sql = "SELECT c.shortname + FROM {enrol} AS e + JOIN {course} AS c ON (c.id = e.customint1) + WHERE e.courseid = :courseid AND e.enrol = :method ORDER BY e.sortorder"; + $params = array('method' => 'meta', 'courseid' => $courseid); + if ($results = $DB->get_records_sql($sql, $params)) { + $shortnames = array(); + foreach ($results as $res) { + $shortnames[] = $res->shortname; + } + $total = count($shortnames); + $suffix = ''; + if ($total > 10) { + $shortnames = array_slice($shortnames, 0, 10); + $diff = $total - count($shortnames); + $plural = $diff > 1 ? 's' : ''; + $suffix = " (and $diff other$plural)"; + } + $shortnames = 'includes '.implode('; ', $shortnames).$suffix; + } + + return isset($shortnames) ? $shortnames : false; +} + +function block_course_overview_get_sorted_courses() { + global $USER; + + $limit = 71; //TODO: Make this a block setting + if (isset($USER->profile['mynumber']) && intval($USER->profile['mynumber']) > 0) { + $limit = intval($USER->profile['mynumber']); + } else { + $USER->profile['mynumber'] = 0; + } + + $courses = enrol_get_my_courses('id, shortname, fullname, modinfo'); + $site = get_site(); + + if (array_key_exists($site->id,$courses)) { + unset($courses[$site->id]); + } + + foreach ($courses as $c) { + if (isset($USER->lastcourseaccess[$c->id])) { + $courses[$c->id]->lastaccess = $USER->lastcourseaccess[$c->id]; + } else { + $courses[$c->id]->lastaccess = 0; + } + } + + $order = array(); + if (isset($USER->profile['myorder'])) { + $order = explode(',', $USER->profile['myorder']); + } + + $courses_sorted = array(); + + //unsorted courses top of the list + foreach ($courses as $c) { + if (count($courses_sorted) >= $limit) { + break; + } + if (!in_array($c->id, $order)) { + $courses_sorted[$c->id] = $c; + } + } + + //get courses in sort order into list + foreach ($order as $o) { + if (count($courses_sorted) >= $limit) { + break; + } + if (isset($courses[intval($o)])) { + $courses_sorted[$o] = $courses[$o]; + } + } + + //append the remaining courses onto the end of the list + foreach ($courses as $c) { + if (count($courses_sorted) >= $limit) { + break; + } + if (!isset($courses_sorted[$c->id])) { + $courses_sorted[$c->id] = $c; + } + } + + return array($courses_sorted, count($courses)); +} diff --git a/blocks/course_overview/module.js b/blocks/course_overview/module.js new file mode 100644 index 00000000000..6a6536d56e9 --- /dev/null +++ b/blocks/course_overview/module.js @@ -0,0 +1,226 @@ +M.block_course_overview = {} + +M.block_course_overview.add_handles = function(Y) { + M.block_course_overview.Y = Y; + YUI().use('dd-constrain', 'dd-proxy', 'dd-drop', 'dd-plugin', function(Y) { + //Static Vars + var goingUp = false, lastY = 0; + + var list = Y.Node.all('#course_list .coursebox'); + list.each(function(v, k) { + var dd = new Y.DD.Drag({ + node: v, + target: { + padding: '0 0 0 20' + } + }).plug(Y.Plugin.DDProxy, { + moveOnEnd: false + }).plug(Y.Plugin.DDConstrained, { + constrain2node: '#course_list' + }); + dd.addHandle('.course_title .move'); + }); + + var drops = Y.Node.all('#coursebox'); + drops.each(function(v, k) { + var tar = new Y.DD.Drop({ + node: v + }); + }); + + Y.DD.DDM.on('drag:start', function(e) { + //Get our drag object + var drag = e.target; + //Set some styles here + drag.get('node').setStyle('opacity', '.25'); + drag.get('dragNode').addClass('block_course_overview'); + drag.get('dragNode').set('innerHTML', drag.get('node').get('innerHTML')); + drag.get('dragNode').setStyles({ + opacity: '.5', + borderColor: drag.get('node').getStyle('borderColor'), + backgroundColor: drag.get('node').getStyle('backgroundColor') + }); + }); + + Y.DD.DDM.on('drag:end', function(e) { + var drag = e.target; + //Put our styles back + drag.get('node').setStyles({ + visibility: '', + opacity: '1' + }); + M.block_course_overview.save(Y); + }); + + Y.DD.DDM.on('drag:drag', function(e) { + //Get the last y point + var y = e.target.lastXY[1]; + //is it greater than the lastY var? + if (y < lastY) { + //We are going up + goingUp = true; + } else { + //We are going down. + goingUp = false; + } + //Cache for next check + lastY = y; + }); + + Y.DD.DDM.on('drop:over', function(e) { + //Get a reference to our drag and drop nodes + var drag = e.drag.get('node'), + drop = e.drop.get('node'); + + //Are we dropping on a li node? + if (drop.hasClass('coursebox')) { + //Are we not going up? + if (!goingUp) { + drop = drop.get('nextSibling'); + } + //Add the node to this list + e.drop.get('node').get('parentNode').insertBefore(drag, drop); + //Resize this nodes shim, so we can drop on it later. + e.drop.sizeShim(); + } + }); + + Y.DD.DDM.on('drag:drophit', function(e) { + var drop = e.drop.get('node'), + drag = e.drag.get('node'); + + //if we are not on an li, we must have been dropped on a ul + if (!drop.hasClass('coursebox')) { + if (!drop.contains(drag)) { + drop.appendChild(drag); + } + } + }); + }); +} + +M.block_course_overview.save = function() { + var Y = M.block_course_overview.Y; + var sortorder = Y.one('#course_list').get('children').getAttribute('id'); + for (var i = 0; i < sortorder.length; i++) { + sortorder[i] = sortorder[i].substring(7); + } + var params = { + sesskey : M.cfg.sesskey, + sortorder : sortorder + }; + Y.io(M.cfg.wwwroot+'/blocks/course_overview/save.php', { + method: 'POST', + data: build_querystring(params), + context: this + }); +} + +/** + * Init a collapsible region, see print_collapsible_region in weblib.php + * @param {YUI} Y YUI3 instance with all libraries loaded + * @param {String} id the HTML id for the div. + * @param {String} userpref the user preference that records the state of this box. false if none. + * @param {String} strtooltip + */ +M.block_course_overview.collapsible = function(Y, id, userpref, strtooltip) { + if (userpref) { + M.block_course_overview.userpref = true; + } + Y.use('anim', function(Y) { + new M.block_course_overview.CollapsibleRegion(Y, id, userpref, strtooltip); + }); +}; + +/** + * Object to handle a collapsible region : instantiate and forget styled object + * + * @class + * @constructor + * @param {YUI} Y YUI3 instance with all libraries loaded + * @param {String} id The HTML id for the div. + * @param {String} userpref The user preference that records the state of this box. false if none. + * @param {String} strtooltip + */ +M.block_course_overview.CollapsibleRegion = function(Y, id, userpref, strtooltip) { + // Record the pref name + this.userpref = userpref; + + // Find the divs in the document. + this.div = Y.one('#'+id); + + // Get the caption for the collapsible region + var caption = this.div.one('#'+id + '_caption'); + caption.setAttribute('title', strtooltip); + + // Create a link + var a = Y.Node.create(''); + // Create a local scoped lamba function to move nodes to a new link + var movenode = function(node){ + node.remove(); + a.append(node); + }; + // Apply the lamba function on each of the captions child nodes + caption.get('children').each(movenode, this); + caption.prepend(a); + + // Get the height of the div at this point before we shrink it if required + var height = this.div.get('offsetHeight'); + if (this.div.hasClass('collapsed')) { + // Shrink the div as it is collapsed by default + this.div.setStyle('height', caption.get('offsetHeight')+'px'); + } + + // Create the animation. + var animation = new Y.Anim({ + node: this.div, + duration: 0.3, + easing: Y.Easing.easeBoth, + to: {height:caption.get('offsetHeight')}, + from: {height:height} + }); + + // Handler for the animation finishing. + animation.on('end', function() { + this.div.toggleClass('collapsed'); + }, this); + + // Hook up the event handler. + caption.on('click', function(e, animation) { + e.preventDefault(); + // Animate to the appropriate size. + if (animation.get('running')) { + animation.stop(); + } + animation.set('reverse', this.div.hasClass('collapsed')); + // Update the user preference. + if (this.userpref) { + M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed')); + } + animation.run(); + }, this, animation); +}; + +M.block_course_overview.userpref = false; + +/** + * The user preference that stores the state of this box. + * @property userpref + * @type String + */ +M.block_course_overview.CollapsibleRegion.prototype.userpref = null; + +/** + * The key divs that make up this + * @property div + * @type Y.Node + */ +M.block_course_overview.CollapsibleRegion.prototype.div = null; + +/** + * The key divs that make up this + * @property icon + * @type Y.Node + */ +M.block_course_overview.CollapsibleRegion.prototype.icon = null; + diff --git a/blocks/course_overview/move.php b/blocks/course_overview/move.php new file mode 100644 index 00000000000..eba21d55979 --- /dev/null +++ b/blocks/course_overview/move.php @@ -0,0 +1,58 @@ +. + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); +require_once($CFG->dirroot.'/user/profile/lib.php'); + +require_login(); + +$source = required_param('source', PARAM_INT); +$target = required_param('target', PARAM_INT); + +profile_load_custom_fields($USER); + +//get current sortorder +$sortorder = array(); +if (isset($USER->profile['myorder'])) { + $mysortorder = explode(',', $USER->profile['myorder']); +} + +$courses_sorted = block_course_overview_get_sorted_courses(); +$sortorder = array_keys($courses_sorted); + +//now resort based on new weight for chosen course +$neworder = array(); +reset($sortorder); +foreach ($sortorder as $key => $value) { + if ($value == $source) { + unset($sortorder[$key]); + break; + } +} +for ($i = 0; $i <= count($sortorder) + 1; $i++) { + if ($i == $target) { + $neworder[] = $source; + } + if (isset($sortorder[$i])) { + $neworder[] = $sortorder[$i]; + } +} +$neworder = implode(',', $neworder); +block_course_overview_update_myorder($neworder); + +redirect(new moodle_url('/my/index.php')); diff --git a/blocks/course_overview/pix/collapse_all.png b/blocks/course_overview/pix/collapse_all.png new file mode 100644 index 0000000000000000000000000000000000000000..5d285a7d1cb3cb6787d286793ef8c14622e19991 GIT binary patch literal 3077 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 literal 0 HcmV?d00001 diff --git a/blocks/course_overview/pix/colltopics-arrow-down.png b/blocks/course_overview/pix/colltopics-arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..228c390e7ddf2c1c12736f4da1082342ea0dd0f3 GIT binary patch literal 3280 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 IM6N<$f`t{iAOHXW literal 0 HcmV?d00001 diff --git a/blocks/course_overview/renderer.php b/blocks/course_overview/renderer.php new file mode 100644 index 00000000000..dc510d5a3b7 --- /dev/null +++ b/blocks/course_overview/renderer.php @@ -0,0 +1,240 @@ +. + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/message/lib.php'); + +class block_course_overview_renderer extends plugin_renderer_base { + + public function course_overview($courses, $overviews, $moving = 0) { + global $OUTPUT; + $html = ''; + $config = get_config('block_course_overview'); + + $html .= html_writer::start_tag('div', array('id' => 'course_list')); + $weight = 0; + $keylist = array_keys($courses); + if ($this->page->user_is_editing() && $moving && $keylist[0] != $moving) { + $html .= $this->course_move_target($moving, $weight); + $weight++; + } + foreach ($courses as $course) { + $cursor = ajaxenabled() && !$moving ? ' cursor' : ''; + $html .= $OUTPUT->box_start('coursebox', "course-{$course->id}"); + $html .= html_writer::start_tag('div', array('class' => 'course_title')); + if ($this->page->user_is_editing()) { + // Move icon. + $icon = ajaxenabled() ? 'i/move_2d' : 't/move'; + $control = array('url' => new moodle_url('/my/index.php', array('course_moveid' => $course->id)), + 'icon' => $icon, 'caption' => get_string('move')); + if (ajaxenabled()) { + $html .= html_writer::tag('div', + html_writer::empty_tag('img', + array('src' => $this->pix_url($control['icon'])->out(false), + 'alt' => $control['caption'], 'class' => 'icon cursor', + 'title' => $control['caption']) + ), array('class' => 'move') + ); + } else { + $html .= html_writer::tag('a', + html_writer::empty_tag('img', array('src' => $this->pix_url($control['icon'])->out(false), 'alt' => $control['caption'])), + array('class' => 'icon move','title' => $control['caption'], 'href' => $control['url'])); + } + } + $link = html_writer::link(new moodle_url('/course/view.php', array('id' => $course->id)), $course->fullname); + $html .= $OUTPUT->heading($link, 2, 'title'); + $html .= $OUTPUT->box('', 'flush'); + $html .= html_writer::end_tag('div'); + + if (isset($config->showchildren) && $config->showchildren) { + //list children here + if ($children = block_course_overview_get_child_shortnames($course->id)) { + $html .= html_writer::tag('span', $children, array('class' => 'coursechildren')); + } + } + + if (isset($overviews[$course->id])) { + $html .= $this->activity_display($course->id, $overviews[$course->id]); + } + + $html .= $OUTPUT->box('', 'flush'); + $html .= $OUTPUT->box_end(); + + if ($this->page->user_is_editing() && $moving && $course->id != $moving) { + //check if next is the course we're moving + $okay = true; + if (isset($keylist[$weight])) { + if ($courses[$keylist[$weight]]->id == $moving) { + $okay = false; + } + } + if ($okay) { + $html .= $this->course_move_target($moving, $weight); + } + } + $weight++; + } + $html .= html_writer::end_tag('div'); + + return $html; + } + + protected function course_move_target($cid, $weight) { + $url = new moodle_url('/blocks/course_overview/move.php', array('source' => $cid, 'target' => $weight)); + return html_writer::tag('a', html_writer::tag('span', get_string('movecoursehere', 'block_course_overview'), + array('class' => 'accesshide')), array('href' => $url, 'class' => 'coursemovetarget')); + } + + protected function activity_display($cid, $overview) { + global $OUTPUT; + $output = html_writer::start_tag('div', array('class' => 'activity_info')); + foreach (array_keys($overview) as $module) { + $output .= html_writer::start_tag('div', array('class' => 'activity_overview')); + $url = new moodle_url("/mod/$module/index.php", array('id' => $cid)); + $icontext = html_writer::link($url, $OUTPUT->pix_icon('icon', get_string('modulename', $module), 'mod_'.$module, array('class'=>'icon')).' '); + if (get_string_manager()->string_exists("activity_$module", 'block_course_overview')) { + $icontext .= get_string("activity_$module", 'block_course_overview'); + } else { + $icontext .= get_string("activityoverview", 'block_course_overview', $module); + } + + //add collapsible region with overview text in it + $output .= $this->collapsible_region($overview[$module], '', 'region_'.$cid.'_'.$module, $icontext, '', true); + + $output .= html_writer::end_tag('div'); + } + $output .= html_writer::end_tag('div'); + return $output; + } + + public function editing_bar_head($max = 0) { + global $OUTPUT, $USER; + $output = $OUTPUT->box_start('notice'); + + $options = array('0' => get_string('alwaysshowall', 'block_course_overview')); + for ($i = 1; $i <= $max; $i++) { + $options[$i] = $i; + } + $url = new moodle_url('/my/index.php'); + $select = new single_select($url, 'mynumber', $options, $USER->profile['mynumber'], array()); + $select->set_label(get_string('numtodisplay', 'block_course_overview')); + $output .= $OUTPUT->render($select); + + $output .= $OUTPUT->box_end(); + return $output; + } + + public function hidden_courses($total) { + global $OUTPUT; + if ($total <= 0) { + return; + } + $output = $OUTPUT->box_start('notice'); + $plural = $total > 1 ? 'plural' : ''; + $output .= get_string('hiddencoursecount'.$plural, 'block_course_overview', $total); + $output .= $OUTPUT->box_end(); + return $output; + } + + protected function collapsible_region($contents, $classes, $id, $caption, $userpref = '', $default = false) { + $output = $this->collapsible_region_start($classes, $id, $caption, $userpref, $default); + $output .= $contents; + $output .= $this->collapsible_region_end(); + + return $output; + } + + /** + * Print (or return) the start of a collapsible region, that has a caption that can + * be clicked to expand or collapse the region. If JavaScript is off, then the region + * will always be expanded. + * + * @param string $classes class names added to the div that is output. + * @param string $id id added to the div that is output. Must not be blank. + * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract. + * @param string $userpref the name of the user preference that stores the user's preferred default state. + * (May be blank if you do not wish the state to be persisted. + * @param boolean $default Initial collapsed state to use if the user_preference it not set. + * @param boolean $return if true, return the HTML as a string, rather than printing it. + */ + protected function collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false) { + global $CFG, $PAGE, $OUTPUT; + + // Work out the initial state. + if (!empty($userpref) and is_string($userpref)) { + user_preference_allow_ajax_update($userpref, PARAM_BOOL); + $collapsed = get_user_preferences($userpref, $default); + } else { + $collapsed = $default; + $userpref = false; + } + + if ($collapsed) { + $classes .= ' collapsed'; + } + + $output = ''; + $output .= '
'; + $output .= '
'; + $output .= '
'; + $output .= $caption . ' '; + $output .= '
'; + $this->page->requires->js_init_call('M.block_course_overview.collapsible', array($id, $userpref, get_string('clicktohideshow'))); + + return $output; + } + + /** + * Close a region started with print_collapsible_region_start. + * + * @param boolean $return if true, return the HTML as a string, rather than printing it. + */ + protected function collapsible_region_end() { + $output = '
'; + return $output; + } + + public function welcome_area() { + global $OUTPUT, $USER; + $output = $OUTPUT->box_start('welcome_area'); + + $picture = $OUTPUT->user_picture($USER, array('size' => 75, 'class' => 'welcome_userpicture')); + $output .= html_writer::tag('div', $picture, array('class' => 'profilepicture')); + + $output .= $OUTPUT->box_start('welcome_message'); + $output .= $OUTPUT->heading(get_string('welcome', 'block_course_overview', $USER->firstname)); + + //messages + $count = message_count_unread_messages($USER); + $plural = 's'; + if ($count > 0) { + $output .= get_string('you_have_messages', 'block_course_overview', $count); + } else { + $output .= get_string('you_have_no_messages', 'block_course_overview'); + if ($count == 1) { + $plural = ''; + } + } + $output .= html_writer::link(new moodle_url('/message/index.php'), get_string('message'.$plural, 'block_course_overview')); + $output .= $OUTPUT->box_end(); + $output .= $OUTPUT->box('', 'flush'); + $output .= $OUTPUT->box_end(); + + return $output; + } +} diff --git a/blocks/course_overview/save.php b/blocks/course_overview/save.php new file mode 100644 index 00000000000..36841ccac3f --- /dev/null +++ b/blocks/course_overview/save.php @@ -0,0 +1,31 @@ +. + +define('AJAX_SCRIPT', true); + +/** Include config */ +require_once(dirname(__FILE__) . '/../../config.php'); +require_once(dirname(__FILE__) . '/locallib.php'); + +if (!confirm_sesskey()) { + throw new moodle_exception('invalidsesskey', 'error'); +} + +$sortorder = required_param('sortorder', PARAM_INT); +$sortorder = implode(',', $sortorder); + +block_course_overview_update_myorder($sortorder); diff --git a/blocks/course_overview/settings.php b/blocks/course_overview/settings.php new file mode 100644 index 00000000000..facd4395f1c --- /dev/null +++ b/blocks/course_overview/settings.php @@ -0,0 +1,16 @@ +fulltree) { + $configs = array(); + $configs[] = new admin_setting_configcheckbox('showchildren', get_string('showchildren', 'block_course_overview'), + get_string('showchildren_desc', 'block_course_overview'), ''); + $configs[] = new admin_setting_configcheckbox('showwelcomearea', get_string('showwelcomearea', 'block_course_overview'), + get_string('showwelcomearea_desc', 'block_course_overview'), 1); + + foreach ($configs as $config) { + $config->plugin = 'block_course_overview'; + $settings->add($config); + } +} diff --git a/blocks/course_overview/styles.css b/blocks/course_overview/styles.css new file mode 100644 index 00000000000..269c4b7b1df --- /dev/null +++ b/blocks/course_overview/styles.css @@ -0,0 +1,83 @@ +.pagelayout-mydashboard #region-main .region-content .block_course_overview .header, +.pagelayout-mydashboard #region-main .region-content .block_course_overview .block_action, +.pagelayout-mydashboard #region-main .region-content .block_course_overview .commands { + display: none; +} + +.block_course_overview .coursechildren { + font-size: 12px; + font-weight: normal; + font-style: italic; +} + +.block_course_overview .content { + margin-left: 20px; +} + +.block_course_overview .profilepicture { + float: left; +} + +.block_course_overview .welcome_area { + width: 100%; + padding-bottom: 5px; +} + +.block_course_overview .welcome_message { + float: left; + padding: 10px; + vertical-align: middle; + border-collapse: separate; + clear: none; +} + +.block_course_overview h2.title { + float: left; + font-size: 1.5em !important; + margin-bottom: 0; + margin-top: 0; + position: relative; +} + +.block_course_overview .course_title { + position: relative; +} + +.editing .block_course_overview h2.title { + left: 20px; +} + +.editing .block_course_overview .coursebox .cursor { + cursor: move; + margin-bottom: 2px; +} + +.editing .block_course_overview .move { + float: left; + position: absolute; + width: 20px; +} + +.block_course_overview #course_list { + width: 100%; +} + +.block_course_overview div.flush { + clear: both; +} + +.block_course_overview .activity_info { + clear: both; +} + +.block_course_overview .activity_overview { + padding: 2px; +} + +.block_course_overview .singleselect { + text-align: left; + margin: 0; +} + +.block_course_overview .coursemovetarget {display: block;height: 1em;margin-bottom: 1em;border-width: 2px;border-style: dashed;} + diff --git a/blocks/course_overview/version.php b/blocks/course_overview/version.php index 68d118f7a01..a117c9f0633 100644 --- a/blocks/course_overview/version.php +++ b/blocks/course_overview/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2012061700; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2012062800; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2012061700; // Requires this Moodle version -$plugin->component = 'block_course_overview'; // Full name of the plugin (used for diagnostics) +$plugin->component = 'block_course_overview'; // Full name of the plugin (used for diagnostics) \ No newline at end of file