MDL-44070 Conditional availability enhancements (3): conditions

Includes all the conditions that were in previous Moodle versions:

* Date
* Grade
* Completion (of another activity)
* User profile field

Also includes conditions that are used to reimplement
groupmembersonly:

* Grouping
* Group

For each condition, the component plus unit tests are included.

PLEASE NOTE: The code to actually check each condition is reused
from previous Moodle versions and has not been modified except to
pass codechecker. This is intentional, to reduce the risk of the
change and maximise the chance that behaviour is preserved. Some
of this code might not be very good and might need updating but
that can happen separately.

AMOS BEGIN
 CPY [contains,core_condition],[op_contains,availability_profile]
 CPY [doesnotcontain,core_condition],[op_doesnotcontain,availability_profile]
 CPY [endswith,core_condition],[op_endswith,availability_profile]
 CPY [isempty,core_condition],[op_isempty,availability_profile]
 CPY [isequalto,core_condition],[op_isequalto,availability_profile]
 CPY [isnotempty,core_condition],[op_isnotempty,availability_profile]
 CPY [startswith,core_condition],[op_startswith,availability_profile]
 CPY [completion_fail,core_condition],[option_fail,availability_completion]
 CPY [completion_pass,core_condition],[option_pass,availability_completion]
 CPY [completion_complete,core_condition],[option_complete,availability_completion]
 CPY [completion_incomplete,core_condition],[option_incomplete,availability_completion]
AMOS END
This commit is contained in:
sam marshall 2014-02-18 16:20:54 +00:00
parent d3db4b037c
commit e01efa2cfd
62 changed files with 5984 additions and 6 deletions

View File

