diff --git a/mod/assign/assignmentplugin.php b/mod/assign/assignmentplugin.php
index 1f676ee7a3f..f3c68c40b18 100644
--- a/mod/assign/assignmentplugin.php
+++ b/mod/assign/assignmentplugin.php
@@ -627,4 +627,5 @@ abstract class assign_plugin {
return true;
}
+
}
diff --git a/mod/assign/backup/moodle2/backup_assign_stepslib.php b/mod/assign/backup/moodle2/backup_assign_stepslib.php
index 088e5fa3107..8696fa8e10a 100644
--- a/mod/assign/backup/moodle2/backup_assign_stepslib.php
+++ b/mod/assign/backup/moodle2/backup_assign_stepslib.php
@@ -62,7 +62,18 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'requireallteammemberssubmit',
'teamsubmissiongroupingid',
'blindmarking',
- 'revealidentities'));
+ 'revealidentities',
+ 'attemptreopenmethod',
+ 'maxattempts'));
+
+ $userflags = new backup_nested_element('userflags');
+
+ $userflag = new backup_nested_element('userflag', array('id'),
+ array('userid',
+ 'assignment',
+ 'mailed',
+ 'locked',
+ 'extensionduedate'));
$submissions = new backup_nested_element('submissions');
@@ -71,7 +82,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'timecreated',
'timemodified',
'status',
- 'groupid'));
+ 'groupid',
+ 'attemptnumber'));
$grades = new backup_nested_element('grades');
@@ -81,9 +93,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'timemodified',
'grader',
'grade',
- 'locked',
- 'mailed',
- 'extensionduedate'));
+ 'attemptnumber'));
$pluginconfigs = new backup_nested_element('plugin_configs');
@@ -94,7 +104,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'value'));
// Build the tree.
-
+ $assign->add_child($userflags);
+ $userflags->add_child($userflag);
$assign->add_child($submissions);
$submissions->add_child($submission);
$assign->add_child($grades);
@@ -108,6 +119,9 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
array('assignment' => backup::VAR_PARENTID));
if ($userinfo) {
+ $userflag->set_source_table('assign_user_flags',
+ array('assignment' => backup::VAR_PARENTID));
+
$submission->set_source_table('assign_submission',
array('assignment' => backup::VAR_PARENTID));
@@ -120,6 +134,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
}
// Define id annotations.
+ $userflag->annotate_ids('user', 'userid');
$submission->annotate_ids('user', 'userid');
$submission->annotate_ids('group', 'groupid');
$grade->annotate_ids('user', 'userid');
diff --git a/mod/assign/backup/moodle2/restore_assign_stepslib.php b/mod/assign/backup/moodle2/restore_assign_stepslib.php
index 2a265dbed88..28f3e37a690 100644
--- a/mod/assign/backup/moodle2/restore_assign_stepslib.php
+++ b/mod/assign/backup/moodle2/restore_assign_stepslib.php
@@ -130,6 +130,30 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$this->set_mapping('submission', $oldid, $newitemid, false, null, $this->task->get_old_contextid());
}
+ /**
+ * Process a user_flags restore
+ * @param object $data The data in object form
+ * @return void
+ */
+ protected function process_assign_userflags($data) {
+ global $DB;
+
+ $data = (object)$data;
+ $oldid = $data->id;
+
+ $data->assignment = $this->get_new_parentid('assign');
+
+ $data->userid = $this->get_mappingid('user', $data->userid);
+ if (!empty($data->extensionduedate)) {
+ $data->extensionduedate = $this->apply_date_offset($data->extensionduedate);
+ } else {
+ $data->extensionduedate = 0;
+ }
+ // Flags mailed and locked need no translation on restore.
+
+ $newitemid = $DB->insert_record('assign_user_flags', $data);
+ }
+
/**
* Process a grade restore
* @param object $data The data in object form
@@ -147,11 +171,20 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$data->timecreated = $this->apply_date_offset($data->timecreated);
$data->userid = $this->get_mappingid('user', $data->userid);
$data->grader = $this->get_mappingid('user', $data->grader);
+
+ // Handle flags restore to a different table.
+ $flags = new stdClass();
+ $flags->assignment = $this->get_new_parentid('assign');
if (!empty($data->extensionduedate)) {
- $data->extensionduedate = $this->apply_date_offset($data->extensionduedate);
- } else {
- $data->extensionduedate = 0;
+ $flags->extensionduedate = $this->apply_date_offset($data->extensionduedate);
}
+ if (!empty($data->mailed)) {
+ $flags->mailed = $data->mailed;
+ }
+ if (!empty($data->locked)) {
+ $flags->locked = $data->locked;
+ }
+ $DB->insert_record('assign_user_flags', $flags);
$newitemid = $DB->insert_record('assign_grades', $data);
diff --git a/mod/assign/db/install.xml b/mod/assign/db/install.xml
index 5a7b78422f2..271062eeaac 100644
--- a/mod/assign/db/install.xml
+++ b/mod/assign/db/install.xml
@@ -1,5 +1,5 @@
-
@@ -23,11 +23,13 @@
-
-
-
+
+
+
+
+
@@ -45,7 +47,8 @@
-
+
+
@@ -53,6 +56,8 @@
+
+
@@ -64,9 +69,7 @@
-
-
-
+
@@ -74,7 +77,8 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php
index f64227939e5..09e06407345 100644
--- a/mod/assign/db/upgrade.php
+++ b/mod/assign/db/upgrade.php
@@ -208,6 +208,145 @@ function xmldb_assign_upgrade($oldversion) {
// Moodle v2.4.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2013030600) {
+ // Define table assign_user_flags to be created.
+ $table = new xmldb_table('assign_user_flags');
+
+ // Adding fields to table assign_user_flags.
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('locked', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('mailed', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0');
+ $table->add_field('extensionduedate', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+
+ // Adding keys to table assign_user_flags.
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+ $table->add_key('assignment', XMLDB_KEY_FOREIGN, array('assignment'), 'assign', array('id'));
+
+ // Adding indexes to table assign_user_flags.
+ $table->add_index('mailed', XMLDB_INDEX_NOTUNIQUE, array('mailed'));
+
+ // Conditionally launch create table for assign_user_flags.
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ // Copy the flags from the old table to the new one.
+ $sql = 'INSERT INTO {assign_user_flags}
+ (assignment, userid, locked, mailed, extensionduedate)
+ SELECT assignment, userid, locked, mailed, extensionduedate
+ FROM {assign_grades}';
+ $DB->execute($sql);
+
+ // And delete the old columns.
+ // Define index mailed (not unique) to be dropped form assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $index = new xmldb_index('mailed', XMLDB_INDEX_NOTUNIQUE, array('mailed'));
+
+ // Conditionally launch drop index mailed.
+ if ($dbman->index_exists($table, $index)) {
+ $dbman->drop_index($table, $index);
+ }
+
+ // Define field locked to be dropped from assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $field = new xmldb_field('locked');
+
+ // Conditionally launch drop field locked.
+ if ($dbman->field_exists($table, $field)) {
+ $dbman->drop_field($table, $field);
+ }
+
+ // Define field mailed to be dropped from assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $field = new xmldb_field('mailed');
+
+ // Conditionally launch drop field mailed.
+ if ($dbman->field_exists($table, $field)) {
+ $dbman->drop_field($table, $field);
+ }
+
+ // Define field extensionduedate to be dropped from assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $field = new xmldb_field('extensionduedate');
+
+ // Conditionally launch drop field extensionduedate.
+ if ($dbman->field_exists($table, $field)) {
+ $dbman->drop_field($table, $field);
+ }
+
+ // Define field attemptreopenmethod to be added to assign.
+ $table = new xmldb_table('assign');
+ $field = new xmldb_field('attemptreopenmethod', XMLDB_TYPE_CHAR, '10', null,
+ XMLDB_NOTNULL, null, 'none', 'revealidentities');
+
+ // Conditionally launch add field attemptreopenmethod.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Define field maxattempts to be added to assign.
+ $table = new xmldb_table('assign');
+ $field = new xmldb_field('maxattempts', XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, '-1', 'attemptreopenmethod');
+
+ // Conditionally launch add field maxattempts.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Define field attemptnumber to be added to assign_submission.
+ $table = new xmldb_table('assign_submission');
+ $field = new xmldb_field('attemptnumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'groupid');
+
+ // Conditionally launch add field attemptnumber.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Define field attemptnumber to be added to assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $field = new xmldb_field('attemptnumber', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'grade');
+
+ // Conditionally launch add field attemptnumber.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Define index attemptnumber (not unique) to be added to assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $index = new xmldb_index('attemptnumber', XMLDB_INDEX_NOTUNIQUE, array('attemptnumber'));
+
+ // Conditionally launch add index attemptnumber.
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Define index uniqueattemptsubmission (unique) to be added to assign_submission.
+ $table = new xmldb_table('assign_submission');
+ $index = new xmldb_index('uniqueattemptsubmission',
+ XMLDB_INDEX_UNIQUE,
+ array('assignment', 'userid', 'groupid', 'attemptnumber'));
+
+ // Conditionally launch add index uniqueattempt.
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Define index uniqueattemptgrade (unique) to be added to assign_grades.
+ $table = new xmldb_table('assign_grades');
+ $index = new xmldb_index('uniqueattemptgrade', XMLDB_INDEX_UNIQUE, array('assignment', 'userid', 'attemptnumber'));
+
+ // Conditionally launch add index uniqueattempt.
+ if (!$dbman->index_exists($table, $index)) {
+ $dbman->add_index($table, $index);
+ }
+
+ // Module assign savepoint reached.
+ upgrade_mod_savepoint(true, 2013030600, 'assign');
+ }
+
return true;
}
diff --git a/mod/assign/externallib.php b/mod/assign/externallib.php
index 58b80836394..15f576e62ca 100644
--- a/mod/assign/externallib.php
+++ b/mod/assign/externallib.php
@@ -96,7 +96,7 @@ class mod_assign_external extends external_api {
$placeholders = array();
list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED);
$sql = "SELECT ag.id,ag.assignment,ag.userid,ag.timecreated,ag.timemodified,".
- "ag.grader,ag.grade,ag.locked,ag.mailed ".
+ "ag.grader,ag.grade ".
"FROM {assign_grades} ag ".
"WHERE ag.assignment ".$inorequalsql.
" AND ag.timemodified >= :since".
@@ -113,8 +113,6 @@ class mod_assign_external extends external_api {
$grade['timemodified'] = $rd->timemodified;
$grade['grader'] = $rd->grader;
$grade['grade'] = (string)$rd->grade;
- $grade['locked'] = $rd->locked;
- $grade['mailed'] = $rd->mailed;
if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) {
if (!is_null($assignment)) {
@@ -165,9 +163,7 @@ class mod_assign_external extends external_api {
'timecreated' => new external_value(PARAM_INT, 'grade creation time'),
'timemodified' => new external_value(PARAM_INT, 'grade last modified time'),
'grader' => new external_value(PARAM_INT, 'grader'),
- 'grade' => new external_value(PARAM_TEXT, 'grade'),
- 'locked' => new external_value(PARAM_BOOL, 'locked'),
- 'mailed' => new external_value(PARAM_BOOL, 'mailed')
+ 'grade' => new external_value(PARAM_TEXT, 'grade')
)
)
)
diff --git a/mod/assign/gradingbatchoperationsform.php b/mod/assign/gradingbatchoperationsform.php
index 637e0034ee8..d9f74a9617f 100644
--- a/mod/assign/gradingbatchoperationsform.php
+++ b/mod/assign/gradingbatchoperationsform.php
@@ -52,6 +52,9 @@ class mod_assign_grading_batch_operations_form extends moodleform {
if ($instance['duedate']) {
$options['grantextension'] = get_string('grantextension', 'assign');
}
+ if ($instance['attemptreopenmethod'] == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL) {
+ $options['addattempt'] = get_string('addattempt', 'assign');
+ }
foreach ($instance['feedbackplugins'] as $plugin) {
if ($plugin->is_visible() && $plugin->is_enabled()) {
diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php
index 8e91f127651..7033fe71a78 100644
--- a/mod/assign/gradingtable.php
+++ b/mod/assign/gradingtable.php
@@ -116,6 +116,9 @@ class assign_grading_table extends table_sql implements renderable {
$params = array();
$params['assignmentid1'] = (int)$this->assignment->get_instance()->id;
$params['assignmentid2'] = (int)$this->assignment->get_instance()->id;
+ $params['assignmentid3'] = (int)$this->assignment->get_instance()->id;
+ $params['assignmentid4'] = (int)$this->assignment->get_instance()->id;
+ $params['assignmentid5'] = (int)$this->assignment->get_instance()->id;
$extrauserfields = get_extra_user_fields($this->assignment->get_context());
@@ -125,15 +128,33 @@ class assign_grading_table extends table_sql implements renderable {
$fields .= 's.id as submissionid, ';
$fields .= 's.timecreated as firstsubmission, ';
$fields .= 's.timemodified as timesubmitted, ';
+ $fields .= 's.attemptnumber as attemptnumber, ';
$fields .= 'g.id as gradeid, ';
$fields .= 'g.grade as grade, ';
$fields .= 'g.timemodified as timemarked, ';
$fields .= 'g.timecreated as firstmarked, ';
- $fields .= 'g.mailed as mailed, ';
- $fields .= 'g.locked as locked, ';
- $fields .= 'g.extensionduedate as extensionduedate';
- $from = '{user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1' .
- ' LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignmentid2';
+ $fields .= 'uf.mailed as mailed, ';
+ $fields .= 'uf.locked as locked, ';
+ $fields .= 'uf.extensionduedate as extensionduedate';
+
+ $submissionmaxattempt = 'SELECT mxs.userid, MAX(mxs.attemptnumber) AS maxattempt
+ FROM {assign_submission} mxs
+ WHERE mxs.assignment = :assignmentid4 GROUP BY mxs.userid';
+ $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
+ FROM {assign_grades} mxg
+ WHERE mxg.assignment = :assignmentid5 GROUP BY mxg.userid';
+ $from = '{user} u
+ LEFT JOIN ( ' . $submissionmaxattempt . ' ) smx ON u.id = smx.userid
+ LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
+ LEFT JOIN {assign_submission} s ON
+ u.id = s.userid AND
+ s.assignment = :assignmentid1 AND
+ s.attemptnumber = smx.maxattempt
+ LEFT JOIN {assign_grades} g ON
+ u.id = g.userid AND
+ g.assignment = :assignmentid2 AND
+ g.attemptnumber = gmx.maxattempt
+ LEFT JOIN {assign_user_flags} uf ON u.id = uf.userid AND uf.assignment = :assignmentid3';
$userparams = array();
$userindex = 0;
@@ -159,6 +180,7 @@ class assign_grading_table extends table_sql implements renderable {
$params['userid'] = $userfilter;
}
}
+
$this->set_sql($fields, $from, $where, $params);
if ($downloadfilename) {
@@ -829,6 +851,23 @@ class assign_grading_table extends table_sql implements renderable {
$actions[$url->out(false)] = $description;
}
+ $ismanual = $this->assignment->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
+ $hassubmission = !empty($row->status);
+ $notreopened = $hassubmission && $row->status != ASSIGN_SUBMISSION_STATUS_REOPENED;
+ $isunlimited = $this->assignment->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
+ $hasattempts = $isunlimited || $row->attemptnumber < $this->assignment->get_instance()->maxattempts - 1;
+
+ if ($ismanual && $hassubmission && $notreopened && $hasattempts) {
+ $urlparams = array('id' => $this->assignment->get_course_module()->id,
+ 'userid'=>$row->id,
+ 'action'=>'addattempt',
+ 'sesskey'=>sesskey(),
+ 'page'=>$this->currpage);
+ $url = new moodle_url('/mod/assign/view.php', $urlparams);
+ $description = get_string('addattempt', 'assign');
+ $actions[$url->out(false)] = $description;
+ }
+
$edit .= $this->output->container_start(array('yui3-menu', 'actionmenu'), 'actionselect' . $row->id);
$edit .= $this->output->container_start(array('yui3-menu-content'));
$edit .= html_writer::start_tag('ul');
diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php
index ff20298bf2a..3f4fa2e8550 100644
--- a/mod/assign/lang/en/assign.php
+++ b/mod/assign/lang/en/assign.php
@@ -24,6 +24,11 @@
$string['activityoverview'] = 'You have assignments that need attention';
$string['addsubmission'] = 'Add submission';
+$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.';
+$string['addnewattemptfromprevious'] = 'Add a new attempt based on previous submission';
+$string['addnewattemptfromprevious_help'] = 'This will copy the contents of your previous submission to a new submission for you to work on.';
$string['allowsubmissions'] = 'Allow the user to continue making submissions to this assignment.';
$string['allowsubmissionsshort'] = 'Allow submission changes';
$string['allowsubmissionsfromdate'] = 'Allow submissions from';
@@ -59,6 +64,15 @@ $string['assignmentplugins'] = 'Assignment plugins';
$string['assignmentsperpage'] = 'Assignments per page';
$string['assignsubmission'] = 'Submission plugin';
$string['assignsubmissionpluginname'] = 'Submission plugin';
+$string['attemptheading'] = 'Attempt: {$a->attemptnumber}. {$a->submissionsummary}';
+$string['attemptnumber'] = 'Attempt number';
+$string['attempthistory'] = 'Previous attempts';
+$string['attemptsettings'] = 'Attempt settings';
+$string['attemptreopenmethod'] = 'Attempts reopened';
+$string['attemptreopenmethod_help'] = 'Determines how student submission attempts are reopened. The available options are:
- Never - The student submission cannot be reopened.
- Manually - The student submission can be reopened by a teacher.
- Automatically until pass - The student submission is automatically reopened until the student achieves the grade to pass value that is set in the gradebook for this assignment.
';
+$string['attemptreopenmethod_manual'] = 'Manually';
+$string['attemptreopenmethod_none'] = 'Never';
+$string['attemptreopenmethod_untilpass'] = 'Automatically until pass';
$string['availability'] = 'Availability';
$string['backtoassignment'] = 'Back to assignment';
$string['batchoperationsdescription'] = 'With selected...';
@@ -66,6 +80,7 @@ $string['batchoperationconfirmlock'] = 'Lock all selected submissions?';
$string['batchoperationconfirmgrantextension'] = 'Grant an extension to all selected submissions?';
$string['batchoperationconfirmunlock'] = 'Unlock all selected submissions?';
$string['batchoperationconfirmreverttodraft'] = 'Revert selected submissions to draft?';
+$string['batchoperationconfirmaddattempt'] = 'Allow another attempt for selected submissions?';
$string['batchoperationlock'] = 'lock submissions';
$string['batchoperationunlock'] = 'unlock submissions';
$string['batchoperationreverttodraft'] = 'revert submissions to draft';
@@ -78,7 +93,7 @@ $string['comment'] = 'Comment';
$string['completionsubmit'] = 'Student must submit to this activity to complete it';
$string['conversionexception'] = 'Could not convert assignment. Exception was: {$a}.';
$string['configshowrecentsubmissions'] = 'Everyone can see notifications of submissions in recent activity reports.';
-$string['confirmsubmission'] = 'Are you sure you want to submit your work for grading? You will not be able to make any more changes';
+$string['confirmsubmission'] = 'Are you sure you want to submit your work for grading? You will not be able to make any more changes.';
$string['confirmbatchgradingoperation'] = 'Are you sure you want to {$a->operation} for {$a->count} students?';
$string['couldnotconvertgrade'] = 'Could not convert assignment grade for user {$a}.';
$string['couldnotconvertsubmission'] = 'Could not convert assignment submission for user {$a}.';
@@ -86,6 +101,8 @@ $string['couldnotcreatecoursemodule'] = 'Could not create course module.';
$string['couldnotcreatenewassignmentinstance'] = 'Could not create new assignment instance.';
$string['couldnotfindassignmenttoupgrade'] = 'Could not find old assignment instance to upgrade.';
$string['currentgrade'] = 'Current grade in gradebook';
+$string['currentattempt'] = 'This is attempt {$a}.';
+$string['currentattemptof'] = 'This is attempt {$a->attemptnumber} ( {$a->maxattempts} attempts allowed ).';
$string['cutoffdate'] = 'Cut-off date';
$string['cutoffdate_help'] = 'If set, the assignment will not accept submissions after this date without an extension.';
$string['cutoffdatevalidation'] = 'The cut-off date cannot be earlier than the due date.';
@@ -106,7 +123,10 @@ $string['duedateno'] = 'No due date';
$string['submissionempty'] = 'Nothing was submitted';
$string['duedatereached'] = 'The due date for this assignment has now passed';
$string['duedatevalidation'] = 'Due date must be after the allow submissions from date.';
-$string['editsubmission'] = 'Edit my submission';
+$string['editattemptfeedback'] = 'Edit the grade and feedback for attempt number {$a}.';
+$string['editingpreviousfeedbackwarning'] = 'You are editing the feedback for a previous attempt. This is attempt {$a->attemptnumber} out of {$a->totalattempts}.';
+$string['editsubmission'] = 'Edit submission';
+$string['editsubmission_help'] = 'Make changes to your submission';
$string['editingstatus'] = 'Editing status';
$string['editaction'] = 'Actions...';
$string['extensionduedate'] = 'Extension due date';
@@ -160,10 +180,11 @@ $string['gradeoutofhelp'] = 'Grade';
$string['gradeoutofhelp_help'] = 'Enter the grade for the student\'s submission here. You may include decimals.';
$string['gradestudent'] = 'Grade student: (id={$a->id}, fullname={$a->fullname}). ';
$string['grading'] = 'Grading';
+$string['gradingchangessaved'] = 'The grade changes were saved';
$string['gradingmethodpreview'] = 'Grading criteria';
$string['gradingoptions'] = 'Options';
$string['gradingstatus'] = 'Grading status';
-$string['gradingstudentprogress'] = 'Grading student {$a->index} of {$a->count}';
+$string['gradingstudent'] = 'Grading student';
$string['gradingsummary'] = 'Grading summary';
$string['hideshow'] = 'Hide/Show';
$string['hiddenuser'] = 'Participant ';
@@ -178,7 +199,9 @@ $string['locksubmissionforstudent'] = 'Prevent any more submissions for student:
$string['locksubmissions'] = 'Lock submissions';
$string['manageassignfeedbackplugins'] = 'Manage assignment feedback plugins';
$string['manageassignsubmissionplugins'] = 'Manage assignment submission plugins';
-$string['maxgrade'] = 'Maximum Grade';
+$string['maxattempts'] = 'Maximum attempts';
+$string['maxattempts_help'] = 'The maximum number of submissions attempts that can be made by a student. After this number of attempts has been made the student's submission will not be able to be reopened.';
+$string['maxgrade'] = 'Maximum grade';
$string['messageprovider:assign_notification'] = 'Assignment notifications';
$string['modulename'] = 'Assignment';
$string['modulename_help'] = 'The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback.
@@ -190,14 +213,15 @@ $string['modulename_link'] = 'mod/assignment/view';
$string['modulenameplural'] = 'Assignments';
$string['mysubmission'] = 'My submission: ';
$string['newsubmissions'] = 'Assignments submitted';
+$string['noattempt'] = 'No attempt';
$string['nofiles'] = 'No files. ';
$string['nograde'] = 'No grade. ';
$string['nolatesubmissions'] = 'No late submissions accepted. ';
+$string['nomoresubmissionsaccepted'] = 'No more submissions accepted';
$string['noonlinesubmissions'] = 'This assignment does not require you to submit anything online';
$string['nosavebutnext'] = 'Next';
$string['nosubmission'] = 'Nothing has been submitted for this assignment';
$string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
-$string['nomoresubmissionsaccepted'] = 'No more submissions accepted';
$string['notgraded'] = 'Not graded';
$string['notgradedyet'] = 'Not graded yet';
$string['notsubmittedyet'] = 'Not submitted yet';
@@ -210,6 +234,7 @@ $string['numberofsubmissionsneedgrading'] = 'Needs grading';
$string['numberofteams'] = 'Groups';
$string['offline'] = 'No online submissions required';
$string['open'] = 'Open';
+$string['outof'] = '{$a->current} out of {$a->total}';
$string['overdue'] = 'Assignment is overdue by: {$a}';
$string['outlinegrade'] = 'Grade: {$a}';
$string['page-mod-assign-x'] = 'Any assignment module page';
@@ -238,6 +263,7 @@ $string['reverttodraft'] = 'Revert the submission to draft status.';
$string['reverttodraftshort'] = 'Revert the submission to draft';
$string['reviewed'] = 'Reviewed';
$string['savechanges'] = 'Save changes';
+$string['savegradingresult'] = 'Grade';
$string['saveallquickgradingchanges'] = 'Save all quick grading changes';
$string['savenext'] = 'Save and show next';
$string['scale'] = 'Scale';
@@ -251,9 +277,20 @@ $string['sendsubmissionreceipts'] = 'Send submission receipt to students';
$string['sendsubmissionreceipts_help'] = 'This switch will enable submission receipts for students. Students will receive a notification every time they successfully submit an assignment';
$string['settings'] = 'Assignment settings';
$string['showrecentsubmissions'] = 'Show recent submissions';
+$string['submissioncopiedtext'] = 'You have made a copy of your previous
+assignment submission for \'{$a->assignment}\'
+
+You can see the status of your assignment submission:
+
+ {$a->url}';
+$string['submissioncopiedhtml'] = 'You have made a copy of your previous
+assignment submission for \'{$a->assignment}\'
+You can see the status of your assignment submission.';
+$string['submissioncopiedsmall'] = 'You have copied your previous assignment submission for {$a->assignment}';
$string['submissiondrafts'] = 'Require students click submit button';
$string['submissiondrafts_help'] = 'If enabled, students will have to click a Submit button to declare their submission as final. This allows students to keep a draft version of the submission on the system. If this setting is changed from "No" to "Yes" after students have already submitted those submissions will be regarded as final.';
$string['submissioneditable'] = 'Student can edit this submission';
+$string['submissionnotcopiedinvalidstatus'] = 'The submission was not copied because it has been edited since it was reopened.';
$string['submissionnoteditable'] = 'Student cannot edit this submission';
$string['submissionnotready'] = 'This assignment is not ready to submit:';
$string['submissionplugins'] = 'Submission plugins';
@@ -282,13 +319,15 @@ $string['submissionstatus_draft'] = 'Draft (not submitted)';
$string['submissionstatusheading'] = 'Submission status';
$string['submissionstatus_marked'] = 'Graded';
$string['submissionstatus_new'] = 'New submission';
+$string['submissionstatus_reopened'] = 'Reopened';
$string['submissionstatus_'] = 'No submission';
$string['submissionstatus'] = 'Submission status';
$string['submissionstatus_submitted'] = 'Submitted for grading';
+$string['submissionsummary'] = '{$a->status}. Last modified on {$a->timemodified}';
$string['submissionteam'] = 'Group';
$string['submission'] = 'Submission';
$string['submitaction'] = 'Submit';
-$string['submitassignment_help'] = 'Once this assignment is submitted you will not be able to make any more changes';
+$string['submitassignment_help'] = 'Once this assignment is submitted you will not be able to make any more changes.';
$string['submitassignment'] = 'Submit assignment';
$string['submittedearly'] = 'Assignment was submitted {$a} early';
$string['submittedlate'] = 'Assignment was submitted {$a} late';
@@ -304,6 +343,8 @@ $string['timemodified'] = 'Last modified';
$string['timeremaining'] = 'Time remaining';
$string['unlocksubmissionforstudent'] = 'Allow submissions for student: (id={$a->id}, fullname={$a->fullname}).';
$string['unlocksubmissions'] = 'Unlock submissions';
+$string['unlimitedattempts'] = 'Unlimited';
+$string['unlimitedattemptsallowed'] = 'Unlimited attempts allowed.';
$string['updategrade'] = 'Update grade';
$string['updatetable'] = 'Save and update table';
$string['upgradenotimplemented'] = 'Upgrade not implemented in plugin ({$a->type} {$a->subtype})';
diff --git a/mod/assign/lib.php b/mod/assign/lib.php
index c57fe3bcaad..60e1b4efd31 100644
--- a/mod/assign/lib.php
+++ b/mod/assign/lib.php
@@ -150,20 +150,33 @@ function assign_update_instance(stdClass $data, $form) {
*/
function assign_supports($feature) {
switch($feature) {
- case FEATURE_GROUPS: return true;
- case FEATURE_GROUPINGS: return true;
- case FEATURE_GROUPMEMBERSONLY: return true;
- case FEATURE_MOD_INTRO: return true;
- case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
- case FEATURE_COMPLETION_HAS_RULES: return true;
- case FEATURE_GRADE_HAS_GRADE: return true;
- case FEATURE_GRADE_OUTCOMES: return true;
- case FEATURE_BACKUP_MOODLE2: return true;
- case FEATURE_SHOW_DESCRIPTION: return true;
- case FEATURE_ADVANCED_GRADING: return true;
- case FEATURE_PLAGIARISM: return true;
+ case FEATURE_GROUPS:
+ return true;
+ case FEATURE_GROUPINGS:
+ return true;
+ case FEATURE_GROUPMEMBERSONLY:
+ return true;
+ case FEATURE_MOD_INTRO:
+ return true;
+ case FEATURE_COMPLETION_TRACKS_VIEWS:
+ return true;
+ case FEATURE_COMPLETION_HAS_RULES:
+ return true;
+ case FEATURE_GRADE_HAS_GRADE:
+ return true;
+ case FEATURE_GRADE_OUTCOMES:
+ return true;
+ case FEATURE_BACKUP_MOODLE2:
+ return true;
+ case FEATURE_SHOW_DESCRIPTION:
+ return true;
+ case FEATURE_ADVANCED_GRADING:
+ return true;
+ case FEATURE_PLAGIARISM:
+ return true;
- default: return null;
+ default:
+ return null;
}
}
diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php
index 9ba0f6de1d7..08cabe2f1d1 100644
--- a/mod/assign/locallib.php
+++ b/mod/assign/locallib.php
@@ -27,6 +27,7 @@
defined('MOODLE_INTERNAL') || die();
// Assignment submission statuses.
+define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
@@ -35,6 +36,14 @@ define('ASSIGN_FILTER_SUBMITTED', 'submitted');
define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
define('ASSIGN_FILTER_REQUIRE_GRADING', 'require_grading');
+// Reopen attempt methods.
+define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
+define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
+define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
+
+// Special value means allow unlimited attempts.
+define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
+
require_once($CFG->libdir . '/accesslib.php');
require_once($CFG->libdir . '/formslib.php');
require_once($CFG->dirroot . '/repository/lib.php');
@@ -338,7 +347,7 @@ class assign {
* the settings for the assignment and the status of the assignment.
*
* @param string $action The current action if any.
- * @return void
+ * @return string - The page output.
*/
public function view($action='') {
@@ -355,10 +364,20 @@ class assign {
$action = 'redirect';
$nextpageparams['action'] = 'view';
}
+ } else if ($action == 'editprevioussubmission') {
+ $action = 'editsubmission';
+ if ($this->process_copy_previous_attempt($notices)) {
+ $action = 'redirect';
+ $nextpageparams['action'] = 'editsubmission';
+ }
} else if ($action == 'lock') {
$this->process_lock();
$action = 'redirect';
$nextpageparams['action'] = 'grading';
+ } else if ($action == 'addattempt') {
+ $this->process_add_attempt(required_param('userid', PARAM_INT));
+ $action = 'redirect';
+ $nextpageparams['action'] = 'grading';
} else if ($action == 'reverttodraft') {
$this->process_revert_to_draft();
$action = 'redirect';
@@ -403,8 +422,8 @@ class assign {
// Save changes button.
$action = 'grade';
if ($this->process_save_grade($mform)) {
- $action = 'redirect';
- $nextpageparams['action'] = 'grading';
+ $message = get_string('gradingchangessaved', 'assign');
+ $action = 'savegradingresult';
}
} else {
// Cancel button.
@@ -439,6 +458,8 @@ class assign {
$nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
redirect($nextpageurl);
return;
+ } else if ($action == 'savegradingresult') {
+ $o .= $this->view_savegrading_result($message);
} else if ($action == 'quickgradingresult') {
$mform = null;
$o .= $this->view_quickgrading_result($message);
@@ -509,6 +530,10 @@ class assign {
$update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
$update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
$update->blindmarking = $formdata->blindmarking;
+ $update->attemptreopenmethod = $formdata->attemptreopenmethod;
+ if (!empty($formdata->maxattempts)) {
+ $update->maxattempts = $formdata->maxattempts;
+ }
$returnid = $DB->insert_record('assign', $update);
$this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
@@ -823,6 +848,10 @@ class assign {
$update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
$update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
$update->blindmarking = $formdata->blindmarking;
+ $update->attemptreopenmethod = $formdata->attemptreopenmethod;
+ if (!empty($formdata->maxattempts)) {
+ $update->maxattempts = $formdata->maxattempts;
+ }
$result = $DB->update_record('assign', $update);
$this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
@@ -867,6 +896,7 @@ class assign {
foreach ($this->feedbackplugins as $plugin) {
if ($plugin->is_enabled() && $plugin->is_visible()) {
$mform->addElement('header', 'header_' . $plugin->get_type(), $plugin->get_name());
+ $mform->setExpanded('header_' . $plugin->get_type());
if (!$plugin->get_form_elements_for_user($grade, $mform, $data, $userid)) {
$mform->removeElement('header_' . $plugin->get_type());
}
@@ -914,7 +944,7 @@ class assign {
* @return void
*/
public function add_all_plugin_settings(MoodleQuickForm $mform) {
- $mform->addElement('header', 'submissiontypes', get_string('submissionsettings', 'assign'));
+ $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
$submissionpluginsenabled = array();
$group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
@@ -923,7 +953,7 @@ class assign {
}
$group->setElements($submissionpluginsenabled);
- $mform->addElement('header', 'feedbacktypes', get_string('feedbacksettings', 'assign'));
+ $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
$feedbackpluginsenabled = array();
$group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
foreach ($this->feedbackplugins as $plugin) {
@@ -1240,16 +1270,29 @@ class assign {
$currentgroup = groups_get_activity_group($this->get_course_module(), true);
list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, false);
+ $submissionmaxattempt = 'SELECT mxs.userid, MAX(mxs.attemptnumber) AS maxattempt
+ FROM {assign_submission} mxs
+ WHERE mxs.assignment = :assignid2 GROUP BY mxs.userid';
+ $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
+ FROM {assign_grades} mxg
+ WHERE mxg.assignment = :assignid3 GROUP BY mxg.userid';
+
$params['assignid'] = $this->get_instance()->id;
+ $params['assignid2'] = $this->get_instance()->id;
+ $params['assignid3'] = $this->get_instance()->id;
$params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
$sql = 'SELECT COUNT(s.userid)
FROM {assign_submission} s
+ LEFT JOIN ( ' . $submissionmaxattempt . ' ) smx ON s.userid = smx.userid
+ LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON s.userid = gmx.userid
LEFT JOIN {assign_grades} g ON
s.assignment = g.assignment AND
- s.userid = g.userid
+ s.userid = g.userid AND
+ g.attemptnumber = gmx.maxattempt
JOIN(' . $esql . ') e ON e.id = s.userid
WHERE
+ s.attemptnumber = smx.maxattempt AND
s.assignment = :assignid AND
s.timemodified IS NOT NULL AND
s.status = :submitted AND
@@ -1299,7 +1342,7 @@ class assign {
if ($this->get_instance()->teamsubmission) {
// We cannot join on the enrolment tables for group submissions (no userid).
- $sql = 'SELECT COUNT(s.groupid)
+ $sql = 'SELECT COUNT(DISTINCT s.groupid)
FROM {assign_submission} s
WHERE
s.assignment = :assignid AND
@@ -1314,7 +1357,7 @@ class assign {
$params['assignid'] = $this->get_instance()->id;
- $sql = 'SELECT COUNT(s.userid)
+ $sql = 'SELECT COUNT(DISTINCT s.userid)
FROM {assign_submission} s
JOIN(' . $esql . ') e ON e.id = s.userid
WHERE
@@ -1338,22 +1381,35 @@ class assign {
list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, false);
$params['assignid'] = $this->get_instance()->id;
+ $params['assignid2'] = $this->get_instance()->id;
$params['submissionstatus'] = $status;
if ($this->get_instance()->teamsubmission) {
+ $maxattemptsql = 'SELECT mxs.groupid, MAX(mxs.attemptnumber) AS maxattempt
+ FROM {assign_submission} mxs
+ WHERE mxs.assignment = :assignid2 GROUP BY mxs.groupid';
+
$sql = 'SELECT COUNT(s.groupid)
FROM {assign_submission} s
+ JOIN(' . $maxattemptsql . ') smx ON s.groupid = smx.groupid
WHERE
+ s.attemptnumber = smx.maxattempt AND
s.assignment = :assignid AND
s.timemodified IS NOT NULL AND
s.userid = :groupuserid AND
s.status = :submissionstatus';
$params['groupuserid'] = 0;
} else {
+ $maxattemptsql = 'SELECT mxs.userid, MAX(mxs.attemptnumber) AS maxattempt
+ FROM {assign_submission} mxs
+ WHERE mxs.assignment = :assignid2 GROUP BY mxs.userid';
+
$sql = 'SELECT COUNT(s.userid)
FROM {assign_submission} s
JOIN(' . $esql . ') e ON e.id = s.userid
+ JOIN(' . $maxattemptsql . ') smx ON s.userid = smx.userid
WHERE
+ s.attemptnumber = smx.maxattempt AND
s.assignment = :assignid AND
s.timemodified IS NOT NULL AND
s.status = :submissionstatus';
@@ -1413,14 +1469,14 @@ class assign {
$timenow = time();
// Collect all submissions from the past 24 hours that require mailing.
- $sql = 'SELECT s.*, a.course, a.name, a.blindmarking, a.revealidentities,
+ $sql = 'SELECT a.course, a.name, a.blindmarking, a.revealidentities,
g.*, g.id as gradeid, g.timemodified as lastmodified
FROM {assign} a
JOIN {assign_grades} g ON g.assignment = a.id
- LEFT JOIN {assign_submission} s ON s.assignment = a.id AND s.userid = g.userid
+ LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
WHERE g.timemodified >= :yesterday AND
g.timemodified <= :today AND
- g.mailed = 0';
+ uf.mailed = 0';
$params = array('yesterday' => $yesterday, 'today' => $timenow);
$submissions = $DB->get_records_sql($sql, $params);
@@ -1541,10 +1597,17 @@ class assign {
$showusers,
$uniqueid);
- $grade = new stdClass();
- $grade->id = $submission->gradeid;
- $grade->mailed = 1;
- $DB->update_record('assign_grades', $grade);
+ $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
+ if ($flags) {
+ $flags->mailed = 1;
+ $DB->update_record('assign_user_flags', $flags);
+ } else {
+ $flags = new stdClass();
+ $flags->userid = $user->id;
+ $flags->assignment = $submission->assignment;
+ $flags->mailed = 1;
+ $DB->insert_record('assign_user_flags', $flags);
+ }
mtrace('Done');
}
@@ -1568,12 +1631,28 @@ class assign {
public function notify_grade_modified($grade) {
global $DB;
- $grade->timemodified = time();
- if ($grade->mailed != 1) {
- $grade->mailed = 0;
+ $flags = $this->get_user_flags($grade->userid, true);
+ if ($flags->mailed != 1) {
+ $flags->mailed = 0;
}
- return $DB->update_record('assign_grades', $grade);
+ return $this->update_user_flags($flags);
+ }
+
+ /**
+ * Update user flags for this user in this assignment.
+ *
+ * @param stdClass $flags a flags record keyed on id
+ * @return bool true for success
+ */
+ public function update_user_flags($flags) {
+ global $DB;
+ if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
+ return false;
+ }
+
+ $result = $DB->update_record('assign_user_flags', $flags);
+ return $result;
}
/**
@@ -1608,6 +1687,19 @@ class assign {
}
$result = $DB->update_record('assign_grades', $grade);
+
+ // Only push to gradebook if the update is for the latest attempt.
+ $submission = null;
+ if ($this->get_instance()->teamsubmission) {
+ $submission = $this->get_group_submission($grade->userid, 0, false);
+ } else {
+ $submission = $this->get_user_submission($grade->userid, false);
+ }
+ // Not the latest attempt.
+ if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
+ return true;
+ }
+
if ($result) {
$this->gradebook_item_update(null, $grade);
}
@@ -1713,7 +1805,7 @@ class assign {
foreach ($members as $id => $member) {
$submission = $this->get_user_submission($member->id, false);
- if ($submission && $submission->status != ASSIGN_SUBMISSION_STATUS_DRAFT) {
+ if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
unset($members[$id]);
} else {
if ($this->is_blind_marking()) {
@@ -1732,9 +1824,10 @@ class assign {
* @param int $groupid The id of the group for this user - may be 0 in which
* case it is determined from the userid.
* @param bool $create If set to true a new submission object will be created in the database
+ * @param int $attemptnumber - -1 means the latest attempt
* @return stdClass The submission
*/
- public function get_group_submission($userid, $groupid, $create) {
+ public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
global $DB;
if ($groupid == 0) {
@@ -1744,30 +1837,18 @@ class assign {
}
}
- if ($create) {
- // Make sure there is a submission for this user.
- $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>0, 'userid'=>$userid);
- $submission = $DB->get_record('assign_submission', $params);
-
- if (!$submission) {
- $submission = new stdClass();
- $submission->assignment = $this->get_instance()->id;
- $submission->userid = $userid;
- $submission->groupid = 0;
- $submission->timecreated = time();
- $submission->timemodified = $submission->timecreated;
-
- if ($this->get_instance()->submissiondrafts) {
- $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
- } else {
- $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
- }
- $DB->insert_record('assign_submission', $submission);
- }
- }
// Now get the group submission.
$params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
- $submission = $DB->get_record('assign_submission', $params);
+ if ($attemptnumber >= 0) {
+ $params['attemptnumber'] = $attemptnumber;
+ }
+
+ // Only return the row with the highest attemptnumber.
+ $submission = null;
+ $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
+ if ($submissions) {
+ $submission = reset($submissions);
+ }
if ($submission) {
return $submission;
@@ -1779,12 +1860,13 @@ class assign {
$submission->groupid = $groupid;
$submission->timecreated = time();
$submission->timemodified = $submission->timecreated;
-
- if ($this->get_instance()->submissiondrafts) {
- $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ if ($attemptnumber >= 0) {
+ $submission->attemptnumber = $attemptnumber;
} else {
- $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ $submission->attemptnumber = 0;
}
+
+ $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
$sid = $DB->insert_record('assign_submission', $submission);
$submission->id = $sid;
return $submission;
@@ -2077,6 +2159,25 @@ class assign {
return $result;
}
+ /**
+ * Display a continue page.
+ *
+ * @return string
+ */
+ protected function view_savegrading_result($message) {
+ $o = '';
+ $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
+ $this->get_context(),
+ $this->show_intro(),
+ $this->get_course_module()->id,
+ get_string('savegradingresult', 'assign')));
+ $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
+ $message,
+ $this->get_course_module()->id);
+ $o .= $this->get_renderer()->render($gradingresult);
+ $o .= $this->view_footer();
+ return $o;
+ }
/**
* Display a grading error.
*
@@ -2090,7 +2191,9 @@ class assign {
$this->show_intro(),
$this->get_course_module()->id,
get_string('quickgradingresult', 'assign')));
- $gradingresult = new assign_quickgrading_result($message, $this->get_course_module()->id);
+ $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
+ $message,
+ $this->get_course_module()->id);
$o .= $this->get_renderer()->render($gradingresult);
$o .= $this->view_footer();
return $o;
@@ -2122,7 +2225,7 @@ class assign {
/**
* Download a zip file of all assignment submissions.
*
- * @return void
+ * @return string - If an error occurs, this will contain the error page.
*/
protected function download_submissions() {
global $CFG, $DB;
@@ -2271,9 +2374,10 @@ class assign {
* @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
* @param bool $create optional - defaults to false. If set to true a new submission object
* will be created in the database.
+ * @param int $attemptnumber - -1 means the latest attempt
* @return stdClass The submission
*/
- public function get_user_submission($userid, $create) {
+ public function get_user_submission($userid, $create, $attemptnumber=-1) {
global $DB, $USER;
if (!$userid) {
@@ -2281,7 +2385,16 @@ class assign {
}
// If the userid is not null then use userid.
$params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
- $submission = $DB->get_record('assign_submission', $params);
+ if ($attemptnumber >= 0) {
+ $params['attemptnumber'] = $attemptnumber;
+ }
+
+ // Only return the row with the highest attemptnumber.
+ $submission = null;
+ $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
+ if ($submissions) {
+ $submission = reset($submissions);
+ }
if ($submission) {
return $submission;
@@ -2293,6 +2406,11 @@ class assign {
$submission->timecreated = time();
$submission->timemodified = $submission->timecreated;
$submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ if ($attemptnumber >= 0) {
+ $submission->attemptnumber = $attemptnumber;
+ } else {
+ $submission->attemptnumber = 0;
+ }
$sid = $DB->insert_record('assign_submission', $submission);
$submission->id = $sid;
return $submission;
@@ -2314,24 +2432,71 @@ class assign {
}
/**
- * This will retrieve a grade object from the db, optionally creating it if required.
+ * This will retrieve a user flags object from the db optionally creating it if required.
+ * The user flags was split from the user_grades table in 2.5.
*
- * @param int $userid The user we are grading
- * @param bool $create If true the grade will be created if it does not exist
- * @return stdClass The grade record
+ * @param int $userid The user we are getting the flags for.
+ * @param bool $create If true the flags record will be created if it does not exist
+ * @return stdClass The flags record
*/
- public function get_user_grade($userid, $create) {
+ public function get_user_flags($userid, $create) {
global $DB, $USER;
+ // If the userid is not null then use userid.
if (!$userid) {
$userid = $USER->id;
}
- // If the userid is not null then use userid.
- $grade = $DB->get_record('assign_grades', array('assignment'=>$this->get_instance()->id, 'userid'=>$userid));
+ $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
- if ($grade) {
- return $grade;
+ $flags = $DB->get_record('assign_user_flags', $params);
+
+ if ($flags) {
+ return $flags;
+ }
+ if ($create) {
+ $flags = new stdClass();
+ $flags->assignment = $this->get_instance()->id;
+ $flags->userid = $userid;
+ $flags->locked = 0;
+ $flags->extensionduedate = 0;
+
+ // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
+ // This is because students only want to be notified about certain types of update (grades and feedback).
+ $flags->mailed = 2;
+
+ $fid = $DB->insert_record('assign_user_flags', $flags);
+ $flags->id = $fid;
+ return $flags;
+ }
+ return false;
+ }
+
+ /**
+ * This will retrieve a grade object from the db, optionally creating it if required.
+ *
+ * @param int $userid The user we are grading
+ * @param bool $create If true the grade will be created if it does not exist
+ * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
+ * @return stdClass The grade record
+ */
+ public function get_user_grade($userid, $create, $attemptnumber=-1) {
+ global $DB, $USER;
+
+ // If the userid is not null then use userid.
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
+ if ($attemptnumber >= 0) {
+ $params['attemptnumber'] = $attemptnumber;
+ }
+
+ $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
+
+ if ($grades) {
+ return reset($grades);
}
if ($create) {
$grade = new stdClass();
@@ -2339,14 +2504,12 @@ class assign {
$grade->userid = $userid;
$grade->timecreated = time();
$grade->timemodified = $grade->timecreated;
- $grade->locked = 0;
$grade->grade = -1;
$grade->grader = $USER->id;
- $grade->extensionduedate = 0;
+ if ($attemptnumber >= 0) {
+ $grade->attemptnumber = $attemptnumber;
+ }
- // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
- // This is because students only want to be notified about certain types of update (grades and feedback).
- $grade->mailed = 2;
$gid = $DB->insert_record('assign_grades', $grade);
$grade->id = $gid;
return $grade;
@@ -2391,12 +2554,21 @@ class assign {
get_string('grading', 'assign'));
$o .= $this->get_renderer()->render($header);
+ // If userid is passed - we are only grading a single student.
$rownum = required_param('rownum', PARAM_INT);
$useridlistid = optional_param('useridlistid', time(), PARAM_INT);
+ $userid = optional_param('userid', 0, PARAM_INT);
+ $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
+
$cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
- if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
- $useridlist = $this->get_grading_userid_list();
+ if (!$userid) {
+ if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
+ $useridlist = $this->get_grading_userid_list();
+ }
$cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
+ } else {
+ $rownum = 0;
+ $useridlist = array($userid);
}
if ($rownum < 0 || $rownum > count($useridlist)) {
@@ -2419,13 +2591,13 @@ class assign {
get_extra_user_fields($this->get_context()));
$o .= $this->get_renderer()->render($usersummary);
}
- $submission = $this->get_user_submission($userid, false);
+ $submission = $this->get_user_submission($userid, false, $attemptnumber);
$submissiongroup = null;
$submissiongroupmemberswhohavenotsubmitted = array();
$teamsubmission = null;
$notsubmitted = array();
if ($instance->teamsubmission) {
- $teamsubmission = $this->get_group_submission($userid, 0, false);
+ $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
$submissiongroup = $this->get_submission_group($userid);
$groupid = 0;
if ($submissiongroup) {
@@ -2435,13 +2607,14 @@ class assign {
}
- // Get the current grade.
- $grade = $this->get_user_grade($userid, false);
+ // Get the requested grade.
+ $grade = $this->get_user_grade($userid, false, $attemptnumber);
+ $flags = $this->get_user_flags($userid, false);
if ($this->can_view_submission($userid)) {
- $gradelocked = ($grade && $grade->locked) || $this->grading_disabled($userid);
+ $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($userid);
$extensionduedate = null;
- if ($grade) {
- $extensionduedate = $grade->extensionduedate;
+ if ($flags) {
+ $extensionduedate = $flags->extensionduedate;
}
$showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
@@ -2483,9 +2656,12 @@ class assign {
$extensionduedate,
$this->get_context(),
$this->is_blind_marking(),
- '');
+ '',
+ $instance->attemptreopenmethod,
+ $instance->maxattempts);
$o .= $this->get_renderer()->render($submissionstatus);
}
+
if ($grade) {
$data = new stdClass();
if ($grade->grade !== null && $grade->grade >= 0) {
@@ -2495,10 +2671,23 @@ class assign {
$data = new stdClass();
$data->grade = '';
}
+ // Warning if required.
+ $allsubmissions = $this->get_all_submissions($userid);
+
+ if ($attemptnumber != -1) {
+ $params = array('attemptnumber'=>$attemptnumber + 1,
+ 'totalattempts'=>count($allsubmissions));
+ $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
+ $o .= $this->get_renderer()->notification($message);
+ }
// Now show the grading form.
if (!$mform) {
- $pagination = array( 'rownum'=>$rownum, 'useridlistid'=>$useridlistid, 'last'=>$last);
+ $pagination = array('rownum'=>$rownum,
+ 'useridlistid'=>$useridlistid,
+ 'last'=>$last,
+ 'userid'=>optional_param('userid', 0, PARAM_INT),
+ 'attemptnumber'=>$attemptnumber);
$formparams = array($this, $data, $pagination);
$mform = new mod_assign_grade_form(null,
$formparams,
@@ -2506,8 +2695,23 @@ class assign {
'',
array('class'=>'gradeform'));
}
+ $o .= $this->get_renderer()->heading(get_string('grade'), 3);
$o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
+ if (count($allsubmissions) > 1 && $attemptnumber == -1) {
+ $allgrades = $this->get_all_grades($userid);
+ $history = new assign_attempt_history($allsubmissions,
+ $allgrades,
+ $this->get_submission_plugins(),
+ $this->get_feedback_plugins(),
+ $this->get_course_module()->id,
+ $this->get_return_action(),
+ $this->get_return_params(),
+ true);
+
+ $o .= $this->get_renderer()->render($history);
+ }
+
$msg = get_string('viewgradingformforstudent',
'assign',
array('id'=>$user->id, 'fullname'=>fullname($user)));
@@ -2642,6 +2846,7 @@ class assign {
$batchformparams = array('cm'=>$cmid,
'submissiondrafts'=>$this->get_instance()->submissiondrafts,
'duedate'=>$this->get_instance()->duedate,
+ 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
'feedbackplugins'=>$this->get_feedback_plugins());
$classoptions = array('class'=>'gradingbatchoperationsform');
@@ -2780,7 +2985,7 @@ class assign {
* @param moodleform $mform
* @param array $notices A list of notices to display at the top of the
* edit submission form (e.g. from plugins).
- * @return void
+ * @return string The page output.
*/
protected function view_edit_submission_page($mform, $notices) {
global $CFG;
@@ -2914,6 +3119,7 @@ class assign {
$batchformparams = array('cm'=>$this->get_course_module()->id,
'submissiondrafts'=>$this->get_instance()->submissiondrafts,
'duedate'=>$this->get_instance()->duedate,
+ 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
'feedbackplugins'=>$this->get_feedback_plugins());
$formclasses = array('class'=>'gradingbatchoperationsform');
$mform = new mod_assign_grading_batch_operations_form(null,
@@ -2950,8 +3156,16 @@ class assign {
$this->process_unlock($userid);
} else if ($data->operation == 'reverttodraft') {
$this->process_revert_to_draft($userid);
+ } else if ($data->operation == 'addattempt') {
+ if (!$this->get_instance()->teamsubmission) {
+ $this->process_add_attempt($userid);
+ }
}
}
+ if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
+ // This needs to be handled separately so that each team submission is only re-opened one time.
+ $this->process_add_attempt_group($userlist);
+ }
}
return 'grading';
@@ -3025,6 +3239,7 @@ class assign {
$instance = $this->get_instance();
$grade = $this->get_user_grade($user->id, false);
+ $flags = $this->get_user_flags($user->id, false);
$submission = $this->get_user_submission($user->id, false);
$o = '';
@@ -3047,7 +3262,7 @@ class assign {
($this->is_any_submission_plugin_enabled()) &&
$showlinks;
- $gradelocked = ($grade && $grade->locked) || $this->grading_disabled($user->id);
+ $gradelocked = ($flags && $flags->locked) || $this->grading_disabled($user->id);
// Grading criteria preview.
$gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
@@ -3060,18 +3275,18 @@ class assign {
}
$showsubmit = ($submission || $teamsubmission) && $showlinks;
- if ($teamsubmission && ($teamsubmission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+ if ($teamsubmission && ($teamsubmission->status != ASSIGN_SUBMISSION_STATUS_DRAFT)) {
$showsubmit = false;
}
- if ($submission && ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+ if ($submission && ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
$showsubmit = false;
}
if (!$this->get_instance()->submissiondrafts) {
$showsubmit = false;
}
$extensionduedate = null;
- if ($grade) {
- $extensionduedate = $grade->extensionduedate;
+ if ($flags) {
+ $extensionduedate = $flags->extensionduedate;
}
$viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
@@ -3099,7 +3314,9 @@ class assign {
$extensionduedate,
$this->get_context(),
$this->is_blind_marking(),
- $gradingcontrollerpreview);
+ $gradingcontrollerpreview,
+ $instance->attemptreopenmethod,
+ $instance->maxattempts);
$o .= $this->get_renderer()->render($submissionstatus);
require_once($CFG->libdir.'/gradelib.php');
@@ -3168,12 +3385,129 @@ class assign {
$this->get_return_params());
$o .= $this->get_renderer()->render($feedbackstatus);
+
+ $allsubmissions = $this->get_all_submissions($user->id);
+
+ if (count($allsubmissions) > 1) {
+ $allgrades = $this->get_all_grades($user->id);
+ $history = new assign_attempt_history($allsubmissions,
+ $allgrades,
+ $this->get_submission_plugins(),
+ $this->get_feedback_plugins(),
+ $this->get_course_module()->id,
+ $this->get_return_action(),
+ $this->get_return_params(),
+ false);
+
+ $o .= $this->get_renderer()->render($history);
+ }
}
}
return $o;
}
+ /**
+ * Get the grades for all previous attempts.
+ * For each grade - the grader is a full user record,
+ * and gradefordisplay is added (rendered from grading manager).
+ *
+ * @param int $userid If not set, $USER->id will be used.
+ * @return array $grades All grade records for this user.
+ */
+ protected function get_all_grades($userid) {
+ global $DB, $USER, $PAGE;
+
+ // If the userid is not null then use userid.
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
+
+ $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
+
+ $gradercache = array();
+ $cangrade = has_capability('mod/assign:grade', $this->get_context());
+
+ // Need gradingitem and gradingmanager.
+ $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
+ $controller = $gradingmanager->get_active_controller();
+
+ $gradinginfo = grade_get_grades($this->get_course()->id,
+ 'mod',
+ 'assign',
+ $this->get_instance()->id,
+ $userid);
+
+ $gradingitem = null;
+ if (isset($gradinginfo->items[0])) {
+ $gradingitem = $gradinginfo->items[0];
+ }
+
+ foreach ($grades as $grade) {
+ // First lookup the grader info.
+ if (isset($gradercache[$grade->grader])) {
+ $grade->grader = $gradercache[$grade->grader];
+ } else {
+ // Not in cache - need to load the grader record.
+ $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
+ $gradercache[$grade->grader->id] = $grade->grader;
+ }
+
+ // Now get the gradefordisplay.
+ if ($controller) {
+ $controller->set_grade_range(make_grades_menu($this->get_instance()->grade));
+ $grade->gradefordisplay = $controller->render_grade($PAGE,
+ $grade->id,
+ $gradingitem,
+ $grade->grade,
+ $cangrade);
+ } else {
+ $grade->gradefordisplay = $this->display_grade($grade->grade, false);
+ }
+
+ }
+
+ return $grades;
+ }
+
+ /**
+ * Get the submissions for all previous attempts.
+ *
+ * @param int $userid If not set, $USER->id will be used.
+ * @return array $submissions All submission records for this user (or group).
+ */
+ protected function get_all_submissions($userid) {
+ global $DB;
+
+ // If the userid is not null then use userid.
+ if (!$userid) {
+ $userid = $USER->id;
+ }
+
+ $params = array();
+
+ if ($this->get_instance()->teamsubmission) {
+ $groupid = 0;
+ $group = $this->get_submission_group($userid);
+ if ($group) {
+ $groupid = $group->id;
+ }
+
+ // Params to get the group submissions.
+ $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
+ } else {
+ // Params to get the user submissions.
+ $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
+ }
+
+ // Return the submissions ordered by attempt.
+ $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
+
+ return $submissions;
+ }
+
/**
* View submissions page (contains details of current submission).
*
@@ -3330,7 +3664,7 @@ class assign {
}
// First update the submission for the current user.
- $mysubmission = $this->get_user_submission($userid, true);
+ $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
$mysubmission->status = $submission->status;
$this->update_submission($mysubmission, 0, $updatetime, false);
@@ -3340,32 +3674,43 @@ class assign {
$allsubmitted = true;
$anysubmitted = false;
- foreach ($team as $member) {
- $membersubmission = $this->get_user_submission($member->id, false);
+ $result = true;
+ if ($submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
+ foreach ($team as $member) {
+ $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
- if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
- $allsubmitted = false;
- if ($anysubmitted) {
- break;
+ if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
+ $allsubmitted = false;
+ if ($anysubmitted) {
+ break;
+ }
+ } else {
+ $anysubmitted = true;
}
- } else {
- $anysubmitted = true;
}
- }
- if ($this->get_instance()->requireallteammemberssubmit) {
- if ($allsubmitted) {
- $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ if ($this->get_instance()->requireallteammemberssubmit) {
+ if ($allsubmitted) {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ } else {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ }
+ $result = $DB->update_record('assign_submission', $submission);
} else {
- $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ if ($anysubmitted) {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ } else {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ }
+ $result = $DB->update_record('assign_submission', $submission);
}
- $result= $DB->update_record('assign_submission', $submission);
} else {
- if ($anysubmitted) {
- $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
- } else {
- $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ // Set the group submission to reopened.
+ foreach ($team as $member) {
+ $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
+ $membersubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
+ $result = $DB->update_record('assign_submission', $membersubmission) && $result;
}
- $result= $DB->update_record('assign_submission', $submission);
+ $result = $DB->update_record('assign_submission', $submission) && $result;
}
$this->gradebook_item_update($submission);
@@ -3423,13 +3768,18 @@ class assign {
if ($this->get_instance()->cutoffdate) {
$finaldate = $this->get_instance()->cutoffdate;
}
+
+ $flags = $this->get_user_flags($userid, false);
+ if ($flags && $flags->locked) {
+ return false;
+ }
+
// User extensions.
if ($finaldate) {
- $grade = $this->get_user_grade($userid, false);
- if ($grade && $grade->extensionduedate) {
+ if ($flags && $flags->extensionduedate) {
// Extension can be before cut off date.
- if ($grade->extensionduedate > $finaldate) {
- $finaldate = $grade->extensionduedate;
+ if ($flags->extensionduedate > $finaldate) {
+ $finaldate = $flags->extensionduedate;
}
}
}
@@ -3461,11 +3811,6 @@ class assign {
return false;
}
}
- if ($grade = $this->get_user_grade($userid, false)) {
- if ($grade->locked) {
- return false;
- }
- }
if ($this->grading_disabled($userid)) {
return false;
@@ -3720,6 +4065,32 @@ class assign {
$this->get_uniqueid_for_user($userfrom->id));
}
+ /**
+ * Notify student upon successful submission copy.
+ *
+ * @param stdClass $submission
+ * @return void
+ */
+ protected function notify_student_submission_copied(stdClass $submission) {
+ global $DB, $USER;
+
+ $adminconfig = $this->get_admin_config();
+ // Use the same setting for this - no need for another one.
+ if (empty($adminconfig->submissionreceipts)) {
+ // No need to do anything.
+ return;
+ }
+ if ($submission->userid) {
+ $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
+ } else {
+ $user = $USER;
+ }
+ $this->send_notification($user,
+ $user,
+ 'submissioncopied',
+ 'assign_notification',
+ $submission->timemodified);
+ }
/**
* Notify student upon successful submission.
*
@@ -3829,7 +4200,7 @@ class assign {
// Give each submission plugin a chance to process the submission.
$plugins = $this->get_submission_plugins();
foreach ($plugins as $plugin) {
- $plugin->submit_for_grading();
+ $plugin->submit_for_grading($submission);
}
$submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
@@ -3873,14 +4244,13 @@ class assign {
protected function save_user_extension($userid, $extensionduedate) {
global $DB;
- $grade = $this->get_user_grade($userid, true);
- $grade->extensionduedate = $extensionduedate;
- $grade->timemodified = time();
+ $flags = $this->get_user_flags($userid, true);
+ $flags->extensionduedate = $extensionduedate;
- $result = $DB->update_record('assign_grades', $grade);
+ $result = $this->update_user_flags($flags);
if ($result) {
- $this->add_to_log('grant extension', $this->format_grade_for_log($grade));
+ $this->add_to_log('grant extension', $userid);
}
return $result;
}
@@ -3977,13 +4347,22 @@ class assign {
}
list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
- $params['assignment'] = $this->get_instance()->id;
+ $params['assignid1'] = $this->get_instance()->id;
+ $params['assignid2'] = $this->get_instance()->id;
// Check them all for currency.
+ $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
+ FROM {assign_grades} mxg
+ WHERE mxg.assignment = :assignid1 GROUP BY mxg.userid';
+
$sql = 'SELECT u.id as userid, g.grade as grade, g.timemodified as lastmodified
- FROM {user} u
- LEFT JOIN {assign_grades} g ON u.id = g.userid AND g.assignment = :assignment
- WHERE u.id ' . $userids;
+ FROM {user} u
+ LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
+ LEFT JOIN {assign_grades} g ON
+ u.id = g.userid AND
+ g.assignment = :assignid2 AND
+ g.attemptnumber = gmx.maxattempt
+ WHERE u.id ' . $userids;
$currentgrades = $DB->get_recordset_sql($sql, $params);
$modifiedusers = array();
@@ -4200,12 +4579,6 @@ class assign {
} else {
$info .= get_string('nograde', 'assign');
}
- if ($grade->locked) {
- $info .= get_string('submissionslocked', 'assign') . '. ';
- }
- if (!empty($grade->extensionduedate)) {
- $info .= get_string('userextensiondate', 'assign', userdate($grade->extensionduedate));
- }
return $info;
}
@@ -4233,6 +4606,100 @@ class assign {
return $info;
}
+ /**
+ * Copy the current assignment submission from the last submitted attempt.
+ *
+ * @param array $notices Any error messages that should be shown
+ * to the user at the top of the edit submission form.
+ * @return bool
+ */
+ protected function process_copy_previous_attempt(&$notices) {
+ global $USER, $CFG;
+
+ $instance = $this->get_instance();
+ if ($instance->teamsubmission) {
+ $submission = $this->get_group_submission($USER->id, 0, true);
+ } else {
+ $submission = $this->get_user_submission($USER->id, true);
+ }
+ if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
+ $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
+ return false;
+ }
+ $flags = $this->get_user_flags($USER->id, false);
+
+ // Get the flags to check if it is locked.
+ if ($flags && $flags->locked) {
+ $notices[] = get_string('submissionslocked', 'assign');
+ return false;
+ }
+ if ($instance->submissiondrafts) {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+ } else {
+ $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ }
+ $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
+
+ // Find the previous submission.
+ if ($instance->teamsubmission) {
+ $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
+ } else {
+ $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
+ }
+
+ if (!$previoussubmission) {
+ // There was no previous submission so there is nothing else to do.
+ return true;
+ }
+
+ $pluginerror = false;
+ foreach ($this->get_submission_plugins() as $plugin) {
+ if ($plugin->is_visible() && $plugin->is_enabled()) {
+ if (!$plugin->copy_submission($previoussubmission, $submission)) {
+ $notices[] = $plugin->get_error();
+ $pluginerror = true;
+ }
+ }
+ }
+ if ($pluginerror) {
+ return false;
+ }
+
+ $this->add_to_log('submissioncopied', $this->format_submission_for_log($submission));
+
+ $complete = COMPLETION_INCOMPLETE;
+ if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
+ $complete = COMPLETION_COMPLETE;
+ }
+ $completion = new completion_info($this->get_course());
+ if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
+ $completion->update_state($this->get_course_module(), $complete, $USER->id);
+ }
+
+ if (!$instance->submissiondrafts) {
+ // There is a case for not notifying the student about the submission copy,
+ // but it provides a record of the event and if they then cancel editing it
+ // is clear that the submission was copied.
+ $this->notify_student_submission_copied($submission);
+ $this->notify_graders($submission);
+ // Trigger assessable_submitted event on submission.
+ // The same logic applies here - we could not notify teachers,
+ // but then they would wonder why there are submitted assignments
+ // and they haven't been notified.
+ $eventdata = new stdClass();
+ $eventdata->modulename = 'assign';
+ $eventdata->cmid = $this->get_course_module()->id;
+ $eventdata->itemid = $submission->id;
+ $eventdata->courseid = $this->get_course()->id;
+ $eventdata->userid = $USER->id;
+ $eventdata->params = array(
+ 'submission_editable' => true,
+ );
+ events_trigger('assessable_submitted', $eventdata);
+ }
+ return true;
+ }
+
/**
* Save assignment submission.
*
@@ -4269,10 +4736,10 @@ class assign {
$submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
}
- $grade = $this->get_user_grade($USER->id, false);
+ $flags = $this->get_user_flags($USER->id, false);
- // Get the grade to check if it is locked.
- if ($grade && $grade->locked) {
+ // Get the flags to check if it is locked.
+ if ($flags && $flags->locked) {
print_error('submissionslocked', 'assign');
return true;
}
@@ -4373,10 +4840,9 @@ class assign {
* @param bool $gradingdisabled
* @return mixed gradingform_instance|null $gradinginstance
*/
- protected function get_grading_instance($userid, $gradingdisabled) {
+ protected function get_grading_instance($userid, $grade, $gradingdisabled) {
global $CFG, $USER;
- $grade = $this->get_user_grade($userid, false);
$grademenu = make_grades_menu($this->get_instance()->grade);
$advancedgradingwarning = false;
@@ -4422,19 +4888,35 @@ class assign {
$rownum = $params['rownum'];
$last = $params['last'];
$useridlistid = $params['useridlistid'];
- $cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
- if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
- $useridlist = $this->get_grading_userid_list();
- $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
+ $userid = $params['userid'];
+ $attemptnumber = $params['attemptnumber'];
+ if (!$userid) {
+ $cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
+ if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
+ $useridlist = $this->get_grading_userid_list();
+ $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
+ }
+ } else {
+ $useridlist = array($userid);
+ $rownum = 0;
+ $useridlistid = '';
}
$userid = $useridlist[$rownum];
- $grade = $this->get_user_grade($userid, false);
+ $grade = $this->get_user_grade($userid, false, $attemptnumber);
+
+ $submission = null;
+ if ($this->get_instance()->teamsubmission) {
+ $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
+ } else {
+ $submission = $this->get_user_submission($userid, false, $attemptnumber);
+ }
// Add advanced grading.
$gradingdisabled = $this->grading_disabled($userid);
- $gradinginstance = $this->get_grading_instance($userid, $gradingdisabled);
+ $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
+ $mform->addElement('header', 'gradeheader', get_string('grade'));
if ($gradinginstance) {
$gradingelement = $mform->addElement('grading',
'advancedgrading',
@@ -4518,12 +5000,13 @@ class assign {
}
$gradestring = $usergrade;
}
- $name = get_string('currentgrade', 'assign') . ':' . $gradestring;
- $mform->addElement('static', 'finalgrade', $name);
+ $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
- $strparams = array('index'=>$rownum+1, 'count'=>count($useridlist));
- $name = get_string('gradingstudentprogress', 'assign', $strparams);
- $mform->addElement('static', 'progress', '', $name);
+ if (count($useridlist) > 1) {
+ $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
+ $name = get_string('outof', 'assign', $strparams);
+ $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
+ }
// Let feedback plugins add elements to the grading form.
$this->add_plugin_grade_elements($grade, $mform, $data, $userid);
@@ -4536,14 +5019,45 @@ class assign {
$mform->setConstant('rownum', $rownum);
$mform->addElement('hidden', 'useridlistid', $useridlistid);
$mform->setType('useridlistid', PARAM_INT);
+ $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
+ $mform->setType('attemptnumber', PARAM_INT);
$mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
$mform->setType('ajax', PARAM_INT);
if ($this->get_instance()->teamsubmission) {
+ $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
$mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
$mform->setDefault('applytoall', 1);
}
+ // Do not show if we are editing a previous attempt.
+ if ($attemptnumber == -1 && $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
+ $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
+ $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
+ $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
+
+ $attemptnumber = 0;
+ if ($submission) {
+ $attemptnumber = $submission->attemptnumber;
+ }
+ $maxattempts = $this->get_instance()->maxattempts;
+ if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
+ $maxattempts = get_string('unlimitedattempts', 'assign');
+ }
+ $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
+ $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
+
+ $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
+ $issubmission = !empty($submission);
+ $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
+ $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
+
+ if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
+ $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
+ $mform->setDefault('addattempt', 0);
+ }
+ }
+
$mform->addElement('hidden', 'action', 'submitgrade');
$mform->setType('action', PARAM_ALPHA);
@@ -4571,6 +5085,8 @@ class assign {
if (!empty($buttonarray)) {
$mform->addGroup($buttonarray, 'navar', '', array(' '), false);
}
+ // The grading form does not work well with shortforms.
+ $mform->setDisableShortforms();
}
/**
@@ -4747,10 +5263,9 @@ class assign {
$userid = required_param('userid', PARAM_INT);
}
- $grade = $this->get_user_grade($userid, true);
- $grade->locked = 1;
- $grade->grader = $USER->id;
- $this->update_grade($grade);
+ $flags = $this->get_user_flags($userid, true);
+ $flags->locked = 1;
+ $this->update_user_flags($flags);
$user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
@@ -4777,10 +5292,9 @@ class assign {
$userid = required_param('userid', PARAM_INT);
}
- $grade = $this->get_user_grade($userid, true);
- $grade->locked = 0;
- $grade->grader = $USER->id;
- $this->update_grade($grade);
+ $flags = $this->get_user_flags($userid, true);
+ $flags->locked = 1;
+ $this->update_user_flags($flags);
$user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
@@ -4795,14 +5309,15 @@ class assign {
*
* @param stdClass $formdata - the data from the form
* @param int $userid - the user to apply the grade to
+ * @param int attemptnumber - The attempt number to apply the grade to.
* @return void
*/
- protected function apply_grade_to_user($formdata, $userid) {
+ protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
global $USER, $CFG, $DB;
- $grade = $this->get_user_grade($userid, true);
+ $grade = $this->get_user_grade($userid, true, $attemptnumber);
$gradingdisabled = $this->grading_disabled($userid);
- $gradinginstance = $this->get_grading_instance($userid, $gradingdisabled);
+ $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
if (!$gradingdisabled) {
if ($gradinginstance) {
$grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
@@ -4810,7 +5325,7 @@ class assign {
} else {
// Handle the case when grade is set to No Grade.
if (isset($formdata->grade)) {
- $grade->grade= grade_floatval(unformat_float($formdata->grade));
+ $grade->grade = grade_floatval(unformat_float($formdata->grade));
}
}
}
@@ -4902,13 +5417,22 @@ class assign {
require_capability('mod/assign:grade', $this->context);
require_sesskey();
+ $instance = $this->get_instance();
$rownum = required_param('rownum', PARAM_INT);
+ $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
$useridlistid = optional_param('useridlistid', time(), PARAM_INT);
+ $userid = optional_param('userid', 0, PARAM_INT);
$cache = cache::make_from_params(cache_store::MODE_SESSION, 'mod_assign', 'useridlist');
- if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
- $useridlist = $this->get_grading_userid_list();
- $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
+ if (!$userid) {
+ if (!$useridlist = $cache->get($this->get_course_module()->id . '_' . $useridlistid)) {
+ $useridlist = $this->get_grading_userid_list();
+ $cache->set($this->get_course_module()->id . '_' . $useridlistid, $useridlist);
+ }
+ } else {
+ $useridlist = array($userid);
+ $rownum = 0;
}
+
$last = false;
$userid = $useridlist[$rownum];
if ($rownum == count($useridlist) - 1) {
@@ -4917,7 +5441,11 @@ class assign {
$data = new stdClass();
- $gradeformparams = array('rownum'=>$rownum, 'useridlistid'=>$useridlistid, 'last'=>false);
+ $gradeformparams = array('rownum'=>$rownum,
+ 'useridlistid'=>$useridlistid,
+ 'last'=>false,
+ 'attemptnumber'=>$attemptnumber,
+ 'userid'=>optional_param('userid', 0, PARAM_INT));
$mform = new mod_assign_grade_form(null,
array($this, $data, $gradeformparams),
'post',
@@ -4925,7 +5453,13 @@ class assign {
array('class'=>'gradeform'));
if ($formdata = $mform->get_data()) {
- if ($this->get_instance()->teamsubmission && $formdata->applytoall) {
+ $submission = null;
+ if ($instance->teamsubmission) {
+ $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
+ } else {
+ $submission = $this->get_user_submission($userid, false, $attemptnumber);
+ }
+ if ($instance->teamsubmission && $formdata->applytoall) {
$groupid = 0;
if ($this->get_submission_group($userid)) {
$group = $this->get_submission_group($userid);
@@ -4936,13 +5470,48 @@ class assign {
$members = $this->get_submission_group_members($groupid, true);
foreach ($members as $member) {
// User may exist in multple groups (which should put them in the default group).
- $this->apply_grade_to_user($formdata, $member->id);
+ $this->apply_grade_to_user($formdata, $member->id, $attemptnumber);
$this->process_outcomes($member->id, $formdata);
}
} else {
- $this->apply_grade_to_user($formdata, $userid);
+ $this->apply_grade_to_user($formdata, $userid, $attemptnumber);
+
$this->process_outcomes($userid, $formdata);
}
+ $maxattemptsreached = !empty($submission) &&
+ $submission->attemptnumber >= ($instance->maxattempts - 1) &&
+ $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
+ $shouldreopen = false;
+ if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
+ // Check the gradetopass from the gradebook.
+ $gradinginfo = grade_get_grades($this->get_course()->id,
+ 'mod',
+ 'assign',
+ $instance->id,
+ $userid);
+
+ // What do we do if the grade has not been added to the gradebook (e.g. blind marking)?
+ $gradingitem = null;
+ $gradebookgrade = null;
+ if (isset($gradinginfo->items[0])) {
+ $gradingitem = $gradinginfo->items[0];
+ $gradebookgrade = $gradingitem->grades[$user->id];
+ }
+ if ($gradebookgrade && !$gradebookgrade->is_passed($gradingitem)) {
+ $shouldreopen = true;
+ }
+ }
+ if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
+ !empty($formdata->addattempt)) {
+ $shouldreopen = true;
+ }
+ // Never reopen if we are editing a previous attempt.
+ if ($attemptnumber != -1) {
+ $shouldreopen = false;
+ }
+ if ($shouldreopen && !$maxattemptsreached) {
+ $this->process_add_attempt($userid);
+ }
} else {
return false;
}
@@ -5032,6 +5601,75 @@ class assign {
return $count;
}
+ /**
+ * Add a new attempt for each user in the list - but reopen each group assignment
+ * at most 1 time.
+ *
+ * @param array $useridlist Array of userids to reopen.
+ * @return bool
+ */
+ protected function process_add_attempt_group($useridlist) {
+ $groupsprocessed = array();
+ $result = true;
+
+ foreach ($useridlist as $userid) {
+ $groupid = 0;
+ $group = $this->get_submission_group($userid);
+ if ($group) {
+ $groupid = $group->id;
+ }
+
+ if (empty($groupsprocessed[$groupid])) {
+ $result = $this->process_add_attempt($userid) && $result;
+ $groupsprocessed[$groupid] = true;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Add a new attempt for a user.
+ *
+ * @param int $userid int The user to add the attempt for
+ * @return bool - true if successful.
+ */
+ protected function process_add_attempt($userid) {
+ require_capability('mod/assign:grade', $this->context);
+ require_sesskey();
+
+ if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
+ return false;
+ }
+
+ if ($this->get_instance()->teamsubmission) {
+ $submission = $this->get_group_submission($userid, 0, false);
+ } else {
+ $submission = $this->get_user_submission($userid, false);
+ }
+
+ if (!$submission) {
+ return false;
+ }
+
+ // No more than max attempts allowed.
+ if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
+ $submission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
+ return false;
+ }
+
+ // Create the new submission record for the group/user.
+ if ($this->get_instance()->teamsubmission) {
+ $submission = $this->get_group_submission($userid, 0, true, $submission->attemptnumber+1);
+ } else {
+ $submission = $this->get_user_submission($userid, true, $submission->attemptnumber+1);
+ }
+
+ // Set the status of the new attempt to reopened.
+ $submission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
+ $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
+ return true;
+ }
+
/**
* Get an upto date list of user grades and feedback for the gradebook.
*
@@ -5063,11 +5701,24 @@ class assign {
}
}
if ($userid) {
- $where = ' WHERE u.id = ? ';
+ $where = ' WHERE u.id = :userid ';
} else {
- $where = ' WHERE u.id != ? ';
+ $where = ' WHERE u.id != :userid ';
}
+ $submissionmaxattempt = 'SELECT mxs.userid, MAX(mxs.attemptnumber) AS maxattempt
+ FROM {assign_submission} mxs
+ WHERE mxs.assignment = :assignid1 GROUP BY mxs.userid';
+ $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
+ FROM {assign_grades} mxg
+ WHERE mxg.assignment = :assignid2 GROUP BY mxg.userid';
+
+ // When the gradebook asks us for grades - only return the last attempt for each user.
+ $params = array('assignid1'=>$assignmentid,
+ 'assignid2'=>$assignmentid,
+ 'assignid3'=>$assignmentid,
+ 'assignid4'=>$assignmentid,
+ 'userid'=>$userid);
$graderesults = $DB->get_recordset_sql('SELECT
u.id as userid,
s.timemodified as datesubmitted,
@@ -5075,11 +5726,15 @@ class assign {
g.timemodified as dategraded,
g.grader as usermodified
FROM {user} u
+ LEFT JOIN ( ' . $submissionmaxattempt . ' ) smx ON u.id = smx.userid
+ LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
LEFT JOIN {assign_submission} s
- ON u.id = s.userid and s.assignment = ?
+ ON u.id = s.userid and s.assignment = :assignid3 AND
+ s.attemptnumber = smx.maxattempt
JOIN {assign_grades} g
- ON u.id = g.userid and g.assignment = ?' .
- $where, array($assignmentid, $assignmentid, $userid));
+ ON u.id = g.userid and g.assignment = :assignid4 AND
+ g.attemptnumber = gmx.maxattempt' .
+ $where, $params);
foreach ($graderesults as $result) {
$gradebookgrade = clone $result;
diff --git a/mod/assign/mod_form.php b/mod/assign/mod_form.php
index 998d2e77d38..6d232f2f889 100644
--- a/mod/assign/mod_form.php
+++ b/mod/assign/mod_form.php
@@ -118,6 +118,22 @@ class mod_assign_mod_form extends moodleform_mod {
$mform->addElement('hidden', 'requiresubmissionstatement', 1);
}
+ $options = array(
+ ASSIGN_ATTEMPT_REOPEN_METHOD_NONE => get_string('attemptreopenmethod_none', 'mod_assign'),
+ ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL => get_string('attemptreopenmethod_manual', 'mod_assign'),
+ ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS => get_string('attemptreopenmethod_untilpass', 'mod_assign')
+ );
+ $mform->addElement('select', 'attemptreopenmethod', get_string('attemptreopenmethod', 'mod_assign'), $options);
+ $mform->setDefault('attemptreopenmethod', ASSIGN_ATTEMPT_REOPEN_METHOD_NONE);
+ $mform->addHelpButton('attemptreopenmethod', 'attemptreopenmethod', 'mod_assign');
+
+ $options = array(ASSIGN_UNLIMITED_ATTEMPTS => get_string('unlimitedattempts', 'mod_assign'));
+ $options += array_combine(range(1, 30), range(1, 30));
+ $mform->addElement('select', 'maxattempts', get_string('maxattempts', 'mod_assign'), $options);
+ $mform->addHelpButton('maxattempts', 'maxattempts', 'assign');
+ $mform->setDefault('maxattempts', -1);
+ $mform->disabledIf('maxattempts', 'attemptreopenmethod', 'eq', ASSIGN_ATTEMPT_REOPEN_METHOD_NONE);
+
$mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
$name = get_string('teamsubmission', 'assign');
diff --git a/mod/assign/module.js b/mod/assign/module.js
index 4f9f8c03f17..90a789c7526 100644
--- a/mod/assign/module.js
+++ b/mod/assign/module.js
@@ -5,7 +5,7 @@ M.mod_assign.init_tree = function(Y, expand_all, htmlid) {
var tree = new Y.YUI2.widget.TreeView(htmlid);
tree.subscribe("clickEvent", function(node, event) {
- // we want normal clicking which redirects to url
+ // We want normal clicking which redirects to url.
return false;
});
@@ -169,12 +169,12 @@ M.mod_assign.init_plugin_summary = function(Y, subtype, type, submissionid) {
fullclassname = 'full_' + thissuffix;
full = Y.one('.' + fullclassname);
if (full) {
- full.hide(true);
+ full.hide(false);
}
summaryclassname = 'summary_' + thissuffix;
summary = Y.one('.' + summaryclassname);
if (summary) {
- summary.show(true);
+ summary.show(false);
}
});
}
@@ -183,7 +183,7 @@ M.mod_assign.init_plugin_summary = function(Y, subtype, type, submissionid) {
full = Y.one('.full_' + suffix);
if (full) {
- full.hide();
+ full.hide(false);
full.toggleClass('hidefull');
}
if (expand) {
@@ -199,12 +199,12 @@ M.mod_assign.init_plugin_summary = function(Y, subtype, type, submissionid) {
summaryclassname = 'summary_' + thissuffix;
summary = Y.one('.' + summaryclassname);
if (summary) {
- summary.hide(true);
+ summary.hide(false);
}
fullclassname = 'full_' + thissuffix;
full = Y.one('.' + fullclassname);
if (full) {
- full.show(true);
+ full.show(false);
}
});
}
diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php
index c62a4cbc687..2056261f380 100644
--- a/mod/assign/renderable.php
+++ b/mod/assign/renderable.php
@@ -52,12 +52,14 @@ class assign_submit_for_grading_page implements renderable {
}
/**
- * Implements a renderable grading error notification
+ * Implements a renderable message notification
* @package mod_assign
* @copyright 2012 NetSpot {@link http://www.netspot.com.au}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class assign_quickgrading_result implements renderable {
+class assign_gradingmessage implements renderable {
+ /** @var string $heading is the heading to display to the user */
+ public $heading = '';
/** @var string $message is the message to display to the user */
public $message = '';
/** @var int $coursemoduleid */
@@ -65,9 +67,11 @@ class assign_quickgrading_result implements renderable {
/**
* Constructor
+ * @param string $heading This is the heading to display
* @param string $message This is the message to display
*/
- public function __construct($message, $coursemoduleid) {
+ public function __construct($heading, $message, $coursemoduleid) {
+ $this->heading = $heading;
$this->message = $message;
$this->coursemoduleid = $coursemoduleid;
}
@@ -363,6 +367,10 @@ class assign_submission_status implements renderable {
public $blindmarking = false;
/** @var string gradingcontrollerpreview */
public $gradingcontrollerpreview = '';
+ /** @var string attemptreopenmethod */
+ public $attemptreopenmethod = 'none';
+ /** @var int maxattempts */
+ public $maxattempts = -1;
/**
* Constructor
@@ -390,7 +398,9 @@ class assign_submission_status implements renderable {
* @param bool $canviewfullnames
* @param int $extensionduedate - Any extension to the due date granted for this user
* @param context $context - Any extension to the due date granted for this user
- * @param blindmarking $blindmarking - Should we hide student identities from graders?
+ * @param bool $blindmarking - Should we hide student identities from graders?
+ * @param string $attemptreopenmethod - The method of reopening student attempts.
+ * @param int $maxattempts - How many attempts can a student make?
*/
public function __construct($allowsubmissionsfromdate,
$alwaysshowdescription,
@@ -416,7 +426,9 @@ class assign_submission_status implements renderable {
$extensionduedate,
$context,
$blindmarking,
- $gradingcontrollerpreview) {
+ $gradingcontrollerpreview,
+ $attemptreopenmethod,
+ $maxattempts) {
$this->allowsubmissionsfromdate = $allowsubmissionsfromdate;
$this->alwaysshowdescription = $alwaysshowdescription;
$this->submission = $submission;
@@ -442,8 +454,66 @@ class assign_submission_status implements renderable {
$this->context = $context;
$this->blindmarking = $blindmarking;
$this->gradingcontrollerpreview = $gradingcontrollerpreview;
+ $this->attemptreopenmethod = $attemptreopenmethod;
+ $this->maxattempts = $maxattempts;
}
+}
+/**
+ * Used to output the attempt history for a particular assignment.
+ *
+ * @package mod_assign
+ * @copyright 2012 Davo Smith, Synergy Learning
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class assign_attempt_history implements renderable {
+
+ /** @var array submissions */
+ public $submissions = array();
+ /** @var array grades */
+ public $grades = array();
+ /** @var array submissionplugins */
+ public $submissionplugins = array();
+ /** @var array feedbackplugins */
+ public $feedbackplugins = array();
+ /** @var int coursemoduleid */
+ public $coursemoduleid = 0;
+ /** @var string returnaction */
+ public $returnaction = '';
+ /** @var string returnparams */
+ public $returnparams = array();
+ /** @var bool cangrade */
+ public $cangrade = false;
+
+ /**
+ * Constructor
+ *
+ * @param $submissions
+ * @param $grades
+ * @param $submissionplugins
+ * @param $feedbackplugins
+ * @param $coursemoduleid
+ * @param $returnaction
+ * @param $returnparams
+ * @param $cangrade
+ */
+ public function __construct($submissions,
+ $grades,
+ $submissionplugins,
+ $feedbackplugins,
+ $coursemoduleid,
+ $returnaction,
+ $returnparams,
+ $cangrade) {
+ $this->submissions = $submissions;
+ $this->grades = $grades;
+ $this->submissionplugins = $submissionplugins;
+ $this->feedbackplugins = $feedbackplugins;
+ $this->coursemoduleid = $coursemoduleid;
+ $this->returnaction = $returnaction;
+ $this->returnparams = $returnparams;
+ $this->cangrade = $cangrade;
+ }
}
/**
diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php
index 7fd5c42cf78..01bd7a7338c 100644
--- a/mod/assign/renderer.php
+++ b/mod/assign/renderer.php
@@ -85,16 +85,16 @@ class mod_assign_renderer extends plugin_renderer_base {
}
/**
- * Render a grading error notification
- * @param assign_quickgrading_result $result The result to render
+ * Render a grading message notification
+ * @param assign_gradingmessage $result The result to render
* @return string
*/
- public function render_assign_quickgrading_result(assign_quickgrading_result $result) {
+ public function render_assign_gradingmessage(assign_gradingmessage $result) {
$urlparams = array('id' => $result->coursemoduleid, 'action'=>'grading');
$url = new moodle_url('/mod/assign/view.php', $urlparams);
$o = '';
- $o .= $this->output->heading(get_string('quickgradingresult', 'assign'), 4);
+ $o .= $this->output->heading($result->heading, 4);
$o .= $this->output->notification($result->message);
$o .= $this->output->continue_button($url);
return $o;
@@ -179,7 +179,9 @@ class mod_assign_renderer extends plugin_renderer_base {
$o .= $this->output->continue_button($cancelurl);
} else {
// All submission plugins ready - show the confirmation form.
+ $o .= $this->output->box_start('generalbox submitconfirm');
$o .= $this->moodleform($page->confirmform);
+ $o .= $this->output->box_end();
}
$o .= $this->output->container_end();
@@ -426,6 +428,32 @@ class mod_assign_renderer extends plugin_renderer_base {
$t->data[] = $row;
}
+ if ($status->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
+ $currentattempt = 1;
+ if (!$status->teamsubmissionenabled) {
+ if ($status->submission) {
+ $currentattempt = $status->submission->attemptnumber + 1;
+ }
+ } else {
+ if ($status->teamsubmission) {
+ $currentattempt = $status->teamsubmission->attemptnumber + 1;
+ }
+ }
+
+ $row = new html_table_row();
+ $cell1 = new html_table_cell(get_string('attemptnumber', 'assign'));
+ $maxattempts = $status->maxattempts;
+ if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
+ $message = get_string('currentattempt', 'assign', $currentattempt);
+ } else {
+ $message = get_string('currentattemptof', 'assign', array('attemptnumber'=>$currentattempt,
+ 'maxattempts'=>$maxattempts));
+ }
+ $cell2 = new html_table_cell($message);
+ $row->cells = array($cell1, $cell2);
+ $t->data[] = $row;
+ }
+
$row = new html_table_row();
$cell1 = new html_table_cell(get_string('submissionstatus', 'assign'));
if (!$status->teamsubmissionenabled) {
@@ -437,7 +465,7 @@ class mod_assign_renderer extends plugin_renderer_base {
if (!$status->submissionsenabled) {
$cell2 = new html_table_cell(get_string('noonlinesubmissions', 'assign'));
} else {
- $cell2 = new html_table_cell(get_string('nosubmission', 'assign'));
+ $cell2 = new html_table_cell(get_string('noattempt', 'assign'));
}
}
$row->cells = array($cell1, $cell2);
@@ -637,23 +665,52 @@ class mod_assign_renderer extends plugin_renderer_base {
if ($status->view == assign_submission_status::STUDENT_VIEW) {
if ($status->canedit) {
if (!$submission) {
+ $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');
+ $o .= $this->output->box_start('boxaligncenter submithelp');
+ $o .= get_string('editsubmission_help', 'assign');
+ $o .= $this->output->box_end();
+ $o .= $this->output->box_end();
+ } else if ($submission->status == ASSIGN_SUBMISSION_STATUS_REOPENED) {
+ $o .= $this->output->box_start('generalbox submissionaction');
+ $urlparams = array('id' => $status->coursemoduleid, 'action' => 'editprevioussubmission');
+ $o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams),
+ get_string('addnewattemptfromprevious', 'assign'), 'get');
+ $o .= $this->output->box_start('boxaligncenter submithelp');
+ $o .= get_string('addnewattemptfromprevious_help', 'assign');
+ $o .= $this->output->box_end();
+ $o .= $this->output->box_end();
+ $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('addnewattempt', 'assign'), 'get');
+ $o .= $this->output->box_start('boxaligncenter submithelp');
+ $o .= get_string('addnewattempt_help', 'assign');
+ $o .= $this->output->box_end();
+ $o .= $this->output->box_end();
} else {
+ $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('editsubmission', 'assign'), 'get');
+ $o .= $this->output->box_start('boxaligncenter submithelp');
+ $o .= get_string('editsubmission_help', 'assign');
+ $o .= $this->output->box_end();
+ $o .= $this->output->box_end();
}
}
if ($status->cansubmit) {
$urlparams = array('id' => $status->coursemoduleid, 'action'=>'submit');
+ $o .= $this->output->box_start('generalbox submissionaction');
$o .= $this->output->single_button(new moodle_url('/mod/assign/view.php', $urlparams),
get_string('submitassignment', 'assign'), 'get');
$o .= $this->output->box_start('boxaligncenter submithelp');
$o .= get_string('submitassignment_help', 'assign');
$o .= $this->output->box_end();
+ $o .= $this->output->box_end();
}
}
@@ -661,6 +718,154 @@ class mod_assign_renderer extends plugin_renderer_base {
return $o;
}
+ /**
+ * Output the attempt history for this assignment
+ *
+ * @param assign_attempt_history $history
+ * @return string
+ */
+ public function render_assign_attempt_history(assign_attempt_history $history) {
+ $o = '';
+
+ $submittedstr = get_string('submitted', 'assign');
+ $gradestr = get_string('grade');
+ $gradedonstr = get_string('gradedon', 'assign');
+ $gradedbystr = get_string('gradedby', 'assign');
+
+ // Don't show the last one because it is the current submission.
+ array_pop($history->submissions);
+
+ // Show newest to oldest.
+ $history->submissions = array_reverse($history->submissions);
+
+ if (empty($history->submissions)) {
+ return '';
+ }
+
+ $containerid = 'attempthistory' . uniqid();
+ $o .= $this->heading(get_string('attempthistory', 'assign'), 3);
+ $o .= $this->box_start('attempthistory', $containerid);
+
+ foreach ($history->submissions as $i => $submission) {
+ $grade = null;
+ foreach ($history->grades as $onegrade) {
+ if ($onegrade->attemptnumber == $submission->attemptnumber) {
+ $grade = $onegrade;
+ break;
+ }
+ }
+
+ $editbtn = '';
+
+ if ($submission) {
+ $submissionsummary = userdate($submission->timemodified);
+ } else {
+ $submissionsummary = get_string('nosubmission', 'assign');
+ }
+
+ $attemptsummaryparams = array('attemptnumber'=>$submission->attemptnumber+1,
+ 'submissionsummary'=>$submissionsummary);
+ $o .= $this->heading(get_string('attemptheading', 'assign', $attemptsummaryparams), 4);
+
+ $t = new html_table();
+
+ if ($submission) {
+ $cell1 = new html_table_cell(get_string('submissionstatus', 'assign'));
+ $cell2 = new html_table_cell(get_string('submissionstatus_' . $submission->status, 'assign'));
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+
+ foreach ($history->submissionplugins as $plugin) {
+ $pluginshowsummary = !$plugin->is_empty($submission) || !$plugin->allow_submissions();
+ if ($plugin->is_enabled() &&
+ $plugin->is_visible() &&
+ $plugin->has_user_summary() &&
+ $pluginshowsummary) {
+
+ $cell1 = new html_table_cell($plugin->get_name());
+ $pluginsubmission = new assign_submission_plugin_submission($plugin,
+ $submission,
+ assign_submission_plugin_submission::SUMMARY,
+ $history->coursemoduleid,
+ $history->returnaction,
+ $history->returnparams);
+ $cell2 = new html_table_cell($this->render($pluginsubmission));
+
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+ }
+ }
+ }
+
+ if ($grade) {
+ // Heading 'feedback'.
+ $title = get_string('feedback', 'assign', $i);
+ $title .= $this->output->spacer(array('width'=>10));
+ if ($history->cangrade) {
+ // Edit previous feedback.
+ $returnparams = http_build_query($history->returnparams);
+ $urlparams = array('id' => $history->coursemoduleid,
+ 'userid'=>$grade->userid,
+ 'attemptnumber'=>$grade->attemptnumber,
+ 'action'=>'grade',
+ 'rownum'=>0,
+ 'returnaction'=>$history->returnaction,
+ 'returnparams'=>$returnparams);
+ $url = new moodle_url('/mod/assign/view.php', $urlparams);
+ $icon = new pix_icon('gradefeedback',
+ get_string('editattemptfeedback', 'assign', $grade->attemptnumber+1),
+ 'mod_assign');
+ $title .= $this->output->action_icon($url, $icon);
+ }
+ $cell = new html_table_cell($title);
+ $cell->attributes['class'] = 'feedbacktitle';
+ $cell->colspan = 2;
+ $t->data[] = new html_table_row(array($cell));
+
+ // Grade.
+ $cell1 = new html_table_cell($gradestr);
+ $cell2 = $grade->gradefordisplay;
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+
+ // Graded on.
+ $cell1 = new html_table_cell($gradedonstr);
+ $cell2 = new html_table_cell(userdate($grade->timemodified));
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+
+ // Graded by.
+ $cell1 = new html_table_cell($gradedbystr);
+ $cell2 = new html_table_cell($this->output->user_picture($grade->grader) .
+ $this->output->spacer(array('width'=>30)) . fullname($grade->grader));
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+
+ // Feedback from plugins.
+ foreach ($history->feedbackplugins as $plugin) {
+ if ($plugin->is_enabled() &&
+ $plugin->is_visible() &&
+ $plugin->has_user_summary() &&
+ !$plugin->is_empty($grade)) {
+
+ $cell1 = new html_table_cell($plugin->get_name());
+ $pluginfeedback = new assign_feedback_plugin_feedback(
+ $plugin, $grade, assign_feedback_plugin_feedback::SUMMARY, $history->coursemoduleid,
+ $history->returnaction, $history->returnparams
+ );
+ $cell2 = new html_table_cell($this->render($pluginfeedback));
+ $t->data[] = new html_table_row(array($cell1, $cell2));
+ }
+
+ }
+
+ }
+
+ $o .= html_writer::table($t);
+ }
+ $o .= $this->box_end();
+ $jsparams = array($containerid);
+
+ $this->page->requires->yui_module('moodle-mod_assign-history', 'Y.one("#' . $containerid . '").history');
+
+ return $o;
+ }
+
/**
* Render a submission plugin submission
*
@@ -751,6 +956,7 @@ class mod_assign_renderer extends plugin_renderer_base {
$this->page->requires->string_for_js('batchoperationconfirmlock', 'assign');
$this->page->requires->string_for_js('batchoperationconfirmreverttodraft', 'assign');
$this->page->requires->string_for_js('batchoperationconfirmunlock', 'assign');
+ $this->page->requires->string_for_js('batchoperationconfirmaddattempt', 'assign');
$this->page->requires->string_for_js('editaction', 'assign');
foreach ($table->plugingradingbatchoperations as $plugin => $operations) {
foreach ($operations as $operation => $description) {
diff --git a/mod/assign/styles.css b/mod/assign/styles.css
index 16bca8b1bd4..fd11bd988cb 100644
--- a/mod/assign/styles.css
+++ b/mod/assign/styles.css
@@ -17,6 +17,8 @@ div.gradingsummary {
div.submissionstatus .generaltable,
div.submissionlinks .generaltable,
div.feedback .generaltable,
+div.submissionsummarytable .generaltable,
+div.attempthistory table,
div.gradingsummary .generaltable {
width: 100%;
}
@@ -30,7 +32,6 @@ div.gradingsummary .generaltable {
margin-top: 1em;
}
-
div.submissionsummarytable table tbody tr td.c0 {
width: 30%;
}
@@ -73,6 +74,12 @@ div.submissionlocked {
background-color: #efefcf;
}
+td.submissionreopened,
+div.submissionreopened {
+ color: black;
+ background-color: #efefef;
+}
+
td.submissiongraded,
div.submissiongraded {
color: black;
@@ -160,3 +167,84 @@ td.submissioneditable {
.jsenabled .quickgradingform form .commentscontainer textarea {
display: inline;
}
+
+#page-mod-assign-view .previousfeedbackwarning {
+ font-size: 140%;
+ font-weight: bold;
+ text-align: center;
+ color: #500;
+}
+
+#page-mod-assign-view .submissionhistory {
+ background-color: #b0b0b0;
+}
+
+#page-mod-assign-view .submissionhistory .cell.historytitle {
+ background-color: #808080;
+}
+
+#page-mod-assign-view .submissionhistory .cell {
+ background-color: #d0d0d0;
+}
+
+#page-mod-assign-view .submissionhistory .singlebutton {
+ display: inline-block;
+ float: right;
+}
+
+#page-mod-assign-view.dir-rtl .submissionhistory .singlebutton {
+ float: left;
+}
+
+#page-mod-assign-view .submissionsummarytable .singlebutton {
+ display: inline-block;
+}
+
+.jsenabled .mod-assign-history-link {
+ display: block;
+ cursor: pointer;
+ margin-bottom: 7px;
+}
+
+.jsenabled .mod-assign-history-link h4 {
+ display: inline;
+}
+
+#page-mod-assign-view.jsenabled .attempthistory h4 {
+ margin-bottom: 7px;
+ text-align: left;
+}
+
+#page-mod-assign-view.jsenabled.dir_rtl .attempthistory h4 {
+ text-align: right;
+}
+
+.dir-rtl.jsenabled .mod-assign-history-link h4 {
+ text-align: right;
+}
+
+.jsenabled .mod-assign-history-link-open {
+ padding: 0 5px 0 20px; background: url([[pix:t/expanded]]) 2px center no-repeat;
+}
+
+.jsenabled .mod-assign-history-link-closed {
+ padding: 0 5px 0 20px; background: url([[pix:t/collapsed]]) 2px center no-repeat;
+}
+
+.dir-rtl.jsenabled .mod-assign-history-link-closed {
+ padding: 0 20px 0 5px; background: url([[pix:t/collapsed_rtl]]) 2px center no-repeat;
+}
+
+#page-mod-assign-view .submithelp {
+ padding: 1em;
+}
+
+#page-mod-assign-view .feedbacktitle {
+ font-weight: bold;
+}
+
+#page-mod-assign-view .submitconfirm,
+#page-mod-assign-view .submissionlinks,
+#page-mod-assign-view .submissionaction {
+ text-align: center;
+}
diff --git a/mod/assign/submission/file/locallib.php b/mod/assign/submission/file/locallib.php
index bd089245806..81a04e96596 100644
--- a/mod/assign/submission/file/locallib.php
+++ b/mod/assign/submission/file/locallib.php
@@ -449,4 +449,35 @@ class assign_submission_file extends assign_submission_plugin {
return array(ASSIGNSUBMISSION_FILE_FILEAREA=>$this->get_name());
}
+ /**
+ * Copy the student's submission from a previous submission. Used when a student opts to base their resubmission
+ * on the last submission.
+ * @param stdClass $sourcesubmission
+ * @param stdClass $destsubmission
+ */
+ public function copy_submission(stdClass $sourcesubmission, stdClass $destsubmission) {
+ global $DB;
+
+ // Copy the files across.
+ $contextid = $this->assignment->get_context()->id;
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($contextid,
+ 'assignsubmission_file',
+ ASSIGNSUBMISSION_FILE_FILEAREA,
+ $sourcesubmission->id,
+ 'id',
+ false);
+ foreach ($files as $file) {
+ $fieldupdates = array('itemid' => $destsubmission->id);
+ $fs->create_file_from_storedfile($fieldupdates, $file);
+ }
+
+ // Copy the assignsubmission_file record.
+ if ($filesubmission = $this->get_file_submission($sourcesubmission->id)) {
+ unset($filesubmission->id);
+ $filesubmission->submission = $destsubmission->id;
+ $DB->insert_record('assignsubmission_file', $filesubmission);
+ }
+ return true;
+ }
}
diff --git a/mod/assign/submission/onlinetext/locallib.php b/mod/assign/submission/onlinetext/locallib.php
index 57e51ab1d8f..e90c8a8ce12 100644
--- a/mod/assign/submission/onlinetext/locallib.php
+++ b/mod/assign/submission/onlinetext/locallib.php
@@ -470,6 +470,34 @@ class assign_submission_onlinetext extends assign_submission_plugin {
return array(ASSIGNSUBMISSION_ONLINETEXT_FILEAREA=>$this->get_name());
}
+ /**
+ * Copy the student's submission from a previous submission. Used when a student opts to base their resubmission
+ * on the last submission.
+ * @param stdClass $sourcesubmission
+ * @param stdClass $destsubmission
+ */
+ public function copy_submission(stdClass $sourcesubmission, stdClass $destsubmission) {
+ global $DB;
+
+ // Copy the files across (attached via the text editor).
+ $contextid = $this->assignment->get_context()->id;
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($contextid, 'assignsubmission_onlinetext',
+ ASSIGNSUBMISSION_ONLINETEXT_FILEAREA, $sourcesubmission->id, 'id', false);
+ foreach ($files as $file) {
+ $fieldupdates = array('itemid' => $destsubmission->id);
+ $fs->create_file_from_storedfile($fieldupdates, $file);
+ }
+
+ // Copy the assignsubmission_onlinetext record.
+ $onlinetextsubmission = $this->get_onlinetext_submission($sourcesubmission->id);
+ if ($onlinetextsubmission) {
+ unset($onlinetextsubmission->id);
+ $onlinetextsubmission->submission = $destsubmission->id;
+ $DB->insert_record('assignsubmission_onlinetext', $onlinetextsubmission);
+ }
+ return true;
+ }
}
diff --git a/mod/assign/submissionplugin.php b/mod/assign/submissionplugin.php
index 6f0d2512d45..68a3da32f22 100644
--- a/mod/assign/submissionplugin.php
+++ b/mod/assign/submissionplugin.php
@@ -60,18 +60,28 @@ abstract class assign_submission_plugin extends assign_plugin {
/**
* Check if the submission plugin has all the required data to allow the work
* to be submitted for grading
+ * @param stdClass $submission the assign_submission record being submitted.
* @return bool|string 'true' if OK to proceed with submission, otherwise a
* a message to display to the user
*/
- public function precheck_submission() {
+ public function precheck_submission($submission) {
return true;
}
/**
* Carry out any extra processing required when the work is submitted for grading
+ * @param stdClass $submission the assign_submission record being submitted.
* @return void
*/
- public function submit_for_grading() {
+ public function submit_for_grading($submission) {
}
+ /**
+ * Copy the plugin specific submission data to a new submission record.
+ *
+ * @return bool
+ */
+ public function copy_submission( stdClass $oldsubmission, stdClass $submission) {
+ return true;
+ }
}
diff --git a/mod/assign/tests/base_test.php b/mod/assign/tests/base_test.php
index e349a2e9b0d..4e6c680f112 100644
--- a/mod/assign/tests/base_test.php
+++ b/mod/assign/tests/base_test.php
@@ -134,7 +134,7 @@ class mod_assign_base_testcase extends advanced_testcase {
/*
* For tests that make sense to use alot of data, create extra students/teachers.
*/
- protected function createExtraUsers() {
+ protected function create_extra_users() {
global $DB;
$this->extrateachers = array();
for ($i = 0; $i < self::EXTRA_TEACHER_COUNT; $i++) {
@@ -217,8 +217,8 @@ class testable_assign extends assign {
return parent::delete_grades();
}
- public function testable_apply_grade_to_user($formdata, $userid) {
- return parent::apply_grade_to_user($formdata, $userid);
+ public function testable_apply_grade_to_user($formdata, $userid, $attemptnumber) {
+ return parent::apply_grade_to_user($formdata, $userid, $attemptnumber);
}
public function testable_get_grading_userid_list() {
@@ -233,6 +233,10 @@ class testable_assign extends assign {
return parent::update_submission($submission, $userid, $updatetime, $teamsubmission);
}
+ public function testable_process_add_attempt($userid = 0) {
+ return parent::process_add_attempt($userid);
+ }
+
public function testable_submissions_open($userid = 0) {
return parent::submissions_open($userid);
}
diff --git a/mod/assign/tests/generator/lib.php b/mod/assign/tests/generator/lib.php
index 065e062fe8e..6c225819bfa 100644
--- a/mod/assign/tests/generator/lib.php
+++ b/mod/assign/tests/generator/lib.php
@@ -63,7 +63,9 @@ class mod_assign_generator extends testing_module_generator {
'requireallteammemberssubmit' => 0,
'teamsubmissiongroupingid' => 0,
'blindmarking' => 0,
- 'cmidnumber' => ''
+ 'cmidnumber' => '',
+ 'attemptreopenmethod' => 'none',
+ 'maxattempts' => -1
);
foreach ($defaultsettings as $name => $value) {
diff --git a/mod/assign/tests/lib_test.php b/mod/assign/tests/lib_test.php
index 93bcd3d2320..420a22f17ea 100644
--- a/mod/assign/tests/lib_test.php
+++ b/mod/assign/tests/lib_test.php
@@ -137,4 +137,3 @@ class mod_assign_lib_testcase extends mod_assign_base_testcase {
}
}
-
diff --git a/mod/assign/tests/locallib_test.php b/mod/assign/tests/locallib_test.php
index 400a432632c..c13d76c0807 100644
--- a/mod/assign/tests/locallib_test.php
+++ b/mod/assign/tests/locallib_test.php
@@ -94,7 +94,6 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$assign->testable_process_reveal_identities();
// Test sesskey is required.
- $nosesskey = true;
$this->setUser($this->editingteachers[0]);
$this->setExpectedException('moodle_exception');
$assign->testable_process_reveal_identities();
@@ -165,7 +164,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
// Now see if the data is in the gradebook.
$gradinginfo = grade_get_grades($this->course->id,
@@ -192,7 +191,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
// Simulate a submission.
$this->setUser($this->students[0]);
@@ -220,7 +219,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
// Simulate a submission.
$this->setUser($this->students[0]);
@@ -286,11 +285,6 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$instance->duedate = $now;
$instance->instance = $instance->id;
$instance->assignsubmission_onlinetext_enabled = 1;
- $instance->assignsubmission_file_enabled = 0;
- $instance->assignsubmission_comments_enabled = 0;
- $instance->assignfeedback_comments_enabled = 0;
- $instance->assignfeedback_file_enabled = 0;
- $instance->assignfeedback_offline_enabled = 0;
$assign->update_instance($instance);
@@ -299,7 +293,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
}
public function test_list_participants() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance(array('grade'=>100));
@@ -307,7 +301,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
}
public function test_count_teams() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance(array('teamsubmission'=>1));
@@ -315,7 +309,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
}
public function test_count_submissions() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance(array('assignsubmission_onlinetext_enabled'=>1));
@@ -334,7 +328,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id, 0);
// Simulate a submission.
$this->setUser($this->extrastudents[1]);
@@ -376,7 +370,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id);
+ $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
$this->assertEquals(2, $assign->count_grades());
$this->assertEquals(4, $assign->count_submissions());
@@ -386,7 +380,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
}
public function test_get_grading_userid_list() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance();
@@ -407,7 +401,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
// Now run cron and see that one message was sent.
$this->preventResetByRollback();
@@ -430,7 +424,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
$this->assertEquals(true, $assign->testable_is_graded($this->students[0]->id));
$this->assertEquals(false, $assign->testable_is_graded($this->students[1]->id));
@@ -456,7 +450,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
public function test_update_submission() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance();
@@ -564,7 +558,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
}
public function test_get_graders() {
- $this->createExtraUsers();
+ $this->create_extra_users();
$this->setUser($this->editingteachers[0]);
$assign = $this->create_instance();
@@ -614,7 +608,7 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->setUser($this->teachers[0]);
$data = new stdClass();
$data->grade = '50.0';
- $assign->testable_apply_grade_to_user($data, $this->students[0]->id);
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
// Now we should see the feedback.
$this->setUser($this->students[0]);
@@ -656,6 +650,98 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
$this->assertEquals(false, strpos($output, 'Graded on'), 'Do not show graded date when there is no grade.');
}
+ public function test_attempt_reopen_method_manual() {
+ global $PAGE;
+
+ $this->setUser($this->editingteachers[0]);
+ $assign = $this->create_instance(array('attemptreopenmethod'=>ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
+ 'maxattempts'=>3,
+ 'submissiondrafts'=>1,
+ 'assignsubmission_onlinetext_enabled'=>1));
+ $PAGE->set_url(new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id)));
+
+ // Student should be able to see an add submission button.
+ $this->setUser($this->students[0]);
+ $output = $assign->view_student_summary($this->students[0], true);
+ $this->assertNotEquals(false, strpos($output, get_string('addsubmission', 'assign')));
+
+ // Add a submission.
+ $now = time();
+ $submission = $assign->get_user_submission($this->students[0]->id, true);
+ $data = new stdClass();
+ $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
+ 'text'=>'Submission text',
+ 'format'=>FORMAT_MOODLE);
+ $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+ $plugin->save($submission, $data);
+
+ // And now submit it for marking.
+ $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+ $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
+
+ // Verify the student cannot make changes to the submission.
+ $output = $assign->view_student_summary($this->students[0], true);
+ $this->assertEquals(false, strpos($output, get_string('addsubmission', 'assign')));
+
+ // Mark the submission.
+ $this->setUser($this->teachers[0]);
+ $data = new stdClass();
+ $data->grade = '50.0';
+ $assign->testable_apply_grade_to_user($data, $this->students[0]->id, 0);
+
+ // Check the student can see the grade.
+ $this->setUser($this->students[0]);
+ $output = $assign->view_student_summary($this->students[0], true);
+ $this->assertNotEquals(false, strpos($output, '50.0'));
+
+ // Allow the student another attempt.
+ $this->teachers[0]->ignoresesskey = true;
+ $this->setUser($this->teachers[0]);
+ $result = $assign->testable_process_add_attempt($this->students[0]->id);
+ $this->assertEquals(true, $result);
+
+ // Check that the previous attempt is now in the submission history table.
+ $this->setUser($this->students[0]);
+ $output = $assign->view_student_summary($this->students[0], true);
+ // Need a better check.
+ $this->assertNotEquals(false, strpos($output, 'Submission text'), 'Contains: Submission text');
+
+ // Check that the student now has a button for Add a new attempt".
+ $this->assertNotEquals(false, strpos($output, get_string('addnewattempt', 'assign')));
+ // Check that the student now does not have a button for Submit.
+ $this->assertEquals(false, strpos($output, get_string('submitassignment', 'assign')));
+
+ // Check that the student now has a submission history.
+ $this->assertNotEquals(false, strpos($output, get_string('attempthistory', 'assign')));
+
+ $this->setUser($this->teachers[0]);
+ // Check that the grading table loads correctly and contains this user.
+ // This is also testing that we do not get duplicate rows in the grading table.
+ $gradingtable = new assign_grading_table($assign, 100, '', 0, true);
+ $output = $assign->get_renderer()->render($gradingtable);
+ $this->assertEquals(true, strpos($output, $this->students[0]->lastname));
+
+ // Should be 1 not 2.
+ $this->assertEquals(1, $assign->count_submissions());
+ $this->assertEquals(1, $assign->count_submissions_with_status('reopened'));
+ $this->assertEquals(0, $assign->count_submissions_need_grading());
+ $this->assertEquals(1, $assign->count_grades());
+
+ // Change max attempts to unlimited.
+ $formdata = clone($assign->get_instance());
+ $formdata->maxattempts = ASSIGN_UNLIMITED_ATTEMPTS;
+ $formdata->instance = $formdata->id;
+ $assign->update_instance($formdata);
+
+ // Check we can repopen still.
+ $result = $assign->testable_process_add_attempt($this->students[0]->id);
+ $this->assertEquals(true, $result);
+
+ $grades = $assign->get_user_grades_for_gradebook($this->students[0]->id);
+ $this->assertEquals(50, (int)$grades[$this->students[0]->id]->rawgrade);
+
+
+ }
}
diff --git a/mod/assign/upgradelib.php b/mod/assign/upgradelib.php
index 12baafb9cae..4ed07f45e16 100644
--- a/mod/assign/upgradelib.php
+++ b/mod/assign/upgradelib.php
@@ -99,6 +99,8 @@ class assign_upgrade_manager {
$data->requireallteammemberssubmit = 0;
$data->teamsubmissiongroupingid = 0;
$data->blindmarking = 0;
+ $data->attemptreopenmethod = 'none';
+ $data->maxattempts = ASSIGN_UNLIMITED_ATTEMPTS;
$newassignment = new assign(null, null, null);
@@ -247,7 +249,14 @@ class assign_upgrade_manager {
$grade->timemodified = $oldsubmission->timemarked;
$grade->timecreated = $oldsubmission->timecreated;
$grade->grade = $oldsubmission->grade;
- $grade->mailed = $oldsubmission->mailed;
+ if ($oldsubmission->mailed) {
+ // The mailed flag goes in the flags table.
+ $flags = new stdClass();
+ $flags->userid = $oldsubmission->userid;
+ $flags->assignment = $newassignment->get_instance()->id;
+ $flags->mailed = 1;
+ $DB->insert_record('assign_user_flags', $flags);
+ }
$grade->id = $DB->insert_record('assign_grades', $grade);
if (!$grade->id) {
$log .= get_string('couldnotinsertgrade', 'mod_assign', $grade->userid);
diff --git a/mod/assign/version.php b/mod/assign/version.php
index ab394dd88a3..a27d8de09f0 100644
--- a/mod/assign/version.php
+++ b/mod/assign/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$module->version = 2012112901; // The current module version (Date: YYYYMMDDXX).
+$module->version = 2013030600; // The current module version (Date: YYYYMMDDXX).
$module->requires = 2012112900; // Requires this Moodle version.
$module->cron = 60;
diff --git a/mod/assign/yui/history/history.js b/mod/assign/yui/history/history.js
new file mode 100644
index 00000000000..eb5cff0040a
--- /dev/null
+++ b/mod/assign/yui/history/history.js
@@ -0,0 +1,71 @@
+YUI.add('moodle-mod_assign-history', function (Y) {
+ // Define a function that will run in the context of a
+ // Node instance:
+ var CSS = {
+ LINK: 'mod-assign-history-link',
+ OPEN: 'mod-assign-history-link-open',
+ CLOSED: 'mod-assign-history-link-closed',
+ PANEL: 'mod-assign-history-panel'
+ },
+ COUNT = 0,
+ TOGGLE = function() {
+ var id = this.get('for'),
+ panel = Y.one('#' + id);
+ console.log(this);
+ if (this.hasClass(CSS.OPEN)) {
+ this.removeClass(CSS.OPEN);
+ this.addClass(CSS.CLOSED);
+ this.setStyle('overflow', 'hidden');
+ panel.hide();
+ } else {
+ this.removeClass(CSS.CLOSED);
+ this.addClass(CSS.OPEN);
+ panel.show();
+ }
+ },
+ HISTORY = function() {
+ var link = null,
+ panel = null,
+ wrapper = null,
+ container = this;
+
+ // Loop through all the children of this container and turn
+ // every odd node to a link to open/close the following panel.
+ this.get('children').each(function () {
+ if (link) {
+ COUNT++;
+ // First convert the link to an anchor.
+ wrapper = Y.Node.create('');
+ panel = this;
+ container.insertBefore(wrapper, link);
+ link.remove(false);
+ wrapper.appendChild(link);
+
+ // Add a for attribute to the link to link to the id of the panel.
+ if (!panel.get('id')) {
+ panel.set('id', CSS.PANEL + COUNT);
+ }
+ wrapper.set('for', panel.get('id'));
+ // Add an aria attribute for the live region.
+ panel.set('aria-live', 'polite');
+
+ wrapper.addClass(CSS.LINK);
+ wrapper.addClass(CSS.CLOSED);
+ panel.addClass(CSS.PANEL);
+ panel.hide();
+ link = null;
+ } else {
+ link = this;
+ }
+ });
+
+ // Setup event listeners.
+ this.delegate('click', TOGGLE, '.' + CSS.LINK);
+ };
+
+ // Use addMethod to add history to the Node prototype:
+ Y.Node.addMethod("history", HISTORY);
+
+ // Extend this functionality to NodeLists.
+ Y.NodeList.importMethod(Y.Node.prototype, "history");
+}, '@VERSION@', { requires: ['node', 'transition'] });