diff --git a/classes/booking_manager.php b/classes/booking_manager.php index 53cc314..ddbaafa 100644 --- a/classes/booking_manager.php +++ b/classes/booking_manager.php @@ -172,6 +172,8 @@ public function validate($timenow = null): array { $sessioncapacitycache = []; $timenow ??= time(); + $usersessions = []; + // Break into rows and validate the multiple interdependant fields together. foreach ($this->get_iterator() as $index => $entry) { $row = $index + 1; @@ -241,6 +243,21 @@ public function validate($timenow = null): array { $sessioncapacitycache[$session->id]['capacity']--; $sessioncapacitycache[$session->id]['rows'][] = $row; } + + // Don't allow users to signup to another session if the signup type is not multiple. + if (isset($userid) && $entry->status !== 'cancelled' && $this->facetoface->signuptype != MOD_FACETOFACE_SIGNUP_MULTIPLE) { + if ($currusersessions = facetoface_get_user_submissions($this->f, $userid)) { + foreach ($currusersessions as $currusersession) { + if ($currusersession->sessionid != $session->id) { + $errors[] = [ + $row, + new lang_string('error:addalreadysignedupattendee', 'mod_facetoface', $entry->email), + ]; + break; + } + } + } + } } // Check user enrolment into the course. @@ -273,6 +290,39 @@ public function validate($timenow = null): array { new lang_string('error:invalidstatusspecified', 'mod_facetoface', $entry->status), ]; } + + // If the user exists and this isn't a cancellation, we need to store the distinct session for checking at the end. + if (isset($userid) && $entry->status !== 'cancelled') { + // Set the user sessions if it hasn't been set yet. + if (!isset($usersessions[$userid])) { + $usersessions[$userid] = [ + 'email' => $entry->email, + 'rows' => [], + 'sessions' => [], + ]; + } + + $usersessions[$userid]['rows'][] = $row; + + if ($session && !in_array($session->id, $usersessions[$userid]['sessions'])) { + $usersessions[$userid]['sessions'][] = $session->id; + } + } + } + + // If the signup type is not set to multiple, we need to create errors for users being added to multiple sessions. + if ($this->facetoface->signuptype != MOD_FACETOFACE_SIGNUP_MULTIPLE) { + // Get all users being added to more than 1 session. + $doublebookedusers = array_filter($usersessions, function($us) { + return count($us['sessions']) > 1; + }); + // Create errors for the user rows. + foreach ($doublebookedusers as $details) { + $errors[] = [ + implode(', ', $details['rows']), + new lang_string('error:multipleusersessions', 'mod_facetoface', $details['email']), + ]; + } } // For all sessions that went over capacity, report it. diff --git a/lang/en/facetoface.php b/lang/en/facetoface.php index 9cdd69e..c8891eb 100644 --- a/lang/en/facetoface.php +++ b/lang/en/facetoface.php @@ -153,6 +153,7 @@ $string['error:sessionoverbooked'] = 'Session ID {$a->session} overbooked by {$a->amount} person(s).'; $string['error:sessiondoesnotexist'] = 'Session ID {$a} does not exist'; $string['error:userdoesnotexist'] = 'User {$a} does not exist'; +$string['error:multipleusersessions'] = 'User {$a} has more than one session'; $string['error:multipleusersmatched'] = 'Multiple users matched to identifier {$a}'; $string['error:addalreadysignedupattendee'] = '{$a} is already signed-up for this Face-to-Face activity.'; $string['error:addattendee'] = 'Could not add {$a} to the session.'; diff --git a/tests/upload_test.php b/tests/upload_test.php index a475849..b88ea97 100644 --- a/tests/upload_test.php +++ b/tests/upload_test.php @@ -138,7 +138,9 @@ public function test_user_validation() { // Generate users. $user = $this->getDataGenerator()->create_user(); - $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $student3 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setCurrentTimeStart(); $now = time(); @@ -156,6 +158,20 @@ public function test_user_validation() { ], ]); + $secondsession = $generator->create_session([ + 'facetoface' => $facetoface->id, + 'capacity' => '3', + 'allowoverbook' => '0', + 'details' => 'xyz', + 'duration' => '1.5', // One and half hours. + 'normalcost' => '111', + 'discountcost' => '11', + 'allowcancellations' => '0', + 'sessiondates' => [ + ['timestart' => $now + 4 * DAYSECS, 'timefinish' => $now + 5 * DAYSECS], + ], + ]); + $remotesession = $generator->create_session([ 'facetoface' => $anotherfacetoface->id, 'capacity' => '3', @@ -170,6 +186,9 @@ public function test_user_validation() { ], ]); + facetoface_user_signup($session, $facetoface, $course, '', + MDL_F2F_TEXT, MDL_F2F_STATUS_BOOKED, $student3->id); + $bm = new booking_manager($facetoface->id); $records = [ @@ -191,7 +210,7 @@ public function test_user_validation() { ], // Test student who is enrolled into the course (no issues). (object) [ - 'email' => $student->email, + 'email' => $student1->email, 'session' => $session->id, 'status' => '', 'notificationtype' => '', @@ -199,15 +218,23 @@ public function test_user_validation() { ], // Test invalid options. (object) [ - 'email' => $student->email, + 'email' => $student2->email, 'session' => $session->id, 'status' => 'helloworld', 'notificationtype' => 'phone', 'discountcode' => '', ], + // Test multiple rows for the same student when f2f signup type is single. + (object) [ + 'email' => $student2->email, + 'session' => $secondsession->id, + 'status' => '', + 'notificationtype' => '', + 'discountcode' => '', + ], // Test permissions (e.g. user not able to upload/process for a f2f activity loaded). (object) [ - 'email' => $student->email, + 'email' => $student2->email, 'session' => $remotesession->id, 'status' => '', 'notificationtype' => '', @@ -215,12 +242,20 @@ public function test_user_validation() { ], // Test email does not match case-wise (in the default case sensitive mode). (object) [ - 'email' => strtoupper($student->email), + 'email' => strtoupper($student2->email), 'session' => $session->id, 'status' => '', 'notificationtype' => '', 'discountcode' => '', ], + // Test student already in a session when f2f signup type is single. + (object) [ + 'email' => $student3->email, + 'session' => $secondsession->id, + 'status' => '', + 'notificationtype' => '', + 'discountcode' => '', + ], ]; $bm->load_from_array($records); @@ -274,7 +309,16 @@ public function test_user_validation() { $this->assertTrue( $this->check_row_validation_error_exists( $errors, - 5, + '4, 5, 6, 7', + new lang_string('error:multipleusersessions', 'mod_facetoface', $student2->email) + ), + 'Expecting multiple sessions error for user when f2f signup type is single.' + ); + + $this->assertTrue( + $this->check_row_validation_error_exists( + $errors, + 6, new lang_string('error:tryingtoupdatesessionfromanothermodule', 'mod_facetoface', (object) [ 'session' => $remotesession->id, 'f' => $facetoface->id, @@ -282,25 +326,35 @@ public function test_user_validation() { ), 'Expecting permission check conflict due to session->facetoface + facetoface id mismatcherror.' ); + $this->assertTrue( $this->check_row_validation_error_exists( $errors, - 6, - new lang_string('error:userdoesnotexist', 'mod_facetoface', strtoupper($student->email)) + 7, + new lang_string('error:userdoesnotexist', 'mod_facetoface', strtoupper($student2->email)) ), 'Expecting user to not exist because email does not match case-wise.' ); + + $this->assertTrue( + $this->check_row_validation_error_exists( + $errors, + 8, + new lang_string('error:addalreadysignedupattendee', 'mod_facetoface', $student3->email) + ), + 'Expecting user already signed up to another session error.' + ); } /** * Helper function to check if a specific error exists in the array of errors. * * @param array $errors Array of errors. - * @param int $expectedrownumber Expected row number. + * @param string $expectedrownumber Expected row number string. * @param string $expectederrormsg Expected error message. * @return bool True if the error exists, false otherwise. */ - private function check_row_validation_error_exists(array $errors, int $expectedrownumber, string $expectederrormsg): bool { + private function check_row_validation_error_exists(array $errors, string $expectedrownumber, string $expectederrormsg): bool { foreach ($errors as $error) { // Note: row number is based on a CSV file human readable format, where there is a header and row data. [$rownumber, $errormsg] = $error; @@ -758,4 +812,73 @@ public function test_email_suppression(string $status, bool $shouldsuppress) { $this->assertNotEmpty($emails); } } + + /** + * Test upload processing multiple sessions with signup type set to multiple. + */ + public function test_processing_signup_multiple() { + /** @var \mod_facetoface_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('mod_facetoface'); + + $course = $this->getDataGenerator()->create_course(); + // Facetoface with signuptype set to multiple. + $facetoface = $generator->create_instance(['course' => $course->id, 'signuptype' => MOD_FACETOFACE_SIGNUP_MULTIPLE]); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $now = time(); + $record = [ + 'facetoface' => $facetoface->id, + 'capacity' => '3', + 'allowoverbook' => '0', + 'details' => 'xyz', + 'duration' => '1.5', // One and half hours. + 'normalcost' => '111', + 'discountcost' => '11', + 'allowcancellations' => '0', + 'sessiondates' => [ + ['timestart' => $now + 3 * DAYSECS, 'timefinish' => $now + 4 * DAYSECS], + ], + ]; + $session1 = $generator->create_session($record); + $session2 = $generator->create_session($record); + $session3 = $generator->create_session($record); + + // Signup student to session3 to test already signed up validating. + facetoface_user_signup($session3, $facetoface, $course, '', + MDL_F2F_TEXT, MDL_F2F_STATUS_BOOKED, $student->id); + + $bm = new booking_manager($facetoface->id); + + $records = [ + // Test user can be added to session 1. + (object) [ + 'email' => $student->email, + 'session' => $session1->id, + 'status' => '', + 'notificationtype' => '', + 'discountcode' => '', + ], + // Test user can be added to session 2. + (object) [ + 'email' => $student->email, + 'session' => $session2->id, + 'status' => '', + 'notificationtype' => '', + 'discountcode' => '', + ], + ]; + + $bm->load_from_array($records); + $errors = $bm->validate(); + + $this->assertEmpty($errors); + $this->assertTrue($bm->process()); + + $sessions = facetoface_get_user_submissions($facetoface->id, $student->id); + $this->assertCount(3, $sessions); + $sessionids = array_column($sessions, 'sessionid'); + $this->assertContains($session1->id, $sessionids); + $this->assertContains($session2->id, $sessionids); + $this->assertContains($session3->id, $sessionids); + } } diff --git a/version.php b/version.php index e34faf9..a51d252 100644 --- a/version.php +++ b/version.php @@ -30,8 +30,8 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024101100; -$plugin->release = 2024101100; +$plugin->version = 2024101101; +$plugin->release = 2024101101; $plugin->requires = 2023100900; // Requires 4.3. $plugin->component = 'mod_facetoface'; $plugin->maturity = MATURITY_STABLE;