@ -0,0 +1,269 @@
<?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/>.
/**
* Activity completion condition.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_completion;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/completionlib.php');
/**
* Activity completion condition.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var int ID of module that this depends on */
protected $cmid;
/** @var int Expected completion type (one of the COMPLETE_xx constants) */
protected $expectedcompletion;
/** @var array Array of modules used in these conditions for course */
protected static $modsusedincondition = array();
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get cmid.
if (isset($structure->cm) && is_int($structure->cm)) {
$this->cmid = $structure->cm;
} else {
throw new \coding_exception('Missing or invalid ->cm for completion condition');
}
// Get expected completion.
if (isset($structure->e) && in_array($structure->e,
array(COMPLETION_COMPLETE, COMPLETION_INCOMPLETE,
COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL))) {
$this->expectedcompletion = $structure->e;
} else {
throw new \coding_exception('Missing or invalid ->e for completion condition');
}
}
public function save() {
return (object)array('type' => 'completion',
'cm' => $this->cmid, 'e' => $this->expectedcompletion);
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$modinfo = $info->get_modinfo();
$completion = new \completion_info($modinfo->get_course());
if (!array_key_exists($this->cmid, $modinfo->cms)) {
// If the cmid cannot be found, always return false regardless
// of the condition or $not state. (Will be displayed in the
// information message.)
$allow = false;
} else {
// The completion system caches its own data so no caching needed here.
$completiondata = $completion->get_data((object)array('id' => $this->cmid),
$grabthelot, $userid, $modinfo);
$allow = true;
if ($this->expectedcompletion == COMPLETION_COMPLETE) {
// Complete also allows the pass, fail states.
switch ($completiondata->completionstate) {
case COMPLETION_COMPLETE:
case COMPLETION_COMPLETE_FAIL:
case COMPLETION_COMPLETE_PASS:
break;
default:
$allow = false;
}
} else {
// Other values require exact match.
if ($completiondata->completionstate != $this->expectedcompletion) {
$allow = false;
}
}
if ($not) {
$allow = !$allow;
}
}
return $allow;
}
/**
* Returns a more readable keyword corresponding to a completion state.
*
* Used to make lang strings easier to read.
*
* @param int $completionstate COMPLETION_xx constant
* @return string Readable keyword
*/
protected static function get_lang_string_keyword($completionstate) {
switch($completionstate) {
case COMPLETION_INCOMPLETE:
return 'incomplete';
case COMPLETION_COMPLETE:
return 'complete';
case COMPLETION_COMPLETE_PASS:
return 'complete_pass';
case COMPLETION_COMPLETE_FAIL:
return 'complete_fail';
default:
throw new \coding_exception('Unexpected completion state: ' . $completionstate);
}
}
public function get_description($full, $not, \core_availability\info $info) {
// Get name for module.
$modinfo = $info->get_modinfo();
if (!array_key_exists($this->cmid, $modinfo->cms)) {
$modname = get_string('missing', 'availability_completion');
} else {
$modname = '<AVAILABILITY_CMNAME_' . $modinfo->cms[$this->cmid]->id . '/>';
}
// Work out which lang string to use.
if ($not) {
// Convert NOT strings to use the equivalent where possible.
switch ($this->expectedcompletion) {
case COMPLETION_INCOMPLETE:
$str = 'requires_' . self::get_lang_string_keyword(COMPLETION_COMPLETE);
break;
case COMPLETION_COMPLETE:
$str = 'requires_' . self::get_lang_string_keyword(COMPLETION_INCOMPLETE);
break;
default:
// The other two cases do not have direct opposites.
$str = 'requires_not_' . self::get_lang_string_keyword($this->expectedcompletion);
break;
}
} else {
$str = 'requires_' . self::get_lang_string_keyword($this->expectedcompletion);
}
return get_string($str, 'availability_completion', $modname);
}
protected function get_debug_string() {
switch ($this->expectedcompletion) {
case COMPLETION_COMPLETE :
$type = 'COMPLETE';
break;
case COMPLETION_INCOMPLETE :
$type = 'INCOMPLETE';
break;
case COMPLETION_COMPLETE_PASS:
$type = 'COMPLETE_PASS';
break;
case COMPLETION_COMPLETE_FAIL:
$type = 'COMPLETE_FAIL';
break;
default:
throw new \coding_exception('Unexpected expected completion');
}
return 'cm' . $this->cmid . ' ' . $type;
}
public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
global $DB;
$rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid);
if (!$rec || !$rec->newitemid) {
// If we are on the same course (e.g. duplicate) then we can just
// use the existing one.
if ($DB->record_exists('course_modules',
array('id' => $this->cmid, 'course' => $courseid))) {
return false;
}
// Otherwise it's a warning.
$this->cmid = 0;
$logger->process('Restored item (' . $name .
') has availability condition on module that was not restored',
\backup::LOG_WARNING);
} else {
$this->cmid = (int)$rec->newitemid;
}
return true;
}
/**
* Used in course/lib.php because we need to disable the completion JS if
* a completion value affects a conditional activity.
*
* @param stdClass $course Moodle course object
* @param int $cmid Course-module id
* @return bool True if this is used in a condition, false otherwise
*/
public static function completion_value_used($course, $cmid) {
// Have we already worked out a list of required completion values
// for this course? If so just use that.
if (!array_key_exists($course->id, self::$modsusedincondition)) {
// We don't have data for this course, build it.
$modinfo = get_fast_modinfo($course);
self::$modsusedincondition[$course->id] = array();
// Activities.
foreach ($modinfo->cms as $othercm) {
if (is_null($othercm->availability)) {
continue;
}
$ci = new \core_availability\info_module($othercm);
$tree = $ci->get_availability_tree();
foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
self::$modsusedincondition[$course->id][$cond->cmid] = true;
}
}
// Sections.
foreach ($modinfo->get_section_info_all() as $section) {
if (is_null($section->availability)) {
continue;
}
$ci = new \core_availability\info_section($section);
$tree = $ci->get_availability_tree();
foreach ($tree->get_all_children('availability_completion\condition') as $cond) {
self::$modsusedincondition[$course->id][$cond->cmid] = true;
}
}
}
return array_key_exists($cmid, self::$modsusedincondition[$course->id]);
}
/**
* Wipes the static cache of modules used in a condition (for unit testing).
*/
public static function wipe_static_cache() {
self::$modsusedincondition = array();
}
public function update_dependency_id($table, $oldid, $newid) {
if ($table === 'course_modules' && (int)$this->cmid === (int)$oldid) {
$this->cmid = $newid;
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,94 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_completion;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
/**
* @var array Cached init parameters
*/
protected $cacheparams = array();
/**
* @var string IDs of course, cm, and section for cache (if any)
*/
protected $cachekey = '';
protected function get_javascript_strings() {
return array('option_complete', 'option_fail', 'option_incomplete', 'option_pass',
'label_cm', 'label_completion');
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
// Use cached result if available. The cache is just because we call it
// twice (once from allow_add) so it's nice to avoid doing all the
// print_string calls twice.
$cachekey = $course->id . ',' . ($cm ? $cm->id : '') . ($section ? $section->id : '');
if ($cachekey !== $this->cachekey) {
// Get list of activities on course which have completion values,
// to fill the dropdown.
$context = \context_course::instance($course->id);
$cms = array();
$modinfo = get_fast_modinfo($course);
foreach ($modinfo->cms as $id => $othercm) {
// Add each course-module if it has completion turned on and is not
// the one currently being edited.
if ($othercm->completion && (empty($cm) || $cm->id != $id)) {
$cms[] = (object)array('id' => $id, 'name' =>
format_string($othercm->name, true, array('context' => $context)));
}
}
$this->cachekey = $cachekey;
$this->cacheinitparams = array($cms);
}
return $this->cacheinitparams;
}
protected function allow_add($course, \cm_info $cm = null,
\section_info $section = null) {
global $CFG;
// Check if completion is enabled for the course.
require_once($CFG->libdir . '/completionlib.php');
$info = new \completion_info($course);
if (!$info->is_enabled()) {
return false;
}
// Check if there's at least one other module with completion info.
$params = $this->get_javascript_init_params($course, $cm, $section);
return ((array)$params[0]) != false;
}
}

View File

@ -0,0 +1,41 @@
<?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/>.
/**
* Language strings.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['description'] = 'Require students to complete (or not complete) another activity.';
$string['error_selectcmid'] = 'You must select an activity for the completion condition.';
$string['label_cm'] = 'Activity or resource';
$string['label_completion'] = 'Required completion status';
$string['missing'] = '(Missing activity)';
$string['option_complete'] = 'must be marked complete';
$string['option_fail'] = 'must be complete with fail grade';
$string['option_incomplete'] = 'must not be marked complete';
$string['option_pass'] = 'must be complete with pass grade';
$string['pluginname'] = 'Restriction by activity completion';
$string['requires_incomplete'] = 'The activity <strong>{$a}</strong> is incomplete';
$string['requires_complete'] = 'The activity <strong>{$a}</strong> is marked complete';
$string['requires_complete_pass'] = 'The activity <strong>{$a}</strong> is complete and passed';
$string['requires_complete_fail'] = 'The activity <strong>{$a}</strong> is complete and failed';
$string['requires_not_complete_pass'] = 'The activity <strong>{$a}</strong> is not complete and passed';
$string['requires_not_complete_fail'] = 'The activity <strong>{$a}</strong> is not complete and failed';
$string['title'] = 'Activity completion';

View File

@ -0,0 +1,62 @@
@availability @availability_completion
Feature: availability_completion
In order to control student access to activities
As a teacher
I need to set completion conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
| Enable completion tracking | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Add a Page with a completion tickbox.
And I add a "Page" to section "1" and I fill the form with:
| Name | Page 1 |
| Description | Test |
| Page content | Test |
| Completion tracking | 1 |
# And another one that depends on it (hidden otherwise).
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | Page 2 |
| Description | Test |
| Page content | Test |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "Activity or resource" to "Page 1"
And I press "Save and return to course"
# Log back in as student.
When I log out
And I log in as "student1"
And I follow "Course 1"
# Page 2 should not appear yet.
Then I should not see "Page 2" in the "region-main" "region"
# Mark page 1 complete
When I click on ".togglecompletion input[type=image]" "css_element"
Then I should see "Page 2" in the "region-main" "region"

View File

@ -0,0 +1,412 @@
<?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 the completion condition.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_completion\condition;
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
/**
* Unit tests for the completion condition.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_completion_condition_testcase extends advanced_testcase {
/**
* Load required classes.
*/
public function setUp() {
// Load the mock info class so that it can be used.
global $CFG;
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using condition as part of tree.
*/
public function test_in_tree() {
global $USER, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
// Create course with completion turned on and a Page.
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
$course = $generator->create_course(array('enablecompletion' => 1));
$page = $generator->get_plugin_generator('mod_page')->create_instance(
array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->get_cm($page->cmid);
$info = new \core_availability\mock_info($course, $USER->id);
$structure = (object)array('op' => '|', 'show' => true, 'c' => array(
(object)array('type' => 'completion', 'cm' => (int)$cm->id,
'e' => COMPLETION_COMPLETE)));
$tree = new \core_availability\tree($structure);
// Initial check (user has not completed activity).
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertFalse($result->is_available());
// Mark activity complete.
$completion = new completion_info($course);
$completion->update_state($cm, COMPLETION_COMPLETE);
// Now it's true!
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertTrue($result->is_available());
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// No parameters.
$structure = new stdClass();
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->cm', $e->getMessage());
}
// Invalid $cm.
$structure->cm = 'hello';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->cm', $e->getMessage());
}
// Missing $e.
$structure->cm = 42;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->e', $e->getMessage());
}
// Invalid $e.
$structure->e = 99;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->e', $e->getMessage());
}
// Successful construct & display with all different expected values.
$structure->e = COMPLETION_COMPLETE;
$cond = new condition($structure);
$this->assertEquals('{completion:cm42 COMPLETE}', (string)$cond);
$structure->e = COMPLETION_COMPLETE_PASS;
$cond = new condition($structure);
$this->assertEquals('{completion:cm42 COMPLETE_PASS}', (string)$cond);
$structure->e = COMPLETION_COMPLETE_FAIL;
$cond = new condition($structure);
$this->assertEquals('{completion:cm42 COMPLETE_FAIL}', (string)$cond);
$structure->e = COMPLETION_INCOMPLETE;
$cond = new condition($structure);
$this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('cm' => 42, 'e' => COMPLETION_COMPLETE);
$cond = new condition($structure);
$structure->type = 'completion';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the is_available and get_description functions.
*/
public function test_usage() {
global $CFG, $DB;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$this->resetAfterTest();
// Create course with completion turned on.
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
$course = $generator->create_course(array('enablecompletion' => 1));
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$this->setUser($user);
// Create a Page with manual completion for basic checks.
$page = $generator->get_plugin_generator('mod_page')->create_instance(
array('course' => $course->id, 'name' => 'Page!',
'completion' => COMPLETION_TRACKING_MANUAL));
// Create an assignment - we need to have something that can be graded
// so as to test the PASS/FAIL states. Set it up to be completed based
// on its grade item.
$assignrow = $this->getDataGenerator()->create_module('assign', array(
'course' => $course->id, 'name' => 'Assign!',
'completion' => COMPLETION_TRACKING_AUTOMATIC));
$DB->set_field('course_modules', 'completiongradeitemnumber', 0,
array('id' => $assignrow->cmid));
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
// Get basic details.
$modinfo = get_fast_modinfo($course);
$pagecm = $modinfo->get_cm($page->cmid);
$assigncm = $assign->get_course_module();
$info = new \core_availability\mock_info($course, $user->id);
// COMPLETE state (false), positive and NOT.
$cond = new condition((object)array(
'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Page!.*is marked complete~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// INCOMPLETE state (true).
$cond = new condition((object)array(
'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Page!.*is marked complete~', $information);
// Mark page complete.
$completion = new completion_info($course);
$completion->update_state($pagecm, COMPLETION_COMPLETE);
// COMPLETE state (true).
$cond = new condition((object)array(
'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Page!.*is incomplete~', $information);
// INCOMPLETE state (false).
$cond = new condition((object)array(
'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Page!.*is incomplete~', $information);
$this->assertTrue($cond->is_available(true, $info,
true, $user->id));
// We are going to need the grade item so that we can get pass/fails.
$gradeitem = $assign->get_grade_item();
grade_object::set_properties($gradeitem, array('gradepass' => 50.0));
$gradeitem->update();
// With no grade, it should return true for INCOMPLETE and false for
// the other three.
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Check $information for COMPLETE_PASS and _FAIL as we haven't yet.
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and passed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and failed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Change the grade to be complete and failed.
self::set_grade($assignrow, $user->id, 40);
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and passed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is not complete and failed~', $information);
// Now change it to pass.
self::set_grade($assignrow, $user->id, 60);
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is not complete and passed~', $information);
$cond = new condition((object)array(
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~Assign!.*is complete and failed~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Simulate deletion of an activity by using an invalid cmid. These
// conditions always fail, regardless of NOT flag or INCOMPLETE.
$cond = new condition((object)array(
'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $course);
$this->assertRegExp('~(Missing activity).*is marked complete~', $information);
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$cond = new condition((object)array(
'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
}
/**
* Tests completion_value_used static function.
*/
public function test_completion_value_used() {
global $CFG, $DB;
$this->resetAfterTest();
// Create course with completion turned on and some sections.
$CFG->enablecompletion = true;
$CFG->enableavailability = true;
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('numsections' => 1, 'enablecompletion' => 1),
array('createsections' => true));
availability_completion\condition::wipe_static_cache();
// Create three pages with manual completion.
$page1 = $generator->get_plugin_generator('mod_page')->create_instance(
array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
$page2 = $generator->get_plugin_generator('mod_page')->create_instance(
array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
$page3 = $generator->get_plugin_generator('mod_page')->create_instance(
array('course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
// Set up page3 to depend on page1, and section1 to depend on page2.
$DB->set_field('course_modules', 'availability',
'{"op":"|","show":true,"c":[' .
'{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}',
array('id' => $page3->cmid));
$DB->set_field('course_sections', 'availability',
'{"op":"|","show":true,"c":[' .
'{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}',
array('course' => $course->id, 'section' => 1));
// Now check: nothing depends on page3 but something does on the others.
$this->assertTrue(availability_completion\condition::completion_value_used(
$course, $page1->cmid));
$this->assertTrue(availability_completion\condition::completion_value_used(
$course, $page2->cmid));
$this->assertFalse(availability_completion\condition::completion_value_used(
$course, $page3->cmid));
}
/**
* Updates the grade of a user in the given assign module instance.
*
* @param stdClass $assignrow Assignment row from database
* @param int $userid User id
* @param float $grade Grade
*/
protected static function set_grade($assignrow, $userid, $grade) {
$grades = array();
$grades[$userid] = (object)array(
'rawgrade' => $grade, 'userid' => $userid);
$assignrow->cmidnumber = null;
assign_grade_item_update($assignrow, $grades);
}
/**
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id() {
$cond = new condition((object)array(
'cm' => 123, 'e' => COMPLETION_COMPLETE));
$this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
$this->assertFalse($cond->update_dependency_id('course_modules', 12, 34));
$this->assertTrue($cond->update_dependency_id('course_modules', 123, 456));
$after = $cond->save();
$this->assertEquals(456, $after->cm);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_completion
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014022800;
$plugin->requires = 2014040401;
$plugin->component = 'availability_completion';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_completion-form",
"builds": {
"moodle-availability_completion-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,77 @@
/**
* JavaScript for form editing completion conditions.
*
* @module moodle-availability_completion-form
*/
M.availability_completion = M.availability_completion || {};
/**
* @class M.availability_completion.form
* @extends M.core_availability.plugin
*/
M.availability_completion.form = Y.Object(M.core_availability.plugin);
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} cms Array of objects containing cmid => name
*/
M.availability_completion.form.initInner = function(cms) {
this.cms = cms;
};
M.availability_completion.form.getNode = function(json) {
// Create HTML structure.
var strings = M.str.availability_completion;
var html = strings.title + ' <span class="availability-group"><label>' +
'<span class="accesshide">' + strings.label_cm + ' </span>' +
'<select name="cm" title="' + strings.label_cm + '">' +
'<option value="0">' + M.str.moodle.choosedots + '</option>';
for (var i = 0; i < this.cms.length; i++) {
var cm = this.cms[i];
// String has already been escaped using format_string.
html += '<option value="' + cm.id + '">' + cm.name + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + strings.label_completion +
' </span><select name="e" title="' + strings.label_completion + '">' +
'<option value="1">' + strings.option_complete + '</option>' +
'<option value="0">' + strings.option_incomplete + '</option>' +
'<option value="2">' + strings.option_pass + '</option>' +
'<option value="3">' + strings.option_fail + '</option>' +
'</select></label></span>';
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial values.
if (json.cm !== undefined &&
node.one('select[name=cm] > option[value=' + json.cm + ']')) {
node.one('select[name=cm]').set('value', json.cm);
}
if (json.e !== undefined) {
node.one('select[name=e]').set('value', json.e);
}
// Add event handlers (first time only).
if (!M.availability_completion.form.addedEvents) {
M.availability_completion.form.addedEvents = true;
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
// Whichever dropdown changed, just update the form.
M.core_availability.form.update();
}, '.availability_completion select');
}
return node;
};
M.availability_completion.form.fillValue = function(value, node) {
value.cm = parseInt(node.one('select[name=cm]').get('value'), 10);
value.e = parseInt(node.one('select[name=e]').get('value'), 10);
};
M.availability_completion.form.fillErrors = function(errors, node) {
var cmid = parseInt(node.one('select[name=cm]').get('value'), 10);
if (cmid === 0) {
errors.push('availability_completion:error_selectcmid');
}
};

View File

@ -0,0 +1,10 @@
{
"moodle-availability_completion-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}

View File

@ -0,0 +1,51 @@
<?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/>.
/**
* Handles AJAX processing (convert date to timestamp using current calendar).
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require(__DIR__ . '/../../../config.php');
// Action verb.
$action = required_param('action', PARAM_ALPHA);
switch ($action) {
case 'totime':
// Converts from time fields to timestamp using current user's calendar and time zone.
echo \availability_date\frontend::get_time_from_fields(
required_param('year', PARAM_INT),
required_param('month', PARAM_INT),
required_param('day', PARAM_INT),
required_param('hour', PARAM_INT),
required_param('minute', PARAM_INT));
exit;
case 'fromtime' :
// Converts from timestamp to time fields.
echo json_encode(\availability_date\frontend::get_fields_from_time(
required_param('time', PARAM_INT)));
exit;
}
// Unexpected actions throw coding_exception (this error should not occur
// unless there is a code bug).
throw new coding_exception('Unexpected action parameter');

View File

@ -0,0 +1,213 @@
<?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/>.
/**
* Date condition.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_date;
defined('MOODLE_INTERNAL') || die();
/**
* Date condition.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var string Availabile only from specified date. */
const DIRECTION_FROM = '>=';
/** @var string Availabile only until specified date. */
const DIRECTION_UNTIL = '<';
/** @var string One of the DIRECTION_xx constants. */
private $direction;
/** @var int Time (Unix epoch seconds) for condition. */
private $time;
/** @var int Forced current time (for unit tests) or 0 for normal. */
private static $forcecurrenttime = 0;
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get direction.
if (isset($structure->d) && in_array($structure->d,
array(self::DIRECTION_FROM, self::DIRECTION_UNTIL))) {
$this->direction = $structure->d;
} else {
throw new \coding_exception('Missing or invalid ->d for date condition');
}
// Get time.
if (isset($structure->t) && is_int($structure->t)) {
$this->time = $structure->t;
} else {
throw new \coding_exception('Missing or invalid ->t for date condition');
}
}
public function save() {
return (object)array('type' => 'date',
'd' => $this->direction, 't' => $this->time);
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
return $this->is_available_for_all($not);
}
public function is_available_for_all($not = false) {
// Check condition.
$now = self::get_time();
switch ($this->direction) {
case self::DIRECTION_FROM:
$allow = $now >= $this->time;
break;
case self::DIRECTION_UNTIL:
$allow = $now < $this->time;
break;
default:
throw new \coding_exception('Unexpected direction');
}
if ($not) {
$allow = !$allow;
}
return $allow;
}
/**
* Obtains the actual direction of checking based on the $not value.
*
* @param bool $not True if condition is negated
* @return string Direction constant
* @throws \coding_exception
*/
protected function get_logical_direction($not) {
switch ($this->direction) {
case self::DIRECTION_FROM:
return $not ? self::DIRECTION_UNTIL : self::DIRECTION_FROM;
case self::DIRECTION_UNTIL:
return $not ? self::DIRECTION_FROM : self::DIRECTION_UNTIL;
default:
throw new \coding_exception('Unexpected direction');
}
}
public function get_description($full, $not, \core_availability\info $info) {
return $this->get_either_description($not, false);
}
public function get_standalone_description(
$full, $not, \core_availability\info $info) {
return $this->get_either_description($not, true);
}
/**
* Shows the description using the different lang strings for the standalone
* version or the full one.
*
* @param bool $not True if NOT is in force
* @param bool $standalone True to use standalone lang strings
*/
protected function get_either_description($not, $standalone) {
$direction = $this->get_logical_direction($not);
$midnight = self::is_midnight($this->time);
$midnighttag = $midnight ? '_date' : '';
$satag = $standalone ? 'short_' : 'full_';
switch ($direction) {
case self::DIRECTION_FROM:
return get_string($satag . 'from' . $midnighttag, 'availability_date',
self::show_time($this->time, $midnight, false));
case self::DIRECTION_UNTIL:
return get_string($satag . 'until' . $midnighttag, 'availability_date',
self::show_time($this->time, $midnight, true));
}
}
protected function get_debug_string() {
return $this->direction . ' ' . gmdate('Y-m-d H:i:s', $this->time);
}
/**
* Gets time. This function is implemented here rather than calling time()
* so that it can be overridden in unit tests. (Would really be nice if
* Moodle had a generic way of doing that, but it doesn't.)
*
* @return int Current time (seconds since epoch)
*/
protected static function get_time() {
if (self::$forcecurrenttime) {
return self::$forcecurrenttime;
} else {
return time();
}
}
/**
* Forces the current time for unit tests.
*
* @param int $forcetime Time to return from the get_time function
*/
public static function set_current_time_for_test($forcetime = 0) {
self::$forcecurrenttime = $forcetime;
}
/**
* Shows a time either as a date or a full date and time, according to
* user's timezone.
*
* @param int $time Time
* @param bool $dateonly If true, uses date only
* @param bool $until If true, and if using date only, shows previous date
* @return string Date
*/
protected function show_time($time, $dateonly, $until = false) {
// For 'until' dates that are at midnight, e.g. midnight 5 March, it
// is better to word the text as 'until end 4 March'.
$daybefore = false;
if ($until && $dateonly) {
$daybefore = true;
$time = strtotime('-1 day', $time);
}
return userdate($time,
get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
}
/**
* Checks whether a given time refers exactly to midnight (in current user
* timezone).
*
* @param int $time Time
* @return bool True if time refers to midnight, false if it's some other
* time or if it is set to zero
*/
protected static function is_midnight($time) {
return $time && usergetmidnight($time) == $time;
}
}

View File

@ -0,0 +1,174 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_date;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
/**
* The date selector popup is not currently supported because the date
* selector is in a messy state (about to be replaced with a new YUI3
* implementation) and MDL-44814 was rejected. I have left the code in
* place, but disabled. When the date selector situation is finalised,
* then this constant should be removed (either applying MDL-44814 if old
* selector is still in use, or modifying the JavaScript code to support the
* new date selector if it has landed).
*
* @var bool
*/
const DATE_SELECTOR_SUPPORTED = false;
protected function get_javascript_strings() {
return array('ajaxerror', 'direction_before', 'direction_from', 'direction_until',
'direction_label');
}
/**
* Given field values, obtains the corresponding timestamp.
*
* @param int $year Year
* @param int $month Month
* @param int $day Day
* @param int $hour Hour
* @param int $minute Minute
* @return int Timestamp
*/
public static function get_time_from_fields($year, $month, $day, $hour, $minute) {
$calendartype = \core_calendar\type_factory::get_calendar_instance();
$gregoriandate = $calendartype->convert_to_gregorian(
$year, $month, $day, $hour, $minute);
return make_timestamp($gregoriandate['year'], $gregoriandate['month'],
$gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute'], 0);
}
/**
* Given a timestamp, obtains corresponding field values.
*
* @param int $time Timestamp
* @return stdClass Object with fields for year, month, day, hour, minute
*/
public static function get_fields_from_time($time) {
$calendartype = \core_calendar\type_factory::get_calendar_instance();
$wrongfields = $calendartype->timestamp_to_date_array($time);
return array('day' => $wrongfields['mday'],
'month' => $wrongfields['mon'], 'year' => $wrongfields['year'],
'hour' => $wrongfields['hours'], 'minute' => $wrongfields['minutes']);
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
global $CFG, $OUTPUT;
require_once($CFG->libdir . '/formslib.php');
// Support internationalised calendars.
$calendartype = \core_calendar\type_factory::get_calendar_instance();
// Get current date, but set time to 00:00 (to make it easier to
// specify whole days) and change name of mday field to match below.
$wrongfields = $calendartype->timestamp_to_date_array(time());
$current = array('day' => $wrongfields['mday'],
'month' => $wrongfields['mon'], 'year' => $wrongfields['year'],
'hour' => 0, 'minute' => 0);
// Time part is handled the same everywhere.
$hours = array();
for ($i = 0; $i <= 23; $i++) {
$hours[$i] = sprintf("%02d", $i);
}
$minutes = array();
for ($i = 0; $i < 60; $i += 5) {
$minutes[$i] = sprintf("%02d", $i);
}
// List date fields.
$fields = $calendartype->get_date_order(
$calendartype->get_min_year(), $calendartype->get_max_year());
// Add time fields - in RTL mode these are switched.
$fields['split'] = '/';
if (right_to_left()) {
$fields['minute'] = $minutes;
$fields['colon'] = ':';
$fields['hour'] = $hours;
} else {
$fields['hour'] = $hours;
$fields['colon'] = ':';
$fields['minute'] = $minutes;
}
// Output all date fields.
$html = '<span class="availability-group">';
foreach ($fields as $field => $options) {
if ($options === '/') {
$html = rtrim($html);
// In Gregorian calendar mode only, we support a date selector popup, reusing
// code from form to ensure consistency.
if ($calendartype->get_name() === 'gregorian' && self::DATE_SELECTOR_SUPPORTED) {
$image = $OUTPUT->pix_icon('i/calendar', get_string('calendar', 'calendar'), 'moodle');
$html .= ' ' . \html_writer::link('#', $image, array('name' => 'x[calendar]'));
form_init_date_js();
}
$html .= '</span> <span class="availability-group">';
continue;
}
if ($options === ':') {
$html .= ': ';
continue;
}
$html .= \html_writer::start_tag('label');
$html .= \html_writer::span(get_string($field) . ' ', 'accesshide');
// NOTE: The fields need to have these weird names in order that they
// match the standard Moodle form control, otherwise the date selector
// won't find them.
$html .= \html_writer::start_tag('select', array('name' => 'x[' . $field . ']'));
foreach ($options as $key => $value) {
$params = array('value' => $key);
if ($current[$field] == $key) {
$params['selected'] = 'selected';
}
$html .= \html_writer::tag('option', s($value), $params);
}
$html .= \html_writer::end_tag('select');
$html .= \html_writer::end_tag('label');
$html .= ' ';
}
$html = rtrim($html) . '</span>';
// Also get the time that corresponds to this default date.
$time = self::get_time_from_fields($current['year'], $current['month'],
$current['day'], $current['hour'], $current['minute']);
return array($html, $time);
}
}

View File

@ -0,0 +1,40 @@
<?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/>.
/**
* Language strings.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['ajaxerror'] = 'Error contacting server to convert times';
$string['direction_before'] = 'Date';
$string['direction_from'] = 'from';
$string['direction_label'] = 'Direction';
$string['direction_until'] = 'until';
$string['description'] = 'Prevent access until (or from) a specified date and time.';
$string['full_from'] = 'It is after <strong>{$a}</strong>';
$string['full_from_date'] = 'It is on or after <strong>{$a}</strong>';
$string['full_until'] = 'It is before <strong>{$a}</strong>';
$string['full_until_date'] = 'It is before end of <strong>{$a}</strong>';
$string['pluginname'] = 'Restriction by date';
$string['short_from'] = 'Available from <strong>{$a}</strong>';
$string['short_from_date'] = 'Available from <strong>{$a}</strong>';
$string['short_until'] = 'Available until <strong>{$a}</strong>';
$string['short_until_date'] = 'Available until end of <strong>{$a}</strong>';
$string['title'] = 'Date';

View File

@ -0,0 +1,65 @@
@availability @availability_date
Feature: availability_date
In order to control student access to activities
As a teacher
I need to set date conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Add a Page with a date condition that does match (from the past).
And I add a "Page" to section "1"
And I set the following fields to these values:
| Name | Page 1 |
| Description | Test |
| Page content | Test |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "year" to "2013"
And I press "Save and return to course"
# Add a Page with a date condition that doesn't match (until the past).
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | Page 2 |
| Description | Test |
| Page content | Test |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "Direction" to "until"
And I set the field "year" to "2013"
And I press "Save and return to course"
# Log back in as student.
When I log out
And I log in as "student1"
And I follow "Course 1"
# Page 1 should appear, but page 2 does not.
Then I should see "Page 1" in the "region-main" "region"
And I should not see "Page 2" in the "region-main" "region"

View File

@ -0,0 +1,236 @@
<?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 the date condition.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_date\condition;
/**
* Unit tests for the date condition.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_date_condition_testcase extends advanced_testcase {
/**
* Load required classes.
*/
public function setUp() {
// Load the mock info class so that it can be used.
global $CFG;
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using date condition as part of tree.
*/
public function test_in_tree() {
global $SITE, $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Set server timezone for test. (Important as otherwise the timezone
// could be anything - this is modified by other unit tests, too.)
date_default_timezone_set('UTC');
// SEt user to GMT+5.
$USER->timezone = 5;
// Construct tree with date condition.
$time = strtotime('2014-02-18 14:20:00 GMT');
$structure = (object)array('op' => '|', 'show' => true, 'c' => array(
(object)array('type' => 'date', 'd' => '>=', 't' => $time)));
$tree = new \core_availability\tree($structure);
$info = new \core_availability\mock_info();
// Check if available (when not available).
condition::set_current_time_for_test($time - 1);
$information = '';
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertFalse($result->is_available());
$information = $tree->get_result_information($info, $result);
// Note: PM is normally upper-case, but an issue with PHP on Mac means
// that on that platform, it is reported lower-case.
$this->assertRegExp('~from.*18 February 2014, 7:20 (PM|pm)~', $information);
// Check if available (when available).
condition::set_current_time_for_test($time);
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertTrue($result->is_available());
$information = $tree->get_result_information($info, $result);
$this->assertEquals('', $information);
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// No parameters.
$structure = (object)array();
try {
$date = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->d', $e->getMessage());
}
// Invalid ->d.
$structure->d = 'woo hah!!';
try {
$date = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->d', $e->getMessage());
}
// Missing ->t.
$structure->d = '>=';
try {
$date = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->t', $e->getMessage());
}
// Invalid ->t.
$structure->t = 'got you all in check';
try {
$date = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->t', $e->getMessage());
}
// Valid conditions of both types.
$structure = (object)array('d' => '>=', 't' => strtotime('2014-02-18 14:43:17 GMT'));
$date = new condition($structure);
$this->assertEquals('{date:>= 2014-02-18 14:43:17}', (string)$date);
$structure->d = '<';
$date = new condition($structure);
$this->assertEquals('{date:< 2014-02-18 14:43:17}', (string)$date);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('d' => '>=', 't' => 12345);
$cond = new condition($structure);
$structure->type = 'date';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the is_available() and is_available_to_all() functions.
*/
public function test_is_available() {
global $SITE, $USER;
$time = strtotime('2014-02-18 14:50:10 GMT');
$info = new \core_availability\mock_info();
// Test with >=.
$date = new condition((object)array('d' => '>=', 't' => $time));
condition::set_current_time_for_test($time - 1);
$this->assertFalse($date->is_available(false, $info, true, $USER->id));
condition::set_current_time_for_test($time);
$this->assertTrue($date->is_available(false, $info, true, $USER->id));
// Test with <.
$date = new condition((object)array('d' => '<', 't' => $time));
condition::set_current_time_for_test($time);
$this->assertFalse($date->is_available(false, $info, true, $USER->id));
condition::set_current_time_for_test($time - 1);
$this->assertTrue($date->is_available(false, $info, true, $USER->id));
// Repeat this test with is_available_to_all() - it should be the same.
$date = new condition((object)array('d' => '<', 't' => $time));
condition::set_current_time_for_test($time);
$this->assertFalse($date->is_available_for_all(false));
condition::set_current_time_for_test($time - 1);
$this->assertTrue($date->is_available_for_all(false));
}
/**
* Tests the get_description and get_standalone_description functions.
*/
public function test_get_description() {
global $SITE;
$modinfo = get_fast_modinfo($SITE);
$info = new \core_availability\mock_info();
$time = strtotime('2014-02-18 14:55:01 GMT');
// Test with >=.
$date = new condition((object)array('d' => '>=', 't' => $time));
$information = $date->get_description(true, false, $info);
$this->assertRegExp('~after.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_description(true, true, $info);
$this->assertRegExp('~before.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_standalone_description(true, false, $info);
$this->assertRegExp('~from.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_standalone_description(true, true, $info);
$this->assertRegExp('~until.*18 February 2014, 2:55 (PM|pm)~', $information);
// Test with <.
$date = new condition((object)array('d' => '<', 't' => $time));
$information = $date->get_description(true, false, $info);
$this->assertRegExp('~before.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_description(true, true, $info);
$this->assertRegExp('~after.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_standalone_description(true, false, $info);
$this->assertRegExp('~until.*18 February 2014, 2:55 (PM|pm)~', $information);
$information = $date->get_standalone_description(true, true, $info);
$this->assertRegExp('~from.*18 February 2014, 2:55 (PM|pm)~', $information);
// Test special case for dates that are midnight.
$date = new condition((object)array('d' => '>=',
't' => strtotime('2014-03-05 00:00 GMT')));
$information = $date->get_description(true, false, $info);
$this->assertRegExp('~on or after.*5 March 2014([^0-9]*)$~', $information);
$information = $date->get_description(true, true, $info);
$this->assertRegExp('~before.*end of.*4 March 2014([^0-9]*)$~', $information);
$information = $date->get_standalone_description(true, false, $info);
$this->assertRegExp('~from.*5 March 2014([^0-9]*)$~', $information);
$information = $date->get_standalone_description(true, true, $info);
$this->assertRegExp('~until end of.*4 March 2014([^0-9]*)$~', $information);
// In the 'until' case for midnight, it shows the previous day. (I.e.
// if the date is 5 March 00:00, then we show it as available until 4
// March, implying 'the end of'.)
$date = new condition((object)array('d' => '<',
't' => strtotime('2014-03-05 00:00 GMT')));
$information = $date->get_description(true, false, $info);
$this->assertRegExp('~before end of.*4 March 2014([^0-9]*)$~', $information);
$information = $date->get_description(true, true, $info);
$this->assertRegExp('~on or after.*5 March 2014([^0-9]*)$~', $information);
$information = $date->get_standalone_description(true, false, $info);
$this->assertRegExp('~until end of.*4 March 2014([^0-9]*)$~', $information);
$information = $date->get_standalone_description(true, true, $info);
$this->assertRegExp('~from.*5 March 2014([^0-9]*)$~', $information);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_date
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014021800;
$plugin->requires = 2014040401;
$plugin->component = 'availability_date';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_date-form",
"builds": {
"moodle-availability_date-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,141 @@
/**
* JavaScript for form editing date conditions.
*
* @module moodle-availability_date-form
*/
M.availability_date = M.availability_date || {};
/**
* @class M.availability_date.form
* @extends M.core_availability.plugin
*/
M.availability_date.form = Y.Object(M.core_availability.plugin);
/**
* Initialises this plugin.
*
* Because the date fields are complex depending on Moodle calendar settings,
* we create the HTML for these fields in PHP and pass it to this method.
*
* @method initInner
* @param {String} html HTML to use for date fields
* @param {Number} defaultTime Time value that corresponds to initial fields
*/
M.availability_date.form.initInner = function(html, defaultTime) {
this.html = html;
this.defaultTime = defaultTime;
};
M.availability_date.form.getNode = function(json) {
var strings = M.str.availability_date;
var html = strings.direction_before + ' <span class="availability-group">' +
'<label><span class="accesshide">' + strings.direction_label + ' </span>' +
'<select name="direction">' +
'<option value="&gt;=">' + strings.direction_from + '</option>' +
'<option value="&lt;">' + strings.direction_until + '</option>' +
'</select></label></span> ' + this.html;
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial value if non-default.
if (json.t !== undefined) {
node.setData('time', json.t);
// Disable everything.
node.all('select:not([name=direction])').each(function(select) {
select.set('disabled', true);
});
var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=fromtime' +
'&time=' + json.t;
Y.io(url, { on : {
success : function(id, response) {
var fields = Y.JSON.parse(response.responseText);
for (var field in fields) {
var select = node.one('select[name=x\\[' + field + '\\]]');
select.set('value', fields[field]);
select.set('disabled', false);
}
},
failure : function() {
window.alert(M.str.availability_date.ajaxerror);
}
}});
} else {
// Set default time that corresponds to the HTML selectors.
node.setData('time', this.defaultTime);
}
if (json.d !== undefined) {
node.one('select[name=direction]').set('value', json.d);
}
// Add event handlers (first time only).
if (!M.availability_date.form.addedEvents) {
M.availability_date.form.addedEvents = true;
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
// For the direction, just update the form fields.
M.core_availability.form.update();
}, '.availability_date select[name=direction]');
root.delegate('change', function() {
// Update time using AJAX call from root node.
M.availability_date.form.updateTime(this.ancestor('span.availability_date'));
}, '.availability_date select:not([name=direction])');
}
if (node.one('a[href=#]')) {
// Add the date selector magic.
M.form.dateselector.init_single_date_selector(node);
// This special handler detects when the date selector changes the year.
var yearSelect = node.one('select[name=x\\[year\\]]');
var oldSet = yearSelect.set;
yearSelect.set = function(name, value) {
oldSet.call(yearSelect, name, value);
if (name === 'selectedIndex') {
// Do this after timeout or the other fields haven't been set yet.
setTimeout(function() {
M.availability_date.form.updateTime(node);
}, 0);
}
};
}
return node;
};
/**
* Updates time from AJAX. Whenever the field values change, we recompute the
* actual time via an AJAX request to Moodle.
*
* This will set the 'time' data on the node and then update the form, once it
* gets an AJAX response.
*
* @method updateTime
* @param {Y.Node} component Node for plugin controls
*/
M.availability_date.form.updateTime = function(node) {
// After a change to the date/time we need to recompute the
// actual time using AJAX because it depends on the user's
// time zone and calendar options.
var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=totime' +
'&year=' + node.one('select[name=x\\[year\\]]').get('value') +
'&month=' + node.one('select[name=x\\[month\\]]').get('value') +
'&day=' + node.one('select[name=x\\[day\\]]').get('value') +
'&hour=' + node.one('select[name=x\\[hour\\]]').get('value') +
'&minute=' + node.one('select[name=x\\[minute\\]]').get('value');
Y.io(url, { on : {
success : function(id, response) {
node.setData('time', response.responseText);
M.core_availability.form.update();
},
failure : function() {
window.alert(M.str.availability_date.ajaxerror);
}
}});
};
M.availability_date.form.fillValue = function(value, node) {
value.d = node.one('select[name=direction]').get('value');
value.t = parseInt(node.getData('time'), 10);
};

View File

@ -0,0 +1,11 @@
{
"moodle-availability_date-form": {
"requires": [
"base",
"node",
"event",
"io",
"moodle-core_availability-form"
]
}
}

View File

@ -0,0 +1,57 @@
<?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/>.
/**
* Observer handling events.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_grade;
defined('MOODLE_INTERNAL') || die();
/**
* Callbacks handling grade changes (to clear cache).
*
* This ought to use the hooks system, but it doesn't exist - calls are
* hard-coded. (The new event system is not suitable for this type of use.)
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class callbacks {
/**
* A user grade has been updated in gradebook.
*
* @param int $userid User ID
*/
public static function grade_changed($userid) {
\cache::make('availability_grade', 'scores')->delete($userid);
}
/**
* A grade item has been updated in gradebook.
*
* @param int $courseid Course id
*/
public static function grade_item_changed($courseid) {
\cache::make('availability_grade', 'items')->delete($courseid);
}
}

View File

@ -0,0 +1,274 @@
<?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/>.
/**
* Condition on grades of current user.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_grade;
defined('MOODLE_INTERNAL') || die();
/**
* Condition on grades of current user.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var int Grade item id */
private $gradeitemid;
/** @var float|null Min grade (must be >= this) or null if none */
private $min;
/** @var float|null Max grade (must be < this) or null if none */
private $max;
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get grade item id.
if (isset($structure->id) && is_int($structure->id)) {
$this->gradeitemid = $structure->id;
} else {
throw new \coding_exception('Missing or invalid ->id for grade condition');
}
// Get min and max.
if (!property_exists($structure, 'min')) {
$this->min = null;
} else if (is_float($structure->min) || is_int($structure->min)) {
$this->min = $structure->min;
} else {
throw new \coding_exception('Missing or invalid ->min for grade condition');
}
if (!property_exists($structure, 'max')) {
$this->max = null;
} else if (is_float($structure->max) || is_int($structure->max)) {
$this->max = $structure->max;
} else {
throw new \coding_exception('Missing or invalid ->max for grade condition');
}
}
public function save() {
$result = (object)array('type' => 'grade', 'id' => $this->gradeitemid);
if (!is_null($this->min)) {
$result->min = $this->min;
}
if (!is_null($this->max)) {
$result->max = $this->max;
}
return $result;
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$course = $info->get_course();
$score = $this->get_cached_grade_score($this->gradeitemid, $course->id, $grabthelot, $userid);
$allow = $score !== false &&
(is_null($this->min) || $score >= $this->min) &&
(is_null($this->max) || $score < $this->max);
if ($not) {
$allow = !$allow;
}
return $allow;
}
public function get_description($full, $not, \core_availability\info $info) {
$course = $info->get_course();
// String depends on type of requirement. We are coy about
// the actual numbers, in case grades aren't released to
// students.
if (is_null($this->min) && is_null($this->max)) {
$string = 'any';
} else if (is_null($this->max)) {
$string = 'min';
} else if (is_null($this->min)) {
$string = 'max';
} else {
$string = 'range';
}
if ($not) {
// The specific strings don't make as much sense with 'not'.
if ($string === 'any') {
$string = 'notany';
} else {
$string = 'notgeneral';
}
}
$name = self::get_cached_grade_name($course->id, $this->gradeitemid);
return get_string('requires_' . $string, 'availability_grade', $name);
}
protected function get_debug_string() {
$out = '#' . $this->gradeitemid;
if (!is_null($this->min)) {
$out .= ' >= ' . sprintf('%.5f', $this->min);
}
if (!is_null($this->max)) {
if (!is_null($this->min)) {
$out .= ',';
}
$out .= ' < ' . sprintf('%.5f', $this->max);
}
return $out;
}
/**
* Obtains the name of a grade item, also checking that it exists. Uses a
* cache. The name returned is suitable for display.
*
* @param int $courseid Course id
* @param int $gradeitemid Grade item id
* @return string Grade name or empty string if no grade with that id
*/
private static function get_cached_grade_name($courseid, $gradeitemid) {
global $DB, $CFG;
require_once($CFG->libdir . '/gradelib.php');
// Get all grade item names from cache, or using db query.
$cache = \cache::make('availability_grade', 'items');
if (($cacheditems = $cache->get($courseid)) === false) {
// We cache the whole items table not the name; the format_string
// call for the name might depend on current user (e.g. multilang)
// and this is a shared cache.
$cacheditems = $DB->get_records('grade_items', array('courseid' => $courseid));
$cache->set($courseid, $cacheditems);
}
// Return name from cached item or a lang string.
if (!array_key_exists($gradeitemid, $cacheditems)) {
return get_string('missing', 'availability_grade');
}
$gradeitemobj = $cacheditems[$gradeitemid];
$item = new \grade_item;
\grade_object::set_properties($item, $gradeitemobj);
return $item->get_name();
}
/**
* Obtains a grade score. Note that this score should not be displayed to
* the user, because gradebook rules might prohibit that. It may be a
* non-final score subject to adjustment later.
*
* @param int $gradeitemid Grade item ID we're interested in
* @param int $courseid Course id
* @param bool $grabthelot If true, grabs all scores for current user on
* this course, so that later ones come from cache
* @param int $userid Set if requesting grade for a different user (does
* not use cache)
* @return float Grade score as a percentage in range 0-100 (e.g. 100.0
* or 37.21), or false if user does not have a grade yet
*/
protected static function get_cached_grade_score($gradeitemid, $courseid,
$grabthelot=false, $userid=0) {
global $USER, $DB;
if (!$userid) {
$userid = $USER->id;
}
$cache = \cache::make('availability_grade', 'scores');
if (($cachedgrades = $cache->get($userid)) === false) {
$cachedgrades = array();
}
if (!array_key_exists($gradeitemid, $cachedgrades)) {
if ($grabthelot) {
// Get all grades for the current course.
$rs = $DB->get_recordset_sql('
SELECT
gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
FROM
{grade_items} gi
LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
WHERE
gi.courseid = ?', array($userid, $courseid));
foreach ($rs as $record) {
if (is_null($record->finalgrade)) {
// No grade = false.
$cachedgrades[$record->id] = false;
} else {
// Otherwise convert grade to percentage.
$cachedgrades[$record->id] =
(($record->finalgrade - $record->rawgrademin) * 100) /
($record->rawgrademax - $record->rawgrademin);
}
}
$rs->close();
// And if it's still not set, well it doesn't exist (eg
// maybe the user set it as a condition, then deleted the
// grade item) so we call it false.
if (!array_key_exists($gradeitemid, $cachedgrades)) {
$cachedgrades[$gradeitemid] = false;
}
} else {
// Just get current grade.
$record = $DB->get_record('grade_grades', array(
'userid' => $userid, 'itemid' => $gradeitemid));
if ($record && !is_null($record->finalgrade)) {
$score = (($record->finalgrade - $record->rawgrademin) * 100) /
($record->rawgrademax - $record->rawgrademin);
} else {
// Treat the case where row exists but is null, same as
// case where row doesn't exist.
$score = false;
}
$cachedgrades[$gradeitemid] = $score;
}
$cache->set($userid, $cachedgrades);
}
return $cachedgrades[$gradeitemid];
}
public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
global $DB;
$rec = \restore_dbops::get_backup_ids_record($restoreid, 'grade_item', $this->gradeitemid);
if (!$rec || !$rec->newitemid) {
// If we are on the same course (e.g. duplicate) then we can just
// use the existing one.
if ($DB->record_exists('grade_items',
array('id' => $this->gradeitemid, 'courseid' => $courseid))) {
return false;
}
// Otherwise it's a warning.
$this->gradeitemid = 0;
$logger->process('Restored item (' . $name .
') has availability condition on grade that was not restored',
\backup::LOG_WARNING);
} else {
$this->gradeitemid = (int)$rec->newitemid;
}
return true;
}
public function update_dependency_id($table, $oldid, $newid) {
if ($table === 'grade_items' && (int)$this->gradeitemid === (int)$oldid) {
$this->gradeitemid = $newid;
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,69 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_grade;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
protected function get_javascript_strings() {
return array('option_min', 'option_max', 'label_min', 'label_max');
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
global $DB, $CFG;
require_once($CFG->libdir . '/gradelib.php');
// Get grades as basic associative array.
$gradeoptions = array();
$items = \grade_item::fetch_all(array('courseid' => $course->id));
// For some reason the fetch_all things return null if none.
$items = $items ? $items : array();
foreach ($items as $id => $item) {
// Do not include grades for current item.
if ($cm && $cm->instance == $item->iteminstance
&& $cm->modname == $item->itemmodule
&& $item->itemtype == 'mod') {
continue;
}
$gradeoptions[$id] = $item->get_name();
}
\core_collator::asort($gradeoptions);
// Change to JS array format and return.
$jsarray = array();
foreach ($gradeoptions as $id => $name) {
$jsarray[] = (object)array('id' => $id, 'name' => $name);
}
return array($jsarray);
}
}

View File

@ -0,0 +1,40 @@
<?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/>.
/**
* Cache definitions.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$definitions = array(
// Used to cache user grades for conditional availability purposes.
'scores' => array(
'mode' => cache_store::MODE_APPLICATION,
'staticacceleration' => true,
'staticaccelerationsize' => 2, // Should not be required for more than one user at a time.
'ttl' => 3600,
),
// Used to cache course grade items for conditional availability purposes.
'items' => array(
'mode' => cache_store::MODE_APPLICATION,
'staticacceleration' => true,
'staticaccelerationsize' => 2, // Should not be required for more than one course at a time.
'ttl' => 3600,
),
);

View File

@ -0,0 +1,43 @@
<?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/>.
/**
* Language strings.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['cachedef_items'] = 'Grade items cached for evaluating conditional availability';
$string['cachedef_scores'] = 'User grades cached for evaluating conditional availability';
$string['description'] = 'Require students to achieve a specified grade.';
$string['error_backwardrange'] = 'When specifying a grade range, the minimum must be lower than the maximum.';
$string['error_invalidnumber'] = 'Grade ranges must be specified with valid percentages.';
$string['error_selectgradeid'] = 'You must select a grade item for the grade condition.';
$string['label_min'] = 'Minimum grade percentage (inclusive)';
$string['label_max'] = 'Maximum grade percentage (exclusive)';
$string['option_min'] = 'must be &#x2265;';
$string['option_max'] = 'must be <';
$string['pluginname'] = 'Restriction by grades';
$string['requires_any'] = 'You have a grade in <strong>{$a}</strong>';
$string['requires_max'] = 'You get an appropriate score in <strong>{$a}</strong>';
$string['requires_min'] = 'You achieve a required score in <strong>{$a}</strong>';
$string['requires_notany'] = 'You do not have a grade in <strong>{$a}</strong>';
$string['requires_notgeneral'] = 'You do not get certain scores in <strong>{$a}</strong>';
$string['requires_range'] = 'You get a particular score in <strong>{$a}</strong>';
$string['missing'] = '(missing activity)';
$string['title'] = 'Grade';

View File

@ -0,0 +1,3 @@
#fitem_id_availabilityconditionsjson .availability_grade input[type=text] {
width: 3em;
}

View File

@ -0,0 +1,119 @@
@availability @availability_grade
Feature: availability_grade
In order to control student access to activities
As a teacher
I need to set date conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username | email |
| teacher1 | t@example.org |
| student1 | s@example.org |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Add an assignment.
And I add a "Assignment" to section "1" and I fill the form with:
| Assignment name | A1 |
| Description | x |
| Online text | 1 |
# Add a Page with a grade condition for 'any grade'.
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | P2 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "Grade" to "A1"
And I press "Save and return to course"
# Add a Page with a grade condition for 50%.
And I add a "Page" to section "3"
And I set the following fields to these values:
| Name | P3 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "Grade" to "A1"
And I click on "min" "checkbox" in the ".availability-item" "css_element"
And I set the field "Minimum grade percentage (inclusive)" to "50"
And I press "Save and return to course"
# Add a Page with a grade condition for 10%.
And I add a "Page" to section "4"
And I set the following fields to these values:
| Name | P4 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Grade" "button" in the "Add restriction..." "dialogue"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the field "Grade" to "A1"
And I click on "min" "checkbox" in the ".availability-item" "css_element"
And I set the field "Minimum grade percentage (inclusive)" to "10"
And I press "Save and return to course"
# Log in as student without a grade yet.
When I log out
And I log in as "student1"
And I follow "Course 1"
# Do the assignment.
And I follow "A1"
And I click on "Add submission" "button"
And I set the field "Online text" to "Q"
And I click on "Save changes" "button"
And I follow "C1"
# None of the pages should appear (check assignment though).
Then I should not see "P2" in the "region-main" "region"
And I should not see "P3" in the "region-main" "region"
And I should not see "P4" in the "region-main" "region"
And I should see "A1" in the "region-main" "region"
# Log back in as teacher.
When I log out
And I log in as "teacher1"
And I follow "Course 1"
# Give the assignment 40%.
And I follow "A1"
And I follow "View/grade all submissions"
# Pick the grade link in the row that has s@example.org in it.
And I click on "//a[contains(@href, 'action=grade') and ancestor::tr/td[normalize-space(.) = 's@example.org']]/img" "xpath_element"
And I set the field "Grade out of 100" to "40"
And I click on "Save changes" "button"
# Log back in as student.
And I log out
And I log in as "student1"
And I follow "Course 1"
# Check pages are visible.
Then I should see "P2" in the "region-main" "region"
And I should see "P4" in the "region-main" "region"
And I should not see "P3" in the "region-main" "region"

View File

@ -0,0 +1,247 @@
<?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 the grade condition.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_grade\condition;
/**
* Unit tests for the grade condition.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_grade_condition_testcase extends advanced_testcase {
/**
* Tests constructing and using grade condition.
*/
public function test_usage() {
global $USER, $CFG;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$this->resetAfterTest();
$CFG->enableavailability = true;
// Make a test course and user.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
// Make assign module.
$assignrow = $this->getDataGenerator()->create_module('assign', array(
'course' => $course->id, 'name' => 'Test!'));
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->get_cm($assignrow->cmid);
$info = new \core_availability\info_module($cm);
// Get the actual grade item.
$item = $assign->get_grade_item();
// Construct tree with grade condition (any grade, specified item).
$structure = (object)array('type' => 'grade', 'id' => (int)$item->id);
$cond = new condition($structure);
// Check if available (not available).
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~have a grade.*Test!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Add grade and check available.
self::set_grade($assignrow, $user->id, 37.2);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not have a grade.*Test!~', $information);
// Construct directly and test remaining conditions; first, min grade (fail).
self::set_grade($assignrow, $user->id, 29.99999);
$structure->min = 30.0;
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~achieve a required score.*Test!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Min grade (success).
self::set_grade($assignrow, $user->id, 30);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not get certain scores.*Test!~', $information);
// Max grade (fail).
unset($structure->min);
$structure->max = 30.0;
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~get an appropriate score.*Test!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Max grade (success).
self::set_grade($assignrow, $user->id, 29.99999);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not get certain scores.*Test!~', $information);
// Max and min (fail).
$structure->min = 30.0;
$structure->max = 34.12345;
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~get a particular score.*Test!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Still fail (other end).
self::set_grade($assignrow, $user->id, 34.12345);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
// Success (top end).
self::set_grade($assignrow, $user->id, 34.12344);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not get certain scores.*Test!~', $information);
// Success (bottom end).
self::set_grade($assignrow, $user->id, 30.0);
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not get certain scores.*Test!~', $information);
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// No parameters.
$structure = new stdClass();
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->id', $e->getMessage());
}
// Invalid id (not int).
$structure->id = 'bourne';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->id', $e->getMessage());
}
// Invalid min (not number).
$structure->id = 42;
$structure->min = 'ute';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->min', $e->getMessage());
}
// Invalid max (not number).
$structure->min = 3.89;
$structure->max = '9000';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->max', $e->getMessage());
}
// All valid.
$structure->max = 4.0;
$cond = new condition($structure);
$this->assertEquals('{grade:#42 >= 3.89000, < 4.00000}', (string)$cond);
// No max.
unset($structure->max);
$cond = new condition($structure);
$this->assertEquals('{grade:#42 >= 3.89000}', (string)$cond);
// No min.
unset($structure->min);
$structure->max = 32.768;
$cond = new condition($structure);
$this->assertEquals('{grade:#42 < 32.76800}', (string)$cond);
// No nothing (only requires that grade exists).
unset($structure->max);
$cond = new condition($structure);
$this->assertEquals('{grade:#42}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('id' => 19);
$cond = new condition($structure);
$structure->type = 'grade';
$this->assertEquals($structure, $cond->save());
$structure = (object)array('id' => 19, 'min' => 4.12345, 'max' => 90);
$cond = new condition($structure);
$structure->type = 'grade';
$this->assertEquals($structure, $cond->save());
}
/**
* Updates the grade of a user in the given assign module instance.
*
* @param stdClass $assignrow Assignment row from database
* @param int $userid User id
* @param float $grade Grade
*/
protected static function set_grade($assignrow, $userid, $grade) {
$grades = array();
$grades[$userid] = (object)array(
'rawgrade' => $grade, 'userid' => $userid);
$assignrow->cmidnumber = null;
assign_grade_item_update($assignrow, $grades);
}
/**
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id() {
$cond = new condition((object)array('id' => 123));
$this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
$this->assertFalse($cond->update_dependency_id('grade_items', 12, 34));
$this->assertTrue($cond->update_dependency_id('grade_items', 123, 456));
$after = $cond->save();
$this->assertEquals(456, $after->id);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_grade
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014022001;
$plugin->requires = 2014040401;
$plugin->component = 'availability_grade';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_grade-form",
"builds": {
"moodle-availability_grade-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,159 @@
/**
* JavaScript for form editing grade conditions.
*
* @module moodle-availability_grade-form
*/
M.availability_grade = M.availability_grade || {};
/**
* @class M.availability_grade.form
* @extends M.core_availability.plugin
*/
M.availability_grade.form = Y.Object(M.core_availability.plugin);
/**
* Grade items available for selection.
*
* @property grades
* @type Array
*/
M.availability_grade.form.grades = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} grades Array of objects containing gradeid => name
*/
M.availability_grade.form.initInner = function(grades) {
this.grades = grades;
this.nodesSoFar = 0;
};
M.availability_grade.form.getNode = function(json) {
// Increment number used for unique ids.
this.nodesSoFar++;
// Create HTML structure.
var strings = M.str.availability_grade;
var html = '<label>' + strings.title + ' <span class="availability-group">' +
'<select name="id"><option value="0">' + M.str.moodle.choosedots + '</option>';
for (var i = 0; i < this.grades.length; i++) {
var grade = this.grades[i];
// String has already been escaped using format_string.
html += '<option value="' + grade.id + '">' + grade.name + '</option>';
}
html += '</select></span></label> <span class="availability-group">' +
'<label><input type="checkbox" name="min"/>' + strings.option_min +
'</label> <label><span class="accesshide">' + strings.label_min +
'</span><input type="text" name="minval" title="' +
strings.label_min + '"/></label>%</span>' +
'<span class="availability-group">' +
'<label><input type="checkbox" name="max"/>' + strings.option_max +
'</label> <label><span class="accesshide">' + strings.label_max +
'</span><input type="text" name="maxval" title="' +
strings.label_max + '"/></label>%</span>';
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial values.
if (json.id !== undefined &&
node.one('select[name=id] > option[value=' + json.id + ']')) {
node.one('select[name=id]').set('value', json.id);
}
if (json.min !== undefined) {
node.one('input[name=min]').set('checked', true);
node.one('input[name=minval]').set('value', json.min);
}
if (json.max !== undefined) {
node.one('input[name=max]').set('checked', true);
node.one('input[name=maxval]').set('value', json.max);
}
// Disables/enables text input fields depending on checkbox.
var updateCheckbox = function(check, focus) {
var input = check.ancestor('label').next('label').one('input');
var checked = check.get('checked');
input.set('disabled', !checked);
if (focus && checked) {
input.focus();
}
return checked;
};
node.all('input[type=checkbox]').each(updateCheckbox);
// Add event handlers (first time only).
if (!M.availability_grade.form.addedEvents) {
M.availability_grade.form.addedEvents = true;
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
// For the grade item, just update the form fields.
M.core_availability.form.update();
}, '.availability_grade select[name=id]');
root.delegate('click', function() {
updateCheckbox(this, true);
}, '.availability_grade input[type=checkbox]');
root.delegate('valuechange', function() {
// For grade values, just update the form fields.
M.core_availability.form.update();
}, '.availability_grade input[type=text]');
}
return node;
};
M.availability_grade.form.fillValue = function(value, node) {
value.id = parseInt(node.one('select[name=id]').get('value'), 10);
if (node.one('input[name=min]').get('checked')) {
value.min = this.getValue('minval', node);
}
if (node.one('input[name=max]').get('checked')) {
value.max = this.getValue('maxval', node);
}
};
/**
* Gets the numeric value of an input field. Supports decimal points (using
* dot or comma).
*
* @method getValue
* @return {Number|String} Value of field as number or string if not valid
*/
M.availability_grade.form.getValue = function(field, node) {
// Get field value.
var value = node.one('input[name=' + field + ']').get('value');
// If it is not a valid positive number, return false.
if (!(/^[0-9]+([.,][0-9]+)?$/.test(value))) {
return value;
}
// Replace comma with dot and parse as floating-point.
var result = parseFloat(value.replace(',', '.'));
if (result < 0 || result > 100) {
return value;
} else {
return result;
}
};
M.availability_grade.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check grade item id.
if (value.id === 0) {
errors.push('availability_grade:error_selectgradeid');
}
// Check numeric values.
if ((value.min !== undefined && typeof(value.min) === 'string') ||
(value.max !== undefined && typeof(value.max) === 'string')) {
errors.push('availability_grade:error_invalidnumber');
} else if (value.min !== undefined && value.max !== undefined &&
value.min >= value.max) {
errors.push('availability_grade:error_backwardrange');
}
};

View File

@ -0,0 +1,10 @@
{
"moodle-availability_grade-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}

View File

@ -0,0 +1,215 @@
<?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/>.
/**
* Condition main class.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_group;
defined('MOODLE_INTERNAL') || die();
/**
* Condition main class.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var array Array from group id => name */
protected static $groupnames = array();
/** @var int ID of group that this condition requires, or 0 = any group */
protected $groupid;
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get group id.
if (!property_exists($structure, 'id')) {
$this->groupid = 0;
} else if (is_int($structure->id)) {
$this->groupid = $structure->id;
} else {
throw new \coding_exception('Invalid ->id for group condition');
}
}
public function save() {
$result = (object)array('type' => 'group');
if ($this->groupid) {
$result->id = $this->groupid;
}
return $result;
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$course = $info->get_course();
$context = \context_course::instance($course->id);
$allow = true;
if (!has_capability('moodle/site:accessallgroups', $context, $userid)) {
// Get all groups the user belongs to.
$groups = $info->get_modinfo()->get_groups();
if ($this->groupid) {
$allow = in_array($this->groupid, $groups);
} else {
// No specific group. Allow if they belong to any group at all.
$allow = $groups ? true : false;
}
// The NOT condition applies before accessallgroups (i.e. if you
// set something to be available to those NOT in group X,
// people with accessallgroups can still access it even if
// they are in group X).
if ($not) {
$allow = !$allow;
}
}
return $allow;
}
public function get_description($full, $not, \core_availability\info $info) {
global $DB;
if ($this->groupid) {
// Need to get the name for the group. Unfortunately this requires
// a database query. To save queries, get all groups for course at
// once in a static cache.
$course = $info->get_course();
if (!array_key_exists($this->groupid, self::$groupnames)) {
$coursegroups = $DB->get_records(
'groups', array('courseid' => $course->id), '', 'id, name');
foreach ($coursegroups as $rec) {
self::$groupnames[$rec->id] = $rec->name;
}
}
// If it still doesn't exist, it must have been misplaced.
if (!array_key_exists($this->groupid, self::$groupnames)) {
$name = get_string('missing', 'availability_group');
} else {
$context = \context_course::instance($course->id);
$name = format_string(self::$groupnames[$this->groupid], true,
array('context' => $context));
}
} else {
return get_string($not ? 'requires_notanygroup' : 'requires_anygroup',
'availability_group');
}
return get_string($not ? 'requires_notgroup' : 'requires_group',
'availability_group', $name);
}
protected function get_debug_string() {
return $this->groupid ? '#' . $this->groupid : 'any';
}
public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
global $DB;
if (!$this->groupid) {
return false;
}
$rec = \restore_dbops::get_backup_ids_record($restoreid, 'group', $this->groupid);
if (!$rec || !$rec->newitemid) {
// If we are on the same course (e.g. duplicate) then we can just
// use the existing one.
if ($DB->record_exists('groups',
array('id' => $this->groupid, 'courseid' => $courseid))) {
return false;
}
// Otherwise it's a warning.
$this->groupid = -1;
$logger->process('Restored item (' . $name .
') has availability condition on group that was not restored',
\backup::LOG_WARNING);
} else {
$this->groupid = (int)$rec->newitemid;
}
return true;
}
public function update_dependency_id($table, $oldid, $newid) {
if ($table === 'groups' && (int)$this->groupid === (int)$oldid) {
$this->groupid = $newid;
return true;
} else {
return false;
}
}
/**
* Wipes the static cache used to store grouping names.
*/
public static function wipe_static_cache() {
self::$groupnames = array();
}
public function is_applied_to_user_lists() {
// Group conditions are assumed to be 'permanent', so they affect the
// display of user lists for activities.
return true;
}
public function filter_user_list(array $users, $not, \core_availability\info $info,
\core_availability\capability_checker $checker) {
global $CFG, $DB;
require_once($CFG->libdir . '/grouplib.php');
$course = $info->get_course();
// List users for this course who match the condition.
if ($this->groupid) {
$groupusers = groups_get_members($this->groupid, 'u.id', 'u.id ASC');
} else {
$groupusers = $DB->get_records_sql("
SELECT DISTINCT gm.userid
FROM {groups} g
JOIN {groups_members} gm ON gm.groupid = g.id
WHERE g.courseid = ?", array($course->id));
}
// List users who have access all groups.
$aagusers = $checker->get_users_by_capability('moodle/site:accessallgroups');
// Filter the user list.
$result = array();
foreach ($users as $id => $user) {
// Always include users with access all groups.
if (array_key_exists($id, $aagusers)) {
$result[$id] = $user;
continue;
}
// Other users are included or not based on group membership.
$allow = array_key_exists($id, $groupusers);
if ($not) {
$allow = !$allow;
}
if ($allow) {
$result[$id] = $user;
}
}
return $result;
}
}

View File

@ -0,0 +1,91 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_group;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
/** @var array Array of group info for course */
protected $allgroups;
/** @var int Course id that $allgroups is for */
protected $allgroupscourseid;
protected function get_javascript_strings() {
return array('anygroup');
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
// Get all groups for course.
$groups = $this->get_all_groups($course->id);
// Change to JS array format and return.
$jsarray = array();
$context = \context_course::instance($course->id);
foreach ($groups as $rec) {
$jsarray[] = (object)array('id' => $rec->id, 'name' =>
format_string($rec->name, true, array('context' => $context)));
}
return array($jsarray);
}
/**
* Gets all groups for the given course.
*
* @param int $courseid Course id
* @return array Array of all the group objects
*/
protected function get_all_groups($courseid) {
global $CFG;
require_once($CFG->libdir . '/grouplib.php');
if ($courseid != $this->allgroupscourseid) {
$this->allgroups = groups_get_all_groups($courseid, 0, 0, 'g.id, g.name');
$this->allgroupscourseid = $courseid;
}
return $this->allgroups;
}
protected function allow_add($course, \cm_info $cm = null,
\section_info $section = null) {
global $CFG;
// Group option can be used on sections, and on modules but only
// if groupmembersonly is turned off. (To avoid confusion.)
if (!is_null($cm) && $CFG->enablegroupmembersonly) {
return false;
}
// Only show this option if there are some groups.
return count($this->get_all_groups($course->id)) > 0;
}
}

View File

@ -0,0 +1,34 @@
<?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/>.
/**
* Language strings.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['anygroup'] = '(Any group)';
$string['description'] = 'Allow only students who belong to a specified group, or all groups.';
$string['missing'] = '(Missing group)';
$string['pluginname'] = 'Restriction by group';
$string['error_selectgroup'] = 'You must select a group.';
$string['requires_anygroup'] = 'You belong to any group';
$string['requires_group'] = 'You belong to <strong>{$a}</strong>';
$string['requires_notanygroup'] = 'You do not belong to any group';
$string['requires_notgroup'] = 'You do not belong to <strong>{$a}</strong>';
$string['title'] = 'Group';

View File

@ -0,0 +1,109 @@
@availability @availability_group
Feature: availability_group
In order to control student access to activities
As a teacher
I need to set group conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Start to add a Page. If there aren't any groups, there's no Group option.
And I add a "Page" to section "1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
Then "Group" "button" should not exist in the "Add restriction..." "dialogue"
And I click on "Cancel" "button" in the "Add restriction..." "dialogue"
# Back to course page but add groups.
Given the following "groups" exist:
| name | course | idnumber |
| G1 | C1 | GI1 |
| G2 | C1 | GI2 |
# This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb
# is not clickable, so we'll go via the home page instead.
And I am on homepage
And I follow "Course 1"
And I add a "Page" to section "1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
Then "Group" "button" should exist in the "Add restriction..." "dialogue"
# Page P1 any group.
Given I click on "Group" "button"
And I set the field "Group" to "(Any group)"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the following fields to these values:
| Name | P1 |
| Description | x |
| Page content | x |
And I click on "Save and return to course" "button"
# Page P2 with group G1.
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | P2 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Group" "button"
And I set the field "Group" to "G1"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Page P3 with group G2
And I add a "Page" to section "3"
And I set the following fields to these values:
| Name | P3 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Group" "button"
And I set the field "Group" to "G2"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Log back in as student.
When I log out
And I log in as "student1"
And I follow "Course 1"
# No pages should appear yet.
Then I should not see "P1" in the "region-main" "region"
And I should not see "P2" in the "region-main" "region"
And I should not see "P3" in the "region-main" "region"
# Add to groups and log out/in again.
Given the following "group members" exist:
| user | group |
| student1 | GI1 |
And I log out
And I log in as "student1"
And I follow "Course 1"
# P1 (any groups) and P2 should show but not P3.
Then I should see "P1" in the "region-main" "region"
And I should see "P2" in the "region-main" "region"
And I should not see "P3" in the "region-main" "region"

View File

@ -0,0 +1,225 @@
<?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 the condition.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_group\condition;
/**
* Unit tests for the condition.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_group_condition_testcase extends advanced_testcase {
/**
* Load required classes.
*/
public function setUp() {
// Load the mock info class so that it can be used.
global $CFG;
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using condition.
*/
public function test_usage() {
global $CFG, $USER;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and user.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$info = new \core_availability\mock_info($course, $user->id);
// Make 2 test groups, one in a grouping and one not.
$grouping = $generator->create_grouping(array('courseid' => $course->id));
$group1 = $generator->create_group(array('courseid' => $course->id, 'name' => 'G1!'));
groups_assign_grouping($grouping->id, $group1->id);
$group2 = $generator->create_group(array('courseid' => $course->id, 'name' => 'G2!'));
// Do test (not in group).
$cond = new condition((object)array('id' => (int)$group1->id));
// Check if available (when not available).
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~You belong to.*G1!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Add user to groups and refresh cache.
groups_add_member($group1, $user);
groups_add_member($group2, $user);
get_fast_modinfo($course->id, 0, true);
// Recheck.
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not belong to.*G1!~', $information);
// Check group 2 works also.
$cond = new condition((object)array('id' => (int)$group2->id));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
// What about an 'any group' condition?
$cond = new condition((object)array());
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not belong to any~', $information);
// Admin user doesn't belong to a group, but they can access it
// either way (positive or NOT).
$this->setAdminUser();
$this->assertTrue($cond->is_available(false, $info, true, $USER->id));
$this->assertTrue($cond->is_available(true, $info, true, $USER->id));
// Group that doesn't exist uses 'missing' text.
$cond = new condition((object)array('id' => $group2->id + 1000));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~You belong to.*\(Missing group\)~', $information);
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// Invalid id (not int).
$structure = (object)array('id' => 'bourne');
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->id', $e->getMessage());
}
// Valid (with id).
$structure->id = 123;
$cond = new condition($structure);
$this->assertEquals('{group:#123}', (string)$cond);
// Valid (no id).
unset($structure->id);
$cond = new condition($structure);
$this->assertEquals('{group:any}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('id' => 123);
$cond = new condition($structure);
$structure->type = 'group';
$this->assertEquals($structure, $cond->save());
$structure = (object)array();
$cond = new condition($structure);
$structure->type = 'group';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id() {
$cond = new condition((object)array('id' => 123));
$this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
$this->assertFalse($cond->update_dependency_id('groups', 12, 34));
$this->assertTrue($cond->update_dependency_id('groups', 123, 456));
$after = $cond->save();
$this->assertEquals(456, $after->id);
}
/**
* Tests the filter_users (bulk checking) function.
*/
public function test_filter_users() {
global $DB;
$this->resetAfterTest();
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and some users.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
$teacher = $generator->create_user();
$generator->enrol_user($teacher->id, $course->id, $roleids['teacher']);
$allusers = array($teacher->id => $teacher);
$students = array();
for ($i = 0; $i < 3; $i++) {
$student = $generator->create_user();
$students[$i] = $student;
$generator->enrol_user($student->id, $course->id, $roleids['student']);
$allusers[$student->id] = $student;
}
$info = new \core_availability\mock_info($course);
// Make test groups.
$group1 = $generator->create_group(array('courseid' => $course->id));
$group2 = $generator->create_group(array('courseid' => $course->id));
// Assign students to groups as follows (teacher is not in a group):
// 0: no groups.
// 1: in group 1.
// 2: in group 2.
groups_add_member($group1, $students[1]);
groups_add_member($group2, $students[2]);
// Test 'any group' condition.
$checker = new \core_availability\capability_checker($info->get_context());
$cond = new condition((object)array());
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[1]->id, $students[2]->id), $result);
// Test NOT version (note that teacher can still access because AAG works
// both ways).
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[0]->id), $result);
// Test specific group.
$cond = new condition((object)array('id' => (int)$group1->id));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[1]->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[0]->id, $students[2]->id), $result);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_group
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014030400;
$plugin->requires = 2014040401;
$plugin->component = 'availability_group';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_group-form",
"builds": {
"moodle-availability_group-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,87 @@
/**
* JavaScript for form editing group conditions.
*
* @module moodle-availability_group-form
*/
M.availability_group = M.availability_group || {};
/**
* @class M.availability_group.form
* @extends M.core_availability.plugin
*/
M.availability_group.form = Y.Object(M.core_availability.plugin);
/**
* Groups available for selection (alphabetical order).
*
* @property groups
* @type Array
*/
M.availability_group.form.groups = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} groups Array of objects containing groupid => name
*/
M.availability_group.form.initInner = function(groups) {
this.groups = groups;
};
M.availability_group.form.getNode = function(json) {
// Create HTML structure.
var strings = M.str.availability_group;
var html = '<label>' + strings.title + ' <span class="availability-group">' +
'<select name="id">' +
'<option value="choose">' + M.str.moodle.choosedots + '</option>' +
'<option value="any">' + strings.anygroup + '</option>';
for (var i = 0; i < this.groups.length; i++) {
var group = this.groups[i];
// String has already been escaped using format_string.
html += '<option value="' + group.id + '">' + group.name + '</option>';
}
html += '</select></span></label>';
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial values (leave default 'choose' if creating afresh).
if (json.creating === undefined) {
if (json.id !== undefined &&
node.one('select[name=id] > option[value=' + json.id + ']')) {
node.one('select[name=id]').set('value', json.id);
} else if (json.id === undefined) {
node.one('select[name=id]').set('value', 'any');
}
}
// Add event handlers (first time only).
if (!M.availability_group.form.addedEvents) {
M.availability_group.form.addedEvents = true;
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
// Just update the form fields.
M.core_availability.form.update();
}, '.availability_group select');
}
return node;
};
M.availability_group.form.fillValue = function(value, node) {
var selected = node.one('select[name=id]').get('value');
if (selected === 'choose') {
value.id = 'choose';
} else if (selected !== 'any') {
value.id = parseInt(selected, 10);
}
};
M.availability_group.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check group item id.
if (value.id && value.id === 'choose') {
errors.push('availability_group:error_selectgroup');
}
};

View File

@ -0,0 +1,10 @@
{
"moodle-availability_group-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}

View File

@ -0,0 +1,242 @@
<?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/>.
/**
* Condition main class.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_grouping;
defined('MOODLE_INTERNAL') || die();
/**
* Condition main class.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var array Array from grouping id => name */
protected static $groupingnames = array();
/** @var int ID of grouping that this condition requires */
protected $groupingid = 0;
/** @var bool If true, indicates that activity $cm->grouping is used */
protected $activitygrouping = false;
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get grouping id.
if (isset($structure->id)) {
if (is_int($structure->id)) {
$this->groupingid = $structure->id;
} else {
throw new \coding_exception('Invalid ->id for grouping condition');
}
} else if (isset($structure->activity)) {
if (is_bool($structure->activity) && $structure->activity) {
$this->activitygrouping = true;
} else {
throw new \coding_exception('Invalid ->activity for grouping condition');
}
} else {
throw new \coding_exception('Missing ->id / ->activity for grouping condition');
}
}
public function save() {
$result = (object)array('type' => 'grouping');
if ($this->groupingid) {
$result->id = $this->groupingid;
} else {
$result->activity = true;
}
return $result;
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$context = \context_course::instance($info->get_course()->id);
$allow = true;
if (!has_capability('moodle/site:accessallgroups', $context, $userid)) {
// If the activity has 'group members only' and you don't have accessallgroups...
$groups = $info->get_modinfo()->get_groups($this->get_grouping_id($info));
if (!$groups) {
// ...and you don't belong to a group, then set it so you can't see/access it.
$allow = false;
}
// The NOT condition applies before accessallgroups (i.e. if you
// set something to be available to those NOT in grouping X,
// people with accessallgroups can still access it even if
// they are in grouping X).
if ($not) {
$allow = !$allow;
}
}
return $allow;
}
/**
* Gets the actual grouping id for the condition. This is either a specified
* id, or a special flag indicating that we use the one for the current cm.
*
* @param \core_availability\info $info Info about context cm
* @return int Grouping id
* @throws \coding_exception If it's set to use a cm but there isn't grouping
*/
protected function get_grouping_id(\core_availability\info $info) {
if ($this->activitygrouping) {
$groupingid = $info->get_course_module()->groupingid;
if (!$groupingid) {
throw new \coding_exception(
'Not supposed to be able to turn on activitygrouping when no grouping');
}
return $groupingid;
} else {
return $this->groupingid;
}
}
public function get_description($full, $not, \core_availability\info $info) {
global $DB;
$course = $info->get_course();
// Need to get the name for the grouping. Unfortunately this requires
// a database query. To save queries, get all groupings for course at
// once in a static cache.
$groupingid = $this->get_grouping_id($info);
if (!array_key_exists($groupingid, self::$groupingnames)) {
$coursegroupings = $DB->get_records(
'groupings', array('courseid' => $course->id), '', 'id, name');
foreach ($coursegroupings as $rec) {
self::$groupingnames[$rec->id] = $rec->name;
}
}
// If it still doesn't exist, it must have been misplaced.
if (!array_key_exists($groupingid, self::$groupingnames)) {
$name = get_string('missing', 'availability_grouping');
} else {
$context = \context_course::instance($course->id);
$name = format_string(self::$groupingnames[$groupingid], true,
array('context' => $context));
}
return get_string($not ? 'requires_notgrouping' : 'requires_grouping',
'availability_grouping', $name);
}
protected function get_debug_string() {
if ($this->activitygrouping) {
return 'CM';
} else {
return '#' . $this->groupingid;
}
}
public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
global $DB;
if (!$this->groupingid) {
// If using 'same as activity' option, no need to change it.
return false;
}
$rec = \restore_dbops::get_backup_ids_record($restoreid, 'grouping', $this->groupingid);
if (!$rec || !$rec->newitemid) {
// If we are on the same course (e.g. duplicate) then we can just
// use the existing one.
if ($DB->record_exists('groupings',
array('id' => $this->groupingid, 'courseid' => $courseid))) {
return false;
}
// Otherwise it's a warning.
$this->groupingid = -1;
$logger->process('Restored item (' . $name .
') has availability condition on grouping that was not restored',
\backup::LOG_WARNING);
} else {
$this->groupingid = (int)$rec->newitemid;
}
return true;
}
public function update_dependency_id($table, $oldid, $newid) {
if ($table === 'groupings' && (int)$this->groupingid === (int)$oldid) {
$this->groupingid = $newid;
return true;
} else {
return false;
}
}
/**
* Wipes the static cache used to store grouping names.
*/
public static function wipe_static_cache() {
self::$groupingnames = array();
}
public function is_applied_to_user_lists() {
// Grouping conditions are assumed to be 'permanent', so they affect the
// display of user lists for activities.
return true;
}
public function filter_user_list(array $users, $not, \core_availability\info $info,
\core_availability\capability_checker $checker) {
global $CFG, $DB;
// List users for this course who match the condition.
$groupingusers = $DB->get_records_sql("
SELECT DISTINCT gm.userid
FROM {groupings_groups} gg
JOIN {groups_members} gm ON gm.groupid = gg.groupid
WHERE gg.groupingid = ?",
array($this->get_grouping_id($info)));
// List users who have access all groups.
$aagusers = $checker->get_users_by_capability('moodle/site:accessallgroups');
// Filter the user list.
$result = array();
foreach ($users as $id => $user) {
// Always include users with access all groups.
if (array_key_exists($id, $aagusers)) {
$result[$id] = $user;
continue;
}
// Other users are included or not based on grouping membership.
$allow = array_key_exists($id, $groupingusers);
if ($not) {
$allow = !$allow;
}
if ($allow) {
$result[$id] = $user;
}
}
return $result;
}
}

View File

@ -0,0 +1,89 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_grouping;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
/** @var array Array of grouping info for course */
protected $allgroupings;
/** @var int Course id that $allgroupings is for */
protected $allgroupingscourseid;
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
// Get all groups for course.
$groupings = $this->get_all_groupings($course->id);
// Change to JS array format and return.
$jsarray = array();
$context = \context_course::instance($course->id);
foreach ($groupings as $rec) {
$jsarray[] = (object)array('id' => $rec->id, 'name' =>
format_string($rec->name, true, array('context' => $context)));
}
return array($jsarray);
}
/**
* Gets all the groupings on the course.
*
* @param int $courseid Course id
* @return array Array of grouping objects
*/
protected function get_all_groupings($courseid) {
global $DB;
if ($courseid != $this->allgroupingscourseid) {
$this->allgroupings = $DB->get_records('groupings',
array('courseid' => $courseid), 'id, name');
$this->allgroupingscourseid = $courseid;
}
return $this->allgroupings;
}
protected function allow_add($course, \cm_info $cm = null,
\section_info $section = null) {
global $CFG, $DB;
// Grouping option can be used on sections, and on modules but only
// if groupmembersonly is turned off. (To avoid confusion.)
if (!is_null($cm) && $CFG->enablegroupmembersonly) {
return false;
}
// Check if groupings are in use for the course. (Unlike the 'group'
// condition there is no case where you might want to set up the
// condition before you set a grouping - there is no 'any grouping'
// option.)
return count($this->get_all_groupings($course->id)) > 0;
}
}

