@@ -110,19 +110,44 @@ public function load_from_array(array $records) {
110110 }
111111
112112 /**
113- * Get the headers for the records.
114- * @return array
113+ * Get the required headers for the records.
114+ * @return array string array
115115 */
116- public static function get_headers (): array {
116+ public static function get_required_headers (): array {
117117 return [
118118 'email ' ,
119119 'session ' ,
120+ ];
121+ }
122+
123+ /**
124+ * Get the optional headers for the records.
125+ * @return array string array
126+ */
127+ public static function get_optional_headers (): array {
128+ return [
120129 'status ' ,
121130 'discountcode ' ,
122131 'notificationtype ' ,
123132 ];
124133 }
125134
135+ /**
136+ * Get statuses allowed to be used in CSV upload.
137+ * @return array status string names
138+ */
139+ public static function get_allowed_session_statuses (): array {
140+ return [
141+ 'waitlisted ' ,
142+ 'booked ' ,
143+ 'partially_attended ' ,
144+ 'fully_attended ' ,
145+ 'no_show ' ,
146+ 'cancelled ' ,
147+ '' , // Defaults to booked or waitlisted (depending on session times).
148+ ];
149+ }
150+
126151 /**
127152 * Get an iterator for the records.
128153 * @return Generator
@@ -138,20 +163,52 @@ private function get_iterator(): \Generator {
138163 $ handle = $ this ->file ->get_content_file_handle ();
139164 $ maxlinelength = 1000 ;
140165 $ delimiter = ', ' ;
141- $ rownumber = 1 ; // First row is headers.
142- $ headers = self ::get_headers ();
143- $ numheaders = count ($ headers );
144- fgets ($ handle ); // Move pointer past first line (headers).
166+ $ rownumber = 0 ;
145167 try {
168+ $ fileheaders = [];
146169 while (($ data = fgetcsv ($ handle , $ maxlinelength , $ delimiter )) !== false ) {
147170 $ rownumber ++;
148- $ numfields = count ($ data );
149- if ($ numfields !== $ numheaders ) {
150- throw new moodle_exception ('error:bookingsuploadfileheaderfieldmismatch ' , 'mod_facetoface ' );
171+
172+ // First row, handle headers.
173+ if ($ rownumber === 1 ) {
174+ // Check for headers that shouldn't be there.
175+ $ validheaders = array_merge (self ::get_required_headers (), self ::get_optional_headers ());
176+ $ invalidheaders = array_filter ($ data , fn ($ fileheader ) => !in_array ($ fileheader , $ validheaders ));
177+ if (!empty ($ invalidheaders )) {
178+ throw new moodle_exception ('error:bookingsuploadfileheaderfieldmismatch ' , 'mod_facetoface ' , '' , implode (', ' , $ invalidheaders ));
179+ }
180+
181+ // Check that all the required headers are there.
182+ $ missingrequired = array_filter (self ::get_required_headers (), fn ($ requiredheader ) => !in_array ($ requiredheader , $ data ));
183+ if (!empty ($ missingrequired )) {
184+ throw new moodle_exception ('error:bookingsuploadfilemissingrequiredheader ' , 'mod_facetoface ' , '' , implode (', ' , $ missingrequired ));
185+ }
186+
187+ // Check for possible duplicate headers (even if they valid headers).
188+ $ hasduplicates = count ($ data ) != count (array_unique ($ data ));
189+ if ($ hasduplicates ) {
190+ throw new moodle_exception ('error:bookingsuploadfileduplicateheaders ' , 'mod_facetoface ' );
191+ }
192+
193+ // Headers ok, store.
194+ $ fileheaders = $ data ;
195+
196+ // Don't yield the headers.
197+ continue ;
151198 }
152- $ record = array_combine ($ headers , $ data );
199+
200+ // Row items count does not match the headers.
201+ if (count ($ data ) !== count ($ fileheaders )) {
202+ throw new moodle_exception ('error:bookingsuploadfileheaderfieldmismatch ' , 'mod_facetoface ' , '' , $ rownumber );
203+ }
204+ $ record = array_combine ($ fileheaders , $ data );
153205 yield (object ) $ record ;
154206 }
207+
208+ // Nothing was processed, the file is empty.
209+ if (empty ($ fileheaders )) {
210+ throw new moodle_exception ('error:bookingsuploadfileempty ' , 'mod_facetoface ' );
211+ }
155212 } finally {
156213 fclose ($ handle );
157214 }
@@ -272,15 +329,7 @@ public function validate($timenow = null): array {
272329 }
273330
274331 // Check to ensure a valid status is set.
275- if (
276- isset ($ entry ->status ) && !in_array (
277- $ entry ->status ,
278- array_merge (facetoface_statuses (), [
279- '' , // Defaults to booked.
280- 'cancelled ' , // Alternative to 'user_cancelled'.
281- ])
282- )
283- ) {
332+ if (isset ($ entry ->status ) && !in_array ($ entry ->status , self ::get_allowed_session_statuses ())) {
284333 $ errors [] = [
285334 $ row ,
286335 new lang_string ('error:invalidstatusspecified ' , 'mod_facetoface ' , $ entry ->status ),
@@ -378,6 +427,8 @@ private function transform_notification_type($type) {
378427 * @throws moodle_exception
379428 */
380429 public function process () {
430+ global $ DB ;
431+
381432 if (!empty ($ this ->validate ())) {
382433 throw new moodle_exception ('error:cannotprocessbookingsvalidationerrorsexist ' , 'facetoface ' );
383434 }
@@ -403,8 +454,14 @@ public function process() {
403454 // Map status to status code.
404455 $ statuscode = array_search ($ entry ->status , facetoface_statuses ()) ?: MDL_F2F_STATUS_BOOKED ;
405456
457+ $ signupstatuses = [MDL_F2F_STATUS_BOOKED , MDL_F2F_STATUS_WAITLISTED ];
458+ $ attendancestatuses = [MDL_F2F_STATUS_NO_SHOW , MDL_F2F_STATUS_PARTIALLY_ATTENDED , MDL_F2F_STATUS_FULLY_ATTENDED ];
459+
460+ $ issignup = in_array ($ statuscode , $ signupstatuses );
461+ $ isattendance = in_array ($ statuscode , $ attendancestatuses );
462+
406463 // Handle signups.
407- if (in_array ( $ statuscode , [ MDL_F2F_STATUS_BOOKED , MDL_F2F_STATUS_WAITLISTED ]) ) {
464+ if ($ issignup ) {
408465 if ($ statuscode === MDL_F2F_STATUS_BOOKED && !$ session ->datetimeknown ) {
409466 // If booked, ensures the status is waitlisted instead, if the datetime is unknown.
410467 $ statuscode = MDL_F2F_STATUS_WAITLISTED ;
@@ -414,25 +471,37 @@ public function process() {
414471 $ session ,
415472 $ this ->facetoface ,
416473 $ this ->course ,
417- $ entry ->discountcode ,
418- $ this ->transform_notification_type ($ entry ->notificationtype ),
474+ $ entry ->discountcode ?? '' ,
475+ $ this ->transform_notification_type ($ entry ->notificationtype ?? '' ),
419476 $ statuscode ,
420477 $ user ->id ,
421478 !$ this ->suppressemail ,
422479 );
423-
424- continue ;
425480 }
426481
427482 // Handle attendance.
428- if (
429- in_array ($ statuscode , [
430- MDL_F2F_STATUS_NO_SHOW ,
431- MDL_F2F_STATUS_PARTIALLY_ATTENDED ,
432- MDL_F2F_STATUS_FULLY_ATTENDED ,
433- ])
434- ) {
483+ if ($ isattendance ) {
484+ // If booking into attendance but the user hasn't been signed up yet (e.g. directly marking as attended),
485+ // then sign the user up.
486+ $ alreadysignedup = $ DB ->record_exists ('facetoface_signups ' , ['sessionid ' => $ session ->id , 'userid ' => $ user ->id ]);
487+ if (!$ alreadysignedup ) {
488+ // We use booked/waitlisted for making the signup,
489+ // and then the actual status from the CSV when taking attendance.
490+ $ signupstatus = $ session ->datetimeknown ? MDL_F2F_STATUS_BOOKED : MDL_F2F_STATUS_WAITLISTED ;
491+ facetoface_user_signup (
492+ $ session ,
493+ $ this ->facetoface ,
494+ $ this ->course ,
495+ $ entry ->discountcode ?? '' ,
496+ $ this ->transform_notification_type ($ entry ->notificationtype ?? '' ),
497+ $ signupstatus ,
498+ $ user ->id ,
499+ !$ this ->suppressemail ,
500+ );
501+ }
502+
435503 $ attendees = facetoface_get_attendees ($ session ->id );
504+
436505 // Get matching attendee.
437506 foreach ($ attendees as $ attendee ) {
438507 if ($ attendee ->email === $ entry ->email ) {
@@ -445,8 +514,6 @@ public function process() {
445514 'submissionid_ ' . $ attendee ->submissionid => $ statuscode ,
446515 ];
447516 facetoface_take_attendance ($ data );
448-
449- continue ;
450517 }
451518 }
452519 }
0 commit comments