From 4ae22703c69d011465cb81fa16343d69eca3fa24 Mon Sep 17 00:00:00 2001 From: Andrew Madden Date: Mon, 7 Feb 2022 00:24:13 +1100 Subject: [PATCH] MDL-73073 mod_assign: Add new time limit fields to external functions * Add activity, activityformat, timelimit and submissionattachments to mod_assign_get_assignments * Add timestarted to mod_assign_get_submissions and mod_assign_get_submission_status * Add assignmentdata to mod_assign_get_submission_status * Move mod_assign external helper methods to an external_api child class to be used with modern external classes * Add start_submission external function * Create mod_assign child class of externallib_advanced_testcase for shared helper functions * Add extra logic to get_assignments on whether to provide intro attachments. --- mod/assign/classes/external/external_api.php | 115 +++++++ .../classes/external/start_submission.php | 113 ++++++ mod/assign/db/services.php | 9 +- mod/assign/externallib.php | 116 +++---- mod/assign/lang/en/assign.php | 3 + mod/assign/locallib.php | 62 ++++ .../tests/external/start_submission_test.php | 178 ++++++++++ .../tests/externallib_advanced_testcase.php | 172 +++++++++ mod/assign/tests/externallib_test.php | 325 ++++++++++-------- mod/assign/tests/generator/lib.php | 3 + mod/assign/tests/locallib_test.php | 197 +++++++++++ mod/assign/version.php | 2 +- 12 files changed, 1093 insertions(+), 202 deletions(-) create mode 100644 mod/assign/classes/external/external_api.php create mode 100644 mod/assign/classes/external/start_submission.php create mode 100644 mod/assign/tests/external/start_submission_test.php create mode 100644 mod/assign/tests/externallib_advanced_testcase.php diff --git a/mod/assign/classes/external/external_api.php b/mod/assign/classes/external/external_api.php new file mode 100644 index 00000000000..e8e9c834620 --- /dev/null +++ b/mod/assign/classes/external/external_api.php @@ -0,0 +1,115 @@ +. + +namespace mod_assign\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once("$CFG->libdir/externallib.php"); +require_once("$CFG->dirroot/mod/assign/locallib.php"); + +/** + * Extend the base external_api class with mod_assign utility methods. + * + * @package mod_assign + * @author Andrew Madden + * @copyright 2021 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class external_api extends \external_api { + + /** + * Generate a warning in a standard structure for a known failure. + * + * @param int $assignmentid - The assignment + * @param string $warningcode - The key for the warning message + * @param string $detail - A description of the error + * @return array - Warning structure containing item, itemid, warningcode, message + */ + protected static function generate_warning(int $assignmentid, string $warningcode, string $detail): array { + $warningmessages = [ + 'couldnotlock' => 'Could not lock the submission for this user.', + 'couldnotunlock' => 'Could not unlock the submission for this user.', + 'couldnotsubmitforgrading' => 'Could not submit assignment for grading.', + 'couldnotrevealidentities' => 'Could not reveal identities.', + 'couldnotgrantextensions' => 'Could not grant submission date extensions.', + 'couldnotrevert' => 'Could not revert submission to draft.', + 'invalidparameters' => 'Invalid parameters.', + 'couldnotsavesubmission' => 'Could not save submission.', + 'couldnotsavegrade' => 'Could not save grade.', + 'couldnotstartsubmission' => 'Could not start submission with time limit.', + 'submissionnotopen' => 'This assignment is not open for submissions', + 'timelimitnotenabled' => 'Time limit is not enabled for assignment.', + 'opensubmissionexists' => 'Open assignment submission already exists.', + ]; + + $message = $warningmessages[$warningcode]; + if (empty($message)) { + $message = 'Unknown warning type.'; + } + + return [ + 'item' => s($detail), + 'itemid' => $assignmentid, + 'warningcode' => $warningcode, + 'message' => $message, + ]; + } + + /** + * Utility function for validating an assign. + * + * @param int $assignid assign instance id + * @return array array containing the assign, course, context and course module objects + * @since Moodle 3.2 + */ + protected static function validate_assign(int $assignid): array { + global $DB; + + // Request and permission validation. + $assign = $DB->get_record('assign', ['id' => $assignid], 'id', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_instance($assign, 'assign'); + + $context = \context_module::instance($cm->id); + // Please, note that is not required to check mod/assign:view because is done by validate_context->require_login. + self::validate_context($context); + $assign = new \assign($context, $cm, $course); + + return [$assign, $course, $cm, $context]; + } + + /** + * Get a submission from an assignment for a user. Encapsulates checking whether it's a solo or team submission. + * + * @param \assign $assignment Assignment object. + * @param int|null $userid User id. + * @param int $groupid Group id. + * @param bool $create Whether a new submission should be created. + * @param int $attemptnumber Attempt number. Use -1 for last attempt. + * @return bool|\stdClass + */ + protected static function get_user_or_group_submission(\assign $assignment, int $userid = null, + int $groupid = 0, bool $create = false, int $attemptnumber = -1) { + if ($assignment->get_instance($userid)->teamsubmission) { + $submission = $assignment->get_group_submission($userid, $groupid, $create, $attemptnumber); + } else { + $submission = $assignment->get_user_submission($userid, $create, $attemptnumber); + } + return $submission; + } +} diff --git a/mod/assign/classes/external/start_submission.php b/mod/assign/classes/external/start_submission.php new file mode 100644 index 00000000000..669a4756764 --- /dev/null +++ b/mod/assign/classes/external/start_submission.php @@ -0,0 +1,113 @@ +. + +namespace mod_assign\external; + +/** + * External function to notify Moodle that an assignment submission is starting. + * + * @package mod_assign + * @author Andrew Madden + * @copyright 2021 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class start_submission extends external_api { + + /** + * Describes the parameters for submission_start. + * + * @return \external_function_parameters + * @since Moodle 4.0 + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters ([ + 'assignid' => new \external_value(PARAM_INT, 'Assignment instance id'), + ] + ); + } + + /** + * Call to start an assignment submission. + * + * @param int $assignid Assignment ID. + * @return array + * @since Moodle 4.0 + */ + public static function execute(int $assignid): array { + global $DB, $USER; + + $result = $warnings = []; + $submission = null; + + [ + 'assignid' => $assignid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'assignid' => $assignid, + ]); + + list($assignment, $course, $cm, $context) = self::validate_assign($assignid); + + $assignment->update_effective_access($USER->id); + $latestsubmission = external_api::get_user_or_group_submission($assignment, $USER->id); + if (!$assignment->submissions_open($USER->id)) { + $warnings[] = self::generate_warning($assignid, + 'submissionnotopen', + get_string('submissionnotopen', 'assign')); + } + + if (!$assignment->is_time_limit_enabled()) { + $warnings[] = self::generate_warning($assignid, + 'timelimitnotenabled', + get_string('timelimitnotenabled', 'assign')); + } else if ($assignment->is_attempt_in_progress()) { + $warnings[] = self::generate_warning($assignid, + 'opensubmissionexists', + get_string('opensubmissionexists', 'assign')); + } + + if (empty($warnings)) { + // If there is an open submission with no start time, use latest submission, otherwise create a new submission. + if (!empty($latestsubmission) + && $latestsubmission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED + && empty($latestsubmission->timestarted)) { + $submission = $latestsubmission; + } else { + $submission = external_api::get_user_or_group_submission($assignment, $USER->id, 0, true); + } + + // Set the start time of the submission. + $submission->timestarted = time(); + $DB->update_record('assign_submission', $submission); + } + + $result['submissionid'] = $submission ? $submission->id : 0; + $result['warnings'] = $warnings; + return $result; + } + + /** + * Describes the submission_start return value. + * + * @return \external_single_structure + * @since Moodle 4.0 + */ + public static function execute_returns(): \external_single_structure { + return new \external_single_structure([ + 'submissionid' => new \external_value(PARAM_INT, 'New submission ID.'), + 'warnings' => new \external_warnings(), + ]); + } +} diff --git a/mod/assign/db/services.php b/mod/assign/db/services.php index 9073b02172e..4d9511ccd46 100644 --- a/mod/assign/db/services.php +++ b/mod/assign/db/services.php @@ -240,5 +240,12 @@ $functions = array( 'capabilities' => 'mod/assign:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), - + 'mod_assign_start_submission' => [ + 'classname' => 'mod_assign\external\start_submission', + 'methodname' => 'execute', + 'description' => 'Start a submission for user if assignment has a time limit.', + 'type' => 'write', + 'capabilities' => 'mod/assign:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ], ); diff --git a/mod/assign/externallib.php b/mod/assign/externallib.php index 58e957f6930..b496de79637 100644 --- a/mod/assign/externallib.php +++ b/mod/assign/externallib.php @@ -34,39 +34,7 @@ require_once("$CFG->dirroot/mod/assign/locallib.php"); * @copyright 2012 Paul Charsley * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class mod_assign_external extends external_api { - - /** - * Generate a warning in a standard structure for a known failure. - * - * @param int $assignmentid - The assignment - * @param string $warningcode - The key for the warning message - * @param string $detail - A description of the error - * @return array - Warning structure containing item, itemid, warningcode, message - */ - private static function generate_warning($assignmentid, $warningcode, $detail) { - $warningmessages = array( - 'couldnotlock'=>'Could not lock the submission for this user.', - 'couldnotunlock'=>'Could not unlock the submission for this user.', - 'couldnotsubmitforgrading'=>'Could not submit assignment for grading.', - 'couldnotrevealidentities'=>'Could not reveal identities.', - 'couldnotgrantextensions'=>'Could not grant submission date extensions.', - 'couldnotrevert'=>'Could not revert submission to draft.', - 'invalidparameters'=>'Invalid parameters.', - 'couldnotsavesubmission'=>'Could not save submission.', - 'couldnotsavegrade'=>'Could not save grade.' - ); - - $message = $warningmessages[$warningcode]; - if (empty($message)) { - $message = 'Unknown warning type.'; - } - - return array('item' => s($detail), - 'itemid'=>$assignmentid, - 'warningcode'=>$warningcode, - 'message'=>$message); - } +class mod_assign_external extends \mod_assign\external\external_api { /** * Describes the parameters for get_grades @@ -384,7 +352,11 @@ class mod_assign_external extends external_api { 'm.requiresubmissionstatement, '. 'm.preventsubmissionnotingroup, '. 'm.intro, '. - 'm.introformat'; + 'm.introformat,' . + 'm.activity,' . + 'm.activityformat,' . + 'm.timelimit,' . + 'm.submissionattachments'; $coursearray = array(); foreach ($courses as $id => $course) { $assignmentarray = array(); @@ -457,6 +429,8 @@ class mod_assign_external extends external_api { 'markingallocation' => $module->markingallocation, 'requiresubmissionstatement' => $module->requiresubmissionstatement, 'preventsubmissionnotingroup' => $module->preventsubmissionnotingroup, + 'timelimit' => $module->timelimit, + 'submissionattachments' => $module->submissionattachments, 'configs' => $configarray ); @@ -468,9 +442,10 @@ class mod_assign_external extends external_api { $options); $assignment['introfiles'] = external_util::get_area_files($context->id, 'mod_assign', 'intro', false, false); - - $assignment['introattachments'] = external_util::get_area_files($context->id, 'mod_assign', - ASSIGN_INTROATTACHMENT_FILEAREA, 0); + if ($assign->should_provide_intro_attachments($USER->id)) { + $assignment['introattachments'] = external_util::get_area_files($context->id, 'mod_assign', + ASSIGN_INTROATTACHMENT_FILEAREA, 0); + } } if ($module->requiresubmissionstatement) { @@ -497,6 +472,13 @@ class mod_assign_external extends external_api { } } + if ($module->activity && $assign->submissions_open($USER->id, true)) { + list($assignment['activity'], $assignment['activityformat']) = external_format_text($module->activity, + $module->activityformat, $context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA); + $assignment['activityattachments'] = external_util::get_area_files($context->id, 'mod_assign', + ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0); + } + $assignmentarray[] = $assignment; } } @@ -561,6 +543,12 @@ class mod_assign_external extends external_api { 'introformat' => new external_format_value('intro', VALUE_OPTIONAL), 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL), 'introattachments' => new external_files('intro attachments files', VALUE_OPTIONAL), + 'activity' => new external_value(PARAM_RAW, 'Description of activity', VALUE_OPTIONAL), + 'activityformat' => new external_format_value('activity', VALUE_OPTIONAL), + 'activityattachments' => new external_files('Files from activity field', VALUE_OPTIONAL), + 'timelimit' => new external_value(PARAM_INT, 'Time limit to complete assigment', VALUE_OPTIONAL), + 'submissionattachments' => new external_value(PARAM_INT, + 'Flag to only show files during submission', VALUE_OPTIONAL), ), 'assignment information object'); } @@ -759,7 +747,7 @@ class mod_assign_external extends external_api { WHERE mxs.assignment = :assignid1 GROUP BY mxs.userid, mxs.groupid'; $sql = "SELECT mas.id, mas.assignment,mas.userid,". - "mas.timecreated,mas.timemodified,mas.status,mas.groupid,mas.attemptnumber ". + "mas.timecreated,mas.timemodified,mas.timestarted,mas.status,mas.groupid,mas.attemptnumber ". "FROM {assign_submission} mas ". "JOIN ( " . $submissionmaxattempt . " ) smx ON mas.userid = smx.userid ". "AND mas.groupid = smx.groupid ". @@ -788,6 +776,7 @@ class mod_assign_external extends external_api { 'userid' => $submissionrecord->userid, 'timecreated' => $submissionrecord->timecreated, 'timemodified' => $submissionrecord->timemodified, + 'timestarted' => $submissionrecord->timestarted, 'status' => $submissionrecord->status, 'attemptnumber' => $submissionrecord->attemptnumber, 'groupid' => $submissionrecord->groupid, @@ -872,6 +861,7 @@ class mod_assign_external extends external_api { 'attemptnumber' => new external_value(PARAM_INT, 'attempt number'), 'timecreated' => new external_value(PARAM_INT, 'submission creation time'), 'timemodified' => new external_value(PARAM_INT, 'submission last modified time'), + 'timestarted' => new external_value(PARAM_INT, 'submission start time', VALUE_OPTIONAL), 'status' => new external_value(PARAM_TEXT, 'submission status'), 'groupid' => new external_value(PARAM_INT, 'group id'), 'assignment' => new external_value(PARAM_INT, 'assignment id', VALUE_OPTIONAL), @@ -2499,6 +2489,25 @@ class mod_assign_external extends external_api { } } + // Send back some assignment data as well. + $instance = $assign->get_instance(); + $assignmentdata = []; + $attachments = []; + if ($assign->should_provide_intro_attachments($user->id)) { + $attachments['intro'] = external_util::get_area_files($context->id, 'mod_assign', + ASSIGN_INTROATTACHMENT_FILEAREA, 0); + } + if ($instance->activity && ($lastattempt || $assign->submissions_open($user->id, true))) { + list($assignmentdata['activity'], $assignmentdata['activityformat']) = external_format_text($instance->activity, + $instance->activityformat, $context->id, 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA); + $attachments['activity'] = external_util::get_area_files($context->id, 'mod_assign', + ASSIGN_ACTIVITYATTACHMENT_FILEAREA, 0); + } + if (!empty($attachments)) { + $assignmentdata['attachments'] = $attachments; + } + $result['assignmentdata'] = $assignmentdata; + $result['warnings'] = $warnings; return $result; } @@ -2544,6 +2553,7 @@ class mod_assign_external extends external_api { 'caneditowner' => new external_value(PARAM_BOOL, 'Whether the owner of the submission can edit it.'), 'cansubmit' => new external_value(PARAM_BOOL, 'Whether the user can submit.'), 'extensionduedate' => new external_value(PARAM_INT, 'Extension due date.'), + 'timelimit' => new external_value(PARAM_INT, 'Time limit for submission.', VALUE_OPTIONAL), 'blindmarking' => new external_value(PARAM_BOOL, 'Whether blind marking is enabled.'), 'gradingstatus' => new external_value(PARAM_ALPHANUMEXT, 'Grading status.'), 'usergroups' => new external_multiple_structure( @@ -2570,6 +2580,14 @@ class mod_assign_external extends external_api { ) ), 'List all the previous attempts did by the user.', VALUE_OPTIONAL ), + 'assignmentdata' => new external_single_structure([ + 'attachments' => new external_single_structure([ + 'intro' => new external_files('Intro attachments files', VALUE_OPTIONAL), + 'activity' => new external_files('Activity attachments files', VALUE_OPTIONAL), + ], 'Intro and activity attachments', VALUE_OPTIONAL), + 'activity' => new external_value(PARAM_RAW, 'Text of activity', VALUE_OPTIONAL), + 'activityformat' => new external_format_value('activity', VALUE_OPTIONAL), + ], 'Extra information about assignment', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ) ); @@ -2890,28 +2908,6 @@ class mod_assign_external extends external_api { )); } - /** - * Utility function for validating an assign. - * - * @param int $assignid assign instance id - * @return array array containing the assign, course, context and course module objects - * @since Moodle 3.2 - */ - protected static function validate_assign($assignid) { - global $DB; - - // Request and permission validation. - $assign = $DB->get_record('assign', array('id' => $assignid), 'id', MUST_EXIST); - list($course, $cm) = get_course_and_cm_from_instance($assign, 'assign'); - - $context = context_module::instance($cm->id); - // Please, note that is not required to check mod/assign:view because is done by validate_context->require_login. - self::validate_context($context); - $assign = new assign($context, $cm, $course); - - return array($assign, $course, $cm, $context); - } - /** * Describes the parameters for view_assign. * diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php index d4380a21710..69f4a1fce70 100644 --- a/mod/assign/lang/en/assign.php +++ b/mod/assign/lang/en/assign.php @@ -410,6 +410,7 @@ $string['numberofsubmissionsneedgradinglabel'] = 'Needs grading: {$a}'; $string['numberofteams'] = 'Groups'; $string['offline'] = 'No online submissions required'; $string['open'] = 'Open'; +$string['opensubmissionexists'] = 'Open assignment submission already exists.'; $string['outof'] = '{$a->current} out of {$a->total}'; $string['overdue'] = 'Assignment is overdue by: {$a}'; $string['override'] = 'Override'; @@ -535,6 +536,7 @@ $string['submissioneditable'] = 'Student can edit this submission'; $string['submissionlog'] = 'Student: {$a->fullname}, Status: {$a->status}'; $string['submissionnotcopiedinvalidstatus'] = 'The submission was not copied because it has been edited since it was reopened.'; $string['submissionnoteditable'] = 'Student cannot edit this submission'; +$string['submissionnotopen'] = 'This assignment is not open for submissions'; $string['submissionnotready'] = 'This assignment is not ready to submit:'; $string['privacy:submissionpath'] = 'submission'; $string['submissionplugins'] = 'Submission plugins'; @@ -608,6 +610,7 @@ $string['teamsubmissiongroupingid_help'] = 'This is the grouping that the assign $string['textinstructions'] = 'Assignment instructions'; $string['timelimit'] = 'Time limit'; $string['timelimit_help'] = 'If enabled, the time limit is stated on the assignment page and a countdown timer is displayed during the assignment.'; +$string['timelimitnotenabled'] = 'Time limit is not enabled for assignment.'; $string['timelimitpassed'] = 'Time limit has been passed'; $string['timemodified'] = 'Last modified'; $string['timeremaining'] = 'Time remaining'; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 4ceb2464f7b..174a6c72a7a 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -1908,6 +1908,33 @@ class assign { return ($this->count_attachments() > 0); } + /** + * Check if the intro attachments should be provided to the user. + * + * @param int $userid User id. + * @return bool + */ + public function should_provide_intro_attachments(int $userid): bool { + $instance = $this->get_instance($userid); + + // Check if user has permission to view attachments regardless of assignment settings. + if (has_capability('moodle/course:manageactivities', $this->get_context())) { + return true; + } + + // If assignment does not show intro, we never provide intro attachments. + if (!$this->show_intro()) { + return false; + } + + // If intro attachments should only be shown when submission is started, check if there is an open submission. + if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) { + return false; + } + + return true; + } + /** * Return a grade in user-friendly form, whether it's a scale or not. * @@ -9686,6 +9713,41 @@ class assign { return $submissionstatement; } + + /** + * Check if time limit for assignment enabled and set up. + * + * @param int|null $userid User ID. If null, use global user. + * @return bool + */ + public function is_time_limit_enabled(?int $userid = null): bool { + $instance = $this->get_instance($userid); + return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit); + } + + /** + * Check if an assignment submission is already started and not yet submitted. + * + * @param int|null $userid User ID. If null, use global user. + * @param int $groupid Group ID. If 0, use user id to determine group. + * @param int $attemptnumber Attempt number. If -1, check latest submission. + * @return bool + */ + public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool { + if ($this->get_instance($userid)->teamsubmission) { + $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber); + } else { + $submission = $this->get_user_submission($userid, false, $attemptnumber); + } + + // If time limit is enabled, we only assume it is in progress if there is a start time for submission. + $timedattemptstarted = true; + if ($this->is_time_limit_enabled($userid)) { + $timedattemptstarted = !empty($submission) && !empty($submission->timestarted); + } + + return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted; + } } /** diff --git a/mod/assign/tests/external/start_submission_test.php b/mod/assign/tests/external/start_submission_test.php new file mode 100644 index 00000000000..03b1fb355fd --- /dev/null +++ b/mod/assign/tests/external/start_submission_test.php @@ -0,0 +1,178 @@ +. + +namespace mod_assign\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/mod/assign/tests/externallib_advanced_testcase.php'); + +/** + * Test the start_submission external function. + * + * @package mod_assign + * @category test + * @covers \mod_assign\external\start_submission + * @author Andrew Madden + * @copyright 2021 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class start_submission_test extends \mod_assign\externallib_advanced_testcase { + /** @var \stdClass $course New course created to hold the assignments */ + protected $course = null; + + /** + * Called before every test. + */ + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + } + + /** + * Test start_submission if assignment doesn't exist matching id. + */ + public function test_start_submission_with_invalid_assign_id() { + $this->expectException(\dml_exception::class); + start_submission::execute(123); + } + + /** + * Test start_submission if user is not able to access activity or course. + */ + public function test_start_submission_when_user_has_no_capability_to_view_assignment() { + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $assign = $generator->create_instance(['course' => $this->course->id]); + + $this->expectException(\require_login_exception::class); + start_submission::execute($assign->id); + } + + /** + * Test start_submission if assignment cut off date has elapsed. + */ + public function test_start_submission_when_assignment_past_due_date() { + $fiveminago = time() - 300; + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status( + false, ['cutoffdate' => $fiveminago]); + $result = start_submission::execute($instance->id); + $filteredwarnings = array_filter($result['warnings'], function($warning) { + return $warning['warningcode'] === 'submissionnotopen'; + }); + $this->assertCount(1, $filteredwarnings); + $this->assertEquals(0, $result['submissionid']); + $warning = array_pop($filteredwarnings); + $this->assertEquals($instance->id, $warning['itemid']); + $this->assertEquals('This assignment is not open for submissions', $warning['item']); + } + + /** + * Test start_submission if time limit is disabled. + */ + public function test_start_submission_when_time_limit_disabled() { + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(); + $result = start_submission::execute($instance->id); + $filteredwarnings = array_filter($result['warnings'], function($warning) { + return $warning['warningcode'] === 'timelimitnotenabled'; + }); + $this->assertCount(1, $filteredwarnings); + $this->assertEquals(0, $result['submissionid']); + $warning = array_pop($filteredwarnings); + $this->assertEquals($instance->id, $warning['itemid']); + $this->assertEquals('Time limit is not enabled for assignment.', $warning['item']); + } + + /** + * Test start_submission if time limit is not set for assignment. + */ + public function test_start_submission_when_time_limit_not_set() { + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status(); + $result = start_submission::execute($instance->id); + $filteredwarnings = array_filter($result['warnings'], function($warning) { + return $warning['warningcode'] === 'timelimitnotenabled'; + }); + $this->assertCount(1, $filteredwarnings); + $this->assertEquals(0, $result['submissionid']); + $warning = array_pop($filteredwarnings); + $this->assertEquals($instance->id, $warning['itemid']); + $this->assertEquals('Time limit is not enabled for assignment.', $warning['item']); + } + + /** + * Test start_submission if user already has open submission. + */ + public function test_start_submission_when_submission_already_open() { + global $DB; + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status( + false, ['timelimit' => 300]); + $submission = $assign->get_user_submission($student1->id, true); + $submission->timestarted = time(); + $DB->update_record('assign_submission', $submission); + $result = start_submission::execute($instance->id); + $filteredwarnings = array_filter($result['warnings'], function($warning) { + return $warning['warningcode'] === 'opensubmissionexists'; + }); + $this->assertCount(1, $filteredwarnings); + $this->assertEquals(0, $result['submissionid']); + $warning = array_pop($filteredwarnings); + $this->assertEquals($instance->id, $warning['itemid']); + $this->assertEquals('Open assignment submission already exists.', $warning['item']); + } + + /** + * Test start_submission if user has already submitted with no additional attempts available. + */ + public function test_start_submission_with_no_attempts_available() { + global $DB; + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status( + false, ['timelimit' => 300]); + $submission = $assign->get_user_submission($student1->id, true); + $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED; + $DB->update_record('assign_submission', $submission); + $result = start_submission::execute($instance->id); + $filteredwarnings = array_filter($result['warnings'], function($warning) { + return $warning['warningcode'] === 'submissionnotopen'; + }); + $this->assertCount(1, $filteredwarnings); + $this->assertEquals(0, $result['submissionid']); + $warning = array_pop($filteredwarnings); + $this->assertEquals($instance->id, $warning['itemid']); + $this->assertEquals('This assignment is not open for submissions', $warning['item']); + } + + /** + * Test start_submission if user has no open submissions. + */ + public function test_start_submission_with_new_submission() { + global $DB; + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status( + false, ['timelimit' => 300]); + // Clear all current submissions. + $DB->delete_records('assign_submission', ['assignment' => $instance->id]); + $result = start_submission::execute($instance->id); + $this->assertCount(0, $result['warnings']); + $this->assertNotEmpty($result['submissionid']); + } +} diff --git a/mod/assign/tests/externallib_advanced_testcase.php b/mod/assign/tests/externallib_advanced_testcase.php new file mode 100644 index 00000000000..e5876290caa --- /dev/null +++ b/mod/assign/tests/externallib_advanced_testcase.php @@ -0,0 +1,172 @@ +. + +namespace mod_assign; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/mod/assign/externallib.php'); +require_once(__DIR__ . '/fixtures/testable_assign.php'); + +/** + * Base class for unit tests for external functions in mod_assign. + * + * @package mod_assign + * @author Andrew Madden + * @copyright 2021 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class externallib_advanced_testcase extends \externallib_advanced_testcase { + + /** + * Create a submission for testing the get_submission_status function. + * @param bool $submitforgrading whether to submit for grading the submission + * @param array $params Optional params to use for creating assignment instance. + * @return array an array containing all the required data for testing + */ + protected function create_submission_for_testing_status(bool $submitforgrading = false, array $params = []): array { + global $DB; + + // Create a course and assignment and users. + $course = self::getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]); + + $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $params = array_merge([ + 'course' => $course->id, + 'assignsubmission_file_maxfiles' => 1, + 'assignsubmission_file_maxsizebytes' => 1024 * 1024, + 'assignsubmission_onlinetext_enabled' => 1, + 'assignsubmission_file_enabled' => 1, + 'submissiondrafts' => 1, + 'assignfeedback_file_enabled' => 1, + 'assignfeedback_comments_enabled' => 1, + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'sendnotifications' => 0 + ], $params); + + set_config('submissionreceipts', 0, 'assign'); + + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = \context_module::instance($cm->id); + + $assign = new \mod_assign_testable_assign($context, $cm, $course); + + $student1 = self::getDataGenerator()->create_user(); + $student2 = self::getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + + $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]); + $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $teacher->id]); + $this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $student2->id]); + $this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $teacher->id]); + + $this->setUser($student1); + + // Create a student1 with an online text submission. + // Simulate a submission. + $assign->get_user_submission($student1->id, true); + + $data = new \stdClass(); + $data->onlinetext_editor = [ + 'itemid' => file_get_unused_draft_itemid(), + 'text' => 'Submission text with a link', + 'format' => FORMAT_MOODLE, + ]; + + $draftidfile = file_get_unused_draft_itemid(); + $usercontext = \context_user::instance($student1->id); + $filerecord = [ + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftidfile, + 'filepath' => '/', + 'filename' => 't.txt', + ]; + $fs = get_file_storage(); + $fs->create_file_from_string($filerecord, 'text contents'); + + $data->files_filemanager = $draftidfile; + + $notices = []; + $assign->save_submission($data, $notices); + + if ($submitforgrading) { + // Now, submit the draft for grading. + $notices = []; + + $data = new \stdClass; + $data->userid = $student1->id; + $assign->submit_for_grading($data, $notices); + } + + return [$assign, $instance, $student1, $student2, $teacher, $group1, $group2]; + } + + /** + * Create a course, assignment module instance, student and teacher and enrol them in + * the course. + * + * @param array $params parameters to be provided to the assignment module creation + * @return array containing the course, assignment module, student and teacher + */ + protected function create_assign_with_student_and_teacher(array $params = []): array { + global $DB; + + $course = $this->getDataGenerator()->create_course(); + $params = array_merge([ + 'course' => $course->id, + 'name' => 'assignment', + 'intro' => 'assignment intro text', + ], $params); + + // Create a course and assignment and users. + $assign = $this->getDataGenerator()->create_module('assign', $params); + + $cm = get_coursemodule_from_instance('assign', $assign->id); + $context = \context_module::instance($cm->id); + + $student = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); + $teacher = $this->getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + + assign_capability('mod/assign:view', CAP_ALLOW, $teacherrole->id, $context->id, true); + assign_capability('mod/assign:viewgrades', CAP_ALLOW, $teacherrole->id, $context->id, true); + assign_capability('mod/assign:grade', CAP_ALLOW, $teacherrole->id, $context->id, true); + accesslib_clear_all_caches_for_unit_testing(); + + return [ + 'course' => $course, + 'assign' => $assign, + 'student' => $student, + 'teacher' => $teacher, + ]; + } +} diff --git a/mod/assign/tests/externallib_test.php b/mod/assign/tests/externallib_test.php index 919e650306f..fa4a3ebce31 100644 --- a/mod/assign/tests/externallib_test.php +++ b/mod/assign/tests/externallib_test.php @@ -17,7 +17,6 @@ namespace mod_assign; use core_user_external; -use externallib_advanced_testcase; use mod_assign_external; use mod_assign_testable_assign; @@ -27,6 +26,7 @@ global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); require_once($CFG->dirroot . '/mod/assign/externallib.php'); +require_once($CFG->dirroot . '/mod/assign/tests/externallib_advanced_testcase.php'); require_once(__DIR__ . '/fixtures/testable_assign.php'); /** @@ -37,7 +37,7 @@ require_once(__DIR__ . '/fixtures/testable_assign.php'); * @copyright 2012 Paul Charsley * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class externallib_test extends externallib_advanced_testcase { +class externallib_test extends \mod_assign\externallib_advanced_testcase { /** * Test get_grades @@ -184,7 +184,11 @@ class externallib_test extends externallib_advanced_testcase { 'intro' => 'the assignment intro text here link', 'introformat' => FORMAT_HTML, 'markingworkflow' => 1, - 'markingallocation' => 1 + 'markingallocation' => 1, + 'activityeditor' => [ + 'text' => 'Test activity', + 'format' => 1, + ], )); // Add a file as assignment attachment. @@ -244,6 +248,11 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals(1, $assignment['markingworkflow']); $this->assertEquals(1, $assignment['markingallocation']); $this->assertEquals(0, $assignment['preventsubmissionnotingroup']); + $this->assertEquals(0, $assignment['timelimit']); + $this->assertEquals(0, $assignment['submissionattachments']); + $this->assertEquals('Test activity', $assignment['activity']); + $this->assertEquals(1, $assignment['activityformat']); + $this->assertEmpty($assignment['activityattachments']); $this->assertCount(1, $assignment['introattachments']); $this->assertEquals('introattachment.txt', $assignment['introattachments'][0]['filename']); @@ -366,6 +375,80 @@ class externallib_test extends externallib_advanced_testcase { $this->assertArrayNotHasKey('submissionstatement', $assignmentret); } + /** + * Test that get_assignments does not return intro attachments if submissionattachments enabled and there is no open submission. + * + * @covers \mod_assign_external::get_assignments + */ + public function test_get_assignments_when_submissionattachments_is_enabled() { + global $DB; + + $this->resetAfterTest(true); + + // Create data. + $course1 = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + // First set submissionattachments to empty. + $assign = self::getDataGenerator()->create_module('assign', array( + 'course' => $course1->id, + 'name' => 'Test assignment', + 'intro' => 'The assignment intro text here', + 'introformat' => FORMAT_HTML, + 'submissionattachments' => 0, + )); + $context = \context_module::instance($assign->cmid); + + // Enrol user as student. + $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'student'); + + // Add a file as assignment attachment. + $filerecord = array('component' => 'mod_assign', 'filearea' => ASSIGN_INTROATTACHMENT_FILEAREA, + 'contextid' => $context->id, 'itemid' => 0, + 'filename' => 'introattachment.txt', 'filepath' => '/'); + $fs = get_file_storage(); + $fs->create_file_from_string($filerecord, 'Test intro attachment file'); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = mod_assign_external::get_assignments(); + $result = \external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result); + + $this->assertEquals(1, count($result['courses'])); + $course = $result['courses'][0]; + $this->assertEquals(1, count($course['assignments'])); + $assignment = $course['assignments'][0]; + $this->assertCount(1, $assignment['introattachments']); + $this->assertEquals('introattachment.txt', $assignment['introattachments'][0]['filename']); + + // Now set submissionattachments to enabled. We will close assignment so assume intro attachments are not sent. + $DB->set_field('assign', 'submissionattachments', '1', ['id' => $assignment['id']]); + $DB->set_field('assign', 'cutoffdate', time() - 300, ['id' => $assignment['id']]); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = mod_assign_external::get_assignments(); + $result = \external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result); + + $this->assertEquals(1, count($result['courses'])); + $course = $result['courses'][0]; + $this->assertEquals(1, count($course['assignments'])); + $assignment = $course['assignments'][0]; + $this->assertArrayNotHasKey('introattachments', $assignment); + + // Enrol a user as a teacher to override the setting. + $this->getDataGenerator()->enrol_user($user->id, $course1->id, 'editingteacher'); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = mod_assign_external::get_assignments(); + $result = \external_api::clean_returnvalue(mod_assign_external::get_assignments_returns(), $result); + + $this->assertEquals(1, count($result['courses'])); + $course = $result['courses'][0]; + $this->assertEquals(1, count($course['assignments'])); + $assignment = $course['assignments'][0]; + $this->assertCount(1, $assignment['introattachments']); + $this->assertEquals('introattachment.txt', $assignment['introattachments'][0]['filename']); + } + /** * Test get_submissions */ @@ -394,17 +477,20 @@ class externallib_test extends externallib_advanced_testcase { $submission->userid = $student->id; $submission->timecreated = time(); $submission->timemodified = $submission->timecreated; + $submission->timestarted = $submission->timecreated;; $submission->status = 'draft'; $submission->attemptnumber = 0; $submission->latest = 0; $sid = $DB->insert_record('assign_submission', $submission); // Second attempt. + $now = time(); $submission = new \stdClass(); $submission->assignment = $assign1->id; $submission->userid = $student->id; - $submission->timecreated = time(); - $submission->timemodified = $submission->timecreated; + $submission->timecreated = $now; + $submission->timemodified = $now; + $submission->timestarted = $now; $submission->status = 'submitted'; $submission->attemptnumber = 1; $submission->latest = 1; @@ -449,6 +535,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals($sid, $submission['id']); $this->assertCount(1, $submission['plugins']); $this->assertEquals('notgraded', $submission['gradingstatus']); + $this->assertEquals($now, $submission['timestarted']); // Test locking the context. set_config('contextlocking', 1); @@ -1907,98 +1994,6 @@ class externallib_test extends externallib_advanced_testcase { } } - /** - * Create a submission for testing the get_submission_status function. - * @param boolean $submitforgrading whether to submit for grading the submission - * @return array an array containing all the required data for testing - */ - private function create_submission_for_testing_status($submitforgrading = false) { - global $DB; - - // Create a course and assignment and users. - $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); - - $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); - $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); - - $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); - $params = array( - 'course' => $course->id, - 'assignsubmission_file_maxfiles' => 1, - 'assignsubmission_file_maxsizebytes' => 1024 * 1024, - 'assignsubmission_onlinetext_enabled' => 1, - 'assignsubmission_file_enabled' => 1, - 'submissiondrafts' => 1, - 'assignfeedback_file_enabled' => 1, - 'assignfeedback_comments_enabled' => 1, - 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, - 'sendnotifications' => 0 - ); - - set_config('submissionreceipts', 0, 'assign'); - - $instance = $generator->create_instance($params); - $cm = get_coursemodule_from_instance('assign', $instance->id); - $context = \context_module::instance($cm->id); - - $assign = new mod_assign_testable_assign($context, $cm, $course); - - $student1 = self::getDataGenerator()->create_user(); - $student2 = self::getDataGenerator()->create_user(); - $studentrole = $DB->get_record('role', array('shortname' => 'student')); - $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); - $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); - $teacher = self::getDataGenerator()->create_user(); - $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); - $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); - - $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id)); - $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $teacher->id)); - $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $student2->id)); - $this->getDataGenerator()->create_group_member(array('groupid' => $group2->id, 'userid' => $teacher->id)); - - $this->setUser($student1); - - // Create a student1 with an online text submission. - // Simulate a submission. - $submission = $assign->get_user_submission($student1->id, true); - - $data = new \stdClass(); - $data->onlinetext_editor = array( - 'itemid' => file_get_unused_draft_itemid(), - 'text' => 'Submission text with a link', - 'format' => FORMAT_MOODLE); - - $draftidfile = file_get_unused_draft_itemid(); - $usercontext = \context_user::instance($student1->id); - $filerecord = array( - 'contextid' => $usercontext->id, - 'component' => 'user', - 'filearea' => 'draft', - 'itemid' => $draftidfile, - 'filepath' => '/', - 'filename' => 't.txt', - ); - $fs = get_file_storage(); - $fs->create_file_from_string($filerecord, 'text contents'); - - $data->files_filemanager = $draftidfile; - - $notices = array(); - $assign->save_submission($data, $notices); - - if ($submitforgrading) { - // Now, submit the draft for grading. - $notices = array(); - - $data = new \stdClass; - $data->userid = $student1->id; - $assign->submit_for_grading($data, $notices); - } - - return array($assign, $instance, $student1, $student2, $teacher, $group1, $group2); - } - /** * Test get_submission_status for a draft submission. */ @@ -2053,6 +2048,9 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals($expectedformat, $submissionplugins['onlinetext']['editorfields'][0]['format']); $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']); $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']); + + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); } /** @@ -2082,7 +2080,10 @@ class externallib_test extends externallib_advanced_testcase { $this->assertFalse($result['lastattempt']['blindmarking']); $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']); $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']); + $this->assertNull($result['lastattempt']['submission']['timestarted']); + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); } /** @@ -2153,6 +2154,9 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals(1, $result['gradingsummary']['submissiondraftscount']); // We have a draft submission. $this->assertEquals(0, $result['gradingsummary']['submissionssubmittedcount']); // We have only draft submissions. $this->assertEquals(0, $result['gradingsummary']['submissionsneedgradingcount']); // We have only draft submissions. + + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); } /** @@ -2247,6 +2251,8 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']); $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']); + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); } /** @@ -2321,6 +2327,9 @@ class externallib_test extends externallib_advanced_testcase { $this->assertTrue(isset($result['feedback'])); $this->assertTrue(isset($result['feedback']['grade'])); $this->assertEquals($teacher->id, $result['feedback']['grade']['grader']); + + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); } /** @@ -2380,6 +2389,85 @@ class externallib_test extends externallib_advanced_testcase { $this->assertFalse($result['lastattempt']['blindmarking']); $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']); $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']); + + // Test assignment data. + $this->assertEquals(['attachments' => ['intro' => []]], $result['assignmentdata']); + } + + /** + * Test get_submission_status with time limit for student. + * + * @covers \mod_assign_external::get_submission_status + */ + public function test_get_submission_status_with_time_limit_enabled() { + $this->resetAfterTest(); + + set_config('enabletimelimit', '1', 'assign'); + + // Add time limit of 5 minutes to assignment. To edit activity, activity editor must not be empty. + list($assign, $instance, $student1, $student2, $teacher, $g1, $g2) = $this->create_submission_for_testing_status( + true, [ + 'timelimit' => 300, + 'activityeditor' => [ + 'text' => 'Test activity', + 'format' => 1, + ], + ] + ); + + // Add an intro attachment. + $fs = get_file_storage(); + $context = \context_module::instance($instance->cmid); + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'mod_assign', + 'filearea' => ASSIGN_INTROATTACHMENT_FILEAREA, + 'filename' => 'Test intro file', + 'itemid' => 0, + 'filepath' => '/' + ); + $fs->create_file_from_string($filerecord, 'Test assign file'); + + // Set optional param to indicate start time required. + $_GET['action'] = 'editsubmission'; + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = \context_module::instance($cm->id); + (new \assign($context, $cm, $cm->course))->get_user_submission(0, true); + + $result = mod_assign_external::get_submission_status($assign->get_instance()->id); + // We expect debugging because of the $PAGE object, this won't happen in a normal WS request. + $this->assertDebuggingCalled(); + $result = \external_api::clean_returnvalue(mod_assign_external::get_submission_status_returns(), $result); + + $this->assertCount(0, $result['warnings']); + $this->assertFalse(isset($result['gradingsummary'])); + $this->assertFalse(isset($result['feedback'])); + $this->assertFalse(isset($result['previousattempts'])); + + $this->assertTrue($result['lastattempt']['submissionsenabled']); + $this->assertFalse($result['lastattempt']['canedit']); + $this->assertFalse($result['lastattempt']['cansubmit']); + $this->assertFalse($result['lastattempt']['locked']); + $this->assertFalse($result['lastattempt']['graded']); + $this->assertEmpty($result['lastattempt']['extensionduedate']); + $this->assertFalse($result['lastattempt']['blindmarking']); + $this->assertCount(0, $result['lastattempt']['submissiongroupmemberswhoneedtosubmit']); + $this->assertEquals('notgraded', $result['lastattempt']['gradingstatus']); + $this->assertEquals(300, $result['lastattempt']['timelimit']); + $this->assertNotNull($result['lastattempt']['submission']['timestarted']); + $this->assertLessThanOrEqual(time(), $result['lastattempt']['submission']['timestarted']); + + // Test assignment data. + $this->assertNotEmpty($result['assignmentdata']); + $this->assertEquals('Test activity', $result['assignmentdata']['activity']); + $this->assertEquals(1, $result['assignmentdata']['activityformat']); + $this->assertCount(2, $result['assignmentdata']['attachments']); + $introattachments = $result['assignmentdata']['attachments']['intro']; + $activityattachments = $result['assignmentdata']['attachments']['activity']; + $this->assertCount(1, $introattachments); + $intro = reset($introattachments); + $this->assertEquals('Test intro file', $intro['filename']); + $this->assertEmpty($activityattachments); } /** @@ -2766,49 +2854,6 @@ class externallib_test extends externallib_advanced_testcase { } } - /** - * Create a a course, assignment module instance, student and teacher and enrol them in - * the course. - * - * @param array $params parameters to be provided to the assignment module creation - * @return array containing the course, assignment module, student and teacher - */ - private function create_assign_with_student_and_teacher($params = array()) { - global $DB; - - $course = $this->getDataGenerator()->create_course(); - $params = array_merge(array( - 'course' => $course->id, - 'name' => 'assignment', - 'intro' => 'assignment intro text', - ), $params); - - // Create a course and assignment and users. - $assign = $this->getDataGenerator()->create_module('assign', $params); - - $cm = get_coursemodule_from_instance('assign', $assign->id); - $context = \context_module::instance($cm->id); - - $student = $this->getDataGenerator()->create_user(); - $studentrole = $DB->get_record('role', array('shortname' => 'student')); - $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); - $teacher = $this->getDataGenerator()->create_user(); - $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); - $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); - - assign_capability('mod/assign:view', CAP_ALLOW, $teacherrole->id, $context->id, true); - assign_capability('mod/assign:viewgrades', CAP_ALLOW, $teacherrole->id, $context->id, true); - assign_capability('mod/assign:grade', CAP_ALLOW, $teacherrole->id, $context->id, true); - accesslib_clear_all_caches_for_unit_testing(); - - return array( - 'course' => $course, - 'assign' => $assign, - 'student' => $student, - 'teacher' => $teacher - ); - } - /** * Test test_view_assign */ diff --git a/mod/assign/tests/generator/lib.php b/mod/assign/tests/generator/lib.php index e72e0de975d..85fe6620afd 100644 --- a/mod/assign/tests/generator/lib.php +++ b/mod/assign/tests/generator/lib.php @@ -56,6 +56,9 @@ class mod_assign_generator extends testing_module_generator { 'maxattempts' => -1, 'markingworkflow' => 0, 'markingallocation' => 0, + 'activityformat' => 0, + 'timelimit' => 0, + 'submissionattachments' => 0, ); if (property_exists($record, 'teamsubmissiongroupingid')) { diff --git a/mod/assign/tests/locallib_test.php b/mod/assign/tests/locallib_test.php index 60637985f60..49d4c218e67 100644 --- a/mod/assign/tests/locallib_test.php +++ b/mod/assign/tests/locallib_test.php @@ -4331,4 +4331,201 @@ Anchor link 2:Link text $assign->get_course_module()->id . '&action=grading">' . get_string('numberofsubmissionsneedgradinglabel', 'assign', 1) . '', $summary); } + + /** + * Test that attachments should not be provided if \assign->show_intro returns false. + * + * @covers \assign::should_provide_intro_attachments + */ + public function test_should_provide_intro_attachments_with_show_intro_disabled() { + $this->resetAfterTest(); + $futuredate = time() + 300; + list($assign, $instance, $student) = $this->create_submission([ + 'alwaysshowdescription' => '0', + 'allowsubmissionsfromdate' => $futuredate, + ]); + $this->assertFalse($assign->should_provide_intro_attachments($student->id)); + } + + /** + * Test that attachments should be provided if user has capability to manage activity. + * + * @covers \assign::should_provide_intro_attachments + */ + public function test_should_provide_intro_attachments_with_bypass_capability() { + $this->resetAfterTest(); + list($assign, $instance, $student) = $this->create_submission([ + 'submissionattachments' => 1, + ]); + // Provide teaching role to student1 so they are able to bypass time limit restrictions on viewing attachments. + $this->getDataGenerator()->enrol_user($student->id, $instance->course, 'editingteacher'); + $this->assertTrue($assign->should_provide_intro_attachments($student->id)); + } + + /** + * Test that attachments should be provided if submissionattachments is disabled. + * + * @covers \assign::should_provide_intro_attachments + */ + public function test_should_provide_intro_attachments_with_submissionattachments_disabled() { + $this->resetAfterTest(); + list($assign, $instance, $student) = $this->create_submission(); + $this->assertTrue($assign->should_provide_intro_attachments($student->id)); + } + + /** + * Test that attachments should not be provided if submissionattachments is enabled with no open submission. + * + * @covers \assign::should_provide_intro_attachments + */ + public function test_should_provide_intro_attachments_with_submissionattachments_enabled_and_submissions_closed() { + $this->resetAfterTest(); + // Set cut-off date to the past. + list($assign, $instance, $student) = $this->create_submission([ + 'timelimit' => '300', + 'submissionattachments' => 1, + 'cutoffdate' => time() - 300, + ]); + $this->assertFalse($assign->should_provide_intro_attachments($student->id)); + } + + /** + * Test that attachments should be provided if submissionattachments is enabled with an open submission. + * + * @covers \assign::should_provide_intro_attachments + */ + public function test_should_provide_intro_attachments_submissionattachments_enabled_and_an_open_submission() { + $this->resetAfterTest(); + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student) = $this->create_submission([ + 'timelimit' => '300', + 'submissionattachments' => 1, + ]); + + // Open a submission. + $assign->get_user_submission($student->id, true); + + $this->assertTrue($assign->should_provide_intro_attachments($student->id)); + } + + /** + * Test that a submission using a time limit is currently open. + * + * @covers \assign::is_attempt_in_progress + */ + public function test_is_attempt_in_progress_with_open_submission() { + global $DB; + $this->resetAfterTest(); + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student) = $this->create_submission([ + 'timelimit' => '300', + ]); + $submission = $assign->get_user_submission($student->id, true); + // Set a timestarted. + $submission->timestarted = time() - 300; + $DB->update_record('assign_submission', $submission); + $this->assertTrue($assign->is_attempt_in_progress()); + } + + /** + * Test that a submission using a time limit is started without a start time. + * + * @covers \assign::is_attempt_in_progress + */ + public function test_is_attempt_in_progress_with_open_submission_and_no_timestarted() { + $this->resetAfterTest(); + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student) = $this->create_submission([ + 'timelimit' => '300', + ]); + $assign->get_user_submission($student->id, true); + $this->assertFalse($assign->is_attempt_in_progress()); + } + + /** + * Test that a submission using a time limit is currently not open. + * + * @covers \assign::is_attempt_in_progress + */ + public function test_is_attempt_in_progress_with_no_open_submission() { + global $DB; + $this->resetAfterTest(); + set_config('enabletimelimit', '1', 'assign'); + list($assign, $instance, $student) = $this->create_submission([ + 'timelimit' => '300', + ]); + // Clear all current submissions. + $DB->delete_records('assign_submission', ['assignment' => $instance->id]); + $this->assertFalse($assign->is_attempt_in_progress()); + } + + /** + * Create a submission for testing. + * @param array $params Optional params to use for creating assignment instance. + * @return array an array containing all the required data for testing + */ + protected function create_submission(array $params = []) { + global $DB; + + // Create a course and assignment and users. + $course = self::getDataGenerator()->create_course(array('groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $params = array_merge(array( + 'course' => $course->id, + 'assignsubmission_file_maxfiles' => 1, + 'assignsubmission_file_maxsizebytes' => 1024 * 1024, + 'assignsubmission_onlinetext_enabled' => 1, + 'assignsubmission_file_enabled' => 1, + 'submissiondrafts' => 1, + 'assignfeedback_file_enabled' => 1, + 'assignfeedback_comments_enabled' => 1, + 'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL, + 'sendnotifications' => 0 + ), $params); + + set_config('submissionreceipts', 0, 'assign'); + + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = \context_module::instance($cm->id); + + $assign = new \mod_assign_testable_assign($context, $cm, $course); + + $student = self::getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); + + $this->setUser($student); + + // Create a student1 with an online text submission. + // Simulate a submission. + $submission = $assign->get_user_submission($student->id, true); + + $data = new \stdClass(); + $data->onlinetext_editor = array( + 'itemid' => file_get_unused_draft_itemid(), + 'text' => 'Submission text with a link', + 'format' => FORMAT_MOODLE); + + $draftidfile = file_get_unused_draft_itemid(); + $usercontext = \context_user::instance($student->id); + $filerecord = array( + 'contextid' => $usercontext->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $draftidfile, + 'filepath' => '/', + 'filename' => 't.txt', + ); + $fs = get_file_storage(); + $fs->create_file_from_string($filerecord, 'text contents'); + + $data->files_filemanager = $draftidfile; + + $notices = array(); + $assign->save_submission($data, $notices); + + return array($assign, $instance, $student); + } } diff --git a/mod/assign/version.php b/mod/assign/version.php index b510c85bfdb..a3802547298 100644 --- a/mod/assign/version.php +++ b/mod/assign/version.php @@ -25,5 +25,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics). -$plugin->version = 2021110901; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2021110902; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2021052500; // Requires this Moodle version.