View File

@ -0,0 +1,31 @@
<?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/>.
/**
* Language strings.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['description'] = 'Allow only students who belong to a group within a specified grouping.';
$string['error_selectgrouping'] = 'You must select a grouping.';
$string['missing'] = '(Missing grouping)';
$string['pluginname'] = 'Restriction by grouping';
$string['requires_grouping'] = 'You belong to a group in <strong>{$a}</strong>';
$string['requires_notgrouping'] = 'You do not belong to a group in <strong>{$a}</strong>';
$string['title'] = 'Grouping';

View File

@ -0,0 +1,100 @@
@availability @availability_grouping
Feature: availability_grouping
In order to control student access to activities
As a teacher
I need to set grouping conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| G1 | C1 | GI1 |
And the following "group members" exist:
| user | group |
| student1 | GI1 |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Start to add a Page. If there aren't any groupings, there's no Grouping option.
And I add a "Page" to section "1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
Then "Grouping" "button" should not exist in the "Add restriction..." "dialogue"
And I click on "Cancel" "button" in the "Add restriction..." "dialogue"
# Back to course page but add groups.
# This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb
# is not clickable, so we'll go via the home page instead.
And I am on homepage
And I follow "Course 1"
And the following "groupings" exist:
| name | course | idnumber |
| GX1 | C1 | GXI1 |
| GX2 | C1 | GXI2 |
And I add a "Page" to section "1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
Then "Grouping" "button" should exist in the "Add restriction..." "dialogue"
# Page P1 grouping GX1.
Given I click on "Grouping" "button"
And I set the field "Grouping" to "GX1"
And I click on ".availability-item .availability-eye img" "css_element"
And I set the following fields to these values:
| Name | P1 |
| Description | x |
| Page content | x |
And I click on "Save and return to course" "button"
# Page P2 with grouping GX2.
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | P2 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Grouping" "button"
And I set the field "Grouping" to "GX2"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Log back in as student.
When I log out
And I log in as "student1"
And I follow "Course 1"
# No pages should appear yet.
Then I should not see "P1" in the "region-main" "region"
And I should not see "P2" in the "region-main" "region"
# Add group to grouping and log out/in again.
And I log out
And the following "grouping groups" exist:
| grouping | group |
| GXI1 | GI1 |
And I log in as "student1"
And I follow "Course 1"
# P1 should show but not B2.
Then I should see "P1" in the "region-main" "region"
And I should not see "P2" in the "region-main" "region"

View File

@ -0,0 +1,273 @@
<?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 the condition.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_grouping\condition;
/**
* Unit tests for the condition.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_grouping_condition_testcase extends advanced_testcase {
/**
* Load required classes.
*/
public function setUp() {
// Load the mock info class so that it can be used.
global $CFG;
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using condition.
*/
public function test_usage() {
global $CFG, $USER;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and user.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$user = $generator->create_user();
$generator->enrol_user($user->id, $course->id);
$info = new \core_availability\mock_info($course, $user->id);
// Make a test grouping and group.
$grouping = $generator->create_grouping(array('courseid' => $course->id,
'name' => 'Grouping!'));
$group = $generator->create_group(array('courseid' => $course->id));
groups_assign_grouping($grouping->id, $group->id);
// Do test (not in grouping).
$structure = (object)array('type' => 'grouping', 'id' => (int)$grouping->id);
$cond = new condition($structure);
// Check if available (when not available).
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~belong to a group in.*Grouping!~', $information);
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
// Add user to grouping and refresh cache.
groups_add_member($group, $user);
get_fast_modinfo($course->id, 0, true);
// Recheck.
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
$information = $cond->get_description(false, true, $info);
$this->assertRegExp('~do not belong to a group in.*Grouping!~', $information);
// Admin user doesn't belong to the grouping, but they can access it
// either way (positive or NOT) because of accessallgroups.
$this->setAdminUser();
$infoadmin = new \core_availability\mock_info($course, $USER->id);
$this->assertTrue($cond->is_available(false, $infoadmin, true, $USER->id));
$this->assertTrue($cond->is_available(true, $infoadmin, true, $USER->id));
// Grouping that doesn't exist uses 'missing' text.
$cond = new condition((object)array('id' => $grouping->id + 1000));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~belong to a group in.*(Missing grouping)~', $information);
// We need an actual cm object to test the 'grouping from cm' option.
$pagegen = $generator->get_plugin_generator('mod_page');
$page = $pagegen->create_instance(array('course' => $course->id,
'groupingid' => $grouping->id, 'availability' =>
'{"op":"|","show":true,"c":[{"type":"grouping","activity":true}]}'));
rebuild_course_cache($course->id, true);
// Check if available using the 'from course-module' grouping option.
$modinfo = get_fast_modinfo($course, $user->id);
$cm = $modinfo->get_cm($page->cmid);
$info = new \core_availability\info_module($cm);
$information = '';
$this->assertTrue($info->is_available($information, false, $user->id));
// Remove user from grouping again and recheck.
groups_remove_member($group, $user);
get_fast_modinfo($course->id, 0, true);
$this->assertFalse($info->is_available($information, false, $user->id));
$this->assertRegExp('~belong to a group in.*Grouping!~', $information);
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// No parameters.
$structure = new stdClass();
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing ->id / ->activity', $e->getMessage());
}
// Invalid id (not int).
$structure->id = 'bourne';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->id', $e->getMessage());
}
// Invalid activity option (not bool).
unset($structure->id);
$structure->activity = 42;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->activity', $e->getMessage());
}
// Invalid activity option (false).
$structure->activity = false;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->activity', $e->getMessage());
}
// Valid with id.
$structure->id = 123;
$cond = new condition($structure);
$this->assertEquals('{grouping:#123}', (string)$cond);
// Valid with activity.
unset($structure->id);
$structure->activity = true;
$cond = new condition($structure);
$this->assertEquals('{grouping:CM}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('id' => 123);
$cond = new condition($structure);
$structure->type = 'grouping';
$this->assertEquals($structure, $cond->save());
$structure = (object)array('activity' => true);
$cond = new condition($structure);
$structure->type = 'grouping';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id() {
$cond = new condition((object)array('id' => 123));
$this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
$this->assertFalse($cond->update_dependency_id('groupings', 12, 34));
$this->assertTrue($cond->update_dependency_id('groupings', 123, 456));
$after = $cond->save();
$this->assertEquals(456, $after->id);
$cond = new condition((object)array('activity' => true));
$this->assertFalse($cond->update_dependency_id('frogs', 123, 456));
}
/**
* Tests the filter_users (bulk checking) function.
*/
public function test_filter_users() {
global $DB, $CFG;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and some users.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
$teacher = $generator->create_user();
$generator->enrol_user($teacher->id, $course->id, $roleids['teacher']);
$allusers = array($teacher->id => $teacher);
$students = array();
for ($i = 0; $i < 3; $i++) {
$student = $generator->create_user();
$students[$i] = $student;
$generator->enrol_user($student->id, $course->id, $roleids['student']);
$allusers[$student->id] = $student;
}
$info = new \core_availability\mock_info($course);
$checker = new \core_availability\capability_checker($info->get_context());
// Make test groups.
$group1 = $generator->create_group(array('courseid' => $course->id));
$group2 = $generator->create_group(array('courseid' => $course->id));
$grouping1 = $generator->create_grouping(array('courseid' => $course->id));
$grouping2 = $generator->create_grouping(array('courseid' => $course->id));
groups_assign_grouping($grouping1->id, $group1->id);
groups_assign_grouping($grouping2->id, $group2->id);
// Make page in grouping 2.
$pagegen = $generator->get_plugin_generator('mod_page');
$page = $pagegen->create_instance(array('course' => $course->id,
'groupingid' => $grouping2->id, 'availability' =>
'{"op":"|","show":true,"c":[{"type":"grouping","activity":true}]}'));
// Assign students to groups as follows (teacher is not in a group):
// 0: no groups.
// 1: in group 1/grouping 1.
// 2: in group 2/grouping 2.
groups_add_member($group1, $students[1]);
groups_add_member($group2, $students[2]);
// Test specific grouping.
$cond = new condition((object)array('id' => (int)$grouping1->id));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[1]->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($teacher->id, $students[0]->id, $students[2]->id), $result);
// Test course-module grouping.
$modinfo = get_fast_modinfo($course);
$cm = $modinfo->get_cm($page->cmid);
$info = new \core_availability\info_module($cm);
$result = array_keys($info->filter_user_list($allusers, $course));
$this->assertEquals(array($teacher->id, $students[2]->id), $result);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_grouping
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014022800;
$plugin->requires = 2014040401;
$plugin->component = 'availability_grouping';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_grouping-form",
"builds": {
"moodle-availability_grouping-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,82 @@
/**
* JavaScript for form editing grouping conditions.
*
* @module moodle-availability_grouping-form
*/
M.availability_grouping = M.availability_grouping || {};
/**
* @class M.availability_grouping.form
* @extends M.core_availability.plugin
*/
M.availability_grouping.form = Y.Object(M.core_availability.plugin);
/**
* Groupings available for selection (alphabetical order).
*
* @property groupings
* @type Array
*/
M.availability_grouping.form.groupings = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} groupings Array of objects containing groupingid => name
*/
M.availability_grouping.form.initInner = function(groupings) {
this.groupings = groupings;
};
M.availability_grouping.form.getNode = function(json) {
// Create HTML structure.
var strings = M.str.availability_grouping;
var html = '<label>' + strings.title + ' <span class="availability-group">' +
'<select name="id">' +
'<option value="choose">' + M.str.moodle.choosedots + '</option>';
for (var i = 0; i < this.groupings.length; i++) {
var grouping = this.groupings[i];
// String has already been escaped using format_string.
html += '<option value="' + grouping.id + '">' + grouping.name + '</option>';
}
html += '</select></span></label>';
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial value if specified.
if (json.id !== undefined &&
node.one('select[name=id] > option[value=' + json.id + ']')) {
node.one('select[name=id]').set('value', json.id);
}
// Add event handlers (first time only).
if (!M.availability_grouping.form.addedEvents) {
M.availability_grouping.form.addedEvents = true;
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
// Just update the form fields.
M.core_availability.form.update();
}, '.availability_grouping select');
}
return node;
};
M.availability_grouping.form.fillValue = function(value, node) {
var selected = node.one('select[name=id]').get('value');
if (selected === 'choose') {
value.id = 'choose';
} else {
value.id = parseInt(selected, 10);
}
};
M.availability_grouping.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check grouping item id.
if (value.id === 'choose') {
errors.push('availability_grouping:error_selectgrouping');
}
};

