mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 16:32:18 +02:00
Merge branch 'MDL-81714_main' of https://github.com/marxjohnson/moodle
This commit is contained in:
commit
56183c786e
46
.upgradenotes/MDL-81714-2025022613184221.yml
Normal file
46
.upgradenotes/MDL-81714-2025022613184221.yml
Normal file
@ -0,0 +1,46 @@
|
||||
issueNumber: MDL-81714
|
||||
notes:
|
||||
core:
|
||||
- message: >
|
||||
The stored progress API has been updated. The
|
||||
`\core\output\stored_progress_bar` class has
|
||||
|
||||
now has a `store_pending()` method, which will create a record for the
|
||||
stored process, but
|
||||
|
||||
without a start time or progress percentage.
|
||||
|
||||
`\core\task\stored_progress_task_trait` has been updated with a new
|
||||
`initialise_stored_progress()` method,
|
||||
|
||||
which will call `store_pending()` for the task's progress bar. This
|
||||
allows the progress bar to be displayed
|
||||
|
||||
in a "pending" state, to show that a process has been queued but not
|
||||
started.
|
||||
type: improved
|
||||
- message: >
|
||||
A new `\core\output\task_indicator` component has been added to display
|
||||
a progress bar and message
|
||||
|
||||
for a background task using `\core\task\stored_progress_task_trait`. See
|
||||
the "Task indicator"
|
||||
|
||||
page in the component library for usage details.
|
||||
type: improved
|
||||
core_grades:
|
||||
- message: >
|
||||
`grade_regrade_final_grades()` now has an additional `async` parameter,
|
||||
which allows full course
|
||||
|
||||
regrades to be performed in the background. This avoids blocking the
|
||||
user for long periods and
|
||||
|
||||
while making changes to a large course. The actual regrade is performed
|
||||
using the
|
||||
|
||||
`\core_course\task\regrade_final_grades` adhoc task, which calls
|
||||
`grade_regrade_final_grades()`
|
||||
|
||||
with `async: false`.
|
||||
type: improved
|
@ -0,0 +1,122 @@
|
||||
---
|
||||
layout: docs
|
||||
title: "Task indicator"
|
||||
description: "A progress indicator for background tasks"
|
||||
date: 2024-08-21T00:00:00+01:00
|
||||
draft: false
|
||||
tags:
|
||||
- MDL-81714
|
||||
- 5.0
|
||||
---
|
||||
|
||||
{{< mustache template="core/task_indicator" >}}
|
||||
{{< /mustache >}}
|
||||
|
||||
## How to use
|
||||
|
||||
The task indicator component is used to display on any page the status and progress of an ad-hoc or scheduled task running in the
|
||||
background. If a task is running that will update the content on the page, it can be displayed in place of the content to inform
|
||||
the user that the current content is out-of-date, and will be updated when the task is complete.
|
||||
|
||||
## Source files
|
||||
|
||||
* lib/amd/src/task_indicator.js
|
||||
* lib/classes/output/task_indicator.php
|
||||
* lib/templates/task_indicator.mustache
|
||||
|
||||
## Usage
|
||||
|
||||
The task indicator can only be used to display the progress of a task if its class uses `core\task\stored_progress_task_trait`.
|
||||
|
||||
When the task is queued, you must call the `initialise_stored_progress()` method to store the progress record in a pending state,
|
||||
for the indicator to display while the task is queued.
|
||||
|
||||
{{< php >}}
|
||||
$task = new \core\task\mytask($id);
|
||||
$taskid = \core\task\manager::queue_adhoc_task($task, true);
|
||||
if ($taskid) {
|
||||
$task->set_id($taskid);
|
||||
$task->initialise_stored_progress();
|
||||
}
|
||||
{{< /php >}}
|
||||
|
||||
When the task runs, it must start, progress and complete its stored progress bar.
|
||||
See `core_course\task\regrade_final_grades` for a real-life example.
|
||||
|
||||
{{< php >}}
|
||||
|
||||
class mytask extends adhoc_task {
|
||||
use \core\task\stored_progress_task_trait;
|
||||
|
||||
public function execute(): void {
|
||||
$this->start_stored_progress();
|
||||
$storedprogress = $this->get_progress();
|
||||
foreach ($this->get_records() as $record) {
|
||||
$this->process_record($record);
|
||||
$storedprogress->progress();
|
||||
}
|
||||
$storedprogress->end_progress();
|
||||
}
|
||||
}
|
||||
|
||||
{{< /php >}}
|
||||
|
||||
Any page that wishes to display the status of the task must create an instance of the task object with the same parameters,
|
||||
and pass it to a `task_indicator`.
|
||||
|
||||
{{< php >}}
|
||||
|
||||
$task = new mytask($id);
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
task: $task,
|
||||
heading: 'Task processing',
|
||||
message: get_string('recalculatinggradesadhoc', 'grades'),
|
||||
icon: new \core\output\pix_icon('i/grades', ''),
|
||||
redirecturl: $PAGE->url,
|
||||
extraclasses: ['mytask'],
|
||||
);
|
||||
|
||||
{{< /php >}}
|
||||
|
||||
If there is currently a queued instance of the task, `$taskindicator->has_task_record()` will return true. We can use this to
|
||||
decide whether we display the indicator. See `grade/report/summary/index.php` for a real-life example.
|
||||
|
||||
{{< php >}}
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
}
|
||||
|
||||
{{< /php >}}
|
||||
|
||||
When the task begins running and the progress is updated, the progress bar will automatically be displayed.
|
||||
|
||||
If the optional `redirecturl` parameter is set when creating the indicator, the page will automatically reload or redirect to
|
||||
this URL when the progress bar completes.
|
||||
|
||||
While the task is still queued, admins will see a "Run now" button below the indicator. This is designed for convenience if
|
||||
a user is blocked on a job and needs the task run immediately. It will run the specific instance of the task tracked by the
|
||||
indicator.
|
||||
|
||||
{{< mustache template="core/task_indicator" >}}
|
||||
{
|
||||
"heading": "Regrade in progress",
|
||||
"icon": {
|
||||
"attributes": [
|
||||
{"name": "src", "value": "/pix/i/timer.svg"},
|
||||
{"name": "alt", "value": ""}
|
||||
]
|
||||
},
|
||||
"message": "Grades are being recalculated due to recent changes.",
|
||||
"progress": {
|
||||
"id": "progressbar_test",
|
||||
"message": "Task pending",
|
||||
"idnumber": "progressbar_test",
|
||||
"class": "stored-progress-bar stored-progress-notstarted",
|
||||
"width": "500",
|
||||
"value": "0"
|
||||
},
|
||||
"runurl": "http://example.com/runtask.php?id=1",
|
||||
"runlabel": "Run now"
|
||||
}
|
||||
{{< /mustache >}}
|
@ -54,6 +54,7 @@ Feature: Backup user data
|
||||
And I navigate to "Recycle bin" in current page administration
|
||||
And I should see "Quiz 1"
|
||||
And I click on "Restore" "link" in the "region-main" "region"
|
||||
And I run all adhoc tasks
|
||||
When I am on the "Course 1" "grades > User report > View" page logged in as "student1"
|
||||
Then "Quiz 1" row "Grade" column of "user-grade" table should contain "50"
|
||||
And "Quiz 1" row "Percentage" column of "user-grade" table should contain "50"
|
||||
|
@ -67,9 +67,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete the course module.
|
||||
course_delete_module($this->quiz->cmid);
|
||||
|
||||
// Now, run the course module deletion adhoc task.
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
|
||||
// Check the course module is now in the recycle bin.
|
||||
$this->assertEquals(1, $DB->count_records('tool_recyclebin_course'));
|
||||
|
||||
@ -111,9 +108,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete the course module.
|
||||
course_delete_module($this->quiz->cmid);
|
||||
|
||||
// Now, run the course module deletion adhoc task.
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
|
||||
// Try purging.
|
||||
$recyclebin = new \tool_recyclebin\course_bin($this->course->id);
|
||||
foreach ($recyclebin->get_items() as $item) {
|
||||
@ -136,9 +130,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete the quiz.
|
||||
course_delete_module($this->quiz->cmid);
|
||||
|
||||
// Now, run the course module deletion adhoc task.
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
|
||||
// Set deleted date to the distant past.
|
||||
$recyclebin = new \tool_recyclebin\course_bin($this->course->id);
|
||||
foreach ($recyclebin->get_items() as $item) {
|
||||
@ -152,9 +143,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
|
||||
course_delete_module($book->cmid);
|
||||
|
||||
// Now, run the course module deletion adhoc task.
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
|
||||
// Should have 2 items now.
|
||||
$this->assertEquals(2, count($recyclebin->get_items()));
|
||||
|
||||
@ -224,7 +212,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete quiz.
|
||||
$cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
|
||||
course_delete_module($cm->id);
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
$quizzes = get_coursemodules_in_course('quiz', $this->course->id);
|
||||
$this->assertEquals(0, count($quizzes));
|
||||
|
||||
@ -261,9 +248,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete the course module.
|
||||
course_delete_module($this->quiz->cmid);
|
||||
|
||||
// Now, run the course module deletion adhoc task.
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
|
||||
// Check there is no items in the recycle bin.
|
||||
$recyclebin = new \tool_recyclebin\course_bin($this->course->id);
|
||||
$this->assertEquals(0, count($recyclebin->get_items()));
|
||||
@ -294,7 +278,6 @@ final class course_bin_test extends \advanced_testcase {
|
||||
// Delete quiz.
|
||||
$cm = get_coursemodule_from_instance('quiz', $this->quiz->id);
|
||||
course_delete_module($cm->id);
|
||||
\phpunit_util::run_all_adhoc_tasks();
|
||||
$quizzes = get_coursemodules_in_course('quiz', $this->course->id);
|
||||
$this->assertEquals(0, count($quizzes));
|
||||
|
||||
|
@ -490,7 +490,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
|
||||
rebuild_course_cache($this->get_courseid(), true);
|
||||
|
||||
// Restore marks items as needing update. Update everything now.
|
||||
grade_regrade_final_grades($this->get_courseid());
|
||||
grade_regrade_final_grades($this->get_courseid(), async: true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
77
course/classes/task/regrade_final_grades.php
Normal file
77
course/classes/task/regrade_final_grades.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?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_course\task;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core\task\adhoc_task;
|
||||
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
|
||||
/**
|
||||
* Asynchronously regrade a course.
|
||||
*
|
||||
* @copyright 2024 onwards Catalyst IT Europe Ltd.
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package core_course
|
||||
*/
|
||||
class regrade_final_grades extends adhoc_task {
|
||||
use \core\task\logging_trait;
|
||||
use \core\task\stored_progress_task_trait;
|
||||
|
||||
/**
|
||||
* Create and return an instance of this task for a given course ID.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return self
|
||||
*/
|
||||
public static function create(int $courseid): self {
|
||||
$task = new regrade_final_grades();
|
||||
$task->set_custom_data((object)['courseid' => $courseid]);
|
||||
$task->set_component('core');
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run regrade_final_grades for the provided course id.
|
||||
*
|
||||
* @return void
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function execute(): void {
|
||||
$data = $this->get_custom_data();
|
||||
$this->start_stored_progress();
|
||||
$this->log_start("Recalculating grades for course ID {$data->courseid}");
|
||||
// Ensure the course exists.
|
||||
try {
|
||||
$course = get_course($data->courseid);
|
||||
} catch (\dml_missing_record_exception $e) {
|
||||
$this->log("Course with id {$data->courseid} not found. It may have been deleted. Skipping regrade.");
|
||||
return;
|
||||
}
|
||||
$this->log("Found course {$course->shortname}. Starting regrade.");
|
||||
$results = grade_regrade_final_grades($course->id, progress: $this->get_progress());
|
||||
if (is_array($results)) {
|
||||
$this->log('Errors reported during regrade:');
|
||||
foreach ($results as $id => $result) {
|
||||
$this->log("Grade item {$id}: {$result}", 2);
|
||||
}
|
||||
}
|
||||
$this->log_finish('Regrade complete.');
|
||||
}
|
||||
}
|
@ -204,13 +204,6 @@ if ($mform->is_cancelled()) {
|
||||
$url = course_get_url($course, $cw->section, $options);
|
||||
}
|
||||
|
||||
// If we need to regrade the course with a progress bar as a result of updating this module,
|
||||
// redirect first to the page that will do this.
|
||||
if (isset($fromform->needsfrontendregrade)) {
|
||||
$url = new moodle_url('/course/modregrade.php', ['id' => $fromform->coursemodule,
|
||||
'url' => $url->out_as_local_url(false)]);
|
||||
}
|
||||
|
||||
redirect($url);
|
||||
exit;
|
||||
|
||||
|
@ -404,13 +404,17 @@ function edit_module_post_actions($moduleinfo, $course) {
|
||||
// And if it actually needs regrading...
|
||||
$courseitem = grade_item::fetch_course_item($course->id);
|
||||
if ($courseitem->needsupdate) {
|
||||
// Then don't do it as part of this form save, do it on an extra web request with a
|
||||
// progress bar.
|
||||
$moduleinfo->needsfrontendregrade = true;
|
||||
// Queue an asynchronous regrade.
|
||||
grade_regrade_final_grades($course->id, async: true);
|
||||
}
|
||||
} else {
|
||||
// Regrade now.
|
||||
grade_regrade_final_grades($course->id);
|
||||
$result = grade_regrade_final_grades($course->id);
|
||||
if (is_array($result)) {
|
||||
foreach ($result as $error) {
|
||||
\core\notification::add($error, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,10 +53,6 @@ $baseurl = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid])
|
||||
|
||||
$PAGE->set_url(new moodle_url('/grade/report/grader/index.php', array('id'=>$courseid)));
|
||||
$PAGE->set_pagelayout('report');
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/stickycolspan', 'init');
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/user', 'init', [$baseurl->out(false)]);
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/feedback_modal', 'init');
|
||||
$PAGE->requires->js_call_amd('core_grades/gradebooksetup_forms', 'init');
|
||||
|
||||
// basic access checks
|
||||
if (!$course = $DB->get_record('course', array('id' => $courseid))) {
|
||||
@ -129,9 +125,14 @@ if (!empty($target) && !empty($action) && confirm_sesskey()) {
|
||||
}
|
||||
|
||||
$reportname = get_string('pluginname', 'gradereport_grader');
|
||||
|
||||
// Do this check just before printing the grade header (and only do it once).
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
$regradetask = \core_course\task\regrade_final_grades::create($courseid);
|
||||
$indicatorheading = get_string('recalculatinggrades', 'grades');
|
||||
$indicatormessage = get_string('recalculatinggradesadhoc', 'grades');
|
||||
$taskindicator = new \core\output\task_indicator($regradetask, $indicatorheading, $indicatormessage, $PAGE->url);
|
||||
if (!$taskindicator->has_task_record()) {
|
||||
// Do this check just before printing the grade header (and only do it once).
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
}
|
||||
|
||||
//Initialise the grader report object that produces the table
|
||||
//the class grade_report_grader_ajax was removed as part of MDL-21562
|
||||
@ -143,13 +144,6 @@ $sort = strtoupper($sort);
|
||||
|
||||
$report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid, $sort);
|
||||
|
||||
// We call this a little later since we need some info from the grader report.
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/collapse', 'init', [
|
||||
'userID' => $USER->id,
|
||||
'courseID' => $courseid,
|
||||
'defaultSort' => $report->get_default_sortable()
|
||||
]);
|
||||
|
||||
$numusers = $report->get_numusers(true, true);
|
||||
|
||||
$actionbar = new \gradereport_grader\output\action_bar($context, $report, $numusers);
|
||||
@ -170,6 +164,23 @@ if ($isediting && ($data = data_submitted()) && confirm_sesskey()) {
|
||||
$warnings = $report->process_data($data);
|
||||
}
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
// We call this a little later since we need some info from the grader report.
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/collapse', 'init', [
|
||||
'userID' => $USER->id,
|
||||
'courseID' => $courseid,
|
||||
'defaultSort' => $report->get_default_sortable(),
|
||||
]);
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/stickycolspan', 'init');
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/user', 'init', [$baseurl->out(false)]);
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/feedback_modal', 'init');
|
||||
$PAGE->requires->js_call_amd('core_grades/gradebooksetup_forms', 'init');
|
||||
|
||||
// Final grades MUST be loaded after the processing.
|
||||
$report->load_users();
|
||||
$report->load_final_grades();
|
||||
|
@ -44,7 +44,13 @@ if (empty($CFG->enableoutcomes)) {
|
||||
}
|
||||
|
||||
// First make sure we have proper final grades.
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
$regradetask = \core_course\task\regrade_final_grades::create($courseid);
|
||||
$indicatormessage = get_string('recalculatinggradesadhoc', 'grades');
|
||||
$indicatorheading = get_string('recalculatinggrades', 'grades');
|
||||
$taskindicator = new \core\output\task_indicator($regradetask, $indicatorheading, $indicatormessage, $PAGE->url);
|
||||
if (!$taskindicator->has_task_record()) {
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
}
|
||||
|
||||
// Grab all outcomes used in course.
|
||||
$report_info = array();
|
||||
@ -96,6 +102,14 @@ foreach ($outcomes as $outcomeid => $outcome) {
|
||||
}
|
||||
}
|
||||
|
||||
print_grade_page_head($courseid, 'report', 'outcomes');
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
$html = '<table class="generaltable boxaligncenter" width="90%" cellspacing="1" cellpadding="5" summary="Outcomes Report">' . "\n";
|
||||
$html .= '<tr><th class="header c0" scope="col">' . get_string('outcomeshortname', 'grades') . '</th>';
|
||||
$html .= '<th class="header c1" scope="col">' . get_string('courseavg', 'grades') . '</th>';
|
||||
@ -176,8 +190,6 @@ foreach ($report_info as $outcomeid => $outcomedata) {
|
||||
|
||||
$html .= '</table>';
|
||||
|
||||
print_grade_page_head($courseid, 'report', 'outcomes');
|
||||
|
||||
echo $html;
|
||||
|
||||
$event = \gradereport_outcomes\event\grade_report_viewed::create(
|
||||
|
@ -90,6 +90,13 @@ $USER->grade_last_report[$course->id] = 'overview';
|
||||
$actionbar = new \core_grades\output\general_action_bar($context,
|
||||
new moodle_url('/grade/report/overview/index.php', ['id' => $courseid]), 'report', 'overview');
|
||||
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
\core_course\task\regrade_final_grades::create($courseid),
|
||||
get_string('recalculatinggrades', 'grades'),
|
||||
get_string('recalculatinggradesadhoc', 'grades'),
|
||||
$PAGE->url,
|
||||
);
|
||||
|
||||
if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
// Please note this would be extremely slow if we wanted to implement this properly for all teachers.
|
||||
$groupmode = groups_get_course_groupmode($course); // Groups are being used
|
||||
@ -115,6 +122,12 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
|
||||
groups_print_course_menu($course, $gpr->get_return_url('index.php?id='.$courseid, array('userid'=>0)));
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user_selector) {
|
||||
$renderer = $PAGE->get_renderer('gradereport_overview');
|
||||
echo $renderer->graded_users_selector('overview', $course, $userid, $currentgroup, false);
|
||||
@ -131,6 +144,12 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
$report->user, $actionbar);
|
||||
groups_print_course_menu($course, $gpr->get_return_url('index.php?id='.$courseid, array('userid'=>0)));
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user_selector) {
|
||||
$renderer = $PAGE->get_renderer('gradereport_overview');
|
||||
echo $renderer->graded_users_selector('overview', $course, $userid, $currentgroup, false);
|
||||
@ -168,6 +187,11 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
if ($report->fill_table(true, true)) {
|
||||
echo html_writer::tag('h3', get_string('coursesiamtaking', 'grades'));
|
||||
echo '<br />' . $report->print_table(true);
|
||||
@ -176,6 +200,11 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
print_grade_page_head($courseid, 'report', 'overview', get_string('pluginname', 'gradereport_overview')
|
||||
. ' - ' . fullname($report->user), false, false, true, null, null,
|
||||
$report->user, $actionbar);
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
if ($report->fill_table()) {
|
||||
echo '<br />' . $report->print_table(true);
|
||||
}
|
||||
@ -189,6 +218,11 @@ if (has_capability('moodle/grade:viewall', $context) && $courseid != SITEID) {
|
||||
}
|
||||
|
||||
if (count($report->teachercourses)) {
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
echo html_writer::tag('h3', get_string('coursesiamteaching', 'grades'));
|
||||
$report->print_teacher_table();
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ class grade_report_overview extends grade_report {
|
||||
if ($frontend) {
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
} else {
|
||||
grade_regrade_final_grades($course->id);
|
||||
grade_regrade_final_grades($course->id, async: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,6 +114,10 @@ final class lib_test extends \advanced_testcase {
|
||||
$gpr = new \stdClass();
|
||||
$report = new \grade_report_overview($user->id, $gpr, '');
|
||||
$report->regrade_all_courses_if_needed($frontend);
|
||||
if (!$frontend) {
|
||||
$this->expectOutputRegex("~^Recalculating grades for course ID {$course->id}~");
|
||||
$this->run_all_adhoc_tasks();
|
||||
}
|
||||
|
||||
// This should have regraded courses 1 and 3, but not 2 (because the user doesn't belong).
|
||||
$this->assertEqualsWithDelta(50.0, $DB->get_field('grade_grades', 'finalgrade',
|
||||
|
@ -135,8 +135,6 @@ switch ($itemtype) {
|
||||
break;
|
||||
}
|
||||
|
||||
$report = new gradereport_singleview\report\singleview($courseid, $gpr, $context, $itemtype, $itemid);
|
||||
|
||||
$pageparams = [
|
||||
'id' => $courseid,
|
||||
'userid' => $userid,
|
||||
@ -152,6 +150,31 @@ if (!is_null($groupid)) {
|
||||
|
||||
$PAGE->set_url(new moodle_url('/grade/report/singleview/index.php', $pageparams));
|
||||
|
||||
// Make sure we have proper final grades.
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
\core_course\task\regrade_final_grades::create($courseid),
|
||||
get_string('recalculatinggrades', 'grades'),
|
||||
get_string('recalculatinggradesadhoc', 'grades'),
|
||||
$PAGE->url,
|
||||
);
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
// We need to bail out early as the report requires recalculations to be complete, so just display a basic header
|
||||
// with navigation, and the indicator.
|
||||
$actionbar = new \core_grades\output\general_action_bar(
|
||||
$context,
|
||||
new moodle_url('/grade/report/singleview/index.php', ['id' => $courseid]),
|
||||
'report',
|
||||
'singleview'
|
||||
);
|
||||
print_grade_page_head($course->id, 'report', 'singleview', actionbar: $actionbar);
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
$report = new gradereport_singleview\report\singleview($courseid, $gpr, $context, $itemtype, $itemid);
|
||||
|
||||
// Build editing on/off button for themes that need it.
|
||||
$button = '';
|
||||
if ($PAGE->user_allowed_editing() && !$PAGE->theme->haseditswitch) {
|
||||
@ -213,6 +236,12 @@ if ($data = data_submitted()) {
|
||||
// Make sure we have proper final grades.
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Save the screen state in a session variable as last viewed state.
|
||||
$SESSION->gradereport_singleview["itemtype-{$context->id}"] = $itemtype;
|
||||
if ($itemid) {
|
||||
|
@ -45,11 +45,20 @@ $PAGE->add_body_class('limitedwidth');
|
||||
require_capability('gradereport/summary:view', $context);
|
||||
require_capability('moodle/grade:viewall', $context);
|
||||
|
||||
print_grade_page_head($courseid, 'report', 'summary', false,
|
||||
false, false, true, null, null,
|
||||
null, null);
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
\core_course\task\regrade_final_grades::create($courseid),
|
||||
get_string('recalculatinggrades', 'grades'),
|
||||
get_string('recalculatinggradesadhoc', 'grades'),
|
||||
$PAGE->url,
|
||||
);
|
||||
|
||||
$report = system_report_factory::create(summary::class, context_course::instance($courseid));
|
||||
print_grade_page_head($courseid, 'report', 'summary');
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
} else {
|
||||
$report = system_report_factory::create(summary::class, context_course::instance($courseid));
|
||||
echo $report->output();
|
||||
}
|
||||
|
||||
echo $report->output();
|
||||
echo $OUTPUT->footer();
|
||||
|
@ -85,12 +85,30 @@ if (!isset($USER->grade_last_report)) {
|
||||
$USER->grade_last_report[$course->id] = 'user';
|
||||
|
||||
// First make sure we have proper final grades.
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
\core_course\task\regrade_final_grades::create($courseid),
|
||||
get_string('recalculatinggrades', 'grades'),
|
||||
get_string('recalculatinggradesadhoc', 'grades'),
|
||||
$PAGE->url,
|
||||
);
|
||||
if (!$taskindicator->has_task_record()) {
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
}
|
||||
|
||||
$gradesrenderer = $PAGE->get_renderer('core_grades');
|
||||
|
||||
// Teachers will see all student reports.
|
||||
if (has_capability('moodle/grade:viewall', $context)) {
|
||||
if ($taskindicator->has_task_record()) {
|
||||
// We need to bail out early as getting the gradeable users requires calculations to be complete,
|
||||
// so just display a basic header with navigation, and the indicator.
|
||||
$actionbar = new \core_grades\output\general_action_bar($context, $PAGE->url, 'report', 'user');
|
||||
print_grade_page_head($course->id, 'report', 'user', actionbar: $actionbar);
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
echo $OUTPUT->footer();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify if we are using groups or not.
|
||||
$groupmode = groups_get_course_groupmode($course);
|
||||
$currentgroup = $gpr->groupid;
|
||||
@ -203,7 +221,9 @@ if (has_capability('moodle/grade:viewall', $context)) {
|
||||
// Print the page.
|
||||
print_grade_page_head($courseid, 'report', 'user', false, false, false, true, null, null, $report->user);
|
||||
|
||||
if ($report->fill_table()) {
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
} else if ($report->fill_table()) {
|
||||
echo $report->print_table(true);
|
||||
}
|
||||
}
|
||||
|
@ -174,7 +174,18 @@ function grade_report_user_settings_definition(&$mform) {
|
||||
* @param boolean $viewasuser True when we are viewing this as the targetted user sees it.
|
||||
*/
|
||||
function grade_report_user_profilereport(object $course, object $user, bool $viewasuser = false) {
|
||||
if (!empty($course->showgrades)) {
|
||||
global $OUTPUT;
|
||||
|
||||
$taskindicator = new \core\output\task_indicator(
|
||||
\core_course\task\regrade_final_grades::create($course->id),
|
||||
get_string('recalculatinggrades', 'grades'),
|
||||
get_string('recalculatinggradesadhoc', 'grades'),
|
||||
new \core\url('/course/user.php', ['mode' => 'grade', 'id' => $course->id, 'user' => $user->id]),
|
||||
);
|
||||
|
||||
if ($taskindicator->has_task_record()) {
|
||||
echo $OUTPUT->render($taskindicator);
|
||||
} else if (!empty($course->showgrades)) {
|
||||
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
|
142
grade/tests/behat/grade_async_regrade.feature
Normal file
142
grade/tests/behat/grade_async_regrade.feature
Normal file
@ -0,0 +1,142 @@
|
||||
@core @core_grades @javascript
|
||||
Feature: Asynchronous regrade on a large course
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| shortname | fullname | idnumber |
|
||||
| C1 | Test course 1 | C1 |
|
||||
| C2 | Test course 2 | C2 |
|
||||
And the following "users" exist:
|
||||
| username |
|
||||
| teacher1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher1 | C2 | editingteacher |
|
||||
And "100" "users" exist with the following data:
|
||||
| username | student[count] |
|
||||
| firstname | Student |
|
||||
| lastname | [count] |
|
||||
| email | student[count]@example.com |
|
||||
And "100" "course enrolments" exist with the following data:
|
||||
| user | student[count] |
|
||||
| course | C1 |
|
||||
| role | student |
|
||||
And the following "activity" exists:
|
||||
| activity | assign |
|
||||
| course | C1 |
|
||||
| idnumber | a1 |
|
||||
| name | Test assignment 1 |
|
||||
| grade | 100 |
|
||||
| intro | Submit your online text |
|
||||
And "100" "grade grades" exist with the following data:
|
||||
| gradeitem | Test assignment 1 |
|
||||
| user | student[count] |
|
||||
| grade | 80.00 |
|
||||
And the following "course enrolment" exists:
|
||||
| user | student1 |
|
||||
| course | C2 |
|
||||
| role | student |
|
||||
And the following "activity" exists:
|
||||
| activity | assign |
|
||||
| course | C2 |
|
||||
| idnumber | a2 |
|
||||
| name | Test assignment 2 |
|
||||
| grade | 100 |
|
||||
| intro | Submit your online text |
|
||||
And the following "grade grade" exists:
|
||||
| gradeitem | Test assignment 2 |
|
||||
| user | student1 |
|
||||
| grade | 80.00 |
|
||||
And I am on the "Test assignment 1" "assign activity editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
And I set the field "Rescale existing grades" to "Yes"
|
||||
And I set the field "Maximum grade" to "50"
|
||||
And I press "Save and return to course"
|
||||
And I log out
|
||||
And I change the viewport size to "medium"
|
||||
|
||||
Scenario Outline: Task indicator displays on all grade reports when a calculation is pending
|
||||
Given I am on the "Test course 2" "<report>" page logged in as "<user>"
|
||||
Then I should not see "The report will update automatically. You don't need to do anything."
|
||||
And <element> should exist
|
||||
When I am on the "Test course 1" "<report>" page logged in as "<user>"
|
||||
Then I should see "The report will update automatically. You don't need to do anything."
|
||||
And <element> should not exist
|
||||
|
||||
Examples:
|
||||
| report | element | user |
|
||||
| grades > Grader report > View | "user-grades" "table" | teacher1 |
|
||||
| grades > Overview report > View | "overview-grade" "table" | teacher1 |
|
||||
| grades > Single view > View | "Search for a user to view all their grades" "text" | teacher1 |
|
||||
| grades > Grade summary > View | "Summary" "table" | teacher1 |
|
||||
| grades > User report > View | "Search for a user to view their report" "text" | teacher1 |
|
||||
| grades > User report > View | "table.user-grade" "css_element" | student1 |
|
||||
|
||||
Scenario Outline: Gradebook settings can be accessed when a regrade is pending
|
||||
Given I am on the "Test course 2" "<page>" page logged in as "teacher1"
|
||||
Then I should see "<text>"
|
||||
And I should not see "The report will update automatically. You don't need to do anything."
|
||||
Given I am on the "Test course 1" "<page>" page logged in as "teacher1"
|
||||
Then I should see "<text>"
|
||||
And I should not see "The report will update automatically. You don't need to do anything."
|
||||
|
||||
Examples:
|
||||
| page | text |
|
||||
| grades > Gradebook setup | Aggregation |
|
||||
| grades > Course grade settings | General settings |
|
||||
|
||||
Scenario: Task indicator displays on user profile grade reports when a grade calculation is pending
|
||||
Given I log in as "student1"
|
||||
When I follow "Grades" in the user menu
|
||||
And I follow "Test course 2"
|
||||
Then "table.user-grade" "css_element" should exist
|
||||
Then I should not see "The report will update automatically. You don't need to do anything."
|
||||
When I follow "Grades" in the user menu
|
||||
And I follow "Test course 1"
|
||||
Then "table.user-grade" "css_element" should not exist
|
||||
Then I should see "The report will update automatically. You don't need to do anything."
|
||||
|
||||
Scenario: Task indicator progresses and redirects when the task is run.
|
||||
When I am on the "Test course 1" "grades > Grader report > View" page logged in as teacher1
|
||||
And I should see "The report will update automatically. You don't need to do anything."
|
||||
And I should not see "Run now"
|
||||
And I should not see "0.0%"
|
||||
And "user-grades" "table" should not exist
|
||||
And I run all adhoc tasks
|
||||
# Progress bar should update.
|
||||
And I wait until "Recalculating grades" "text" exists
|
||||
And I should see "100%"
|
||||
# The page should reload after a short delay.
|
||||
Then I wait until "Recalculating grades" "text" does not exist
|
||||
And I set the field "Search users" to "Student 1"
|
||||
And "user-grades" "table" should exist
|
||||
And "40.00" "text" should exist in the "student1@example.com" "table_row"
|
||||
|
||||
Scenario: Admin should see a "Run now" button in the task indicator
|
||||
When I am on the "Test course 1" "grades > Grader report > View" page logged in as admin
|
||||
And I should see "The report will update automatically. You don't need to do anything."
|
||||
And I should not see "0.0%"
|
||||
And I should see "Run now"
|
||||
|
||||
Scenario: Making changes on course with less than 100 grades performs the regrade synchronously, no indicator is shown.
|
||||
Given I am on the "Test assignment 2" "assign activity editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
And I set the field "Rescale existing grades" to "Yes"
|
||||
And I set the field "Maximum grade" to "50"
|
||||
And I press "Save and return to course"
|
||||
When I am on the "Test course 2" "grades > Grader report > View" page
|
||||
Then I should not see "The report will update automatically. You don't need to do anything."
|
||||
And "user-grades" "table" should exist
|
||||
|
||||
Scenario: Editing weights triggers a regrade, but further edits are possible
|
||||
Given I run all adhoc tasks
|
||||
And I am on the "Test course 1" "grades > Grader report > View" page logged in as "teacher1"
|
||||
And I should not see "The report will update automatically. You don't need to do anything."
|
||||
And I am on the "Test course 1" "grades > Gradebook setup" page
|
||||
When I set the field "Override weight of Test assignment 1" to "1"
|
||||
And I press "Save changes"
|
||||
And I am on the "Test course 1" "grades > Grader report > View" page
|
||||
And I should see "The report will update automatically. You don't need to do anything."
|
||||
And I am on the "Test course 1" "grades > Gradebook setup" page
|
||||
And I should not see "The report will update automatically. You don't need to do anything."
|
@ -480,6 +480,9 @@ final class report_graderlib_test extends \advanced_testcase {
|
||||
// Set the grade for the second one to 0 (note, you have to do this after creating it,
|
||||
// otherwise it doesn't create an ungraded grade item).
|
||||
quiz_settings::create($ungradedquiz->id)->get_grade_calculator()->update_quiz_maximum_grade(0);
|
||||
ob_start();
|
||||
$this->run_all_adhoc_tasks();
|
||||
ob_end_clean();
|
||||
|
||||
// Set current user.
|
||||
$this->setUser($manager);
|
||||
|
@ -707,6 +707,7 @@ $string['real'] = 'Real';
|
||||
$string['realletter'] = 'Real (letter)';
|
||||
$string['realpercentage'] = 'Real (percentage)';
|
||||
$string['recalculatinggrades'] = 'Recalculating grades';
|
||||
$string['recalculatinggradesadhoc'] = 'The report will update automatically. You don\'t need to do anything.';
|
||||
$string['recovergradesdefault'] = 'Recover grades default';
|
||||
$string['recovergradesdefault_help'] = 'By default recover old grades when re-enrolling a user in a course.';
|
||||
$string['refreshpreview'] = 'Refresh preview';
|
||||
|
14
lib/amd/build/task_indicator.min.js
vendored
Normal file
14
lib/amd/build/task_indicator.min.js
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
define("core/task_indicator",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default=
|
||||
/**
|
||||
* Task indicator
|
||||
*
|
||||
* Watches the progress bar inside the task indicator for updates, and redirects when the progress is complete.
|
||||
*
|
||||
* @module core/task_indicator
|
||||
* @copyright 2024 Catalyst IT Europe Ltd
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class{static init(id,redirectUrl){const bar=document.getElementById(id);bar.addEventListener("update",(event=>{var _event$detail;const percent=null==event||null===(_event$detail=event.detail)||void 0===_event$detail?void 0:_event$detail.percent;if(percent>0){bar.classList.remove("stored-progress-notstarted");const runlink=document.querySelector(".runlink[data-idnumber=".concat(id,"]"));runlink&&runlink.remove()}""!==redirectUrl&&100===percent&&window.setTimeout((()=>window.location.assign(redirectUrl)),2e3)}))}},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=task_indicator.min.js.map
|
1
lib/amd/build/task_indicator.min.js.map
Normal file
1
lib/amd/build/task_indicator.min.js.map
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"task_indicator.min.js","sources":["../src/task_indicator.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Task indicator\n *\n * Watches the progress bar inside the task indicator for updates, and redirects when the progress is complete.\n *\n * @module core/task_indicator\n * @copyright 2024 Catalyst IT Europe Ltd\n * @author Mark Johnson <mark.johnson@catalyst-eu.net>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n /**\n * Watch the progress bar for updates.\n *\n * When the progress bar is updated to 100%, wait a couple of seconds so the user gets to see it if they are watching,\n * then redirect to the specified URL.\n *\n * @param {String} id The ID of the progress bar element.\n * @param {String} redirectUrl Optional URL to redirect to once the task is complete.\n */\n static init(id, redirectUrl) {\n const bar = document.getElementById(id);\n bar.addEventListener('update', (event) => {\n const percent = event?.detail?.percent;\n if (percent > 0) {\n // Once progress starts, display the progress bar and remove the run link.\n bar.classList.remove('stored-progress-notstarted');\n const runlink = document.querySelector(`.runlink[data-idnumber=${id}]`);\n if (runlink) {\n runlink.remove();\n }\n }\n // Once the progress bar completes, redirect the page.\n if (redirectUrl !== '' && percent === 100) {\n window.setTimeout(() => window.location.assign(redirectUrl), 2000);\n }\n });\n }\n}\n"],"names":["id","redirectUrl","bar","document","getElementById","addEventListener","event","percent","detail","_event$detail","classList","remove","runlink","querySelector","window","setTimeout","location","assign"],"mappings":";;;;;;;;;;;kBAmCgBA,GAAIC,mBACNC,IAAMC,SAASC,eAAeJ,IACpCE,IAAIG,iBAAiB,UAAWC,gCACtBC,QAAUD,MAAAA,6BAAAA,MAAOE,uCAAPC,cAAeF,WAC3BA,QAAU,EAAG,CAEbL,IAAIQ,UAAUC,OAAO,oCACfC,QAAUT,SAASU,+CAAwCb,SAC7DY,SACAA,QAAQD,SAII,KAAhBV,aAAkC,MAAZM,SACtBO,OAAOC,YAAW,IAAMD,OAAOE,SAASC,OAAOhB,cAAc"}
|
54
lib/amd/src/task_indicator.js
Normal file
54
lib/amd/src/task_indicator.js
Normal file
@ -0,0 +1,54 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Task indicator
|
||||
*
|
||||
* Watches the progress bar inside the task indicator for updates, and redirects when the progress is complete.
|
||||
*
|
||||
* @module core/task_indicator
|
||||
* @copyright 2024 Catalyst IT Europe Ltd
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
export default class {
|
||||
/**
|
||||
* Watch the progress bar for updates.
|
||||
*
|
||||
* When the progress bar is updated to 100%, wait a couple of seconds so the user gets to see it if they are watching,
|
||||
* then redirect to the specified URL.
|
||||
*
|
||||
* @param {String} id The ID of the progress bar element.
|
||||
* @param {String} redirectUrl Optional URL to redirect to once the task is complete.
|
||||
*/
|
||||
static init(id, redirectUrl) {
|
||||
const bar = document.getElementById(id);
|
||||
bar.addEventListener('update', (event) => {
|
||||
const percent = event?.detail?.percent;
|
||||
if (percent > 0) {
|
||||
// Once progress starts, display the progress bar and remove the run link.
|
||||
bar.classList.remove('stored-progress-notstarted');
|
||||
const runlink = document.querySelector(`.runlink[data-idnumber=${id}]`);
|
||||
if (runlink) {
|
||||
runlink.remove();
|
||||
}
|
||||
}
|
||||
// Once the progress bar completes, redirect the page.
|
||||
if (redirectUrl !== '' && percent === 100) {
|
||||
window.setTimeout(() => window.location.assign(redirectUrl), 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -29,6 +29,9 @@ class stored_progress_bar extends progress_bar {
|
||||
/** @var bool Can use output buffering. */
|
||||
protected static $supportsoutputbuffering = true;
|
||||
|
||||
/** @var bool Flag to indicate the Javascript module has been initialised already. */
|
||||
protected static $jsloaded = false;
|
||||
|
||||
/** @var int DB record ID */
|
||||
protected $recordid;
|
||||
|
||||
@ -41,15 +44,19 @@ class stored_progress_bar extends progress_bar {
|
||||
/**
|
||||
* This overwrites the progress_bar::__construct method.
|
||||
*
|
||||
* The stored progress bar does not need to check NO_OUTPUT_BUFFERING since it outputs to the page
|
||||
* then polls for updates asynchronously, rather than waiting for synchronous updates in later output.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @param int $width The suggested width.
|
||||
* @param bool $autostart Whether to start the progress bar right away.
|
||||
*/
|
||||
public function __construct($idnumber) {
|
||||
public function __construct(string $idnumber, int $width = 0, bool $autostart = true) {
|
||||
|
||||
$this->clock = \core\di::get(\core\clock::class);
|
||||
|
||||
// Construct from the parent.
|
||||
parent::__construct($idnumber, 0, true);
|
||||
|
||||
parent::__construct($idnumber, $width, $autostart);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,10 +132,10 @@ class stored_progress_bar extends progress_bar {
|
||||
/**
|
||||
* Set the time we started the process.
|
||||
*
|
||||
* @param int $value
|
||||
* @param ?int $value
|
||||
* @return void
|
||||
*/
|
||||
protected function set_time_started(int $value): void {
|
||||
protected function set_time_started(?int $value): void {
|
||||
$this->timestart = $value;
|
||||
}
|
||||
|
||||
@ -185,17 +192,33 @@ class stored_progress_bar extends progress_bar {
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Javascript for stored progress bars.
|
||||
*
|
||||
* The javascript polls the status of all progress bars on the page, so it only needs to be initialised once.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_js(): void {
|
||||
global $PAGE;
|
||||
if (self::$jsloaded) {
|
||||
return;
|
||||
}
|
||||
$PAGE->requires->js_call_amd('core/stored_progress', 'init', [
|
||||
self::get_timeout(),
|
||||
]);
|
||||
self::$jsloaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display the progress bar and start polling via AJAX
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
global $CFG, $PAGE, $OUTPUT;
|
||||
global $OUTPUT;
|
||||
|
||||
$PAGE->requires->js_call_amd('core/stored_progress', 'init', [
|
||||
self::get_timeout(),
|
||||
]);
|
||||
$this->init_js();
|
||||
|
||||
$context = $this->export_for_template($OUTPUT);
|
||||
return $OUTPUT->render_from_template('core/progress_bar', $context);
|
||||
@ -208,11 +231,15 @@ class stored_progress_bar extends progress_bar {
|
||||
* @return array
|
||||
*/
|
||||
public function export_for_template(\renderer_base $output): array {
|
||||
$class = 'stored-progress-bar';
|
||||
if (empty($this->timestart)) {
|
||||
$class .= ' stored-progress-notstarted';
|
||||
}
|
||||
return [
|
||||
'id' => $this->recordid,
|
||||
'idnumber' => $this->idnumber,
|
||||
'width' => $this->width,
|
||||
'class' => 'stored-progress-bar',
|
||||
'class' => $class,
|
||||
'value' => $this->percent,
|
||||
'message' => $this->message,
|
||||
'error' => $this->haserrored,
|
||||
@ -233,8 +260,19 @@ class stored_progress_bar extends progress_bar {
|
||||
$OUTPUT->render_progress_bar($this);
|
||||
}
|
||||
|
||||
// Delete any existing records for this.
|
||||
$this->clear_records();
|
||||
$record = $DB->get_record('stored_progress', ['idnumber' => $this->idnumber]);
|
||||
if ($record) {
|
||||
if ($record->timestart == 0) {
|
||||
// Set the timestart now and return.
|
||||
$record->timestart = $this->timestart;
|
||||
$DB->update_record('stored_progress', $record);
|
||||
$this->recordid = $record->id;
|
||||
return $this->recordid;
|
||||
} else {
|
||||
// Delete any existing records for this.
|
||||
$this->clear_records();
|
||||
}
|
||||
}
|
||||
|
||||
// Create new progress record.
|
||||
$this->recordid = $DB->insert_record('stored_progress', [
|
||||
@ -362,4 +400,25 @@ class stored_progress_bar extends progress_bar {
|
||||
return $CFG->progresspollinterval ?? 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a progress bar record in a pending state.
|
||||
*
|
||||
* @return int ID of the DB record
|
||||
*/
|
||||
public function store_pending(): int {
|
||||
global $DB;
|
||||
|
||||
// Delete any existing records for this.
|
||||
$this->clear_records();
|
||||
|
||||
// Create new progress record.
|
||||
$this->recordid = $DB->insert_record('stored_progress', [
|
||||
'idnumber' => $this->idnumber,
|
||||
'timestart' => $this->timestart,
|
||||
'message' => '',
|
||||
]);
|
||||
|
||||
return $this->recordid;
|
||||
}
|
||||
|
||||
}
|
||||
|
137
lib/classes/output/task_indicator.php
Normal file
137
lib/classes/output/task_indicator.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?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\output;
|
||||
|
||||
use core\plugin_manager;
|
||||
use core\task\adhoc_task;
|
||||
use core\task\stored_progress_task_trait;
|
||||
use core\url;
|
||||
use core\context\system;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Indicator for displaying status and progress of a background task
|
||||
*
|
||||
* This will display a section containing an icon, heading and message describing the background task being performed,
|
||||
* as well as a progress bar that is updated as the task progresses. Optionally, it will redirect to a given URL (or reload
|
||||
* the current one) when the task completes. If the task is still waiting in the queue, an admin viewing the indicator
|
||||
* will also see a "Run now" button.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class task_indicator implements renderable, templatable {
|
||||
/** @var ?stdClass $taskrecord */
|
||||
protected ?stdClass $taskrecord;
|
||||
|
||||
/** @var ?stored_progress_bar $progressbar */
|
||||
protected ?stored_progress_bar $progressbar;
|
||||
|
||||
/** @var ?url $runurl The URL to manually run the task. */
|
||||
protected ?url $runurl = null;
|
||||
|
||||
/** @var string $runlabel Label for the link to run the task. */
|
||||
protected string $runlabel = '';
|
||||
|
||||
/**
|
||||
* Find the task record, and get the progress bar object.
|
||||
*
|
||||
* @param adhoc_task $task The task whose progress is being indicated. The task class must use stored_progress_task_trait.
|
||||
* @param string $heading The header text for the indicator.
|
||||
* @param string $message A message to explain what is happening while the task is running.
|
||||
* @param ?url $redirecturl An optional URL to redirect to when the task completes.
|
||||
* @param ?pix_icon $icon An optional icon to display with the heading.
|
||||
* @param array $extraclasses Extra class names to apply to the indicator's container.
|
||||
* @throws \coding_exception
|
||||
*/
|
||||
public function __construct(
|
||||
/** @var adhoc_task $task The task whose progress is being indicated. The task class must use stored_progress_task_trait. */
|
||||
protected adhoc_task $task,
|
||||
/** @var string $heading The header text for the indicator. */
|
||||
protected string $heading,
|
||||
/** @var string $message A message to explain what is happening while the task is running. */
|
||||
protected string $message,
|
||||
/** @var ?url $redirecturl An optional URL to redirect to when the task completes. */
|
||||
protected ?url $redirecturl = null,
|
||||
/** @var ?pix_icon $icon An optional icon to display with the heading. */
|
||||
protected ?pix_icon $icon = new pix_icon('i/timer', ''),
|
||||
/** @var array $extraclasses Extra class names to apply to the indicator's container. */
|
||||
protected array $extraclasses = [],
|
||||
) {
|
||||
if (!class_uses($task::class, stored_progress_task_trait::class)) {
|
||||
throw new \coding_exception('task_indicator can only be used for tasks using stored_progress_task_trait.');
|
||||
}
|
||||
$this->setup_task_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the task record matching the current task, if there is one.
|
||||
*
|
||||
* If one exists, also set up a progress bar, and set up the "run now" link if permitted.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setup_task_data(): void {
|
||||
$this->taskrecord = \core\task\manager::get_queued_adhoc_task_record($this->task) ?: null;
|
||||
if ($this->taskrecord) {
|
||||
$this->task->set_id($this->taskrecord->id);
|
||||
$idnumber = stored_progress_bar::convert_to_idnumber($this->task::class, $this->task->get_id());
|
||||
$this->progressbar = stored_progress_bar::get_by_idnumber($idnumber);
|
||||
// As long as the tool_task plugin hasn't been removed,
|
||||
// allow admins to trigger the task manually if it's not running yet.
|
||||
if (
|
||||
array_key_exists('task', plugin_manager::instance()->get_present_plugins('tool'))
|
||||
&& is_null($this->taskrecord->timestarted)
|
||||
&& has_capability('moodle/site:config', system::instance())
|
||||
) {
|
||||
$this->runurl = new url('/admin/tool/task/run_adhoctasks.php', ['id' => $this->taskrecord->id]);
|
||||
$this->runlabel = get_string('runnow', 'tool_task');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a task record matching the defined task.
|
||||
*
|
||||
* If so, set the task ID and progress bar, then return true. If not, return false.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_task_record(): bool {
|
||||
$this->setup_task_data();
|
||||
return !is_null($this->taskrecord);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function export_for_template(renderer_base $output): array {
|
||||
$export = [];
|
||||
if ($this->taskrecord) {
|
||||
$export['heading'] = $this->heading;
|
||||
$export['message'] = $this->message;
|
||||
$export['progress'] = $this->progressbar->export_for_template($output);
|
||||
$export['icon'] = $this->icon ? $this->icon->export_for_template($output) : '';
|
||||
$export['redirecturl'] = $this->redirecturl?->out();
|
||||
$export['extraclasses'] = implode(' ', $this->extraclasses);
|
||||
$export['runurl'] = $this->runurl?->out();
|
||||
$export['runlabel'] = $this->runlabel;
|
||||
$this->progressbar->init_js();
|
||||
}
|
||||
return $export;
|
||||
}
|
||||
}
|
60
lib/classes/progress/stored.php
Normal file
60
lib/classes/progress/stored.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?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\progress;
|
||||
|
||||
use core\output\stored_progress_bar;
|
||||
|
||||
/**
|
||||
* Progress handler which updates a stored progress bar.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 Catalyst IT Europe Ltd.
|
||||
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class stored extends base {
|
||||
/**
|
||||
* Constructs the progress reporter.
|
||||
*
|
||||
* @param stored_progress_bar $bar The stored progress bar to update.
|
||||
*/
|
||||
public function __construct(
|
||||
/**
|
||||
* @var stored_progress_bar $bar The stored progress bar to update.
|
||||
*/
|
||||
protected stored_progress_bar $bar,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress in the database.
|
||||
* Database update frequency is set by $interval.
|
||||
*
|
||||
* @see \core\progress\base::update_progress()
|
||||
*/
|
||||
public function update_progress() {
|
||||
// Get progress.
|
||||
[$min] = $this->get_progress_proportion_range();
|
||||
|
||||
$message = '';
|
||||
if ($this->is_in_progress_section()) {
|
||||
$message = $this->get_current_description();
|
||||
}
|
||||
// Update progress bar.
|
||||
$this->bar->update_full($min * 100, $message);
|
||||
}
|
||||
}
|
@ -194,7 +194,7 @@ class manager {
|
||||
* @param adhoc_task $task
|
||||
* @return \stdClass|false
|
||||
*/
|
||||
protected static function get_queued_adhoc_task_record($task) {
|
||||
public static function get_queued_adhoc_task_record($task) {
|
||||
global $DB;
|
||||
|
||||
$record = self::record_from_adhoc_task($task);
|
||||
|
@ -16,6 +16,10 @@
|
||||
|
||||
namespace core\task;
|
||||
|
||||
use core\progress\db_updater;
|
||||
use core\progress\stored;
|
||||
use core\output\stored_progress_bar;
|
||||
|
||||
/**
|
||||
* Trait to use in tasks to automatically add stored progress functionality.
|
||||
*
|
||||
@ -25,10 +29,44 @@ namespace core\task;
|
||||
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
|
||||
*/
|
||||
trait stored_progress_task_trait {
|
||||
|
||||
/** @var \core\output\stored_progress_bar|null $progress */
|
||||
/** @var ?stored_progress_bar $progress */
|
||||
protected $progress = null;
|
||||
|
||||
/**
|
||||
* Construct a unique name for the progress bar.
|
||||
*
|
||||
* For adhoc tasks, this will need the ID in it. For scheduled tasks just the class name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_progress_name(): string {
|
||||
if (method_exists($this, 'get_id')) {
|
||||
return get_class($this) . '_' . $this->get_id();
|
||||
} else {
|
||||
return get_class($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise a stored progress record.
|
||||
*/
|
||||
public function initialise_stored_progress(): void {
|
||||
$this->progress = new stored_progress_bar(
|
||||
stored_progress_bar::convert_to_idnumber($this->get_progress_name()),
|
||||
autostart: false,
|
||||
);
|
||||
$this->progress->store_pending();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a stored object for the stored progress record.
|
||||
*
|
||||
* @return stored
|
||||
*/
|
||||
public function get_progress(): stored {
|
||||
return new stored($this->progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a stored progress bar implementation for the task this trait is used in.
|
||||
*
|
||||
@ -40,16 +78,8 @@ trait stored_progress_task_trait {
|
||||
// To get around the issue in MDL-80770, we are manually setting the renderer to cli.
|
||||
$OUTPUT = $PAGE->get_renderer('core', null, 'cli');
|
||||
|
||||
// Construct a unique name for the progress bar.
|
||||
// For adhoc tasks, this will need the ID in it. For scheduled tasks just the class name.
|
||||
if (method_exists($this, 'get_id')) {
|
||||
$name = get_class($this) . '_' . $this->get_id();
|
||||
} else {
|
||||
$name = get_class($this);
|
||||
}
|
||||
|
||||
$this->progress = new \core\output\stored_progress_bar(
|
||||
\core\output\stored_progress_bar::convert_to_idnumber($name)
|
||||
$this->progress = new stored_progress_bar(
|
||||
stored_progress_bar::convert_to_idnumber($this->get_progress_name())
|
||||
);
|
||||
|
||||
// Start the progress.
|
||||
|
@ -444,7 +444,7 @@ final class grade_category_test extends \grade_base_testcase {
|
||||
$category_grade_item = $grade_category->get_grade_item();
|
||||
|
||||
// This creates all the grade_grades we need.
|
||||
grade_regrade_final_grades($this->courseid);
|
||||
grade_regrade_final_grades($this->courseid, async: true);
|
||||
|
||||
$grade = $DB->get_record('grade_grades', array('itemid'=>$category_grade_item->id, 'userid'=>$this->userid));
|
||||
$this->assertWithinMargin($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
|
||||
|
@ -383,43 +383,16 @@ function grade_needs_regrade_progress_bar($courseid) {
|
||||
* @return moodle_url|false The URL to redirect to if redirecting
|
||||
*/
|
||||
function grade_regrade_final_grades_if_required($course, ?callable $callback = null) {
|
||||
global $PAGE, $OUTPUT;
|
||||
global $PAGE;
|
||||
|
||||
if (!grade_needs_regrade_final_grades($course->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (grade_needs_regrade_progress_bar($course->id)) {
|
||||
if ($PAGE->state !== moodle_page::STATE_IN_BODY) {
|
||||
$PAGE->set_heading($course->fullname);
|
||||
echo $OUTPUT->header();
|
||||
}
|
||||
echo $OUTPUT->heading(get_string('recalculatinggrades', 'grades'));
|
||||
$progress = new \core\progress\display(true);
|
||||
$status = grade_regrade_final_grades($course->id, null, null, $progress);
|
||||
|
||||
// Show regrade errors and set the course to no longer needing regrade (stop endless loop).
|
||||
if (is_array($status)) {
|
||||
foreach ($status as $error) {
|
||||
$errortext = new \core\output\notification($error, \core\output\notification::NOTIFY_ERROR);
|
||||
echo $OUTPUT->render($errortext);
|
||||
}
|
||||
$courseitem = grade_item::fetch_course_item($course->id);
|
||||
$courseitem->regrading_finished();
|
||||
}
|
||||
|
||||
if ($callback) {
|
||||
//
|
||||
$url = call_user_func($callback);
|
||||
}
|
||||
|
||||
if (empty($url)) {
|
||||
$url = $PAGE->url;
|
||||
}
|
||||
|
||||
echo $OUTPUT->continue_button($url);
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
// Queue ad-hoc task and redirect.
|
||||
grade_regrade_final_grades($course->id, async: true);
|
||||
return $callback ? call_user_func($callback) : $PAGE->url;
|
||||
} else {
|
||||
$result = grade_regrade_final_grades($course->id);
|
||||
if ($callback) {
|
||||
@ -1153,9 +1126,10 @@ function grade_recover_history_grades($userid, $courseid) {
|
||||
* @param int $userid If specified try to do a quick regrading of the grades of this user only
|
||||
* @param object $updated_item Optional grade item to be marked for regrading. It is required if $userid is set.
|
||||
* @param \core\progress\base $progress If provided, will be used to update progress on this long operation.
|
||||
* @param bool $async If true, and we are recalculating an entire course's grades, defer processing to an ad-hoc task.
|
||||
* @return array|true true if ok, array of errors if problems found. Grade item id => error message
|
||||
*/
|
||||
function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null) {
|
||||
function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null, $progress=null, bool $async = false) {
|
||||
// This may take a very long time and extra memory.
|
||||
\core_php_time_limit::raise();
|
||||
raise_memory_limit(MEMORY_EXTRA);
|
||||
@ -1179,6 +1153,35 @@ function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null,
|
||||
} else {
|
||||
if (!$course_item->needsupdate) {
|
||||
// nothing to do :-)
|
||||
if ($progress instanceof \core\progress\stored) {
|
||||
// The regrade was already run elsewhere without the stored progress, so just start and end it now.
|
||||
$progress->start_progress(get_string('recalculatinggrades', 'grades'));
|
||||
$progress->end_progress();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Defer recalculation to an ad-hoc task.
|
||||
if ($async) {
|
||||
$regradecache = cache::make_from_params(
|
||||
mode: cache_store::MODE_REQUEST,
|
||||
component: 'core',
|
||||
area: 'grade_regrade_final_grades',
|
||||
options: [
|
||||
'simplekeys' => true,
|
||||
'simpledata' => true,
|
||||
],
|
||||
);
|
||||
// If the courseid already exists in the cache, return so we don't do this multiple times per request.
|
||||
if ($regradecache->get($courseid)) {
|
||||
return true;
|
||||
}
|
||||
$task = \core_course\task\regrade_final_grades::create($courseid);
|
||||
$taskid = \core\task\manager::queue_adhoc_task($task, true);
|
||||
if ($taskid) {
|
||||
$task->set_id($taskid);
|
||||
$task->initialise_stored_progress();
|
||||
}
|
||||
$regradecache->set($courseid, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1221,7 +1224,7 @@ function grade_regrade_final_grades($courseid, $userid=null, $updated_item=null,
|
||||
}
|
||||
}
|
||||
|
||||
$progress->start_progress('regrade_course', $progresstotal);
|
||||
$progress->start_progress(get_string('recalculatinggrades', 'grades'), $progresstotal);
|
||||
|
||||
$errors = array();
|
||||
$finalids = array();
|
||||
@ -1601,7 +1604,7 @@ function grade_course_reset($courseid) {
|
||||
grade_grab_course_grades($courseid);
|
||||
|
||||
// recalculate all grades
|
||||
grade_regrade_final_grades($courseid);
|
||||
grade_regrade_final_grades($courseid, async: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -5326,7 +5326,7 @@ function reset_course_userdata($data) {
|
||||
if (!empty($data->reset_gradebook_items)) {
|
||||
remove_course_grades($data->courseid, false);
|
||||
grade_grab_course_grades($data->courseid);
|
||||
grade_regrade_final_grades($data->courseid);
|
||||
grade_regrade_final_grades($data->courseid, async: true);
|
||||
$status[] = array('component' => $componentstr, 'item' => get_string('removeallcourseitems', 'grades'), 'error' => false);
|
||||
|
||||
} else if (!empty($data->reset_gradebook_grades)) {
|
||||
|
@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div style="flex: 1 1 0; min-width: 0;">
|
||||
<div id="{{idnumber}}_status" class="text-truncate"> </div>
|
||||
<div id="{{idnumber}}_status" class="text-truncate">{{message}}</div>
|
||||
</div>
|
||||
<div class="text-end ps-3" style="flex: 0 0 content">
|
||||
<span id="{{idnumber}}_estimate" class=""> </span>
|
||||
|
68
lib/templates/task_indicator.mustache
Normal file
68
lib/templates/task_indicator.mustache
Normal file
@ -0,0 +1,68 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/task_indicator
|
||||
|
||||
Display the stored progress of a background task and optionally redirect when it is complete.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"heading": "Regrade in progress",
|
||||
"icon": {
|
||||
"attributes": [
|
||||
{"name": "src", "value": "/pix/i/timer.svg"},
|
||||
{"name": "alt", "value": ""}
|
||||
]
|
||||
},
|
||||
"message": "Grades are being recalculated due to recent changes.",
|
||||
"progress": {
|
||||
"id": "progressbar_test",
|
||||
"message": "Recalculating grades",
|
||||
"idnumber": "progressbar_test",
|
||||
"class": "stored-progress-bar",
|
||||
"width": "500",
|
||||
"value": "50"
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div class="task-indicator {{extraclasses}}">
|
||||
<div class="text-center">
|
||||
{{#icon}}
|
||||
<div>
|
||||
{{>core/pix_icon}}
|
||||
</div>
|
||||
{{/icon}}
|
||||
<h2>{{heading}}</h2>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
{{#runurl}}
|
||||
<p class="text-center">
|
||||
<a class="runlink btn btn-primary" href="{{runurl}}" data-idnumber="{{progress.idnumber}}">{{runlabel}}</a>
|
||||
</p>
|
||||
{{/runurl}}
|
||||
{{#progress}}
|
||||
{{>core/progress_bar}}
|
||||
{{/progress}}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{{#js}}
|
||||
require(['core/task_indicator'], function(TaskIndicator) {
|
||||
TaskIndicator.init('{{progress.idnumber}}', '{{redirecturl}}');
|
||||
});
|
||||
{{/js}}
|
@ -45,6 +45,7 @@ Feature: Teacher can reset H5P activity grades
|
||||
And I click on "Reset course" "button" in the "Reset course?" "dialogue"
|
||||
Then I should see "Done" in the "Gradebook" "table_row"
|
||||
And I press "Continue"
|
||||
And I run all adhoc tasks
|
||||
# Confirm that previously saved grades are gone
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should not see "7.00" in the "First Student" "table_row"
|
||||
|
@ -139,6 +139,7 @@ final class grader_test extends \advanced_testcase {
|
||||
$grader->grade_item_update($param);
|
||||
|
||||
// Check new grade item and grades.
|
||||
grade_regrade_final_grades($course->id);
|
||||
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
|
||||
$item = array_shift($gradeinfo->items);
|
||||
$this->assertEquals($scaleid, $item->scaleid);
|
||||
@ -245,6 +246,7 @@ final class grader_test extends \advanced_testcase {
|
||||
$grader->update_grades($userid);
|
||||
|
||||
// Check new grade item and grades.
|
||||
grade_regrade_final_grades($course->id);
|
||||
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
|
||||
$item = array_shift($gradeinfo->items);
|
||||
$this->assertArrayHasKey($user1->id, $item->grades);
|
||||
|
11
pix/i/timer.svg
Normal file
11
pix/i/timer.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="109" height="109" viewBox="0 0 109 109" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Timer">
|
||||
<path id="Vector" d="M54.48 108.96C84.5685 108.96 108.96 84.5685 108.96 54.48C108.96 24.3915 84.5685 0 54.48 0C24.3915 0 0 24.3915 0 54.48C0 84.5685 24.3915 108.96 54.48 108.96Z" fill="#C4E5FF"/>
|
||||
<g id="timer">
|
||||
<path id="Vector_2" d="M77.4187 27.8948L69.895 35.4184C68.8484 36.465 68.8484 38.1619 69.895 39.2085C70.9416 40.2551 72.6385 40.2551 73.6851 39.2085L81.2087 31.6849C82.2553 30.6383 82.2554 28.9414 81.2087 27.8948C80.1621 26.8482 78.4653 26.8482 77.4187 27.8948Z" fill="#0056B2"/>
|
||||
<path id="Vector_3" d="M60.32 16H49.68C48.1999 16 47 17.1999 47 18.68C47 20.1601 48.1999 21.36 49.68 21.36H60.32C61.8001 21.36 63 20.1601 63 18.68C63 17.1999 61.8001 16 60.32 16Z" fill="#0056B2"/>
|
||||
<path id="Vector_4" d="M86.7 58.85C86.7 76.4403 72.4403 90.7 54.85 90.7C37.2597 90.7 23 76.4403 23 58.85C23 41.2597 37.2597 27 54.85 27C72.4403 27 86.7 41.2597 86.7 58.85Z" fill="white" stroke="#0056B2" stroke-width="4"/>
|
||||
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M51.64 43.68C51.64 42.1999 52.8399 41 54.32 41C55.8001 41 57 42.1999 57 43.68V56H64.9509C66.431 56 67.6309 57.1999 67.6309 58.68C67.6309 60.1601 66.431 61.36 64.9509 61.36H54.3109C52.8307 61.36 51.6309 60.1601 51.6309 58.68C51.6309 58.6049 51.634 58.5304 51.64 58.4569V43.68Z" fill="#0056B2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -48,3 +48,4 @@
|
||||
@import "moodle/moodlenet";
|
||||
@import "moodle/dropdown";
|
||||
@import "moodle/deprecated";
|
||||
@import "moodle/task-indicator";
|
||||
|
25
theme/boost/scss/moodle/task-indicator.scss
Normal file
25
theme/boost/scss/moodle/task-indicator.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.task-indicator {
|
||||
padding-top: 2rem;
|
||||
|
||||
img.icon {
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
max-height: 145px;
|
||||
max-width: 145px;
|
||||
font-size: 145px;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progressbar_container {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.stored-progress-bar {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.stored-progress-notstarted {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@ -47238,6 +47238,28 @@ input[type=button].btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-indicator {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
.task-indicator img.icon {
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
max-height: 145px;
|
||||
max-width: 145px;
|
||||
font-size: 145px;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.task-indicator .progressbar_container {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
.task-indicator .stored-progress-bar {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.task-indicator .stored-progress-notstarted {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
@ -47172,6 +47172,28 @@ input[type=button].btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-indicator {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
.task-indicator img.icon {
|
||||
width: 145px;
|
||||
height: 145px;
|
||||
max-height: 145px;
|
||||
max-width: 145px;
|
||||
font-size: 145px;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.task-indicator .progressbar_container {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
.task-indicator .stored-progress-bar {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.task-indicator .stored-progress-notstarted {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
Loading…
x
Reference in New Issue
Block a user