';
+$string['email_slotopenedup_plain'] = 'An appointment has become available for a slot on {$a->date} at {$a->time} with the {$a->staffrole} {$a->attendant}.
+
+This is regarding the activity titled "{$a->module}", in the course "{$a->course_short}: {$a->course}" on the website "{$a->site}".';
+$string['email_slotopenedup_subject'] = '{$a->course_short}: A slot became available';
+
$string['email_reminder_html'] = '
You have an upcoming appointment on {$a->date}
from {$a->time} to {$a->endtime}
with {$a->attendant}.
diff --git a/version.php b/version.php
index 0bdf3505..d144339c 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
*/
$plugin->component = 'mod_scheduler'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2023050806; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2023050808; // The current module version (Date: YYYYMMDDXX).
$plugin->release = '4.x dev'; // Human-friendly version name.
$plugin->requires = 2022041900; // Requires Moodle 4.0.
$plugin->maturity = MATURITY_ALPHA; // Development release - not for production use.
From 2afc761b3a9346dce84c6f02cd9de39f166db338 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Thu, 6 Feb 2020 19:11:23 +0800
Subject: [PATCH 09/29] Update the privacy API provider to include watchers
---
classes/model/scheduler.php | 24 +++++++++++--
classes/privacy/provider.php | 68 ++++++++++++++++++++++++++++++------
lang/en/scheduler.php | 4 +++
renderer.php | 4 ++-
tests/generator/lib.php | 7 ++++
tests/privacy_test.php | 19 +++++++++-
6 files changed, 110 insertions(+), 16 deletions(-)
diff --git a/classes/model/scheduler.php b/classes/model/scheduler.php
index e88cfa26..88de2022 100644
--- a/classes/model/scheduler.php
+++ b/classes/model/scheduler.php
@@ -147,6 +147,24 @@ public function delete_all_slots() {
scheduler_grade_item_update($this, 'reset');
}
+ /**
+ * Delete a student's watchlist.
+ *
+ * @param int $userid The user ID.
+ * @return void
+ */
+ public function delete_student_watchlist($userid) {
+ global $DB;
+ $sql = "DELETE FROM {scheduler_watcher}
+ WHERE userid = :userid
+ AND slotid IN (
+ SELECT id
+ FROM {scheduler_slots}
+ WHERE schedulerid = :schedulerid)";
+ $params = ['schedulerid' => $this->data->id, 'userid' => $userid];
+ $DB->execute($sql, $params);
+ }
+
/**
* Retrieve the course module id of this scheduler
*
@@ -1335,9 +1353,9 @@ public static function free_obsolete_watchers() {
global $DB;
$sql = "DELETE FROM {scheduler_watcher}
WHERE slotid IN (
- SELECT s.id
- FROM {scheduler_slots} s
- WHERE s.starttime <= :starttime)";
+ SELECT id
+ FROM {scheduler_slots}
+ WHERE starttime <= :starttime)";
$params = ['starttime' => time() - DAYSECS];
$DB->execute($sql, $params);
}
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index 490de14f..99c42d67 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -91,6 +91,15 @@ public static function get_metadata(collection $collection) : collection {
'privacy:metadata:scheduler_appointment'
);
+ $collection->add_database_table(
+ 'scheduler_watcher',
+ [
+ 'userid' => 'privacy:metadata:scheduler_watcher:userid',
+ 'slotid' => 'privacy:metadata:scheduler_watcher:slotid',
+ ],
+ 'privacy:metadata:scheduler_watcher'
+ );
+
// Subsystems used.
$collection->link_subsystem('core_files', 'privacy:metadata:filepurpose');
@@ -130,13 +139,16 @@ public static function get_contexts_for_userid(int $userid) : contextlist {
INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
INNER JOIN {scheduler} s ON s.id = cm.instance
INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id
- INNER JOIN {scheduler_appointment} a ON a.slotid = t.id
- WHERE a.studentid = :userid";
+ LEFT JOIN {scheduler_appointment} a ON a.slotid = t.id AND a.studentid = :userid1
+ LEFT JOIN {scheduler_watcher} w ON w.slotid = t.id AND w.userid = :userid2
+ WHERE a.id IS NOT NULL
+ OR w.id IS NOT NULL";
$params = [
'modname' => 'scheduler',
'contextlevel' => CONTEXT_MODULE,
- 'userid' => $userid
+ 'userid1' => $userid,
+ 'userid2' => $userid
];
$contextlist->add_from_sql($sql, $params);
@@ -188,6 +200,20 @@ public static function get_users_in_context(userlist $userlist) {
$userlist->add_from_sql('studentid', $sql, $params);
+ // Fetch watchers.
+ $sql = "SELECT w.userid
+ FROM {course_modules} cm
+ INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
+ INNER JOIN {scheduler} s ON s.id = cm.instance
+ INNER JOIN {scheduler_slots} t ON t.schedulerid = s.id
+ INNER JOIN {scheduler_watcher} w ON w.slotid = t.id
+ WHERE cm.id = :cmid";
+ $params = [
+ 'modname' => 'scheduler',
+ 'cmid'=> $context->instanceid
+ ];
+ $userlist->add_from_sql('userid', $sql, $params);
+
return $userlist;
}
@@ -232,8 +258,6 @@ public static function export_user_data(approved_contextlist $contextlist) {
return;
}
- self::$renderer = new \mod_scheduler_renderer();
-
$user = $contextlist->get_user();
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
@@ -244,18 +268,21 @@ public static function export_user_data(approved_contextlist $contextlist) {
a.studentid, a.attended, a.grade,
a.appointmentnote, a.appointmentnoteformat,
a.teachernote, a.teachernoteformat,
- a.studentnote, a.studentnoteformat
+ a.studentnote, a.studentnoteformat,
+ w.id AS iswatcher
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modname
JOIN {scheduler} s ON s.id = cm.instance
JOIN {scheduler_slots} t ON t.schedulerid = s.id
- JOIN {scheduler_appointment} a ON a.slotid = t.id
+ LEFT JOIN {scheduler_appointment} a ON a.slotid = t.id AND a.studentid =:userid2
+ LEFT JOIN {scheduler_watcher} w ON w.slotid = t.id AND w.userid = :userid3
WHERE ctx.id {$contextsql} AND ctx.contextlevel = :contextlevel
- AND t.teacherid = :userid1 OR a.studentid = :userid2
+ AND t.teacherid = :userid1
+ OR (a.id IS NOT NULL OR w.id IS NOT NULL)
ORDER BY cm.id, t.id, a.id";
$rs = $DB->get_recordset_sql($sql, $contextparams + ['contextlevel' => CONTEXT_MODULE,
- 'modname' => 'scheduler', 'userid1' => $user->id, 'userid2' => $user->id]);
+ 'modname' => 'scheduler', 'userid1' => $user->id, 'userid2' => $user->id, 'userid3' => $user->id]);
$context = null;
$lastrow = null;
@@ -330,6 +357,7 @@ protected static function export_slot($context, $user, $record) {
'notes' => self::format_note($record->notes, $record->notesformat,
'slotnote', $record->slotid, $context, $wrc, $slotarea),
'exclusivity' => $record->exclusivity,
+ 'watching_slot' => transform::yesno(!empty($record->iswatcher))
];
// Data about the slot.
@@ -346,7 +374,7 @@ protected static function export_slot($context, $user, $record) {
* @param \stdClass $record
*/
protected static function export_appointment($context, $scheduler, $user, $record) {
- if (!$record) {
+ if (!$record || empty($record->appointmentid)) {
return;
}
$wrc = writer::with_context($context);
@@ -358,7 +386,7 @@ protected static function export_appointment($context, $scheduler, $user, $recor
$data = [
'studentid' => transform::user($record->studentid),
'attended' => transform::yesno($record->attended),
- 'grade' => self::$renderer->format_grade($scheduler, $record->grade),
+ 'grade' => self::get_renderer()->format_grade($scheduler, $record->grade),
'appointmentnote' => self::format_note($record->appointmentnote, $record->appointmentnoteformat,
'appointmentnote', $record->appointmentid, $context, $wrc, $apparea),
'studentnote' => self::format_note($record->studentnote, $record->studentnoteformat,
@@ -432,6 +460,7 @@ public static function delete_data_for_user(approved_contextlist $contextlist) {
foreach ($apps as $app) {
$app->delete();
}
+ $scheduler->delete_student_watchlist($user->id);
}
}
}
@@ -456,8 +485,25 @@ public static function delete_data_for_users(approved_userlist $userlist) {
foreach ($apps as $app) {
$app->delete();
}
+ $scheduler->delete_student_watchlist($userid);
}
}
}
+ /**
+ * Get our renderer.
+ *
+ * We must include the file directly because autoloading does not work
+ * for classes placed at the root the plugin.
+ *
+ * @return \mod_scheduler_renderer
+ */
+ private static function get_renderer() {
+ global $CFG;
+ require_once($CFG->dirroot . '/mod/scheduler/renderer.php');
+ if (!isset(self::$renderer)) {
+ self::$renderer = new \mod_scheduler_renderer();
+ }
+ return self::$renderer;
+ }
}
diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php
index 987b11cc..a13174f7 100644
--- a/lang/en/scheduler.php
+++ b/lang/en/scheduler.php
@@ -96,6 +96,10 @@
$string['privacy:metadata:scheduler_appointment:studentnote'] = "Note by student";
$string['privacy:metadata:scheduler_appointment:studentnoteformat'] = "Format of student note";
+$string['privacy:metadata:scheduler_watcher'] = "Represents a student watching a slot";
+$string['privacy:metadata:scheduler_watcher:userid'] = "Student who watching the slot";
+$string['privacy:metadata:scheduler_watcher:slotid'] = "The slot being watched";
+
$string['privacy:metadata:filepurpose'] = 'File used in notes for the slot or appointment';
diff --git a/renderer.php b/renderer.php
index 8bc1237a..a45eef81 100644
--- a/renderer.php
+++ b/renderer.php
@@ -696,7 +696,9 @@ public function render_scheduler_slot_booker(scheduler_slot_booker $booker) {
$rowdata[] = $this->render($button);
} else if ($slot->canwatch) {
$what = $slot->iswatching ? 'unwatchslot' : 'watchslot';
- $bookurl = new moodle_url($booker->actionurl, ['slotid' => $slot->slotid, 'what' => $what]);
+ $bookurl = new moodle_url($booker->actionurl, ['slotid' => $slot->slotid,
+ 'what' => $what,
+ ]);
$button = new single_button($bookurl, get_string($what, 'mod_scheduler'));
$rowdata[] = $this->render($button);
} else {
diff --git a/tests/generator/lib.php b/tests/generator/lib.php
index e1276424..124c6969 100644
--- a/tests/generator/lib.php
+++ b/tests/generator/lib.php
@@ -114,6 +114,13 @@ public function create_instance($record = null, array $options = null) {
$appointmentid = $DB->insert_record('scheduler_appointment', $appointment);
}
}
+
+ if (isset($options['slotwatchers'][$slotkey])) {
+ $userids = (array) $options['slotwatchers'][$slotkey];
+ foreach ($userids as $userid) {
+ $DB->insert_record('scheduler_watcher', (object) ['userid' => $userid, 'slotid' => $slotid]);
+ }
+ }
}
}
diff --git a/tests/privacy_test.php b/tests/privacy_test.php
index dd4abf4e..0c0c5969 100644
--- a/tests/privacy_test.php
+++ b/tests/privacy_test.php
@@ -97,11 +97,13 @@ protected function setUp(): void {
$this->student1 = $this->getDataGenerator()->create_user();
$this->student2 = $this->getDataGenerator()->create_user();
- $this->allstudents = [$this->student1->id, $this->student2->id];
+ $this->student3 = $this->getDataGenerator()->create_user();
+ $this->allstudents = [$this->student1->id, $this->student2->id, $this->student3->id];
$options = array();
$options['slottimes'] = array();
$options['slotstudents'] = array();
+ $options['slotwatchers'] = [];
for ($c = 0; $c < 4; $c++) {
$options['slottimes'][$c] = time() + ($c + 1) * DAYSECS;
$stud = $this->getDataGenerator()->create_user()->id;
@@ -114,6 +116,8 @@ protected function setUp(): void {
$this->student1->id,
$this->student2->id
);
+ $options['slotwatchers'][4] = [$this->student1->id];
+ $options['slotwatchers'][0] = [$this->student3->id];
$scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options);
$coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid));
@@ -157,6 +161,10 @@ public function test_get_contexts_for_userid() {
// Get contexts for the first user.
$contextids = provider::get_contexts_for_userid($this->student1->id)->get_contextids();
$this->assertEquals([$this->context->id], $contextids, '', 0.0, 10, true);
+
+ // Get contexts for the watcher user.
+ $contextids = provider::get_contexts_for_userid($this->student3->id)->get_contextids();
+ $this->assertEquals([$this->context->id], $contextids);
}
/**
@@ -219,10 +227,12 @@ public function test_export_user_data1() {
* @covers \mod_scheduler\privacy\provider::delete_data_for_all_users_in_context
*/
public function test_delete_data_for_all_users_in_context() {
+ global $DB;
provider::delete_data_for_all_users_in_context($this->context);
foreach ($this->allstudents as $u) {
$this->assert_appointment_status($this->schedulerid, $u, false);
+ $this->assertFalse($DB->record_exists('scheduler_watcher', ['userid' => $u]));
}
}
@@ -232,12 +242,15 @@ public function test_delete_data_for_all_users_in_context() {
* @covers \mod_scheduler\privacy\provider::delete_data_for_user
*/
public function test_delete_data_for_user() {
+ global $DB;
$appctx = new approved_contextlist($this->student1, 'mod_scheduler', [$this->context->id]);
provider::delete_data_for_user($appctx);
$this->assert_appointment_status($this->schedulerid, $this->student1->id, false);
$this->assert_appointment_status($this->schedulerid, $this->student2->id, true);
+ $this->assertFalse($DB->record_exists('scheduler_watcher', ['userid' => $this->student1->id]));
+ $this->assertTrue($DB->record_exists('scheduler_watcher', ['userid' => $this->student3->id]));
}
/**
@@ -246,6 +259,7 @@ public function test_delete_data_for_user() {
* @covers \mod_scheduler\privacy\provider::delete_data_for_users
*/
public function test_delete_data_for_users() {
+ global $DB;
$component = 'mod_scheduler';
$approveduserids = [$this->student1->id, $this->student2->id];
@@ -254,5 +268,8 @@ public function test_delete_data_for_users() {
$this->assert_appointment_status($this->schedulerid, $this->student1->id, false);
$this->assert_appointment_status($this->schedulerid, $this->student2->id, false);
+
+ $this->assertFalse($DB->record_exists('scheduler_watcher', ['userid' => $this->student1->id]));
+ $this->assertTrue($DB->record_exists('scheduler_watcher', ['userid' => $this->student3->id]));
}
}
From 34b6a9ad4855760c2370f4ab6dce80a4935ecc57 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Fri, 7 Feb 2020 10:32:04 +0800
Subject: [PATCH 10/29] Include watchers in backups containing user information
---
backup/moodle2/backup_scheduler_stepslib.php | 8 ++++++++
backup/moodle2/restore_scheduler_stepslib.php | 18 ++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/backup/moodle2/backup_scheduler_stepslib.php b/backup/moodle2/backup_scheduler_stepslib.php
index 08574dc6..4a7c2942 100644
--- a/backup/moodle2/backup_scheduler_stepslib.php
+++ b/backup/moodle2/backup_scheduler_stepslib.php
@@ -63,13 +63,19 @@ protected function define_structure() {
'appointmentnote', 'appointmentnoteformat', 'teachernote', 'teachernoteformat',
'studentnote', 'studentnoteformat', 'timecreated', 'timemodified'));
+ $watchers = new backup_nested_element('watchers');
+ $watcher = new backup_nested_element('watcher', ['id'], ['slotid', 'userid']);
+
// Build the tree.
$scheduler->add_child($slots);
$slots->add_child($slot);
$slot->add_child($appointments);
+ $slot->add_child($watchers);
+
$appointments->add_child($appointment);
+ $watchers->add_child($watcher);
// Define sources.
$scheduler->set_source_table('scheduler', array('id' => backup::VAR_ACTIVITYID));
@@ -79,6 +85,7 @@ protected function define_structure() {
if ($userinfo) {
$slot->set_source_table('scheduler_slots', array('schedulerid' => backup::VAR_PARENTID));
$appointment->set_source_table('scheduler_appointment', array('slotid' => backup::VAR_PARENTID));
+ $watcher->set_source_table('scheduler_watcher', ['slotid' => backup::VAR_PARENTID]);
}
// Define id annotations.
@@ -87,6 +94,7 @@ protected function define_structure() {
if ($userinfo) {
$slot->annotate_ids('user', 'teacherid');
$appointment->annotate_ids('user', 'studentid');
+ $watcher->annotate_ids('user', 'userid');
}
// Define file annotations.
diff --git a/backup/moodle2/restore_scheduler_stepslib.php b/backup/moodle2/restore_scheduler_stepslib.php
index 39332709..1c130854 100644
--- a/backup/moodle2/restore_scheduler_stepslib.php
+++ b/backup/moodle2/restore_scheduler_stepslib.php
@@ -50,6 +50,7 @@ protected function define_structure() {
$appointment = new restore_path_element('scheduler_appointment',
'/activity/scheduler/slots/slot/appointments/appointment');
$paths[] = $appointment;
+ $paths[] = new restore_path_element('scheduler_watcher', '/activity/scheduler/slots/slot/watchers/watcher');
}
// Return the paths wrapped into standard activity structure.
@@ -133,6 +134,23 @@ protected function process_scheduler_appointment($data) {
$this->set_mapping('scheduler_appointment', $oldid, $newitemid, true);
}
+ /**
+ * Process watcher.
+ *
+ * @param stdClass $data
+ */
+ protected function process_scheduler_watcher($data) {
+ global $DB;
+
+ $data = (object)$data;
+ $oldid = $data->id;
+
+ $data->slotid = $this->get_new_parentid('scheduler_slot');
+ $data->userid = $this->get_mappingid('user', $data->userid);
+
+ $newitemid = $DB->insert_record('scheduler_watcher', $data);
+ }
+
/**
* after_execute
*/
From 30c560c33dafd178d04bf0ac7bd0fe72bbab3cdd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Tue, 11 Feb 2020 21:19:01 +0800
Subject: [PATCH 11/29] Students can view and book slots from the mobile app
---
bookingform.php | 24 +-
classes/external.php | 633 +++++++++++++++++-
classes/model/scheduler.php | 18 +
classes/output/datetime_filter.php | 2 +-
classes/output/mobile.php | 356 ++++++++++
classes/permission/scheduler_permissions.php | 28 +
db/mobile.php | 44 ++
db/services.php | 30 +-
lang/en/scheduler.php | 5 +
locallib.php | 113 ++++
studentview.controller.php | 87 +--
templates/mobile_book_slot.mustache | 63 ++
templates/mobile_noguests.mustache | 8 +
templates/mobile_slot.mustache | 126 ++++
.../mobile_student_bookable_slots.mustache | 38 ++
.../mobile_student_landing_page.mustache | 75 +++
version.php | 2 +-
view.php | 5 +-
18 files changed, 1540 insertions(+), 117 deletions(-)
create mode 100644 classes/output/mobile.php
create mode 100644 db/mobile.php
create mode 100644 templates/mobile_book_slot.mustache
create mode 100644 templates/mobile_noguests.mustache
create mode 100644 templates/mobile_slot.mustache
create mode 100644 templates/mobile_student_bookable_slots.mustache
create mode 100644 templates/mobile_student_landing_page.mustache
diff --git a/bookingform.php b/bookingform.php
index 4ae84b0e..df31baad 100644
--- a/bookingform.php
+++ b/bookingform.php
@@ -74,9 +74,7 @@ protected function definition() {
'context' => $scheduler->get_context(),
'collapsed' => true);
- $this->uploadoptions = array('subdirs' => 0,
- 'maxbytes' => $scheduler->uploadmaxsize,
- 'maxfiles' => $scheduler->uploadmaxfiles);
+ $this->uploadoptions = mod_scheduler_get_student_upload_options($scheduler);
// Text field for student-supplied data.
if ($scheduler->uses_studentnotes()) {
@@ -157,24 +155,4 @@ public function prepare_booking_data(appointment $appointment) {
return $newdata;
}
- /**
- * save_booking_data
- *
- * @param stdClass $formdata
- * @param appointment $appointment
- */
- public function save_booking_data(stdClass $formdata, appointment $appointment) {
- $scheduler = $appointment->get_scheduler();
- if ($scheduler->uses_studentnotes() && isset($formdata->studentnote_editor)) {
- $editor = $formdata->studentnote_editor;
- $appointment->studentnote = $editor['text'];
- $appointment->studentnoteformat = $editor['format'];
- }
- if ($scheduler->uses_studentfiles()) {
- file_save_draft_area_files($formdata->studentfiles, $scheduler->context->id,
- 'mod_scheduler', 'studentfiles', $appointment->id,
- $this->uploadoptions);
- }
- $appointment->save();
- }
}
diff --git a/classes/external.php b/classes/external.php
index d880df67..47cc9d99 100644
--- a/classes/external.php
+++ b/classes/external.php
@@ -29,9 +29,15 @@
use core_user;
use external_api;
use external_function_parameters;
+use extenral_multiple_structure;
+use external_single_structure;
use external_value;
+use moodle_exception;
+use mod_scheduler\model\appointment;
use mod_scheduler\model\scheduler;
+use mod_scheduler\model\slot;
use mod_scheduler\permission\scheduler_permissions;
+use mod_scheduler_renderer as renderer;
use scheduler_messenger;
require_once($CFG->libdir . '/externallib.php');
@@ -52,7 +58,219 @@ class external extends external_api {
*
* @return external_function_parameters
*/
- public function revoke_appointment_parameters() {
+ public static function booking_form_viewed_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT),
+ ]);
+ }
+
+ /**
+ * Trigger booking form viewed event.
+ *
+ * @param int $cmid The cmid.
+ * @return null
+ */
+ public static function booking_form_viewed($cmid) {
+ global $USER;
+
+ $params = self::validate_parameters(self::booking_form_viewed_parameters(), ['cmid' => $cmid]);
+ $cmid = $params['cmid'];
+
+ $scheduler = scheduler::load_by_coursemodule_id($cmid);
+ self::validate_context($scheduler->get_context());
+ $permissions = new scheduler_permissions($scheduler->get_context(), $USER->id);
+
+ $permissions->ensure($permissions->is_student());
+ \mod_scheduler\event\booking_form_viewed::create_from_scheduler($scheduler)->trigger();
+
+ return null;
+ }
+
+ /**
+ * External function return structure.
+ *
+ * @return external_value
+ */
+ public static function booking_form_viewed_returns() {
+ return new external_value(null);
+ }
+
+ /**
+ * External function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function book_slot_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT),
+ 'slotid' => new external_value(PARAM_INT),
+ 'groupid' => new external_value(PARAM_INT, '', VALUE_DEFAULT, 0),
+ 'bookingdata' => new external_single_structure([
+ 'studentnote' => new external_value(PARAM_RAW, '', VALUE_DEFAULT, ''),
+ 'studentnoteformat' => new external_value(PARAM_INT, '', VALUE_DEFAULT, FORMAT_PLAIN),
+ ], '', VALUE_DEFAULT, [])
+ ]);
+ }
+
+ /**
+ * Trigger booking form viewed event.
+ *
+ * @param int $cmid The cmid.
+ * @param int $slotid The slot ID.
+ * @param int $groupid The group ID, if any.
+ * @param array|null $bookingdata The booking data.
+ * @return null
+ */
+ public static function book_slot($cmid, $slotid, $groupid = 0, $bookingdata = []) {
+ global $USER;
+
+ $params = self::validate_parameters(self::book_slot_parameters(), ['cmid' => $cmid, 'slotid' => $slotid,
+ 'groupid' => $groupid, 'bookingdata' => $bookingdata]);
+ $cmid = $params['cmid'];
+ $slotid = $params['slotid'];
+ $groupid = $params['groupid'];
+ $bookingdata = $params['bookingdata'];
+
+ $scheduler = scheduler::load_by_coursemodule_id($cmid);
+ $context = $scheduler->get_context();
+ self::validate_context($context);
+ $permissions = new scheduler_permissions($context, $USER->id);
+
+ $permissions->ensure($permissions->is_student());
+ require_capability('mod/scheduler:appoint', $context);
+ $slot = $scheduler->get_slot($slotid);
+
+ if (!static::is_in_app_booking_supported($scheduler)) {
+ throw new moodle_exception('bookingnotsupported', 'mod_scheduler');
+ } else if ($slot->is_booked_by_student($USER->id)) {
+ throw new moodle_exception('alreadybookedbyyou', 'mod_scheduler');
+ }
+
+ if (empty($bookingdata)) {
+ $bookingdata = ['studentnote' => '', 'studentnoteformat' => FORMAT_PLAIN];
+ }
+ $bookingdata = (object) $bookingdata;
+
+ if ($scheduler->uses_bookingform()) {
+ if ($scheduler->is_studentnotes_required() && static::is_empty($bookingdata->studentnote)) {
+ throw new moodle_exception('studentnotemissing', 'mod_scheduler');
+ }
+ }
+
+ $formdata = (object) [];
+ if ($scheduler->uses_studentnotes()) {
+ $formdata->studentnote_editor = [
+ 'text' => $bookingdata->studentnote,
+ 'format' => $bookingdata->studentnoteformat ?: FORMAT_PLAIN,
+ ];
+ }
+ mod_scheduler_book_slot($scheduler, $slotid, $USER->id, $groupid, $formdata);
+
+ return self::serialize_appointment($scheduler->get_slot($slotid)->get_student_appointment($USER->id));
+ }
+
+ /**
+ * External function return structure.
+ *
+ * @return external_value
+ */
+ public static function book_slot_returns() {
+ return static::appointment_structure();
+ }
+
+ /**
+ * External function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function get_available_slots_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT),
+ 'page' => new external_value(PARAM_INT, '', VALUE_DEFAULT, 1),
+ 'perpage' => new external_value(PARAM_INT, '', VALUE_DEFAULT, 25),
+ ]);
+ }
+
+ /**
+ * Get available slots.
+ *
+ * When the student can watch slots, this will also return slots that are fully booked but
+ * can be watched in order to be notified when they open up.
+ *
+ * @param int $cmid The cmid.
+ * @param int $page The page number.
+ * @param int $perpage The number of items per page.
+ * @return array
+ */
+ public static function get_available_slots($cmid, $page = 1, $perpage = 25) {
+ global $USER;
+
+ $params = self::validate_parameters(self::get_available_slots_parameters(), ['cmid' => $cmid, 'page' => $page]);
+ $cmid = $params['cmid'];
+ $page = max($params['page'], 1);
+ $offset = $page - 1 * $perpage;
+
+ $userid = $USER->id;
+ $scheduler = scheduler::load_by_coursemodule_id($cmid);
+ $context = $scheduler->get_context();
+ self::validate_context($context);
+
+ require_capability('mod/scheduler:viewslots', $context);
+ require_capability('mod/scheduler:appoint', $context);
+ $canbook = has_capability('mod/scheduler:appoint', $context);
+ $canwatch = has_capability('mod/scheduler:watchslots', $context) && $scheduler->is_watching_enabled();
+ $canseeothers = has_capability('mod/scheduler:seeotherstudentsbooking', $context);
+
+ $nobookingsremaining = $scheduler->count_bookable_appointments($userid, false);
+ $canbookslots = $canbook && $nobookingsremaining != 0;
+ $canwatchslots = $canwatch && $canbookslots;
+ $bookableslots = [];
+ $totalbookableslots = 0;
+
+ if ($canbookslots) {
+ $bookableslotsraw = array_values($scheduler->get_slots_available_to_student($userid, $canwatchslots));
+ $totalbookableslots = count($bookableslotsraw);
+ $bookableslots = array_map(function($slot) use ($canbookslots, $canwatchslots, $canseeothers) {
+ return static::serialize_slot($slot, $canbookslots, $canwatchslots, $canseeothers);
+ }, array_slice($bookableslotsraw, ($page - 1) * $perpage, $perpage));
+ }
+
+ $hasnextpage = $page * $perpage < $totalbookableslots;
+
+ return [
+ 'bookingsremaining' => $nobookingsremaining,
+ 'canbookslots' => $canbookslots,
+ 'canwatchslots' => $canwatchslots,
+ 'hasnextpage' => $hasnextpage,
+ 'page' => $page,
+ 'slots' => $bookableslots,
+ 'total' => $totalbookableslots,
+ ];
+ }
+
+ /**
+ * External function return structure.
+ *
+ * @return external_value
+ */
+ public static function get_available_slots_returns() {
+ return new external_single_structure([
+ 'bookingsremaining' => new external_value(PARAM_INT, 'The number of bookings remaining, -1 means unlimited.'),
+ 'canbookslots' => new external_value(PARAM_BOOL, 'Whether the user can book additional slots.'),
+ 'canwatchslots' => new external_value(PARAM_BOOL, 'Whether the user can watch additional slots.'),
+ 'hasnextpage' => new external_value(PARAM_BOOL, 'Whether we can browse another page.'),
+ 'page' => new external_value(PARAM_INT, 'The current page number.'),
+ 'slots' => new extenral_multiple_structure(static::slot_structure(), 'The slots'),
+ 'total' => new external_value(PARAM_INT, 'The total number of slots in the set.'),
+ ]);
+ }
+
+ /**
+ * External function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function revoke_appointment_parameters() {
return new external_function_parameters([
'cmid' => new external_value(PARAM_INT),
'appointmentid' => new external_value(PARAM_INT),
@@ -66,7 +284,7 @@ public function revoke_appointment_parameters() {
* @param int $appointmentid The appointment ID.
* @return null
*/
- public function revoke_appointment($cmid, $appointmentid) {
+ public static function revoke_appointment($cmid, $appointmentid) {
global $USER;
$params = self::validate_parameters(self::revoke_appointment_parameters(),
@@ -93,7 +311,7 @@ public function revoke_appointment($cmid, $appointmentid) {
$slot->save();
- return null;
+ return true;
}
/**
@@ -101,8 +319,413 @@ public function revoke_appointment($cmid, $appointmentid) {
*
* @return external_value
*/
- public function revoke_appointment_returns() {
- return new external_value(null);
+ public static function revoke_appointment_returns() {
+ return new external_value(PARAM_BOOL);
+ }
+
+ /**
+ * External function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function watch_slot_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT),
+ 'slotid' => new external_value(PARAM_INT),
+ ]);
}
+ /**
+ * Watch a slot.
+ *
+ * @param int $cmid The cmid.
+ * @param int $slotid The slot ID..
+ * @return true
+ */
+ public static function watch_slot($cmid, $slotid) {
+ global $USER;
+
+ $params = self::validate_parameters(self::watch_slot_parameters(), ['cmid' => $cmid, 'slotid' => $slotid]);
+ $cmid = $params['cmid'];
+ $slotid = $params['slotid'];
+
+ $scheduler = scheduler::load_by_coursemodule_id($cmid);
+ $context = $scheduler->get_context();
+ self::validate_context($context);
+ $permissions = new scheduler_permissions($context, $USER->id);
+
+ $permissions->ensure($permissions->is_student());
+ require_capability('mod/scheduler:watchslots', $context);
+
+ if (!$scheduler->is_watching_enabled()) {
+ throw new moodle_exception('error');
+ }
+
+ $slot = $scheduler->get_slot($slotid);
+ if (!$slot) {
+ throw new moodle_exception('error');
+ } else if (!$slot->is_watchable_by_student($USER->id)) {
+ throw new moodle_exception('nopermissions');
+ }
+
+ $watcher = $slot->add_watcher($USER->id);
+ if ($watcher) {
+ \mod_scheduler\event\slot_watched::create_from_watcher($watcher)->trigger();
+ }
+
+ return true;
+ }
+
+ /**
+ * External function return structure.
+ *
+ * @return external_value
+ */
+ public static function watch_slot_returns() {
+ return new external_value(PARAM_BOOL);
+ }
+
+ /**
+ * External function parameters.
+ *
+ * @return external_function_parameters
+ */
+ public static function unwatch_slot_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT),
+ 'slotid' => new external_value(PARAM_INT),
+ ]);
+ }
+
+ /**
+ * Watch a slot.
+ *
+ * @param int $cmid The cmid.
+ * @param int $slotid The slot ID..
+ * @return true
+ */
+ public static function unwatch_slot($cmid, $slotid) {
+ global $USER;
+
+ $params = self::validate_parameters(self::unwatch_slot_parameters(), ['cmid' => $cmid, 'slotid' => $slotid]);
+ $cmid = $params['cmid'];
+ $slotid = $params['slotid'];
+
+ $scheduler = scheduler::load_by_coursemodule_id($cmid);
+ $context = $scheduler->get_context();
+ self::validate_context($context);
+ $permissions = new scheduler_permissions($context, $USER->id);
+
+ $permissions->ensure($permissions->is_student());
+ require_capability('mod/scheduler:watchslots', $context);
+
+ $slot = $scheduler->get_slot($slotid);
+ if (!$slot) {
+ throw new moodle_exception('error');
+ }
+
+ $watcher = $slot->remove_watcher($USER->id);
+ if ($watcher) {
+ \mod_scheduler\event\slot_unwatched::create_from_watcher($watcher)->trigger();
+ }
+
+ return true;
+ }
+
+ /**
+ * External function return structure.
+ *
+ * @return external_value
+ */
+ public static function unwatch_slot_returns() {
+ return new external_value(PARAM_BOOL);
+ }
+
+ /**
+ * Serialize an appointment.
+ *
+ * @param appointment $app The appointment.
+ * @param bool $includeteachernote Whether to include the teacher's note.
+ * @return array
+ */
+ public static function serialize_appointment(appointment $app, $includeteachernote = false, $renderer = null) {
+ global $PAGE;
+
+ $context = $app->get_scheduler()->get_context();
+ $renderer = $renderer ?: $PAGE->get_renderer('mod_scheduler');
+
+ $teachernote = null;
+ $teachernoteformat = null;
+ $teachernoteformatted = null;
+
+ if ($includeteachernote) {
+ $teachernote = $app->teachernote;
+ $teachernoteformat = $app->teachernoteformat;
+ $teachernoteformatted = external_format_text($app->appointmentnote, $app->appointmentnoteformat, $context->id,
+ 'mod_scheduler', 'teachernote', $app->id)[0];
+ }
+
+ return [
+ 'id' => $app->id,
+
+ 'appointmentnote' => $app->appointmentnote,
+ 'appointmentnoteformat' => $app->appointmentnoteformat,
+ 'appointmentnoteformatted' => external_format_text($app->appointmentnote, $app->appointmentnoteformat, $context->id,
+ 'mod_scheduler', 'appointmentnote', $app->id)[0],
+ 'hasappointmentnote' => !static::is_empty($app->appointmentnote),
+
+ 'gradeformatted' => $renderer->format_grade($app->get_scheduler(), $app->grade),
+ 'hasgrade' => $app->grade !== null,
+ 'isattended' => $app->attended,
+
+ 'studentnote' => $app->studentnote,
+ 'studentnoteformat' => $app->studentnoteformat,
+ 'studentnoteformatted' => external_format_text($app->studentnote, $app->studentnoteformat, $context->id)[0],
+ 'hasstudentnote' => !static::is_empty($app->studentnote),
+
+ 'teachernote' => $teachernote,
+ 'teachernoteformat' => $teachernoteformat,
+ 'teachernoteformatted' => $teachernoteformatted,
+ 'hasteachernote' => !static::is_empty($teachernote),
+
+ 'student' => static::serialize_user($app->student)
+ ];
+ }
+
+ /**
+ * Get appointment structure.
+ *
+ * @return external_value
+ */
+ protected static function appointment_structure() {
+ return new external_single_structure([
+ 'id' => new external_value(PARAM_INT),
+
+ 'appointmentnote' => new external_value(PARAM_RAW, 'Notes for the student'),
+ 'appointmentnoteformat' => new external_value(PARAM_INT),
+ 'appointmentnoteformatted' => new external_value(PARAM_RAW),
+ 'hasappointmentnote' => new external_value(PARAM_BOOL),
+
+ 'gradeformatted' => new external_value(PARAM_RAW, 'The grade formatted'),
+ 'hasgrade' => new external_value(PARAM_BOOL),
+ 'isattended' => new external_value(PARAM_BOOL),
+
+ 'studentnote' => new external_value(PARAM_RAW, 'Notes by the student.'),
+ 'studentnoteformat' => new external_value(PARAM_INT),
+ 'studentnoteformatted' => new external_value(PARAM_RAW),
+ 'hasstudentnote' => new external_value(PARAM_BOOL),
+
+ 'teachernote' => new external_value(PARAM_RAW, 'Notes for the teacher, hidden to student', VALUE_DEFAULT, null),
+ 'teachernoteformat' => new external_value(PARAM_INT, '', VALUE_DEFAULT, null),
+ 'teachernoteformatted' => new external_value(PARAM_RAW, '', VALUE_DEFAULT, null),
+ 'hasteachernote' => new external_value(PARAM_BOOL),
+
+ 'student' => static::user_structure(),
+ ]);
+ }
+
+ /**
+ * Serialize a scheduler.
+ *
+ * @param scheduler $scheduler The scheduler.
+ * @return array
+ */
+ public static function serialize_scheduler(scheduler $scheduler) {
+ $context = $scheduler->get_context();
+
+ $data = (object) [
+ 'id' => $scheduler->get_id(),
+ 'courseid' => $scheduler->get_courseid(),
+ 'cmid' => $scheduler->get_cmid(),
+ 'name' => external_format_string($scheduler->name, $context),
+
+ 'bookinginstructions' => $scheduler->bookinginstructions,
+ 'bookinginstructionsformat' => $scheduler->bookinginstructionsformat,
+ 'hasbookinginstructions' => $scheduler->has_bookinginstructions(),
+
+ 'intro' => $scheduler->intro,
+ 'introformat' => $scheduler->introformat,
+ 'hasintro' => !static::is_empty($scheduler->intro),
+
+ 'isinappbookingsupported' => static::is_in_app_booking_supported($scheduler),
+ 'isindividualbookingenabled' => $scheduler->is_individual_scheduling_enabled(),
+ 'isgroupbookingenabled' => $scheduler->is_group_scheduling_enabled(),
+
+ 'teachername' => $scheduler->get_teacher_name(),
+ 'usesbookingform' => $scheduler->uses_bookingform(),
+ 'usesgrades' => $scheduler->uses_grades(),
+ 'usesstudentnotes' => $scheduler->uses_studentnotes(),
+ ];
+
+ list($data->introformatted, $unused) = external_format_text($scheduler->intro, $scheduler->introformat,
+ $context->id, 'mod_scheduler', 'intro');
+
+ list($data->bookinginstructionsformatted, $unused) = external_format_text($scheduler->bookinginstructions,
+ $scheduler->bookinginstructionsformat, $context->id, 'mod_scheduler', 'bookinginstructions', 0);
+
+ return (array) $data;
+ }
+
+ /**
+ * Serialize a slot.
+ *
+ * @param slot $slot The slot.
+ * @return array
+ */
+ public static function serialize_slot(slot $slot) {
+ global $USER;
+
+ $context = $slot->get_scheduler()->get_context();
+ $nremaining = $slot->count_remaining_appointments();
+
+ $canbookslots = has_capability('mod/scheduler:appoint', $context);
+ $canwatchslots = has_capability('mod/scheduler:appoint', $context) && $slot->get_scheduler()->is_watching_enabled();
+ $canseeothers = has_capability('mod/scheduler:seeotherstudentsbooking', $context);
+
+ $canbookslot = $canbookslots && $nremaining != 0 && $slot->is_in_bookable_period();
+ $canwatchslot = $canwatchslots && $slot->is_watchable_by_student($USER->id);
+ $iswatching = $canwatchslot && $slot->is_watched_by_student($USER->id);
+
+ return [
+ 'id' => $slot->id,
+
+ 'starttime' => $slot->starttime,
+ 'duration' => $slot->duration,
+ 'timeformatted' => renderer::slotdatetime($slot->starttime, $slot->duration),
+
+ 'notes' => $slot->notes,
+ 'notesformat' => $slot->notesformat,
+ 'notesformatted' => external_format_text($slot->notes, $slot->notesformat, $context->id,
+ 'mod_scheduler', 'slotnote', $slot->id)[0],
+ 'hasnotes' => !static::is_empty($slot->notes),
+
+ 'appointments' => array_map(function($app) {
+ return static::serialize_appointment($app);
+ }, $slot->get_appointments($canseeothers ? [$USER->id] : null)),
+ 'appointmentlocation' => $slot->appointmentlocation,
+ 'hasappointmentlocation' => !static::is_empty($slot->appointmentlocation),
+
+ 'canbookslot' => $canbookslot,
+ 'canwatchslot' => $canwatchslot,
+ 'iswatching' => $iswatching,
+
+ 'isunlimited' => $slot->exclusivity == 0,
+ 'isexclusive' => $slot->exclusivity == 1,
+ 'isgroupallowed' => !$slot->exclusivity || $slot->exclusivity >= 1,
+ 'isfull' => $nremaining == 0,
+
+ 'nremaining' => $nremaining,
+ 'ntaken' => $slot->exclusivity > 0 ? $slot->exclusivity - $nremaining : 0,
+ 'maxappointments' => $slot->exclusivity,
+
+ 'teacher' => static::serialize_user($slot->teacher)
+ ];
+ }
+
+ /**
+ * Get slot structure.
+ *
+ * @return external_value
+ */
+ protected static function slot_structure() {
+ return new external_single_structure([
+ 'id' => new external_value(PARAM_INT),
+
+ 'starttime' => new external_value(PARAM_INT),
+ 'duration' => new external_value(PARAM_INT),
+ 'timeformatted' => new external_single_structure([
+ 'date' => new external_value(PARAM_RAW),
+ 'starttime' => new external_value(PARAM_RAW),
+ 'shortdatetime' => new external_value(PARAM_RAW),
+ 'endtime' => new external_value(PARAM_RAW),
+ ]),
+
+ 'notes' => new external_value(PARAM_RAW),
+ 'notesformat' => new external_value(PARAM_INT),
+ 'notesformatted' => new external_value(PARAM_HTML),
+ 'hasnotes' => new external_value(PARAM_BOOL),
+
+ 'appointments' => new extenral_multiple_structure(
+ static::appointment_structure()
+ ),
+ 'appointmentlocation' => new external_value(PARAM_RAW),
+ 'hasappointmentlocation' => new external_value(PARAM_BOOL),
+
+ 'canbookslot' => new external_value(PARAM_BOOL),
+ 'canwatchslot' => new external_value(PARAM_BOOL),
+ 'iswatching' => new external_value(PARAM_BOOL),
+ 'isunlimited' => new external_value(PARAM_BOOL),
+ 'isexclusive' => new external_value(PARAM_BOOL),
+ 'isgroupallowed' => new external_value(PARAM_BOOL),
+ 'isfull' => new external_value(PARAM_BOOL),
+
+ 'nremaining' => new external_value(PARAM_INT),
+ 'ntaken' => new external_value(PARAM_INT, 'The number of seats taken when not unlimited.'),
+ 'maxappointments' => new external_value(PARAM_INT),
+
+ 'teacher' => static::user_structure(),
+ ]);
+ }
+
+ /**
+ * Serialize a user.
+ *
+ * @param object $user The user.
+ * @return array
+ */
+ public static function serialize_user($user) {
+ global $PAGE;
+ $userpicture = new \user_picture($user);
+ $userpicture->size = 1;
+ $profileimageurl = $userpicture->get_url($PAGE)->out(false);
+ return [
+ 'id' => $user->id,
+ 'fullname' => fullname($user),
+ 'profileimageurl' => $profileimageurl,
+ ];
+ }
+
+ /**
+ * Get the user structure.
+ *
+ * @return external_value
+ */
+ protected static function user_structure() {
+ return new external_single_structure([
+ 'id' => new external_value(PARAM_INT),
+ 'fullname' => new external_value(PARAM_RAW),
+ 'profileimageurl' => new external_value(PARAM_URL)
+ ]);
+ }
+
+ /**
+ * Check whether a value is empty.
+ *
+ * @param mixed $value The value.
+ * @return bool
+ */
+ public static function is_empty($value) {
+ if (is_string($value)) {
+ $value = trim(strip_tags($value));
+ }
+ return empty($value);
+ }
+
+ /**
+ * Whether in-app booking is supported.
+ *
+ * @param scheduler $scheduler The scheduler.
+ * @return bool
+ */
+ public static function is_in_app_booking_supported(scheduler $scheduler) {
+ $bookingsupported = true;
+ if ($scheduler->uses_bookingform()) {
+ if ($scheduler->uses_bookingcaptcha()) {
+ $bookingsupported = false;
+ } else if ($scheduler->is_studentfiles_required()) {
+ $bookingsupported = false;
+ }
+ }
+ return $bookingsupported;
+ }
}
diff --git a/classes/model/scheduler.php b/classes/model/scheduler.php
index 88de2022..a8f30314 100644
--- a/classes/model/scheduler.php
+++ b/classes/model/scheduler.php
@@ -305,6 +305,24 @@ public function is_individual_scheduling_enabled() {
}
}
+ /**
+ * Whether students should upload at least one file.
+ *
+ * @return bool
+ */
+ public function is_studentfiles_required() {
+ return $this->uses_studentfiles() && $this->requireupload;
+ }
+
+ /**
+ * Whether students must provide notes.
+ *
+ * @return bool
+ */
+ public function is_studentnotes_required() {
+ return $this->uses_studentnotes() && $this->usestudentnotes;
+ }
+
/**
* Whether this scheduler supports watching.
*
diff --git a/classes/output/datetime_filter.php b/classes/output/datetime_filter.php
index b4a43d85..18c31007 100644
--- a/classes/output/datetime_filter.php
+++ b/classes/output/datetime_filter.php
@@ -70,7 +70,7 @@ public function _createElements() {
$datetime = $this->createFormElement('date_time_selector', $this->getName() . '[dt]', '', [
'optional' => true,
- 'defaulttime' => strtotime('midnight')
+ 'defaulttime' => strtotime('0 min 0 sec')
]);
$this->_elements[] = $datetime;
diff --git a/classes/output/mobile.php b/classes/output/mobile.php
new file mode 100644
index 00000000..2a51388b
--- /dev/null
+++ b/classes/output/mobile.php
@@ -0,0 +1,356 @@
+.
+
+/**
+ * Mobile renderer.
+ *
+ * @package mod_scheduler
+ * @copyright 2019 Royal College of Art
+ * @author Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_scheduler\output;
+defined('MOODLE_INTERNAL') || die();
+
+use moodle_exception;
+use mod_scheduler_renderer as renderer;
+use mod_scheduler\external;
+use mod_scheduler\model\scheduler;
+use mod_scheduler\permission\scheduler_permissions;
+
+/**
+ * Mobile renderer.
+ *
+ * @package mod_scheduler
+ * @copyright 2019 Royal College of Art
+ * @author Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mobile {
+
+ protected static function pre($args) {
+ global $PAGE, $USER;
+
+ $args = (object) $args;
+ $renderer = $PAGE->get_renderer('mod_scheduler');
+
+ $scheduler = scheduler::load_by_coursemodule_id($args->cmid);
+ require_login($scheduler->get_courseid(), true, $scheduler->get_cm());
+ $permissions = new scheduler_permissions($scheduler->get_context(), $USER->id);
+
+ return (object) ['scheduler' => $scheduler, 'permissions' => $permissions, 'renderer' => $renderer];
+ }
+
+ /**
+ * Get common template data.
+ *
+ * @param scheduler $scheduler The scheduler.
+ * @return array
+ */
+ protected static function get_common_data(scheduler $scheduler) {
+ $module = (object) external::serialize_scheduler($scheduler);
+ return [
+ 'cmid' => $scheduler->get_cmid(),
+ 'courseid' => $scheduler->get_courseid(),
+ 'scheduler' => $module,
+ ];
+ }
+
+ public static function book_slot($args) {
+ global $USER;
+
+ $args = (object) $args;
+ $slotid = (int) $args->id;
+
+ $pre = static::pre($args);
+ $permissions = $pre->permissions;
+ $scheduler = $pre->scheduler;
+ $renderer = $pre->renderer;
+ $context = $scheduler->get_context();
+ $userid = $USER->id;
+
+ require_capability('mod/scheduler:appoint', $context);
+ $slot = $scheduler->get_slot($slotid);
+ $data = static::get_common_data($scheduler);
+
+ if (!$data['scheduler']->isinappbookingsupported) {
+ throw new moodle_exception('bookingnotsupported', 'mod_scheduler');
+ } else if (!$slot->is_in_bookable_period()) {
+ throw new moodle_exception('error');
+ }
+
+ $groups = [];
+ if ($scheduler->is_group_scheduling_enabled()) {
+ $groups = groups_get_all_groups($scheduler->courseid, $userid, $scheduler->bookingrouping, 'g.id, g.name');
+ }
+
+ $data = array_merge($data, [
+ 'slot' => external::serialize_slot($slot),
+ 'isstudentnotesrequired' => $scheduler->is_studentnotes_required(),
+ 'hasgroups' => !empty($groups),
+ 'groups' => array_values($groups),
+ ]);
+
+ return [
+ 'templates' => [
+ [
+ 'id' => 'book_slot',
+ 'html' => $renderer->render_from_template('mod_scheduler/mobile_book_slot', $data)
+ ]
+ ],
+ 'javascript' => '',
+ 'otherdata' => '',
+ 'files' => [],
+ ];
+ }
+
+ public static function landing_page($args) {
+ $args = (object) $args;
+
+ $pre = static::pre($args);
+ $permissions = $pre->permissions;
+ $scheduler = $pre->scheduler;
+ $renderer = $pre->renderer;
+
+ if ($permissions->is_teacher()) {
+ } else if ($permissions->is_student()) {
+ return static::student_landing_page($args, $renderer, $scheduler, $permissions);
+ }
+
+ return [
+ 'templates' => [
+ [
+ 'id' => 'noguests',
+ 'html' => $renderer->render_from_template('mod_scheduler/mobile_noguests', [])
+ ]
+ ],
+ 'javascript' => '',
+ 'otherdata' => '',
+ 'files' => [],
+ ];
+ }
+
+ public static function slot($args) {
+ global $USER;
+
+ $args = (object) $args;
+ $slotid = (int) $args->id;
+
+ $pre = static::pre($args);
+ $permissions = $pre->permissions;
+ $scheduler = $pre->scheduler;
+ $renderer = $pre->renderer;
+ $context = $scheduler->get_context();
+ $userid = $USER->id;
+
+ $isstudent = $permissions->is_student();
+ $isteacher = $permissions->is_teacher();
+ $permissions->ensure($isstudent || $isteacher);
+
+ $slot = $scheduler->get_slot($slotid);
+ $appointment = null;
+ if ($isstudent) {
+ $appointment = $slot->get_student_appointment($userid);
+ }
+
+ $data = array_merge(static::get_common_data($scheduler), [
+ 'slot' => external::serialize_slot($slot),
+ 'appointment' => $appointment ? external::serialize_appointment($appointment) : null,
+ 'isstudent' => $isstudent,
+ 'isteacher' => $isteacher,
+ ]);
+
+ return [
+ 'templates' => [
+ [
+ 'id' => 'slot',
+ 'html' => $renderer->render_from_template('mod_scheduler/mobile_slot', $data)
+ ]
+ ],
+ 'javascript' => '',
+ 'otherdata' => '',
+ 'files' => [],
+ ];
+ }
+
+ /**
+ * Student bookable slots.
+ *
+ * @param object $args Contains cmid and optionally page.
+ * @return array
+ */
+ public static function student_bookable_slots($args) {
+ $args = (object) $args;
+
+ $pre = static::pre($args);
+ $permissions = $pre->permissions;
+ $scheduler = $pre->scheduler;
+ $renderer = $pre->renderer;
+ $context = $scheduler->get_context();
+
+ $permissions->ensure($permissions->is_student());
+ require_capability('mod/scheduler:viewslots', $context);
+ require_capability('mod/scheduler:appoint', $context);
+ $canwatch = has_capability('mod/scheduler:watchslots', $context) && $scheduler->is_watching_enabled();
+
+ $page = isset($args->page) ? (int) $args->page : 1;
+ $result = (object) external::get_available_slots($scheduler->get_cmid(), $page);
+
+ $data = array_merge(static::get_common_data($scheduler), [
+ 'prevpage' => max($result->page - 1, 0),
+ 'hasnext' => $result->hasnextpage,
+ 'hasprev' => $result->page > 1,
+ 'nextpage' => $result->page + 1,
+ 'prevpage' => max($result->page - 1, 0),
+ 'slots' => $result->slots,
+ ]);
+
+ return [
+ 'templates' => [
+ [
+ 'id' => 'student_bookable_slots',
+ 'html' => $renderer->render_from_template('mod_scheduler/mobile_student_bookable_slots', $data)
+ ]
+ ],
+ 'javascript' => '',
+ 'otherdata' => '',
+ 'files' => [],
+ ];
+ }
+
+ /**
+ * Student landing page.
+ *
+ * @param object $args The original arguments.
+ * @param renderer_base $renderer The renderer.
+ * @param scheduler $scheduler The scheduler.
+ * @param scheduler_permissions $permissions The permissions.
+ * @return array
+ */
+ public static function student_landing_page($args, $renderer, scheduler $scheduler, scheduler_permissions $permissions) {
+ global $USER;
+
+ $userid = $USER->id;
+ $context = $scheduler->get_context();
+
+ require_capability('mod/scheduler:viewslots', $context);
+
+ // Find attended slots.
+ $pastslotsraw = $scheduler->get_attended_slots_for_student($userid);
+ $pastslots = array_map(function($slot) {
+ return external::serialize_slot($slot);
+ }, $pastslotsraw);
+
+ // Find the upcoming slots.
+ $upcomingslotsraw = $scheduler->get_upcoming_slots_for_student($userid);
+ $upcomingslots = array_map(function($slot) use ($context, $scheduler, $userid) {
+ // $appointment = $slot->get_student_appointment($userid);
+ // $cancancel = $slot->is_in_bookable_period();
+ // $canedit = $cancancel && $scheduler->uses_studentdata();
+ // $canview = !$cancancel && $scheduler->uses_studentdata();
+ // if ($scheduler->is_group_scheduling_enabled()) {
+ // $cancancel = $cancancel && ($appointgroup >= 0);
+ // }
+ return external::serialize_slot($slot);
+ }, $upcomingslotsraw);
+
+ // Display the bookable slots.
+ $result = (object) external::get_available_slots($scheduler->get_cmid(), 1, 10);
+ $nobookingsremaining = $result->bookingsremaining;
+ $bookableslots = $result->slots;
+ $totalbookableslots = $result->total;
+
+ $nobookingmessage = '';
+ if (!$nobookingsremaining) {
+ $nobookingmessage = get_string('canbooknofurtherappointments', 'mod_scheduler');
+ } else if (!count($bookableslots)) {
+ $nobookingmessage = get_string('noslotsavailable', 'mod_scheduler');
+ }
+
+ $bookingmessage = '';
+ if ($nobookingsremaining == 1) {
+ $msgkey = ($scheduler->schedulermode == 'oneonly') ? 'canbooksingleappointment' : 'canbook1appointment';
+ $bookingmessage = get_string($msgkey, 'mod_scheduler');
+ } else if ($nobookingsremaining > 1) {
+ $bookingmessage = get_string('canbooknappointments', 'mod_scheduler');
+ } else if ($nobookingsremaining < 0) {
+ $bookingmessage = get_string('canbookunlimitedappointments', 'mod_scheduler');
+ }
+
+ $data = array_merge(static::get_common_data($scheduler), [
+ 'haspastslots' => !empty($pastslots),
+ 'pastslots' => $pastslots,
+
+ 'hasupcomingslots' => !empty($upcomingslots),
+ 'upcomingslots' => $upcomingslots,
+
+ 'hasbookableslots' => !empty($bookableslots),
+ 'hasmorebookableslots' => count($bookableslots) < $totalbookableslots,
+ 'bookableslots' => $bookableslots,
+ 'bookingmessage' => $bookingmessage,
+ 'nobookingmessage' => $nobookingmessage,
+ ]);
+
+ return [
+ 'templates' => [
+ [
+ 'id' => 'student_landing_page',
+ 'html' => $renderer->render_from_template('mod_scheduler/mobile_student_landing_page', $data)
+ ]
+ ],
+ 'javascript' => '',
+ 'otherdata' => '',
+ 'files' => [],
+ ];
+ }
+
+ /**
+ * Watch a slot.
+ *
+ * @param array $args The arguments.
+ * @return array
+ */
+ public static function watch_slot($args) {
+ global $USER;
+
+ $args = (object) $args;
+ $slotid = (int) $args->id;
+
+ $pre = static::pre($args);
+
+ external::watch_slot($args->cmid, $slotid);
+
+ return static::slot($args);
+ }
+
+ /**
+ * Unwatch a slot.
+ *
+ * @param array $args The arguments.
+ * @return array
+ */
+ public static function unwatch_slot($args) {
+ global $USER;
+
+ $args = (object) $args;
+ $slotid = (int) $args->id;
+
+ $pre = static::pre($args);
+ external::unwatch_slot($args->cmid, $slotid);
+ return static::slot($args);
+ }
+}
diff --git a/classes/permission/scheduler_permissions.php b/classes/permission/scheduler_permissions.php
index acb8adc2..50fb0afe 100644
--- a/classes/permission/scheduler_permissions.php
+++ b/classes/permission/scheduler_permissions.php
@@ -170,4 +170,32 @@ public function can_edit_notes(\mod_scheduler\model\appointment $app) {
}
}
+ /**
+ * Whether the user appears to be a teacher based on their permissions.
+ *
+ * This check is only based on the permissions assigned within the scheduler
+ * activity and will not perform checks based on roles assigned in the course.
+ *
+ * @return bool
+ */
+ public function is_teacher() {
+ $caps = ['manage', 'manageallappointments', 'canseeotherteachersbooking'];
+ return $this->has_any_capability($caps);
+ }
+
+ /**
+ * Whether the user appears to be a student based on their permissions.
+ *
+ * This check is only based on the permissions assigned within the scheduler
+ * activity and will not perform checks based on roles assigned in the course.
+ *
+ * For our purpose, a user cannot be both a teacher and student, therefore
+ * the teacher role will take precedence over the student one.
+ *
+ * @return bool
+ */
+ public function is_student() {
+ return !$this->is_teacher() && $this->has_capability('viewslots');
+ }
+
}
diff --git a/db/mobile.php b/db/mobile.php
new file mode 100644
index 00000000..e604afdf
--- /dev/null
+++ b/db/mobile.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * Mobile support.
+ *
+ * @package mod_scheduler
+ * @copyright 2019 Royal College of Art
+ * @author Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$addons = [
+ 'mod_scheduler' => [
+ 'handlers' => [
+ 'coursescheduler' => [
+ 'displaydata' => [
+ 'icon' => $CFG->wwwroot . '/mod/scheduler/pix/icon.gif',
+ 'class' => ''
+ ],
+ 'delegate' => 'CoreCourseModuleDelegate',
+ 'method' => 'landing_page',
+ ]
+ ],
+ 'lang' => [
+ ['modulename', 'mod_scheduler']
+ ]
+ ],
+];
diff --git a/db/services.php b/db/services.php
index 21705957..5c7b92e0 100644
--- a/db/services.php
+++ b/db/services.php
@@ -26,11 +26,39 @@
defined('MOODLE_INTERNAL') || die();
$functions = [
+ 'mod_scheduler_booking_form_viewed' => [
+ 'classname' => 'mod_scheduler\\external',
+ 'methodname' => 'booking_form_viewed',
+ 'description' => 'Trigger the event reporting that booking for was viewed',
+ 'type' => 'write',
+ 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile']
+ ],
+ 'mod_scheduler_book_slot' => [
+ 'classname' => 'mod_scheduler\\external',
+ 'methodname' => 'book_slot',
+ 'description' => 'Book a slot',
+ 'type' => 'write',
+ 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile']
+ ],
'mod_scheduler_revoke_appointment' => [
'classname' => 'mod_scheduler\\external',
'methodname' => 'revoke_appointment',
'description' => 'Revoke an appointment',
'type' => 'write',
'ajax' => true,
+ ],
+ 'mod_scheduler_watch_slot' => [
+ 'classname' => 'mod_scheduler\\external',
+ 'methodname' => 'watch_slot',
+ 'description' => 'Watch a slot',
+ 'type' => 'write',
+ 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile']
+ ],
+ 'mod_scheduler_unwatch_slot' => [
+ 'classname' => 'mod_scheduler\\external',
+ 'methodname' => 'unwatch_slot',
+ 'description' => 'Unwatch a slot',
+ 'type' => 'write',
+ 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE, 'local_mobile']
]
-];
\ No newline at end of file
+];
diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php
index a13174f7..b84240ec 100644
--- a/lang/en/scheduler.php
+++ b/lang/en/scheduler.php
@@ -126,6 +126,7 @@
$string['allappointments'] = 'All appointments';
$string['allononepage'] = 'All slots on one page';
$string['allowgroup'] = 'Exclusive slot - click to change';
+$string['alreadybookedbyyou'] = 'This slot has already been booked for you.';
$string['alreadyappointed'] = 'Cannot make the appointment. The slot is already fully booked.';
$string['applyfilters'] = 'Apply filters';
$string['appointfor'] = 'Make appointment for';
@@ -135,6 +136,7 @@
$string['appointment'] = 'Appointment';
$string['appointmentno'] = 'Appointment {$a}';
$string['appointmentnote'] = 'Notes for appointment (visible to student)';
+$string['appointmentnotes'] = 'Appointment notes';
$string['appointments'] = 'Appointments';
$string['appointmentsgrouped'] = 'Appointments grouped by slot';
$string['appointsolo'] = 'just me';
@@ -152,6 +154,7 @@
$string['bookingformoptions'] = 'Booking form and student-supplied data';
$string['bookinginstructions'] = 'Booking instructions';
$string['bookinginstructions_help'] = 'This text will be displayed to students before they make a booking. It can, for example, instruct students how to fill out the optional message field or which files to upload.';
+$string['bookingnotsupported'] = 'Booking not supported, please use the online version.';
$string['bookslot'] = 'Book slot';
$string['bookaslot'] = 'Book a slot';
$string['bookingdetails'] = 'Booking details';
@@ -505,6 +508,7 @@
$string['studentfiles'] = 'Uploaded files';
$string['studentmultiselect'] = 'Each student can be selected only once in this slot';
$string['studentnote'] = 'Message by student';
+$string['studentnotemissing'] = 'The message from the student is missing.';
$string['students'] = 'Students';
$string['studentprovided'] = 'Student provided: {$a}';
$string['sunday'] = 'Sunday';
@@ -553,6 +557,7 @@
$string['usestudentnotes'] = 'Let students enter a message';
$string['usestudentnotes_help'] = 'If enabled, the booking screen will contain a text box in which students can enter a message. Use the "booking instructions" above to instruct students what information they should supply.';
$string['viewbooking'] = 'See details';
+$string['viewmoreoptions'] = 'View more options';
$string['watchslotsintro'] = 'To be notified when a fully booked slot becomes available, click the "Watch slot" button for that corresponding slot.';
$string['wednesday'] = 'Wednesday';
$string['welcomebackstudent'] = 'You can book additional slots by clicking on the corresponding "Book slot" button below.';
diff --git a/locallib.php b/locallib.php
index 60ce8232..0c9b5b09 100644
--- a/locallib.php
+++ b/locallib.php
@@ -27,6 +27,8 @@
require_once($CFG->libdir.'/filelib.php');
require_once(dirname(__FILE__).'/customlib.php');
+use mod_scheduler\model\scheduler;
+
/* Events related functions */
@@ -336,3 +338,114 @@ public function get_parent() {
return $this->browser->get_file_info($this->context);
}
}
+
+/**
+ * Get the upload options for student files.
+ *
+ * @param scheduler $scheduler The scheduler.
+ * @return array
+ */
+function mod_scheduler_get_student_upload_options(scheduler $scheduler) {
+ return ['subdirs' => 0, 'maxbytes' => $scheduler->uploadmaxsize, 'maxfiles' => $scheduler->uploadmaxfiles];
+}
+
+/**
+ * Book a slot.
+ *
+ * @param scheduler $scheduler The scheduler.
+ * @param int $slotid The slot ID.
+ * @param int $userid The user ID.
+ * @param int $groupid The group ID, or 0.
+ * @param mixed $formdata The form data from {@link scheduler_booking_form}.
+ * @throws mixed moodle_exception
+ */
+function mod_scheduler_book_slot($scheduler, $slotid, $userid, $groupid, $formdata) {
+ global $DB;
+
+ $slot = $scheduler->get_slot($slotid);
+ if (!$slot) {
+ throw new moodle_exception('error');
+ }
+
+ if (!$slot->is_in_bookable_period()) {
+ throw new moodle_exception('nopermissions');
+ }
+
+ $requiredcapacity = 1;
+ $userstobook = array($userid);
+ if ($groupid > 0) {
+ if (!$scheduler->is_group_scheduling_enabled()) {
+ throw new moodle_exception('error');
+ }
+ $groupmembers = $scheduler->get_available_students($groupid);
+ $requiredcapacity = count($groupmembers);
+ $userstobook = array_keys($groupmembers);
+ } else if ($groupid == 0) {
+ if (!$scheduler->is_individual_scheduling_enabled()) {
+ throw new moodle_exception('error');
+ }
+ } else {
+ // Group scheduling enabled but no group selected.
+ throw new moodle_exception('error');
+ }
+
+ $errormessage = '';
+
+ $bookinglimit = $scheduler->count_bookable_appointments($userid, false);
+ if ($bookinglimit == 0) {
+ throw new moodle_exception('selectedtoomany', 'mod_scheduler', null, $bookinglimit);
+
+ } else {
+ // Validate our user ids.
+ $existingstudents = array();
+ foreach ($slot->get_appointments() as $app) {
+ $existingstudents[] = $app->studentid;
+ }
+ $userstobook = array_diff($userstobook, $existingstudents);
+
+ $remaining = $slot->count_remaining_appointments();
+ // If the slot is already overcrowded...
+ if ($remaining >= 0 && $remaining < $requiredcapacity) {
+ if ($requiredcapacity > 1) {
+ throw new moodle_exception('notenoughplaces', 'mod_scheduler');
+ } else {
+ throw new moodle_exception('slot_is_just_in_use', 'mod_scheduler');
+ }
+ }
+ }
+
+ // Create new appointment for each member of the group.
+ foreach ($userstobook as $studentid) {
+ $appointment = $slot->create_appointment();
+ $appointment->studentid = $studentid;
+ $appointment->attended = 0;
+ $appointment->timecreated = time();
+ $appointment->timemodified = time();
+ $appointment->save();
+
+ if ($studentid == $userid && $formdata) {
+ if ($scheduler->uses_studentnotes() && isset($formdata->studentnote_editor)) {
+ $editor = $formdata->studentnote_editor;
+ $appointment->studentnote = $editor['text'];
+ $appointment->studentnoteformat = $editor['format'];
+ }
+ if ($scheduler->uses_studentfiles() && !empty($formdata->studentfiles)) {
+ file_save_draft_area_files($formdata->studentfiles, $scheduler->context->id, 'mod_scheduler',
+ 'studentfiles', $appointment->id, mod_scheduler_get_student_upload_options($scheduler));
+ }
+ $appointment->save();
+ }
+
+ \mod_scheduler\event\booking_added::create_from_slot($slot)->trigger();
+
+ // Notify the teacher.
+ if ($scheduler->allownotifications) {
+ $student = $DB->get_record('user', array('id' => $appointment->studentid), '*', MUST_EXIST);
+ $teacher = $DB->get_record('user', array('id' => $slot->teacherid), '*', MUST_EXIST);
+ scheduler_messenger::send_slot_notification($slot, 'bookingnotification', 'applied',
+ $student, $teacher, $teacher, $student, $scheduler->get_courserec());
+ }
+ }
+
+ $slot->save();
+}
diff --git a/studentview.controller.php b/studentview.controller.php
index bfe8b001..d57f7ae9 100644
--- a/studentview.controller.php
+++ b/studentview.controller.php
@@ -39,91 +39,12 @@
* @throws mixed moodle_exception
*/
function scheduler_book_slot($scheduler, $slotid, $userid, $groupid, $mform, $formdata, $returnurl) {
-
- global $DB, $COURSE, $output;
-
- $slot = $scheduler->get_slot($slotid);
- if (!$slot) {
- throw new moodle_exception('error');
- }
-
- if (!$slot->is_in_bookable_period()) {
- throw new moodle_exception('nopermissions');
- }
-
- $requiredcapacity = 1;
- $userstobook = array($userid);
- if ($groupid > 0) {
- if (!$scheduler->is_group_scheduling_enabled()) {
- throw new moodle_exception('error');
- }
- $groupmembers = $scheduler->get_available_students($groupid);
- $requiredcapacity = count($groupmembers);
- $userstobook = array_keys($groupmembers);
- } else if ($groupid == 0) {
- if (!$scheduler->is_individual_scheduling_enabled()) {
- throw new moodle_exception('error');
- }
- } else {
- // Group scheduling enabled but no group selected.
- throw new moodle_exception('error');
- }
-
- $errormessage = '';
-
- $bookinglimit = $scheduler->count_bookable_appointments($userid, false);
- if ($bookinglimit == 0) {
- $errormessage = get_string('selectedtoomany', 'scheduler', $bookinglimit);
- } else {
- // Validate our user ids.
- $existingstudents = array();
- foreach ($slot->get_appointments() as $app) {
- $existingstudents[] = $app->studentid;
- }
- $userstobook = array_diff($userstobook, $existingstudents);
-
- $remaining = $slot->count_remaining_appointments();
- // If the slot is already overcrowded...
- if ($remaining >= 0 && $remaining < $requiredcapacity) {
- if ($requiredcapacity > 1) {
- $errormessage = get_string('notenoughplaces', 'scheduler');
- } else {
- $errormessage = get_string('slot_is_just_in_use', 'scheduler');
- }
- }
- }
-
- if ($errormessage) {
- \core\notification::error($errormessage);
- redirect($returnurl);
- }
-
- // Create new appointment for each member of the group.
- foreach ($userstobook as $studentid) {
- $appointment = $slot->create_appointment();
- $appointment->studentid = $studentid;
- $appointment->attended = 0;
- $appointment->timecreated = time();
- $appointment->timemodified = time();
- $appointment->save();
-
- if (($studentid == $userid) && $mform) {
- $mform->save_booking_data($formdata, $appointment);
- }
-
- \mod_scheduler\event\booking_added::create_from_slot($slot)->trigger();
-
- // Notify the teacher.
- if ($scheduler->allownotifications) {
- $student = $DB->get_record('user', array('id' => $appointment->studentid), '*', MUST_EXIST);
- $teacher = $DB->get_record('user', array('id' => $slot->teacherid), '*', MUST_EXIST);
- scheduler_messenger::send_slot_notification($slot, 'bookingnotification', 'applied',
- $student, $teacher, $teacher, $student, $COURSE);
- }
+ try {
+ mod_scheduler_book_slot($scheduler, $slotid, $userid, $groupid, $formdata);
+ } catch (moodle_exception $e) {
+ \core\notification::error($e->getMessage());
}
- $slot->save();
redirect($returnurl);
-
}
$returnurlparas = array('id' => $cm->id);
diff --git a/templates/mobile_book_slot.mustache b/templates/mobile_book_slot.mustache
new file mode 100644
index 00000000..0f1dd766
--- /dev/null
+++ b/templates/mobile_book_slot.mustache
@@ -0,0 +1,63 @@
+{{=<% %>=}}
+
+
<%/ app.caneditnotes %>
From d8b2bdada71ff3562280242b5145c314d71d47e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Tue, 25 Feb 2020 10:10:02 +0800
Subject: [PATCH 27/29] Use 'Full' instead of 'Booked' for students viewing
fully booked slots
---
lang/en/scheduler.php | 1 +
studentview.php | 2 +-
templates/mobile_student_bookable_slots.mustache | 2 +-
templates/mobile_student_landing_page.mustache | 2 +-
4 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php
index 9a03f4d3..6f5ad82c 100644
--- a/lang/en/scheduler.php
+++ b/lang/en/scheduler.php
@@ -304,6 +304,7 @@
$string['forcewhenoverlap'] = 'Force when overlap';
$string['forcourses'] = 'Choose students in courses';
$string['friday'] = 'Friday';
+$string['full'] = 'Full';
$string['generalconfig'] = 'General configuration';
$string['grade'] = 'Grade';
$string['gradeingradebook'] = 'Grade in gradebook';
diff --git a/studentview.php b/studentview.php
index afe5aabb..d49d2308 100644
--- a/studentview.php
+++ b/studentview.php
@@ -218,7 +218,7 @@
if ($remaining > 0) {
$groupinfo = get_string('limited', 'scheduler', $remaining.'/'.$slot->exclusivity);
} else { // Group info should not be visible to students.
- $groupinfo = get_string('complete', 'scheduler');
+ $groupinfo = get_string('full', 'scheduler');
$canbookthisslot = false;
}
}
diff --git a/templates/mobile_student_bookable_slots.mustache b/templates/mobile_student_bookable_slots.mustache
index c7a1733e..ab8e815b 100644
--- a/templates/mobile_student_bookable_slots.mustache
+++ b/templates/mobile_student_bookable_slots.mustache
@@ -19,7 +19,7 @@
<% ntaken %> / <% maxappointments %>
<%/ isfull %>
<%# isfull %>
- <%# str %>complete, mod_scheduler<%/ str %>
+ <%# str %>full, mod_scheduler<%/ str %>
<%/ isfull %>
<%/ isunlimited %>
diff --git a/templates/mobile_student_landing_page.mustache b/templates/mobile_student_landing_page.mustache
index 9b9e0a25..471c77b9 100644
--- a/templates/mobile_student_landing_page.mustache
+++ b/templates/mobile_student_landing_page.mustache
@@ -55,7 +55,7 @@
<% ntaken %> / <% maxappointments %>
<%/ isfull %>
<%# isfull %>
- <%# str %>complete, mod_scheduler<%/ str %>
+ <%# str %>full, mod_scheduler<%/ str %>
<%/ isfull %>
<%/ isunlimited %>
From f18ce5c32a34bdde818ab801f1e1c8540f2ae600 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Fri, 6 Mar 2020 18:02:04 +0800
Subject: [PATCH 28/29] Support for limiting the number of slots students can
watch
---
classes/external.php | 15 +++++++++-----
classes/model/scheduler.php | 40 +++++++++++++++++++++++++++++++++++++
lang/en/scheduler.php | 4 ++++
settings.php | 5 +++++
studentview.php | 15 +++++++++-----
version.php | 2 +-
6 files changed, 70 insertions(+), 11 deletions(-)
diff --git a/classes/external.php b/classes/external.php
index 9310008e..8401a53f 100644
--- a/classes/external.php
+++ b/classes/external.php
@@ -279,8 +279,8 @@ public static function get_available_slots($cmid, $page = 1, $perpage = 25) {
if ($canbookslots) {
$bookableslotsraw = array_values($scheduler->get_slots_available_to_student($userid, $canwatchslots));
$totalbookableslots = count($bookableslotsraw);
- $bookableslots = array_map(function($slot) use ($canbookslots, $canwatchslots, $canseeothers) {
- return static::serialize_slot($slot, $canbookslots, $canwatchslots, $canseeothers);
+ $bookableslots = array_map(function($slot) {
+ return static::serialize_slot($slot);
}, array_slice($bookableslotsraw, ($page - 1) * $perpage, $perpage));
}
@@ -526,6 +526,8 @@ public static function watch_slot($cmid, $slotid) {
if (!$scheduler->is_watching_enabled()) {
throw new moodle_exception('error');
+ } else if (!$scheduler->student_can_watch_more_slots($USER->id)) {
+ throw new moodle_exception('cannotwatchmoreslots');
}
$slot = $scheduler->get_slot($slotid);
@@ -786,12 +788,15 @@ public static function serialize_slot(slot $slot) {
$nremaining = $slot->count_remaining_appointments();
$canbookslots = has_capability('mod/scheduler:appoint', $context);
- $canwatchslots = has_capability('mod/scheduler:appoint', $context) && $slot->get_scheduler()->is_watching_enabled();
+ $canwatchslots = has_capability('mod/scheduler:appoint', $context)
+ && $slot->get_scheduler()->is_watching_enabled()
+ && $slot->get_scheduler()->student_can_watch_more_slots($USER->id);
$canseeothers = has_capability('mod/scheduler:seeotherstudentsbooking', $context);
$canbookslot = $canbookslots && $nremaining != 0 && $slot->is_in_bookable_period();
- $canwatchslot = $canwatchslots && $slot->is_watchable_by_student($USER->id);
- $iswatching = $canwatchslot && $slot->is_watched_by_student($USER->id);
+ $isslotwatchable = $slot->is_watchable_by_student($USER->id);
+ $iswatching = $isslotwatchable && $slot->is_watched_by_student($USER->id);
+ $canwatchslot = ($canwatchslots && $isslotwatchable) || $iswatching;
$appointments = array_map(function($app) {
return static::serialize_appointment($app);
diff --git a/classes/model/scheduler.php b/classes/model/scheduler.php
index aa33154e..af017622 100644
--- a/classes/model/scheduler.php
+++ b/classes/model/scheduler.php
@@ -323,6 +323,16 @@ public function is_studentnotes_required() {
return $this->uses_studentnotes() && $this->usestudentnotes == 2;
}
+ /**
+ * The maximum number of slots a student can watch at a time.
+ *
+ * @return int Where 0 means unlimited.
+ */
+ public function get_maximum_slots_watched() {
+ $config = get_config('mod_scheduler');
+ return isset($config->maxslotswatched) ? (int) $config->maxslotswatched : 0;
+ }
+
/**
* Whether this scheduler supports watching.
*
@@ -332,6 +342,36 @@ public function is_watching_enabled() {
return (bool) $this->data->canwatch && $this->is_individual_scheduling_enabled();
}
+ /**
+ * Whether the student can watch more slots.
+ *
+ * @param int $studentid The student ID.
+ * @return bool
+ */
+ public function student_can_watch_more_slots($studentid) {
+ global $DB;
+
+ $max = $this->get_maximum_slots_watched();
+ if (!$max) {
+ return true;
+ }
+
+ $sql = "SELECT COUNT(w.id)
+ FROM {scheduler_watcher} w
+ JOIN {scheduler_slots} s
+ ON s.id = w.slotid
+ WHERE w.userid = :userid
+ AND s.starttime > :cutofftime
+ AND s.hideuntil < :nowhide";
+ $params = [
+ 'userid' => $studentid,
+ 'nowhide' => time(),
+ 'cutofftime' => time() + $this->guardtime
+ ];
+
+ return $DB->count_records_sql($sql, $params) < $max;
+ }
+
/**
* get the last location of a certain teacher in this scheduler
*
diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php
index 6f5ad82c..55f258d3 100644
--- a/lang/en/scheduler.php
+++ b/lang/en/scheduler.php
@@ -168,6 +168,7 @@
$string['canbooknofurtherappointments'] = 'You cannot book further appointments in this scheduler.';
$string['canbookunlimitedappointments'] = 'You can book any number of appointments in this scheduler.';
$string['cannotscheduleslotforothers'] = 'You cannot schedule appointments for other staff members.';
+$string['cannotwatchmoreslots'] = 'You cannot watch more slots at this time.';
$string['chooseexisting'] = 'Choose existing';
$string['choosegrouptobook'] = 'Please select a group to assign the booking to.';
$string['choosingslotstart'] = 'Choosing the start time';
@@ -367,6 +368,8 @@
$string['markseen'] = 'After you have had an appointment with a student please mark them as "Seen" by clicking the checkbox near to their user picture above.';
$string['markasseennow'] = 'Mark as seen now';
$string['maxgrade'] = 'Take the highest grade';
+$string['maxslotswatched'] = 'Maximum number of slots watched';
+$string['maxslotswatched_desc'] = 'The maximum number of slots a student can watch per scheduler activity. When set to 0, students can watch an unlimited number of slots.';
$string['maxstudentsperslot'] = 'Maximum number of students per slot';
$string['maxstudentsperslot_desc'] = 'Group slots / non-exclusive slots can have at most this number of students. Note that in addition, the setting "unlimited" can always be chosen for a slot.';
$string['maxstudentlistsize'] = 'Maximum length of student list';
@@ -568,6 +571,7 @@
$string['visitwebtoeditcancel'] = 'Please visit the website if you wish to make changes to the booking.';
$string['visitwebtouploadfiles'] = 'Please visit the website to attach files to your booking.';
$string['watchslotsintro'] = 'To be notified when a fully booked slot becomes available, click the "Watch slot" button for that corresponding slot.';
+$string['watchslotsintromax'] = 'To be notified when a fully booked slot becomes available, click the "Watch slot" button for that corresponding slot. Note that you cannot watch more than {$a} slot(s) at any given time.';
$string['wednesday'] = 'Wednesday';
$string['welcomebackstudent'] = 'You can book additional slots by clicking on the corresponding "Book slot" button below.';
$string['welcomenewstudent'] = 'The table below shows all available slots for an appointment. Make your choice by clicking on the corresponding "Book slot" button. If you need to make a change later you can revisit this page.';
diff --git a/settings.php b/settings.php
index b2fee38b..ca894a38 100644
--- a/settings.php
+++ b/settings.php
@@ -48,6 +48,11 @@
get_string('maxstudentlistsize_desc', 'scheduler'),
200, PARAM_INT));
+ $settings->add(new admin_setting_configtext('mod_scheduler/maxslotswatched',
+ get_string('maxslotswatched', 'mod_scheduler'),
+ get_string('maxslotswatched_desc', 'mod_scheduler'),
+ 3, PARAM_INT));
+
$settings->add(new admin_setting_configtext('mod_scheduler/uploadmaxfiles',
get_string('uploadmaxfilesglobal', 'scheduler'),
get_string('uploadmaxfilesglobal_desc', 'scheduler'),
diff --git a/studentview.php b/studentview.php
index d49d2308..60498064 100644
--- a/studentview.php
+++ b/studentview.php
@@ -167,6 +167,7 @@
$bookablecnt = $scheduler->count_bookable_appointments($USER->id, false);
$canbookslots = $canbook && $bookablecnt != 0;
$canwatchslots = $canwatch && $canbookslots && !$appointgroup;
+$canwatchmoreslots = $canwatchslots && $scheduler->student_can_watch_more_slots($USER->id);
$bookableslots = array_values($scheduler->get_slots_available_to_student($USER->id, $canseefull || $canwatchslots));
if (!$canseefull && $bookablecnt == 0) {
@@ -183,7 +184,6 @@
// Show the booking form.
$booker = new scheduler_slot_booker($scheduler, $USER->id, $actionurl, $bookablecnt);
- $haswatchableslots = false;
$pagesize = 25;
$total = count($bookableslots);
@@ -223,9 +223,9 @@
}
}
- $canwatchthisslot = $canwatchslots && $slot->is_watchable_by_student($USER->id);
- $iswatching = $canwatchthisslot && $slot->is_watched_by_student($USER->id);
- $haswatchableslots = $haswatchableslots || $canwatchthisslot;
+ $isslotwatchable = $slot->is_watchable_by_student($USER->id);
+ $iswatching = $isslotwatchable && $slot->is_watched_by_student($USER->id);
+ $canwatchthisslot = ($canwatchmoreslots && $isslotwatchable) || $iswatching;
$booker->add_slot($slot, $canbookthisslot, false, $groupinfo, $others, $canwatchthisslot, $iswatching);
}
@@ -259,7 +259,12 @@
}
if ($canwatchslots) {
- echo html_writer::tag('p', get_string('watchslotsintro', 'mod_scheduler'));
+ $maxwatched = $scheduler->get_maximum_slots_watched();
+ if (!$maxwatched) {
+ echo html_writer::tag('p', get_string('watchslotsintro', 'mod_scheduler'));
+ } else {
+ echo html_writer::tag('p', get_string('watchslotsintromax', 'mod_scheduler', $maxwatched));
+ }
}
}
diff --git a/version.php b/version.php
index 6330ff6a..1c65cc4e 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
*/
$plugin->component = 'mod_scheduler'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2023050815; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2023050816; // The current module version (Date: YYYYMMDDXX).
$plugin->release = '4.x dev'; // Human-friendly version name.
$plugin->requires = 2022041900; // Requires Moodle 4.0.
$plugin->maturity = MATURITY_ALPHA; // Development release - not for production use.
From 0dc2c86dfb84715005895116b1719c3e8a206ee0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?=
Date: Mon, 8 May 2023 15:20:12 +0800
Subject: [PATCH 29/29] Minor changes for styling and compatibility with Moodle
4.x
---
classes/csv_slots_importer.php | 8 ++---
classes/event/slot_unwatched.php | 1 -
classes/event/slot_watched.php | 1 -
.../local/iterator/csv_reader_iterator.php | 9 +++---
classes/local/iterator/map_iterator.php | 30 ++++++++++++++++---
classes/model/appointment.php | 2 +-
classes/model/watcher.php | 1 -
classes/model/watcher_factory.php | 2 --
classes/output/datetime_filter.php | 4 +--
classes/output/mobile.php | 2 --
classes/slots_query_builder.php | 1 -
classes/task/purge_obsolete_watchers.php | 1 -
import.php | 6 ++--
lang/en/scheduler.php | 18 +++++------
locallib.php | 2 +-
15 files changed, 51 insertions(+), 37 deletions(-)
diff --git a/classes/csv_slots_importer.php b/classes/csv_slots_importer.php
index 57fb2845..43237964 100644
--- a/classes/csv_slots_importer.php
+++ b/classes/csv_slots_importer.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler;
-defined('MOODLE_INTERNAL') || die();
use core_user;
use csv_import_reader;
@@ -33,6 +32,7 @@
use mod_scheduler\local\iterator\map_iterator;
use mod_scheduler\model\scheduler;
use mod_scheduler\permission\scheduler_permissions;
+use Traversable;
/**
* Slots importer from CSV.
@@ -130,7 +130,7 @@ protected function get_allowed_teachers() {
*
* @return \Iterator
*/
- public function getIterator() {
+ public function getIterator(): Traversable { // @codingStandardsIgnoreLine
return new map_iterator(
new csv_reader_iterator($this->cir),
function($line, $lineno) {
@@ -142,7 +142,7 @@ function($line, $lineno) {
/**
* Make a slot from a processed line.
*
- * @param object $info Result from {@link self::process_line}.
+ * @param object $info Result from {@see self::process_line}.
* @return slot
*/
public function make_slot_from_processed_line($info) {
@@ -243,7 +243,7 @@ public function validate_csv() {
/**
* Validates the slot data.
*
- * @param object $data Data from {@link self::convert_line}.
+ * @param object $data Data from {@see self::convert_line}.
* @return string[]
*/
protected function validate_data($data) {
diff --git a/classes/event/slot_unwatched.php b/classes/event/slot_unwatched.php
index e8a39c2c..48f7167b 100644
--- a/classes/event/slot_unwatched.php
+++ b/classes/event/slot_unwatched.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\event;
-defined('MOODLE_INTERNAL') || die();
/**
* Slot unwatched.
diff --git a/classes/event/slot_watched.php b/classes/event/slot_watched.php
index fa8bd061..620e57b1 100644
--- a/classes/event/slot_watched.php
+++ b/classes/event/slot_watched.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\event;
-defined('MOODLE_INTERNAL') || die();
/**
* Slot watched.
diff --git a/classes/local/iterator/csv_reader_iterator.php b/classes/local/iterator/csv_reader_iterator.php
index e0bef994..1c48df31 100644
--- a/classes/local/iterator/csv_reader_iterator.php
+++ b/classes/local/iterator/csv_reader_iterator.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\local\iterator;
-defined('MOODLE_INTERNAL') || die();
use csv_import_reader;
@@ -56,6 +55,7 @@ public function __construct(csv_import_reader $cir) {
$this->cir = $cir;
}
+ #[\ReturnTypeWillChange]
/**
* Return current value.
*
@@ -81,6 +81,7 @@ protected function ensure_initialised() {
}
}
+ #[\ReturnTypeWillChange]
/**
* Return the line number.
*
@@ -97,7 +98,7 @@ public function key() {
*
* @return void
*/
- public function next() {
+ public function next(): void {
$this->ensure_initialised();
$this->pos++;
$this->current = $this->cir->next();
@@ -108,7 +109,7 @@ public function next() {
*
* @return void
*/
- public function rewind() {
+ public function rewind(): void {
$this->pos = 0;
$this->initialised = false;
$this->current = null;
@@ -120,7 +121,7 @@ public function rewind() {
*
* @return bool
*/
- public function valid() {
+ public function valid(): bool {
return $this->current !== false;
}
}
diff --git a/classes/local/iterator/map_iterator.php b/classes/local/iterator/map_iterator.php
index 641b7281..c38d786b 100644
--- a/classes/local/iterator/map_iterator.php
+++ b/classes/local/iterator/map_iterator.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\local\iterator;
-defined('MOODLE_INTERNAL') || die();
/**
* Iterator map.
@@ -52,24 +51,47 @@ public function __construct(\Iterator $iterator, callable $callback) {
$this->callback = $callback;
}
+ #[\ReturnTypeWillChange]
+ /**
+ * Current.
+ *
+ * @return mixed
+ */
public function current() {
$cb = $this->callback;
return $cb($this->iterator->current(), $this->iterator->key());
}
+ #[\ReturnTypeWillChange]
+ /**
+ * Key.
+ *
+ * @return mixed
+ */
public function key() {
return $this->iterator->key();
}
- public function next() {
+ /**
+ * Next.
+ */
+ public function next(): void {
$this->iterator->next();
}
- public function rewind() {
+ /**
+ * Rewing.
+ */
+ public function rewind(): void {
$this->iterator->rewind();
}
- public function valid() {
+ /**
+ * Valid.
+ *
+ * @return bool
+ */
+ public function valid(): bool {
return $this->iterator->valid();
}
diff --git a/classes/model/appointment.php b/classes/model/appointment.php
index 3a6d1c19..3d110792 100644
--- a/classes/model/appointment.php
+++ b/classes/model/appointment.php
@@ -178,7 +178,7 @@ public function count_studentfiles() {
* Set attended.
*
* This method is protected as it currently is only meant to be used from
- * the {@link mvc_record_model::__set} method.
+ * the {@see mvc_record_model::__set} method.
*
* We use this method to observe whether the value has changed and decide
* whether to inform the scheduler that it should be updating the completion
diff --git a/classes/model/watcher.php b/classes/model/watcher.php
index 781d9f26..2ef68a22 100644
--- a/classes/model/watcher.php
+++ b/classes/model/watcher.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\model;
-defined('MOODLE_INTERNAL') || die();
/**
* Watcher.
diff --git a/classes/model/watcher_factory.php b/classes/model/watcher_factory.php
index a5bfbee2..17816b5c 100644
--- a/classes/model/watcher_factory.php
+++ b/classes/model/watcher_factory.php
@@ -24,8 +24,6 @@
*/
namespace mod_scheduler\model;
-defined('MOODLE_INTERNAL') || die();
-
/**
* Watcher factory.
diff --git a/classes/output/datetime_filter.php b/classes/output/datetime_filter.php
index 2737baff..60837ef8 100644
--- a/classes/output/datetime_filter.php
+++ b/classes/output/datetime_filter.php
@@ -57,7 +57,7 @@ public function __construct($elementname = null, $elementlabel = null, $elements
*
* @return void
*/
- public function _createElements() {
+ public function _createElements() { // @codingStandardsIgnoreLine
$this->_elements = [];
$operator = $this->createFormElement('select', $this->getName() . '[op]', '', [
@@ -88,7 +88,7 @@ public function _createElements() {
* @param bool $notused Not used.
* @return array field name => value. The value is the time interval in seconds.
*/
- function exportValue(&$submitvalues, $notused = false) {
+ public function exportValue(&$submitvalues, $notused = false) { // @codingStandardsIgnoreLine
// Get the values from all the child elements.
$values = [];
foreach ($this->_elements as $element) {
diff --git a/classes/output/mobile.php b/classes/output/mobile.php
index 390a5c9b..fef629eb 100644
--- a/classes/output/mobile.php
+++ b/classes/output/mobile.php
@@ -24,10 +24,8 @@
*/
namespace mod_scheduler\output;
-defined('MOODLE_INTERNAL') || die();
use moodle_exception;
-use mod_scheduler_renderer as renderer;
use mod_scheduler\external;
use mod_scheduler\slots_query_builder;
use mod_scheduler\model\scheduler;
diff --git a/classes/slots_query_builder.php b/classes/slots_query_builder.php
index ac447a3b..47b00338 100644
--- a/classes/slots_query_builder.php
+++ b/classes/slots_query_builder.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler;
-defined('MOODLE_INTERNAL') || die();
use coding_exception;
use core\dml\sql_join;
diff --git a/classes/task/purge_obsolete_watchers.php b/classes/task/purge_obsolete_watchers.php
index bde56604..4587a7ba 100644
--- a/classes/task/purge_obsolete_watchers.php
+++ b/classes/task/purge_obsolete_watchers.php
@@ -24,7 +24,6 @@
*/
namespace mod_scheduler\task;
-defined('MOODLE_INTERNAL') || die();
/**
* Purge obsolete watchers.
diff --git a/import.php b/import.php
index 426d4f64..2ed0351c 100644
--- a/import.php
+++ b/import.php
@@ -38,7 +38,7 @@
// Check permissions and whether we have teachers.
$permissions->ensure($permissions->can_edit_own_slots());
if (!$scheduler->has_available_teachers()) {
- print_error('needteachers', 'scheduler', $returnurl);
+ throw new moodle_exception('needteachers', 'scheduler', $returnurl);
}
// While we don't yet have a valid file.
@@ -64,7 +64,7 @@
if (!empty($errors)) {
$errorkey = array_keys($errors)[0];
$error = reset($errors);
- print_error($errorkey, '', $baseurl, $error);
+ throw new moodle_exception($errorkey, 'core_error', $baseurl, $error);
}
$table = new flexible_table('import-slot-preview');
@@ -141,7 +141,7 @@
if (!empty($errors)) {
$errorkey = array_keys($errors)[0];
$error = reset($errors);
- print_error($errorkey, '', $baseurl, $error);
+ throw new moodle_exception($errorkey, 'core_error', $baseurl, $error);
}
$imported = 0;
diff --git a/lang/en/scheduler.php b/lang/en/scheduler.php
index 55f258d3..aa2604f6 100644
--- a/lang/en/scheduler.php
+++ b/lang/en/scheduler.php
@@ -343,19 +343,19 @@
$string['importslots_help'] = '
Slots can be imported from a CSV file, containing the following __mandatory__ columns:
-- `date`: The date at which the slot starts
-- `time`: The time of the day at which the slots starts
-- `duration`: The duration of the slots, in minutes
+- date: The date at which the slot starts
+- time: The time of the day at which the slots starts
+- duration: The duration of the slots, in minutes
The following columns are also supported:
-- `maxstudents`: The maximum number of students in the slot. Use `1` for exclusive, or `0` for unlimited.
-- `location`: The location of the appointment.
-- `teacher`: The username of the Moodle account of the teacher. An empty value defaults to the current user.
-- `displayfrom`: The date from which the slot will be visible.
-- `comment`: Notes to be attached to the slot. The Markdown format is supported.
+- maxstudents: The maximum number of students in the slot. Use 1 for exclusive, or 0 for unlimited.
+- location: The location of the appointment.
+- teacher: The username of the Moodle account of the teacher. An empty value defaults to the current user.
+- displayfrom: The date from which the slot will be visible.
+- comment: Notes to be attached to the slot. The Markdown format is supported.
-The dates can be expressed in either of these formats: `MM/DD/YYYY`, `YYYY-MM-DD` or `DD-MM-YYYY`. The time can be expressed in 12h or 24h form: `2:00pm` or `14:00`.
+The dates can be expressed in either of these formats: MM/DD/YYYY, YYYY-MM-DD or DD-MM-YYYY. The time can be expressed in 12h or 24h form: 2:00pm or 14:00.
';
$string['importslotsintro'] = 'Please provide a CSV matching the required format, an example file can be downloaded [here]({$a->exampleurl}).';
$string['importallvalidslots'] = 'Import all valid slots';
diff --git a/locallib.php b/locallib.php
index 41a2aae0..2b0a8551 100644
--- a/locallib.php
+++ b/locallib.php
@@ -356,7 +356,7 @@ function mod_scheduler_get_student_upload_options(scheduler $scheduler) {
* @param int $slotid The slot ID.
* @param int $userid The user ID.
* @param int $groupid The group ID, or 0.
- * @param mixed $formdata The form data from {@link scheduler_booking_form}.
+ * @param mixed $formdata The form data from {@see scheduler_booking_form}.
* @throws mixed moodle_exception
*/
function mod_scheduler_book_slot($scheduler, $slotid, $userid, $groupid, $formdata) {