View File

@ -0,0 +1,10 @@
{
"moodle-availability_grouping-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}

View File

@ -0,0 +1,455 @@
<?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/>.
/**
* User profile field condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_profile;
defined('MOODLE_INTERNAL') || die();
/**
* User profile field condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var string Operator: field contains value */
const OP_CONTAINS = 'contains';
/** @var string Operator: field does not contain value */
const OP_DOES_NOT_CONTAIN = 'doesnotcontain';
/** @var string Operator: field equals value */
const OP_IS_EQUAL_TO = 'isequalto';
/** @var string Operator: field starts with value */
const OP_STARTS_WITH = 'startswith';
/** @var string Operator: field ends with value */
const OP_ENDS_WITH = 'endswith';
/** @var string Operator: field is empty */
const OP_IS_EMPTY = 'isempty';
/** @var string Operator: field is not empty */
const OP_IS_NOT_EMPTY = 'isnotempty';
/** @var array|null Array of custom profile fields (static cache within request) */
protected static $customprofilefields = null;
/** @var string Field name (for standard fields) or '' if custom field */
protected $standardfield = '';
/** @var int Field name (for custom fields) or '' if standard field */
protected $customfield = '';
/** @var string Operator type (OP_xx constant) */
protected $operator;
/** @var string Expected value for field */
protected $value = '';
/**
* Constructor.
*
* @param stdClass $structure Data structure from JSON decode
* @throws coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get operator.
if (isset($structure->op) && in_array($structure->op, array(self::OP_CONTAINS,
self::OP_DOES_NOT_CONTAIN, self::OP_IS_EQUAL_TO, self::OP_STARTS_WITH,
self::OP_ENDS_WITH, self::OP_IS_EMPTY, self::OP_IS_NOT_EMPTY), true)) {
$this->operator = $structure->op;
} else {
throw new \coding_exception('Missing or invalid ->op for profile condition');
}
// For operators other than the empty/not empty ones, require value.
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
if (isset($structure->v)) {
throw new \coding_exception('Unexpected ->v for non-value operator');
}
break;
default:
if (isset($structure->v) && is_string($structure->v)) {
$this->value = $structure->v;
} else {
throw new \coding_exception('Missing or invalid ->v for profile condition');
}
break;
}
// Get field type.
if (property_exists($structure, 'sf')) {
if (property_exists($structure, 'cf')) {
throw new \coding_exception('Both ->sf and ->cf for profile condition');
}
if (is_string($structure->sf)) {
$this->standardfield = $structure->sf;
} else {
throw new \coding_exception('Invalid ->sf for profile condition');
}
} else if (property_exists($structure, 'cf')) {
if (is_string($structure->cf)) {
$this->customfield = $structure->cf;
} else {
throw new \coding_exception('Invalid ->cf for profile condition');
}
} else {
throw new \coding_exception('Missing ->sf or ->cf for profile condition');
}
}
public function save() {
$result = (object)array('type' => 'profile', 'op' => $this->operator);
if ($this->customfield) {
$result->cf = $this->customfield;
} else {
$result->sf = $this->standardfield;
}
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
break;
default:
$result->v = $this->value;
break;
}
return $result;
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$uservalue = $this->get_cached_user_profile_field($userid);
$allow = self::is_field_condition_met($this->operator, $uservalue, $this->value);
if ($not) {
$allow = !$allow;
}
return $allow;
}
public function get_description($full, $not, \core_availability\info $info) {
$course = $info->get_course();
// Display the fieldname into current lang.
if ($this->customfield) {
// Is a custom profile field (will use multilang).
$customfields = self::get_custom_profile_fields();
if (array_key_exists($this->customfield, $customfields)) {
$translatedfieldname = $customfields[$this->customfield]->name;
} else {
$translatedfieldname = get_string('missing', 'availability_profile',
$this->customfield);
}
} else {
$translatedfieldname = get_user_field_name($this->standardfield);
}
$context = \context_course::instance($course->id);
$a = new \stdClass();
$a->field = format_string($translatedfieldname, true, array('context' => $context));
$a->value = s($this->value);
if ($not) {
// When doing NOT strings, we replace the operator with its inverse.
// Some of them don't have inverses, so for those we use a new
// identifier which is only used for this lang string.
switch($this->operator) {
case self::OP_CONTAINS:
$opname = self::OP_DOES_NOT_CONTAIN;
break;
case self::OP_DOES_NOT_CONTAIN:
$opname = self::OP_CONTAINS;
break;
case self::OP_ENDS_WITH:
$opname = 'notendswith';
break;
case self::OP_IS_EMPTY:
$opname = self::OP_IS_NOT_EMPTY;
break;
case self::OP_IS_EQUAL_TO:
$opname = 'notisequalto';
break;
case self::OP_IS_NOT_EMPTY:
$opname = self::OP_IS_EMPTY;
break;
case self::OP_STARTS_WITH:
$opname = 'notstartswith';
break;
default:
throw new \coding_exception('Unexpected operator: ' . $this->operator);
}
} else {
$opname = $this->operator;
}
return get_string('requires_' . $opname, 'availability_profile', $a);
}
protected function get_debug_string() {
if ($this->customfield) {
$out = '*' . $this->customfield;
} else {
$out = $this->standardfield;
}
$out .= ' ' . $this->operator;
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
break;
default:
$out .= ' ' . $this->value;
break;
}
return $out;
}
/**
* Returns true if a field meets the required conditions, false otherwise.
*
* @param string $operator the requirement/condition
* @param string $uservalue the user's value
* @param string $value the value required
* @return boolean True if conditions are met
*/
protected static function is_field_condition_met($operator, $uservalue, $value) {
if ($uservalue === false) {
// If the user value is false this is an instant fail.
// All user values come from the database as either data or the default.
// They will always be a string.
return false;
}
$fieldconditionmet = true;
// Just to be doubly sure it is a string.
$uservalue = (string)$uservalue;
switch($operator) {
case self::OP_CONTAINS:
$pos = strpos($uservalue, $value);
if ($pos === false) {
$fieldconditionmet = false;
}
break;
case self::OP_DOES_NOT_CONTAIN:
if (!empty($value)) {
$pos = strpos($uservalue, $value);
if ($pos !== false) {
$fieldconditionmet = false;
}
}
break;
case self::OP_IS_EQUAL_TO:
if ($value !== $uservalue) {
$fieldconditionmet = false;
}
break;
case self::OP_STARTS_WITH:
$length = strlen($value);
if ((substr($uservalue, 0, $length) !== $value)) {
$fieldconditionmet = false;
}
break;
case self::OP_ENDS_WITH:
$length = strlen($value);
$start = $length * -1;
if (substr($uservalue, $start) !== $value) {
$fieldconditionmet = false;
}
break;
case self::OP_IS_EMPTY:
if (!empty($uservalue)) {
$fieldconditionmet = false;
}
break;
case self::OP_IS_NOT_EMPTY:
if (empty($uservalue)) {
$fieldconditionmet = false;
}
break;
}
return $fieldconditionmet;
}
/**
* Gets data about custom profile fields. Cached statically in current
* request.
*
* @return array Array of records indexed by shortname
*/
public static function get_custom_profile_fields() {
global $DB;
if (self::$customprofilefields === null) {
self::$customprofilefields = $DB->get_records('user_info_field', null,
'id ASC', 'shortname, id, name, defaultdata');
}
return self::$customprofilefields;
}
/**
* Wipes the static cache (for use in unit tests).
*/
public static function wipe_static_cache() {
self::$customprofilefields = null;
}
/**
* Return the value for a user's profile field
*
* @param int $userid User ID
* @return string|bool Value, or false if user does not have a value for this field
*/
protected function get_cached_user_profile_field($userid) {
global $USER, $DB, $CFG;
$iscurrentuser = $USER->id == $userid;
if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
// Must be logged in and can't be the guest.
return false;
}
// Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
$iscustomprofilefield = $this->customfield ? true : false;
if ($iscustomprofilefield) {
// As its a custom profile field we need to map the id back to the actual field.
// We'll also preload all of the other custom profile fields just in case and ensure we have the
// default value available as well.
if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) {
// No such field exists.
// This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
// or when restoring a backup of a course with user profile field conditions.
return false;
}
$field = $this->customfield;
} else {
$field = $this->standardfield;
}
// If its the current user than most likely we will be able to get this information from $USER.
// If its a regular profile field then it should already be available, if not then we have a mega problem.
// If its a custom profile field then it should be available but may not be. If it is then we use the value
// available, otherwise we load all custom profile fields into a temp object and refer to that.
// Noting its not going be great for performance if we have to use the temp object as it involves loading the
// custom profile field API and classes.
if ($iscurrentuser) {
if (!$iscustomprofilefield) {
if (property_exists($USER, $field)) {
return $USER->{$field};
} else {
// Unknown user field. This should not happen.
throw new \coding_exception('Requested user profile field does not exist');
}
}
// Checking if the custom profile fields are already available.
if (!isset($USER->profile)) {
// Drat! they're not. We need to use a temp object and load them.
// We don't use $USER as the profile fields are loaded into the object.
$user = new \stdClass;
$user->id = $USER->id;
// This should ALWAYS be set, but just in case we check.
require_once($CFG->dirroot . '/user/profile/lib.php');
profile_load_custom_fields($user);
if (array_key_exists($field, $user->profile)) {
return $user->profile[$field];
}
} else if (array_key_exists($field, $USER->profile)) {
// Hurrah they're available, this is easy.
return $USER->profile[$field];
}
// The profile field doesn't exist.
return false;
} else {
// Loading for another user.
if ($iscustomprofilefield) {
// Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
// for us (this will likely be hit again).
// We are able to do this because we've already pre-loaded the custom fields.
$data = $DB->get_field('user_info_data', 'data', array('userid' => $userid,
'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING);
// If we have data return that, otherwise return the default.
if ($data !== false) {
return $data;
} else {
return self::$customprofilefields[$field]->defaultdata;
}
} else {
// Its a standard field, retrieve it from the user.
return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
}
}
return false;
}
public function is_applied_to_user_lists() {
// Profile conditions are assumed to be 'permanent', so they affect the
// display of user lists for activities.
return true;
}
public function filter_user_list(array $users, $not, \core_availability\info $info,
\core_availability\capability_checker $checker) {
global $CFG, $DB;
// Get all users from the list who match the condition.
list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
if ($this->customfield) {
$customfields = self::get_custom_profile_fields();
if (!array_key_exists($this->customfield, $customfields)) {
// If the field isn't found, nobody matches.
return array();
}
$customfield = $customfields[$this->customfield];
// Fetch custom field value for all users.
$values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql,
array_merge(array($customfield->id), $params),
'', 'userid, data');
$valuefield = 'data';
$default = $customfield->defaultdata;
} else {
$values = $DB->get_records_select('user', 'id ' . $sql, $params,
'', 'id, '. $this->standardfield);
$valuefield = $this->standardfield;
$default = '';
}
// Filter the user list.
$result = array();
foreach ($users as $id => $user) {
// Get value for user.
if (array_key_exists($id, $values)) {
$value = $values[$id]->{$valuefield};
} else {
$value = $default;
}
// Check value.
$allow = $this->is_field_condition_met($this->operator, $value, $this->value);
if ($not) {
$allow = !$allow;
}
if ($allow) {
$result[$id] = $user;
}
}
return $result;
}
}

