Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a1d59ef61 | |||
| a2c785e795 | |||
| 9badb9eab5 | |||
| c142ad2370 | |||
| 6ebae3663e | |||
| 39c6bfbfdb | |||
| bcd17420a8 | |||
| 8fcbfe5218 | |||
| e1c1ff4be3 | |||
| 031220a009 | |||
| acb2ce417f | |||
| 89ec99bfa3 | |||
| a30aece8ad | |||
| 2d08bba8ab | |||
| 8c7fcbf590 | |||
| ea2f0af064 | |||
| 1eae36faf6 | |||
| fa60800431 | |||
| e564206e33 | |||
| c046a86fa5 | |||
| c02f5c7391 | |||
| aa335a7aba | |||
| 857507ce09 | |||
| f14576473d | |||
| 475702b9ef | |||
| 9be29c8ded | |||
| f574806a9a | |||
| 8622c37ecc | |||
| 0a4c70d7c2 | |||
| decc44fb26 | |||
| 151fd4caf9 | |||
| ca1da020ee | |||
| 4756608dbe | |||
| 8176483e0a | |||
| 4fe25d925f | |||
| f1a34c2ea0 | |||
| b611b74e33 | |||
| 6f6d548cfa | |||
| ed0f8f1de4 | |||
| 09b8347175 | |||
| ab65333718 |
@@ -7,6 +7,12 @@ on:
|
||||
- MOODLE_[0-9]+_STABLE
|
||||
tags-ignore:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
phpunit_extra_options:
|
||||
description: Additional options to apply to PHPUnit
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
php: 7.4
|
||||
@@ -109,4 +115,4 @@ jobs:
|
||||
- name: Running PHPUnit tests
|
||||
env:
|
||||
dbtype: ${{ matrix.db }}
|
||||
run: vendor/bin/phpunit -v
|
||||
run: vendor/bin/phpunit -v ${{ inputs.phpunit_extra_options }}
|
||||
|
||||
@@ -4305,4 +4305,195 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.4" requires="4.1.2">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.6.7" />
|
||||
<VENDOR name="mysql" version="8.0" />
|
||||
<VENDOR name="postgres" version="13" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -75,7 +75,7 @@ trait reader {
|
||||
*/
|
||||
public static function decode_other(?string $other) {
|
||||
if ($other === 'N;' || preg_match('~^.:~', $other)) {
|
||||
return unserialize($other);
|
||||
return unserialize($other, ['allowed_classes' => [stdClass::class]]);
|
||||
} else {
|
||||
return json_decode($other, true);
|
||||
}
|
||||
|
||||
@@ -37,3 +37,13 @@ function tool_task_status_checks() : array {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to handle mtrace by outputting the text to normal browser window.
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param string $eol End of line character
|
||||
*/
|
||||
function tool_task_mtrace_wrapper(string $message, string $eol): void {
|
||||
$message = s($message);
|
||||
echo $message . $eol;
|
||||
}
|
||||
|
||||
@@ -30,16 +30,6 @@ require('../../../config.php');
|
||||
|
||||
require_once($CFG->libdir.'/cronlib.php');
|
||||
|
||||
/**
|
||||
* Function used to handle mtrace by outputting the text to normal browser window.
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param string $eol End of line character
|
||||
*/
|
||||
function tool_task_mtrace_wrapper($message, $eol) {
|
||||
echo s($message . $eol);
|
||||
}
|
||||
|
||||
// Allow execution of single task. This requires login and has different rules.
|
||||
$taskname = required_param('task', PARAM_RAW_TRIMMED);
|
||||
|
||||
@@ -85,6 +75,7 @@ require_sesskey();
|
||||
|
||||
// Prepare to handle output via mtrace.
|
||||
echo html_writer::start_tag('pre');
|
||||
require('lib.php');
|
||||
$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
|
||||
|
||||
// Run the specified task (this will output an error if it doesn't exist).
|
||||
|
||||
@@ -1518,7 +1518,13 @@ class backup_block_instance_structure_step extends backup_structure_step {
|
||||
// Transform configdata information if needed (process links and friends)
|
||||
$blockrec = $DB->get_record('block_instances', array('id' => $this->task->get_blockid()));
|
||||
if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
|
||||
$configdata = (array)unserialize(base64_decode($blockrec->configdata));
|
||||
$configdata = array_filter(
|
||||
(array) unserialize_object(base64_decode($blockrec->configdata)),
|
||||
static function($value): bool {
|
||||
return !($value instanceof __PHP_Incomplete_Class);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($configdata as $attribute => $value) {
|
||||
if (in_array($attribute, $attrstotransform)) {
|
||||
$configdata[$attribute] = $this->contenttransformer->process($value);
|
||||
|
||||
@@ -4196,7 +4196,13 @@ class restore_block_instance_structure_step extends restore_structure_step {
|
||||
// Let's look for anything within configdata neededing processing
|
||||
// (nulls and uses of legacy file.php)
|
||||
if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
|
||||
$configdata = (array) unserialize_object(base64_decode($data->configdata));
|
||||
$configdata = array_filter(
|
||||
(array) unserialize_object(base64_decode($data->configdata)),
|
||||
static function($value): bool {
|
||||
return !($value instanceof __PHP_Incomplete_Class);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($configdata as $attribute => $value) {
|
||||
if (in_array($attribute, $attrstotransform)) {
|
||||
$configdata[$attribute] = $this->contentprocessor->process_cdata($value);
|
||||
|
||||
@@ -54,6 +54,8 @@ $badge = new badge($badgeid);
|
||||
$context = $badge->get_context();
|
||||
$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
|
||||
|
||||
require_capability('moodle/badges:viewawarded', $context);
|
||||
|
||||
if ($badge->type == BADGE_TYPE_COURSE) {
|
||||
if (empty($CFG->badges_allowcoursebadges)) {
|
||||
print_error('coursebadgesdisabled', 'badges');
|
||||
|
||||
@@ -1122,6 +1122,43 @@ class core_enrollib_testcase extends advanced_testcase {
|
||||
$this->assertCount(1, enrol_get_course_users($course1->id, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* test_course_users in groups
|
||||
*
|
||||
* @covers \enrol_get_course_users()
|
||||
* @return void
|
||||
*/
|
||||
public function test_course_users_in_groups() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course->id);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $user1->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $user1->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $user2->id]);
|
||||
|
||||
$this->assertCount(3, enrol_get_course_users($course->id));
|
||||
$this->assertCount(1, enrol_get_course_users($course->id, false, [], [], [$group1->id]));
|
||||
$this->assertCount(2, enrol_get_course_users($course->id, false, [], [], [$group2->id]));
|
||||
|
||||
$instances = enrol_get_instances($course->id, true);
|
||||
$manualinstance = reset($instances);
|
||||
|
||||
$manualplugin = enrol_get_plugin('manual');
|
||||
$manualplugin->update_user_enrol($manualinstance, $user1->id, ENROL_USER_SUSPENDED);
|
||||
$this->assertCount(2, enrol_get_course_users($course->id, false, [], [], [$group2->id]));
|
||||
$this->assertCount(1, enrol_get_course_users($course->id, true, [], [], [$group2->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test count of enrolled users
|
||||
*
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Teacher can delete marking guide
|
||||
As a teacher,
|
||||
I should be able to delete a marking guide
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Marking guide 1 |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Criterion 1 | Criterion 1 description for student | Criterion 1 description for markers | 100 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
|
||||
Scenario: Delete a marking guide
|
||||
Given I am on the "Course 1" course page
|
||||
And I go to "Assign 1" advanced grading page
|
||||
When I click on "Delete the currently defined form" "link"
|
||||
And I press "Cancel"
|
||||
# Confirm that marking guide was not deleted if Cancel is pressed
|
||||
Then I should see "Marking guide 1 Ready for use"
|
||||
And I should see "Criterion 1"
|
||||
And I click on "Delete the currently defined form" "link"
|
||||
And I press "Continue"
|
||||
# Confirm that marking guide was deleted successfully if Continue is pressed
|
||||
And I should see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
|
||||
And I should not see "Marking guide 1 Ready for use"
|
||||
And I should not see "Criterion 1"
|
||||
@@ -43,9 +43,9 @@ Feature: Site settings can be used to hide parts of the gradebook UI
|
||||
|
||||
@javascript
|
||||
Scenario: Disable category overriding
|
||||
And "tr .course input[type='text']" "css_element" should exist
|
||||
Then I navigate to "Grades > Grade category settings" in site administration
|
||||
Given "Student 1 Course total" "field" should exist
|
||||
And I navigate to "Grades > Grade category settings" in site administration
|
||||
And I set the field "Allow category grades to be manually overridden" to "0"
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" "grades > Grader report > View" page
|
||||
And "tr .course input[type='text']" "css_element" should not exist
|
||||
When I am on the "Course 1" "grades > Grader report > View" page
|
||||
Then "Student 1 Course total" "field" should not exist
|
||||
|
||||
@@ -19,50 +19,23 @@ Feature: We can use a minimum grade different than zero
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "grade categories" exist:
|
||||
| fullname | course |
|
||||
| Sub category 1 | C1 |
|
||||
| Sub category 2 | C1 |
|
||||
| fullname | course | aggregateonlygraded |
|
||||
| Sub category 1 | C1 | 0 |
|
||||
| Sub category 2 | C1 | 0 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | course |
|
||||
| Manual item 1 | -100 | C1 |
|
||||
| Manual item 2 | 50 | C1 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | grademax | course | gradecategory |
|
||||
| Manual item 3 | -100 | 50 | C1 | Sub category 1 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | course | gradecategory |
|
||||
| Manual item 4 | -100 | C1 | Sub category 1 |
|
||||
| Manual item 5 | 50 | C1 | Sub category 2 |
|
||||
| Manual item 6 | 50 | C1 | Sub category 2 |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
|
||||
And I am on the "Course 1" "grades > gradebook setup" page
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 1 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Course 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 2 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Course 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 3 |
|
||||
| Maximum grade | 50 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Sub category 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 4 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Sub category 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 5 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Sub category 2 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 6 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Sub category 2 |
|
||||
And I press "Save changes"
|
||||
And I navigate to "Setup > Course grade settings" in the course gradebook
|
||||
And I set the field "Show weightings" to "Show"
|
||||
And I set the field "Show contribution to course total" to "Show"
|
||||
@@ -70,13 +43,7 @@ Feature: We can use a minimum grade different than zero
|
||||
|
||||
@javascript
|
||||
Scenario: Natural aggregation with negative and positive grade
|
||||
And I navigate to "Setup > Gradebook setup" in the course gradebook
|
||||
And I set the following settings for grade item "Sub category 1":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
And I set the following settings for grade item "Sub category 2":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
Given I navigate to "Setup > Gradebook setup" in the course gradebook
|
||||
And I set the following settings for grade item "Course 1":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
|
||||
@@ -965,9 +965,38 @@ class manager {
|
||||
$command = "{$phpbinary} {$scriptpath} {$taskarg}";
|
||||
|
||||
// Execute it.
|
||||
passthru($command);
|
||||
self::passthru_via_mtrace($command);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This behaves similar to passthru but filters every line via
|
||||
* the mtrace function so it can be post processed.
|
||||
*
|
||||
* @param string $command to run
|
||||
* @return void
|
||||
*/
|
||||
public static function passthru_via_mtrace(string $command) {
|
||||
$descriptorspec = [
|
||||
0 => ['pipe', 'r'], // STDIN.
|
||||
1 => ['pipe', 'w'], // STDOUT.
|
||||
2 => ['pipe', 'w'], // STDERR.
|
||||
];
|
||||
flush();
|
||||
$process = proc_open($command, $descriptorspec, $pipes, realpath('./'), []);
|
||||
if (is_resource($process)) {
|
||||
while ($s = fgets($pipes[1])) {
|
||||
mtrace($s, '');
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
proc_close($process);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
-1
@@ -1740,9 +1740,11 @@ function enrol_get_course_by_user_enrolment_id($ueid) {
|
||||
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
|
||||
* @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
|
||||
* @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
|
||||
* @param array $usergroups Limit the results of users to the ones that belong to one of the submitted group ids.
|
||||
* @return stdClass[]
|
||||
*/
|
||||
function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
|
||||
function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = [], $uefilter = [],
|
||||
$usergroups = []) {
|
||||
global $DB;
|
||||
|
||||
if (!$courseid && !$usersfilter && !$uefilter) {
|
||||
@@ -1785,6 +1787,16 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
|
||||
$params = $params + $ueparams;
|
||||
}
|
||||
|
||||
// Only select enrolled users that belong to a specific group(s).
|
||||
if (!empty($usergroups)) {
|
||||
$usergroups = array_map(function ($item) { // Sanitize groupid to int to be save for sql.
|
||||
return (int)$item;
|
||||
}, $usergroups);
|
||||
list($ugsql, $ugparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
|
||||
$conditions[] = 'ue.userid IN (SELECT userid FROM {groups_members} WHERE groupid ' . $ugsql . ')';
|
||||
$params = $params + $ugparams;
|
||||
}
|
||||
|
||||
return $DB->get_records_sql($sql . ' ' . implode(' AND ', $conditions), $params);
|
||||
}
|
||||
|
||||
|
||||
@@ -643,20 +643,19 @@ abstract class testing_util {
|
||||
$borkedmysql = false;
|
||||
if ($DB->get_dbfamily() === 'mysql') {
|
||||
$version = $DB->get_server_info();
|
||||
if (version_compare($version['version'], '5.6.0') == 1 and version_compare($version['version'], '5.6.16') == -1) {
|
||||
// Everything that comes from Oracle is evil!
|
||||
//
|
||||
// See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
|
||||
// You cannot reset the counter to a value less than or equal to to the value that is currently in use.
|
||||
//
|
||||
// From 5.6.16 release notes:
|
||||
// InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
|
||||
// (Bug #17250787, Bug #69882)
|
||||
// Everything that comes from Oracle is evil!
|
||||
//
|
||||
// See http://dev.mysql.com/doc/refman/5.6/en/alter-table.html
|
||||
// You cannot reset the counter to a value less than or equal to to the value that is currently in use.
|
||||
//
|
||||
// From 5.6.16 release notes:
|
||||
// InnoDB: The ALTER TABLE INPLACE algorithm would fail to decrease the auto-increment value.
|
||||
// (Bug #17250787, Bug #69882)
|
||||
if (version_compare($version['version'], '5.6.0', '>=') && version_compare($version['version'], '5.6.16', '<')) {
|
||||
$borkedmysql = true;
|
||||
}
|
||||
|
||||
} else if (version_compare($version['version'], '10.0.0') == 1) {
|
||||
// And MariaDB is no better!
|
||||
// Let's hope they pick the patch sometime later...
|
||||
if (version_compare($version['version'], '5.7.0', '>=') && version_compare($version['version'], '5.7.4', '<')) {
|
||||
$borkedmysql = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core\testing;
|
||||
|
||||
/**
|
||||
* Testing util tests.
|
||||
*
|
||||
* @package core
|
||||
* @category phpunit
|
||||
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class util_test extends \advanced_testcase {
|
||||
/**
|
||||
* Note: This test is required for the other two parts because the first time
|
||||
* a table is written to it may not have had the initial value reset.
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
public function test_increment_reset_part_one(): void {
|
||||
global $DB;
|
||||
|
||||
switch ($DB->get_dbfamily()) {
|
||||
case 'mssql':
|
||||
$this->markTestSkipped('MSSQL does not support sequences');
|
||||
return;
|
||||
case 'mysql':
|
||||
$version = $DB->get_server_info();
|
||||
if (version_compare($version['version'], '5.7.4', '<')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetAfterTest();
|
||||
$DB->insert_record('config_plugins', [
|
||||
'plugin' => 'example',
|
||||
'name' => 'test_increment',
|
||||
'value' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversNothing
|
||||
* @depends test_increment_reset_part_one
|
||||
*/
|
||||
public function test_increment_reset_part_two(): int {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
return $DB->insert_record('config_plugins', [
|
||||
'plugin' => 'example',
|
||||
'name' => 'test_increment',
|
||||
'value' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends test_increment_reset_part_two
|
||||
*/
|
||||
public function test_increment_reset_part_three(int $previousid): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$id = $DB->insert_record('config_plugins', [
|
||||
'plugin' => 'example',
|
||||
'name' => 'test_increment',
|
||||
'value' => 0,
|
||||
]);
|
||||
$this->assertEquals($previousid, $id);
|
||||
}
|
||||
}
|
||||
@@ -762,7 +762,7 @@ class core_component_testcase extends advanced_testcase {
|
||||
'separators' => ['\\'],
|
||||
'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php",
|
||||
],
|
||||
'Getting a file with multiple namespaces' => [
|
||||
'Getting a file with multiple namespaces (non-existent)' => [
|
||||
'classname' => 'Nonexistant\\Namespace\\Test',
|
||||
'prefix' => "Test",
|
||||
'path' => 'test/src',
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
This files describes API changes in core libraries and APIs,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 3.9.25 ===
|
||||
* In enrollib.php, the method enrol_get_course_users() got an optional 5th parameter $usergroups that
|
||||
defaults to an empty array. Here, user group ids can be provided, to select enrolled users in a course
|
||||
that are also members of these groups.
|
||||
|
||||
=== 3.9.24 ===
|
||||
* New Behat step \behat_general::the_page_title_should_contain() has been added to allow checking of page titles. You can use this
|
||||
when writing feature files to check that the page title contains the expected string.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
@mod @mod_assign
|
||||
Feature: Teacher can enable anonymous submissions for an assignment
|
||||
In order to make an anonymous submission to an assignment
|
||||
As a teacher
|
||||
I should be able to enable anonymous submissions
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | assignsubmission_onlinetext_enabled | blindmarking |
|
||||
| assign | C1 | Assign 1 | 1 | 1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Teacher can enable anonymous submissions
|
||||
# Submit an assignment as student1
|
||||
Given the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Assign 1 | student1 | I'm the student's first submission |
|
||||
When I am on the "Assign 1" "assign activity editing" page logged in as teacher1
|
||||
# Confirm that anonymous submissions can't be changed to no anymore
|
||||
Then "blindmarking" "select" should not exist
|
||||
And I press "Cancel"
|
||||
And I am on the "Assign 1" "assign activity" page
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
# Confirm that Participant [n] is displayed instead of Student One - student name is hidden
|
||||
And I should not see "Student One" in the "Participant" "table_row"
|
||||
And I click on "Grade" "link" in the "Participant" "table_row"
|
||||
And I set the field "Grade out of 100" to "70"
|
||||
And I press "Save changes"
|
||||
And I am on the "Assign 1" "assign activity" page
|
||||
And I navigate to "View all submissions" in current page administration
|
||||
And I select "Reveal student identities" from the "Grading action" singleselect
|
||||
And I should see "Are you sure you want to reveal student identities for this assignment? This operation cannot be undone. Once the student identities have been revealed, the marks will be released to the gradebook."
|
||||
And I press "Continue"
|
||||
# Confirm that student identity is no longer hidden and grade is retained
|
||||
And I should not see "Participant" in the "Student One" "table_row"
|
||||
And I should see "70.00" in the "Student One" "table_row"
|
||||
@@ -0,0 +1,40 @@
|
||||
@mod @mod_data
|
||||
Feature: Students can view upcoming data activities in the timeline block
|
||||
In order for student to see upcoming data activities in timeline block
|
||||
As a teacher
|
||||
I should be able to set the availability dates of data activities
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Student can view upcoming data activities in the timeline block
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | id_timeavailablefrom_enabled | timeavailablefrom | id_timeavailableto_enabled | timeavailableto |
|
||||
| data | C1 | DB Past | 1 | <pastfrom> | 1 | <pastto> |
|
||||
| data | C1 | DB Future | 1 | <futurefrom> | 1 | <futureto> |
|
||||
| data | C1 | DB No date | 0 | | 0 | |
|
||||
# Confirm that student can see future but not past db activity in the timeline block
|
||||
When I log in as "student 1"
|
||||
Then I should not see "DB Past" in the "Timeline" "block"
|
||||
# Also confirm that student can't see db activity where availability is disabled
|
||||
And I should not see "DB No Date" in the "Timeline" "block"
|
||||
And I should see "DB Future" in the "Timeline" "block"
|
||||
# Confirm link works and redirects to db activity
|
||||
And I click on "DB Future" "link" in the "Timeline" "block"
|
||||
And I should see "Sorry, this activity is not available until"
|
||||
And I should see "<futurefrom>%A, %d %B %Y, %I:%M##"
|
||||
|
||||
Examples:
|
||||
| pastfrom | pastto | futurefrom | futureto |
|
||||
| ##1 month ago## | ##yesterday## | ##tomorrow## | ##tomorrow +1day## |
|
||||
| ##yesterday## | ##yesterday +3hours## | ##tomorrow noon## | ##tomorrow noon +3hours## |
|
||||
| ##6 months ago## | ##1 week ago## | ##tomorrow +5days## | ##tomorrow +6days## |
|
||||
@@ -0,0 +1,51 @@
|
||||
@mod @mod_h5pactivity @core_h5p
|
||||
Feature: Teacher can reset H5P activity grades
|
||||
As a teacher,
|
||||
I should be able to reset H5P activity grades
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
| student1 | First | Student | student1@example.com |
|
||||
| student2 | Second | Student | student2@example.com |
|
||||
| student3 | Third | Student | student3@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 |
|
||||
| student3 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | grade[modgrade_type] | grade[modgrade_point] |
|
||||
| h5pactivity | C1 | H5P Activity | point | 10 |
|
||||
|
||||
@javascript
|
||||
Scenario:Teacher can reset H5P activity grades
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I turn editing mode on
|
||||
And I give the grade "7" to the user "First Student" for the grade item "H5P Activity"
|
||||
And I give the grade "5" to the user "Second Student" for the grade item "H5P Activity"
|
||||
And I give the grade "0" to the user "Third Student" for the grade item "H5P Activity"
|
||||
And I press "Save changes"
|
||||
# Confirm that grade was sucessfully saved
|
||||
And I turn editing mode off
|
||||
And I should see "7.00" in the "First Student" "table_row"
|
||||
And I should see "5.00" in the "Second Student" "table_row"
|
||||
And I should see "0.00" in the "Third Student" "table_row"
|
||||
And I am on "Course 1" course homepage
|
||||
# Check `Delete all grades` in course reset page to reset grades
|
||||
When I navigate to "Reset" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Delete all grades | 1 |
|
||||
And I press "Reset course"
|
||||
And I press "Continue"
|
||||
# Confirm that previously saved grades are gone
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
Then I should not see "7.00" in the "First Student" "table_row"
|
||||
And I should not see "5.00" in the "Second Student" "table_row"
|
||||
And I should not see "0.00" in the "Third Student" "table_row"
|
||||
@@ -0,0 +1,60 @@
|
||||
@mod @mod_scorm
|
||||
Feature: Scorm display options
|
||||
In order to set how Scorm is displayed
|
||||
As a teacher
|
||||
I need to be able to choose from Scorm package display options
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format | activitytype |
|
||||
| Course 1 | C1 | topics | |
|
||||
| Course 2 | C2 | singleactivity | scorm |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| teacher1 | C2 | editingteacher |
|
||||
| student1 | C2 | student |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can change to various Scorm package display options
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | hidetoc | nav |
|
||||
| scorm | C1 | C1 Scorm 1 | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | <toc> | <shownavigation> |
|
||||
And I am on the "C1 Scorm 1" "scorm activity" page logged in as teacher1
|
||||
When I click on "Preview" "radio"
|
||||
And I press "Enter"
|
||||
# Confirm TOC display
|
||||
# Used css_element to check > and < button display in TOC since similar buttons also exist in navigation
|
||||
Then I <tocdisplay> see "Golf Explained - Minimum Run-time Calls"
|
||||
And "[title='Show']" "css_element" <showdisplay> exist
|
||||
And "[title='Hide']" "css_element" <hidedisplay> exist
|
||||
# Confirm TOC dropdown display
|
||||
And "scoid" "select" <tocdropdown> exist
|
||||
# Confirm the navigation display
|
||||
And "[id='scorm_nav']" "css_element" <navbar> exist
|
||||
|
||||
Examples:
|
||||
| toc | shownavigation | tocdisplay | showdisplay | hidedisplay | tocdropdown | navbar |
|
||||
| 1 | | should not | should | should not | should not | should not |
|
||||
| 2 | 1 | should | should | should not | should | should not |
|
||||
| 0 | 0 | should | should not | should | should not | should not |
|
||||
| 0 | 1 | should | should not | should | should not | should |
|
||||
|
||||
Scenario: Student can exit Scorm activity in single activity course format
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | packagefilepath | popup |
|
||||
| scorm | C2 | C2 Scorm 1 | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
|
||||
And I am on the "C2 Scorm 1" "scorm activity" page logged in as student1
|
||||
And I press "Enter"
|
||||
When I click on "Exit activity" "link"
|
||||
# Confirm that student can exit activity
|
||||
Then "Preview" "radio" should exist
|
||||
And "Normal" "radio" should exist
|
||||
And "Enter" "button" should exist
|
||||
And "Exit activity" "link" should not exist
|
||||
And I should not see "Golf Explained - Minimum Run-time Calls"
|
||||
+10
-1
@@ -808,7 +808,16 @@ function survey_supports($feature) {
|
||||
function survey_extend_settings_navigation($settings, $surveynode) {
|
||||
global $PAGE;
|
||||
|
||||
if (has_capability('mod/survey:readresponses', $PAGE->cm->context)) {
|
||||
$cm = get_coursemodule_from_id('survey', $PAGE->cm->id);
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
// Check to see if groups are being used in this survey, confirm user can access.
|
||||
$groupmode = groups_get_activity_groupmode($cm);
|
||||
$currentgroup = groups_get_activity_group($cm, true);
|
||||
|
||||
if (has_capability('mod/survey:readresponses', $context) &&
|
||||
!($currentgroup === 0 && $groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context))) {
|
||||
|
||||
$responsesnode = $surveynode->add(get_string("responsereports", "survey"));
|
||||
|
||||
$url = new moodle_url('/mod/survey/report.php', array('id' => $PAGE->cm->id, 'action'=>'summary'));
|
||||
|
||||
+11
-3
@@ -121,9 +121,16 @@
|
||||
echo $OUTPUT->heading(format_string($survey->name));
|
||||
|
||||
/// Check to see if groups are being used in this survey
|
||||
if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
|
||||
$groupmode = groups_get_activity_groupmode($cm);
|
||||
if ($groupmode != NOGROUPS) {
|
||||
$menuaction = $action == "student" ? "students" : $action;
|
||||
|
||||
// Get the current activity group, confirm user can access.
|
||||
$currentgroup = groups_get_activity_group($cm, true);
|
||||
if ($currentgroup === 0 && $groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
throw new moodle_exception('notingroup');
|
||||
}
|
||||
|
||||
groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/survey/report.php?id=$cm->id&action=$menuaction&qid=$qid");
|
||||
} else {
|
||||
$currentgroup = 0;
|
||||
@@ -378,8 +385,9 @@
|
||||
break;
|
||||
|
||||
case "student":
|
||||
if (!$user = $DB->get_record("user", array("id"=>$student))) {
|
||||
print_error('invaliduserid');
|
||||
$user = core_user::get_user($student, '*', MUST_EXIST);
|
||||
if ($currentgroup && !array_key_exists($user->id, $users)) {
|
||||
throw new moodle_exception('usernotavailable', 'error');
|
||||
}
|
||||
|
||||
echo $OUTPUT->heading(get_string("analysisof", "survey", fullname($user)), 3);
|
||||
|
||||
+4
-1
@@ -83,7 +83,10 @@ if (has_capability('mod/survey:readresponses', $context) or ($groupmode == VISIB
|
||||
$currentgroup = 0;
|
||||
}
|
||||
|
||||
if (has_capability('mod/survey:readresponses', $context)) {
|
||||
$currentactivitygroup = groups_get_activity_group($cm, true);
|
||||
if (has_capability('mod/survey:readresponses', $context) &&
|
||||
!($currentactivitygroup === 0 && $groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context))) {
|
||||
|
||||
$numusers = survey_count_responses($survey->id, $currentgroup, $groupingid);
|
||||
echo "<div class=\"reportlink\"><a href=\"report.php?id=$cm->id\">".
|
||||
get_string("viewsurveyresponses", "survey", $numusers)."</a></div>";
|
||||
|
||||
@@ -93,6 +93,9 @@ class report_log_renderable implements renderable {
|
||||
/** @var table_log table log which will be used for rendering logs */
|
||||
public $tablelog;
|
||||
|
||||
/** @var array group ids */
|
||||
public $grouplist;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -343,30 +346,41 @@ class report_log_renderable implements renderable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of groups.
|
||||
* Return list of groups that are used in this course. This is done when groups are used in the course
|
||||
* and the user is allowed to see all groups or groups are visible anyway. If groups are used but the
|
||||
* mode is separate groups and the user is not allowed to see all groups, the list contains the groups
|
||||
* only, where the user is member.
|
||||
* If the course uses no groups, the list is empty.
|
||||
*
|
||||
* @return array list of groups.
|
||||
*/
|
||||
public function get_group_list() {
|
||||
global $USER;
|
||||
|
||||
if ($this->grouplist !== null) {
|
||||
return $this->grouplist;
|
||||
}
|
||||
|
||||
// No groups for system.
|
||||
if (empty($this->course)) {
|
||||
return array();
|
||||
$this->grouplist = [];
|
||||
return $this->grouplist;
|
||||
}
|
||||
|
||||
$context = context_course::instance($this->course->id);
|
||||
$groups = array();
|
||||
$this->grouplist = [];
|
||||
$groupmode = groups_get_course_groupmode($this->course);
|
||||
$cgroups = [];
|
||||
if (($groupmode == VISIBLEGROUPS) ||
|
||||
($groupmode == SEPARATEGROUPS and has_capability('moodle/site:accessallgroups', $context))) {
|
||||
// Get all groups.
|
||||
if ($cgroups = groups_get_all_groups($this->course->id)) {
|
||||
foreach ($cgroups as $cgroup) {
|
||||
$groups[$cgroup->id] = $cgroup->name;
|
||||
}
|
||||
}
|
||||
($groupmode == SEPARATEGROUPS && has_capability('moodle/site:accessallgroups', $context))) {
|
||||
$cgroups = groups_get_all_groups($this->course->id);
|
||||
} else if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
$cgroups = groups_get_all_groups($this->course->id, $USER->id);
|
||||
}
|
||||
return $groups;
|
||||
foreach ($cgroups as $cgroup) {
|
||||
$this->grouplist[$cgroup->id] = $cgroup->name;
|
||||
}
|
||||
return $this->grouplist;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,8 +398,28 @@ class report_log_renderable implements renderable {
|
||||
$context = context_course::instance($courseid);
|
||||
$limitfrom = empty($this->showusers) ? 0 : '';
|
||||
$limitnum = empty($this->showusers) ? COURSE_MAX_USERS_PER_DROPDOWN + 1 : '';
|
||||
$courseusers = get_enrolled_users($context, '', $this->groupid, 'u.id, ' . get_all_user_name_fields(true, 'u'),
|
||||
|
||||
// Get the groups of that course.
|
||||
$groups = $this->get_group_list();
|
||||
// Check here if we are not in group mode, or in group mode but narrow the group selection
|
||||
// to the group of the user.
|
||||
if (empty($groups) || !empty($this->groupid) && isset($groups[(int)$this->groupid])) {
|
||||
// No groups are used in that course, therefore get all users (maybe limited to one group).
|
||||
$courseusers = get_enrolled_users($context, '', $this->groupid, 'u.id, ' . get_all_user_name_fields(true, 'u'),
|
||||
null, $limitfrom, $limitnum);
|
||||
} else {
|
||||
// The course uses groups, get the users from these groups.
|
||||
$groupids = array_keys($groups);
|
||||
try {
|
||||
$enrolments = enrol_get_course_users($courseid, false, [], [], $groupids);
|
||||
$courseusers = [];
|
||||
foreach ($enrolments as $enrolment) {
|
||||
$courseusers[$enrolment->id] = $enrolment;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$courseusers = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$this->showusers) {
|
||||
$this->showusers = 1;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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/>.
|
||||
|
||||
namespace report_log;
|
||||
|
||||
/**
|
||||
* Class report_log\renderable_test to cover functions in \report_log_renderable.
|
||||
*
|
||||
* @package report_log
|
||||
* @copyright 2023 Stephan Robotta <stephan.robotta@bfh.ch>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
|
||||
*/
|
||||
class renderable_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* @var [stdClass] The students.
|
||||
*/
|
||||
private $student = [];
|
||||
|
||||
/**
|
||||
* @var [stdClass] The teachers.
|
||||
*/
|
||||
private $teacher = [];
|
||||
|
||||
/**
|
||||
* @var [stdClass] The groups.
|
||||
*/
|
||||
private $group = [];
|
||||
|
||||
/**
|
||||
* @var stdClass The course.
|
||||
*/
|
||||
private $course;
|
||||
|
||||
/**
|
||||
* Set up a course with two groups, three students being each in one of the groups,
|
||||
* two teachers each in either group while the second teacher is also member of the other group.
|
||||
* @return void
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $PAGE;
|
||||
$this->course = $this->getDataGenerator()->create_course(['groupmode' => 1]);
|
||||
$this->group[] = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]);
|
||||
$this->group[] = $this->getDataGenerator()->create_group(['courseid' => $this->course->id]);
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$this->student[$i] = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($this->student[$i]->id, $this->course->id, 'student');
|
||||
$this->getDataGenerator()->create_group_member([
|
||||
'groupid' => $this->group[$i % 2]->id,
|
||||
'userid' => $this->student[$i]->id,
|
||||
]);
|
||||
}
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$this->teacher[$i] = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($this->teacher[$i]->id, $this->course->id, 'editingteacher');
|
||||
$this->getDataGenerator()->create_group_member([
|
||||
'groupid' => $this->group[$i]->id,
|
||||
'userid' => $this->teacher[$i]->id,
|
||||
]);
|
||||
}
|
||||
// Make teacher2 also member of group1.
|
||||
$this->getDataGenerator()->create_group_member([
|
||||
'groupid' => $this->group[0]->id,
|
||||
'userid' => $this->teacher[1]->id,
|
||||
]);
|
||||
|
||||
$PAGE->set_url('/report/log/index.php?id=' . $this->course->id);
|
||||
$this->resetAfterTest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test report_log_renderable::get_user_list().
|
||||
* @covers \report_log_renderable::get_user_list
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_user_list() {
|
||||
// Fetch all users of group 1 and the guest user.
|
||||
$userids = $this->fetch_users_from_renderable((int)$this->student[0]->id);
|
||||
$this->assertCount(5, $userids);
|
||||
$this->assertContains((int)$this->student[0]->id, $userids); // His own group (group 1).
|
||||
$this->assertNotContains((int)$this->student[1]->id, $userids); // He is in group 2.
|
||||
$this->assertContains((int)$this->teacher[0]->id, $userids); // He is in group 1.
|
||||
$this->assertContains((int)$this->teacher[1]->id, $userids); // He is in both groups.
|
||||
|
||||
// Fetch users of all groups and the guest user. The teacher has the capability moodle/site:accessallgroups.
|
||||
$this->setUser($this->teacher[1]->id);
|
||||
$renderable = new \report_log_renderable("", (int)$this->course->id, $this->teacher[1]->id);
|
||||
$users = $renderable->get_user_list();
|
||||
$this->assertCount(6, $users);
|
||||
|
||||
// Fetch users of group 2 and the guest user.
|
||||
$userids = $this->fetch_users_from_renderable((int)$this->student[1]->id);
|
||||
$this->assertCount( 3, $userids);
|
||||
$this->assertNotContains((int)$this->student[0]->id, $userids);
|
||||
$this->assertContains((int)$this->student[1]->id, $userids);
|
||||
$this->assertNotContains((int)$this->teacher[0]->id, $userids);
|
||||
$this->assertContains((int)$this->teacher[1]->id, $userids);
|
||||
|
||||
// Fetch users of group 2 and test user as teacher2 but limited to his group.
|
||||
$userids = $this->fetch_users_from_renderable((int)$this->teacher[1]->id, (int)$this->group[1]->id);
|
||||
$this->assertCount( 3, $userids);
|
||||
$this->assertNotContains((int)$this->student[0]->id, $userids);
|
||||
$this->assertContains((int)$this->student[1]->id, $userids);
|
||||
$this->assertNotContains((int)$this->teacher[0]->id, $userids);
|
||||
$this->assertContains((int)$this->teacher[1]->id, $userids);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return a list of user ids from the renderable object.
|
||||
* @param int $userid
|
||||
* @param ?int $groupid
|
||||
* @return array
|
||||
*/
|
||||
protected function fetch_users_from_renderable(int $userid, ?int $groupid = 0): array {
|
||||
$this->setUser($userid);
|
||||
$renderable = new \report_log_renderable(
|
||||
"", (int)$this->course->id, $userid, 0, '', $groupid);
|
||||
$users = $renderable->get_user_list();
|
||||
return \array_keys($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test report_log_renderable::get_group_list().
|
||||
* @covers \report_log_renderable::get_group_list
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_group_list() {
|
||||
|
||||
// The student sees his own group only.
|
||||
$this->setUser($this->student[0]->id);
|
||||
$renderable = new \report_log_renderable("", (int)$this->course->id, $this->student[0]->id);
|
||||
$groups = $renderable->get_group_list();
|
||||
$this->assertCount(1, $groups);
|
||||
|
||||
// While the teacher is allowed to see all groups.
|
||||
$this->setUser($this->teacher[0]->id);
|
||||
$renderable = new \report_log_renderable("", (int)$this->course->id, $this->teacher[0]->id);
|
||||
$groups = $renderable->get_group_list();
|
||||
$this->assertCount(2, $groups);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,8 @@ class report_loglive_renderable implements renderable {
|
||||
* @return stdClass filters
|
||||
*/
|
||||
protected function setup_filters() {
|
||||
global $USER;
|
||||
|
||||
$readers = $this->get_readers();
|
||||
|
||||
// Set up filters.
|
||||
@@ -178,6 +180,15 @@ class report_loglive_renderable implements renderable {
|
||||
if (!has_capability('moodle/site:viewanonymousevents', $context)) {
|
||||
$filter->anonymous = 0;
|
||||
}
|
||||
if (groups_get_course_groupmode($this->course) == SEPARATEGROUPS &&
|
||||
!has_capability('moodle/site:accessallgroups', $context)) {
|
||||
if ($cgroups = groups_get_all_groups($this->course->id, $USER->id)) {
|
||||
$filter->groups = [];
|
||||
foreach ($cgroups as $cgroup) {
|
||||
$filter->groups[] = (int)$cgroup->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$filter->courseid = 0;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ class report_loglive_table_log extends table_sql {
|
||||
* - int userid: user id
|
||||
* - int|string modid: Module id or "site_errors" to view site errors
|
||||
* - int groupid: Group id
|
||||
* - array groups: List of group ids
|
||||
* - \core\log\sql_reader logreader: reader from which data will be fetched.
|
||||
* - int edulevel: educational level.
|
||||
* - string action: view action
|
||||
@@ -304,14 +305,29 @@ class report_loglive_table_log extends table_sql {
|
||||
* @param bool $useinitialsbar do you want to use the initials bar.
|
||||
*/
|
||||
public function query_db($pagesize, $useinitialsbar = true) {
|
||||
global $DB;
|
||||
|
||||
$joins = array();
|
||||
$params = array();
|
||||
|
||||
// Set up filtering.
|
||||
if (!empty($this->filterparams->courseid)) {
|
||||
// For a normal course, set the course filter.
|
||||
$joins[] = "courseid = :courseid";
|
||||
$params['courseid'] = $this->filterparams->courseid;
|
||||
// If we have a course, then check if the groups filter is set.
|
||||
if ($this->filterparams->courseid != SITEID && !empty($this->filterparams->groups)) {
|
||||
// If that's the case, limit the users to be in the groups only, defined by the filter.
|
||||
$useringroups = [];
|
||||
foreach ($this->filterparams->groups as $groupid) {
|
||||
$gusers = groups_get_members($groupid, 'u.id');
|
||||
$useringroups = array_merge($useringroups, array_keys($gusers));
|
||||
}
|
||||
$useringroups = array_unique($useringroups);
|
||||
list($ugsql, $ugparams) = $DB->get_in_or_equal($useringroups, SQL_PARAMS_NAMED);
|
||||
$joins[] = 'userid ' . $ugsql;
|
||||
$params = array_merge($params, $ugparams);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->filterparams->date)) {
|
||||
@@ -323,7 +339,6 @@ class report_loglive_table_log extends table_sql {
|
||||
$joins[] = "anonymous = :anon";
|
||||
$params['anon'] = $this->filterparams->anonymous;
|
||||
}
|
||||
|
||||
$selector = implode(' AND ', $joins);
|
||||
|
||||
$total = $this->filterparams->logreader->get_events_select_count($selector, $params);
|
||||
|
||||
+11
-2
@@ -38,6 +38,10 @@ require_once(__DIR__.'/locallib.php');
|
||||
class repository_url extends repository {
|
||||
/** @var int Maximum time of recursion. */
|
||||
const MAX_RECURSION_TIME = 5;
|
||||
/** @var int Maximum number of CSS imports. */
|
||||
const MAX_CSS_IMPORTS = 10;
|
||||
/** @var int CSS import counter. */
|
||||
var $cssimportcounter = 0;
|
||||
var $processedfiles = array();
|
||||
/** @var int Recursion counter. */
|
||||
var $recursioncounter = 0;
|
||||
@@ -134,8 +138,8 @@ EOD;
|
||||
// Avoid endless recursion for the same URL with same parameters.
|
||||
return;
|
||||
}
|
||||
// Remove the query string before check.
|
||||
$recursioncheckurl = preg_replace('/\?.*/', '', $url);
|
||||
// Remove the query string and anchors before check.
|
||||
$recursioncheckurl = (new moodle_url($url))->out_omit_querystring();
|
||||
if (in_array($recursioncheckurl, $this->processedfiles)) {
|
||||
$this->recursioncounter++;
|
||||
}
|
||||
@@ -200,6 +204,11 @@ EOD;
|
||||
}
|
||||
if (!empty($urls['import'])) {
|
||||
foreach ($urls['import'] as $cssurl) {
|
||||
// Limit the number of CSS imports to avoid infinite imports.
|
||||
if ($this->cssimportcounter >= self::MAX_CSS_IMPORTS) {
|
||||
return;
|
||||
}
|
||||
$this->cssimportcounter++;
|
||||
$this->parse_file($info['url'], $cssurl, $list);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2020061524.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
$version = 2020061525.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '3.9.24 (Build: 20231009)'; // Human-friendly version name
|
||||
$release = '3.9.25 (Build: 20231211)'; // Human-friendly version name
|
||||
$branch = '39'; // This version's branch.
|
||||
$maturity = MATURITY_STABLE; // This version's maturity level.
|
||||
|
||||
Reference in New Issue
Block a user