mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 16:36:37 +02:00
Merge branch 'MDL-70909-311' of git://github.com/ferranrecio/moodle into MOODLE_311_STABLE
This commit is contained in:
commit
66131d9307
13 changed files with 555 additions and 68 deletions
|
@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||||
* @copyright 2012 Petr Skoda {@link http://skodak.org}
|
* @copyright 2012 Petr Skoda {@link http://skodak.org}
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/
|
*/
|
||||||
class core_enrollib_testcase extends advanced_testcase {
|
class enrollib_test extends advanced_testcase {
|
||||||
|
|
||||||
public function test_enrol_get_all_users_courses() {
|
public function test_enrol_get_all_users_courses() {
|
||||||
global $DB, $CFG;
|
global $DB, $CFG;
|
||||||
|
@ -1415,4 +1415,77 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||||
$durationinday = $duration / DAYSECS;
|
$durationinday = $duration / DAYSECS;
|
||||||
$this->assertEquals(9, $durationinday);
|
$this->assertEquals(9, $durationinday);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test get_enrolled_with_capabilities_join cannotmatchanyrows attribute.
|
||||||
|
*
|
||||||
|
* @dataProvider get_enrolled_with_capabilities_join_cannotmatchanyrows_data()
|
||||||
|
* @param string $capability the tested capability
|
||||||
|
* @param bool $useprohibit if the capability must be assigned to prohibit
|
||||||
|
* @param int $expectedmatch expected cannotmatchanyrows value
|
||||||
|
* @param int $expectedcount expceted count value
|
||||||
|
*/
|
||||||
|
public function test_get_enrolled_with_capabilities_join_cannotmatchanyrows(
|
||||||
|
string $capability,
|
||||||
|
bool $useprohibit,
|
||||||
|
int $expectedmatch,
|
||||||
|
int $expectedcount
|
||||||
|
) {
|
||||||
|
global $DB, $CFG;
|
||||||
|
|
||||||
|
$this->resetAfterTest();
|
||||||
|
|
||||||
|
$course = $this->getDataGenerator()->create_course();
|
||||||
|
$context = context_course::instance($course->id);
|
||||||
|
|
||||||
|
$roleid = $CFG->defaultuserroleid;
|
||||||
|
|
||||||
|
// Override capability if necessary.
|
||||||
|
if ($useprohibit && $capability) {
|
||||||
|
assign_capability($capability, CAP_PROHIBIT, $roleid, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we must enrol or not.
|
||||||
|
$this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||||
|
|
||||||
|
$join = get_enrolled_with_capabilities_join($context, '', $capability);
|
||||||
|
|
||||||
|
// Execute query.
|
||||||
|
$sql = "SELECT COUNT(DISTINCT u.id)
|
||||||
|
FROM {user} u {$join->joins}
|
||||||
|
WHERE {$join->wheres}";
|
||||||
|
$countrecords = $DB->count_records_sql($sql, $join->params);
|
||||||
|
|
||||||
|
// Validate cannotmatchanyrows.
|
||||||
|
$this->assertEquals($expectedmatch, $join->cannotmatchanyrows);
|
||||||
|
$this->assertEquals($expectedcount, $countrecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for test_get_enrolled_with_capabilities_join_cannotmatchanyrows
|
||||||
|
*
|
||||||
|
* @return @array of testing scenarios
|
||||||
|
*/
|
||||||
|
public function get_enrolled_with_capabilities_join_cannotmatchanyrows_data() {
|
||||||
|
return [
|
||||||
|
'no prohibits, no capability' => [
|
||||||
|
'capability' => '',
|
||||||
|
'useprohibit' => false,
|
||||||
|
'expectedmatch' => 0,
|
||||||
|
'expectedcount' => 1,
|
||||||
|
],
|
||||||
|
'no prohibits with capability' => [
|
||||||
|
'capability' => 'moodle/course:manageactivities',
|
||||||
|
'useprohibit' => false,
|
||||||
|
'expectedmatch' => 0,
|
||||||
|
'expectedcount' => 1,
|
||||||
|
],
|
||||||
|
'prohibits with capability' => [
|
||||||
|
'capability' => 'moodle/course:manageactivities',
|
||||||
|
'useprohibit' => true,
|
||||||
|
'expectedmatch' => 1,
|
||||||
|
'expectedcount' => 0,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,10 +25,11 @@
|
||||||
|
|
||||||
defined('MOODLE_INTERNAL') || die();
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/../../behat/behat_base.php');
|
||||||
|
|
||||||
use Behat\Gherkin\Node\TableNode as TableNode;
|
use Behat\Gherkin\Node\TableNode as TableNode;
|
||||||
use Behat\Behat\Tester\Exception\PendingException as PendingException;
|
use Behat\Behat\Tester\Exception\PendingException as PendingException;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to quickly create Behat test data using component data generators.
|
* Class to quickly create Behat test data using component data generators.
|
||||||
*
|
*
|
||||||
|
@ -480,53 +481,7 @@ abstract class behat_generator_base {
|
||||||
* @return context
|
* @return context
|
||||||
*/
|
*/
|
||||||
protected function get_context($levelname, $contextref) {
|
protected function get_context($levelname, $contextref) {
|
||||||
global $DB;
|
return behat_base::get_context($levelname, $contextref);
|
||||||
|
|
||||||
// Getting context levels and names (we will be using the English ones as it is the test site language).
|
|
||||||
$contextlevels = context_helper::get_all_levels();
|
|
||||||
$contextnames = array();
|
|
||||||
foreach ($contextlevels as $level => $classname) {
|
|
||||||
$contextnames[context_helper::get_level_name($level)] = $level;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($contextnames[$levelname])) {
|
|
||||||
throw new Exception('The specified "' . $levelname . '" context level does not exist');
|
|
||||||
}
|
|
||||||
$contextlevel = $contextnames[$levelname];
|
|
||||||
|
|
||||||
// Return it, we don't need to look for other internal ids.
|
|
||||||
if ($contextlevel == CONTEXT_SYSTEM) {
|
|
||||||
return context_system::instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($contextlevel) {
|
|
||||||
|
|
||||||
case CONTEXT_USER:
|
|
||||||
$instanceid = $DB->get_field('user', 'id', array('username' => $contextref));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CONTEXT_COURSECAT:
|
|
||||||
$instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CONTEXT_COURSE:
|
|
||||||
$instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CONTEXT_MODULE:
|
|
||||||
$instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$contextclass = $contextlevels[$contextlevel];
|
|
||||||
if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) {
|
|
||||||
throw new Exception('The specified "' . $contextref . '" context reference does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1068,6 +1068,69 @@ EOF;
|
||||||
|
|
||||||
\core\session\manager::set_user($user);
|
\core\session\manager::set_user($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the internal moodle context id from the context reference.
|
||||||
|
*
|
||||||
|
* The context reference changes depending on the context
|
||||||
|
* level, it can be the system, a user, a category, a course or
|
||||||
|
* a module.
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
* @param string $levelname The context level string introduced by the test writer
|
||||||
|
* @param string $contextref The context reference introduced by the test writer
|
||||||
|
* @return context
|
||||||
|
*/
|
||||||
|
public static function get_context(string $levelname, string $contextref): context {
|
||||||
|
global $DB;
|
||||||
|
|
||||||
|
// Getting context levels and names (we will be using the English ones as it is the test site language).
|
||||||
|
$contextlevels = context_helper::get_all_levels();
|
||||||
|
$contextnames = array();
|
||||||
|
foreach ($contextlevels as $level => $classname) {
|
||||||
|
$contextnames[context_helper::get_level_name($level)] = $level;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($contextnames[$levelname])) {
|
||||||
|
throw new Exception('The specified "' . $levelname . '" context level does not exist');
|
||||||
|
}
|
||||||
|
$contextlevel = $contextnames[$levelname];
|
||||||
|
|
||||||
|
// Return it, we don't need to look for other internal ids.
|
||||||
|
if ($contextlevel == CONTEXT_SYSTEM) {
|
||||||
|
return context_system::instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($contextlevel) {
|
||||||
|
|
||||||
|
case CONTEXT_USER:
|
||||||
|
$instanceid = $DB->get_field('user', 'id', array('username' => $contextref));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTEXT_COURSECAT:
|
||||||
|
$instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTEXT_COURSE:
|
||||||
|
$instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONTEXT_MODULE:
|
||||||
|
$instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contextclass = $contextlevels[$contextlevel];
|
||||||
|
if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) {
|
||||||
|
throw new Exception('The specified "' . $contextref . '" context reference does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger click on node via javascript instead of actually clicking on it via pointer.
|
* Trigger click on node via javascript instead of actually clicking on it via pointer.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1383,6 +1383,10 @@ function is_enrolled(context $context, $user = null, $withcapability = '', $only
|
||||||
* several times (e.g. as manual enrolment, and as self enrolment). You may
|
* several times (e.g. as manual enrolment, and as self enrolment). You may
|
||||||
* need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
|
* need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
|
||||||
*
|
*
|
||||||
|
* In case is guaranteed some of the joins never match any rows, the resulting
|
||||||
|
* join_sql->cannotmatchanyrows will be true. This happens when the capability
|
||||||
|
* is prohibited.
|
||||||
|
*
|
||||||
* @param context $context
|
* @param context $context
|
||||||
* @param string $prefix optional, a prefix to the user id column
|
* @param string $prefix optional, a prefix to the user id column
|
||||||
* @param string|array $capability optional, may include a capability name, or array of names.
|
* @param string|array $capability optional, may include a capability name, or array of names.
|
||||||
|
@ -1392,24 +1396,27 @@ function is_enrolled(context $context, $user = null, $withcapability = '', $only
|
||||||
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
|
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
|
||||||
* @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
|
* @param bool $onlysuspended inverse of onlyactive, consider only suspended enrolments
|
||||||
* @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
|
* @param int $enrolid The enrolment ID. If not 0, only users enrolled using this enrolment method will be returned.
|
||||||
* @return \core\dml\sql_join Contains joins, wheres, params
|
* @return \core\dml\sql_join Contains joins, wheres, params and cannotmatchanyrows
|
||||||
*/
|
*/
|
||||||
function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
|
function get_enrolled_with_capabilities_join(context $context, $prefix = '', $capability = '', $group = 0,
|
||||||
$onlyactive = false, $onlysuspended = false, $enrolid = 0) {
|
$onlyactive = false, $onlysuspended = false, $enrolid = 0) {
|
||||||
$uid = $prefix . 'u.id';
|
$uid = $prefix . 'u.id';
|
||||||
$joins = array();
|
$joins = array();
|
||||||
$wheres = array();
|
$wheres = array();
|
||||||
|
$cannotmatchanyrows = false;
|
||||||
|
|
||||||
$enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
|
$enrolledjoin = get_enrolled_join($context, $uid, $onlyactive, $onlysuspended, $enrolid);
|
||||||
$joins[] = $enrolledjoin->joins;
|
$joins[] = $enrolledjoin->joins;
|
||||||
$wheres[] = $enrolledjoin->wheres;
|
$wheres[] = $enrolledjoin->wheres;
|
||||||
$params = $enrolledjoin->params;
|
$params = $enrolledjoin->params;
|
||||||
|
$cannotmatchanyrows = $cannotmatchanyrows || $enrolledjoin->cannotmatchanyrows;
|
||||||
|
|
||||||
if (!empty($capability)) {
|
if (!empty($capability)) {
|
||||||
$capjoin = get_with_capability_join($context, $capability, $uid);
|
$capjoin = get_with_capability_join($context, $capability, $uid);
|
||||||
$joins[] = $capjoin->joins;
|
$joins[] = $capjoin->joins;
|
||||||
$wheres[] = $capjoin->wheres;
|
$wheres[] = $capjoin->wheres;
|
||||||
$params = array_merge($params, $capjoin->params);
|
$params = array_merge($params, $capjoin->params);
|
||||||
|
$cannotmatchanyrows = $cannotmatchanyrows || $capjoin->cannotmatchanyrows;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($group) {
|
if ($group) {
|
||||||
|
@ -1419,13 +1426,14 @@ function get_enrolled_with_capabilities_join(context $context, $prefix = '', $ca
|
||||||
if (!empty($groupjoin->wheres)) {
|
if (!empty($groupjoin->wheres)) {
|
||||||
$wheres[] = $groupjoin->wheres;
|
$wheres[] = $groupjoin->wheres;
|
||||||
}
|
}
|
||||||
|
$cannotmatchanyrows = $cannotmatchanyrows || $groupjoin->cannotmatchanyrows;
|
||||||
}
|
}
|
||||||
|
|
||||||
$joins = implode("\n", $joins);
|
$joins = implode("\n", $joins);
|
||||||
$wheres[] = "{$prefix}u.deleted = 0";
|
$wheres[] = "{$prefix}u.deleted = 0";
|
||||||
$wheres = implode(" AND ", $wheres);
|
$wheres = implode(" AND ", $wheres);
|
||||||
|
|
||||||
return new \core\dml\sql_join($joins, $wheres, $params);
|
return new \core\dml\sql_join($joins, $wheres, $params, $cannotmatchanyrows);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -249,4 +249,44 @@ class behat_permissions extends behat_base {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark context as frozen.
|
||||||
|
*
|
||||||
|
* @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" is context frozen$/
|
||||||
|
* @throws ExpectationException if the context cannot be frozen or found
|
||||||
|
* @param string $element Element we look on
|
||||||
|
* @param string $selector The type of where we look (activity, course)
|
||||||
|
*/
|
||||||
|
public function the_context_is_context_frozen(string $element, string $selector) {
|
||||||
|
|
||||||
|
// Enable context freeze if it is not done yet.
|
||||||
|
set_config('contextlocking', 1);
|
||||||
|
|
||||||
|
// Find context.
|
||||||
|
$context = self::get_context($selector, $element);
|
||||||
|
|
||||||
|
// Freeze context.
|
||||||
|
$context->set_locked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmark context as frozen.
|
||||||
|
*
|
||||||
|
* @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" is not context frozen$/
|
||||||
|
* @throws ExpectationException if the context cannot be frozen or found
|
||||||
|
* @param string $element Element we look on
|
||||||
|
* @param string $selector The type of where we look (activity, course)
|
||||||
|
*/
|
||||||
|
public function the_context_is_not_context_frozen(string $element, string $selector) {
|
||||||
|
|
||||||
|
// Enable context freeze if it is not done yet.
|
||||||
|
set_config('contextlocking', 1);
|
||||||
|
|
||||||
|
// Find context.
|
||||||
|
$context = self::get_context($selector, $element);
|
||||||
|
|
||||||
|
// Freeze context.
|
||||||
|
$context->set_locked(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ class get_user_attempts extends external_api {
|
||||||
|
|
||||||
$coursecontext = \context_course::instance($course->id);
|
$coursecontext = \context_course::instance($course->id);
|
||||||
|
|
||||||
$users = get_enrolled_users($coursecontext, '', 0, 'u.id, u.firstname, u.lastname',
|
$users = self::get_active_users($manager, 'u.id, u.firstname, u.lastname',
|
||||||
$sortorder, $page * $perpage, $perpage);
|
$sortorder, $page * $perpage, $perpage);
|
||||||
|
|
||||||
$usersattempts = [];
|
$usersattempts = [];
|
||||||
|
@ -160,6 +160,39 @@ class get_user_attempts extends external_api {
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the active users list
|
||||||
|
*
|
||||||
|
* @param manager $manager the h5pactivity manager
|
||||||
|
* @param string $userfields the user fields to get
|
||||||
|
* @param string $sortorder the SQL sortorder
|
||||||
|
* @param int $limitfrom SQL limit from
|
||||||
|
* @param int $limitnum SQL limit num
|
||||||
|
*/
|
||||||
|
private static function get_active_users(
|
||||||
|
manager $manager,
|
||||||
|
string $userfields = 'u.*',
|
||||||
|
string $sortorder = null,
|
||||||
|
int $limitfrom = 0,
|
||||||
|
int $limitnum = 0
|
||||||
|
): array {
|
||||||
|
|
||||||
|
global $DB;
|
||||||
|
|
||||||
|
$capjoin = $manager->get_active_users_join(true);
|
||||||
|
|
||||||
|
// Final SQL.
|
||||||
|
$sql = "SELECT DISTINCT {$userfields}
|
||||||
|
FROM {user} u {$capjoin->joins}
|
||||||
|
WHERE {$capjoin->wheres}";
|
||||||
|
|
||||||
|
if (!empty($sortorder)) {
|
||||||
|
$sql .= " ORDER BY {$sortorder}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $DB->get_records_sql($sql, $capjoin->params, $limitfrom, $limitnum);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export attempts data for a specific user.
|
* Export attempts data for a specific user.
|
||||||
*
|
*
|
||||||
|
|
|
@ -33,6 +33,7 @@ use cm_info;
|
||||||
use moodle_recordset;
|
use moodle_recordset;
|
||||||
use core_user;
|
use core_user;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
use core\dml\sql_join;
|
||||||
use mod_h5pactivity\event\course_module_viewed;
|
use mod_h5pactivity\event\course_module_viewed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -288,21 +289,88 @@ class manager {
|
||||||
/**
|
/**
|
||||||
* Count the activity completed attempts.
|
* Count the activity completed attempts.
|
||||||
*
|
*
|
||||||
* If no user is provided will count all activity attempts.
|
* If no user is provided the method will count all active users attempts.
|
||||||
|
* Check get_active_users_join PHPdoc to a more detailed description of "active users".
|
||||||
*
|
*
|
||||||
* @param int|null $userid optional user id (default null)
|
* @param int|null $userid optional user id (default null)
|
||||||
* @return int the total amount of attempts
|
* @return int the total amount of attempts
|
||||||
*/
|
*/
|
||||||
public function count_attempts(int $userid = null): int {
|
public function count_attempts(int $userid = null): int {
|
||||||
global $DB;
|
global $DB;
|
||||||
$params = [
|
|
||||||
'h5pactivityid' => $this->instance->id,
|
// Counting records is enough for one user.
|
||||||
'completion' => 1
|
|
||||||
];
|
|
||||||
if ($userid) {
|
if ($userid) {
|
||||||
$params['userid'] = $userid;
|
$params['userid'] = $userid;
|
||||||
|
$params = [
|
||||||
|
'h5pactivityid' => $this->instance->id,
|
||||||
|
'userid' => $userid,
|
||||||
|
'completion' => 1,
|
||||||
|
];
|
||||||
|
return $DB->count_records('h5pactivity_attempts', $params);
|
||||||
}
|
}
|
||||||
return $DB->count_records('h5pactivity_attempts', $params);
|
|
||||||
|
$usersjoin = $this->get_active_users_join();
|
||||||
|
|
||||||
|
// Final SQL.
|
||||||
|
return $DB->count_records_sql(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM {user} u $usersjoin->joins
|
||||||
|
WHERE $usersjoin->wheres",
|
||||||
|
array_merge($usersjoin->params)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the join to collect all activity active users.
|
||||||
|
*
|
||||||
|
* The concept of active user is relative to the activity permissions. All users with
|
||||||
|
* "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
|
||||||
|
* are evaluators and they don't count as valid submitters.
|
||||||
|
*
|
||||||
|
* Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
|
||||||
|
* but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
|
||||||
|
*
|
||||||
|
* @since Moodle 3.11
|
||||||
|
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
|
||||||
|
* @return sql_join the active users attempts join
|
||||||
|
*/
|
||||||
|
public function get_active_users_join (bool $allpotentialusers = false): sql_join {
|
||||||
|
|
||||||
|
// Only valid users counts. By default, all users with submit capability are considered potential ones.
|
||||||
|
$context = $this->get_context();
|
||||||
|
|
||||||
|
// We want to present all potential users.
|
||||||
|
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view');
|
||||||
|
|
||||||
|
if ($capjoin->cannotmatchanyrows) {
|
||||||
|
return $capjoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// But excluding all reviewattempts users converting a capabilities join into left join.
|
||||||
|
$reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
|
||||||
|
|
||||||
|
$capjoin = new sql_join(
|
||||||
|
$capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
|
||||||
|
$capjoin->wheres . " AND reviewer.userid IS NULL",
|
||||||
|
$capjoin->params
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($allpotentialusers) {
|
||||||
|
return $capjoin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attempts join.
|
||||||
|
$where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
|
||||||
|
$params = [
|
||||||
|
'h5pactivityid' => $this->instance->id,
|
||||||
|
'completion' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
return new sql_join(
|
||||||
|
$capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
|
||||||
|
$capjoin->wheres . " AND $where",
|
||||||
|
array_merge($capjoin->params, $params)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -374,6 +442,12 @@ class manager {
|
||||||
*/
|
*/
|
||||||
public function get_report(int $userid = null, int $attemptid = null): ?report {
|
public function get_report(int $userid = null, int $attemptid = null): ?report {
|
||||||
global $USER;
|
global $USER;
|
||||||
|
|
||||||
|
// If tracking is disabled, no reports are available.
|
||||||
|
if (!$this->instance->enabletracking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$attempt = null;
|
$attempt = null;
|
||||||
if ($attemptid) {
|
if ($attemptid) {
|
||||||
$attempt = $this->get_attempt($attemptid);
|
$attempt = $this->get_attempt($attemptid);
|
||||||
|
@ -395,8 +469,8 @@ class manager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if that user can be tracked.
|
// Only enrolled users has reports.
|
||||||
if ($user && !$this->is_tracking_enabled($user)) {
|
if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace mod_h5pactivity\local\report;
|
||||||
use mod_h5pactivity\local\report;
|
use mod_h5pactivity\local\report;
|
||||||
use mod_h5pactivity\local\manager;
|
use mod_h5pactivity\local\manager;
|
||||||
use mod_h5pactivity\local\attempt;
|
use mod_h5pactivity\local\attempt;
|
||||||
|
use core\dml\sql_join;
|
||||||
use table_sql;
|
use table_sql;
|
||||||
use moodle_url;
|
use moodle_url;
|
||||||
use html_writer;
|
use html_writer;
|
||||||
|
@ -82,8 +83,9 @@ class participants extends table_sql implements report {
|
||||||
$this->no_sorting('attempts');
|
$this->no_sorting('attempts');
|
||||||
$this->pageable(true);
|
$this->pageable(true);
|
||||||
|
|
||||||
// Set query SQL.
|
$capjoin = $this->manager->get_active_users_join(true);
|
||||||
$capjoin = get_enrolled_with_capabilities_join($this->manager->get_context(), '', 'mod/h5pactivity:submit');
|
|
||||||
|
// Final SQL.
|
||||||
$this->set_sql(
|
$this->set_sql(
|
||||||
'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic,
|
'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic,
|
||||||
u.middlename, u.alternatename, u.imagealt, u.email',
|
u.middlename, u.alternatename, u.imagealt, u.email',
|
||||||
|
|
100
mod/h5pactivity/tests/behat/locking.feature
Normal file
100
mod/h5pactivity/tests/behat/locking.feature
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
@mod @mod_h5pactivity @core_h5p
|
||||||
|
Feature: Add H5P activity context locking
|
||||||
|
In order to let users access a H5P attempts
|
||||||
|
As a user
|
||||||
|
I need to access attempts reports even if no more users can submit attempts
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following config values are set as admin:
|
||||||
|
| contextlocking | 1 |
|
||||||
|
And the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||||
|
| student1 | Student | 1 | student1@example.com |
|
||||||
|
| student2 | Student | 2 | student2@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category |
|
||||||
|
| Course 1 | C1 | 0 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| teacher1 | C1 | editingteacher |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
| student2 | C1 | student |
|
||||||
|
# This test is only about reporting, we don't need to specify any valid H5P file for it.
|
||||||
|
And the following "activity" exists:
|
||||||
|
| activity | h5pactivity |
|
||||||
|
| name | H5P package |
|
||||||
|
| intro | Test H5P description |
|
||||||
|
| course | C1 |
|
||||||
|
| idnumber | h5ppackage |
|
||||||
|
And the following "mod_h5pactivity > attempt" exists:
|
||||||
|
| user | student1 |
|
||||||
|
| h5pactivity | H5P package |
|
||||||
|
| attempt | 1 |
|
||||||
|
| interactiontype | compound |
|
||||||
|
| rawscore | 2 |
|
||||||
|
| maxscore | 2 |
|
||||||
|
| duration | 4 |
|
||||||
|
| completion | 1 |
|
||||||
|
| success | 1 |
|
||||||
|
|
||||||
|
Scenario: Access participants report on a freeze context
|
||||||
|
Given the "h5ppackage" "Activity module" is context frozen
|
||||||
|
And I am on the "C1" "Course" page logged in as "admin"
|
||||||
|
And I follow "H5P package"
|
||||||
|
When I follow "View all attempts (1 submitted)"
|
||||||
|
Then I should see "Student 1"
|
||||||
|
And I should see "View user attempts (1)" in the "Student 1" "table_row"
|
||||||
|
And I should see "Student 2"
|
||||||
|
And I should not see "Teacher 1"
|
||||||
|
|
||||||
|
Scenario: Access own attempts on a freeze context
|
||||||
|
Given the "h5ppackage" "Activity module" is context frozen
|
||||||
|
When I am on the "C1" "Course" page logged in as "student1"
|
||||||
|
And I follow "H5P package"
|
||||||
|
When I follow "View my attempts"
|
||||||
|
And I follow "View report"
|
||||||
|
Then I should see "Attempt #1: Student 1"
|
||||||
|
And I should see "This attempt is completed"
|
||||||
|
|
||||||
|
Scenario: Access participants report without any user with submit capability
|
||||||
|
Given the following "permission overrides" exist:
|
||||||
|
| capability | permission | role | contextlevel | reference |
|
||||||
|
| mod/h5pactivity:submit | Prohibit | student | System | |
|
||||||
|
And I am on the "C1" "Course" page logged in as "admin"
|
||||||
|
And I follow "H5P package"
|
||||||
|
When I follow "View all attempts (1 submitted)"
|
||||||
|
Then I should see "Student 1"
|
||||||
|
And I should see "View user attempts (1)" in the "Student 1" "table_row"
|
||||||
|
And I should see "Student 2"
|
||||||
|
And I should not see "Teacher 1"
|
||||||
|
|
||||||
|
Scenario: Access participant report to list students with submit capability but no view one
|
||||||
|
Given the following "permission overrides" exist:
|
||||||
|
| capability | permission | role | contextlevel | reference |
|
||||||
|
| mod/h5pactivity:view | Prohibit | student | System | |
|
||||||
|
When I am on the "C1" "Course" page logged in as "admin"
|
||||||
|
And I follow "H5P package"
|
||||||
|
When I follow "View all attempts (0 submitted)"
|
||||||
|
Then I should see "No participants to display"
|
||||||
|
|
||||||
|
Scenario: Access participant report but with no users with view or submit capability
|
||||||
|
Given the following "permission overrides" exist:
|
||||||
|
| capability | permission | role | contextlevel | reference |
|
||||||
|
| mod/h5pactivity:submit | Prohibit | student | System | |
|
||||||
|
| mod/h5pactivity:view | Prohibit | student | System | |
|
||||||
|
When I am on the "C1" "Course" page logged in as "admin"
|
||||||
|
And I follow "H5P package"
|
||||||
|
When I follow "View all attempts (0 submitted)"
|
||||||
|
Then I should see "No participants to display"
|
||||||
|
|
||||||
|
Scenario: Access participant report in a hidden activity
|
||||||
|
Given I log in as "admin"
|
||||||
|
And I am on "Course 1" course homepage with editing mode on
|
||||||
|
And I click on "Hide" "link" in the "H5P package" activity
|
||||||
|
When I follow "H5P package"
|
||||||
|
And I follow "View all attempts (1 submitted)"
|
||||||
|
Then I should see "Student 1"
|
||||||
|
And I should see "View user attempts (1)"
|
||||||
|
And I should see "Student 2"
|
||||||
|
And I should not see "Teacher 1"
|
|
@ -468,7 +468,7 @@ class get_attempts_testcase extends externallib_advanced_testcase {
|
||||||
'editingteacher', ['student1', 'noattempts'], [], ['student1', 'noattempts']
|
'editingteacher', ['student1', 'noattempts'], [], ['student1', 'noattempts']
|
||||||
],
|
],
|
||||||
'Teacher checking no students' => [
|
'Teacher checking no students' => [
|
||||||
'editingteacher', [], ['editingteacher'], []
|
'editingteacher', [], [], ['editingteacher']
|
||||||
],
|
],
|
||||||
'Teacher checking one student and a no enrolled user' => [
|
'Teacher checking one student and a no enrolled user' => [
|
||||||
'editingteacher', ['student1', 'noenrolled'], ['noenrolled'], ['student1']
|
'editingteacher', ['student1', 'noenrolled'], ['noenrolled'], ['student1']
|
||||||
|
|
|
@ -151,25 +151,25 @@ class get_user_attempts_testcase extends externallib_advanced_testcase {
|
||||||
'Teacher checking students with attempts' => [
|
'Teacher checking students with attempts' => [
|
||||||
'editingteacher',
|
'editingteacher',
|
||||||
['student1', 'student2', 'student3', 'student4', 'student5'],
|
['student1', 'student2', 'student3', 'student4', 'student5'],
|
||||||
['editingteacher'],
|
[],
|
||||||
['student1', 'student2', 'student3', 'student4', 'student5'],
|
['student1', 'student2', 'student3', 'student4', 'student5'],
|
||||||
],
|
],
|
||||||
'Teacher checking 2 students with atempts and one not' => [
|
'Teacher checking 2 students with atempts and one not' => [
|
||||||
'editingteacher',
|
'editingteacher',
|
||||||
['student1', 'student2', 'noattempts'],
|
['student1', 'student2', 'noattempts'],
|
||||||
['editingteacher'],
|
[],
|
||||||
['student1', 'student2', 'noattempts'],
|
['student1', 'student2', 'noattempts'],
|
||||||
],
|
],
|
||||||
'Teacher checking no students' => [
|
'Teacher checking no students' => [
|
||||||
'editingteacher',
|
'editingteacher',
|
||||||
[],
|
[],
|
||||||
['editingteacher'],
|
[],
|
||||||
[],
|
[],
|
||||||
],
|
],
|
||||||
'Teacher checking one student and a no enrolled user' => [
|
'Teacher checking one student and a no enrolled user' => [
|
||||||
'editingteacher',
|
'editingteacher',
|
||||||
['student1', 'noenrolled'],
|
['student1', 'noenrolled'],
|
||||||
['editingteacher'],
|
[],
|
||||||
['student1'],
|
['student1'],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -530,7 +530,7 @@ class manager_testcase extends \advanced_testcase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test static count_attempts.
|
* Test static count_attempts of one user.
|
||||||
*/
|
*/
|
||||||
public function test_count_attempts() {
|
public function test_count_attempts() {
|
||||||
|
|
||||||
|
@ -560,6 +560,138 @@ class manager_testcase extends \advanced_testcase {
|
||||||
$this->assertEquals(3, $manager->count_attempts($user3->id));
|
$this->assertEquals(3, $manager->count_attempts($user3->id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test static count_attempts of all active participants.
|
||||||
|
*
|
||||||
|
* @dataProvider count_attempts_all_data
|
||||||
|
* @param bool $canview if the student role has mod_h5pactivity/view capability
|
||||||
|
* @param bool $cansubmit if the student role has mod_h5pactivity/submit capability
|
||||||
|
* @param bool $extrarole if an extra role without submit capability is required
|
||||||
|
* @param int $result the expected result
|
||||||
|
*/
|
||||||
|
public function test_count_attempts_all(bool $canview, bool $cansubmit, bool $extrarole, int $result) {
|
||||||
|
global $DB;
|
||||||
|
|
||||||
|
$this->resetAfterTest();
|
||||||
|
$this->setAdminUser();
|
||||||
|
|
||||||
|
$course = $this->getDataGenerator()->create_course();
|
||||||
|
$activity = $this->getDataGenerator()->create_module(
|
||||||
|
'h5pactivity',
|
||||||
|
['course' => $course]
|
||||||
|
);
|
||||||
|
|
||||||
|
$manager = manager::create_from_instance($activity);
|
||||||
|
|
||||||
|
$roleid = $DB->get_field('role', 'id', ['shortname' => 'student']);
|
||||||
|
|
||||||
|
$newcap = ($canview) ? CAP_ALLOW : CAP_PROHIBIT;
|
||||||
|
role_change_permission($roleid, $manager->get_context(), 'mod/h5pactivity:view', $newcap);
|
||||||
|
|
||||||
|
$newcap = ($cansubmit) ? CAP_ALLOW : CAP_PROHIBIT;
|
||||||
|
role_change_permission($roleid, $manager->get_context(), 'mod/h5pactivity:submit', $newcap);
|
||||||
|
|
||||||
|
// Teacher with review capability and attempts (should not be listed).
|
||||||
|
if ($extrarole) {
|
||||||
|
$user1 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||||
|
$this->generate_fake_attempts($activity, $user1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Student with attempts.
|
||||||
|
$user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$this->generate_fake_attempts($activity, $user2, 1);
|
||||||
|
|
||||||
|
// Another student with attempts.
|
||||||
|
$user3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$this->generate_fake_attempts($activity, $user3, 1);
|
||||||
|
|
||||||
|
$this->assertEquals($result, $manager->count_attempts());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for test_count_attempts_all.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function count_attempts_all_data(): array {
|
||||||
|
return [
|
||||||
|
'Students with both view and submit capability' => [true, true, false, 6],
|
||||||
|
'Students without view but with submit capability' => [false, true, false, 0],
|
||||||
|
'Students with view but without submit capability' => [true, false, false, 6],
|
||||||
|
'Students without both view and submit capability' => [false, false, false, 0],
|
||||||
|
'Students with both view and submit capability and extra role' => [true, true, true, 6],
|
||||||
|
'Students without view but with submit capability and extra role' => [false, true, true, 0],
|
||||||
|
'Students with view but without submit capability and extra role' => [true, false, true, 6],
|
||||||
|
'Students without both view and submit capability and extra role' => [false, false, true, 0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test static count_attempts of all active participants.
|
||||||
|
*
|
||||||
|
* Most method scenarios are tested in test_count_attempts_all so we only
|
||||||
|
* need to test the with $allpotentialusers true and false.
|
||||||
|
*
|
||||||
|
* @dataProvider get_active_users_join_data
|
||||||
|
* @param bool $allpotentialusers if the join should return all potential users or only the submitted ones.
|
||||||
|
* @param int $result the expected result
|
||||||
|
*/
|
||||||
|
public function test_get_active_users_join(bool $allpotentialusers, int $result) {
|
||||||
|
global $DB;
|
||||||
|
|
||||||
|
$this->resetAfterTest();
|
||||||
|
$this->setAdminUser();
|
||||||
|
|
||||||
|
$course = $this->getDataGenerator()->create_course();
|
||||||
|
$activity = $this->getDataGenerator()->create_module(
|
||||||
|
'h5pactivity',
|
||||||
|
['course' => $course]
|
||||||
|
);
|
||||||
|
|
||||||
|
$manager = manager::create_from_instance($activity);
|
||||||
|
|
||||||
|
$user1 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||||
|
$this->generate_fake_attempts($activity, $user1, 1);
|
||||||
|
|
||||||
|
// Student with attempts.
|
||||||
|
$user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$this->generate_fake_attempts($activity, $user2, 1);
|
||||||
|
|
||||||
|
// 2 more students without attempts.
|
||||||
|
$this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
|
||||||
|
$usersjoin = $manager->get_active_users_join($allpotentialusers);
|
||||||
|
|
||||||
|
// Final SQL.
|
||||||
|
$num = $DB->count_records_sql(
|
||||||
|
"SELECT COUNT(DISTINCT u.id)
|
||||||
|
FROM {user} u $usersjoin->joins
|
||||||
|
WHERE $usersjoin->wheres",
|
||||||
|
array_merge($usersjoin->params)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($result, $num);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data provider for test_get_active_users_join.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_active_users_join_data(): array {
|
||||||
|
return [
|
||||||
|
'All potential users' => [
|
||||||
|
'allpotentialusers' => true,
|
||||||
|
'result' => 3,
|
||||||
|
],
|
||||||
|
'Users with attempts' => [
|
||||||
|
'allpotentialusers' => false,
|
||||||
|
'result' => 1,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test static count_attempts.
|
* Test static count_attempts.
|
||||||
*/
|
*/
|
||||||
|
|
7
mod/h5pactivity/upgrade.txt
Normal file
7
mod/h5pactivity/upgrade.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
This files describes API changes in /mod/h5pactivity/*,
|
||||||
|
information provided here is intended especially for developers.
|
||||||
|
|
||||||
|
=== 3.11 ===
|
||||||
|
|
||||||
|
* Added mod_h5pactivity\local\manager::get_active_users_join method to query all active
|
||||||
|
users from a specific activity, even in a freeze context.
|
Loading…
Add table
Add a link
Reference in a new issue