View File

@ -0,0 +1,79 @@
<?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/>.
/**
* Front-end class.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_profile;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
protected function get_javascript_strings() {
return array('op_contains', 'op_doesnotcontain', 'op_endswith', 'op_isempty',
'op_isequalto', 'op_isnotempty', 'op_startswith', 'conditiontitle',
'label_operator', 'label_value');
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
// Standard user fields.
$standardfields = array(
'firstname' => get_user_field_name('firstname'),
'lastname' => get_user_field_name('lastname'),
'email' => get_user_field_name('email'),
'city' => get_user_field_name('city'),
'country' => get_user_field_name('country'),
'url' => get_user_field_name('url'),
'icq' => get_user_field_name('icq'),
'skype' => get_user_field_name('skype'),
'aim' => get_user_field_name('aim'),
'yahoo' => get_user_field_name('yahoo'),
'msn' => get_user_field_name('msn'),
'idnumber' => get_user_field_name('idnumber'),
'institution' => get_user_field_name('institution'),
'department' => get_user_field_name('department'),
'phone1' => get_user_field_name('phone1'),
'phone2' => get_user_field_name('phone2'),
'address' => get_user_field_name('address')
);
\core_collator::asort($standardfields);
// Custom fields.
$customfields = array();
$options = array('context' => \context_course::instance($course->id));
foreach (condition::get_custom_profile_fields() as $field) {
$customfields[$field->shortname] = format_string($field->name, true, $options);
}
\core_collator::asort($customfields);
// Make arrays into JavaScript format (non-associative, ordered) and return.
return array(self::convert_associative_array_for_js($standardfields, 'field', 'display'),
self::convert_associative_array_for_js($customfields, 'field', 'display'));
}
}

View File

@ -0,0 +1,50 @@
<?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/>.
/**
* Language strings.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['conditiontitle'] = 'User profile field';
$string['description'] = 'Control access based on fields within the student&rsquo;s profile.';
$string['error_selectfield'] = 'You must select a profile field.';
$string['error_setvalue'] = 'You must type a value.';
$string['label_operator'] = 'Method of comparison';
$string['label_value'] = 'Value to compare against';
$string['pluginname'] = 'Restriction by profile';
$string['requires_contains'] = 'Your <strong>{$a->field}</strong> contains <strong>{$a->value}</strong>';
$string['requires_doesnotcontain'] = 'Your <strong>{$a->field}</strong> does not contain <strong>{$a->value}</strong>';
$string['requires_endswith'] = 'Your <strong>{$a->field}</strong> ends with <strong>{$a->value}</strong>';
$string['requires_isempty'] = 'Your <strong>{$a->field}</strong> is empty';
$string['requires_isequalto'] = 'Your <strong>{$a->field}</strong> is <strong>{$a->value}</strong>';
$string['requires_isnotempty'] = 'Your <strong>{$a->field}</strong> is not empty';
$string['requires_notendswith'] = 'Your <strong>{$a->field}</strong> does not end with <strong>{$a->value}</strong>';
$string['requires_notisequalto'] = 'Your <strong>{$a->field}</strong> is not <strong>{$a->value}</strong>';
$string['requires_notstartswith'] = 'Your <strong>{$a->field}</strong> does not start with <strong>{$a->value}</strong>';
$string['requires_startswith'] = 'Your <strong>{$a->field}</strong> starts with <strong>{$a->value}</strong>';
$string['missing'] = '(Missing custom field: {$a})';
$string['title'] = 'User profile';
$string['op_contains'] = 'contains';
$string['op_doesnotcontain'] = 'doesn&rsquo;t contain';
$string['op_endswith'] = 'ends with';
$string['op_isempty'] = 'is empty';
$string['op_isequalto'] = 'is equal to';
$string['op_isnotempty'] = 'is not empty';
$string['op_startswith'] = 'starts with';

View File

@ -0,0 +1,66 @@
@availability @availability_profile
Feature: availability_profile
In order to control student access to activities
As a teacher
I need to set profile conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username | email |
| teacher1 | t@example.org |
| student1 | s@example.org |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Test condition
# Basic setup.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Add
And I add a "Page" to section "1"
And I set the following fields to these values:
| Name | P1 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "s@example.org"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Add
And I add a "Page" to section "2"
And I set the following fields to these values:
| Name | P2 |
| Description | x |
| Page content | x |
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "q@example.org"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Log back in as student.
When I log out
And I log in as "student1"
And I follow "Course 1"
# I see P1 but not P2.
Then I should see "P1" in the "region-main" "region"
And I should not see "P2" in the "region-main" "region"

View File

@ -0,0 +1,421 @@
<?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 the user profile condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use availability_profile\condition;
/**
* Unit tests for the user profile condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_profile_condition_testcase extends advanced_testcase {
/** @var profile_define_text Profile field for testing */
protected $profilefield;
/** @var array Array of user IDs for whome we already set the profile field */
protected $setusers = array();
public function setUp() {
global $DB, $CFG;
$this->resetAfterTest();
// Add a custom profile field type. The API for doing this is indescribably
// horrid and tightly intertwined with the form UI, so it's best to add
// it directly in database.
$DB->insert_record('user_info_field', array(
'shortname' => 'frogtype', 'name' => 'Type of frog', 'categoryid' => 1,
'datatype' => 'text'));
$this->profilefield = $DB->get_record('user_info_field',
array('shortname' => 'frogtype'));
// Clear static cache.
\availability_profile\condition::wipe_static_cache();
// Load the mock info class so that it can be used.
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using date condition as part of tree.
*/
public function test_in_tree() {
global $USER;
$this->setAdminUser();
$info = new \core_availability\mock_info();
$structure = (object)array('op' => '|', 'show' => true, 'c' => array(
(object)array('type' => 'profile',
'op' => condition::OP_IS_EQUAL_TO,
'cf' => 'frogtype', 'v' => 'tree')));
$tree = new \core_availability\tree($structure);
// Initial check (user does not have custom field).
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertFalse($result->is_available());
// Set field.
$this->set_field($USER->id, 'tree');
// Now it's true!
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertTrue($result->is_available());
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor() {
// No parameters.
$structure = new stdClass();
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->op', $e->getMessage());
}
// Invalid op.
$structure->op = 'isklingonfor';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->op', $e->getMessage());
}
// Missing value.
$structure->op = condition::OP_IS_EQUAL_TO;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->v', $e->getMessage());
}
// Invalid value (not string).
$structure->v = false;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing or invalid ->v', $e->getMessage());
}
// Unexpected value.
$structure->op = condition::OP_IS_EMPTY;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Unexpected ->v', $e->getMessage());
}
// Missing field.
$structure->op = condition::OP_IS_EQUAL_TO;
$structure->v = 'flying';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Missing ->sf or ->cf', $e->getMessage());
}
// Invalid field (not string).
$structure->sf = 42;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->sf', $e->getMessage());
}
// Both fields.
$structure->sf = 'department';
$structure->cf = 'frogtype';
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Both ->sf and ->cf', $e->getMessage());
}
// Invalid ->cf field (not string).
unset($structure->sf);
$structure->cf = false;
try {
$cond = new condition($structure);
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Invalid ->cf', $e->getMessage());
}
// Valid examples (checks values are correctly included).
$structure->cf = 'frogtype';
$cond = new condition($structure);
$this->assertEquals('{profile:*frogtype isequalto flying}', (string)$cond);
unset($structure->v);
$structure->op = condition::OP_IS_EMPTY;
$cond = new condition($structure);
$this->assertEquals('{profile:*frogtype isempty}', (string)$cond);
unset($structure->cf);
$structure->sf = 'department';
$cond = new condition($structure);
$this->assertEquals('{profile:department isempty}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save() {
$structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_IS_EMPTY);
$cond = new condition($structure);
$structure->type = 'profile';
$this->assertEquals($structure, $cond->save());
$structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_ENDS_WITH,
'v' => 'bouncy');
$cond = new condition($structure);
$structure->type = 'profile';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the is_available function. There is no separate test for
* get_full_information because that function is called from is_available
* and we test its values here.
*/
public function test_is_available() {
global $USER, $SITE, $DB;
$this->setAdminUser();
$info = new \core_availability\mock_info();
// Prepare to test with all operators against custom field using all
// combinations of NOT and true/false states..
$information = 'x';
$structure = (object)array('cf' => 'frogtype');
$structure->op = condition::OP_IS_NOT_EMPTY;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*is not empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'poison dart');
$this->assert_is_available_result(true, '~Type of frog.*is empty~',
$cond, $info, $USER->id);
$structure->op = condition::OP_IS_EMPTY;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~.*Type of frog.*is empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, null);
$this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, '');
$this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
$cond, $info, $USER->id);
$structure->op = condition::OP_CONTAINS;
$structure->v = 'llf';
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*contains.*llf~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'bullfrog');
$this->assert_is_available_result(true, '~Type of frog.*does not contain.*llf~',
$cond, $info, $USER->id);
$structure->op = condition::OP_DOES_NOT_CONTAIN;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*does not contain.*llf~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'goliath');
$this->assert_is_available_result(true, '~Type of frog.*contains.*llf~',
$cond, $info, $USER->id);
$structure->op = condition::OP_IS_EQUAL_TO;
$structure->v = 'Kermit';
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*is <.*Kermit~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Kermit');
$this->assert_is_available_result(true, '~Type of frog.*is not.*Kermit~',
$cond, $info, $USER->id);
$structure->op = condition::OP_STARTS_WITH;
$structure->v = 'Kerm';
$cond = new condition($structure);
$this->assert_is_available_result(true, '~Type of frog.*does not start.*Kerm~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Keroppi');
$this->assert_is_available_result(false, '~Type of frog.*starts.*Kerm~',
$cond, $info, $USER->id);
$structure->op = condition::OP_ENDS_WITH;
$structure->v = 'ppi';
$cond = new condition($structure);
$this->assert_is_available_result(true, '~Type of frog.*does not end.*ppi~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Kermit');
$this->assert_is_available_result(false, '~Type of frog.*ends.*ppi~',
$cond, $info, $USER->id);
// Also test is_available for a different (not current) user.
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$structure->op = condition::OP_CONTAINS;
$structure->v = 'rne';
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->set_field($user->id, 'horned');
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
// Now check with a standard field (department).
$structure = (object)array('op' => condition::OP_IS_EQUAL_TO,
'sf' => 'department', 'v' => 'Cheese Studies');
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $USER->id));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
// Check the message (should be using lang string with capital, which
// is evidence that it called the right function to get the name).
$information = $cond->get_description(false, false, $info);
$this->assertRegExp('~Department~', $information);
// Set the field to true for both users and retry.
$DB->set_field('user', 'department', 'Cheese Studies', array('id' => $user->id));
$USER->department = 'Cheese Studies';
$this->assertTrue($cond->is_available(false, $info, true, $USER->id));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
}
/**
* Sets the custom profile field used for testing.
*
* @param int $userid User id
* @param string|null $value Field value or null to clear
*/
protected function set_field($userid, $value) {
global $DB, $USER;
$alreadyset = array_key_exists($userid, $this->setusers);
if (is_null($value)) {
$DB->delete_records('user_info_data',
array('userid' => $userid, 'fieldid' => $this->profilefield->id));
unset($this->setusers[$userid]);
} else if ($alreadyset) {
$DB->set_field('user_info_data', 'data', $value,
array('userid' => $userid, 'fieldid' => $this->profilefield->id));
} else {
$DB->insert_record('user_info_data', array('userid' => $userid,
'fieldid' => $this->profilefield->id, 'data' => $value));
$this->setusers[$userid] = true;
}
}
/**
* Checks the result of is_available. This function is to save duplicated
* code; it does two checks (the normal is_available with $not set to true
* and set to false). Whichever result is expected to be true, it checks
* $information ends up as empty string for that one, and as a regex match
* for another one.
*
* @param bool $yes If the positive test is expected to return true
* @param string $failpattern Regex pattern to match text when it returns false
* @param condition $cond Condition
* @param \core_availability\info $info Information about current context
* @param int $userid User id
*/
protected function assert_is_available_result($yes, $failpattern, condition $cond,
\core_availability\info $info, $userid) {
// Positive (normal) test.
$this->assertEquals($yes, $cond->is_available(false, $info, true, $userid),
'Failed checking normal (positive) result');
if (!$yes) {
$information = $cond->get_description(false, false, $info);
$this->assertRegExp($failpattern, $information);
}
// Negative (NOT) test.
$this->assertEquals(!$yes, $cond->is_available(true, $info, true, $userid),
'Failed checking NOT (negative) result');
if ($yes) {
$information = $cond->get_description(false, true, $info);
$this->assertRegExp($failpattern, $information);
}
}
/**
* Tests the filter_users (bulk checking) function.
*/
public function test_filter_users() {
global $DB, $CFG;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and some users.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$student1 = $generator->create_user(array('institution' => 'Unseen University'));
$student2 = $generator->create_user(array('institution' => 'Hogwarts'));
$student3 = $generator->create_user(array('institution' => 'Unseen University'));
$allusers = array();
foreach (array($student1, $student2, $student3) as $student) {
$generator->enrol_user($student->id, $course->id);
$allusers[$student->id] = $student;
}
$this->set_field($student1->id, 'poison dart');
$this->set_field($student2->id, 'poison dart');
$info = new \core_availability\mock_info($course);
$checker = new \core_availability\capability_checker($info->get_context());
// Test standard field condition (positive and negative).
$cond = new condition((object)array('sf' => 'institution', 'op' => 'contains', 'v' => 'Unseen'));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($student1->id, $student3->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($student2->id), $result);
// Test custom field condition.
$cond = new condition((object)array('cf' => 'frogtype', 'op' => 'contains', 'v' => 'poison'));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($student1->id, $student2->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($student3->id), $result);
}
}

