Skip to content

Commit 090f87f

Browse files
authored
Merge pull request #271 from catalyst/269-upload-handle-headers
Upload changes
2 parents f73ecd1 + e54805c commit 090f87f

File tree

5 files changed

+331
-39
lines changed

5 files changed

+331
-39
lines changed

classes/booking_manager.php

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

classes/form/upload_bookings_form.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use moodle_url;
2020
use html_writer;
21+
use mod_facetoface\booking_manager;
2122

2223
defined('MOODLE_INTERNAL') || die();
2324
require_once($CFG->dirroot . '/repository/lib.php');
@@ -59,11 +60,14 @@ public function definition() {
5960
$mform->setType('csvfile', PARAM_INT);
6061
$mform->addRule('csvfile', get_string('required'), 'required', null, 'client');
6162

63+
// Allowed statuses minus the '' one, as we have additional help text to explain '' status specifically.
64+
$allowedstatuses = array_filter(booking_manager::get_allowed_session_statuses());
65+
$help = nl2br(get_string('facetoface:uploadbookingsfiledesc', 'mod_facetoface', implode(', ', $allowedstatuses)));
6266
$mform->addElement(
6367
'static',
6468
'csvuploadhelp',
6569
'',
66-
nl2br(get_string('facetoface:uploadbookingsfiledesc', 'mod_facetoface'))
70+
$help,
6771
);
6872

6973
$mform->addElement('advcheckbox', 'caseinsensitive', get_string('caseinsensitive', 'mod_facetoface'));

lang/en/facetoface.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,12 @@
149149
$string['enrolled'] = 'enrolled';
150150
$string['error:addalreadysignedupattendee'] = '{$a} is already signed-up for this Face-to-Face activity.';
151151
$string['error:addattendee'] = 'Could not add {$a} to the session.';
152+
$string['error:bookingsuploadfileduplicateheaders'] = 'Uploaded file had duplicate headers';
153+
$string['error:bookingsuploadfileempty'] = 'Uploaded bookings file was empty';
152154
$string['error:bookingsuploadfileerrorsfound'] = '{$a} errors were found in the uploaded file. Bookings cannot be processed until they are resolved.';
153155
$string['error:bookingsuploadfileheaderfieldmismatch'] = 'Mismatched number of fields in the uploaded file on row {$a}.';
156+
$string['error:bookingsuploadfilemissingrequiredheader'] = 'Uploaded file is missing required header(s): {$a}';
157+
$string['error:bookingsuploadinvalidheaders'] = 'Invalid headers found in upload file: {$a}';
154158
$string['error:cancelbooking'] = 'There was a problem cancelling your booking';
155159
$string['error:cancellationsnotallowed'] = 'You are not allowed to cancel this sign-up.';
156160
$string['error:cancellationtooclose'] = 'You are not allowed to cancel this sign-up. Bookings can only be cancelled {$a} before session.';
@@ -241,13 +245,18 @@
241245
$string['facetoface:uploadandpreview'] = 'Upload and validate bookings';
242246
$string['facetoface:uploadbookings'] = 'Upload bookings';
243247
$string['facetoface:uploadbookingsfile'] = 'Bookings file';
244-
$string['facetoface:uploadbookingsfiledesc'] = "
248+
$string['facetoface:uploadbookingsfiledesc'] = '
245249
Fields expected:
246250
- Email address (required)
247251
- Session number (required)
252+
- Status (optional - one of: {$a}). If not given, the default is \'booked\'.
248253
- Discount code (optional)
249-
- Notification type (optional - valid options are 'email', 'ical', or 'both')
250-
";
254+
- Notification type (optional - valid options are \'email\', \'ical\', or \'both\')
255+
256+
Note:
257+
- If you directly assign an attendance status (i.e. a status other than \'booked\' or \'waitlisted\') but the user has not already been signed up, they will automatically be signed up using the details provided on that row (e.g. calendar, discountcode).
258+
- Attendance can only be taken for current or past sessions. Even if you specify a attendance status here, it will be ignored (however, the above note still applies).
259+
';
251260
$string['facetoface:uploadreadytoprocess'] = 'Uploaded file has been validated and ready to be processed.';
252261
$string['facetoface:validatebookings'] = 'Validate bookings';
253262
$string['facetoface:view'] = 'View Face-to-Face activities and sessions';

lib.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2042,7 +2042,7 @@ function facetoface_user_signup(
20422042
$notificationtype,
20432043
$statuscode,
20442044
$userid = false,
2045-
$notifyuser = true
2045+
$notifyuser = true,
20462046
) {
20472047

20482048
global $CFG, $DB;

0 commit comments

Comments
 (0)