'idnumber' => new lang_string('sort_idnumber', 'admin'),
);
$temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
- $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
+ $temp->add(new admin_setting_configtext('navcourselimit', new lang_string('navcourselimit', 'admin'),
+ new lang_string('confignavcourselimit', 'admin'), 10, PARAM_INT));
$temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 1));
$temp->add(new admin_setting_configcheckbox('linkcoursesections', new lang_string('linkcoursesections', 'admin'), new lang_string('linkcoursesections_help', 'admin'), 0));
$optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
- $choices = array();
- $choices[0] = new lang_string('no');
- $choices[1] = new lang_string('yes');
- $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'),
- new lang_string('allowstealthmodules_help'), 0, $choices));
+ $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
+ new lang_string('allowstealthmodules_help'), 0, 1, 0));
}
}
}
- // Roll dates.
- $data->timecreated = $this->apply_date_offset($data->timecreated);
+ // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
// Revert other to its original php way.
$data->other = unserialize(base64_decode($data->other));
];
$confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params);
- // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
- $data->link = $confirmationurl->out();
+ $data->link = $confirmationurl->out(false);
+ $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
- $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
+ $data->link = $confirmationurl->out();
$messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
$user->mailformat = 1; // Always send HTML version as well.
];
$confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params);
- $data->link = $confirmationurl->out();
+ $data->link = $confirmationurl->out(false);
+ $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
- $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
+ $data->link = $confirmationurl->out();
$messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
$user->mailformat = 1; // Always send HTML version as well.
protected function set_in_database($availability) {
global $DB;
- $DB->set_field('course_sections', 'availability', $availability,
- array('id' => $this->section->id));
+
+ $section = new \stdClass();
+ $section->id = $this->section->id;
+ $section->availability = $availability;
+ $section->timemodified = time();
+ $DB->update_record('course_sections', $section);
}
/**
// Save the updated course module.
if ($changed) {
- $DB->set_field('course_sections', 'availability', json_encode($tree->save()),
- array('id' => $section->id));
+ $updatesection = new \stdClass();
+ $updatesection->id = $section->id;
+ $updatesection->availability = json_encode($tree->save());
+ $updatesection->timemodified = time();
+ $DB->update_record('course_sections', $updatesection);
+
$anychanged = true;
}
}
$section = new backup_nested_element('section', array('id'), array(
'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
- 'availabilityjson'));
+ 'availabilityjson', 'timemodified'));
// attach format plugin structure to $section element, only one allowed
$this->add_plugin_structure('format', $section, false);
if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => $i);
+ 'section' => $i,
+ 'timemodified' => time());
$DB->insert_record('course_sections', $sectionrec); // missing section created
}
}
$section = new stdclass();
$section->course = $this->get_courseid();
$section->section = $data->number;
+ $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
// Section doesn't exist, create it with all the info from backup
- if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
+ if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
$section->name = $data->name;
$section->summary = $data->summary;
$section->summaryformat = $data->summaryformat;
array('id' => $availfield->coursesectionid), MUST_EXIST);
$newvalue = \core_availability\info::add_legacy_availability_field_condition(
$currentvalue, $availfield, $show);
- $DB->set_field('course_sections', 'availability', $newvalue,
- array('id' => $availfield->coursesectionid));
+
+ $section = new stdClass();
+ $section->id = $availfield->coursesectionid;
+ $section->availability = $newvalue;
+ $section->timemodified = time();
+ $DB->update_record('course_sections', $section);
}
}
$data = (object)($data);
- $data->time = $this->apply_date_offset($data->time);
+ // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
$data->userid = $this->get_mappingid('user', $data->userid);
$data->course = $this->get_courseid();
$data->cmid = 0;
$data = (object)($data);
- $data->time = $this->apply_date_offset($data->time);
+ // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
$data->userid = $this->get_mappingid('user', $data->userid);
$data->course = $this->get_courseid();
$data->cmid = $this->task->get_moduleid();
if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => 0);
+ 'section' => 0,
+ 'timemodified' => time());
$DB->insert_record('course_sections', $sectionrec); // section 0
$sectionrec = array(
'course' => $this->get_courseid(),
- 'section' => 1);
+ 'section' => 1,
+ 'timemodified' => time());
$data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
}
$data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping
} else {
$sequence = $newitemid;
}
- $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
+
+ $updatesection = new \stdClass();
+ $updatesection->id = $data->section;
+ $updatesection->sequence = $sequence;
+ $updatesection->timemodified = time();
+ $DB->update_record('course_sections', $updatesection);
// If there is the legacy showavailability data, store this for later use.
// (This data is not present when restoring 'new' backups.)
if ($USER->id != $user->id) {
echo html_writer::start_tag('tr');
echo html_writer::start_tag('td', array('colspan' => '2'));
- echo html_writer::tag('b', get_string('showinguser', 'completion'));
+ echo html_writer::tag('b', get_string('showinguser', 'completion') . ' ');
$url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
echo html_writer::link($url, fullname($user));
echo html_writer::end_tag('td');
echo html_writer::start_tag('tr');
echo html_writer::start_tag('td', array('colspan' => '2'));
-echo html_writer::tag('b', get_string('status'));
+echo html_writer::tag('b', get_string('status') . ' ');
// Is course complete?
$coursecomplete = $info->is_course_complete($user->id);
} else {
echo html_writer::start_tag('tr');
echo html_writer::start_tag('td', array('colspan' => '2'));
- echo html_writer::tag('b', get_string('required'));
+ echo html_writer::tag('b', get_string('required') . ' ');
// Get overall aggregation method.
$overall = $info->get_aggregation_method();
echo core_text::strtolower(get_string('any', 'completion'));
}
- echo html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
+ echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
$agg_type = false;
}
}
--- /dev/null
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Javascript used to save the user's tab preference.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/custom_interaction_events',
+ 'core/notification'], function($, Ajax, CustomEvents, Notification) {
+
+ /**
+ * Registers an event that saves the user's tab preference when switching between them.
+ *
+ * @param {object} root The container element
+ */
+ var registerEventListeners = function(root) {
+ CustomEvents.define(root, [CustomEvents.events.activate]);
+ root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+ var tabname = $(e.currentTarget).data('tabname');
+ // Bootstrap does not change the URL when using BS tabs, so need to do this here.
+ // Also check to make sure the browser supports the history API.
+ if (typeof window.history.pushState === "function") {
+ window.history.pushState(null, null, '?myoverviewtab=' + tabname);
+ }
+ var request = {
+ methodname: 'core_user_update_user_preferences',
+ args: {
+ preferences: [
+ {
+ type: 'block_myoverview_last_tab',
+ value: tabname
+ }
+ ]
+ }
+ };
+
+ Ajax.call([request])[0]
+ .fail(Notification.exception);
+ });
+ };
+
+ return {
+ registerEventListeners: registerEventListeners
+ };
+});
return $this->content;
}
- $renderable = new \block_myoverview\output\main();
+ // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
+ if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
+ // Check if the user has no preference, if so get the site setting.
+ if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
+ $config = get_config('block_myoverview');
+ $tab = $config->defaulttab;
+ }
+ }
+
+ $renderable = new \block_myoverview\output\main($tab);
$renderer = $this->page->get_renderer('block_myoverview');
$this->content = new stdClass();
public function applicable_formats() {
return array('my' => true);
}
+
+ /**
+ * This block does contain a configuration settings.
+ *
+ * @return boolean
+ */
+ public function has_config() {
+ return true;
+ }
}
* @return array
*/
public function export_for_template(renderer_base $output) {
- $today = time();
-
// Build courses view data structure.
$coursesview = [
'hascourses' => !empty($this->courses)
// How many courses we have per status?
$coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0];
foreach ($this->courses as $course) {
- $startdate = $course->startdate;
- $enddate = $course->enddate;
$courseid = $course->id;
$context = \context_course::instance($courseid);
$exporter = new course_summary_exporter($course, [
// Convert summary to plain text.
$exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
+ $courseprogress = null;
+
+ $classified = course_classify_for_timeline($course);
+
if (isset($this->coursesprogress[$courseid])) {
- $coursecompleted = $this->coursesprogress[$courseid]['completed'];
$courseprogress = $this->coursesprogress[$courseid]['progress'];
$exportedcourse->hasprogress = !is_null($courseprogress);
$exportedcourse->progress = $courseprogress;
}
- if ((isset($coursecompleted) && $coursecompleted) || (!empty($enddate) && $enddate < $today)) {
+ if ($classified == COURSE_TIMELINE_PAST) {
// Courses that have already ended.
$pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE);
$coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
$coursesview['past']['haspages'] = true;
$coursesbystatus['past']++;
- } else if ($startdate > $today) {
+ } else if ($classified == COURSE_TIMELINE_FUTURE) {
// Courses that have not started yet.
$futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE);
use templatable;
use core_completion\progress;
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
require_once($CFG->libdir . '/completionlib.php');
/**
*/
class main implements renderable, templatable {
+ /**
+ * @var string The tab to display.
+ */
+ public $tab;
+
+ /**
+ * Constructor.
+ *
+ * @param string $tab The tab to display.
+ */
+ public function __construct($tab) {
+ $this->tab = $tab;
+ }
+
/**
* Export this data so it can be used as the context for a mustache template.
*
$nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
$noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
+ // Now, set the tab we are going to be viewing.
+ $viewingtimeline = false;
+ $viewingcourses = false;
+ if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
+ $viewingtimeline = true;
+ } else {
+ $viewingcourses = true;
+ }
+
return [
'midnight' => usergetmidnight(time()),
'coursesview' => $coursesview->export_for_template($output),
'urls' => [
'nocourses' => $nocoursesurl,
'noevents' => $noeventsurl
- ]
+ ],
+ 'viewingtimeline' => $viewingtimeline,
+ 'viewingcourses' => $viewingcourses
];
}
}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['defaulttab'] = 'Default tab';
+$string['defaulttab_desc'] = 'This is the default tab that will be shown to a user.';
$string['future'] = 'Future';
$string['inprogress'] = 'In progress';
$string['morecourses'] = 'More courses';
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains functions called by core.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The timeline view.
+ */
+define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+
+/**
+ * The courses view.
+ */
+define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_myoverview_user_preferences() {
+ $preferences = array();
+ $preferences['block_myoverview_last_tab'] = array(
+ 'type' => PARAM_ALPHA,
+ 'null' => NULL_NOT_ALLOWED,
+ 'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
+ 'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
+ );
+
+ return $preferences;
+}
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Settings for the overview block.
+ *
+ * @package block_myoverview
+ * @copyright 2017 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
+
+if ($ADMIN->fulltree) {
+
+ $options = [
+ BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
+ BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
+ ];
+
+ $settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
+ get_string('defaulttab', 'block_myoverview'),
+ get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
+}
}}
<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
- <ul class="nav nav-tabs" role="tablist">
+ <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
<li class="nav-item">
- <a class="nav-link active" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+ <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
{{#str}} timeline, block_myoverview {{/str}}
</a>
</li>
<li class="nav-item">
- <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+ <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
{{#str}} courses {{/str}}
</a>
</li>
</ul>
<div class="tab-content content-centred">
- <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+ <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
{{> block_myoverview/timeline-view }}
</div>
- <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+ <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
{{#coursesview}}
{{> block_myoverview/courses-view }}
{{/coursesview}}
</div>
</div>
</div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+ var root = $('#block-myoverview-view-choices-{{uniqid}}');
+ TabPreferences.registerEventListeners(root);
+});
+{{/js}}
And I am on "Course 1" course homepage
And I follow "Test choice 1"
And I follow "Dashboard" in the user menu
+ And I click on "Timeline" "link" in the "Course overview" "block"
And I click on "Sort by courses" "link" in the "Course overview" "block"
And I should see "100%" in the "Course overview" "block"
And I click on "Courses" "link" in the "Course overview" "block"
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017051502; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
* @return data_object instance of data_object or false if none found.
*/
public static function fetch($params) {
- return self::fetch_helper('course_completions', __CLASS__, $params);
+ $cache = cache::make('core', 'coursecompletion');
+
+ $key = $params['userid'] . '_' . $params['course'];
+ if ($hit = $cache->get($key)) {
+ return $hit['value'];
+ }
+
+ $tocache = self::fetch_helper('course_completions', __CLASS__, $params);
+ $cache->set($key, ['value' => $tocache]);
+ return $tocache;
}
/**
$this->timeenrolled = 0;
}
+ $result = false;
// Save record
if ($this->id) {
- return $this->update();
+ $result = $this->update();
} else {
// Make sure reaggregate field is not null
if (!$this->reaggregate) {
$this->timestarted = 0;
}
- return $this->insert();
+ $result = $this->insert();
+ }
+
+ if ($result) {
+ // Update the cached record.
+ $cache = cache::make('core', 'coursecompletion');
+ $data = $this->get_record_data();
+ $key = $data->userid . '_' . $data->course;
+ $cache->set($key, ['value' => $data]);
}
+
+ return $result;
}
}
define('MOD_CLASS_ACTIVITY', 0);
define('MOD_CLASS_RESOURCE', 1);
+define('COURSE_TIMELINE_PAST', 'past');
+define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
+define('COURSE_TIMELINE_FUTURE', 'future');
+
function make_log_url($module, $url) {
switch ($module) {
case 'course':
$cw->name = null;
$cw->visible = 1;
$cw->availability = null;
+ $cw->timemodified = time();
$cw->id = $DB->insert_record("course_sections", $cw);
// Now move it to the specified position.
// Update record in the DB and course format options.
$data['id'] = $section->id;
+ $data['timemodified'] = time();
$DB->update_record('course_sections', $data);
rebuild_course_cache($courseid, true);
course_get_format($courseid)->update_section_format_options($data);
return array($instances, $warnings);
}
+/**
+ * This function classifies a course as past, in progress or future.
+ *
+ * This function may incur a DB hit to calculate course completion.
+ * @param stdClass $course Course record
+ * @param stdClass $user User record (optional - defaults to $USER).
+ * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
+ * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
+ */
+function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
+ global $USER;
+
+ if ($user == null) {
+ $user = $USER;
+ }
+
+ $today = time();
+ // End date past.
+ if (!empty($course->enddate) && $course->enddate < $today) {
+ return COURSE_TIMELINE_PAST;
+ }
+
+ if ($completioninfo == null) {
+ $completioninfo = new completion_info($course);
+ }
+
+ // Course was completed.
+ if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
+ return COURSE_TIMELINE_PAST;
+ }
+
+ // Start date not reached.
+ if (!empty($course->startdate) && $course->startdate > $today) {
+ return COURSE_TIMELINE_FUTURE;
+ }
+
+ // Everything else is in progress.
+ return COURSE_TIMELINE_INPROGRESS;
+}
+
/**
* Check module updates since a given time.
* This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
}
}
+ public function test_update_course_section_time_modified() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ // Create the course with sections.
+ $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+ $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+ // Get the last section's time modified value.
+ $section = array_pop($sections);
+ $oldtimemodified = $section->timemodified;
+
+ // Update the section.
+ $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
+ course_update_section($course, $section, array());
+
+ // Check that the time has changed.
+ $section = $DB->get_record('course_sections', array('id' => $section->id));
+ $newtimemodified = $section->timemodified;
+ $this->assertGreaterThan($oldtimemodified, $newtimemodified);
+ }
+
public function test_course_add_cm_to_section() {
global $DB;
$this->resetAfterTest(true);
}
$this->assertEquals(2, $count);
}
+
+ public function test_classify_course_for_timeline() {
+ global $DB, $CFG;
+
+ require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+ set_config('enablecompletion', COMPLETION_ENABLED);
+
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ // Create courses for testing.
+ $generator = $this->getDataGenerator();
+ $future = time() + 3600;
+ $past = time() - 3600;
+ $futurecourse = $generator->create_course(['startdate' => $future]);
+ $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
+ $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+ $inprogresscourse = $generator->create_course();
+
+ // Set completion rules.
+ $criteriadata = new stdClass();
+ $criteriadata->id = $completedcourse->id;
+
+ // Self completion.
+ $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+ $class = 'completion_criteria_self';
+ $criterion = new $class();
+ $criterion->update_config($criteriadata);
+
+ $user = $this->getDataGenerator()->create_user();
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
+ $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
+ $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
+ $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
+
+ $this->setUser($user);
+ core_completion_external::mark_course_self_completed($completedcourse->id);
+ $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
+ $ccompletion->mark_complete();
+
+ // Aggregate the completions.
+ $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
+ $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
+ $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
+ $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
+ }
}
$mform =& $this->_form;
if ($id = $mform->getElementValue('id')) {
- $grade_item = grade_item::fetch(array('id'=>$id));
+ $gradeitem = grade_item::fetch(array('id' => $id));
+ $parentcategory = $gradeitem->get_parent_category();
+ } else {
+ // If we do not have an id, we are creating a new grade item.
+ $gradeitem = new grade_item(array('courseid' => $COURSE->id, 'itemtype' => 'manual'), false);
+
+ // Assign the course category to this grade item.
+ $parentcategory = grade_category::fetch_course_category($COURSE->id);
+ $gradeitem->parent_category = $parentcategory;
+ }
+
+ if (!$gradeitem->is_raw_used()) {
+ $mform->removeElement('plusfactor');
+ $mform->removeElement('multfactor');
+ }
- if (!$grade_item->is_raw_used()) {
- $mform->removeElement('plusfactor');
- $mform->removeElement('multfactor');
+ if ($gradeitem->is_outcome_item()) {
+ // We have to prevent incompatible modifications of outcomes if outcomes disabled.
+ $mform->removeElement('grademax');
+ if ($mform->elementExists('grademin')) {
+ $mform->removeElement('grademin');
}
+ $mform->removeElement('gradetype');
+ $mform->removeElement('display');
+ $mform->removeElement('decimals');
+ $mform->hardFreeze('scaleid');
- if ($grade_item->is_outcome_item()) {
- // we have to prevent incompatible modifications of outcomes if outcomes disabled
- $mform->removeElement('grademax');
+ } else {
+ if ($gradeitem->is_external_item()) {
+ // Following items are set up from modules and should not be overrided by user.
if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
+ // The site setting grade_report_showmin may have prevented grademin being added to the form.
+ $mform->hardFreeze('grademin');
+ }
+ $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
+ if ($gradeitem->itemnumber == 0) {
+ // The idnumber of grade itemnumber 0 is synced with course_modules.
+ $mform->hardFreeze('idnumber');
}
- $mform->removeElement('gradetype');
- $mform->removeElement('display');
- $mform->removeElement('decimals');
- $mform->hardFreeze('scaleid');
- } else {
- if ($grade_item->is_external_item()) {
- // following items are set up from modules and should not be overrided by user
+ // For external items we can not change the grade type, even if no grades exist, so if it is set to
+ // scale, then remove the grademax and grademin fields from the form - no point displaying them.
+ if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+ $mform->removeElement('grademax');
if ($mform->elementExists('grademin')) {
- // The site setting grade_report_showmin may have prevented grademin being added to the form.
- $mform->hardFreeze('grademin');
- }
- $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
- if ($grade_item->itemnumber == 0) {
- // the idnumber of grade itemnumber 0 is synced with course_modules
- $mform->hardFreeze('idnumber');
+ $mform->removeElement('grademin');
}
+ } else { // Not using scale, so remove it.
+ $mform->removeElement('scaleid');
+ }
- // For external items we can not change the grade type, even if no grades exist, so if it is set to
- // scale, then remove the grademax and grademin fields from the form - no point displaying them.
- if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
- $mform->removeElement('grademax');
- if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
- }
- } else { // Not using scale, so remove it.
- $mform->removeElement('scaleid');
- }
+ // Always remove the rescale grades element if it's an external item.
+ $mform->removeElement('rescalegrades');
+ } else if ($gradeitem->has_grades()) {
+ // Can't change the grade type or the scale if there are grades.
+ $mform->hardFreeze('gradetype, scaleid');
- // Always remove the rescale grades element if it's an external item.
+ // If we are using scales then remove the unnecessary rescale and grade fields.
+ if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
$mform->removeElement('rescalegrades');
- } else if ($grade_item->has_grades()) {
- // Can't change the grade type or the scale if there are grades.
- $mform->hardFreeze('gradetype, scaleid');
-
- // If we are using scales then remove the unnecessary rescale and grade fields.
- if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
- $mform->removeElement('rescalegrades');
- $mform->removeElement('grademax');
- if ($mform->elementExists('grademin')) {
- $mform->removeElement('grademin');
- }
- } else { // Remove the scale field.
- $mform->removeElement('scaleid');
- // Set the maximum grade to disabled unless a grade is chosen.
- $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
+ $mform->removeElement('grademax');
+ if ($mform->elementExists('grademin')) {
+ $mform->removeElement('grademin');
}
- } else {
- // Remove the rescale element if there are no grades.
- $mform->removeElement('rescalegrades');
+ } else { // Remove the scale field.
+ $mform->removeElement('scaleid');
+ // Set the maximum grade to disabled unless a grade is chosen.
+ $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
}
+ } else {
+ // Remove the rescale element if there are no grades.
+ $mform->removeElement('rescalegrades');
}
+ }
+
+ // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
+ if ($id && $mform->elementExists('parentcategory')) {
+ $mform->hardFreeze('parentcategory');
+ }
+
+ $parentcategory->apply_forced_settings();
- // if we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
- if ($mform->elementExists('parentcategory')) {
- $mform->hardFreeze('parentcategory');
+ if (!$parentcategory->is_aggregationcoef_used()) {
+ if ($mform->elementExists('aggregationcoef')) {
+ $mform->removeElement('aggregationcoef');
}
- $parent_category = $grade_item->get_parent_category();
- $parent_category->apply_forced_settings();
+ } else {
+ $coefstring = $gradeitem->get_coefstring();
- if (!$parent_category->is_aggregationcoef_used()) {
- if ($mform->elementExists('aggregationcoef')) {
- $mform->removeElement('aggregationcoef');
+ if ($coefstring !== '') {
+ if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
+ // The advcheckbox is not compatible with disabledIf!
+ $coefstring = 'aggregationcoefextrasum';
+ $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
+ } else {
+ $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
}
-
- } else {
- $coefstring = $grade_item->get_coefstring();
-
- if ($coefstring !== '') {
- if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
- // advcheckbox is not compatible with disabledIf!
- $coefstring = 'aggregationcoefextrasum';
- $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
- } else {
- $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
- }
- if ($mform->elementExists('parentcategory')) {
- $mform->insertElementBefore($element, 'parentcategory');
- } else {
- $mform->insertElementBefore($element, 'id');
- }
- $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
+ if ($mform->elementExists('parentcategory')) {
+ $mform->insertElementBefore($element, 'parentcategory');
+ } else {
+ $mform->insertElementBefore($element, 'id');
}
- $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
- $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
- $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
+ $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
}
+ $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
+ $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
+ $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id);
+ }
- // Remove fields used by natural weighting if the parent category is not using natural weighting.
- // Or if the item is a scale and scales are not used in aggregation.
- if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
- || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
- if ($mform->elementExists('weightoverride')) {
- $mform->removeElement('weightoverride');
- }
- if ($mform->elementExists('aggregationcoef2')) {
- $mform->removeElement('aggregationcoef2');
- }
+ // Remove fields used by natural weighting if the parent category is not using natural weighting.
+ // Or if the item is a scale and scales are not used in aggregation.
+ if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM
+ || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) {
+ if ($mform->elementExists('weightoverride')) {
+ $mform->removeElement('weightoverride');
}
+ if ($mform->elementExists('aggregationcoef2')) {
+ $mform->removeElement('aggregationcoef2');
+ }
+ }
- if ($category = $grade_item->get_item_category()) {
- if ($category->aggregation == GRADE_AGGREGATE_SUM) {
- if ($mform->elementExists('gradetype')) {
- $mform->hardFreeze('gradetype');
- }
- if ($mform->elementExists('grademin')) {
- $mform->hardFreeze('grademin');
- }
- if ($mform->elementExists('grademax')) {
- $mform->hardFreeze('grademax');
- }
- if ($mform->elementExists('scaleid')) {
- $mform->removeElement('scaleid');
- }
+ if ($category = $gradeitem->get_item_category()) {
+ if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+ if ($mform->elementExists('gradetype')) {
+ $mform->hardFreeze('gradetype');
+ }
+ if ($mform->elementExists('grademin')) {
+ $mform->hardFreeze('grademin');
+ }
+ if ($mform->elementExists('grademax')) {
+ $mform->hardFreeze('grademax');
+ }
+ if ($mform->elementExists('scaleid')) {
+ $mform->removeElement('scaleid');
}
}
-
- } else {
- // all new items are manual, children of course category
- $mform->removeElement('plusfactor');
- $mform->removeElement('multfactor');
- $mform->removeElement('rescalegrades');
}
// no parent header for course category
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Group index page.
+ *
+ * @package core_group
+ * @copyright 2017 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_group\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+
+/**
+ * Group index page class.
+ *
+ * @package core_group
+ * @copyright 2017 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class index_page implements renderable, templatable {
+
+ /** @var int $courseid The course ID. */
+ public $courseid;
+
+ /** @var array The array of groups to be rendered. */
+ public $groups;
+
+ /** @var string The name of the currently selected group. */
+ public $selectedgroupname;
+
+ /** @var array The array of group members to be rendered, if a group is selected. */
+ public $selectedgroupmembers;
+
+ /** @var bool Whether to disable the add members/edit group buttons. */
+ public $disableaddedit;
+
+ /** @var bool Whether to disable the delete group button. */
+ public $disabledelete;
+
+ /** @var array Groups that can't be deleted by the user. */
+ public $undeletablegroups;
+
+ /**
+ * index_page constructor.
+ *
+ * @param int $courseid The course ID.
+ * @param array $groups The array of groups to be rendered.
+ * @param string $selectedgroupname The name of the currently selected group.
+ * @param array $selectedgroupmembers The array of group members to be rendered, if a group is selected.
+ * @param bool $disableaddedit Whether to disable the add members/edit group buttons.
+ * @param bool $disabledelete Whether to disable the delete group button.
+ * @param array $undeletablegroups Groups that can't be deleted by the user.
+ */
+ public function __construct($courseid, $groups, $selectedgroupname, $selectedgroupmembers, $disableaddedit, $disabledelete,
+ $undeletablegroups) {
+ $this->courseid = $courseid;
+ $this->groups = $groups;
+ $this->selectedgroupname = $selectedgroupname;
+ $this->selectedgroupmembers = $selectedgroupmembers;
+ $this->disableaddedit = $disableaddedit;
+ $this->disabledelete = $disabledelete;
+ $this->undeletablegroups = $undeletablegroups;
+ }
+
+ /**
+ * Export the data.
+ *
+ * @param renderer_base $output
+ * @return stdClass
+ */
+ public function export_for_template(renderer_base $output) {
+ global $CFG;
+
+ $data = new stdClass();
+
+ // Variables that will be passed to the JS helper.
+ $data->courseid = $this->courseid;
+ $data->wwwroot = $CFG->wwwroot;
+ // To be passed to the JS init script in the template. Encode as a JSON string.
+ $data->undeletablegroups = json_encode($this->undeletablegroups);
+
+ // Some buttons are enabled if single group selected.
+ $data->addmembersdisabled = $this->disableaddedit;
+ $data->editgroupsettingsdisabled = $this->disableaddedit;
+ $data->deletegroupdisabled = $this->disabledelete;
+ $data->groups = $this->groups;
+ $data->members = $this->selectedgroupmembers;
+ $data->selectedgroup = $this->selectedgroupname;
+
+ return $data;
+ }
+}
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Renderers.
+ *
+ * @package core_group
+ * @copyright 2017 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_group\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use plugin_renderer_base;
+
+/**
+ * Renderer class.
+ *
+ * @package core_group
+ * @copyright 2017 Jun Pataleta
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+ /**
+ * Defer to template.
+ *
+ * @param index_page $page
+ * @return string
+ */
+ public function render_index_page(index_page $page) {
+ $data = $page->export_for_template($this);
+ return parent::render_from_template('core_group/index', $data);
+ }
+}
require_capability('moodle/course:managegroups', $context);
$PAGE->requires->js('/group/clientlib.js');
+$PAGE->requires->js('/group/module.js');
// Check for multiple/no group errors
if (!$singlegroup) {
$currenttab = 'groups';
require('tabs.php');
-$disabled = 'disabled="disabled"';
-
-// Some buttons are enabled if single group selected.
-$showaddmembersform_disabled = $singlegroup ? '' : $disabled;
-$showeditgroupsettingsform_disabled = $singlegroup ? '' : $disabled;
-$deletegroup_disabled = count($groupids) > 0 ? '' : $disabled;
-
echo $OUTPUT->heading(format_string($course->shortname, true, array('context' => $context)) .' '.$strgroups, 3);
-echo '<form id="groupeditform" action="index.php" method="post">'."\n";
-echo '<div>'."\n";
-echo '<input type="hidden" name="id" value="' . $courseid . '" />'."\n";
-
-echo html_writer::start_tag('div', array('class' => 'groupmanagementtable boxaligncenter'));
-echo html_writer::start_tag('div', array('class' => 'groups'));
-
-echo '<p><label for="groups"><span id="groupslabel">'.get_string('groups').':</span><span id="thegrouping"> </span></label></p>'."\n";
-
-$onchange = 'M.core_group.membersCombo.refreshMembers();';
-
-echo '<select name="groups[]" multiple="multiple" id="groups" size="15" class="select" onchange="'.$onchange.'">'."\n";
$groups = groups_get_all_groups($courseid);
-$selectedname = ' ';
+$selectedname = null;
$preventgroupremoval = array();
+// Get list of groups to render.
+$groupoptions = array();
if ($groups) {
- // Print out the HTML
foreach ($groups as $group) {
- $select = '';
- $usercount = $DB->count_records('groups_members', array('groupid'=>$group->id));
- $groupname = format_string($group->name).' ('.$usercount.')';
- if (in_array($group->id,$groupids)) {
- $select = ' selected="selected"';
+ $selected = false;
+ $usercount = $DB->count_records('groups_members', array('groupid' => $group->id));
+ $groupname = format_string($group->name) . ' (' . $usercount . ')';
+ if (in_array($group->id, $groupids)) {
+ $selected = true;
if ($singlegroup) {
- // Only keep selected name if there is one group selected
+ // Only keep selected name if there is one group selected.
$selectedname = $groupname;
}
}
$preventgroupremoval[$group->id] = true;
}
- echo "<option value=\"{$group->id}\"$select title=\"$groupname\">$groupname</option>\n";
+ $groupoptions[] = (object) [
+ 'value' => $group->id,
+ 'selected' => $selected,
+ 'text' => $groupname
+ ];
}
-} else {
- // Print an empty option to avoid the XHTML error of having an empty select element
- echo '<option> </option>';
}
-echo '</select>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" name="act_updatemembers" id="updatemembers" value="'
- . get_string('showmembersforgroup', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $showeditgroupsettingsform_disabled .
- ' name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="'
- . get_string('editgroupsettings', 'group') . '" /></p>'."\n";
-echo '<p><input class="btn btn-secondary" type="submit" '. $deletegroup_disabled .
- ' name="act_deletegroup" id="deletegroup" value="'
- . get_string('deleteselectedgroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="'
- . get_string('creategroup', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="'
- . get_string('autocreategroups', 'group') . '" /></p>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" name="act_showimportgroups" id="showimportgroups" value="'
- . get_string('importgroups', 'core_group') . '" /></p>'."\n";
-
-echo html_writer::end_tag('div');
-echo html_writer::start_tag('div', array('class' => 'members'));
-
-echo '<p><label for="members"><span id="memberslabel">'.
- get_string('membersofselectedgroup', 'group').
- ' </span><span id="thegroup">'.$selectedname.'</span></label></p>'."\n";
-//NOTE: the SELECT was, multiple="multiple" name="user[]" - not used and breaks onclick.
-echo '<select name="user" id="members" size="15" class="select"'."\n";
-echo ' onclick="window.status=this.options[this.selectedIndex].title;" onmouseout="window.status=\'\';">'."\n";
-
-$member_names = array();
-
-$atleastonemember = false;
+// Get list of group members to render if there is a single selected group.
+$members = array();
if ($singlegroup) {
- if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid, 'u.id, ' . get_all_user_name_fields(true, 'u'))) {
- foreach($groupmemberroles as $roleid=>$roledata) {
- echo '<optgroup label="'.s($roledata->name).'">';
- foreach($roledata->users as $member) {
- echo '<option value="'.$member->id.'">'.fullname($member, true).'</option>';
- $atleastonemember = true;
+ $usernamefields = get_all_user_name_fields(true, 'u');
+ if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid, 'u.id, ' . $usernamefields)) {
+ foreach ($groupmemberroles as $roleid => $roledata) {
+ $users = array();
+ foreach ($roledata->users as $member) {
+ $users[] = (object)[
+ 'value' => $member->id,
+ 'text' => fullname($member, true)
+ ];
}
- echo '</optgroup>';
+ $members[] = (object)[
+ 'role' => s($roledata->name),
+ 'rolemembers' => $users
+ ];
}
}
}
-if (!$atleastonemember) {
- // Print an empty option to avoid the XHTML error of having an empty select element
- echo '<option> </option>';
-}
-
-echo '</select>'."\n";
-
-echo '<p><input class="btn btn-secondary" type="submit" ' . $showaddmembersform_disabled . ' name="act_showaddmembersform" '
- . 'id="showaddmembersform" value="' . get_string('adduserstogroup', 'group'). '" /></p>'."\n";
-echo html_writer::end_tag('div');
-echo html_writer::end_tag('div');
-
-//<input type="hidden" name="rand" value="om" />
-echo '</div>'."\n";
-echo '</form>'."\n";
-
-$PAGE->requires->js_init_call('M.core_group.init_index', array($CFG->wwwroot, $courseid));
-$PAGE->requires->js_init_call('M.core_group.groupslist', array($preventgroupremoval));
+$disableaddedit = !$singlegroup;
+$disabledelete = !empty($groupids);
+$renderable = new \core_group\output\index_page($courseid, $groupoptions, $selectedname, $members, $disableaddedit, $disabledelete,
+ $preventgroupremoval);
+$output = $PAGE->get_renderer('core_group');
+echo $output->render($renderable);
echo $OUTPUT->footer();
--- /dev/null
+{{!
+ 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 <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_group/index
+
+ Template for the Groups page.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * courseid int The course ID.
+ * selectedgroup string The initially selected group.
+ * editgroupsettingsdisabled bool Whether to disable the "Edit group settings" button on load.
+ * deletegroupdisabled bool Whether to disable the "Delete selected group" button on load.
+ * addmembersdisabled bool Whether to disable the "Add/remove users" button on load.
+ * groups array The list of groups.
+ * members array The list of members, grouped based on roles.
+ * undeletablegroups string A JSON string containing an array of group IDs that a user cannot delete.
+
+ Example context (json):
+ {
+ "courseid": "1",
+ "selectedgroup": "Group 1 (3)",
+ "editgroupsettingsdisabled": false,
+ "deletegroupdisabled": false,
+ "addmembersdisabled": false,
+ "groups": [
+ {
+ "value": "1",
+ "text": "Group 1 (3)",
+ "selected": true
+ },
+ {
+ "value": "2",
+ "text": "Group 2 (2)"
+ }
+ ],
+ "members": [
+ {
+ "role": "Student",
+ "rolemembers": [
+ {
+ "value": "1",
+ "text": "John Doe"
+ },
+ {
+ "value": "2",
+ "text": "Jane Doe"
+ },
+ {
+ "value": "3",
+ "text": "John Smith"
+ }
+ ]
+ }
+ ],
+ "undeletablegroups": "[1: true, 3: true]"
+ }
+}}
+<form id="groupeditform" action="index.php" method="post">
+ <div class="container-fluid groupmanagementtable">
+ <div class="row row-fluid rtl-compatible">
+ <input type="hidden" name="id" value="{{courseid}}">
+ <div class="col-md-6 span6 m-b-1 groups">
+ <div class="form-group">
+ <label for="groups">
+ <span id="groupslabel">{{#str}}groups{{/str}}</span>
+ <span id="thegrouping"> </span>
+ </label>
+ <select name="groups[]" multiple="multiple" id="groups" size="15" class="form-control input-block-level">
+ {{#groups}}
+ <option value="{{value}}" {{#selected}}selected="selected"{{/selected}} title="{{{text}}}">{{{text}}}</option>
+ {{/groups}}
+ </select>
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_updatemembers" id="updatemembers" value="{{#str}}showmembersforgroup, group{{/str}}" class="btn btn-default" />
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_showgroupsettingsform" id="showeditgroupsettingsform" value="{{#str}}editgroupsettings, group{{/str}}" {{#editgroupsettingsdisabled}}disabled="disabled"{{/editgroupsettingsdisabled}} class="btn btn-default" />
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_deletegroup" id="deletegroup" value="{{#str}}deleteselectedgroup, group{{/str}}" {{#deletegroupdisabled}}disabled="disabled"{{/deletegroupdisabled}} class="btn btn-default" />
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_showcreateorphangroupform" id="showcreateorphangroupform" value="{{#str}}creategroup, group{{/str}}" class="btn btn-default" />
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_showautocreategroupsform" id="showautocreategroupsform" value="{{#str}}autocreategroups, group{{/str}}" class="btn btn-default" />
+ </div>
+ <div class="form-group">
+ <input type="submit" name="act_showimportgroups" id="showimportgroups" value="{{#str}}importgroups, group{{/str}}" class="btn btn-default" />
+ </div>
+ </div>
+ <div class="col-md-6 span6 m-b-1 members">
+ <div class="form-group">
+ <label for="members">
+ <span id="memberslabel">{{#str}}membersofselectedgroup, group{{/str}}</span>
+ <span id="thegroup">{{{selectedgroup}}}</span>
+ </label>
+ <select size="15" multiple="multiple" class="form-control input-block-level" id="members" name="user">
+ {{#members}}
+ <optgroup label="{{role}}">
+ {{#rolemembers}}
+ <option value="{{value}}">{{{text}}}</option>
+ {{/rolemembers}}
+ </optgroup>
+ {{/members}}
+ </select>
+ </div>
+ <div class="form-group">
+ <input type="submit" value="{{#str}}adduserstogroup, group{{/str}}" class="btn btn-default" {{#addmembersdisabled}}disabled="disabled"{{/addmembersdisabled}} name="act_showaddmembersform" id="showaddmembersform"/>
+ </div>
+ </div>
+ </div>
+ </div>
+</form>
+{{#js}}
+ require(['jquery', 'core/yui'], function($) {
+ $("#groups").change(function() {
+ M.core_group.membersCombo.refreshMembers();
+ });
+ M.core_group.init_index(Y, "{{wwwroot}}", {{courseid}});
+ var undeletableGroups = JSON.parse('{{{undeletablegroups}}}');
+ M.core_group.groupslist(Y, undeletableGroups);
+ });
+{{/js}}
$string['cliincorrectvalueerror'] = '錯誤,將“{$a->option}”的值設為“{$a->value}”是不正確的';
$string['cliincorrectvalueretry'] = '不正確值,請重試';
$string['clitypevalue'] = '輸入值';
-$string['clitypevaluedefault'] = '輸入值,按回車使用預設值({$a})';
-$string['cliunknowoption'] = '錯誤選項:{$a}請使用 --help 選項。';
+$string['clitypevaluedefault'] = '輸入值,按Enter 可使用預設值({$a})';
+$string['cliunknowoption'] = '無法辨識的選項:
+{$a}
+請使用 --help 選項。';
$string['cliyesnoprompt'] = '輸入y(表示是)或n(表示否)';
$string['environmentrequireinstall'] = '必須安裝並啟用';
-$string['environmentrequireversion'] = '需求版本為{$a->needed},而您目前版本為 {$a->current}';
+$string['environmentrequireversion'] = '需要得版本為{$a->needed},而您目前執行的版本為 {$a->current}';
$string['upgradekeyset'] = '升級密鑰(若不要設定請保持空白)';
$string['configmycoursesperpage'] = 'Maximum number of courses to display in any list of a user\'s own courses';
$string['configmymoodleredirect'] = 'This setting forces redirects to /my on login for non-admins and replaces the top level site navigation with /my';
$string['configmypagelocked'] = 'This setting prevents the default page from being edited by any non-admins';
-$string['confignavcourselimit'] = 'Limits the number of courses shown to the user when they are either not logged in or are not enrolled in any courses.';
+$string['confignavcourselimit'] = 'Limits the number of courses shown to the user in the navigation.';
$string['confignavshowallcourses'] = 'This setting determines whether users who are enrolled in courses can see Courses (listing all courses) in the navigation, in addition to My Courses (listing courses in which they are enrolled).';
$string['confignavshowcategories'] = 'Show course categories in the navigation bar and navigation blocks. This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.';
$string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private). This setting will also be used as the envelope sender when sending email.';
$string['cachedef_config'] = 'Config settings';
$string['cachedef_coursecat'] = 'Course categories lists for particular user';
$string['cachedef_coursecatrecords'] = 'Course categories records';
-$string['cachedef_coursecontacts'] = 'List of course contacts';
$string['cachedef_coursecattree'] = 'Course categories tree';
+$string['cachedef_coursecompletion'] = 'Course completion status';
+$string['cachedef_coursecontacts'] = 'List of course contacts';
$string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course';
$string['cachedef_completion'] = 'Activity completion status';
$string['cachedef_databasemeta'] = 'Database meta information';
$string['cachedef_observers'] = 'Event observers';
$string['cachedef_plugin_functions'] = 'Plugins available callbacks';
$string['cachedef_plugin_manager'] = 'Plugin info manager';
+$string['cachedef_postprocessedcss'] = 'Post processed CSS';
$string['cachedef_tagindexbuilder'] = 'Search results for tagged items';
$string['cachedef_questiondata'] = 'Question definitions';
$string['cachedef_repositories'] = 'Repositories instances data';
$string['showtheselogs'] = 'Show these logs';
$string['showthishelpinlanguage'] = 'Show this help in language: {$a}';
$string['schedule'] = 'Schedule';
+$string['sidepanel'] = 'Side panel';
$string['signoutofotherservices'] = 'Sign out everywhere';
$string['signoutofotherservices_help'] = 'If ticked, the account will be signed out of all devices and systems which use web services, such as the mobile app.';
$string['since'] = 'Since';
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
-define(['jquery', 'core/config'], function($, config) {
+define(['jquery', 'core/config', 'core/log'], function($, config, Log) {
+
+ // Keeps track of when the user leaves the page so we know not to show an error.
+ var unloading = false;
/**
* Success handler. Called when the ajax call succeeds. Checks each response and
for (i = 0; i < requests.length; i++) {
var request = requests[i];
- request.deferred.reject(textStatus);
+ if (unloading) {
+ // No need to trigger an error because we are already navigating.
+ Log.error("Page unload: " + textStatus);
+ } else {
+ request.deferred.reject(textStatus);
+ }
}
};
* @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
*/
call: function(requests, async, loginrequired) {
+ $(window).bind('beforeunload', function() {
+ unloading = true;
+ });
var ajaxRequestData = [],
i,
promises = [],
'core:i/marker' => 'fa-circle-o',
'core:i/mean' => 'fa-calculator',
'core:i/menu' => 'fa-ellipsis-v',
+ 'core:i/menubars' => 'fa-bars',
'core:i/mnethost' => 'fa-external-link',
'core:i/moodle_host' => 'fa-graduation-cap',
'core:i/move_2d' => 'fa-arrows',
*/
public static function get_enabled_plugins() {
global $DB;
- return $DB->get_records_menu('repository', array('visible'=>1), 'type ASC', 'type, type AS val');
+ return $DB->get_records_menu('repository', null, 'type ASC', 'type, type AS val');
}
public function get_settings_section_name() {
// Load criteria from database
$records = (array)$DB->get_records('course_completion_criteria', $params);
+ // Order records so activities are in the same order as they appear on the course view page.
+ if ($records) {
+ $activitiesorder = array_keys(get_fast_modinfo($this->course)->get_cms());
+ usort($records, function ($a, $b) use ($activitiesorder) {
+ $aidx = ($a->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
+ array_search($a->moduleinstance, $activitiesorder) : false;
+ $bidx = ($b->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) ?
+ array_search($b->moduleinstance, $activitiesorder) : false;
+ if ($aidx === false || $bidx === false || $aidx == $bidx) {
+ return 0;
+ }
+ return ($aidx < $bidx) ? -1 : 1;
+ });
+ }
+
// Build array of criteria objects
$this->criteria = array();
foreach ($records as $record) {
// Difficult to find affected users, just purge all completion cache.
cache::make('core', 'completion')->purge();
+ cache::make('core', 'coursecompletion')->purge();
}
/**
// Difficult to find affected users, just purge all completion cache.
cache::make('core', 'completion')->purge();
+ cache::make('core', 'coursecompletion')->purge();
}
/**
'staticaccelerationsize' => 2, // Should be current course and site course.
),
+ // Used to cache course completion status.
+ 'coursecompletion' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true,
+ 'simpledata' => true,
+ 'ttl' => 3600,
+ 'staticacceleration' => true,
+ 'staticaccelerationsize' => 30, // Will be users list of current courses in nav.
+ ),
+
// A simple cache that stores whether a user can expand a course in the navigation.
// The key is the course ID and the value will either be 1 or 0 (cast to bool).
// The cache isn't always up to date, it should only ever be used to save a costly call to
'staticaccelerationsize' => 1
),
+ // Caches processed CSS.
+ 'postprocessedcss' => array(
+ 'mode' => cache_store::MODE_APPLICATION,
+ 'simplekeys' => true,
+ 'simpledata' => true,
+ 'staticacceleration' => false,
+ ),
);
<FIELD NAME="sequence" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="availability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Availability restrictions for viewing this section, in JSON format. Null if no restrictions."/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time at which the course section was last changed."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
'classname' => 'core_course_external',
'methodname' => 'duplicate_course',
'classpath' => 'course/externallib.php',
- 'description' => 'Duplicate an existing course (creating a new one), without user data',
+ 'description' => 'Duplicate an existing course (creating a new one).',
'type' => 'write',
'capabilities' => 'moodle/backup:backupcourse, moodle/restore:restorecourse, moodle/course:create'
),
// Automatically generated Moodle v3.3.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2017061201.00) {
+ $table = new xmldb_table('course_sections');
+ $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'availability');
+
+ // Define a field 'timemodified' in the 'course_sections' table.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ upgrade_main_savepoint(true, 2017061201.00);
+ }
+
+ if ($oldversion < 2017061301.00) {
+ // Check if the value of 'navcourselimit' is set to the old default value, if so, change it to the new default.
+ if ($CFG->navcourselimit == 20) {
+ set_config('navcourselimit', 10);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2017061301.00);
+ }
+
return true;
}
const COURSE_MY = 1;
/** var int Course the current user is currently viewing */
const COURSE_CURRENT = 2;
+ /** var string The course index page navigation node */
+ const COURSE_INDEX_PAGE = 'courseindexpage';
/** @var int Parameter to aid the coder in tracking [optional] */
public $id = null;
public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false) {
if ($this->showinflatnavigation) {
$indent = 0;
- if ($this->type == self::TYPE_COURSE) {
+ if ($this->type == self::TYPE_COURSE || $this->key == self::COURSE_INDEX_PAGE) {
$indent = 1;
}
$flat = new flat_navigation_node($this, $indent);
$coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id);
$coursenode->showinflatnavigation = $coursetype == self::COURSE_MY;
+
$coursenode->hidden = (!$course->visible);
$coursenode->title(format_string($course->fullname, true, array('context' => $coursecontext, 'escape' => false)));
if ($canexpandcourse) {
*/
protected function load_courses_enrolled() {
global $CFG;
+
+ $limit = (int) $CFG->navcourselimit;
+
$sortorder = 'visible DESC';
// Prevent undefined $CFG->navsortmycoursessort errors.
if (empty($CFG->navsortmycoursessort)) {
}
// Append the chosen sortorder.
$sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
- $courses = enrol_get_my_courses(null, $sortorder);
- if (count($courses) && $this->show_my_categories()) {
+ $courses = enrol_get_my_courses('*', $sortorder);
+ $flatnavcourses = [];
+
+ // Go through the courses and see which ones we want to display in the flatnav.
+ foreach ($courses as $course) {
+ $classify = course_classify_for_timeline($course);
+
+ if ($classify == COURSE_TIMELINE_INPROGRESS) {
+ $flatnavcourses[$course->id] = $course;
+ }
+ }
+
+ // Get the number of courses that can be displayed in the nav block and in the flatnav.
+ $numtotalcourses = count($courses);
+ $numtotalflatnavcourses = count($flatnavcourses);
+
+ // Reduce the size of the arrays to abide by the 'navcourselimit' setting.
+ $courses = array_slice($courses, 0, $limit, true);
+ $flatnavcourses = array_slice($flatnavcourses, 0, $limit, true);
+
+ // Get the number of courses we are going to show for each.
+ $numshowncourses = count($courses);
+ $numshownflatnavcourses = count($flatnavcourses);
+ if ($numshowncourses && $this->show_my_categories()) {
// Generate an array containing unique values of all the courses' categories.
$categoryids = array();
foreach ($courses as $course) {
$this->add_category($mycategory, $parent, self::TYPE_MY_CATEGORY);
}
}
+
+ // Go through each course now and add it to the nav block, and the flatnav if applicable.
foreach ($courses as $course) {
- $this->add_course($course, false, self::COURSE_MY);
+ $node = $this->add_course($course, false, self::COURSE_MY);
+ if ($node) {
+ $node->showinflatnavigation = false;
+ // Check if we should also add this to the flat nav as well.
+ if (isset($flatnavcourses[$course->id])) {
+ $node->showinflatnavigation = true;
+ }
+ }
+ }
+
+ // Go through each course in the flatnav now.
+ foreach ($flatnavcourses as $course) {
+ // Check if we haven't already added it.
+ if (!isset($courses[$course->id])) {
+ // Ok, add it to the flatnav only.
+ $node = $this->add_course($course, false, self::COURSE_MY);
+ $node->display = false;
+ $node->showinflatnavigation = true;
+ }
+ }
+
+ $showmorelinkinnav = $numtotalcourses > $numshowncourses;
+ $showmorelinkinflatnav = $numtotalflatnavcourses > $numshownflatnavcourses;
+ // Show a link to the course page if there are more courses the user is enrolled in.
+ if ($showmorelinkinnav || $showmorelinkinflatnav) {
+ // Adding hash to URL so the link is not highlighted in the navigation when clicked.
+ $url = new moodle_url('/course/index.php#');
+ $parent = $this->rootnodes['mycourses'];
+ $coursenode = $parent->add(get_string('morenavigationlinks'), $url, self::TYPE_CUSTOM, null, self::COURSE_INDEX_PAGE);
+
+ if ($showmorelinkinnav) {
+ $coursenode->display = true;
+ }
+
+ if ($showmorelinkinflatnav) {
+ $coursenode->showinflatnavigation = true;
+ }
}
}
}
// If category is shown in MyHome then only show enrolled courses and hide empty subcategories,
// else show all courses.
if ($nodetype === self::TYPE_MY_CATEGORY) {
- $courses = enrol_get_my_courses();
+ $courses = enrol_get_my_courses('*');
$categoryids = array();
// Only search for categories if basecategory was found.
$cache->purge();
}
+ // Purge compiled post processed css.
+ cache::make('core', 'postprocessedcss')->purge();
+
if ($PAGE) {
$PAGE->reload_theme();
}
return $csscontent;
}
+ /**
+ * Set post processed CSS content cache.
+ *
+ * @param string $csscontent The post processed CSS content.
+ * @return bool True if the content was successfully cached.
+ */
+ public function set_css_content_cache($csscontent) {
+
+ $cache = cache::make('core', 'postprocessedcss');
+ $key = $this->get_css_cache_key();
+
+ return $cache->set($key, $csscontent);
+ }
+
+ /**
+ * Return cached post processed CSS content.
+ *
+ * @return bool|string The cached css content or false if not found.
+ */
+ public function get_css_cached_content() {
+
+ $key = $this->get_css_cache_key();
+ $cache = cache::make('core', 'postprocessedcss');
+
+ return $cache->get($key);
+ }
+
+ /**
+ * Generate the css content cache key.
+ *
+ * @return string The post processed css cache key.
+ */
+ public function get_css_cache_key() {
+ $nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : '';
+ $rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr';
+
+ return $nosvg . $this->name . '_' . $rtlmode;
+ }
/**
* Get the theme designer css markup,
// If you enable db debugging and exception is thrown, the print footer prints a lot of rubbish
$DB->set_debug(0);
}
- echo $OUTPUT->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
+ if (AJAX_SCRIPT) {
+ // If we are in an AJAX script we don't want to use PREFERRED_RENDERER_TARGET.
+ // Because we know we will want to use ajax format.
+ $renderer = $PAGE->get_renderer('core', null, 'ajax');
+ } else {
+ $renderer = $OUTPUT;
+ }
+ echo $renderer->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
$info->errorcode);
} catch (Exception $e) {
$out_ex = $e;
// (That is, the Moodle server uses http, with an external box translating everything to https).
if (empty($CFG->sslproxy)) {
if ($rurl['scheme'] === 'http' and $wwwroot['scheme'] === 'https') {
- print_error('sslonlyaccess', 'error');
+ if (defined('REQUIRE_CORRECT_ACCESS') && REQUIRE_CORRECT_ACCESS) {
+ print_error('sslonlyaccess', 'error');
+ } else {
+ redirect($CFG->wwwroot, get_string('wwwrootmismatch', 'error', $CFG->wwwroot), 3);
+ }
}
} else {
if ($wwwroot['scheme'] !== 'https') {
* Of course you can use sub-queries, JOINS etc. by putting them in the
* appropriate clause of the query.
*/
- function set_sql($fields, $from, $where, array $params = NULL) {
+ function set_sql($fields, $from, $where, array $params = array()) {
$this->sql = new stdClass();
$this->sql->fields = $fields;
$this->sql->from = $from;
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/ajax', 'core/notification', 'core/log'], function($, Ajax, Notification, Log) {
+define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
/**
* Retrieve a list of messages from the server.
*
var promise = Ajax.call([request])[0];
- promise.fail(function(e) {
- Log.error('Could not retrieve unread message count: ' + e.message);
- });
+ promise.fail(Notification.exception);
return promise;
};
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['core/ajax', 'core/notification', 'core/log'], function(Ajax, Notification, Log) {
+define(['core/ajax', 'core/notification'], function(Ajax, Notification) {
/**
* Retrieve a list of notifications from the server.
*
var promise = Ajax.call([request])[0];
- promise.fail(function(e) {
- Log.error('Could not retrieve notifications count: ' + e.message);
- });
+ promise.fail(Notification.exception);
return promise;
};
// Automatically generated Moodle v3.3.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2017061200) {
+ // Data fix any assign group override event priorities which may have been accidentally nulled due to a bug on the group
+ // overrides edit form.
+
+ // First, find all assign group override events having null priority (and join their corresponding assign_overrides entry).
+ $sql = "SELECT e.id AS id, o.sortorder AS priority
+ FROM {assign_overrides} o
+ JOIN {event} e ON (e.modulename = 'assign' AND o.assignid = e.instance AND e.groupid = o.groupid)
+ WHERE o.groupid IS NOT NULL AND e.priority IS NULL
+ ORDER BY o.id";
+ $affectedrs = $DB->get_recordset_sql($sql);
+
+ // Now update the event's priority based on the assign_overrides sortorder we found. This uses similar logic to
+ // assign_refresh_events(), except we've restricted the set of assignments and overrides we're dealing with here.
+ foreach ($affectedrs as $record) {
+ $DB->set_field('event', 'priority', $record->priority, ['id' => $record->id]);
+ }
+ $affectedrs->close();
+
+ // Main savepoint reached.
+ upgrade_mod_savepoint(true, 2017061200, 'assign');
+ }
return true;
}
global $DB;
$feedbackcomment = $this->get_feedback_comments($grade->id);
$quickgradecomments = optional_param('quickgrade_comments_' . $userid, null, PARAM_RAW);
- if (!$quickgradecomments) {
+ if (!$quickgradecomments && $quickgradecomments !== '') {
return true;
}
if ($feedbackcomment) {
--- /dev/null
+@mod @mod_assign @assignfeedback @assignfeedback_comments
+Feature: In an assignment, teachers can provide feedback comments on student submissions
+ In order to provide feedback to students on their assignments
+ As a teacher,
+ I need to create feedback comments against their submissions.
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category | groupmode |
+ | Course 1 | C1 | 0 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | teacher |
+ | student1 | C1 | student |
+
+ @javascript
+ Scenario: Teachers should be able to add and remove feedback comments via the quick grading interface
+ Given the following "activities" exist:
+ | activity | course | idnumber | name | assignsubmission_onlinetext_enabled | assignfeedback_comments_enabled |
+ | assign | C1 | assign1 | Test assignment1 | 1 | 1 |
+ And I log in as "student1"
+ And I am on "Course 1" course homepage
+ And I follow "Test assignment1"
+ And I press "Add submission"
+ And I set the following fields to these values:
+ | Online text | I'm the student1 submission |
+ And I press "Save changes"
+ And I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test assignment1"
+ And I navigate to "View all submissions" in current page administration
+ Then I click on "Quick grading" "checkbox"
+ And I set the field "Feedback comments" to "Feedback from teacher."
+ And I press "Save all quick grading changes"
+ And I should see "The grade changes were saved"
+ And I press "Continue"
+ And I should see "Feedback from teacher."
+ And I set the field "Feedback comments" to ""
+ And I press "Save all quick grading changes"
+ And I should see "The grade changes were saved"
+ And I press "Continue"
+ And I should not see "Feedback from teacher."
static $markers = null;
static $markerlist = array();
if ($markers === null) {
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->assignment->get_context(), 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
$markerlist[0] = get_string('choosemarker', 'assign');
$viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context());
foreach ($markers as $marker) {
// Get markers to use in drop lists.
$markingallocationoptions = array();
if ($markingallocation) {
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
$markingallocationoptions[''] = get_string('filternone', 'assign');
$markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
$viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
'usershtml' => $usershtml,
);
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
$markerlist = array();
foreach ($markers as $marker) {
$markerlist[$marker->id] = fullname($marker);
if ($markingallocation) {
$markingallocationoptions[''] = get_string('filternone', 'assign');
$markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
foreach ($markers as $marker) {
$markingallocationoptions[$marker->id] = fullname($marker);
}
$this->get_instance()->markingallocation &&
has_capability('mod/assign:manageallocations', $this->context)) {
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
$markerlist = array('' => get_string('choosemarker', 'assign'));
$viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
foreach ($markers as $marker) {
'usershtml' => '' // initialise these parameters with real information.
);
- list($sort, $params) = users_order_by_sql();
- $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+ list($sort, $params) = users_order_by_sql('u');
+ // Only enrolled users could be assigned as potential markers.
+ $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
$markerlist = array();
foreach ($markers as $marker) {
$markerlist[$marker->id] = fullname($marker);
/** @var int userid, if provided. */
protected $userid;
+ /** @var int sortorder, if provided. */
+ protected $sortorder;
+
/**
* Constructor.
* @param moodle_url $submiturl the form action URL.
$this->groupmode = $groupmode;
$this->groupid = empty($override->groupid) ? 0 : $override->groupid;
$this->userid = empty($override->userid) ? 0 : $override->userid;
+ $this->sortorder = empty($override->sortorder) ? null : $override->sortorder;
parent::__construct($submiturl, null, 'post');
$mform->addElement('select', 'groupid',
get_string('overridegroup', 'assign'), $groupchoices);
$mform->freeze('groupid');
+ // Add a sortorder element.
+ $mform->addElement('hidden', 'sortorder', $this->sortorder);
+ $mform->setType('sortorder', PARAM_INT);
+ $mform->freeze('sortorder');
} else {
// Prepare the list of groups.
$groups = groups_get_all_groups($cm->course);
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for (some of) mod/assign/markerallocaion_test.php.
+ *
+ * @package mod_assign
+ * @category test
+ * @copyright 2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/accesslib.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * This class tests some of marker allocation functionality.
+ *
+ * @package mod_assign
+ * @copyright 2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_assign_markerallocation_testcase extends advanced_testcase {
+
+ /**
+ * Create all the needed elements to test the difference between both functions.
+ */
+ public function test_markerusers() {
+ $this->resetAfterTest();
+ global $DB;
+
+ // Create a course, by default it is created with 5 sections.
+ $this->course = $this->getDataGenerator()->create_course();
+
+ // Setting assing module, markingworkflow and markingallocation set to 1 to enable marker allocation.
+ $record = new stdClass();
+ $record->course = $this->course;
+
+ $modulesettings = array(
+ 'alwaysshowdescription' => 1,
+ 'submissiondrafts' => 1,
+ 'requiresubmissionstatement' => 0,
+ 'sendnotifications' => 0,
+ 'sendstudentnotifications' => 1,
+ 'sendlatenotifications' => 0,
+ 'duedate' => 0,
+ 'allowsubmissionsfromdate' => 0,
+ 'grade' => 100,
+ 'cutoffdate' => 0,
+ 'teamsubmission' => 0,
+ 'requireallteammemberssubmit' => 0,
+ 'teamsubmissiongroupingid' => 0,
+ 'blindmarking' => 0,
+ 'attemptreopenmethod' => 'none',
+ 'maxattempts' => -1,
+ 'markingworkflow' => 1,
+ 'markingallocation' => 1,
+ );
+
+ $assignelement = $this->getDataGenerator()->create_module('assign', $record, $modulesettings);
+
+ $coursesectionid = course_add_cm_to_section($this->course->id, $assignelement->id, 1);
+
+ // Adding users to the course.
+ $userdata = array();
+ $userdata['firstname'] = 'teacher1';
+ $userdata['lasttname'] = 'lastname_teacher1';
+
+ $user1 = $this->getDataGenerator()->create_user($userdata);
+
+ $this->getDataGenerator()->enrol_user($user1->id, $this->course->id, 'teacher');
+
+ $userdata = array();
+ $userdata['firstname'] = 'teacher2';
+ $userdata['lasttname'] = 'lastname_teacher2';
+
+ $user2 = $this->getDataGenerator()->create_user($userdata);
+
+ $this->getDataGenerator()->enrol_user($user2->id, $this->course->id, 'teacher');
+
+ $userdata = array();
+ $userdata['firstname'] = 'student';
+ $userdata['lasttname'] = 'lastname_student';
+
+ $user3 = $this->getDataGenerator()->create_user($userdata);
+
+ $this->getDataGenerator()->enrol_user($user3->id, $this->course->id, 'student');
+
+ // Adding manager to the system.
+ $userdata = array();
+ $userdata['firstname'] = 'Manager';
+ $userdata['lasttname'] = 'lastname_Manager';
+
+ $user4 = $this->getDataGenerator()->create_user($userdata);
+
+ // Getting id of manager role.
+ $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+ if (!empty($managerrole)) {
+ // By default the context of the system is assigned.
+ $idassignment = $this->getDataGenerator()->role_assign($managerrole->id, $user4->id);
+ }
+
+ $oldusers = array($user1, $user2, $user4);
+ $newusers = array($user1, $user2);
+
+ list($sort, $params) = users_order_by_sql('u');
+
+ // Old code, it must return 3 users: teacher1, teacher2 and Manger.
+ $oldmarkers = get_users_by_capability(context_course::instance($this->course->id), 'mod/assign:grade', '', $sort);
+ // New code, it must return 2 users: teacher1 and teacher2.
+ $newmarkers = get_enrolled_users(context_course::instance($this->course->id), 'mod/assign:grade', 0, 'u.*', $sort);
+
+ // Test result quantity.
+ $this->assertEquals(count($oldusers), count($oldmarkers));
+ $this->assertEquals(count($newusers), count($newmarkers));
+ $this->assertEquals(count($oldmarkers) > count($newmarkers), true);
+
+ // Elements expected with new code.
+ foreach ($newmarkers as $key => $nm) {
+ $this->assertEquals($nm, $newusers[array_search($nm, $newusers)]);
+ }
+
+ // Elements expected with old code.
+ foreach ($oldusers as $key => $os) {
+ $this->assertEquals($os->id, $oldmarkers[$os->id]->id);
+ unset($oldmarkers[$os->id]);
+ }
+
+ $this->assertEquals(count($oldmarkers), 0);
+
+ }
+}
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2017051500; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2017061200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2017050500; // Requires this Moodle version.
$plugin->cron = 60;
// Update completion state.
$completion = new completion_info($this->cm->get_course());
- if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) && $this->feedback->completionsubmit) {
+ if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) &&
+ $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {
$completion->update_state($this->cm, COMPLETION_COMPLETE);
}
}
$feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
$responsestable = new mod_feedback_responses_table($feedbackstructure, $groupid);
+ // Ensure responses number is correct prior returning them.
+ $feedbackstructure->shuffle_anonym_responses();
$anonresponsestable = new mod_feedback_responses_anon_table($feedbackstructure, $groupid);
$result = array(
if ($completeds = $DB->get_records('feedback_completed', $params)) {
$completion = new completion_info($course);
foreach ($completeds as $completed) {
+ $DB->delete_records('feedback_completed', array('id' => $completed->id));
// Update completion state
- if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+ if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+ $feedback->completionsubmit) {
$completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
}
- $DB->delete_records('feedback_completed', array('id'=>$completed->id));
}
}
$DB->delete_records('feedback_completedtmp', array('feedback'=>$feedback->id));
if ($completeds = $DB->get_records('feedback_completed', array('feedback'=>$feedback->id))) {
$completion = new completion_info($course);
foreach ($completeds as $completed) {
+ $DB->delete_records('feedback_completed', array('id' => $completed->id));
// Update completion state
- if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+ if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+ $feedback->completionsubmit) {
$completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
}
- $DB->delete_records('feedback_completed', array('id'=>$completed->id));
}
}
//first we delete all related values
$DB->delete_records('feedback_value', array('completed' => $completed->id));
+ // Delete the completed record.
+ $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
+
// Update completion state
$completion = new completion_info($course);
- if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+ if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) {
$completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
}
- // Last we delete the completed-record.
- $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
-
// Trigger event for the delete action we performed.
$event = \mod_feedback\event\response_deleted::create_from_record($completed, $cm, $feedback);
$event->trigger();
}
list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
+ // Convert page object to page record.
+ foreach ($answerpages as $answerp) {
+ $answerp->page = self::get_page_fields($answerp->page);
+ }
$result = array(
'answerpages' => $answerpages,
'answerpages' => new external_multiple_structure(
new external_single_structure(
array(
+ 'page' => self::get_page_structure(VALUE_OPTIONAL),
'title' => new external_value(PARAM_RAW, 'Page title.'),
'contents' => new external_value(PARAM_RAW, 'Page contents.'),
'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
while ($pageid != 0) { // EOL
$page = $lessonpages[$pageid];
$answerpage = new stdClass;
+ // Keep the original page object.
+ $answerpage->page = $page;
$data ='';
$answerdata = new stdClass;
$this->assertEquals(1, $result['userstats']['gradeinfo']['total']); // Total correct answers.
$this->assertEquals(100, $result['userstats']['gradeinfo']['grade']); // Correct answer.
+ // Check page object contains the lesson pages answered.
+ $pagesanswered = array();
+ foreach ($result['answerpages'] as $answerp) {
+ $pagesanswered[] = $answerp['page']['id'];
+ }
+ sort($pagesanswered);
+ $this->assertEquals(array($this->page1->id, $this->page2->id), $pagesanswered);
+
// Test second attempt unfinished.
$result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
This files describes API changes in the lesson code.
+=== 3.4 ===
+
+* External function mod_lesson_external::get_user_attempt() now returns the full page object inside each answerpages.
+
=== 3.3 ===
* lesson::callback_on_view() has an additional optional parameter $redirect default to true.
'classname' => 'mod_lti_external',
'methodname' => 'view_lti',
'description' => 'Trigger the course module viewed event and update the module completion status.',
- 'type' => 'write',
+ 'type' => 'read',
'capabilities' => 'mod/lti:view',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
$cm = get_fast_modinfo($event->courseid)->instances['scorm'][$event->instance];
+ if (has_capability('mod/scorm:viewreport', $cm->context)) {
+ // Teachers do not need to be reminded to complete a scorm.
+ return null;
+ }
+
if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
// The scorm has closed so the user can no longer submit anything.
return null;
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
+ // Only students see scorm events.
+ $this->setUser($this->student);
+
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
+ // Only students see scorm events.
+ $this->setUser($this->student);
+
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
+ // Only students see scorm events.
+ $this->setUser($this->student);
+
// Create an action factory.
$factory = new \core_calendar\action_factory();
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package qtype_multichoice
+ * @copyright 2015 onwards Nadav Kavalerchik
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Admin settings class for the multichoice question type method.
+ *
+ * Just so we can lazy-load the numbering style choices.
+ *
+ * @copyright 2015 onwards Nadav Kavalerchik
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multichoice_admin_setting_answernumbering extends admin_setting_configselect {
+
+ /**
+ * This function may be used in ancestors for lazy loading of choices
+ *
+ * Override this method if loading of choices is expensive, such
+ * as when it requires multiple db requests.
+ *
+ * @return bool true if loaded, false if error
+ */
+ public function load_choices() {
+ global $CFG;
+
+ if (is_array($this->choices)) {
+ return true;
+ }
+
+ require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php');
+ $this->choices = qtype_multichoice::get_numbering_styles();
+
+ return true;
+ }
+}
);
$mform->addElement('select', 'single',
get_string('answerhowmany', 'qtype_multichoice'), $menu);
- $mform->setDefault('single', 1);
+ $mform->setDefault('single', get_config('qtype_multichoice', 'answerhowmany'));
$mform->addElement('advcheckbox', 'shuffleanswers',
get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0, 1));
$mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice');
- $mform->setDefault('shuffleanswers', 1);
+ $mform->setDefault('shuffleanswers', get_config('qtype_multichoice', 'shuffleanswers'));
$mform->addElement('select', 'answernumbering',
get_string('answernumbering', 'qtype_multichoice'),
qtype_multichoice::get_numbering_styles());
- $mform->setDefault('answernumbering', 'abc');
+ $mform->setDefault('answernumbering', get_config('qtype_multichoice', 'answernumbering'));
$this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'),
question_bank::fraction_options_full(), max(5, QUESTION_NUMANS_START));
*/
$string['answerhowmany'] = 'One or multiple answers?';
+$string['answerhowmany_desc'] = 'Should the default for new multichoice questions be to require single or multiple answers?';
$string['answernumbering'] = 'Number the choices?';
$string['answernumbering123'] = '1., 2., 3., ...';
$string['answernumberingabc'] = 'a., b., c., ...';
$string['answernumberingiii'] = 'i., ii., iii., ...';
$string['answernumberingIIII'] = 'I., II., III., ...';
$string['answernumberingnone'] = 'No numbering';
+$string['answernumbering_desc'] = 'Set the default numbering style for new multichoice questions.';
$string['answersingleno'] = 'Multiple answers allowed';
$string['answersingleyes'] = 'One answer only';
$string['choiceno'] = 'Choice {$a}';
$string['selectmulti'] = 'Select one or more:';
$string['selectone'] = 'Select one:';
$string['shuffleanswers'] = 'Shuffle the choices?';
+$string['shuffleanswers_desc'] = 'Should the default for new nultichoice questions be to shuffle answers?';
$string['shuffleanswers_help'] = 'If enabled, the order of the answers is randomly shuffled for each attempt, provided that "Shuffle within questions" in the activity settings is also enabled.';
$string['singleanswer'] = 'Choose one answer.';
$string['toomanyselected'] = 'You have selected too many options.';
}
$radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
html_writer::tag('label',
- $this->number_in_style($value, $question->answernumbering) .
+ html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
$question->make_html_inline($question->format_text(
$ans->answer, $ans->answerformat,
$qa, 'question', 'answer', $ansid)),
- array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
+ array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
// Param $options->suppresschoicefeedback is a hack specific to the
// oumultiresponse question type. It would be good to refactor to
--- /dev/null
+<?php
+// 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 <http://www.gnu.org/licenses/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package qtype_multichoice
+ * @copyright 2015 onwards Nadav Kavalerchik
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($ADMIN->fulltree) {
+ $menu = array(
+ new lang_string('answersingleno', 'qtype_multichoice'),
+ new lang_string('answersingleyes', 'qtype_multichoice'),
+ );
+ $settings->add(new admin_setting_configselect('qtype_multichoice/answerhowmany',
+ new lang_string('answerhowmany', 'qtype_multichoice'),
+ new lang_string('answerhowmany_desc', 'qtype_multichoice'), '1', $menu));
+
+ $settings->add(new admin_setting_configcheckbox('qtype_multichoice/shuffleanswers',
+ new lang_string('shuffleanswers', 'qtype_multichoice'),
+ new lang_string('shuffleanswers_desc', 'qtype_multichoice'), '1'));
+
+ $settings->add(new qtype_multichoice_admin_setting_answernumbering('qtype_multichoice/answernumbering',
+ new lang_string('answernumbering', 'qtype_multichoice'),
+ new lang_string('answernumbering_desc', 'qtype_multichoice'), 'abc', null ));
+
+}
// bad luck, we can not link other report
} else if (empty($param->crosstab)) {
foreach ($stats as $stat) {
- $a = array(userdate($stat->timeend-(60*60*24),get_string('strftimedate'),$CFG->timezone),$stat->line1);
+ $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone),
+ $stat->line1);
if (isset($stat->line2)) {
$a[] = $stat->line2;
}
}
}
if (!array_key_exists($stat->timeend,$times)) {
- $times[$stat->timeend] = userdate($stat->timeend,get_string('strftimedate'),$CFG->timezone);
+ $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'),
+ $CFG->timezone);
}
}
foreach ($stats as $stat) {
// Build the array of formatted times indexed by timestamp used as labels.
if (!array_key_exists($stat->timeend, $times)) {
- $times[$stat->timeend] = userdate($stat->timeend, get_string('strftimedate'), $CFG->timezone);
+ $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone);
// Just add the data if the time hasn't been added yet.
// The number of lines of data must match the number of labels.
// Build the array of formatted times indexed by timestamp used as labels.
if (!array_key_exists($stat->timeend, $times)) {
- $times[$stat->timeend] = userdate($stat->timeend, get_string('strftimedate'), $CFG->timezone);
+ $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone);
}
}
// Fill empty days with zero to avoid chart errors.
if (!empty($stat->zerofixed)) { // Don't know why this is necessary, see stats_fix_zeros above - MD
continue;
}
- $a = array(userdate($stat->timeend,get_string('strftimedate'),$CFG->timezone),$stat->line1);
+ $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone), $stat->line1);
$a[] = $stat->line2;
$a[] = $stat->line3;
$table->data[] = $a;
<div class="container-fluid navbar-nav">
<div data-region="drawer-toggle">
- <button aria-expanded="{{#navdraweropen}}true{{/navdraweropen}}{{^navdraweropen}}false{{/navdraweropen}}" aria-controls="nav-drawer" type="button" class="btn pull-xs-left m-r-1 btn-secondary" data-action="toggle-drawer" data-side="left" data-preference="drawer-open-nav"><span aria-hidden="true">☰</span><span class="sr-only">{{#str}}expand, core{{/str}}</span></button>
+ <button aria-expanded="{{#navdraweropen}}true{{/navdraweropen}}{{^navdraweropen}}false{{/navdraweropen}}" aria-controls="nav-drawer" type="button" class="btn pull-xs-left m-r-1 btn-secondary" data-action="toggle-drawer" data-side="left" data-preference="drawer-open-nav">{{#pix}}i/menubars{{/pix}}<span class="sr-only">{{#str}}sidepanel, core{{/str}}</span></button>
</div>
<a href="{{{ config.wwwroot }}}" class="navbar-brand {{# output.should_display_navbar_logo }}has-logo{{/ output.should_display_navbar_logo }}
* @return moodle_url|false
*/
public function get_logo_url($maxwidth = null, $maxheight = 100) {
+ global $CFG;
+
if (!empty($this->page->theme->settings->logo)) {
- return $this->page->theme->setting_file_url('logo', 'logo');
+ $url = $this->page->theme->setting_file_url('logo', 'logo');
+ // Get a URL suitable for moodle_url.
+ $relativebaseurl = preg_replace('|^https?://|i', '//', $CFG->wwwroot);
+ $url = str_replace($relativebaseurl, '', $url);
+ return new moodle_url($url);
}
return parent::get_logo_url($maxwidth, $maxheight);
}
* @return moodle_url|false
*/
public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
+ global $CFG;
+
if (!empty($this->page->theme->settings->smalllogo)) {
- return $this->page->theme->setting_file_url('smalllogo', 'smalllogo');
+ $url = $this->page->theme->setting_file_url('smalllogo', 'smalllogo');
+ // Get a URL suitable for moodle_url.
+ $relativebaseurl = preg_replace('|^https?://|i', '//', $CFG->wwwroot);
+ $url = str_replace($relativebaseurl, '', $url);
+ return new moodle_url($url);
}
return parent::get_compact_logo_url($maxwidth, $maxheight);
}
css_store_css($theme, "$candidatedir/editor.css", $csscontent, false);
} else {
+ // Fetch a lock whilst the CSS is fetched as this can be slow and CPU intensive.
+ // Each client should wait for one to finish the compilation before starting the compiler.
+ $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
+ $lock = $lockfactory->get_lock($themename, rand(90, 120));
+
+ if (file_exists($candidatesheet)) {
+ // The file was built while we waited for the lock, we release the lock and serve the file.
+ if ($lock) {
+ $lock->release();
+ }
- $lock = null;
- // Lock system to prevent concurrent requests to compile LESS/SCSS, which is really slow and CPU intensive.
- // Each client should wait for one to finish the compilation before starting a new compiling process.
- // We only do this when the file will be cached...
- if (in_array($type, ['less', 'scss']) && $cache) {
- $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
- // We wait for the lock to be acquired, the timeout does not need to be strict here.
- $lock = $lockfactory->get_lock($themename, rand(15, 30));
- if (file_exists($candidatesheet)) {
- // The file was built while we waited for the lock, we release the lock and serve the file.
- if ($lock) {
- $lock->release();
- }
+ if ($cache) {
css_send_cached_css($candidatesheet, $etag);
+ } else {
+ css_send_uncached_css(file_get_contents($candidatesheet));
}
}
- // Older IEs require smaller chunks.
- $csscontent = $theme->get_css_content();
+ // The lock is still held, and the sheet still does not exist.
+ // Compile the CSS content.
+ if (!$csscontent = $theme->get_css_cached_content()) {
+ $csscontent = $theme->get_css_content();
+ $theme->set_css_content_cache($csscontent);
+ }
$relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
if (!empty($slashargument)) {
css_store_css($theme, "$candidatedir/$type.css", $csscontent, true, $chunkurl);
- // Release the lock.
if ($lock) {
+ // Now that the CSS has been generated and/or stored, release the lock.
+ // This will allow waiting clients to use the newly generated and stored CSS.
$lock->release();
}
}
// Visitor (Not a guest user, userid=0).
$CFG->forceloginforprofiles = 1;
$this->setUser($user8);
+ $this->assertFalse(user_can_view_profile($user1));
$allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id');
// Let us test with guest user.
}
// Even with cap, still guests should not be allowed in.
- assign_capability('moodle/user:viewdetails', CAP_ALLOW, $allroles['guest'], context_system::instance()->id, true);
+ $guestrole = $DB->get_records_menu('role', array('shortname' => 'guest'), 'id', 'archetype, id');
+ assign_capability('moodle/user:viewdetails', CAP_ALLOW, $guestrole['guest'], context_system::instance()->id, true);
reload_all_capabilities();
foreach ($users as $user) {
$this->assertFalse(user_can_view_profile($user));
defined('MOODLE_INTERNAL') || die();
-$version = 2017060800.01; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2017061900.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
-$release = '3.4dev (Build: 20170608)'; // Human-friendly version name
+$release = '3.4dev (Build: 20170616)'; // Human-friendly version name
$branch = '34'; // This version's branch.
$maturity = MATURITY_ALPHA; // This version's maturity level.