View File

@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014022700;
$plugin->requires = 2014040401;
$plugin->component = 'availability_profile';

View File

@ -0,0 +1,10 @@
{
"name": "moodle-availability_profile-form",
"builds": {
"moodle-availability_profile-form": {
"jsfiles": [
"form.js"
]
}
}
}

View File

@ -0,0 +1,130 @@
/**
* JavaScript for form editing profile conditions.
*
* @module moodle-availability_profile-form
*/
M.availability_profile = M.availability_profile || {};
/**
* @class M.availability_profile.form
* @extends M.core_availability.plugin
*/
M.availability_profile.form = Y.Object(M.core_availability.plugin);
/**
* Groupings available for selection (alphabetical order).
*
* @property profiles
* @type Array
*/
M.availability_profile.form.profiles = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} standardFields Array of objects with .field, .display
* @param {Array} customFields Array of objects with .field, .display
*/
M.availability_profile.form.initInner = function(standardFields, customFields) {
this.standardFields = standardFields;
this.customFields = customFields;
};
M.availability_profile.form.getNode = function(json) {
// Create HTML structure.
var strings = M.str.availability_profile;
var html = '<span class="availability-group"><label>' + strings.conditiontitle + ' ' +
'<select name="field">' +
'<option value="choose">' + M.str.moodle.choosedots + '</option>';
var fieldInfo;
for (var i = 0; i < this.standardFields.length; i++) {
fieldInfo = this.standardFields[i];
// String has already been escaped using format_string.
html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
for (i = 0; i < this.customFields.length; i++) {
fieldInfo = this.customFields[i];
// String has already been escaped using format_string.
html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + strings.label_operator +
' </span><select name="op" title="' + strings.label_operator + '">';
var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
'isempty', 'isnotempty'];
for (i = 0; i < operators.length; i++) {
html += '<option value="' + operators[i] + '">' +
strings['op_' + operators[i]] + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + strings.label_value +
'</span><input name="value" type="text" style="width: 10em" title="' +
strings.label_value + '"/></label></span>';
var node = Y.Node.create('<span>' + html + '</span>');
// Set initial values if specified.
if (json.sf !== undefined &&
node.one('select[name=field] > option[value=sf_' + json.sf + ']')) {
node.one('select[name=field]').set('value', 'sf_' + json.sf);
} else if (json.cf !== undefined &&
node.one('select[name=field] > option[value=cf_' + json.sf + ']')) {
node.one('select[name=field]').set('value', 'cf_' + json.cf);
}
if (json.op !== undefined &&
node.one('select[name=op] > option[value=' + json.op + ']')) {
node.one('select[name=op]').set('value', json.op);
}
if (json.v !== undefined) {
node.one('input').set('value', json.v);
}
// Add event handlers (first time only).
if (!M.availability_profile.form.addedEvents) {
M.availability_profile.form.addedEvents = true;
var updateForm = function(input) {
var ancestorNode = input.ancestor('span.availability_profile');
var op = ancestorNode.one('select[name=op]');
var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty');
ancestorNode.one('input[name=value]').set('disabled', novalue);
M.core_availability.form.update();
};
var root = Y.one('#fitem_id_availabilityconditionsjson');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile select');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile input[name=value]');
}
return node;
};
M.availability_profile.form.fillValue = function(value, node) {
// Set field.
var field = node.one('select[name=field]').get('value');
if (field.substr(0, 3) === 'sf_') {
value.sf = field.substr(3);
} else if (field.substr(0, 3) === 'cf_') {
value.cf = field.substr(3);
}
// Operator and value
value.op = node.one('select[name=op]').get('value');
var valueNode = node.one('input[name=value]');
if (!valueNode.get('disabled')) {
value.v = valueNode.get('value');
}
};
M.availability_profile.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check profile item id.
if (value.sf === undefined && value.cf === undefined) {
errors.push('availability_profile:error_selectfield');
}
if (value.v !== undefined && /^\s*$/.test(value.v)) {
errors.push('availability_profile:error_setvalue');
}
};

