diff --git a/classes/booking_manager.php b/classes/booking_manager.php index 37644d0..4dfc359 100644 --- a/classes/booking_manager.php +++ b/classes/booking_manager.php @@ -158,12 +158,14 @@ private function get_iterator(): \Generator { * As there are multiple dependant data points (users, sessions, capacity) * that are checked. They are all in this method. * + * @param int $timenow The current time to use for validation. * @return array An array of errors. */ - public function validate(): array { + public function validate($timenow = null): array { global $DB; $errors = []; $sessioncapacitycache = []; + $timenow ??= time(); // Break into rows and validate the multiple interdependant fields together. foreach ($this->get_iterator() as $index => $entry) { @@ -197,8 +199,6 @@ public function validate(): array { // Check for session overbooking, that is, if it would go over session capacity. if ($session) { - $timenow = time(); - // If the session supplied does not link to the face-to-face module expected, then it's invalid. if ($session->facetoface != $this->f) { $errors[] = [ @@ -215,7 +215,7 @@ public function validate(): array { } if ($session->datetimeknown - && $entry->status !== 'cancelled' + && in_array($entry->status, ['', 'booked']) && facetoface_has_session_started($session, $timenow)) { $inprogressstr = get_string('cannotsignupsessioninprogress', 'facetoface'); $overstr = get_string('cannotsignupsessionover', 'facetoface'); @@ -328,6 +328,7 @@ public function process() { // Get signup type. if ($entry->status === 'cancelled') { + // Handle cancellation. if (facetoface_user_cancel($session, $user->id, true, $cancelerr)) { // Notify the user of the cancellation if the session hasn't started yet. $timenow = time(); @@ -339,26 +340,51 @@ public function process() { } } else { // Map status to status code. - $statuscode = array_search($entry->status, facetoface_statuses()); - if ($statuscode === false) { - // Defaults to booked if not found. - $statuscode = MDL_F2F_STATUS_BOOKED; - } - if ($statuscode === MDL_F2F_STATUS_BOOKED && !$session->datetimeknown) { - // If booked, ensures the status is waitlisted instead, if the datetime is unknown. - $statuscode = MDL_F2F_STATUS_WAITLISTED; + $statuscode = array_search($entry->status, facetoface_statuses()) ?: MDL_F2F_STATUS_BOOKED; + + // Handle signups. + if (in_array($statuscode, [MDL_F2F_STATUS_BOOKED, MDL_F2F_STATUS_WAITLISTED])) { + if ($statuscode === MDL_F2F_STATUS_BOOKED && !$session->datetimeknown) { + // If booked, ensures the status is waitlisted instead, if the datetime is unknown. + $statuscode = MDL_F2F_STATUS_WAITLISTED; + } + + facetoface_user_signup( + $session, + $this->facetoface, + $this->course, + $entry->discountcode, + $this->transform_notification_type($entry->notificationtype), + $statuscode, + $user->id, + true + ); + + continue; } - facetoface_user_signup( - $session, - $this->facetoface, - $this->course, - $entry->discountcode, - $this->transform_notification_type($entry->notificationtype), - $statuscode, - $user->id, - true - ); + // Handle attendance. + if (in_array($statuscode, [ + MDL_F2F_STATUS_NO_SHOW, + MDL_F2F_STATUS_PARTIALLY_ATTENDED, + MDL_F2F_STATUS_FULLY_ATTENDED, + ])) { + $attendees = facetoface_get_attendees($session->id); + // Get matching attendee. + foreach ($attendees as $attendee) { + if ($attendee->email === $entry->email) { + break; + } + } + + $data = (object) [ + 's' => $session->id, + 'submissionid_' . $attendee->submissionid => $statuscode, + ]; + facetoface_take_attendance($data); + + continue; + } } } diff --git a/tests/upload_test.php b/tests/upload_test.php index d4cf0f6..26b2c45 100644 --- a/tests/upload_test.php +++ b/tests/upload_test.php @@ -426,4 +426,121 @@ public function test_processing_cancellation() { $this->assertEquals($student->id, current($users)->id); $this->assertNotEmpty(current($users)->timecancelled); } + + /** + * Updates via uploads can be done for previous sessions, only if they are to update attendance. + * + * Book someone in, then once the session is over, update their attendance. This should work. + */ + public function test_updates_for_previous_sessions() { + global $DB; + /** @var \mod_facetoface_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('mod_facetoface'); + + $course = $this->getDataGenerator()->create_course(); + $facetoface = $generator->create_instance(['course' => $course->id]); + + // Generate users. + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $this->setCurrentTimeStart(); + $now = time(); + $session = $generator->create_session([ + 'facetoface' => $facetoface->id, + 'capacity' => '3', + 'allowoverbook' => '0', + 'details' => 'xyz', + 'duration' => '2', // One and half hours. + 'normalcost' => '111', + 'discountcost' => '11', + 'allowcancellations' => '0', + 'sessiondates' => [ + ['timestart' => $now + 5, 'timefinish' => $now + 10], // Lasts one second. + ], + ]); + $bm = new booking_manager($facetoface->id); + + // Book the student. + $records = [ + (object) [ + 'email' => $student->email, + 'session' => $session->id, + 'status' => 'booked', + ], + ]; + + $bm->load_from_array($records); + $errors = $bm->validate(); + $this->assertFalse( + $this->check_row_validation_error_exists( + $errors, + 1, + '' + ), + 'Expecting user to be booked without issues.' + ); + $bm->process(); + + $DB->update_record( + 'facetoface_sessions_dates', + (object) [ + 'timestart' => 0, + 'timefinish' => 1, + 'id' => $session->sessiondates[0]->id, + ], + ); + + // It should detect an error (e.g. cannot book a session in progress). + $errors = $bm->validate(time() + 1); + $this->assertTrue( + $this->check_row_validation_error_exists( + $errors, + 1, + get_string('cannotsignupsessionover', 'facetoface') + ), + 'Expecting user to not be bookable since the session has started.' + ); + + // Update the student's attendance after the session finishes. + $attendanceupdates = [ + (object) [ + 'email' => $student->email, + 'session' => $session->id, + 'status' => 'no_show', + 'grade_expected' => 0, + ], + (object) [ + 'email' => $student->email, + 'session' => $session->id, + 'status' => 'partially_attended', + 'grade_expected' => 50, + ], + (object) [ + 'email' => $student->email, + 'session' => $session->id, + 'status' => 'fully_attended', + 'grade_expected' => 100, + ], + ]; + + $timenow = time() + 4 * DAYSECS; // Two days after the session started. + foreach ($attendanceupdates as $update) { + $bm->load_from_array([$update]); + + $errors = $bm->validate($timenow); + $this->assertFalse( + $this->check_row_validation_error_exists( + $errors, + 1, + '' + ), + 'Expecting update to be valid (even though session has started or finished).' + ); + $bm->process(); + + // Check to ensure the grade is as expected from the update. + $grade = facetoface_get_grade($student->id, $course->id, $facetoface->id); + $this->assertEquals($update->grade_expected, $grade->grade); + } + } }