MDL-68913 Assign: Per attempt timing

This patch provides functionality to allow the
assign activity to have timed attempts for submissions.
It includes the logic, UI and adminstration changes
fot timed submissions.
This commit is contained in:
Matt Porritt 2020-06-21 17:53:06 +00:00 committed by Cameron Ball
parent d135a1200a
commit eaa1f56704
27 changed files with 771 additions and 95 deletions

2
mod/assign/amd/build/timer.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
define ("mod_assign/timer",["exports","core/notification","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var d=0,e=null,f=null,g=function(a){var b=Math.floor(a/3600),c=Math.floor(a/60)%60;return[b,c,a%60].filter(function(a,b){return 0!==a||0<b}).map(function(a){return"".concat(a).padStart(2,"0")}).join(":")},h=function(){if(e){clearTimeout(e)}},i=function(){var a=new Date().getTime(),j=Math.floor((d-a)/1e3);if(0>=j){f.classList.add("alert","alert-danger");f.innerHTML="00:00:00";if(document.getElementById("mod_assign_timelimit_block")){(0,c.get_string)("caneditsubmission","mod_assign").done(function(a){return b.default.addNotification({type:"error",message:a})}).fail(b.default.exception)}h();return}else if(300>j){f.classList.remove("alert-warning");f.classList.add("alert","alert-danger")}else if(900>j){f.classList.remove("alert-danger");f.classList.add("alert","alert-warning")}f.innerHTML=g(j);e=setTimeout(i,500)},j=function(a){f=document.getElementById(a);d=M.pageloadstarttime.getTime()+1e3*f.dataset.starttime;i()};a.init=j});
//# sourceMappingURL=timer.min.js.map

File diff suppressed because one or more lines are too long

130
mod/assign/amd/src/timer.js Normal file
View File

@ -0,0 +1,130 @@
// 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/>.
import Notification from 'core/notification';
import {get_string as getString} from 'core/str';
/**
* A javascript module for the time in the assign module.
*
* @copyright 2020 Matt Porritt <mattp@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Timestamp at which time runs out.
*
* @property {Number} endTime
*/
let endTime = 0;
/**
* ID of the timeout that updates the clock.
*
* @property {Number} timeoutId
*/
let timeoutId = null;
/**
* The timer element.
*
* @property {Element} timer
*/
let timer = null;
/**
* Helper method to convert time remaining in seconds into HH:MM:SS format.
*
* @method formatSeconds
* @param {Number} secs Time remaining in seconds to get value for.
* @return {String} Time remaining in HH:MM:SS format.
*/
const formatSeconds = (secs) => {
const hours = Math.floor(secs / 3600);
const minutes = Math.floor(secs / 60) % 60;
const seconds = secs % 60;
return [hours, minutes, seconds]
// Remove the hours column if there is less than 1 hour left.
.filter((value, index) => value !== 0 || index > 0)
// Ensure that all fields are two digit numbers.
.map(value => `${value}`.padStart(2, '0'))
.join(":");
};
/**
* Stop the timer, if it is running.
*
* @method stop
*/
const stop = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
/**
* Function to update the clock with the current time left.
*
* @method update
*/
const update = () => {
const now = new Date().getTime();
const secondsLeft = Math.floor((endTime - now) / 1000);
// If time has expired, set the hidden form field that says time has expired.
if (secondsLeft <= 0) {
timer.classList.add('alert', 'alert-danger');
timer.innerHTML = '00:00:00';
// Only add a notification on the assign submission page.
if (document.getElementById("mod_assign_timelimit_block")) {
getString('caneditsubmission', 'mod_assign')
.done(
str => Notification.addNotification({
type: "error",
message: str
})
).fail(Notification.exception);
}
stop();
return;
} else if (secondsLeft < 300) { // Add danger style when less than 5 minutes left.
timer.classList.remove('alert-warning');
timer.classList.add('alert', 'alert-danger');
} else if (secondsLeft < 900) { // Add warning style when less than 15 minutes left.
timer.classList.remove('alert-danger');
timer.classList.add('alert', 'alert-warning');
}
// Update the time display.
timer.innerHTML = formatSeconds(secondsLeft);
// Arrange for this method to be called again soon.
timeoutId = setTimeout(update, 500);
};
/**
* Set up the submission timer.
*
* @method init
* @param {Number} timerId Unique ID of the timer element.
*/
export const init = (timerId) => {
timer = document.getElementById(timerId);
endTime = M.pageloadstarttime.getTime() + (timer.dataset.starttime * 1000);
update();
};

View File

@ -75,6 +75,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'sendstudentnotifications',
'duedate',
'cutoffdate',
'timelimit',
'gradingduedate',
'allowsubmissionsfromdate',
'grade',
@ -110,6 +111,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
array('userid',
'timecreated',
'timemodified',
'timestarted',
'status',
'groupid',
'attemptnumber',

View File

@ -112,6 +112,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$data->teamsubmissiongroupingid = 0;
}
if (!isset($data->timelimit)) {
$data->timelimit = 0;
}
if (!isset($data->cutoffdate)) {
$data->cutoffdate = 0;
}

View File

@ -47,6 +47,8 @@ class assign_header implements \renderable {
public $postfix;
/** @var \moodle_url|null $subpageurl link for the sub page */
public $subpageurl;
/** @var bool $activity optional show activity text. */
public $activity;
/**
* Constructor
@ -59,6 +61,7 @@ class assign_header implements \renderable {
* @param string $preface An optional preface to show before the heading.
* @param string $postfix An optional postfix to show after the intro.
* @param \moodle_url|null $subpageurl An optional sub page URL link for the subpage.
* @param bool $activity Optional show activity text if true.
*/
public function __construct(
\stdClass $assign,
@ -68,7 +71,8 @@ class assign_header implements \renderable {
$subpage = '',
$preface = '',
$postfix = '',
\moodle_url $subpageurl = null
\moodle_url $subpageurl = null,
bool $activity = false
) {
$this->assign = $assign;
$this->context = $context;
@ -78,5 +82,6 @@ class assign_header implements \renderable {
$this->preface = $preface;
$this->postfix = $postfix;
$this->subpageurl = $subpageurl;
$this->activity = $activity;
}
}

View File

@ -96,6 +96,8 @@ class assign_submission_status implements \renderable {
public $preventsubmissionnotingroup = 0;
/** @var array usergroups */
public $usergroups = array();
/** @var int The time limit for the assignment */
public $timelimit = 0;
/**
* Constructor
@ -130,6 +132,7 @@ class assign_submission_status implements \renderable {
* @param string $gradingstatus The submission status (ie. Graded, Not Released etc).
* @param bool $preventsubmissionnotingroup Prevent submission if user is not in a group.
* @param array $usergroups Array containing all groups the user is assigned to.
* @param int $timelimit The time limit for the assignment.
*/
public function __construct(
$allowsubmissionsfromdate,
@ -161,7 +164,8 @@ class assign_submission_status implements \renderable {
$maxattempts,
$gradingstatus,
$preventsubmissionnotingroup,
$usergroups
$usergroups,
$timelimit
) {
$this->allowsubmissionsfromdate = $allowsubmissionsfromdate;
$this->alwaysshowdescription = $alwaysshowdescription;
@ -193,5 +197,6 @@ class assign_submission_status implements \renderable {
$this->gradingstatus = $gradingstatus;
$this->preventsubmissionnotingroup = $preventsubmissionnotingroup;
$this->usergroups = $usergroups;
$this->timelimit = $timelimit;
}
}

View File

@ -24,6 +24,8 @@
namespace mod_assign\output;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/assign/locallib.php');
use \mod_assign\output\grading_app;
@ -263,6 +265,9 @@ class renderer extends \plugin_renderer_base {
if ($header->showintro) {
$o .= $this->output->box_start('generalbox boxaligncenter', 'intro');
$o .= format_module_intro('assign', $header->assign, $header->coursemoduleid);
if ($header->activity) {
$o .= $this->format_activity_text($header->assign, $header->coursemoduleid);
}
$o .= $header->postfix;
$o .= $this->output->box_end();
}
@ -467,7 +472,6 @@ class renderer extends \plugin_renderer_base {
$o = '';
$o .= $this->output->container_start('submissionstatustable');
$o .= $this->output->heading(get_string('submission', 'assign'), 3);
$time = time();
if ($status->teamsubmissionenabled) {
$group = $status->submissiongroup;
@ -567,33 +571,20 @@ class renderer extends \plugin_renderer_base {
// Extension date.
$duedate = $status->extensionduedate;
}
}
// Time remaining.
$classname = 'timeremaining';
if ($duedate - $time <= 0) {
if (!$submission ||
$submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
if ($status->submissionsenabled) {
$remaining = get_string('overdue', 'assign', format_time($time - $duedate));
$classname = 'overdue';
} else {
$remaining = get_string('duedatereached', 'assign');
}
} else {
if ($submission->timemodified > $duedate) {
$remaining = get_string('submittedlate',
'assign',
format_time($submission->timemodified - $duedate));
$classname = 'latesubmission';
} else {
$remaining = get_string('submittedearly',
'assign',
format_time($submission->timemodified - $duedate));
$classname = 'earlysubmission';
}
}
} else {
$remaining = get_string('paramtimeremaining', 'assign', format_time($duedate - $time));
// Time remaining.
// Only add the row if there is a due date, or a countdown.
if ($status->duedate > 0 || !empty($submission->timestarted)) {
[$remaining, $classname] = $this->get_time_remaining($status);
// If the assignment is not submitted, and there is a submission in progress,
// Add a heading for the time limit.
if (!empty($submission) &&
$submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED &&
!empty($submission->timestarted)
) {
$o .= $this->output->container(get_string('timeremaining', 'assign'));
}
$o .= $this->output->container($remaining, $classname);
}
@ -801,36 +792,22 @@ class renderer extends \plugin_renderer_base {
$this->add_table_row_tuple($t, $cell1content, $cell2content);
$duedate = $status->extensionduedate;
}
}
// Time remaining.
// Time remaining.
// Only add the row if there is a due date, or a countdown.
if ($status->duedate > 0 || !empty($submission->timestarted)) {
$cell1content = get_string('timeremaining', 'assign');
$cell2attributes = [];
if ($duedate - $time <= 0) {
if (!$submission ||
$submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
if ($status->submissionsenabled) {
$cell2content = get_string('overdue', 'assign', format_time($time - $duedate));
$cell2attributes = array('class' => 'overdue');
} else {
$cell2content = get_string('duedatereached', 'assign');
}
} else {
if ($submission->timemodified > $duedate) {
$cell2content = get_string('submittedlate',
'assign',
format_time($submission->timemodified - $duedate));
$cell2attributes = array('class' => 'latesubmission');
} else {
$cell2content = get_string('submittedearly',
'assign',
format_time($submission->timemodified - $duedate));
$cell2attributes = array('class' => 'earlysubmission');
}
}
} else {
$cell2content = format_time($duedate - $time);
}
$this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes);
[$cell2content, $cell2attributes] = $this->get_time_remaining($status);
$this->add_table_row_tuple($t, $cell1content, $cell2content, [], ['class' => $cell2attributes]);
}
// Add time limit info if there is one.
$timelimitenabled = get_config('assign', 'enabletimelimit') && $status->timelimit > 0;
if ($timelimitenabled && $status->timelimit > 0) {
$cell1content = get_string('timelimit', 'assign');
$cell2content = format_time($status->timelimit);
$this->add_table_row_tuple($t, $cell1content, $cell2content, [], []);
}
// Show graders whether this submission is editable by students.
@ -850,7 +827,7 @@ class renderer extends \plugin_renderer_base {
if (!empty($status->gradingcontrollerpreview)) {
$cell1content = get_string('gradingmethodpreview', 'assign');
$cell2content = $status->gradingcontrollerpreview;
$this->add_table_row_tuple($t, $cell1content, $cell2content, [], $cell2attributes);
$this->add_table_row_tuple($t, $cell1content, $cell2content, [], []);
}
// Last modified.
@ -899,8 +876,30 @@ class renderer extends \plugin_renderer_base {
if (!$submission || $submission->status == ASSIGN_SUBMISSION_STATUS_NEW) {
$o .= $this->output->box_start('generalbox submissionaction');
$urlparams = array('id' => $status->coursemoduleid, 'action' => 'editsubmission');
$o .= $this->output->single_button(new \moodle_url('/mod/assign/view.php', $urlparams),
get_string('addsubmission', 'assign'), 'get');
if ($timelimitenabled && empty($submission->timestarted)) {
$confirmation = new \confirm_action(
get_string(
'confirmstart',
'assign',
format_time($status->timelimit)
),
null,
get_string('beginassignment', 'assign')
);
$o .= $this->output->action_link(
new \moodle_url('/mod/assign/view.php', $urlparams),
get_string('beginassignment', 'assign'),
$confirmation,
array('class' => 'btn btn-primary')
);
} else {
$o .= $this->output->single_button(
new \moodle_url('/mod/assign/view.php', $urlparams),
get_string('addsubmission', 'assign'), 'get', array('primary' => true)
);
}
$o .= $this->output->box_start('boxaligncenter submithelp');
$o .= get_string('addsubmission_help', 'assign');
$o .= $this->output->box_end();
@ -1379,7 +1378,60 @@ class renderer extends \plugin_renderer_base {
return $o;
}
/**
* Get the time remaining for a submission.
*
* @param \mod_assign\output\assign_submission_status $status
* @return array The first element is the time remaining as a human readable
* string and the second is a CSS class.
*/
protected function get_time_remaining(\mod_assign\output\assign_submission_status $status): array {
$time = time();
$submission = $status->teamsubmission ? $status->teamsubmission : $status->submission;
$timelimitenabled = get_config('assign', 'enabletimelimit') && $status->timelimit > 0 && $submission->timestarted;
$duedatereached = $status->duedate > 0 && $status->duedate - $time <= 0;
$timelimitenabledbeforeduedate = $timelimitenabled && !$duedatereached;
// There is a submission, display the relevant early/late message.
if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
$latecalculation = $submission->timemodified - ($timelimitenabledbeforeduedate ? $submission->timecreated : 0);
$latethreshold = $timelimitenabledbeforeduedate ? $status->timelimit : $status->duedate;
$earlystring = $timelimitenabledbeforeduedate ? 'submittedundertime' : 'submittedearly';
$latestring = $timelimitenabledbeforeduedate ? 'submittedovertime' : 'submittedlate';
$ontime = $latecalculation <= $latethreshold;
return [
get_string(
$ontime ? $earlystring : $latestring,
'assign',
format_time($latecalculation - $latethreshold)
),
$ontime ? 'earlysubmission' : 'latesubmission'
];
}
// There is no submission, due date has passed, show assignment is overdue.
if ($duedatereached) {
return [
get_string(
$status->submissionsenabled ? 'overdue' : 'duedatereached',
'assign',
format_time($time - $status->duedate)
),
'overdue'
];
}
// An attempt has started and there is a time limit, display the time limit.
if ($timelimitenabled && !empty($submission->timestarted)) {
return [
(new \assign($status->context, null, null))->get_timelimit_panel($submission),
'timeremaining'
];
}
// Assignment is not overdue, and no submission has been made. Just display the due date.
return [get_string('paramtimeremaining', 'assign', format_time($status->duedate - $time)), 'timeremaining'];
}
/**
* Internal function - creates htmls structure suitable for YUI tree.
@ -1488,4 +1540,20 @@ class renderer extends \plugin_renderer_base {
return $this->render_from_template('mod_assign/grading_app', $context);
}
/**
* Formats activity intro text.
*
* @param object $assign Instance of assign.
* @param int $cmid Course module ID.
* @return string
*/
public function format_activity_text($assign, $cmid) {
global $CFG;
require_once("$CFG->libdir/filelib.php");
$context = \context_module::instance($cmid);
$options = array('noclean' => true, 'para' => false, 'filter' => true, 'context' => $context, 'overflowdiv' => true);
$activity = file_rewrite_pluginfile_urls(
$assign->activity, 'pluginfile.php', $context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0);
return trim(format_text($activity, $assign->activityformat, $options, null));
}
}

View File

@ -0,0 +1,84 @@
<?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/>.
/**
* Represents the timer panel.
*
* @package mod_assign
* @copyright 2020 Ilya Tregubov <ilyatregubov@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_assign\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* Represents the timer panel.
*
* @package mod_assign
* @copyright 2020 Ilya Tregubov <ilyatregubov@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class timelimit_panel implements templatable, renderable {
/** @var \stdClass assign submission attempt.*/
protected $submission;
/** @var object assign object.*/
protected $assign;
/**
* Constructor.
*
* @param \stdClass $submission assign submission.
* @param \stdClass $assign assign object.
*/
public function __construct(\stdClass $submission, \stdClass $assign) {
$this->submission = $submission;
$this->assign = $assign;
}
/**
* Render timer.
*
* @param renderer_base $output The current page renderer.
* @return stdClass - Flat list of exported data.
*/
public function export_for_template(renderer_base $output): stdClass {
return (object)['timerstartvalue' => $this->end_time() - time()];
}
/**
* Compute end time for this assign attempt.
*
* @return int the time when assign attempt is due.
*/
private function end_time(): int {
$timedue = $this->submission->timestarted + $this->assign->timelimit;
if ($this->assign->duedate) {
return min($timedue, $this->assign->duedate);
}
if ($this->assign->cutoffdate) {
return min($timedue, $this->assign->cutoffdate);
}
return $timedue;
}
}

View File

@ -89,6 +89,7 @@ class provider implements
'userid' => 'privacy:metadata:userid',
'timecreated' => 'privacy:metadata:timecreated',
'timemodified' => 'timemodified',
'timestarted' => 'privacy:metadata:timestarted',
'status' => 'gradingstatus',
'groupid' => 'privacy:metadata:groupid',
'attemptnumber' => 'attemptnumber',
@ -566,6 +567,7 @@ class provider implements
$submissiondata = (object)[
'timecreated' => transform::datetime($submission->timecreated),
'timemodified' => transform::datetime($submission->timemodified),
'timestarted' => transform::datetime($submission->timestarted),
'status' => get_string('submissionstatus_' . $submission->status, 'mod_assign'),
'groupid' => $submission->groupid,
'attemptnumber' => ($submission->attemptnumber + 1),

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/assign/db" VERSION="20140724" COMMENT="XMLDB file for Moodle mod/assign"
<XMLDB PATH="mod/assign/db" VERSION="20210930" COMMENT="XMLDB file for Moodle mod/assign"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@ -36,6 +36,10 @@
<FIELD NAME="markingallocation" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled, marking allocation features will be used in this assignment"/>
<FIELD NAME="sendstudentnotifications" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Default for send student notifications checkbox when grading."/>
<FIELD NAME="preventsubmissionnotingroup" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled a user will be unable to make a submission unless they are a member of a group."/>
<FIELD NAME="activity" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="activityformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="submissionattachments" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="The unique id for this assignment instance."/>
@ -52,6 +56,7 @@
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time of the first student submission to this assignment."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this assignment submission was modified by a student."/>
<FIELD NAME="timestarted" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The time when the student stared the submission."/>
<FIELD NAME="status" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The status of this assignment submission. The current statuses are DRAFT and SUBMITTED."/>
<FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The group id for team submissions"/>
<FIELD NAME="attemptnumber" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Used to track attempts for an assignment"/>
@ -150,6 +155,7 @@
<FIELD NAME="allowsubmissionsfromdate" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time at which students may start attempting this assign. Can be null, in which case the assign default is used."/>
<FIELD NAME="duedate" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time by which students must have completed their attempt. Can be null, in which case the assign default is used."/>
<FIELD NAME="cutoffdate" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time by which students must have completed their attempt. Can be null, in which case the assign default is used."/>
<FIELD NAME="timelimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Time limit in seconds. Can be null, in which case the quiz default is used."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>

View File

@ -59,5 +59,58 @@ function xmldb_assign_upgrade($oldversion) {
// Automatically generated Moodle v3.9.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2021093000) {
// Define field activity to be added to assign.
$table = new xmldb_table('assign');
$field = new xmldb_field('activity', XMLDB_TYPE_TEXT, null, null, null, null, null, 'alwaysshowdescription');
// Conditionally launch add field activity.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$field = new xmldb_field('activityformat', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'activity');
// Conditionally launch add field activityformat.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$field = new xmldb_field('timelimit', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'cutoffdate');
// Conditionally launch add field timelimit.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$field = new xmldb_field('submissionattachments', XMLDB_TYPE_INTEGER, '2',
null, XMLDB_NOTNULL, null, '0', 'activityformat');
// Conditionally launch add field submissionattachments.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
$table = new xmldb_table('assign_submission');
$field = new xmldb_field('timestarted', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'timemodified');
// Conditionally launch add field timestarted.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field timelimit to be added to assign_overrides.
$table = new xmldb_table('assign_overrides');
$field = new xmldb_field('timelimit', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'cutoffdate');
// Conditionally launch add field timelimit.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Assign savepoint reached.
upgrade_mod_savepoint(true, 2021093000, 'assign');
}
return true;
}

View File

@ -1078,6 +1078,7 @@ class assign_grading_table extends table_sql implements renderable {
$o = '';
$instance = $this->assignment->get_instance($row->userid);
$timelimitenabled = get_config('assign', 'enabletimelimit');
$due = $instance->duedate;
if ($row->extensionduedate) {
@ -1121,6 +1122,14 @@ class assign_grading_table extends table_sql implements renderable {
'assign',
$usertime);
$o .= $this->output->container($latemessage, 'latesubmission');
} else if ($timelimitenabled && $instance->timelimit && !empty($submission->timestarted)
&& ($timesubmitted - $submission->timestarted > $instance->timelimit)
&& $status != ASSIGN_SUBMISSION_STATUS_NEW) {
$usertime = format_time($timesubmitted - $submission->timestarted - $instance->timelimit);
$latemessage = get_string('submittedlateshort',
'assign',
$usertime);
$o .= $this->output->container($latemessage, 'latesubmission');
}
if ($row->locked) {
$lockedstr = get_string('submissionslockedshort', 'assign');

View File

@ -22,12 +22,13 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['activityattachments'] = 'Assignment activity attachments';
$string['activitydate:submissionsdue'] = 'Due:';
$string['activitydate:submissionsopen'] = 'Opens:';
$string['activitydate:submissionsopened'] = 'Opened:';
$string['activityeditor'] = 'Activity';
$string['activityeditor_help'] = 'The actions you would like the student to complete for this assignment. This is only shown on the submission page where a students edits and submits their assignment.';
$string['activityoverview'] = 'You have assignments that need attention';
$string['addsubmission'] = 'Add submission';
$string['addsubmission_help'] = 'You have not made a submission yet.';
$string['addattempt'] = 'Allow another attempt';
$string['addnewattempt'] = 'Add a new attempt';
$string['addnewattempt_help'] = 'This will create a new blank submission for you to work on.';
@ -35,6 +36,8 @@ $string['addnewattemptfromprevious'] = 'Add a new attempt based on previous subm
$string['addnewattemptfromprevious_help'] = 'This will copy the contents of your previous submission to a new submission for you to work on.';
$string['addnewgroupoverride'] = 'Add group override';
$string['addnewuseroverride'] = 'Add user override';
$string['addsubmission'] = 'Add submission';
$string['addsubmission_help'] = 'You have not made a submission yet.';
$string['allocatedmarker'] = 'Allocated Marker';
$string['allocatedmarker_help'] = 'Marker allocated to this submission.';
$string['allowsubmissions'] = 'Allow the user to continue making submissions to this assignment.';
@ -81,6 +84,7 @@ $string['assignmentplugins'] = 'Assignment plugins';
$string['assignmentsperpage'] = 'Assignments per page';
$string['assignsubmission'] = 'Submission plugin';
$string['assignsubmissionpluginname'] = 'Submission plugin';
$string['assigntimeleft'] = 'Time left';
$string['attemptheading'] = 'Attempt {$a->attemptnumber}: {$a->submissionsummary}';
$string['attempthistory'] = 'Previous attempts';
$string['attemptnumber'] = 'Attempt number';
@ -111,12 +115,14 @@ $string['batchoperationunlock'] = 'unlock submissions';
$string['batchoperationreverttodraft'] = 'revert submissions to draft';
$string['batchsetallocatedmarker'] = 'Set allocated marker for {$a} selected user(s).';
$string['batchsetmarkingworkflowstateforusers'] = 'Set marking workflow state for {$a} selected user(s).';
$string['beginassignment'] = 'Begin assignment';
$string['blindmarking'] = 'Anonymous submissions';
$string['blindmarkingenabledwarning'] = 'Anonymous submissions are enabled for this activity. Grades will not be added to the gradebook until student identities are revealed via the grading action menu.';
$string['blindmarking_help'] = 'Anonymous submissions hide the identity of students from markers. Anonymous submission settings will be locked once a submission or grade has been made in relation to this assignment.';
$string['cachedef_overrides'] = 'User and group override information';
$string['calendardue'] = '{$a} is due';
$string['calendargradingdue'] = '{$a} is due to be graded';
$string['caneditsubmission'] = 'You can submit/edit submission after time limit passed, but it will be marked as late.';
$string['changeuser'] = 'Change user';
$string['changefilters'] = 'Change filters';
$string['choosegradingaction'] = 'Grading action';
@ -179,6 +185,8 @@ $string['editsubmission_help'] = 'You can still make changes to your submission.
$string['editingstatus'] = 'Editing status';
$string['editaction'] = 'Actions...';
$string['enabled'] = 'Enabled';
$string['enabletimelimit'] = 'Enable timed assignments';
$string['enabletimelimit_help'] = 'If enabled, you can set a time limit on assignment settings page.';
$string['eventallsubmissionsdownloaded'] = 'All the submissions are being downloaded.';
$string['eventassessablesubmitted'] = 'A submission has been submitted.';
$string['eventbatchsetmarkerallocationviewed'] = 'Batch set marker allocation viewed';
@ -314,7 +322,7 @@ $string['indicator:socialbreadthdef_help'] = 'The participant has reached this p
$string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth';
$string['instructionfiles'] = 'Instruction files';
$string['introattachments'] = 'Additional files';
$string['introattachments_help'] = 'Additional files for use in the assignment, such as answer templates, may be added. Download links for the files will then be displayed on the assignment page under the description.';
$string['introattachments_help'] = 'Additional files for use in the assignment, such as answer templates, may be added. Download links for the files will then be displayed on the assignment page under the activty description.';
$string['invalidgradeforscale'] = 'The grade supplied was not valid for the current scale';
$string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}';
$string['invalidoverrideid'] = 'Invalid override id';
@ -445,6 +453,7 @@ $string['privacy:metadata:groupid'] = 'Group ID that the user is a member of.';
$string['privacy:metadata:latest'] = 'Greatly simplifies queries wanting to know information about only the latest attempt.';
$string['privacy:metadata:mailed'] = 'Has this user been mailed yet?';
$string['privacy:metadata:timecreated'] = 'Time created';
$string['privacy:metadata:timestarted'] = 'Time started';
$string['privacy:metadata:userid'] = 'ID of the user';
$string['privacy:studentpath'] = 'studentsubmissions';
$string['quickgrading'] = 'Quick grading';
@ -502,6 +511,10 @@ $string['settings'] = 'Assignment settings';
$string['showrecentsubmissions'] = 'Show recent submissions';
$string['status'] = 'Status';
$string['studentnotificationworkflowstateerror'] = 'Marking workflow state must be \'Released\' to notify students.';
$string['submissionattachments'] = 'Only show files during submission.';
$string['submissionattachments_help'] = 'When enabled files will only be shown on submission screen.
When disabled files will be shown on both assignment view and submission screens.';
$string['confirmstart'] = 'Your submission will have a time limit of {$a}. When you start, the timer will begin to count down and cannot be paused. You must finish your submission before it expires. Are you sure you wish to start now? ';
$string['submissioncopiedtext'] = 'You have made a copy of your previous
assignment submission for \'{$a->assignment}\'
@ -574,6 +587,8 @@ $string['submitassignment_help'] = 'Once this assignment is submitted you will n
$string['submitassignment'] = 'Submit assignment';
$string['submittedearly'] = 'Assignment was submitted {$a} early';
$string['submittedlate'] = 'Assignment was submitted {$a} late';
$string['submittedovertime'] = 'Assignment was submitted {$a} over the time limit';
$string['submittedundertime'] = 'Assignment was submitted {$a} under the time limit';
$string['submittedlateshort'] = '{$a} late';
$string['submitted'] = 'Submitted';
$string['subpagetitle'] = '{$a->contextname} - {$a->subpage}';
@ -587,6 +602,9 @@ $string['teamsubmission_help'] = 'If enabled, students will be divided into grou
$string['teamsubmissiongroupingid'] = 'Grouping for student groups';
$string['teamsubmissiongroupingid_help'] = 'This is the grouping that the assignment will use to find groups for student groups. If not set, the default set of groups will be used.';
$string['textinstructions'] = 'Assignment instructions';
$string['timelimit'] = 'Time limit';
$string['timelimit_help'] = 'If enabled, the time limit is stated on the assignment page and a countdown timer is displayed during the assignment.';
$string['timelimitpassed'] = 'Time limit has been passed';
$string['timemodified'] = 'Last modified';
$string['timeremaining'] = 'Time remaining';
$string['timeremainingcolon'] = 'Time remaining: {$a}';
@ -646,3 +664,4 @@ $string['allowsubmissionsfromdatesummary'] = 'This assignment will accept submis
$string['allowsubmissionsanddescriptionfromdatesummary'] = 'The assignment details and submission form will be available from <strong>{$a}</strong>';
$string['relativedatessubmissionduedateafter'] = '{$a->datediffstr} after course start';
$string['relativedatessubmissionduedatebefore'] = '{$a->datediffstr} before course start';

View File

@ -164,7 +164,7 @@ function assign_prepare_update_events($assign, $course = null, $cm = null) {
$assignment->update_calendar($cm->id);
// Refresh the calendar events also for the assignment overrides.
$overrides = $DB->get_records('assign_overrides', ['assignid' => $assign->id], '',
'id, groupid, userid, duedate, sortorder');
'id, groupid, userid, duedate, sortorder, timelimit');
foreach ($overrides as $override) {
if (empty($override->userid)) {
unset($override->userid);
@ -293,6 +293,7 @@ function assign_update_events($assign, $override = null) {
$groupid = isset($current->groupid) ? $current->groupid : 0;
$userid = isset($current->userid) ? $current->userid : 0;
$duedate = isset($current->duedate) ? $current->duedate : $assigninstance->duedate;
$timelimit = isset($current->timelimit) ? $current->timelimit : 0;
// Only add 'due' events for an override if they differ from the assign default.
$addclose = empty($current->id) || !empty($current->duedate);
@ -308,7 +309,7 @@ function assign_update_events($assign, $override = null) {
$event->modulename = 'assign';
$event->instance = $assigninstance->id;
$event->timestart = $duedate;
$event->timeduration = 0;
$event->timeduration = $timelimit;
$event->timesort = $event->timestart + $event->timeduration;
$event->visible = instance_is_visible('assign', $assigninstance);
$event->eventtype = ASSIGN_EVENT_TYPE_DUE;
@ -1161,7 +1162,10 @@ function assign_get_file_areas($course, $cm, $context) {
global $CFG;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$areas = array(ASSIGN_INTROATTACHMENT_FILEAREA => get_string('introattachments', 'mod_assign'));
$areas = array(
ASSIGN_INTROATTACHMENT_FILEAREA => get_string('introattachments', 'mod_assign'),
ASSIGN_ACTIVITYATTACHMENT_FILEAREA => get_string('activityattachments', 'mod_assign'),
);
$assignment = new assign($context, $cm, $course);
foreach ($assignment->get_submission_plugins() as $plugin) {
@ -1223,7 +1227,7 @@ function assign_get_file_info($browser,
// Need to find where this belongs to.
$assignment = new assign($context, $cm, $course);
if ($filearea === ASSIGN_INTROATTACHMENT_FILEAREA) {
if ($filearea === ASSIGN_INTROATTACHMENT_FILEAREA || $filearea === ASSIGN_ACTIVITYATTACHMENT_FILEAREA) {
if (!has_capability('moodle/course:managefiles', $context)) {
// Students can not peak here!
return null;
@ -1412,7 +1416,7 @@ function assign_pluginfile($course,
require_once($CFG->dirroot . '/mod/assign/locallib.php');
$assign = new assign($context, $cm, $course);
if ($filearea !== ASSIGN_INTROATTACHMENT_FILEAREA) {
if ($filearea !== ASSIGN_INTROATTACHMENT_FILEAREA && $filearea !== ASSIGN_ACTIVITYATTACHMENT_FILEAREA) {
return false;
}
if (!$assign->show_intro()) {

View File

@ -73,6 +73,9 @@ define("ASSIGN_MAX_EVENT_LENGTH", "432000");
// Name of file area for intro attachments.
define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
// Name of file area for activity attachments.
define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment');
// Event types.
define('ASSIGN_EVENT_TYPE_DUE', 'due');
define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
@ -94,6 +97,7 @@ require_once($CFG->libdir . '/portfolio/caller.php');
use \mod_assign\output\grading_app;
use \mod_assign\output\assign_header;
use \mod_assign\output\assign_submission_status;
use mod_assign\output\timelimit_panel;
/**
* Standard base class for mod_assign (assignment types).
@ -705,6 +709,13 @@ class assign {
$update->intro = $formdata->intro;
$update->introformat = $formdata->introformat;
$update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
if (isset($formdata->activityeditor)) {
$update->activity = $this->save_editor_draft_files($formdata);
$update->activityformat = $formdata->activityeditor['format'];
}
if (isset($formdata->submissionattachments)) {
$update->submissionattachments = $formdata->submissionattachments;
}
$update->submissiondrafts = $formdata->submissiondrafts;
$update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
$update->sendnotifications = $formdata->sendnotifications;
@ -716,6 +727,9 @@ class assign {
$update->duedate = $formdata->duedate;
$update->cutoffdate = $formdata->cutoffdate;
$update->gradingduedate = $formdata->gradingduedate;
if (isset($formdata->timelimit)) {
$update->timelimit = $formdata->timelimit;
}
$update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
$update->grade = $formdata->grade;
$update->completionsubmit = !empty($formdata->completionsubmit);
@ -750,6 +764,7 @@ class assign {
$this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
$this->save_intro_draft_files($formdata);
$this->save_editor_draft_files($formdata);
if ($callplugins) {
// Call save_settings hook for submission plugins.
@ -940,7 +955,7 @@ class assign {
$override = $this->override_exists($userid);
// Merge with assign defaults.
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
foreach ($keys as $key) {
if (isset($override->{$key})) {
$this->get_instance($userid)->{$key} = $override->{$key};
@ -1007,7 +1022,7 @@ class assign {
// return arrays containing keys for only the defined overrides. So we get the
// desired behaviour as per the algorithm.
return (object)array_merge(
['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
$getgroupoverride($userid),
$getuseroverride($userid)
);
@ -1460,6 +1475,13 @@ class assign {
$update->intro = $formdata->intro;
$update->introformat = $formdata->introformat;
$update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
if (isset($formdata->activityeditor)) {
$update->activity = $this->save_editor_draft_files($formdata);
$update->activityformat = $formdata->activityeditor['format'];
}
if (isset($formdata->submissionattachments)) {
$update->submissionattachments = $formdata->submissionattachments;
}
$update->submissiondrafts = $formdata->submissiondrafts;
$update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
$update->sendnotifications = $formdata->sendnotifications;
@ -1470,6 +1492,9 @@ class assign {
}
$update->duedate = $formdata->duedate;
$update->cutoffdate = $formdata->cutoffdate;
if (isset($formdata->timelimit)) {
$update->timelimit = $formdata->timelimit;
}
$update->gradingduedate = $formdata->gradingduedate;
$update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
$update->grade = $formdata->grade;
@ -1537,7 +1562,7 @@ class assign {
}
/**
* Save the attachments in the draft areas.
* Save the attachments in the intro description.
*
* @param stdClass $formdata
*/
@ -1548,6 +1573,25 @@ class assign {
}
}
/**
* Save the attachments in the editor description.
*
* @param stdClass $formdata
*/
protected function save_editor_draft_files($formdata): string {
$text = '';
if (isset($formdata->activityeditor)) {
$text = $formdata->activityeditor['text'];
if (isset($formdata->activityeditor['itemid'])) {
$text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id,
'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
0, array('subdirs' => true), $formdata->activityeditor['text']);
}
}
return $text;
}
/**
* Add elements in grading plugin form.
*
@ -3038,6 +3082,15 @@ class assign {
}
if ($submission) {
if ($create) {
$action = optional_param('action', '', PARAM_TEXT);
if ($action == 'editsubmission') {
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
$submission->timestarted = time();
$DB->update_record('assign_submission', $submission);
}
}
}
return $submission;
}
if ($create) {
@ -3810,6 +3863,15 @@ class assign {
}
if ($submission) {
if ($create) {
$action = optional_param('action', '', PARAM_TEXT);
if ($action == 'editsubmission') {
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
$submission->timestarted = time();
$DB->update_record('assign_submission', $submission);
}
}
}
return $submission;
}
if ($create) {
@ -4077,7 +4139,8 @@ class assign {
$instance->maxattempts,
$this->get_grading_status($userid),
$instance->preventsubmissionnotingroup,
$usergroups);
$usergroups,
$instance->timelimit);
$o .= $this->get_renderer()->render($submissionstatus);
}
@ -4242,7 +4305,6 @@ class assign {
$showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
$viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
$usergroups = $this->get_all_groups($user->id);
$submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
$instance->alwaysshowdescription,
$submission,
@ -4272,7 +4334,8 @@ class assign {
$instance->maxattempts,
$this->get_grading_status($userid),
$instance->preventsubmissionnotingroup,
$usergroups);
$usergroups,
$instance->timelimit);
$o .= $this->get_renderer()->render($submissionstatus);
}
@ -4787,13 +4850,14 @@ class assign {
* @return string The page output.
*/
protected function view_edit_submission_page($mform, $notices) {
global $CFG, $USER, $DB;
global $CFG, $USER, $DB, $PAGE;
$o = '';
require_once($CFG->dirroot . '/mod/assign/submission_form.php');
// Need submit permission to submit an assignment.
$userid = optional_param('userid', $USER->id, PARAM_INT);
$user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
$timelimitenabled = get_config('assign', 'enabletimelimit');
// This variation on the url will link direct to this student.
// The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
@ -4826,14 +4890,6 @@ class assign {
if ($this->has_visible_attachments()) {
$postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
}
$o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
$this->get_context(),
$this->show_intro(),
$this->get_course_module()->id,
$title, '', $postfix));
// Show plagiarism disclosure for any user submitter.
$o .= $this->plagiarism_print_disclosure();
$data = new stdClass();
$data->userid = $userid;
@ -4841,12 +4897,44 @@ class assign {
$mform = new mod_assign_submission_form(null, array($this, $data));
}
if ($this->get_instance()->teamsubmission) {
$submission = $this->get_group_submission($userid, 0, false);
} else {
$submission = $this->get_user_submission($userid, false);
}
if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
$navbc = $this->get_timelimit_panel($submission);
$regions = $PAGE->blocks->get_regions();
$bc = new \block_contents();
$bc->attributes['id'] = 'mod_assign_timelimit_block';
$bc->attributes['role'] = 'navigation';
$bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
$bc->title = get_string('assigntimeleft', 'assign');
$bc->content = $navbc;
$PAGE->blocks->add_fake_block($bc, reset($regions));
}
$o .= $this->get_renderer()->render(
new assign_header($this->get_instance(),
$this->get_context(),
$this->show_intro(),
$this->get_course_module()->id,
$title,
'',
$postfix,
null,
true
)
);
// Show plagiarism disclosure for any user submitter.
$o .= $this->plagiarism_print_disclosure();
foreach ($notices as $notice) {
$o .= $this->get_renderer()->notification($notice);
}
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
$o .= $this->view_footer();
\mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
@ -4854,6 +4942,21 @@ class assign {
return $o;
}
/**
* Get the time limit panel object for this submission attempt.
*
* @param stdClass $submission assign submission.
* @return string the panel output.
*/
public function get_timelimit_panel(stdClass $submission): string {
global $USER;
// Apply overrides.
$this->update_effective_access($USER->id);
$panel = new timelimit_panel($submission, $this->get_instance());
return $this->get_renderer()->render($panel);
}
/**
* See if this assignment has a grade yet.
*
@ -5337,7 +5440,8 @@ class assign {
$instance->maxattempts,
$gradingstatus,
$instance->preventsubmissionnotingroup,
$usergroups);
$usergroups,
$instance->timelimit);
return $submissionstatus;
}
@ -5713,6 +5817,7 @@ class assign {
$this->count_submissions_with_status($submitted, $activitygroup),
$instance->cutoffdate,
$this->get_duedate($activitygroup),
$instance->timelimit,
$this->get_course_module()->id,
$this->count_submissions_need_grading($activitygroup),
$instance->teamsubmission,
@ -5733,6 +5838,7 @@ class assign {
$this->count_submissions_with_status($submitted, $activitygroup),
$instance->cutoffdate,
$this->get_duedate($activitygroup),
$instance->timelimit,
$this->get_course_module()->id,
$this->count_submissions_need_grading($activitygroup),
$instance->teamsubmission,
@ -5784,9 +5890,10 @@ class assign {
$o = '';
$postfix = '';
if ($this->has_visible_attachments()) {
if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
$postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
}
$o .= $this->get_renderer()->render(new assign_header($instance,
$this->get_context(),
$this->show_intro(),
@ -9932,3 +10039,14 @@ function reorder_group_overrides($assignid) {
}
}
}
/**
* Get the information about the standard assign JavaScript module.
* @return array a standard jsmodule structure.
*/
function assign_get_js_module() {
return array(
'name' => 'mod_assign',
'fullpath' => '/mod/assign/module.js',
);
}

View File

@ -58,11 +58,21 @@ class mod_assign_mod_form extends moodleform_mod {
$this->standard_intro_elements(get_string('description', 'assign'));
// Activity.
$mform->addElement('editor', 'activityeditor',
get_string('activityeditor', 'assign'), array('rows' => 10), array('maxfiles' => EDITOR_UNLIMITED_FILES,
'noclean' => true, 'context' => $this->context, 'subdirs' => true));
$mform->addHelpButton('activityeditor', 'activityeditor', 'assign');
$mform->setType('activityeditor', PARAM_RAW);
$mform->addElement('filemanager', 'introattachments',
get_string('introattachments', 'assign'),
null, array('subdirs' => 0, 'maxbytes' => $COURSE->maxbytes) );
$mform->addHelpButton('introattachments', 'introattachments', 'assign');
$mform->addElement('advcheckbox', 'submissionattachments', get_string('submissionattachments', 'assign'));
$mform->addHelpButton('submissionattachments', 'submissionattachments', 'assign');
$ctx = null;
if ($this->current && $this->current->coursemodule) {
$cm = get_coursemodule_from_instance('assign', $this->current->id, 0, false, MUST_EXIST);
@ -77,8 +87,6 @@ class mod_assign_mod_form extends moodleform_mod {
$assignment->set_course($course);
}
$config = get_config('assign');
$mform->addElement('header', 'availability', get_string('availability', 'assign'));
$mform->setExpanded('availability', true);
@ -99,6 +107,14 @@ class mod_assign_mod_form extends moodleform_mod {
$mform->addElement('date_time_selector', 'gradingduedate', $name, array('optional' => true));
$mform->addHelpButton('gradingduedate', 'gradingduedate', 'assign');
$timelimitenabled = get_config('assign', 'enabletimelimit');
// Time limit.
if ($timelimitenabled) {
$mform->addElement('duration', 'timelimit', get_string('timelimit', 'assign'),
array('optional' => true));
$mform->addHelpButton('timelimit', 'timelimit', 'assign');
}
$name = get_string('alwaysshowdescription', 'assign');
$mform->addElement('checkbox', 'alwaysshowdescription', $name);
$mform->addHelpButton('alwaysshowdescription', 'alwaysshowdescription', 'assign');
@ -284,6 +300,17 @@ class mod_assign_mod_form extends moodleform_mod {
0, array('subdirs' => 0));
$defaultvalues['introattachments'] = $draftitemid;
// Activity editor fields.
$activitydraftitemid = file_get_submitted_draft_itemid('activityeditor');
if (!empty($defaultvalues['activity'])) {
$defaultvalues['activityeditor'] = array(
'text' => file_prepare_draft_area($activitydraftitemid, $ctx->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
0, array('subdirs' => 0), $defaultvalues['activity']),
'format' => $defaultvalues['activityformat'],
'itemid' => $activitydraftitemid
);
}
$assignment->plugin_data_preprocessing($defaultvalues);
}

View File

@ -268,6 +268,11 @@ class assign_override_form extends moodleform {
userdate($assigninstance->extensionduedate));
}
// Time limit.
$mform->addElement('duration', 'timelimit',
get_string('timelimit', 'assign'), array('optional' => true));
$mform->setDefault('timelimit', $assigninstance->timelimit);
// Submit buttons.
$mform->addElement('submit', 'resetbutton',
get_string('reverttodefaults', 'assign'));
@ -343,7 +348,7 @@ class assign_override_form extends moodleform {
// Ensure that at least one assign setting was changed.
$changed = false;
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
foreach ($keys as $key) {
if ($data[$key] != $assigninstance->{$key}) {
$changed = true;

View File

@ -96,7 +96,7 @@ if ($overrideid) {
}
// Merge assign defaults with data.
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
$keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
foreach ($keys as $key) {
if (!isset($data->{$key}) || $reset) {
$data->{$key} = $assigninstance->{$key};

View File

@ -199,6 +199,12 @@ foreach ($overrides as $override) {
$values[] = $override->cutoffdate > 0 ? userdate($override->cutoffdate) : get_string('noclose', 'assign');
}
// Format timelimit.
if (isset($override->timelimit)) {
$fields[] = get_string('timelimit', 'assign');
$values[] = $override->timelimit > 0 ? format_time($override->timelimit) : get_string('none', 'assign');
}
// Icons.
$iconstr = '';

View File

@ -525,6 +525,8 @@ class assign_grading_summary implements renderable {
public $duedate = 0;
/** @var int cutoffdate - The assignment cut off date (if one is set) */
public $cutoffdate = 0;
/** @var int timelimit - The assignment time limit (if one is set) */
public $timelimit = 0;
/** @var int coursemoduleid - The assignment course module id */
public $coursemoduleid = 0;
/** @var boolean teamsubmission - Are team submissions enabled for this assignment */
@ -557,6 +559,7 @@ class assign_grading_summary implements renderable {
* @param int $submissionssubmittedcount
* @param int $cutoffdate
* @param int $duedate
* @param int $timelimit
* @param int $coursemoduleid
* @param int $submissionsneedgradingcount
* @param bool $teamsubmission
@ -573,6 +576,7 @@ class assign_grading_summary implements renderable {
$submissionssubmittedcount,
$cutoffdate,
$duedate,
$timelimit,
$coursemoduleid,
$submissionsneedgradingcount,
$teamsubmission,
@ -588,6 +592,7 @@ class assign_grading_summary implements renderable {
$this->submissionssubmittedcount = $submissionssubmittedcount;
$this->duedate = $duedate;
$this->cutoffdate = $cutoffdate;
$this->timelimit = $timelimit;
$this->coursemoduleid = $coursemoduleid;
$this->submissionsneedgradingcount = $submissionsneedgradingcount;
$this->teamsubmission = $teamsubmission;

View File

@ -151,6 +151,14 @@ if ($ADMIN->fulltree) {
$setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false);
$settings->add($setting);
$name = new lang_string('enabletimelimit', 'mod_assign');
$description = new lang_string('enabletimelimit_help', 'mod_assign');
$setting = new admin_setting_configcheckbox('assign/enabletimelimit',
$name,
$description,
0);
$settings->add($setting);
$name = new lang_string('gradingduedate', 'mod_assign');
$description = new lang_string('gradingduedate_help', 'mod_assign');
$setting = new admin_setting_configduration('assign/gradingduedate',

View File

@ -1226,3 +1226,10 @@
padding: 0;
}
/** End of YUI tree fix **/
/* Countdown timer. */
div[id*='mod_assign-timer-'] {
display: block;
font-weight: 600;
font-size: 1.4em;
}

View File

@ -0,0 +1,45 @@
{{!
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 mod_assign/timelimit_panel
Show a timer that counts down to 0.
Classes required for JS:
* None
Data attibutes required for JS:
* None
Example context (json):
{
"timerstartvalue": 1729000
}
}}
<div{{!
}} id="mod_assign-timer-{{uniqid}}"{{!
}} role="timer" aria-atomic="true"{{!
}} aria-relevant="text"{{!
}} data-starttime="{{timerstartvalue}}"{{!
}}>
</div>
{{#js}}
require(['mod_assign/timer'], function(Timer) {
Timer.init("mod_assign-timer-{{uniqid}}");
});
{{/js}}

View File

@ -120,6 +120,65 @@ Feature: Set availability dates for an assignment
And I should see "Submitted for grading" in the "Student 1" "table_row"
And I should see "2 days 5 hours late" in the "Student 1" "table_row"
@_file_upload
Scenario: Student can submit an assignment before the time limit runs out
Given I log in as "admin"
And I change the window size to "large"
And I set the following administration settings values:
| Enable timed assignments | 1 |
And I log out
And I am on the "Assignment name" Activity page logged in as teacher1
And I navigate to "Settings" in current page administration
And I follow "Expand all"
# Set 'Time limit' to 20 seconds.
And I set the field "timelimit[enabled]" to "1"
And I set the field "timelimit[number]" to "20"
And I set the field "timelimit[timeunit]" to "seconds"
And I press "Save and return to course"
And I log out
When I am on the "Assignment name" Activity page logged in as student1
And I should see "20 secs" in the "Time limit" "table_row"
And "Begin assignment" "link" should exist
And I follow "Begin assignment"
And I wait "1" seconds
And "Begin assignment" "button" should exist
And I press "Begin assignment"
And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager
When I press "Save changes"
Then I should see "Submitted for grading" in the "Submission status" "table_row"
And I should see "secs under the time limit" in the "Time remaining" "table_row"
@_file_upload
Scenario: Assignment with time limit and due date shows how late assignment is submitted relative to due date
Given I log in as "admin"
And I change the window size to "large"
And I set the following administration settings values:
| Enable timed assignments | 1 |
And I log out
And I am on the "Assignment name" Activity page logged in as teacher1
And I navigate to "Settings" in current page administration
And I follow "Expand all"
# Set 'Time limit' to 5 seconds.
And I set the field "timelimit[enabled]" to "1"
And I set the field "timelimit[number]" to "5"
And I set the field "timelimit[timeunit]" to "seconds"
# Set 'Due date' to 2 days 5 hours 30 minutes ago.
And I set the field "Due date" to "##2 days 5 hours 30 minutes ago##"
And I press "Save and return to course"
And I log out
When I am on the "Assignment name" Activity page logged in as student1
And "Begin assignment" "link" should exist
And I follow "Begin assignment"
And I wait "1" seconds
And "Begin assignment" "button" should exist
And I press "Begin assignment"
And I wait "5" seconds
And I upload "lib/tests/fixtures/empty.txt" file to "File submissions" filemanager
When I press "Save changes"
Then I should see "Assignment was submitted 2 days 5 hours late" in the "Time remaining" "table_row"
Scenario: Student cannot submit an assignment after the cut-off date
Given I am on the "Assignment name" Activity page logged in as teacher1
And I navigate to "Settings" in current page administration

View File

@ -3660,7 +3660,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'sortorder' => 1,
'allowsubmissionsfromdate' => 1,
'duedate' => 2,
'cutoffdate' => 3
'cutoffdate' => 3,
'timelimit' => null
],
(object) [
// Override for group 2, lower priority (numerically higher sortorder).
@ -3670,7 +3671,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'sortorder' => 2,
'allowsubmissionsfromdate' => 5,
'duedate' => 6,
'cutoffdate' => 6
'cutoffdate' => 6,
'timelimit' => null
],
(object) [
// User override.
@ -3680,7 +3682,8 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
'sortorder' => null,
'allowsubmissionsfromdate' => 7,
'duedate' => 8,
'cutoffdate' => 9
'cutoffdate' => 9,
'timelimit' => null
],
];

View File

@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
$plugin->version = 2021093000; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2021110900; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2021052500; // Requires this Moodle version.