From df211804f1419fe2c8be38e60f10b36581634147 Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Wed, 13 Mar 2013 11:36:23 +0800 Subject: [PATCH] MDL-36804 mod_assign - allow students to resubmit work and display a submission + grading history This is based on work by Davo Smith with input from Fernando Oliveira (Thanks guys!). --- mod/assign/assignmentplugin.php | 1 + .../backup/moodle2/backup_assign_stepslib.php | 27 +- .../moodle2/restore_assign_stepslib.php | 39 +- mod/assign/db/install.xml | 40 +- mod/assign/db/upgrade.php | 139 +++ mod/assign/externallib.php | 8 +- mod/assign/gradingbatchoperationsform.php | 3 + mod/assign/gradingtable.php | 49 +- mod/assign/lang/en/assign.php | 53 +- mod/assign/lib.php | 39 +- mod/assign/locallib.php | 991 +++++++++++++++--- mod/assign/mod_form.php | 16 + mod/assign/module.js | 12 +- mod/assign/renderable.php | 80 +- mod/assign/renderer.php | 216 +++- mod/assign/styles.css | 90 +- mod/assign/submission/file/locallib.php | 31 + mod/assign/submission/onlinetext/locallib.php | 28 + mod/assign/submissionplugin.php | 14 +- mod/assign/tests/base_test.php | 10 +- mod/assign/tests/generator/lib.php | 4 +- mod/assign/tests/lib_test.php | 1 - mod/assign/tests/locallib_test.php | 126 ++- mod/assign/upgradelib.php | 11 +- mod/assign/version.php | 2 +- mod/assign/yui/history/history.js | 71 ++ 26 files changed, 1839 insertions(+), 262 deletions(-) create mode 100644 mod/assign/yui/history/history.js 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 @@ - + +
@@ -108,5 +112,23 @@
+ + + + + + + + + + + + + + + + + +
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: '; +$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'] });