View File

@ -0,0 +1,10 @@
{
"moodle-availability_profile-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}

View File

@ -771,13 +771,14 @@ class grade_grade extends grade_object {
*
* @param bool $deleted True if grade was actually deleted
*/
function notify_changed($deleted) {
protected function notify_changed($deleted) {
global $CFG;
// Inform conditionlib since it may cache the grades for conditional availability of modules or sections.
if (!empty($CFG->enableavailability)) {
require_once($CFG->libdir.'/conditionlib.php');
condition_info_base::inform_grade_changed($this, $deleted);
// Condition code may cache the grades for conditional availability of
// modules or sections. (This code should use a hook for communication
// with plugin, but hooks are not implemented at time of writing.)
if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
\availability_grade\callbacks::grade_changed($this->userid);
}
require_once($CFG->libdir.'/completionlib.php');

View File

@ -2139,4 +2139,21 @@ class grade_item extends grade_object {
}
return parent::can_control_visibility();
}
/**
* Used to notify the completion system (if necessary) that a user's grade
* has changed, and clear up a possible score cache.
*
* @param bool $deleted True if grade was actually deleted
*/
protected function notify_changed($deleted) {
global $CFG;
// Condition code may cache the grades for conditional availability of
// modules or sections. (This code should use a hook for communication
// with plugin, but hooks are not implemented at time of writing.)
if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
\availability_grade\callbacks::grade_item_changed($this->courseid);
}
}
}

View File

@ -396,7 +396,7 @@ abstract class grade_object {
*
* @param bool $deleted
*/
function notify_changed($deleted) {
protected function notify_changed($deleted) {
}
/**