├── tests ├── fixtures │ └── studentfile.txt ├── behat │ ├── group_availability.feature │ ├── studentdata.feature │ ├── add_slots.feature │ ├── officehours.feature │ ├── behat_mod_scheduler.php │ ├── groupscheduling.feature │ ├── notes.feature │ └── conflicts.feature ├── generator │ └── lib.php ├── model_test.php └── privacy_test.php ├── pix ├── icon.gif ├── icon.png ├── ticked.gif ├── unticked.gif ├── attachment.png └── attachment.svg ├── yui ├── src │ ├── saveseen │ │ ├── meta │ │ │ └── saveseen.json │ │ ├── build.json │ │ └── js │ │ │ └── saveseen.js │ ├── delselected │ │ ├── meta │ │ │ └── delselected.json │ │ ├── build.json │ │ └── js │ │ │ └── delselected.js │ └── studentlist │ │ ├── meta │ │ └── studentlist.json │ │ ├── build.json │ │ └── js │ │ └── studentlist.js └── build │ ├── moodle-mod_scheduler-delselected │ ├── moodle-mod_scheduler-delselected-min.js │ ├── moodle-mod_scheduler-delselected.js │ └── moodle-mod_scheduler-delselected-debug.js │ ├── moodle-mod_scheduler-studentlist │ ├── moodle-mod_scheduler-studentlist-min.js │ ├── moodle-mod_scheduler-studentlist.js │ └── moodle-mod_scheduler-studentlist-debug.js │ └── moodle-mod_scheduler-saveseen │ ├── moodle-mod_scheduler-saveseen-min.js │ ├── moodle-mod_scheduler-saveseen.js │ └── moodle-mod_scheduler-saveseen-debug.js ├── db ├── messages.php ├── tasks.php ├── access.php └── install.xml ├── classes ├── search │ └── activity.php ├── event │ ├── course_module_instance_list_viewed.php │ ├── slot_added.php │ ├── booking_added.php │ ├── booking_form_viewed.php │ ├── booking_removed.php │ ├── appointment_list_viewed.php │ ├── slot_deleted.php │ ├── slot_base.php │ ├── appointment_base.php │ └── scheduler_base.php └── task │ ├── purge_unused_slots.php │ └── send_reminders.php ├── version.php ├── ajax.php ├── customlib.php ├── backup └── moodle2 │ ├── backup_scheduler_activity_task.class.php │ ├── restore_scheduler_activity_task.class.php │ ├── backup_scheduler_stepslib.php │ └── restore_scheduler_stepslib.php ├── settings.php ├── index.php ├── view.php ├── message_form.php ├── export.php ├── model └── scheduler_appointment.php ├── viewstudent.php ├── styles.css ├── bookingform.php ├── appointmentforms.php ├── exportform.php ├── README.txt ├── datelist.php └── mailtemplatelib.php /tests/fixtures/studentfile.txt: -------------------------------------------------------------------------------- 1 | Test file to be uploaded by a student 2 | -------------------------------------------------------------------------------- /pix/icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_scheduler/master/pix/icon.gif -------------------------------------------------------------------------------- /pix/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_scheduler/master/pix/icon.png -------------------------------------------------------------------------------- /pix/ticked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_scheduler/master/pix/ticked.gif -------------------------------------------------------------------------------- /pix/unticked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_scheduler/master/pix/unticked.gif -------------------------------------------------------------------------------- /pix/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marinaglancy/moodle-mod_scheduler/master/pix/attachment.png -------------------------------------------------------------------------------- /yui/src/saveseen/meta/saveseen.json: -------------------------------------------------------------------------------- 1 | { 2 | "moodle-mod_scheduler-saveseen": { 3 | "requires": [ 4 | "base", "node", "event" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /yui/src/delselected/meta/delselected.json: -------------------------------------------------------------------------------- 1 | { 2 | "moodle-mod_scheduler-delselected": { 3 | "requires": [ 4 | "base", "node", "event" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /yui/src/studentlist/meta/studentlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "moodle-mod_scheduler-studentlist": { 3 | "requires": [ 4 | "base", "node", "event", "io" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /yui/src/saveseen/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moodle-mod_scheduler-saveseen", 3 | "builds": { 4 | "moodle-mod_scheduler-saveseen": { 5 | "jsfiles": [ 6 | "saveseen.js" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /yui/src/delselected/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moodle-mod_scheduler-delselected", 3 | "builds": { 4 | "moodle-mod_scheduler-delselected": { 5 | "jsfiles": [ 6 | "delselected.js" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /yui/src/studentlist/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moodle-mod_scheduler-studentlist", 3 | "builds": { 4 | "moodle-mod_scheduler-studentlist": { 5 | "jsfiles": [ 6 | "studentlist.js" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-min.js: -------------------------------------------------------------------------------- 1 | YUI.add("moodle-mod_scheduler-delselected",function(e,t){var n={DELACTION:"div.commandbar a#delselected",SELECTBOX:"table#slotmanager input.slotselect"},r;M.mod_scheduler=M.mod_scheduler||{},r=M.mod_scheduler.delselected={},r.collect_selection=function(t,r){var i="";e.all(n.SELECTBOX).each(function(e){e.get("checked")&&(i.length>0&&(i+=","),i+=e.get("value"))}),t.setAttribute("href",r+"&items="+i)},r.init=function(t){var r=e.one(n.DELACTION);r!=null&&r.on("click",function(e){M.mod_scheduler.delselected.collect_selection(r,t)})}},"@VERSION@",{requires:["base","node","event"]}); 2 | -------------------------------------------------------------------------------- /db/messages.php: -------------------------------------------------------------------------------- 1 | array( 17 | ), 18 | 19 | // Notifications about bookings (to teachers or students). 20 | 'bookingnotification' => array( 21 | ), 22 | 23 | // Automated reminders about upcoming appointments. 24 | 'reminder' => array( 25 | ), 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /classes/search/activity.php: -------------------------------------------------------------------------------- 1 | component = 'mod_scheduler'; // Full name of the plugin (used for diagnostics). 18 | $plugin->version = 2018112700; // The current module version (Date: YYYYMMDDXX). 19 | $plugin->release = '3.x dev'; // Human-friendly version name. 20 | $plugin->requires = 2017051200; // Requires Moodle 3.3. 21 | $plugin->maturity = MATURITY_ALPHA; // Alpha development code - not for production sites! 22 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-min.js: -------------------------------------------------------------------------------- 1 | YUI.add("moodle-mod_scheduler-studentlist",function(e,t){var n={EXPANDED:"expanded",COLLAPSED:"collapsed"};M.mod_scheduler=M.mod_scheduler||{},MOD=M.mod_scheduler.studentlist={},MOD.setState=function(t,r){image=e.one("#"+t),content=e.one("#list"+t),r?(content.removeClass(n.COLLAPSED),content.addClass(n.EXPANDED),image.set("src",M.util.image_url("t/expanded"))):(content.removeClass(n.EXPANDED),content.addClass(n.COLLAPSED),image.set("src",M.util.image_url("t/collapsed")))},MOD.toggleState=function(t){content=e.one("#list"+t),isVisible=content.hasClass(n.EXPANDED),this.setState(t,!isVisible)},MOD.init=function(t,n){this.setState(t,n),e.one("#"+t).on("click",function(e){M.mod_scheduler.studentlist.toggleState(t)})}},"@VERSION@",{requires:["base","node","event","io"]}); 2 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-min.js: -------------------------------------------------------------------------------- 1 | YUI.add("moodle-mod_scheduler-saveseen",function(e,t){var n={CHECKBOXES:"table#slotmanager form.studentselectform input.studentselect"},r;M.mod_scheduler=M.mod_scheduler||{},r=M.mod_scheduler.saveseen={},r.save_status=function(t,n,r,i){e.io(M.cfg.wwwroot+"/mod/scheduler/ajax.php",{data:{action:"saveseen",id:t,appointmentid:n,seen:r,sesskey:M.cfg.sesskey},timeout:5e3,on:{start:function(e){i.show()},success:function(e,t){window.setTimeout(function(){i.hide()},250)},failure:function(e,t){var n={name:t.status+" "+t.statusText,message:t.responseText};return i.hide(),new M.core.exception(n)}},context:this})},r.init=function(t){e.all(n.CHECKBOXES).each(function(n){n.on("change",function(r){var i=M.util.add_spinner(e,n.ancestor("div"));M.mod_scheduler.saveseen.save_status(t,n.get("value"),n.get("checked"),i)})})}},"@VERSION@",{requires:["base","node","event"]}); 2 | -------------------------------------------------------------------------------- /db/tasks.php: -------------------------------------------------------------------------------- 1 | 'mod_scheduler\task\send_reminders', 16 | 'minute' => 'R', 17 | 'hour' => '*', 18 | 'day' => '*', 19 | 'dayofweek' => '*', 20 | 'month' => '*' 21 | ), 22 | array( 23 | 'classname' => 'mod_scheduler\task\purge_unused_slots', 24 | 'minute' => '*/5', 25 | 'hour' => '*', 26 | 'day' => '*', 27 | 'dayofweek' => '*', 28 | 'month' => '*' 29 | ) 30 | ); -------------------------------------------------------------------------------- /yui/src/delselected/js/delselected.js: -------------------------------------------------------------------------------- 1 | var SELECTORS = { 2 | DELACTION: 'div.commandbar a#delselected', 3 | SELECTBOX: 'table#slotmanager input.slotselect' 4 | }, 5 | MOD; 6 | 7 | M.mod_scheduler = M.mod_scheduler || {}; 8 | MOD = M.mod_scheduler.delselected = {}; 9 | 10 | /** 11 | * Copy the selected boexs into an input parameter of the respective form 12 | * 13 | * @return void 14 | */ 15 | MOD.collect_selection = function(link, baseurl) { 16 | 17 | var sellist = ''; 18 | Y.all(SELECTORS.SELECTBOX).each( function(box) { 19 | if (box.get('checked')) { 20 | if (sellist.length > 0) { 21 | sellist += ','; 22 | } 23 | sellist += box.get('value'); 24 | } 25 | }); 26 | link.setAttribute('href', baseurl+'&items='+sellist); 27 | }; 28 | 29 | MOD.init = function(baseurl) { 30 | var link = Y.one(SELECTORS.DELACTION); 31 | if (link != null) { 32 | link.on('click', function(e) { 33 | M.mod_scheduler.delselected.collect_selection(link, baseurl); 34 | }); 35 | } 36 | }; -------------------------------------------------------------------------------- /yui/src/studentlist/js/studentlist.js: -------------------------------------------------------------------------------- 1 | 2 | var CSS = { 3 | EXPANDED: 'expanded', 4 | COLLAPSED: 'collapsed' 5 | }; 6 | 7 | M.mod_scheduler = M.mod_scheduler || {}; 8 | MOD = M.mod_scheduler.studentlist = {}; 9 | 10 | MOD.setState = function(id, expanded) { 11 | image = Y.one('#'+id); 12 | content = Y.one('#list'+id); 13 | if (expanded) { 14 | content.removeClass(CSS.COLLAPSED); 15 | content.addClass(CSS.EXPANDED); 16 | image.set('src', M.util.image_url('t/expanded')); 17 | } else { 18 | content.removeClass(CSS.EXPANDED); 19 | content.addClass(CSS.COLLAPSED); 20 | image.set('src', M.util.image_url('t/collapsed')); 21 | } 22 | }; 23 | 24 | MOD.toggleState = function(id) { 25 | content = Y.one('#list'+id); 26 | isVisible = content.hasClass(CSS.EXPANDED); 27 | this.setState(id, !isVisible); 28 | }; 29 | 30 | MOD.init = function(imageid, expanded) { 31 | this.setState(imageid, expanded); 32 | Y.one('#'+imageid).on('click', function(e){ 33 | M.mod_scheduler.studentlist.toggleState(imageid); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /classes/task/purge_unused_slots.php: -------------------------------------------------------------------------------- 1 | 0) { 23 | sellist += ','; 24 | } 25 | sellist += box.get('value'); 26 | } 27 | }); 28 | link.setAttribute('href', baseurl+'&items='+sellist); 29 | }; 30 | 31 | MOD.init = function(baseurl) { 32 | var link = Y.one(SELECTORS.DELACTION); 33 | if (link != null) { 34 | link.on('click', function(e) { 35 | M.mod_scheduler.delselected.collect_selection(link, baseurl); 36 | }); 37 | } 38 | }; 39 | 40 | }, '@VERSION@', {"requires": ["base", "node", "event"]}); 41 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-delselected/moodle-mod_scheduler-delselected-debug.js: -------------------------------------------------------------------------------- 1 | YUI.add('moodle-mod_scheduler-delselected', function (Y, NAME) { 2 | 3 | var SELECTORS = { 4 | DELACTION: 'div.commandbar a#delselected', 5 | SELECTBOX: 'table#slotmanager input.slotselect' 6 | }, 7 | MOD; 8 | 9 | M.mod_scheduler = M.mod_scheduler || {}; 10 | MOD = M.mod_scheduler.delselected = {}; 11 | 12 | /** 13 | * Copy the selected boexs into an input parameter of the respective form 14 | * 15 | * @return void 16 | */ 17 | MOD.collect_selection = function(link, baseurl) { 18 | 19 | var sellist = ''; 20 | Y.all(SELECTORS.SELECTBOX).each( function(box) { 21 | if (box.get('checked')) { 22 | if (sellist.length > 0) { 23 | sellist += ','; 24 | } 25 | sellist += box.get('value'); 26 | } 27 | }); 28 | link.setAttribute('href', baseurl+'&items='+sellist); 29 | }; 30 | 31 | MOD.init = function(baseurl) { 32 | var link = Y.one(SELECTORS.DELACTION); 33 | if (link != null) { 34 | link.on('click', function(e) { 35 | M.mod_scheduler.delselected.collect_selection(link, baseurl); 36 | }); 37 | } 38 | }; 39 | 40 | }, '@VERSION@', {"requires": ["base", "node", "event"]}); 41 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist.js: -------------------------------------------------------------------------------- 1 | YUI.add('moodle-mod_scheduler-studentlist', function (Y, NAME) { 2 | 3 | 4 | var CSS = { 5 | EXPANDED: 'expanded', 6 | COLLAPSED: 'collapsed' 7 | }; 8 | 9 | M.mod_scheduler = M.mod_scheduler || {}; 10 | MOD = M.mod_scheduler.studentlist = {}; 11 | 12 | MOD.setState = function(id, expanded) { 13 | image = Y.one('#'+id); 14 | content = Y.one('#list'+id); 15 | if (expanded) { 16 | content.removeClass(CSS.COLLAPSED); 17 | content.addClass(CSS.EXPANDED); 18 | image.set('src', M.util.image_url('t/expanded')); 19 | } else { 20 | content.removeClass(CSS.EXPANDED); 21 | content.addClass(CSS.COLLAPSED); 22 | image.set('src', M.util.image_url('t/collapsed')); 23 | } 24 | }; 25 | 26 | MOD.toggleState = function(id) { 27 | content = Y.one('#list'+id); 28 | isVisible = content.hasClass(CSS.EXPANDED); 29 | this.setState(id, !isVisible); 30 | }; 31 | 32 | MOD.init = function(imageid, expanded) { 33 | this.setState(imageid, expanded); 34 | Y.one('#'+imageid).on('click', function(e){ 35 | M.mod_scheduler.studentlist.toggleState(imageid); 36 | }); 37 | }; 38 | 39 | 40 | }, '@VERSION@', {"requires": ["base", "node", "event", "io"]}); 41 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-studentlist/moodle-mod_scheduler-studentlist-debug.js: -------------------------------------------------------------------------------- 1 | YUI.add('moodle-mod_scheduler-studentlist', function (Y, NAME) { 2 | 3 | 4 | var CSS = { 5 | EXPANDED: 'expanded', 6 | COLLAPSED: 'collapsed' 7 | }; 8 | 9 | M.mod_scheduler = M.mod_scheduler || {}; 10 | MOD = M.mod_scheduler.studentlist = {}; 11 | 12 | MOD.setState = function(id, expanded) { 13 | image = Y.one('#'+id); 14 | content = Y.one('#list'+id); 15 | if (expanded) { 16 | content.removeClass(CSS.COLLAPSED); 17 | content.addClass(CSS.EXPANDED); 18 | image.set('src', M.util.image_url('t/expanded')); 19 | } else { 20 | content.removeClass(CSS.EXPANDED); 21 | content.addClass(CSS.COLLAPSED); 22 | image.set('src', M.util.image_url('t/collapsed')); 23 | } 24 | }; 25 | 26 | MOD.toggleState = function(id) { 27 | content = Y.one('#list'+id); 28 | isVisible = content.hasClass(CSS.EXPANDED); 29 | this.setState(id, !isVisible); 30 | }; 31 | 32 | MOD.init = function(imageid, expanded) { 33 | this.setState(imageid, expanded); 34 | Y.one('#'+imageid).on('click', function(e){ 35 | M.mod_scheduler.studentlist.toggleState(imageid); 36 | }); 37 | }; 38 | 39 | 40 | }, '@VERSION@', {"requires": ["base", "node", "event", "io"]}); 41 | -------------------------------------------------------------------------------- /ajax.php: -------------------------------------------------------------------------------- 1 | get_record('course', array('id' => $cm->course), '*', MUST_EXIST); 21 | $scheduler = scheduler_instance::load_by_coursemodule_id($id); 22 | 23 | require_login($course, true, $cm); 24 | require_sesskey(); 25 | 26 | $return = 'OK'; 27 | 28 | switch ($action) { 29 | case 'saveseen': 30 | 31 | $appid = required_param('appointmentid', PARAM_INT); 32 | $slotid = $DB->get_field('scheduler_appointment', 'slotid', array('id' => $appid)); 33 | $slot = $scheduler->get_slot($slotid); 34 | $app = $slot->get_appointment($appid); 35 | $newseen = required_param('seen', PARAM_BOOL); 36 | 37 | if ($USER->id != $slot->teacherid) { 38 | require_capability('mod/scheduler:manageallappointments', $scheduler->context); 39 | } 40 | 41 | $app->attended = $newseen; 42 | $slot->save(); 43 | 44 | break; 45 | } 46 | 47 | echo json_encode($return); 48 | die; 49 | -------------------------------------------------------------------------------- /classes/event/slot_added.php: -------------------------------------------------------------------------------- 1 | set_slot($slot); 35 | return $event; 36 | } 37 | 38 | /** 39 | * Init method. 40 | */ 41 | protected function init() { 42 | $this->data['crud'] = 'c'; 43 | $this->data['edulevel'] = self::LEVEL_TEACHING; 44 | } 45 | 46 | /** 47 | * Returns localised general event name. 48 | * 49 | * @return string 50 | */ 51 | public static function get_name() { 52 | return get_string('event_slotadded', 'scheduler'); 53 | } 54 | 55 | /** 56 | * Returns non-localised event description with id's for admin use only. 57 | * 58 | * @return string 59 | */ 60 | public function get_description() { 61 | return "The user with id '$this->userid' created the slot with id '{$this->objectid}'" 62 | ." in the scheduler with course module id '$this->contextinstanceid'."; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /classes/event/booking_added.php: -------------------------------------------------------------------------------- 1 | set_slot($slot); 34 | return $event; 35 | } 36 | 37 | /** 38 | * Init method. 39 | */ 40 | protected function init() { 41 | $this->data['crud'] = 'c'; 42 | $this->data['edulevel'] = self::LEVEL_PARTICIPATING; 43 | } 44 | 45 | /** 46 | * Returns localised general event name. 47 | * 48 | * @return string 49 | */ 50 | public static function get_name() { 51 | return get_string('event_bookingadded', 'scheduler'); 52 | } 53 | 54 | /** 55 | * Returns non-localised event description with id's for admin use only. 56 | * 57 | * @return string 58 | */ 59 | public function get_description() { 60 | return "The user with id '$this->userid' has booked into the slot with id '{$this->objectid}'" 61 | ." in the scheduler with course module id '$this->contextinstanceid'."; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /yui/src/saveseen/js/saveseen.js: -------------------------------------------------------------------------------- 1 | var SELECTORS = { 2 | CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' 3 | }, 4 | MOD; 5 | 6 | M.mod_scheduler = M.mod_scheduler || {}; 7 | MOD = M.mod_scheduler.saveseen = {}; 8 | 9 | /** 10 | * Save the "seen" status. 11 | * 12 | * @param cmid the coursemodule id 13 | * @param appid the id of the relevant appointment 14 | * @param spinner The spinner icon shown while saving 15 | * @return void 16 | */ 17 | MOD.save_status = function(cmid, appid, newseen, spinner) { 18 | 19 | Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { 20 | // The request paramaters. 21 | data: { 22 | action: 'saveseen', 23 | id: cmid, 24 | appointmentid : appid, 25 | seen: newseen, 26 | sesskey: M.cfg.sesskey 27 | }, 28 | 29 | timeout: 5000, // 5 seconds of timeout. 30 | 31 | //Define the events. 32 | on: { 33 | start : function(transactionid) { 34 | spinner.show(); 35 | }, 36 | success : function(transactionid, xhr) { 37 | window.setTimeout(function() { 38 | spinner.hide(); 39 | }, 250); 40 | }, 41 | failure : function(transactionid, xhr) { 42 | var msg = { 43 | name : xhr.status+' '+xhr.statusText, 44 | message : xhr.responseText 45 | }; 46 | spinner.hide(); 47 | return new M.core.exception(msg); 48 | } 49 | }, 50 | context:this 51 | }); 52 | }; 53 | 54 | 55 | MOD.init = function(cmid) { 56 | Y.all(SELECTORS.CHECKBOXES).each( function(box) { 57 | box.on('change', function(e) { 58 | var spinner = M.util.add_spinner(Y, box.ancestor('div')); 59 | M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); 60 | }) 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /classes/event/booking_form_viewed.php: -------------------------------------------------------------------------------- 1 | set_scheduler($scheduler); 35 | return $event; 36 | } 37 | 38 | /** 39 | * Init method. 40 | */ 41 | protected function init() { 42 | $this->data['crud'] = 'r'; 43 | $this->data['edulevel'] = self::LEVEL_PARTICIPATING; 44 | } 45 | 46 | /** 47 | * Returns localised general event name. 48 | * 49 | * @return string 50 | */ 51 | public static function get_name() { 52 | return get_string('event_bookingformviewed', 'scheduler'); 53 | } 54 | 55 | /** 56 | * Returns non-localised event description with id's for admin use only. 57 | * 58 | * @return string 59 | */ 60 | public function get_description() { 61 | return "The user with id '$this->userid' has viewed the booking form in the scheduler with course module id '$this->contextinstanceid'."; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /classes/event/booking_removed.php: -------------------------------------------------------------------------------- 1 | set_slot($slot); 35 | return $event; 36 | } 37 | 38 | /** 39 | * Init method. 40 | */ 41 | protected function init() { 42 | $this->data['crud'] = 'd'; 43 | $this->data['edulevel'] = self::LEVEL_PARTICIPATING; 44 | } 45 | 46 | /** 47 | * Returns localised general event name. 48 | * 49 | * @return string 50 | */ 51 | public static function get_name() { 52 | return get_string('event_bookingremoved', 'scheduler'); 53 | } 54 | 55 | /** 56 | * Returns non-localised event description with id's for admin use only. 57 | * 58 | * @return string 59 | */ 60 | public function get_description() { 61 | return "The user with id '$this->userid' has removed their booking from the slot with id '{$this->objectid}'" 62 | ." in the scheduler with course module id '$this->contextinstanceid'."; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /classes/event/appointment_list_viewed.php: -------------------------------------------------------------------------------- 1 | set_scheduler($scheduler); 34 | return $event; 35 | } 36 | 37 | /** 38 | * Init method. 39 | */ 40 | protected function init() { 41 | $this->data['crud'] = 'r'; 42 | $this->data['edulevel'] = self::LEVEL_TEACHING; 43 | } 44 | 45 | /** 46 | * Returns localised general event name. 47 | * 48 | * @return string 49 | */ 50 | public static function get_name() { 51 | return get_string('event_appointmentlistviewed', 'scheduler'); 52 | } 53 | 54 | /** 55 | * Returns non-localised event description with id's for admin use only. 56 | * 57 | * @return string 58 | */ 59 | public function get_description() { 60 | return "The user with id '$this->userid' has viewed the list of appointments in the scheduler with course module id '$this->contextinstanceid'."; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen.js: -------------------------------------------------------------------------------- 1 | YUI.add('moodle-mod_scheduler-saveseen', function (Y, NAME) { 2 | 3 | var SELECTORS = { 4 | CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' 5 | }, 6 | MOD; 7 | 8 | M.mod_scheduler = M.mod_scheduler || {}; 9 | MOD = M.mod_scheduler.saveseen = {}; 10 | 11 | /** 12 | * Save the "seen" status. 13 | * 14 | * @param cmid the coursemodule id 15 | * @param appid the id of the relevant appointment 16 | * @param spinner The spinner icon shown while saving 17 | * @return void 18 | */ 19 | MOD.save_status = function(cmid, appid, newseen, spinner) { 20 | 21 | Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { 22 | // The request paramaters. 23 | data: { 24 | action: 'saveseen', 25 | id: cmid, 26 | appointmentid : appid, 27 | seen: newseen, 28 | sesskey: M.cfg.sesskey 29 | }, 30 | 31 | timeout: 5000, // 5 seconds of timeout. 32 | 33 | //Define the events. 34 | on: { 35 | start : function(transactionid) { 36 | spinner.show(); 37 | }, 38 | success : function(transactionid, xhr) { 39 | window.setTimeout(function() { 40 | spinner.hide(); 41 | }, 250); 42 | }, 43 | failure : function(transactionid, xhr) { 44 | var msg = { 45 | name : xhr.status+' '+xhr.statusText, 46 | message : xhr.responseText 47 | }; 48 | spinner.hide(); 49 | return new M.core.exception(msg); 50 | } 51 | }, 52 | context:this 53 | }); 54 | }; 55 | 56 | 57 | MOD.init = function(cmid) { 58 | Y.all(SELECTORS.CHECKBOXES).each( function(box) { 59 | box.on('change', function(e) { 60 | var spinner = M.util.add_spinner(Y, box.ancestor('div')); 61 | M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); 62 | }) 63 | }); 64 | }; 65 | 66 | 67 | }, '@VERSION@', {"requires": ["base", "node", "event"]}); 68 | -------------------------------------------------------------------------------- /yui/build/moodle-mod_scheduler-saveseen/moodle-mod_scheduler-saveseen-debug.js: -------------------------------------------------------------------------------- 1 | YUI.add('moodle-mod_scheduler-saveseen', function (Y, NAME) { 2 | 3 | var SELECTORS = { 4 | CHECKBOXES: 'table#slotmanager form.studentselectform input.studentselect' 5 | }, 6 | MOD; 7 | 8 | M.mod_scheduler = M.mod_scheduler || {}; 9 | MOD = M.mod_scheduler.saveseen = {}; 10 | 11 | /** 12 | * Save the "seen" status. 13 | * 14 | * @param cmid the coursemodule id 15 | * @param appid the id of the relevant appointment 16 | * @param spinner The spinner icon shown while saving 17 | * @return void 18 | */ 19 | MOD.save_status = function(cmid, appid, newseen, spinner) { 20 | 21 | Y.io(M.cfg.wwwroot + '/mod/scheduler/ajax.php', { 22 | // The request paramaters. 23 | data: { 24 | action: 'saveseen', 25 | id: cmid, 26 | appointmentid : appid, 27 | seen: newseen, 28 | sesskey: M.cfg.sesskey 29 | }, 30 | 31 | timeout: 5000, // 5 seconds of timeout. 32 | 33 | //Define the events. 34 | on: { 35 | start : function(transactionid) { 36 | spinner.show(); 37 | }, 38 | success : function(transactionid, xhr) { 39 | window.setTimeout(function() { 40 | spinner.hide(); 41 | }, 250); 42 | }, 43 | failure : function(transactionid, xhr) { 44 | var msg = { 45 | name : xhr.status+' '+xhr.statusText, 46 | message : xhr.responseText 47 | }; 48 | spinner.hide(); 49 | return new M.core.exception(msg); 50 | } 51 | }, 52 | context:this 53 | }); 54 | }; 55 | 56 | 57 | MOD.init = function(cmid) { 58 | Y.all(SELECTORS.CHECKBOXES).each( function(box) { 59 | box.on('change', function(e) { 60 | var spinner = M.util.add_spinner(Y, box.ancestor('div')); 61 | M.mod_scheduler.saveseen.save_status(cmid, box.get('value'), box.get('checked'), spinner); 62 | }) 63 | }); 64 | }; 65 | 66 | 67 | }, '@VERSION@', {"requires": ["base", "node", "event"]}); 68 | -------------------------------------------------------------------------------- /customlib.php: -------------------------------------------------------------------------------- 1 | title : Displayable title of the field 22 | * $field->value : Value of the field for this user (not set if $user is null) 23 | * 24 | * @param stdClass $user the user record; may be null 25 | * @param context $context context for permission checks 26 | * @return array an array of field objects 27 | */ 28 | function scheduler_get_user_fields($user, $context) { 29 | 30 | $fields = array(); 31 | 32 | if (has_capability('moodle/site:viewuseridentity', $context)) { 33 | $emailfield = new stdClass(); 34 | $fields[] = $emailfield; 35 | $emailfield->title = get_string('email'); 36 | if ($user) { 37 | $emailfield->value = obfuscate_mailto($user->email); 38 | } 39 | } 40 | 41 | /* 42 | * As an example: Uncomment the following lines in order to display the user's city and country. 43 | */ 44 | 45 | /* 46 | $cityfield = new stdClass(); 47 | $cityfield->title = get_string('city'); 48 | $fields[] = $cityfield; 49 | 50 | $countryfield = new stdClass(); 51 | $countryfield->title = get_string('country'); 52 | $fields[] = $countryfield; 53 | 54 | if ($user) { 55 | $cityfield->value = $user->city; 56 | if ($user->country) { 57 | $countryfield->value = get_string($user->country, 'countries'); 58 | } 59 | else { 60 | $countryfield->value = ''; 61 | } 62 | } 63 | */ 64 | return $fields; 65 | } 66 | -------------------------------------------------------------------------------- /classes/event/slot_deleted.php: -------------------------------------------------------------------------------- 1 | $action); 35 | $event = self::create($data); 36 | $event->set_slot($slot); 37 | return $event; 38 | } 39 | 40 | /** 41 | * Init method. 42 | */ 43 | protected function init() { 44 | $this->data['crud'] = 'd'; 45 | $this->data['edulevel'] = self::LEVEL_TEACHING; 46 | } 47 | 48 | /** 49 | * Returns localised general event name. 50 | * 51 | * @return string 52 | */ 53 | public static function get_name() { 54 | return get_string('event_slotdeleted', 'scheduler'); 55 | } 56 | 57 | /** 58 | * Returns non-localised event description with id's for admin use only. 59 | * 60 | * @return string 61 | */ 62 | public function get_description() { 63 | $desc = "The user with id '$this->userid' deleted the slot with id '{$this->objectid}'" 64 | ." in the scheduler with course module id '$this->contextinstanceid'"; 65 | if ($act = $this->other['action']) { 66 | $desc .= " during action '$act'"; 67 | } 68 | $desc .= '.'; 69 | return $desc; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /backup/moodle2/backup_scheduler_activity_task.class.php: -------------------------------------------------------------------------------- 1 | dirroot . '/mod/scheduler/backup/moodle2/backup_scheduler_stepslib.php'); 14 | 15 | /** 16 | * Scheduler backup task that provides all the settings and steps to perform one 17 | * complete backup of the activity. 18 | * 19 | * @copyright 2016 Henning Bostelmann and others (see README.txt) 20 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 21 | */ 22 | class backup_scheduler_activity_task extends backup_activity_task { 23 | 24 | /** 25 | * Define (add) particular settings this activity can have 26 | */ 27 | protected function define_my_settings() { 28 | // No particular settings for this activity. 29 | } 30 | 31 | /** 32 | * Define (add) particular steps this activity can have 33 | */ 34 | protected function define_my_steps() { 35 | // Scheduler only has one structure step. 36 | $this->add_step(new backup_scheduler_activity_structure_step('scheduler_structure', 'scheduler.xml')); 37 | } 38 | 39 | /** 40 | * Code the transformations to perform in the activity in 41 | * order to get transportable (encoded) links 42 | * 43 | * @param string $content some HTML text that eventually contains URLs to the activity instance scripts 44 | */ 45 | static public function encode_content_links($content) { 46 | global $CFG; 47 | 48 | $base = preg_quote($CFG->wwwroot, "/"); 49 | 50 | // Link to the list of schedulers. 51 | $search = "/(".$base."\/mod\/scheduler\/index.php\?id\=)([0-9]+)/"; 52 | $content = preg_replace($search, '$@SCHEDULERINDEX*$2@$', $content); 53 | 54 | // Link to scheduler view by coursemoduleid. 55 | $search = "/(".$base."\/mod\/scheduler\/view.php\?id\=)([0-9]+)/"; 56 | $content = preg_replace($search, '$@SCHEDULERVIEWBYID*$2@$', $content); 57 | 58 | return $content; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | fulltree) { 14 | 15 | require_once($CFG->dirroot.'/mod/scheduler/lib.php'); 16 | 17 | $settings->add(new admin_setting_configcheckbox('mod_scheduler/allteachersgrading', 18 | get_string('allteachersgrading', 'scheduler'), 19 | get_string('allteachersgrading_desc', 'scheduler'), 20 | 0)); 21 | 22 | $settings->add(new admin_setting_configcheckbox('mod_scheduler/showemailplain', 23 | get_string('showemailplain', 'scheduler'), 24 | get_string('showemailplain_desc', 'scheduler'), 25 | 0)); 26 | 27 | $settings->add(new admin_setting_configcheckbox('mod_scheduler/groupscheduling', 28 | get_string('groupscheduling', 'scheduler'), 29 | get_string('groupscheduling_desc', 'scheduler'), 30 | 1)); 31 | 32 | $settings->add(new admin_setting_configcheckbox('mod_scheduler/mixindivgroup', 33 | get_string('mixindivgroup', 'scheduler'), 34 | get_string('mixindivgroup_desc', 'scheduler'), 35 | 1)); 36 | 37 | $settings->add(new admin_setting_configtext('mod_scheduler/maxstudentlistsize', 38 | get_string('maxstudentlistsize', 'scheduler'), 39 | get_string('maxstudentlistsize_desc', 'scheduler'), 40 | 200, PARAM_INT)); 41 | 42 | $settings->add(new admin_setting_configtext('mod_scheduler/uploadmaxfiles', 43 | get_string('uploadmaxfilesglobal', 'scheduler'), 44 | get_string('uploadmaxfilesglobal_desc', 'scheduler'), 45 | 5, PARAM_INT)); 46 | 47 | $settings->add(new admin_setting_configcheckbox('mod_scheduler/revealteachernotes', 48 | get_string('revealteachernotes', 'scheduler'), 49 | get_string('revealteachernotes_desc', 'scheduler'), 50 | 0)); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /classes/task/send_reminders.php: -------------------------------------------------------------------------------- 1 | 0 AND emaildate <= ? AND starttime > ?'; 41 | $slots = $DB->get_records_select('scheduler_slots', $select, array($date, $date), 'starttime'); 42 | 43 | foreach ($slots as $slot) { 44 | // Get teacher record. 45 | $teacher = $DB->get_record('user', array('id' => $slot->teacherid)); 46 | 47 | // Get scheduler, slot and course. 48 | $scheduler = \scheduler_instance::load_by_id($slot->schedulerid); 49 | $slotm = $scheduler->get_slot($slot->id); 50 | $course = $scheduler->get_courserec(); 51 | 52 | // Mark as sent. (Do this first for safe fallback in case of an exception.) 53 | $slot->emaildate = -1; 54 | $DB->update_record('scheduler_slots', $slot); 55 | 56 | // Send reminder to all students in the slot. 57 | foreach ($slotm->get_appointments() as $appointment) { 58 | $student = $DB->get_record('user', array('id' => $appointment->studentid)); 59 | cron_setup_user($student, $course); 60 | \scheduler_messenger::send_slot_notification($slotm, 61 | 'reminder', 'reminder', $teacher, $student, $teacher, $student, $course); 62 | } 63 | } 64 | cron_setup_user(); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /classes/event/slot_base.php: -------------------------------------------------------------------------------- 1 | $slot->get_scheduler()->get_context(), 37 | 'objectid' => $slot->id, 38 | 'relateduserid' => $slot->teacherid 39 | ); 40 | } 41 | 42 | /** 43 | * Set the slot associated with this event 44 | * 45 | * @param \scheduler_slot $slot 46 | */ 47 | protected function set_slot(\scheduler_slot $slot) { 48 | $this->add_record_snapshot('scheduler_slots', $slot->data); 49 | $this->add_record_snapshot('scheduler', $slot->get_scheduler()->data); 50 | $this->slot = $slot; 51 | $this->data['objecttable'] = 'scheduler_slots'; 52 | } 53 | 54 | /** 55 | * Get slot object. 56 | * 57 | * NOTE: to be used from observers only. 58 | * 59 | * @throws \coding_exception 60 | * @return \scheduler_slot 61 | */ 62 | public function get_slot() { 63 | if ($this->is_restored()) { 64 | throw new \coding_exception('get_slot() is intended for event observers only'); 65 | } 66 | return $this->slot; 67 | } 68 | 69 | /** 70 | * Returns relevant URL. 71 | * 72 | * @return \moodle_url 73 | */ 74 | public function get_url() { 75 | return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); 76 | } 77 | 78 | /** 79 | * Custom validation. 80 | * 81 | * @throws \coding_exception 82 | */ 83 | protected function validate_data() { 84 | parent::validate_data(); 85 | 86 | if ($this->contextlevel != CONTEXT_MODULE) { 87 | throw new \coding_exception('Context level must be CONTEXT_MODULE.'); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /classes/event/appointment_base.php: -------------------------------------------------------------------------------- 1 | $appointment->get_parent()->get_context(), 38 | 'objectid' => $appointment->id 39 | ); 40 | } 41 | 42 | /** 43 | * Set data of the event from an appointment record. 44 | * 45 | * @param \scheduler_appointment $appointment 46 | */ 47 | protected function set_appointment(\scheduler_appointment $appointment) { 48 | $this->add_record_snapshot('scheduler_appointment', $appointment->data); 49 | $this->add_record_snapshot('scheduler_slots', $appointment->get_parent()->data); 50 | $this->add_record_snapshot('scheduler', $appointment->get_parent()->get_parent()->data); 51 | $this->appointment = $appointment; 52 | $this->data['objecttable'] = 'scheduler_appointments'; 53 | } 54 | 55 | /** 56 | * Get appointment object. 57 | * 58 | * NOTE: to be used from observers only. 59 | * 60 | * @throws \coding_exception 61 | * @return \scheduler_appointment 62 | */ 63 | public function get_appointment() { 64 | if ($this->is_restored()) { 65 | throw new \coding_exception('get_appointment() is intended for event observers only'); 66 | } 67 | return $this->appointment; 68 | } 69 | 70 | /** 71 | * Returns relevant URL. 72 | * 73 | * @return \moodle_url 74 | */ 75 | public function get_url() { 76 | return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); 77 | } 78 | 79 | /** 80 | * Custom validation. 81 | * 82 | * @throws \coding_exception 83 | */ 84 | protected function validate_data() { 85 | parent::validate_data(); 86 | 87 | if ($this->contextlevel != CONTEXT_MODULE) { 88 | throw new \coding_exception('Context level must be CONTEXT_MODULE.'); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | get_record('course', array('id' => $id), '*', MUST_EXIST); 17 | 18 | $PAGE->set_url('/mod/scheduler/index.php', array('id' => $id)); 19 | $PAGE->set_pagelayout('incourse'); 20 | 21 | $coursecontext = context_course::instance($id); 22 | require_login($course->id); 23 | 24 | $event = \mod_scheduler\event\course_module_instance_list_viewed::create(array( 25 | 'context' => $coursecontext 26 | )); 27 | $event->add_record_snapshot('course', $course); 28 | $event->trigger(); 29 | 30 | // Get all required strings. 31 | 32 | $strschedulers = get_string('modulenameplural', 'scheduler'); 33 | $strscheduler = get_string('modulename', 'scheduler'); 34 | 35 | // Print the header. 36 | 37 | $title = $course->shortname . ': ' . $strschedulers; 38 | $PAGE->set_title($title); 39 | $PAGE->set_heading($course->fullname); 40 | echo $OUTPUT->header($course); 41 | 42 | 43 | // Get all the appropriate data. 44 | 45 | if (!$schedulers = get_all_instances_in_course('scheduler', $course)) { 46 | notice(get_string('noschedulers', 'scheduler'), "../../course/view.php?id=$course->id"); 47 | die; 48 | } 49 | 50 | // Print the list of instances. 51 | 52 | $timenow = time(); 53 | $strname = get_string('name'); 54 | $strweek = get_string('week'); 55 | $strtopic = get_string('topic'); 56 | 57 | $table = new html_table(); 58 | 59 | if ($course->format == 'weeks') { 60 | $table->head = array ($strweek, $strname); 61 | $table->align = array ('CENTER', 'LEFT'); 62 | } else if ($course->format == 'topics') { 63 | $table->head = array ($strtopic, $strname); 64 | $table->align = array ('CENTER', 'LEFT', 'LEFT', 'LEFT'); 65 | } else { 66 | $table->head = array ($strname); 67 | $table->align = array ('LEFT', 'LEFT', 'LEFT'); 68 | } 69 | 70 | foreach ($schedulers as $scheduler) { 71 | $url = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->coursemodule)); 72 | // Show dimmed if the mod is hidden. 73 | $attr = $scheduler->visible ? null : array('class' => 'dimmed'); 74 | $link = html_writer::link($url, $scheduler->name, $attr); 75 | if ($scheduler->visible or has_capability('moodle/course:viewhiddenactivities', $coursecontext)) { 76 | if ($course->format == 'weeks' or $course->format == 'topics') { 77 | $table->data[] = array ($scheduler->section, $link); 78 | } else { 79 | $table->data[] = array ($link); 80 | } 81 | } 82 | } 83 | 84 | echo html_writer::table($table); 85 | 86 | // Finish the page. 87 | 88 | echo $OUTPUT->footer($course); 89 | 90 | -------------------------------------------------------------------------------- /view.php: -------------------------------------------------------------------------------- 1 | dirroot.'/mod/scheduler/lib.php'); 14 | require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); 15 | require_once($CFG->dirroot.'/mod/scheduler/renderable.php'); 16 | 17 | // Read common request parameters. 18 | $id = optional_param('id', '', PARAM_INT); // Course Module ID - if it's not specified, must specify 'a', see below. 19 | $action = optional_param('what', 'view', PARAM_ALPHA); 20 | $subaction = optional_param('subaction', '', PARAM_ALPHA); 21 | $offset = optional_param('offset', -1, PARAM_INT); 22 | 23 | if ($id) { 24 | $cm = get_coursemodule_from_id('scheduler', $id, 0, false, MUST_EXIST); 25 | $scheduler = scheduler_instance::load_by_coursemodule_id($id); 26 | } else { 27 | $a = required_param('a', PARAM_INT); // Scheduler ID. 28 | $scheduler = scheduler_instance::load_by_id($a); 29 | $cm = $scheduler->get_cm(); 30 | } 31 | $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); 32 | 33 | $defaultsubpage = groups_get_activity_groupmode($cm) ? 'myappointments' : 'allappointments'; 34 | $subpage = optional_param('subpage', $defaultsubpage, PARAM_ALPHA); 35 | 36 | require_login($course->id, true, $cm); 37 | $context = context_module::instance($cm->id); 38 | 39 | // Initialize $PAGE, compute blocks. 40 | $PAGE->set_url('/mod/scheduler/view.php', array('id' => $cm->id)); 41 | 42 | $output = $PAGE->get_renderer('mod_scheduler'); 43 | 44 | // Print the page header. 45 | 46 | $title = $course->shortname . ': ' . format_string($scheduler->name); 47 | $PAGE->set_title($title); 48 | $PAGE->set_heading($course->fullname); 49 | 50 | 51 | // Route to screen. 52 | 53 | $isteacher = has_capability('mod/scheduler:manage', $context); 54 | $isstudent = has_capability('mod/scheduler:viewslots', $context); 55 | if ($isteacher) { 56 | // Teacher side. 57 | if ($action == 'viewstatistics') { 58 | include($CFG->dirroot.'/mod/scheduler/viewstatistics.php'); 59 | } else if ($action == 'viewstudent') { 60 | include($CFG->dirroot.'/mod/scheduler/viewstudent.php'); 61 | } else if ($action == 'export') { 62 | include($CFG->dirroot.'/mod/scheduler/export.php'); 63 | } else if ($action == 'datelist') { 64 | include($CFG->dirroot.'/mod/scheduler/datelist.php'); 65 | } else { 66 | include($CFG->dirroot.'/mod/scheduler/teacherview.php'); 67 | } 68 | 69 | } else if ($isstudent) { 70 | // Student side. 71 | include($CFG->dirroot.'/mod/scheduler/studentview.php'); 72 | 73 | } else { 74 | // For guests. 75 | echo $OUTPUT->header(); 76 | echo $OUTPUT->box(get_string('guestscantdoanything', 'scheduler'), 'generalbox'); 77 | echo $OUTPUT->footer($course); 78 | } 79 | -------------------------------------------------------------------------------- /pix/attachment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /classes/event/scheduler_base.php: -------------------------------------------------------------------------------- 1 | $scheduler->get_context(), 45 | 'objectid' => $scheduler->id 46 | ); 47 | } 48 | 49 | /** 50 | * Set the scheduler associated with this event. 51 | * 52 | * @param \scheduler_instance $scheduler 53 | */ 54 | protected function set_scheduler(\scheduler_instance $scheduler) { 55 | $this->add_record_snapshot('scheduler', $scheduler->data); 56 | $this->scheduler = $scheduler; 57 | $this->data['objecttable'] = 'scheduler'; 58 | } 59 | 60 | /** 61 | * Get scheduler instance. 62 | * 63 | * NOTE: to be used from observers only. 64 | * 65 | * @throws \coding_exception 66 | * @return \scheduler_instance 67 | */ 68 | public function get_scheduler() { 69 | if ($this->is_restored()) { 70 | throw new \coding_exception('get_scheduler() is intended for event observers only'); 71 | } 72 | if (!isset($this->scheduler)) { 73 | debugging('scheduler property should be initialised in each event', DEBUG_DEVELOPER); 74 | global $CFG; 75 | require_once($CFG->dirroot . '/mod/scheduler/locallib.php'); 76 | $this->scheduler = \scheduler_instance::load_by_coursemodule_id($this->contextinstanceid); 77 | } 78 | return $this->scheduler; 79 | } 80 | 81 | 82 | /** 83 | * Returns relevant URL. 84 | * 85 | * @return \moodle_url 86 | */ 87 | public function get_url() { 88 | return new \moodle_url('/mod/scheduler/view.php', array('id' => $this->contextinstanceid)); 89 | } 90 | 91 | /** 92 | * Init method. 93 | */ 94 | protected function init() { 95 | $this->data['objecttable'] = 'scheduler'; 96 | } 97 | 98 | /** 99 | * Custom validation. 100 | * 101 | * @throws \coding_exception 102 | */ 103 | protected function validate_data() { 104 | parent::validate_data(); 105 | 106 | if ($this->contextlevel != CONTEXT_MODULE) { 107 | throw new \coding_exception('Context level must be CONTEXT_MODULE.'); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /backup/moodle2/restore_scheduler_activity_task.class.php: -------------------------------------------------------------------------------- 1 | dirroot . '/mod/scheduler/backup/moodle2/restore_scheduler_stepslib.php'); 15 | 16 | /** 17 | * scheduler restore task that provides all the settings and steps to perform one 18 | * complete restore of the activity 19 | * 20 | * @copyright 2016 Henning Bostelmann and others (see README.txt) 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | class restore_scheduler_activity_task extends restore_activity_task { 24 | 25 | /** 26 | * Define (add) particular settings this activity can have 27 | */ 28 | protected function define_my_settings() { 29 | // No particular settings for this activity. 30 | } 31 | 32 | /** 33 | * Define (add) particular steps this activity can have 34 | */ 35 | protected function define_my_steps() { 36 | // Scheduler has only one structure step. 37 | $this->add_step(new restore_scheduler_activity_structure_step('scheduler_structure', 'scheduler.xml')); 38 | } 39 | 40 | /** 41 | * Define the contents in the activity that must be 42 | * processed by the link decoder 43 | */ 44 | static public function define_decode_contents() { 45 | $contents = array(); 46 | 47 | $contents[] = new restore_decode_content('scheduler', array('intro'), 'scheduler'); 48 | 49 | return $contents; 50 | } 51 | 52 | /** 53 | * Define the decoding rules for links belonging 54 | * to the activity to be executed by the link decoder 55 | */ 56 | static public function define_decode_rules() { 57 | $rules = array(); 58 | 59 | $rules[] = new restore_decode_rule('SCHEDULERVIEWBYID', '/mod/scheduler/view.php?id=$1', 'course_module'); 60 | $rules[] = new restore_decode_rule('SCHEDULERINDEX', '/mod/scheduler/index.php?id=$1', 'course'); 61 | 62 | return $rules; 63 | 64 | } 65 | 66 | /** 67 | * Define the restore log rules that will be applied 68 | * by the {@link restore_logs_processor} when restoring 69 | * scheduler logs. It must return one array 70 | * of {@link restore_log_rule} objects 71 | */ 72 | static public function define_restore_log_rules() { 73 | $rules = array(); 74 | 75 | $rules[] = new restore_log_rule('scheduler', 'add', 'view.php?id={course_module}', '{scheduler}'); 76 | $rules[] = new restore_log_rule('scheduler', 'update', 'view.php?id={course_module}', '{scheduler}'); 77 | $rules[] = new restore_log_rule('scheduler', 'view', 'view.php?id={course_module}', '{scheduler}'); 78 | 79 | return $rules; 80 | } 81 | 82 | /** 83 | * Define the restore log rules that will be applied 84 | * by the {@link restore_logs_processor} when restoring 85 | * course logs. It must return one array 86 | * of {@link restore_log_rule} objects 87 | * 88 | * Note this rules are applied when restoring course logs 89 | * by the restore final task, but are defined here at 90 | * activity level. All them are rules not linked to any module instance (cmid = 0) 91 | */ 92 | static public function define_restore_log_rules_for_course() { 93 | $rules = array(); 94 | 95 | $rules[] = new restore_log_rule('scheduler', 'view all', 'index.php?id={course}', null); 96 | 97 | return $rules; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /message_form.php: -------------------------------------------------------------------------------- 1 | libdir.'/formslib.php'); 14 | 15 | /** 16 | * Message form for invitations 17 | * (using Moodle formslib) 18 | * 19 | * @package mod_scheduler 20 | * @copyright 2016 Henning Bostelmann and others (see README.txt) 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | class scheduler_message_form extends moodleform { 24 | 25 | /** 26 | * @var scheduler_instance scheduler in whose context the messages are sent 27 | */ 28 | protected $scheduler; 29 | 30 | /** 31 | * Create a new messge form 32 | * 33 | * @param string $action 34 | * @param scheduler_instance $scheduler scheduler in whose context the messages are sent 35 | * @param object $customdata 36 | */ 37 | public function __construct($action, scheduler_instance $scheduler, $customdata=null) { 38 | $this->scheduler = $scheduler; 39 | parent::__construct($action, $customdata); 40 | } 41 | 42 | protected function definition() { 43 | 44 | $mform = $this->_form; 45 | 46 | // Select users to sent the message to. 47 | $checkboxes = array(); 48 | $recipients = $this->_customdata['recipients']; 49 | foreach ($recipients as $recipient) { 50 | $inputid = 'recipient['.$recipient->id.']'; 51 | $label = fullname($recipient); 52 | $checkboxes[] = $mform->createElement('checkbox', $inputid, '', $label); 53 | $mform->setDefault($inputid, true); 54 | } 55 | $mform->addGroup($checkboxes, 'recipients', get_string('recipients', 'scheduler'), null, false); 56 | 57 | if (get_config('mod_scheduler', 'showemailplain')) { 58 | $maillist = array(); 59 | foreach ($recipients as $recipient) { 60 | $maillist[] = trim($recipient->email); 61 | } 62 | $maildisplay = html_writer::div(implode(', ', $maillist)); 63 | $mform->addElement('html', $maildisplay); 64 | } 65 | 66 | $mform->addElement('selectyesno', 'copytomyself', get_string('copytomyself', 'scheduler')); 67 | $mform->setDefault('copytomyself', true); 68 | 69 | $mform->addElement('text', 'subject', get_string('messagesubject', 'scheduler'), array('size' => '60')); 70 | $mform->setType('subject', PARAM_TEXT); 71 | $mform->addRule('subject', null, 'required'); 72 | if (isset($this->_customdata['subject'])) { 73 | $mform->setDefault('subject', $this->_customdata['subject']); 74 | } 75 | 76 | $bodyedit = $mform->addElement('editor', 'body', get_string('messagebody', 'scheduler'), 77 | array('rows' => 15, 'columns' => 60), array('collapsed' => true)); 78 | $mform->setType('body', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. 79 | if (isset($this->_customdata['body'])) { 80 | $bodyedit->setValue(array('text' => $this->_customdata['body'])); 81 | } 82 | 83 | $buttonarray = array(); 84 | $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('sendmessage', 'scheduler')); 85 | $buttonarray[] = $mform->createElement('cancel'); 86 | $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); 87 | 88 | } 89 | 90 | public function validation($data, $files) { 91 | $errors = parent::validation($data, $files); 92 | 93 | return $errors; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /tests/behat/group_availability.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: As a teacher I need to see an accurate list of users to be scheduled 3 | In order to see who needs to schedule an appointment 4 | As a teacher 5 | I need to view the table of students in the teacher view 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher | Teacher | Teacher | teacher@example.com | 11 | | student1 | Student | 1 | student.1@example.com | 12 | | student2 | Student | 2 | student.2@example.com | 13 | | student3 | Student | 3 | student.3@example.com | 14 | And the following "courses" exist: 15 | | fullname | shortname | category | 16 | | Course 1 | C1 | 0 | 17 | And the following "course enrolments" exist: 18 | | user | course | role | 19 | | teacher | C1 | editingteacher | 20 | | student1 | C1 | student | 21 | | student2 | C1 | student | 22 | | student3 | C1 | student | 23 | And the following "groups" exist: 24 | | name | course | idnumber | 25 | | Group 1 | C1 | G1 | 26 | | Group 2 | C1 | G2 | 27 | And the following "group members" exist: 28 | | user | group | 29 | | student1 | G1 | 30 | | student2 | G2 | 31 | And the following "groupings" exist: 32 | | name | course | idnumber | 33 | | Grouping 1 | C1 | GG1 | 34 | And the following "grouping groups" exist: 35 | | grouping | group | 36 | | GG1 | G1 | 37 | And the following config values are set as admin: 38 | | enableavailability | 1 | 39 | And the following "activities" exist: 40 | | activity | name | intro | course | idnumber | 41 | | scheduler | Test scheduler | n | C1 | scheduler1 | 42 | And I log in as "teacher" 43 | And I am on "Course 1" course homepage 44 | 45 | @javascript 46 | Scenario: A scheduler that is restricted to a single group 47 | When I follow "Test scheduler" 48 | Then I should see "Student 1" in the "studentstoschedule" "table" 49 | And I should see "Student 2" in the "studentstoschedule" "table" 50 | And I should see "Student 3" in the "studentstoschedule" "table" 51 | 52 | When I navigate to "Edit settings" in current page administration 53 | And I expand all fieldsets 54 | And I click on "Add restriction..." "button" 55 | And I click on "Group" "button" in the "Add restriction..." "dialogue" 56 | And I set the field with xpath "//select[@name='id']" to "Group 2" 57 | And I press "Save and display" 58 | Then I should not see "Student 1" in the "studentstoschedule" "table" 59 | And I should see "Student 2" in the "studentstoschedule" "table" 60 | And I should not see "Student 3" in the "studentstoschedule" "table" 61 | 62 | @javascript 63 | Scenario: A scheduler that is restricted to a grouping 64 | When I follow "Test scheduler" 65 | Then I should see "Student 1" in the "studentstoschedule" "table" 66 | And I should see "Student 2" in the "studentstoschedule" "table" 67 | And I should see "Student 3" in the "studentstoschedule" "table" 68 | 69 | When I navigate to "Edit settings" in current page administration 70 | And I expand all fieldsets 71 | And I click on "Add restriction..." "button" 72 | And I click on "Grouping" "button" in the "Add restriction..." "dialogue" 73 | And I set the field with xpath "//select[@name='id']" to "Grouping 1" 74 | And I press "Save and display" 75 | Then I should see "Student 1" in the "studentstoschedule" "table" 76 | And I should not see "Student 2" in the "studentstoschedule" "table" 77 | And I should not see "Student 3" in the "studentstoschedule" "table" 78 | -------------------------------------------------------------------------------- /tests/behat/studentdata.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler @javascript @_file_upload 2 | Feature: Student-supplied data 3 | In order to collect data from students 4 | As a teacher 5 | I can configure a booking form for the scheduler. 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | | student2 | Student | 2 | student2@example.com | 13 | And the following "courses" exist: 14 | | fullname | shortname | category | 15 | | Course 1 | C1 | 0 | 16 | And the following "course enrolments" exist: 17 | | user | course | role | 18 | | teacher1 | C1 | editingteacher | 19 | | student1 | C1 | student | 20 | | student2 | C1 | student | 21 | And the following "activities" exist: 22 | | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | 23 | | scheduler | Test scheduler | n | C1 | scheduler1 | 0 | oneonly | 0 | 24 | 25 | @javascript 26 | Scenario: A teacher configures a booking form, and students enter data 27 | When I log in as "teacher1" 28 | And I am on "Course 1" course homepage 29 | And I follow "Test scheduler" 30 | And I navigate to "Edit settings" in current page administration 31 | And I expand all fieldsets 32 | And I set the field "Use booking form" to "1" 33 | And I set the field "Booking instructions" to "Please enter your first name" 34 | And I set the field "Let students enter a message" to "Yes, student must enter a message" 35 | And I set the field "Maximum number of uploaded files" to "1" 36 | And I click on "Save and display" "button" 37 | And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: 38 | | Location | My office | 39 | Then I should see "10 slots have been added" 40 | And I log out 41 | 42 | When I log in as "student1" 43 | And I am on "Course 1" course homepage 44 | And I follow "Test scheduler" 45 | Then I should see "3:00 AM" in the "slotbookertable" "table" 46 | 47 | When I click on "Book slot" "button" in the "3:00 AM" "table_row" 48 | Then I should see "Please enter your first name" 49 | 50 | When I click on "Confirm booking" "button" 51 | Then I should see "You must enter text into this field" 52 | 53 | When I set the field "Your message" to "Joe" 54 | And I click on "Confirm booking" "button" 55 | Then "Cancel booking" "button" should exist 56 | And I log out 57 | 58 | When I log in as "student2" 59 | And I am on "Course 1" course homepage 60 | And I follow "Test scheduler" 61 | And I click on "Book slot" "button" in the "4:00 AM" "table_row" 62 | Then I should see "Please enter your first name" 63 | 64 | When I set the field "Your message" to "Jill" 65 | And I upload "mod/scheduler/tests/fixtures/studentfile.txt" file to "Upload files" filemanager 66 | And I click on "Confirm booking" "button" 67 | Then "Cancel booking" "button" should exist 68 | And I log out 69 | 70 | When I log in as "teacher1" 71 | And I am on "Course 1" course homepage 72 | And I follow "Test scheduler" 73 | And I follow "Statistics" 74 | And I follow "My appointments" 75 | Then I should see "Student 1" in the "3:00 AM" "table_row" 76 | And I should see "Student 2" in the "4:00 AM" "table_row" 77 | 78 | When I click on "Student 1" "text" in the "3:00 AM" "table_row" 79 | Then I should see "Student 1" 80 | And I should see "Joe" 81 | And I should not see "studentfile.txt" 82 | 83 | When I click on "Continue" "button" 84 | And I click on "Student 2" "text" in the "4:00 AM" "table_row" 85 | Then I should see "Student 2" 86 | And I should see "Jill" 87 | And I should see "studentfile.txt" 88 | And I log out 89 | -------------------------------------------------------------------------------- /backup/moodle2/backup_scheduler_stepslib.php: -------------------------------------------------------------------------------- 1 | get_setting_value('userinfo'); 25 | 26 | // Define each element separated. 27 | $scheduler = new backup_nested_element('scheduler', array('id'), array( 28 | 'name', 'intro', 'introformat', 'schedulermode', 'maxbookings', 29 | 'guardtime', 'defaultslotduration', 'allownotifications', 'staffrolename', 30 | 'scale', 'gradingstrategy', 'bookingrouping', 'usenotes', 31 | 'usebookingform', 'bookinginstructions', 'bookinginstructionsformat', 32 | 'usestudentnotes', 'requireupload', 'uploadmaxfiles', 'uploadmaxsize', 33 | 'usecaptcha', 'timemodified')); 34 | 35 | $slots = new backup_nested_element('slots'); 36 | 37 | $slot = new backup_nested_element('slot', array('id'), array( 38 | 'starttime', 'duration', 'teacherid', 'appointmentlocation', 39 | 'timemodified', 'notes', 'notesformat', 'exclusivity', 40 | 'emaildate', 'hideuntil')); 41 | 42 | $appointments = new backup_nested_element('appointments'); 43 | 44 | $appointment = new backup_nested_element('appointment', array('id'), array( 45 | 'studentid', 'attended', 'grade', 46 | 'appointmentnote', 'appointmentnoteformat', 'teachernote', 'teachernoteformat', 47 | 'studentnote', 'studentnoteformat', 'timecreated', 'timemodified')); 48 | 49 | // Build the tree. 50 | 51 | $scheduler->add_child($slots); 52 | $slots->add_child($slot); 53 | 54 | $slot->add_child($appointments); 55 | $appointments->add_child($appointment); 56 | 57 | // Define sources. 58 | $scheduler->set_source_table('scheduler', array('id' => backup::VAR_ACTIVITYID)); 59 | $scheduler->annotate_ids('grouping', 'bookingrouping'); 60 | 61 | // Include appointments only if we back up user information. 62 | if ($userinfo) { 63 | $slot->set_source_table('scheduler_slots', array('schedulerid' => backup::VAR_PARENTID)); 64 | $appointment->set_source_table('scheduler_appointment', array('slotid' => backup::VAR_PARENTID)); 65 | } 66 | 67 | // Define id annotations. 68 | $scheduler->annotate_ids('scale', 'scale'); 69 | 70 | if ($userinfo) { 71 | $slot->annotate_ids('user', 'teacherid'); 72 | $appointment->annotate_ids('user', 'studentid'); 73 | } 74 | 75 | // Define file annotations. 76 | $scheduler->annotate_files('mod_scheduler', 'intro', null); // Files stored in intro field. 77 | $scheduler->annotate_files('mod_scheduler', 'bookinginstructions', null); // Files stored in intro field. 78 | $slot->annotate_files('mod_scheduler', 'slotnote', 'id'); // Files stored in slot notes. 79 | $appointment->annotate_files('mod_scheduler', 'appointmentnote', 'id'); // Files stored in appointment notes. 80 | $appointment->annotate_files('mod_scheduler', 'teachernote', 'id'); // Files stored in teacher-only notes. 81 | $appointment->annotate_files('mod_scheduler', 'studentfiles', 'id'); // Files uploaded by students. 82 | 83 | // Return the root element (scheduler), wrapped into standard activity structure. 84 | return $this->prepare_activity_structure($scheduler); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /export.php: -------------------------------------------------------------------------------- 1 | set_docs_path('mod/scheduler/export'); 16 | 17 | // Find active group in case that group mode is in use. 18 | $currentgroupid = 0; 19 | $groupmode = groups_get_activity_groupmode($scheduler->cm); 20 | if ($groupmode) { 21 | $currentgroupid = groups_get_activity_group($scheduler->cm, true); 22 | } 23 | 24 | $actionurl = new moodle_url('/mod/scheduler/view.php', array('what' => 'export', 'id' => $scheduler->cmid)); 25 | $returnurl = new moodle_url('/mod/scheduler/view.php', array('what' => 'view', 'id' => $scheduler->cmid)); 26 | $PAGE->set_url($actionurl); 27 | $mform = new scheduler_export_form($actionurl, $scheduler); 28 | 29 | if ($mform->is_cancelled()) { 30 | redirect($returnurl); 31 | } 32 | 33 | $data = $mform->get_data(); 34 | if ($data) { 35 | $availablefields = scheduler_get_export_fields($scheduler); 36 | $selectedfields = array(); 37 | foreach ($availablefields as $field) { 38 | $inputid = 'field-'.$field->get_id(); 39 | if (isset($data->{$inputid}) && $data->{$inputid} == 1) { 40 | $selectedfields[] = $field; 41 | $field->set_renderer($output); 42 | } 43 | } 44 | $userid = $USER->id; 45 | if (isset($data->includewhom) && $data->includewhom == 'all') { 46 | require_capability('mod/scheduler:canseeotherteachersbooking', $context); 47 | $userid = 0; 48 | } 49 | $pageperteacher = isset($data->paging) && $data->paging == 'perteacher'; 50 | $preview = isset($data->preview); 51 | } else { 52 | $preview = false; 53 | } 54 | 55 | if (!$data || $preview) { 56 | echo $OUTPUT->header(); 57 | 58 | // Print top tabs. 59 | $taburl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid, 'what' => 'export')); 60 | echo $output->teacherview_tabs($scheduler, $taburl, 'export'); 61 | 62 | if ($groupmode) { 63 | groups_print_activity_menu($scheduler->cm, $taburl); 64 | } 65 | 66 | echo $output->heading(get_string('exporthdr', 'scheduler'), 2); 67 | 68 | $mform->display(); 69 | 70 | if ($preview) { 71 | $canvas = new scheduler_html_canvas(); 72 | $export = new scheduler_export($canvas); 73 | 74 | $export->build($scheduler, 75 | $selectedfields, 76 | $data->content, 77 | $userid, 78 | $currentgroupid, 79 | $data->includeemptyslots, 80 | $pageperteacher); 81 | 82 | $limit = 20; 83 | echo $canvas->as_html($limit, false); 84 | 85 | echo html_writer::div(get_string('previewlimited', 'scheduler', $limit), 'previewlimited'); 86 | } 87 | 88 | echo $output->footer(); 89 | exit(); 90 | } 91 | 92 | switch ($data->outputformat) { 93 | case 'csv': 94 | $canvas = new scheduler_csv_canvas($data->csvseparator); 95 | break; 96 | case 'xls': 97 | $canvas = new scheduler_excel_canvas(); 98 | break; 99 | case 'ods': 100 | $canvas = new scheduler_ods_canvas(); 101 | break; 102 | case 'html': 103 | $canvas = new scheduler_html_canvas($returnurl); 104 | break; 105 | case 'pdf': 106 | $canvas = new scheduler_pdf_canvas($data->pdforientation); 107 | break; 108 | } 109 | 110 | $export = new scheduler_export($canvas); 111 | 112 | $export->build($scheduler, 113 | $selectedfields, 114 | $data->content, 115 | $userid, 116 | $currentgroupid, 117 | $data->includeemptyslots, 118 | $pageperteacher); 119 | 120 | $filename = clean_filename(format_string($course->shortname).'_'.format_string($scheduler->name)); 121 | $canvas->send($filename); 122 | 123 | -------------------------------------------------------------------------------- /tests/generator/lib.php: -------------------------------------------------------------------------------- 1 | $property)) { 25 | $record->$property = $value; 26 | } 27 | } 28 | 29 | /** 30 | * Create new scheduler module instance 31 | * @param array|stdClass $record 32 | * @param array $options 33 | * @return stdClass activity record with extra cmid field 34 | */ 35 | public function create_instance($record = null, array $options = null) { 36 | global $CFG, $DB; 37 | require_once("$CFG->dirroot/mod/scheduler/lib.php"); 38 | 39 | $this->instancecount++; 40 | $i = $this->instancecount; 41 | 42 | $record = (object)(array)$record; 43 | $options = (array)$options; 44 | 45 | if (empty($record->course)) { 46 | throw new coding_exception('module generator requires $record->course'); 47 | } 48 | self::set_default($record, 'name', get_string('pluginname', 'scheduler').' '.$i); 49 | self::set_default($record, 'intro', 'Test scheduler '.$i); 50 | self::set_default($record, 'introformat', FORMAT_MOODLE); 51 | self::set_default($record, 'schedulermode', 'onetime'); 52 | self::set_default($record, 'guardtime', 0); 53 | self::set_default($record, 'defaultslotduration', 15); 54 | self::set_default($record, 'staffrolename', ''); 55 | self::set_default($record, 'scale', 0); 56 | if (isset($options['idnumber'])) { 57 | $record->cmidnumber = $options['idnumber']; 58 | } else { 59 | $record->cmidnumber = ''; 60 | } 61 | 62 | $record->coursemodule = $this->precreate_course_module($record->course, $options); 63 | $id = scheduler_add_instance($record); 64 | $modinst = $this->post_add_instance($id, $record->coursemodule); 65 | 66 | if (isset($options['slottimes'])) { 67 | $slottimes = (array) $options['slottimes']; 68 | foreach ($slottimes as $slotkey => $time) { 69 | $slot = new stdClass(); 70 | $slot->schedulerid = $id; 71 | $slot->starttime = $time; 72 | $slot->duration = 10; 73 | $slot->teacherid = 2; // Admin user - for the moment. 74 | $slot->appointmentlocation = 'Test Loc'; 75 | $slot->timemodified = time(); 76 | $slot->notes = ''; 77 | $slot->slotnote = ''; 78 | $slot->exclusivity = isset($options['slotexclusivity'][$slotkey]) ? $options['slotexclusivity'][$slotkey] : 0; 79 | $slot->emaildate = 0; 80 | $slot->hideuntil = 0; 81 | $slotid = $DB->insert_record('scheduler_slots', $slot); 82 | 83 | if (isset($options['slotstudents'][$slotkey])) { 84 | $students = (array)$options['slotstudents'][$slotkey]; 85 | foreach ($students as $studentkey => $userid) { 86 | $appointment = new stdClass(); 87 | $appointment->slotid = $slotid; 88 | $appointment->studentid = $userid; 89 | $appointment->attended = isset($options['slotattended'][$slotkey]) && $options['slotattended'][$slotkey]; 90 | $appointment->grade = 0; 91 | $appointment->appointmentnote = ''; 92 | $appointment->teachernote = ''; 93 | $appointment->timecreated = time(); 94 | $appointment->timemodified = time(); 95 | $appointmentid = $DB->insert_record('scheduler_appointment', $appointment); 96 | } 97 | } 98 | } 99 | } 100 | 101 | return $modinst; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /model/scheduler_appointment.php: -------------------------------------------------------------------------------- 1 | data = new stdClass(); 33 | $this->set_parent($slot); 34 | $this->data->slotid = $slot->get_id(); 35 | $this->data->attended = 0; 36 | $this->data->appointmentnoteformat = FORMAT_HTML; 37 | $this->data->teachernoteformat = FORMAT_HTML; 38 | } 39 | 40 | public function save() { 41 | $this->data->slotid = $this->get_parent()->get_id(); 42 | parent::save(); 43 | $scheddata = $this->get_scheduler()->get_data(); 44 | scheduler_update_grades($scheddata, $this->studentid); 45 | } 46 | 47 | public function delete() { 48 | $studid = $this->studentid; 49 | parent::delete(); 50 | 51 | $scheddata = $this->get_scheduler()->get_data(); 52 | scheduler_update_grades($scheddata, $studid); 53 | 54 | $fs = get_file_storage(); 55 | $cid = $this->get_scheduler()->get_context()->id; 56 | $fs->delete_area_files($cid, 'mod_scheduler', 'appointmentnote', $this->get_id()); 57 | $fs->delete_area_files($cid, 'mod_scheduler', 'teachernote', $this->get_id()); 58 | $fs->delete_area_files($cid, 'mod_scheduler', 'studentnote', $this->get_id()); 59 | 60 | } 61 | 62 | /** 63 | * Retrieve the slot associated with this appointment 64 | * 65 | * @return scheduler_slot; 66 | */ 67 | public function get_slot() { 68 | return $this->get_parent(); 69 | } 70 | 71 | /** 72 | * Retrieve the scheduler associated with this appointment 73 | * 74 | * @return scheduler_instance 75 | */ 76 | public function get_scheduler() { 77 | return $this->get_parent()->get_parent(); 78 | } 79 | 80 | /** 81 | * Return the student object. 82 | * May be null if no student is assigned to this appointment (this _should_ never happen). 83 | */ 84 | public function get_student() { 85 | global $DB; 86 | if ($this->data->studentid) { 87 | return $DB->get_record('user', array('id' => $this->data->studentid), '*', MUST_EXIST); 88 | } else { 89 | return null; 90 | } 91 | } 92 | 93 | /** 94 | * Has this student attended? 95 | */ 96 | public function is_attended() { 97 | return (boolean) $this->data->attended; 98 | } 99 | 100 | /** 101 | * Are there any student notes associated with this appointment? 102 | * @return boolean 103 | */ 104 | public function has_studentnotes() { 105 | return $this->get_scheduler()->uses_studentnotes() && 106 | strlen(trim(strip_tags($this->studentnote))) > 0; 107 | } 108 | 109 | /** 110 | * How many files has the student uploaded for this appointment? 111 | * 112 | * @return int 113 | */ 114 | public function count_studentfiles() { 115 | if (!$this->get_scheduler()->uses_studentnotes()) { 116 | return 0; 117 | } 118 | $ctx = $this->get_scheduler()->context->id; 119 | $fs = get_file_storage(); 120 | $files = $fs->get_area_files($ctx, 'mod_scheduler', 'studentfiles', $this->id, "filename", false); 121 | return count($files); 122 | } 123 | 124 | } 125 | 126 | /** 127 | * A factory class for scheduler appointments. 128 | * 129 | * @copyright 2011 Henning Bostelmann and others (see README.txt) 130 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 131 | */ 132 | class scheduler_appointment_factory extends mvc_child_model_factory { 133 | public function create_child(mvc_record_model $parent) { 134 | return new scheduler_appointment($parent); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /backup/moodle2/restore_scheduler_stepslib.php: -------------------------------------------------------------------------------- 1 | get_setting_value('userinfo'); 25 | 26 | $scheduler = new restore_path_element('scheduler', '/activity/scheduler'); 27 | $paths[] = $scheduler; 28 | 29 | if ($userinfo) { 30 | $slot = new restore_path_element('scheduler_slot', '/activity/scheduler/slots/slot'); 31 | $paths[] = $slot; 32 | 33 | $appointment = new restore_path_element('scheduler_appointment', 34 | '/activity/scheduler/slots/slot/appointments/appointment'); 35 | $paths[] = $appointment; 36 | } 37 | 38 | // Return the paths wrapped into standard activity structure. 39 | return $this->prepare_activity_structure($paths); 40 | } 41 | 42 | protected function process_scheduler($data) { 43 | global $DB; 44 | 45 | $data = (object)$data; 46 | $oldid = $data->id; 47 | $data->course = $this->get_courseid(); 48 | 49 | $data->timemodified = $this->apply_date_offset($data->timemodified); 50 | 51 | if ($data->scale < 0) { // Scale found, get mapping. 52 | $data->scale = -($this->get_mappingid('scale', abs($data->scale))); 53 | } 54 | 55 | if (is_null($data->gradingstrategy)) { // Catch inconsistent data created by pre-1.9 DB schema. 56 | $data->gradingstrategy = 0; 57 | } 58 | 59 | if ($data->bookingrouping > 0) { 60 | $data->bookingrouping = $this->get_mappingid('grouping', $data->bookingrouping); 61 | } 62 | 63 | // Insert the scheduler record. 64 | $newitemid = $DB->insert_record('scheduler', $data); 65 | // Immediately after inserting "activity" record, call this. 66 | $this->apply_activity_instance($newitemid); 67 | } 68 | 69 | protected function process_scheduler_slot($data) { 70 | global $DB; 71 | 72 | $data = (object)$data; 73 | $oldid = $data->id; 74 | 75 | $data->schedulerid = $this->get_new_parentid('scheduler'); 76 | $data->starttime = $this->apply_date_offset($data->starttime); 77 | $data->timemodified = $this->apply_date_offset($data->timemodified); 78 | $data->emaildate = $this->apply_date_offset($data->emaildate); 79 | $data->hideuntil = $this->apply_date_offset($data->hideuntil); 80 | 81 | $data->teacherid = $this->get_mappingid('user', $data->teacherid); 82 | 83 | $newitemid = $DB->insert_record('scheduler_slots', $data); 84 | $this->set_mapping('scheduler_slot', $oldid, $newitemid, true); 85 | } 86 | 87 | protected function process_scheduler_appointment($data) { 88 | global $DB; 89 | 90 | $data = (object)$data; 91 | $oldid = $data->id; 92 | 93 | $data->slotid = $this->get_new_parentid('scheduler_slot'); 94 | 95 | $data->timecreated = $this->apply_date_offset($data->timecreated); 96 | $data->timemodified = $this->apply_date_offset($data->timemodified); 97 | 98 | $data->studentid = $this->get_mappingid('user', $data->studentid); 99 | 100 | $newitemid = $DB->insert_record('scheduler_appointment', $data); 101 | $this->set_mapping('scheduler_appointment', $oldid, $newitemid, true); 102 | } 103 | 104 | protected function after_execute() { 105 | // Add scheduler related files. 106 | $this->add_related_files('mod_scheduler', 'intro', null); 107 | $this->add_related_files('mod_scheduler', 'bookinginstructions', null); 108 | $this->add_related_files('mod_scheduler', 'slotnote', 'scheduler_slot'); 109 | $this->add_related_files('mod_scheduler', 'appointmentnote', 'scheduler_appointment'); 110 | $this->add_related_files('mod_scheduler', 'teachernote', 'scheduler_appointment'); 111 | $this->add_related_files('mod_scheduler', 'studentfiles', 'scheduler_appointment'); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | array( 16 | 'riskbitmask' => RISK_XSS, 17 | 18 | 'captype' => 'write', 19 | 'contextlevel' => CONTEXT_COURSE, 20 | 'archetypes' => array( 21 | 'editingteacher' => CAP_ALLOW, 22 | 'manager' => CAP_ALLOW 23 | ), 24 | 'clonepermissionsfrom' => 'moodle/course:manageactivities' 25 | ), 26 | 27 | 'mod/scheduler:appoint' => array( 28 | 'captype' => 'write', 29 | 'contextlevel' => CONTEXT_MODULE, 30 | 'archetypes' => array( 31 | 'student' => CAP_ALLOW, 32 | ) 33 | ), 34 | 35 | 'mod/scheduler:attend' => array( 36 | 'captype' => 'read', 37 | 'contextlevel' => CONTEXT_MODULE, 38 | 'archetypes' => array( 39 | 'teacher' => CAP_ALLOW, 40 | 'editingteacher' => CAP_ALLOW 41 | ) 42 | ), 43 | 44 | 'mod/scheduler:manage' => array( 45 | 'captype' => 'write', 46 | 'contextlevel' => CONTEXT_MODULE, 47 | 'archetypes' => array( 48 | 'teacher' => CAP_ALLOW, 49 | 'editingteacher' => CAP_ALLOW, 50 | 'coursecreator' => CAP_ALLOW, 51 | 'manager' => CAP_ALLOW 52 | ) 53 | ), 54 | 55 | 'mod/scheduler:manageallappointments' => array( 56 | 'captype' => 'write', 57 | 'contextlevel' => CONTEXT_MODULE, 58 | 'archetypes' => array( 59 | 'editingteacher' => CAP_ALLOW, 60 | 'coursecreator' => CAP_ALLOW, 61 | 'manager' => CAP_ALLOW 62 | ) 63 | ), 64 | 65 | 'mod/scheduler:canscheduletootherteachers' => array( 66 | 'captype' => 'write', 67 | 'contextlevel' => CONTEXT_MODULE, 68 | 'archetypes' => array( 69 | 'editingteacher' => CAP_ALLOW, 70 | 'coursecreator' => CAP_ALLOW, 71 | 'manager' => CAP_ALLOW 72 | ) 73 | ), 74 | 75 | 'mod/scheduler:canseeotherteachersbooking' => array( 76 | 'captype' => 'read', 77 | 'contextlevel' => CONTEXT_MODULE, 78 | 'archetypes' => array( 79 | 'editingteacher' => CAP_ALLOW, 80 | 'coursecreator' => CAP_ALLOW, 81 | 'manager' => CAP_ALLOW 82 | ) 83 | ), 84 | 85 | 'mod/scheduler:seeoverviewoutsideactivity' => array( 86 | 'captype' => 'read', 87 | 'contextlevel' => CONTEXT_MODULE, 88 | 'archetypes' => array( 89 | 'teacher' => CAP_ALLOW, 90 | 'editingteacher' => CAP_ALLOW, 91 | 'coursecreator' => CAP_ALLOW, 92 | 'manager' => CAP_ALLOW 93 | ) 94 | ), 95 | 96 | 'mod/scheduler:disengage' => array( 97 | 'captype' => 'write', 98 | 'contextlevel' => CONTEXT_MODULE, 99 | 'archetypes' => array( 100 | 'student' => CAP_ALLOW, 101 | 'teacher' => CAP_ALLOW, 102 | 'editingteacher' => CAP_ALLOW, 103 | 'coursecreator' => CAP_ALLOW, 104 | 'manager' => CAP_ALLOW 105 | ) 106 | ), 107 | 108 | 'mod/scheduler:viewslots' => array( 109 | 'captype' => 'read', 110 | 'contextlevel' => CONTEXT_MODULE, 111 | 'archetypes' => array( 112 | 'student' => CAP_ALLOW, 113 | ), 114 | 'clonepermissionsfrom' => 'mod/scheduler:appoint' 115 | ), 116 | 117 | 'mod/scheduler:viewfullslots' => array( 118 | 'captype' => 'read', 119 | 'contextlevel' => CONTEXT_MODULE, 120 | 'archetypes' => array( 121 | ) 122 | ), 123 | 124 | 'mod/scheduler:seeotherstudentsbooking' => array( 125 | 'captype' => 'read', 126 | 'contextlevel' => CONTEXT_MODULE, 127 | 'archetypes' => array( 128 | 'student' => CAP_ALLOW, 129 | 'teacher' => CAP_ALLOW, 130 | 'editingteacher' => CAP_ALLOW, 131 | 'coursecreator' => CAP_ALLOW, 132 | 'manager' => CAP_ALLOW 133 | ) 134 | ), 135 | 136 | 'mod/scheduler:seeotherstudentsresults' => array( 137 | 'captype' => 'read', 138 | 'contextlevel' => CONTEXT_MODULE, 139 | 'archetypes' => array( 140 | 'teacher' => CAP_ALLOW, 141 | 'editingteacher' => CAP_ALLOW, 142 | 'coursecreator' => CAP_ALLOW, 143 | 'manager' => CAP_ALLOW 144 | ) 145 | ) 146 | 147 | ); 148 | 149 | 150 | -------------------------------------------------------------------------------- /tests/model_test.php: -------------------------------------------------------------------------------- 1 | dirroot . '/mod/scheduler/locallib.php'); 15 | 16 | /** 17 | * Unit tests for the MVC model classes 18 | * 19 | * @group mod_scheduler 20 | * @copyright 2014 Henning Bostelmann and others (see README.txt) 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | class mod_scheduler_model_testcase extends advanced_testcase { 24 | 25 | /** 26 | * @var int Course_modules id used for testing 27 | */ 28 | protected $moduleid; 29 | 30 | /** 31 | * @var int Course id used for testing 32 | */ 33 | protected $courseid; 34 | 35 | /** 36 | * @var int Scheduler id used for testing 37 | */ 38 | protected $schedulerid; 39 | 40 | /** 41 | * @var int User id used for testing 42 | */ 43 | protected $userid; 44 | 45 | protected function setUp() { 46 | global $DB, $CFG; 47 | 48 | $this->resetAfterTest(true); 49 | 50 | $course = $this->getDataGenerator()->create_course(); 51 | $options = array(); 52 | $options['slottimes'] = array(); 53 | $options['slotstudents'] = array(); 54 | for ($c = 0; $c < 4; $c++) { 55 | $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; 56 | $options['slotstudents'][$c] = array($this->getDataGenerator()->create_user()->id); 57 | } 58 | $options['slottimes'][4] = time() + 10 * DAYSECS; 59 | $options['slottimes'][5] = time() + 11 * DAYSECS; 60 | $options['slotstudents'][5] = array( 61 | $this->getDataGenerator()->create_user()->id, 62 | $this->getDataGenerator()->create_user()->id 63 | ); 64 | 65 | $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); 66 | $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); 67 | 68 | $this->schedulerid = $scheduler->id; 69 | $this->moduleid = $coursemodule->id; 70 | $this->courseid = $coursemodule->course; 71 | $this->userid = 2; // Admin user. 72 | } 73 | 74 | /** 75 | * Test loading a scheduler instance from the database 76 | */ 77 | public function test_scheduler_instance() { 78 | global $DB; 79 | 80 | $dbdata = $DB->get_record('scheduler', array('id' => $this->schedulerid)); 81 | 82 | $instance = scheduler_instance::load_by_coursemodule_id($this->moduleid); 83 | 84 | $this->assertEquals( $dbdata->name, $instance->get_name()); 85 | 86 | } 87 | 88 | /** 89 | * Test the "appointment" data object 90 | * (basic functionality, with minimal reference to slots) 91 | **/ 92 | public function test_appointment() { 93 | 94 | global $DB; 95 | 96 | $instance = scheduler_instance::load_by_coursemodule_id($this->moduleid); 97 | $slot = array_values($instance->get_slots())[0]; 98 | $factory = new scheduler_appointment_factory($slot); 99 | 100 | $user = $this->getdataGenerator()->create_user(); 101 | 102 | $app0 = new stdClass(); 103 | $app0->slotid = 1; 104 | $app0->studentid = $user->id; 105 | $app0->attended = 0; 106 | $app0->grade = 0; 107 | $app0->appointmentnote = 'testnote'; 108 | $app0->teachernote = 'confidentialtestnote'; 109 | $app0->timecreated = time(); 110 | $app0->timemodified = time(); 111 | 112 | $id1 = $DB->insert_record('scheduler_appointment', $app0); 113 | 114 | $appobj = $factory->create_from_id($id1); 115 | $this->assertEquals($user->id, $appobj->studentid); 116 | $this->assertEquals(fullname($user), fullname($appobj->get_student())); 117 | $this->assertFalse($appobj->is_attended()); 118 | $this->assertEquals(0, $appobj->grade); 119 | 120 | $app0->attended = 1; 121 | $app0->grade = -7; 122 | $id2 = $DB->insert_record('scheduler_appointment', $app0); 123 | 124 | $appobj = $factory->create_from_id($id2); 125 | $this->assertEquals($user->id, $appobj->studentid); 126 | $this->assertEquals(fullname($user), fullname($appobj->get_student())); 127 | $this->assertTrue($appobj->is_attended()); 128 | $this->assertEquals(-7, $appobj->grade); 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /tests/behat/add_slots.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: Teacher can add slots to a scheduler activity 3 | In order to allow students to book a slot 4 | As a teacher 5 | I need to add slots to the scheduler 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | teacher1 | Teacher | 1 | teacher1@example.com | 11 | | student1 | Student | 1 | student1@example.com | 12 | | student2 | Student | 2 | student2@example.com | 13 | | student3 | Student | 3 | student3@example.com | 14 | | student4 | Student | 4 | student4@example.com | 15 | And the following "courses" exist: 16 | | fullname | shortname | category | 17 | | Course 1 | C1 | 0 | 18 | And the following "course enrolments" exist: 19 | | user | course | role | 20 | | teacher1 | C1 | editingteacher | 21 | | student1 | C1 | student | 22 | | student2 | C1 | student | 23 | | student3 | C1 | student | 24 | | student4 | C1 | student | 25 | And the following "activities" exist: 26 | | activity | name | intro | course | idnumber | 27 | | scheduler | Test scheduler | n | C1 | scheduler1 | 28 | 29 | @javascript 30 | Scenario: Teacher adds a single, empty slot to the scheduler 31 | When I log in as "teacher1" 32 | And I am on "Course 1" course homepage 33 | And I follow "Test scheduler" 34 | And I click on "Add slots" "link" 35 | And I follow "Add single slot" 36 | And I set the following fields to these values: 37 | | starttime[day] | 1 | 38 | | starttime[month] | April | 39 | | starttime[year] | 2050 | 40 | | duration | 30 | 41 | And I click on "Save changes" "button" 42 | Then I should see "1 slot added" 43 | And I should see "Friday, 1 April 2050" 44 | 45 | @javascript 46 | Scenario: Teacher enters invalid values when adding a slot 47 | When I log in as "teacher1" 48 | And I am on "Course 1" course homepage 49 | And I follow "Test scheduler" 50 | And I click on "Add slots" "link" 51 | And I follow "Add single slot" 52 | And I set the following fields to these values: 53 | | starttime[day] | 1 | 54 | | starttime[month] | April | 55 | | starttime[year] | 2010 | 56 | And I click on "Save changes" "button" 57 | Then I should see "in the past" 58 | And I set the following fields to these values: 59 | | starttime[year] | 2050 | 60 | | duration | -1 | 61 | When I click on "Save changes" "button" 62 | Then I should see "Slot duration must be between" 63 | And I set the following fields to these values: 64 | | duration | 10 | 65 | | exclusivity | -10 | 66 | And I click on "Save changes" "button" 67 | And I should see "needs to be 1 or more" 68 | And I set the following fields to these values: 69 | | exclusivity | 5 | 70 | And I click on "Save changes" "button" 71 | And I should see "1 slot added" 72 | 73 | @javascript 74 | Scenario: Teacher enters a slot and schedules 3 students 75 | When I log in as "teacher1" 76 | And I am on "Course 1" course homepage 77 | And I follow "Test scheduler" 78 | And I click on "Add slots" "link" 79 | And I follow "Add single slot" 80 | And I set the following fields to these values: 81 | | starttime[day] | 1 | 82 | | starttime[month] | April | 83 | | starttime[year] | 2050 | 84 | | exclusivity | 2 | 85 | | studentid[0] | Student 1 | 86 | And I click on "Add another student" "button" 87 | And I set the following fields to these values: 88 | | studentid[1] | Student 2 | 89 | And I click on "Add another student" "button" 90 | And I set the following fields to these values: 91 | | studentid[2] | Student 3 | 92 | And I click on "Save changes" "button" 93 | Then I should see "more than allowed" 94 | And I set the following fields to these values: 95 | | exclusivity | 3 | 96 | And I click on "Save changes" "button" 97 | And I should see "1 slot added" 98 | And I should see "Student 1" 99 | And I should see "Student 2" 100 | And I should see "Student 3" 101 | 102 | 103 | @javascript 104 | Scenario: Teacher creates 10 slots at once 105 | When I log in as "teacher1" 106 | And I am on "Course 1" course homepage 107 | And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: 108 | | Location | Here | 109 | Then I should see "10 slots have been added" 110 | And I should see "1:00 AM" 111 | And I should see "2:00 AM" 112 | And I should see "10:00 AM" 113 | And I should not see "11:00 AM" 114 | -------------------------------------------------------------------------------- /tests/behat/officehours.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: Office hours bookings with Scheduler, one booking per student 3 | In order to organize my office hours 4 | As a teacher 5 | I can use a scheduler to let students choose a time slot. 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | manager1 | Manager | 1 | manager1@example.com | 11 | | teacher1 | Teacher | 1 | teacher1@example.com | 12 | | student1 | Student | 1 | student1@example.com | 13 | | student2 | Student | 2 | student2@example.com | 14 | | student3 | Student | 3 | student3@example.com | 15 | | student4 | Student | 4 | student4@example.com | 16 | And the following "courses" exist: 17 | | fullname | shortname | category | 18 | | Course 1 | C1 | 0 | 19 | And the following "course enrolments" exist: 20 | | user | course | role | 21 | | teacher1 | C1 | editingteacher | 22 | | student1 | C1 | student | 23 | | student2 | C1 | student | 24 | | student3 | C1 | student | 25 | | student4 | C1 | student | 26 | And the following "system role assigns" exist: 27 | | user | role | 28 | | manager1 | manager | 29 | And the following "activities" exist: 30 | | activity | name | intro | course | idnumber | schedulermode | 31 | | scheduler | Test scheduler | n | C1 | scheduler1 | oneonly | 32 | And I add the upcoming events block globally 33 | 34 | @javascript 35 | Scenario: The teacher adds slots, and students book them 36 | When I log in as "teacher1" 37 | And I am on "Course 1" course homepage 38 | And I add 10 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: 39 | | Location | My office | 40 | Then I should see "10 slots have been added" 41 | And I should see "4 students still need to make an appointment" 42 | And I should see "Student 1" in the "studentstoschedule" "table" 43 | And I should see "Student 2" in the "studentstoschedule" "table" 44 | And I should see "Student 3" in the "studentstoschedule" "table" 45 | And I should see "Student 4" in the "studentstoschedule" "table" 46 | And I log out 47 | 48 | When I log in as "student1" 49 | And I am on "Course 1" course homepage 50 | And I follow "Test scheduler" 51 | Then I should see "1:00 AM" in the "slotbookertable" "table" 52 | And I should see "10:00 AM" in the "slotbookertable" "table" 53 | When I click on "Book slot" "button" in the "2:00 AM" "table_row" 54 | Then "Cancel booking" "button" should exist 55 | And I should see "Meeting with your Teacher, Teacher 1" in the "Upcoming events" "block" 56 | And I log out 57 | 58 | When I log in as "student3" 59 | And I am on "Course 1" course homepage 60 | And I follow "Test scheduler" 61 | Then I should see "1:00 AM" in the "slotbookertable" "table" 62 | And I should not see "2:00 AM" in the "slotbookertable" "table" 63 | And I should see "10:00 AM" in the "slotbookertable" "table" 64 | When I click on "Book slot" "button" in the "5:00 AM" "table_row" 65 | Then "Cancel booking" "button" should exist 66 | And I should see "Meeting with your Teacher, Teacher 1" in the "Upcoming events" "block" 67 | And I log out 68 | 69 | When I log in as "teacher1" 70 | And I am on "Course 1" course homepage 71 | And I follow "Test scheduler" 72 | Then I should see "1:00 AM" in the "slotmanager" "table" 73 | And I should see "Student 1" in the "2:00 AM" "table_row" 74 | And I should see "Student 3" in the "5:00 AM" "table_row" 75 | And I should see "10:00 AM" in the "slotmanager" "table" 76 | And I should see "Meeting with your Student, Student 1" in the "Upcoming events" "block" 77 | And I should see "Meeting with your Student, Student 3" in the "Upcoming events" "block" 78 | And I should see "2 students still need to make an appointment" 79 | And I should not see "Student 1" in the "studentstoschedule" "table" 80 | And I should see "Student 2" in the "studentstoschedule" "table" 81 | And I should not see "Student 3" in the "studentstoschedule" "table" 82 | And I should see "Student 4" in the "studentstoschedule" "table" 83 | When I click on "seen[]" "checkbox" in the "2:00 AM" "table_row" 84 | And I follow "Test scheduler" 85 | Then I should not see "Meeting with your Student, Student 1" in the "Upcoming events" "block" 86 | And I should see "Meeting with your Student, Student 3" in the "Upcoming events" "block" 87 | And I log out 88 | 89 | When I log in as "student1" 90 | And I am on "Course 1" course homepage 91 | And I follow "Test scheduler" 92 | Then I should see "Attended slots" 93 | And "slotbookertable" "table" should not exist 94 | And I should not see "Cancel booking" 95 | And I should not see "Meeting with your" in the "Upcoming events" "block" 96 | And I log out 97 | -------------------------------------------------------------------------------- /viewstudent.php: -------------------------------------------------------------------------------- 1 | dirroot.'/mod/scheduler/locallib.php'); 14 | 15 | if (!has_capability('mod/scheduler:manage', $context)) { 16 | require_capability('mod/scheduler:manageallappointments', $context); 17 | } 18 | 19 | $appointmentid = required_param('appointmentid', PARAM_INT); 20 | list($slot, $appointment) = $scheduler->get_slot_appointment($appointmentid); 21 | $studentid = $appointment->studentid; 22 | 23 | $urlparas = array('what' => 'viewstudent', 24 | 'id' => $scheduler->cmid, 25 | 'appointmentid' => $appointmentid, 26 | 'course' => $scheduler->courseid); 27 | $taburl = new moodle_url('/mod/scheduler/view.php', $urlparas); 28 | $PAGE->set_url($taburl); 29 | 30 | $appts = $scheduler->get_appointments_for_student($studentid); 31 | 32 | $pages = array('thisappointment'); 33 | if ($slot->get_appointment_count() > 1) { 34 | $pages[] = 'otherstudents'; 35 | } 36 | if (count($appts) > 1) { 37 | $pages[] = 'otherappointments'; 38 | } 39 | 40 | if (!in_array($subpage, $pages) ) { 41 | $subpage = 'thisappointment'; 42 | } 43 | 44 | // Process edit form before page output starts. 45 | if ($subpage == 'thisappointment') { 46 | require_once($CFG->dirroot.'/mod/scheduler/appointmentforms.php'); 47 | 48 | $actionurl = new moodle_url($taburl, array('page' => 'thisappointment')); 49 | $returnurl = new moodle_url($taburl, array('page' => 'thisappointment')); 50 | 51 | $distribute = ($slot->get_appointment_count() > 1); 52 | $gradeedit = ($slot->teacherid == $USER->id) || get_config('mod_scheduler', 'allteachersgrading'); 53 | $mform = new scheduler_editappointment_form($appointment, $actionurl, $gradeedit, $distribute); 54 | $mform->set_data($mform->prepare_appointment_data($appointment)); 55 | 56 | if ($mform->is_cancelled()) { 57 | redirect($returnurl); 58 | } else if ($formdata = $mform->get_data()) { 59 | $mform->save_appointment_data($formdata, $appointment); 60 | redirect($returnurl); 61 | } 62 | } 63 | 64 | echo $output->header(); 65 | 66 | // Print user summary. 67 | 68 | scheduler_print_user($DB->get_record('user', array('id' => $appointment->studentid)), $course); 69 | 70 | // Print tabs. 71 | $tabrows = array(); 72 | $row = array(); 73 | 74 | if (count($pages) > 1) { 75 | foreach ($pages as $tabpage) { 76 | $tabname = get_string('tab-'.$tabpage, 'scheduler'); 77 | $row[] = new tabobject($tabpage, new moodle_url($taburl, array('subpage' => $tabpage)), $tabname); 78 | } 79 | $tabrows[] = $row; 80 | print_tabs($tabrows, $subpage); 81 | } 82 | 83 | $totalgradeinfo = new scheduler_totalgrade_info($scheduler, $scheduler->get_gradebook_info($appointment->studentid)); 84 | 85 | if ($subpage == 'thisappointment') { 86 | 87 | $ai = scheduler_appointment_info::make_for_teacher($slot, $appointment); 88 | echo $output->render($ai); 89 | 90 | $mform->display(); 91 | 92 | if ($scheduler->uses_grades()) { 93 | echo $output->render($totalgradeinfo); 94 | } 95 | 96 | } else if ($subpage == 'otherappointments') { 97 | // Print table of other appointments of the same student. 98 | 99 | $studenturl = new moodle_url($taburl, array('page' => 'thisappointment')); 100 | $table = new scheduler_slot_table($scheduler, true, $studenturl); 101 | $table->showattended = true; 102 | $table->showteachernotes = true; 103 | $table->showeditlink = true; 104 | $table->showlocation = false; 105 | 106 | foreach ($appts as $appt) { 107 | $table->add_slot($appt->get_slot(), $appt, null, false); 108 | } 109 | 110 | echo $output->render($table); 111 | 112 | if ($scheduler->uses_grades()) { 113 | $totalgradeinfo->showtotalgrade = true; 114 | $totalgradeinfo->totalgrade = $scheduler->get_user_grade($appointment->studentid); 115 | echo $output->render($totalgradeinfo); 116 | } 117 | 118 | } else if ($subpage == 'otherstudents') { 119 | // Print table of other students in the same slot. 120 | 121 | $ai = scheduler_appointment_info::make_from_slot($slot, false); 122 | echo $output->render($ai); 123 | 124 | $studenturl = new moodle_url($taburl, array('page' => 'thisappointment')); 125 | $table = new scheduler_slot_table($scheduler, true, $studenturl); 126 | $table->showattended = true; 127 | $table->showslot = false; 128 | $table->showstudent = true; 129 | $table->showteachernotes = true; 130 | $table->showeditlink = true; 131 | 132 | foreach ($slot->get_appointments() as $otherappointment) { 133 | $table->add_slot($otherappointment->get_slot(), $otherappointment, null, false); 134 | } 135 | 136 | echo $output->render($table); 137 | } 138 | 139 | echo $output->continue_button(new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid))); 140 | echo $output->footer($course); 141 | exit; 142 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .path-mod-scheduler .timelabel { 2 | color: #808080; 3 | } 4 | 5 | .path-mod-scheduler .attended { 6 | color: green; 7 | } 8 | 9 | .path-mod-scheduler div.otherstudent.highlight { 10 | font-weight: bold; 11 | } 12 | 13 | .path-mod-scheduler div.slotnotes { 14 | background-color: #e8e9ee; 15 | border: solid 1px #a7abbe; 16 | font-size: 0.9em; 17 | padding: 2px; 18 | margin: 1px; 19 | } 20 | 21 | div .path-mod-scheduler .appointmentnote { 22 | background-color: #e7efe7; 23 | border: solid 1px #a0c5a4; 24 | font-size: 0.9em; 25 | padding: 2px; 26 | margin: 1px; 27 | } 28 | 29 | .path-mod-scheduler #slotbookertable { 30 | margin-left: auto; 31 | margin-right: auto; 32 | } 33 | 34 | .path-mod-scheduler #slotbookertable { 35 | margin-left: auto; 36 | margin-right: auto; 37 | } 38 | 39 | .path-mod-scheduler div.bookercontrols { 40 | text-align: center; 41 | } 42 | 43 | .path-mod-scheduler div.studentlist.expanded { 44 | display: block; 45 | } 46 | 47 | .path-mod-scheduler div.studentlist.collapsed { 48 | display: none; 49 | } 50 | 51 | .path-mod-scheduler div.commandbar { 52 | width: 100%; 53 | margin-left: auto; 54 | margin-right: auto; 55 | background-color: #eee; 56 | padding: 0.5em; 57 | box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | -webkit-box-sizing: border-box; 60 | } 61 | 62 | /* Reduce space usage by single buttons in table cells */ 63 | .path-mod-scheduler table div.singlebutton div { 64 | margin-bottom: 0px; 65 | } 66 | .path-mod-scheduler table div.singlebutton input { 67 | margin: 0px; 68 | } 69 | 70 | .path-mod-scheduler div.commandbar span.title { 71 | float: left; 72 | clear: right; 73 | width: 8em; 74 | text-align: left; 75 | font-weight: bold; 76 | } 77 | 78 | .path-mod-scheduler div.commandbar .moodle-actionmenu { 79 | display: inline-block; 80 | } 81 | 82 | .path-mod-scheduler div.commandbar .moodle-actionmenu.show[data-enhanced] .menu.align-tr-br { 83 | left: 0; 84 | right: auto; 85 | } 86 | 87 | .path-mod-scheduler div.commandbar .moodle-actionmenu .menubar { 88 | width: 12em; 89 | } 90 | 91 | .path-mod-scheduler .moodle-actionmenu img.iconsmall { 92 | width: auto; 93 | } 94 | .path-mod-scheduler .moodle-actionmenu .menu-action-text { 95 | display: inline; 96 | } 97 | 98 | 99 | body.path-mod-scheduler input.slotselect { 100 | display: none; 101 | } 102 | body.path-mod-scheduler.jsenabled input.slotselect { 103 | display: inline; 104 | } 105 | 106 | body.path-mod-scheduler.jsenabled input.studentselectsubmit { 107 | display: none; 108 | } 109 | 110 | .path-mod-scheduler img.statictickbox { 111 | padding-right: 5px; 112 | } 113 | 114 | .path-mod-scheduler .maildisplay { 115 | width: 90%; 116 | margin-left: auto; 117 | margin-right: auto; 118 | background: #eee; 119 | text-align: center; 120 | } 121 | 122 | .path-mod-scheduler div.schedulelist.halfsize { 123 | width: 46%; 124 | display: inline-table; 125 | padding: 3px; 126 | } 127 | 128 | .path-mod-scheduler div.schedulelist.fullsize { 129 | width: 96%; 130 | display: block; 131 | padding: 3px; 132 | } 133 | 134 | .path-mod-scheduler div.schedulelist div.singlebutton, 135 | .path-mod-scheduler div.schedulelist div.singlebutton form { 136 | display: inline; 137 | } 138 | 139 | .path-mod-scheduler div.actionmessage { 140 | width: 50%; 141 | margin-left: auto; 142 | margin-right: auto; 143 | margin-bottom: 10px; 144 | border: solid 2px; 145 | padding: 5px; 146 | display: block; 147 | text-align: center; 148 | font-weight: bold; 149 | } 150 | 151 | .path-mod-scheduler div.actionmessage.success { 152 | background-color: #96fca6; 153 | border-color: #14fa34; 154 | } 155 | 156 | .path-mod-scheduler div.actionmessage.error { 157 | background-color: #ffb2b8; 158 | border-color: #f40000; 159 | } 160 | 161 | .path-mod-scheduler div.totalgrade { 162 | padding-bottom: 25px; 163 | } 164 | .path-mod-scheduler dl.totalgrade dl { 165 | width: 100%; 166 | } 167 | .path-mod-scheduler dl.totalgrade dt { 168 | float: left; 169 | clear: left; 170 | width: 30%; 171 | } 172 | .path-mod-scheduler dl.totalgrade dd { 173 | float: left; 174 | width: 60%; 175 | } 176 | 177 | .path-mod-scheduler div.dropdownmenu { 178 | display: inline-block; 179 | padding-right: 1em; 180 | } 181 | 182 | .path-mod-scheduler div.dropdownmenu select { 183 | vertical-align: middle; 184 | } 185 | 186 | /* Format data fields in vertical rather than horizontal list. */ 187 | 188 | .path-mod-scheduler #id_datafieldhdr .form-group, 189 | .path-mod-scheduler #id_datafieldhdr .fitem_fgroup { 190 | float: left; 191 | clear: none; 192 | } 193 | 194 | .path-mod-scheduler #id_datafieldhdr .col-md-3, 195 | .path-mod-scheduler #id_datafieldhdr fieldset.fgroup { 196 | width: 100%; 197 | text-align: left; 198 | margin-left: 0; 199 | } 200 | 201 | .path-mod-scheduler #id_datafieldhdr .col-md-9 { 202 | float: none; 203 | width: 100%; 204 | } 205 | 206 | .path-mod-scheduler #id_datafieldhdr .col-form-label, 207 | .path-mod-scheduler #id_datafieldhdr .fitemtitle { 208 | font-weight: bold; 209 | text-align: left; 210 | } 211 | 212 | .path-mod-scheduler #id_datafieldhdr .form-group .felement .fitem, 213 | .path-mod-scheduler #id_datafieldhdr fieldset.fgroup > span { 214 | clear: left; 215 | float: left; 216 | margin-left: 0.5em; 217 | } 218 | -------------------------------------------------------------------------------- /bookingform.php: -------------------------------------------------------------------------------- 1 | libdir.'/formslib.php'); 15 | 16 | /** 17 | * Student-side form to book or edit an appointment in a selected slot 18 | */ 19 | class scheduler_booking_form extends moodleform { 20 | 21 | protected $slot; 22 | protected $appointment = null; 23 | protected $uploadoptions; 24 | protected $existing; 25 | 26 | public function __construct(scheduler_slot $slot, $action, $existing = false) { 27 | $this->slot = $slot; 28 | $this->existing = $existing; 29 | parent::__construct($action, null); 30 | } 31 | 32 | protected function definition() { 33 | 34 | global $CFG, $output; 35 | 36 | $mform = $this->_form; 37 | $scheduler = $this->slot->get_scheduler(); 38 | 39 | $this->noteoptions = array('trusttext' => false, 'maxfiles' => 0, 'maxbytes' => 0, 40 | 'context' => $scheduler->get_context(), 41 | 'collapsed' => true); 42 | 43 | $this->uploadoptions = array('subdirs' => 0, 44 | 'maxbytes' => $scheduler->uploadmaxsize, 45 | 'maxfiles' => $scheduler->uploadmaxfiles); 46 | 47 | // Text field for student-supplied data. 48 | if ($scheduler->uses_studentnotes()) { 49 | 50 | $mform->addElement('editor', 'studentnote_editor', get_string('yourstudentnote', 'scheduler'), 51 | array('rows' => 3, 'columns' => 60), $this->noteoptions); 52 | $mform->setType('studentnote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. 53 | if ($scheduler->usestudentnotes == 2) { 54 | $mform->addRule('studentnote_editor', get_string('notesrequired', 'scheduler'), 'required'); 55 | } 56 | } 57 | 58 | // Student file upload. 59 | if ($scheduler->uses_studentfiles()) { 60 | $mform->addElement('filemanager', 'studentfiles', 61 | get_string('uploadstudentfiles', 'scheduler'), 62 | null, $this->uploadoptions ); 63 | if ($scheduler->requireupload) { 64 | $mform->addRule('studentfiles', get_string('uploadrequired', 'scheduler'), 'required'); 65 | } 66 | } 67 | 68 | // Captcha. 69 | if ($scheduler->uses_bookingcaptcha() && !$this->existing) { 70 | $mform->addElement('recaptcha', 'bookingcaptcha', get_string('security_question', 'auth'), array('https' => true)); 71 | $mform->addHelpButton('bookingcaptcha', 'recaptcha', 'auth'); 72 | $mform->closeHeaderBefore('bookingcaptcha'); 73 | } 74 | 75 | $submitlabel = $this->existing ? null : get_string('confirmbooking', 'scheduler'); 76 | $this->add_action_buttons(true, $submitlabel); 77 | } 78 | 79 | public function validation($data, $files) { 80 | $errors = parent::validation($data, $files); 81 | 82 | if (!$this->existing && $this->slot->get_scheduler()->uses_bookingcaptcha()) { 83 | $recaptcha = $this->_form->getElement('bookingcaptcha'); 84 | if (!empty($this->_form->_submitValues['g-recaptcha-response'])) { 85 | $response = $this->_form->_submitValues['g-recaptcha-response']; 86 | if (true !== ($result = $recaptcha->verify($response))) { 87 | $errors['bookingcaptcha'] = $result; 88 | } 89 | } else { 90 | $errors['bookingcaptcha'] = get_string('missingrecaptchachallengefield'); 91 | } 92 | } 93 | 94 | return $errors; 95 | } 96 | 97 | public function prepare_booking_data(scheduler_appointment $appointment) { 98 | $this->appointment = $appointment; 99 | 100 | $newdata = clone($appointment->get_data()); 101 | $context = $appointment->get_scheduler()->get_context(); 102 | 103 | $newdata = file_prepare_standard_editor($newdata, 'studentnote', $this->noteoptions, $context); 104 | 105 | $draftitemid = file_get_submitted_draft_itemid('studentfiles'); 106 | file_prepare_draft_area($draftitemid, $context->id, 'mod_scheduler', 'studentfiles', $appointment->id); 107 | $newdata->studentfiles = $draftitemid; 108 | 109 | return $newdata; 110 | } 111 | 112 | public function save_booking_data(stdClass $formdata, scheduler_appointment $appointment) { 113 | $scheduler = $appointment->get_scheduler(); 114 | if ($scheduler->uses_studentnotes() && isset($formdata->studentnote_editor)) { 115 | $editor = $formdata->studentnote_editor; 116 | $appointment->studentnote = $editor['text']; 117 | $appointment->studentnoteformat = $editor['format']; 118 | } 119 | if ($scheduler->uses_studentfiles()) { 120 | file_save_draft_area_files($formdata->studentfiles, $scheduler->context->id, 121 | 'mod_scheduler', 'studentfiles', $appointment->id, 122 | $this->uploadoptions); 123 | } 124 | $appointment->save(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/behat/behat_mod_scheduler.php: -------------------------------------------------------------------------------- 1 | (?:[^"]|\\")*)" scheduler and I fill the form with:$/ 30 | * 31 | * @param int $daysahead 32 | * @param int $time 33 | * @param string $activityname 34 | * @param TableNode $fielddata 35 | */ 36 | public function i_add_a_slot_days_ahead_at_in_scheduler_and_i_fill_the_form_with( 37 | $daysahead, $time, $activityname, TableNode $fielddata) { 38 | 39 | $hours = floor($time / 100); 40 | $mins = $time - 100 * $hours; 41 | $startdate = time() + $daysahead * DAYSECS; 42 | 43 | $this->execute('behat_general::click_link', $this->escape($activityname)); 44 | $this->execute('behat_general::i_click_on', array('Add slots', 'link')); 45 | $this->execute('behat_general::click_link', 'Add single slot'); 46 | 47 | $rows = array(); 48 | $rows[] = array('starttime[day]', date("j", $startdate)); 49 | $rows[] = array('starttime[month]', date("F", $startdate)); 50 | $rows[] = array('starttime[year]', date("Y", $startdate)); 51 | $rows[] = array('starttime[hour]', $hours); 52 | $rows[] = array('starttime[minute]', $mins); 53 | $rows[] = array('duration', '45'); 54 | foreach ($fielddata->getRows() as $row) { 55 | $rows[] = $row; 56 | } 57 | $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode($rows)); 58 | 59 | $this->execute('behat_general::i_click_on', array('Save changes', 'button')); 60 | } 61 | 62 | 63 | /** 64 | * Adds a series of slots to the scheduler 65 | * 66 | * @Given /^I add (\d+) slots (\d+) days ahead in "(?P(?:[^"]|\\")*)" scheduler and I fill the form with:$/ 67 | * 68 | * @param int $slotcount 69 | * @param int $daysahead 70 | * @param string $activityname 71 | * @param TableNode $fielddata 72 | */ 73 | public function i_add_slots_days_ahead_in_scheduler_and_i_fill_the_form_with( 74 | $slotcount, $daysahead, $activityname, TableNode $fielddata) { 75 | 76 | $startdate = time() + $daysahead * DAYSECS; 77 | 78 | $this->execute('behat_general::click_link', $this->escape($activityname)); 79 | $this->execute('behat_general::i_click_on', array('Add slots', 'link')); 80 | $this->execute('behat_general::click_link', 'Add repeated slots'); 81 | 82 | $rows = array(); 83 | $rows[] = array('rangestart[day]', date("j", $startdate)); 84 | $rows[] = array('rangestart[month]', date("F", $startdate)); 85 | $rows[] = array('rangestart[year]', date("Y", $startdate)); 86 | $rows[] = array('Saturday', '1'); 87 | $rows[] = array('Sunday', '1'); 88 | $rows[] = array('starthour', '1'); 89 | $rows[] = array('endhour', $slotcount + 1); 90 | $rows[] = array('duration', '45'); 91 | $rows[] = array('break', '15'); 92 | foreach ($fielddata->getRows() as $row) { 93 | $rows[] = $row; 94 | } 95 | 96 | $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode($rows)); 97 | 98 | $this->execute('behat_general::i_click_on', array('Save changes', 'button')); 99 | 100 | } 101 | 102 | /** 103 | * Add the "upcoming events" block, globally on every page. 104 | * 105 | * This is useful as it provides an easy way of checking a user's calendar entries. 106 | * 107 | * @Given /^I add the upcoming events block globally$/ 108 | */ 109 | public function i_add_the_upcoming_events_block_globally() { 110 | 111 | $home = $this->escape(get_string('sitehome')); 112 | 113 | $this->execute('behat_data_generators::the_following_exist', array('users', 114 | new TableNode(array( 115 | array('username', 'firstname', 'lastname', 'email'), 116 | array('globalmanager1', 'GlobalManager', '1', 'globalmanager1@example.com') 117 | )) ) ); 118 | 119 | $this->execute('behat_data_generators::the_following_exist', array('system role assigns', 120 | new TableNode(array( 121 | array('user', 'role'), 122 | array('globalmanager1', 'manager') 123 | )) ) ); 124 | $this->execute('behat_auth::i_log_in_as', 'globalmanager1'); 125 | $this->execute('behat_general::click_link', $home); 126 | $this->execute('behat_navigation::i_navigate_to_in_current_page_administration', array('Turn editing on')); 127 | $this->execute('behat_blocks::i_add_the_block', 'Upcoming events'); 128 | 129 | $this->execute('behat_blocks::i_open_the_blocks_action_menu', 'Upcoming events'); 130 | $this->execute('behat_general::click_link', 'Configure Upcoming events block'); 131 | $this->execute('behat_forms::i_set_the_following_fields_to_these_values', new TableNode(array( 132 | array('Page contexts', 'Display throughout the entire site') 133 | )) ); 134 | $this->execute('behat_general::i_click_on', array('Save changes', 'button')); 135 | $this->execute('behat_auth::i_log_out'); 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /appointmentforms.php: -------------------------------------------------------------------------------- 1 | libdir.'/formslib.php'); 15 | 16 | /** 17 | * Form to edit one appointment 18 | * 19 | * @package mod_scheduler 20 | * @copyright 2016 Henning Bostelmann and others (see README.txt) 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | class scheduler_editappointment_form extends moodleform { 24 | 25 | /** 26 | * @var scheduler_appointment the appointment being edited 27 | */ 28 | protected $appointment; 29 | 30 | /** 31 | * @var bool whether to distribute grade to all group members 32 | */ 33 | protected $distribute; 34 | 35 | /** 36 | * @var whether the teacher can edit grades 37 | */ 38 | protected $editgrade; 39 | 40 | /** 41 | * @var array options for notes fields 42 | */ 43 | public $noteoptions; 44 | 45 | /** 46 | * Create a new edit appointment form 47 | * 48 | * @param scheduler_appointment $appointment the appointment to edit 49 | * @param mixed $action the action attribute for the form 50 | * @param bool $editgrade whether the grade can be edited 51 | * @param bool $distribute whether to distribute grades to all group members 52 | */ 53 | public function __construct(scheduler_appointment $appointment, $action, $editgrade, $distribute) { 54 | $this->appointment = $appointment; 55 | $this->distribute = $distribute; 56 | $this->editgrade = $editgrade; 57 | $this->noteoptions = array('trusttext' => true, 'maxfiles' => -1, 'maxbytes' => 0, 58 | 'context' => $appointment->get_scheduler()->get_context(), 59 | 'subdirs' => false, 'collapsed' => true); 60 | parent::__construct($action, null); 61 | } 62 | 63 | protected function definition() { 64 | 65 | global $output; 66 | 67 | $mform = $this->_form; 68 | $scheduler = $this->appointment->get_scheduler(); 69 | 70 | // Seen tickbox. 71 | $mform->addElement('checkbox', 'attended', get_string('attended', 'scheduler')); 72 | 73 | // Grade. 74 | if ($scheduler->scale != 0) { 75 | if ($this->editgrade) { 76 | $gradechoices = $output->grading_choices($scheduler); 77 | $mform->addElement('select', 'grade', get_string('grade', 'scheduler'), $gradechoices); 78 | } else { 79 | $gradetext = $output->format_grade($scheduler, $this->appointment->grade); 80 | $mform->addElement('static', 'gradedisplay', get_string('grade', 'scheduler'), $gradetext); 81 | } 82 | } 83 | // Appointment notes (visible to teacher and/or student). 84 | if ($scheduler->uses_appointmentnotes()) { 85 | $mform->addElement('editor', 'appointmentnote_editor', get_string('appointmentnote', 'scheduler'), 86 | array('rows' => 3, 'columns' => 60), $this->noteoptions); 87 | $mform->setType('appointmentnote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. 88 | } 89 | if ($scheduler->uses_teachernotes()) { 90 | $mform->addElement('editor', 'teachernote_editor', get_string('teachernote', 'scheduler'), 91 | array('rows' => 3, 'columns' => 60), $this->noteoptions); 92 | $mform->setType('teachernote', PARAM_RAW); // Must be PARAM_RAW for rich text editor content. 93 | } 94 | if ($this->distribute && ($scheduler->uses_appointmentnotes() || $scheduler->uses_teachernotes() || $this->editgrade) ) { 95 | $mform->addElement('checkbox', 'distribute', get_string('distributetoslot', 'scheduler')); 96 | $mform->setDefault('distribute', false); 97 | } 98 | 99 | $this->add_action_buttons(); 100 | } 101 | 102 | public function validation($data, $files) { 103 | $errors = parent::validation($data, $files); 104 | 105 | return $errors; 106 | } 107 | 108 | /** 109 | * Prepare form data from an appointment record 110 | * 111 | * @param scheduler_appointment $appointment appointment to edit 112 | * @return stdClass form data 113 | */ 114 | public function prepare_appointment_data(scheduler_appointment $appointment) { 115 | $newdata = clone($appointment->get_data()); 116 | $context = $this->appointment->get_scheduler()->get_context(); 117 | 118 | $newdata = file_prepare_standard_editor($newdata, 'appointmentnote', $this->noteoptions, $context, 119 | 'mod_scheduler', 'appointmentnote', $this->appointment->id); 120 | 121 | $newdata = file_prepare_standard_editor($newdata, 'teachernote', $this->noteoptions, $context, 122 | 'mod_scheduler', 'teachernote', $this->appointment->id); 123 | return $newdata; 124 | } 125 | 126 | /** 127 | * Save form data into appointment record 128 | * 129 | * @param stdClass $formdata data extracted from form 130 | * @param scheduler_appointment $appointment appointment to update 131 | */ 132 | public function save_appointment_data(stdClass $formdata, scheduler_appointment $appointment) { 133 | $scheduler = $appointment->get_scheduler(); 134 | $cid = $scheduler->context->id; 135 | $appointment->set_data($formdata); 136 | $appointment->attended = isset($formdata->attended); 137 | if ($scheduler->uses_appointmentnotes() && isset($formdata->appointmentnote_editor)) { 138 | $editor = $formdata->appointmentnote_editor; 139 | $appointment->appointmentnote = file_save_draft_area_files($editor['itemid'], $cid, 140 | 'mod_scheduler', 'appointmentnote', $appointment->id, 141 | $this->noteoptions, $editor['text']); 142 | $appointment->appointmentnoteformat = $editor['format']; 143 | } 144 | if ($scheduler->uses_teachernotes() && isset($formdata->teachernote_editor)) { 145 | $editor = $formdata->teachernote_editor; 146 | $appointment->teachernote = file_save_draft_area_files($editor['itemid'], $cid, 147 | 'mod_scheduler', 'teachernote', $appointment->id, 148 | $this->noteoptions, $editor['text']); 149 | $appointment->teachernoteformat = $editor['format']; 150 | } 151 | $appointment->save(); 152 | if (isset($formdata->distribute)) { 153 | $slot = $appointment->get_slot(); 154 | $slot->distribute_appointment_data($appointment); 155 | } 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /db/install.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 |
-------------------------------------------------------------------------------- /exportform.php: -------------------------------------------------------------------------------- 1 | libdir.'/formslib.php'); 14 | require_once($CFG->dirroot.'/mod/scheduler/exportlib.php'); 15 | 16 | /** 17 | * Export settings form 18 | * (using Moodle formslib) 19 | * 20 | * @package mod_scheduler 21 | * @copyright 2015 Henning Bostelmann and others (see README.txt) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | class scheduler_export_form extends moodleform { 25 | 26 | /** 27 | * @var scheduler_instance the scheduler to be exported 28 | */ 29 | protected $scheduler; 30 | 31 | /** 32 | * Create a new export settings form. 33 | * 34 | * @param string $action 35 | * @param scheduler_instance $scheduler the scheduler to export 36 | * @param object $customdata 37 | */ 38 | public function __construct($action, scheduler_instance $scheduler, $customdata=null) { 39 | $this->scheduler = $scheduler; 40 | parent::__construct($action, $customdata); 41 | } 42 | 43 | protected function definition() { 44 | 45 | $mform = $this->_form; 46 | 47 | // General introduction. 48 | $mform->addElement('header', 'general', get_string('general', 'form')); 49 | 50 | $radios = array(); 51 | $radios[] = $mform->createElement('radio', 'content', '', 52 | get_string('onelineperslot', 'scheduler'), 'onelineperslot'); 53 | $radios[] = $mform->createElement('radio', 'content', '', 54 | get_string('onelineperappointment', 'scheduler'), 'onelineperappointment'); 55 | $radios[] = $mform->createElement('radio', 'content', '', 56 | get_string('appointmentsgrouped', 'scheduler'), 'appointmentsgrouped'); 57 | $mform->addGroup($radios, 'contentgroup', 58 | get_string('contentformat', 'scheduler'), null, false); 59 | $mform->setDefault('content', 'onelineperappointment'); 60 | $mform->addHelpButton('contentgroup', 'contentformat', 'scheduler'); 61 | 62 | if (has_capability('mod/scheduler:canseeotherteachersbooking', $this->scheduler->get_context())) { 63 | $selopt = array('me' => get_string('myself', 'scheduler'), 64 | 'all' => get_string ('everyone', 'scheduler')); 65 | $mform->addElement('select', 'includewhom', get_string('includeslotsfor', 'scheduler'), $selopt); 66 | $mform->setDefault('includewhom', 'all'); 67 | 68 | $selopt = array('all' => get_string('allononepage', 'scheduler'), 69 | 'perteacher' => get_string('pageperteacher', 'scheduler', $this->scheduler->get_teacher_name()) ); 70 | $mform->addElement('select', 'paging', get_string('pagination', 'scheduler'), $selopt); 71 | $mform->addHelpButton('paging', 'pagination', 'scheduler'); 72 | 73 | } 74 | 75 | $mform->addElement('selectyesno', 'includeemptyslots', get_string('includeemptyslots', 'scheduler')); 76 | $mform->setDefault('includeemptyslots', 1); 77 | 78 | // Select data to export. 79 | $mform->addElement('header', 'datafieldhdr', get_string('datatoinclude', 'scheduler')); 80 | $mform->addHelpButton('datafieldhdr', 'datatoinclude', 'scheduler'); 81 | 82 | $this->add_exportfield_group('slot', 'slot'); 83 | $this->add_exportfield_group('student', 'student'); 84 | $this->add_exportfield_group('appointment', 'appointment'); 85 | 86 | $mform->setDefault('field-date', 1); 87 | $mform->setDefault('field-starttime', 1); 88 | $mform->setDefault('field-endtime', 1); 89 | $mform->setDefault('field-teachername', 1); 90 | $mform->setDefault('field-studentfullname', 1); 91 | $mform->setDefault('field-attended', 1); 92 | 93 | // Output file format. 94 | $mform->addElement('header', 'fileformathdr', get_string('fileformat', 'scheduler')); 95 | $mform->addHelpButton('fileformathdr', 'fileformat', 'scheduler'); 96 | 97 | $radios = array(); 98 | $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('csvformat', 'scheduler'), 'csv'); 99 | $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('excelformat', 'scheduler'), 'xls'); 100 | $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('odsformat', 'scheduler'), 'ods'); 101 | $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('htmlformat', 'scheduler'), 'html'); 102 | $radios[] = $mform->createElement('radio', 'outputformat', '', get_string('pdfformat', 'scheduler'), 'pdf'); 103 | $mform->addGroup($radios, 'outputformatgroup', get_string('fileformat', 'scheduler'), null, false); 104 | $mform->setDefault('outputformat', 'csv'); 105 | 106 | $selopt = array('comma' => get_string('sepcomma', 'scheduler'), 107 | 'colon' => get_string('sepcolon', 'scheduler'), 108 | 'semicolon' => get_string('sepsemicolon', 'scheduler'), 109 | 'tab' => get_string('septab', 'scheduler')); 110 | $mform->addElement('select', 'csvseparator', get_string('csvfieldseparator', 'scheduler'), $selopt); 111 | $mform->setDefault('csvseparator', 'comma'); 112 | $mform->disabledIf('csvseparator', 'outputformat', 'neq', 'csv'); 113 | 114 | $selopt = array('P' => get_string('portrait', 'scheduler'), 115 | 'L' => get_string('landscape', 'scheduler')); 116 | $mform->addElement('select', 'pdforientation', get_string('pdforientation', 'scheduler'), $selopt); 117 | $mform->disabledIf('pdforientation', 'outputformat', 'neq', 'pdf'); 118 | 119 | $buttonarray = array(); 120 | $buttonarray[] = $mform->createElement('submit', 'preview', get_string('preview', 'scheduler')); 121 | $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('createexport', 'scheduler')); 122 | $buttonarray[] = $mform->createElement('cancel'); 123 | $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); 124 | $mform->closeHeaderBefore('buttonar'); 125 | 126 | } 127 | 128 | /** 129 | * Add a group of export fields to the form. 130 | * 131 | * @param string $groupid id of the group in the list of fields 132 | * @param string $labelid language string id for the group label 133 | */ 134 | private function add_exportfield_group($groupid, $labelid) { 135 | 136 | $mform = $this->_form; 137 | $fields = scheduler_get_export_fields($this->scheduler); 138 | $checkboxes = array(); 139 | 140 | foreach ($fields as $field) { 141 | if ($field->get_group() == $groupid && $field->is_available($this->scheduler)) { 142 | $inputid = 'field-'.$field->get_id(); 143 | $label = $field->get_formlabel($this->scheduler); 144 | $checkboxes[] = $mform->createElement('checkbox', $inputid, '', $label); 145 | } 146 | } 147 | $grouplabel = get_string($labelid, 'scheduler'); 148 | $mform->addGroup($checkboxes, 'fields-'.$groupid, $grouplabel, null, false); 149 | } 150 | 151 | public function validation($data, $files) { 152 | $errors = parent::validation($data, $files); 153 | 154 | return $errors; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /tests/behat/groupscheduling.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: Entire groups can be booked into slots at once 3 | In order to allow booking of entire groups 4 | As a teacher 5 | I need to use a scheduler with group bookings 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | edteacher1 | Editingteacher | 1 | edteacher1@example.com | 11 | | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | 12 | | student1 | Student | 1 | student1@example.com | 13 | | student2 | Student | 2 | student2@example.com | 14 | | student3 | Student | 3 | student3@example.com | 15 | | student4 | Student | 4 | student4@example.com | 16 | And the following "courses" exist: 17 | | fullname | shortname | category | 18 | | Course 1 | C1 | 0 | 19 | And the following "course enrolments" exist: 20 | | user | course | role | 21 | | edteacher1 | C1 | editingteacher | 22 | | neteacher1 | C1 | teacher | 23 | | student1 | C1 | student | 24 | | student2 | C1 | student | 25 | | student3 | C1 | student | 26 | | student4 | C1 | student | 27 | And the following "groups" exist: 28 | | name | course | idnumber | 29 | | Group A1 | C1 | GA1 | 30 | | Group A2 | C1 | GA2 | 31 | | Group B1 | C1 | GB1 | 32 | | Group B2 | C1 | GB2 | 33 | And the following "groupings" exist: 34 | | name | course | idnumber | 35 | | Grouping A | C1 | GROUPINGA | 36 | | Grouping B | C1 | GROUPINGB | 37 | And the following "group members" exist: 38 | | user | group | 39 | | neteacher1 | GB1 | 40 | | neteacher1 | GA1 | 41 | | student1 | GA1 | 42 | | student2 | GA1 | 43 | | student3 | GA2 | 44 | | student4 | GA2 | 45 | | student1 | GB1 | 46 | | student2 | GB2 | 47 | | student3 | GB1 | 48 | | student4 | GB2 | 49 | And the following "grouping groups" exist: 50 | | grouping | group | 51 | | GROUPINGA | GA1 | 52 | | GROUPINGA | GA2 | 53 | | GROUPINGB | GB1 | 54 | | GROUPINGB | GB2 | 55 | And the following "activities" exist: 56 | | activity | name | intro | course | idnumber | 57 | | scheduler | Test scheduler no grouping | n | C1 | schedulern | 58 | | scheduler | Test scheduler grouping A | n | C1 | schedulera | 59 | | scheduler | Test scheduler grouping B | n | C1 | schedulerb | 60 | And I log in as "edteacher1" 61 | And I am on "Course 1" course homepage 62 | And I follow "Test scheduler no grouping" 63 | And I navigate to "Edit settings" in current page administration 64 | And I set the following fields to these values: 65 | | Booking in groups | Yes, for all groups | 66 | And I click on "Save and return to course" "button" 67 | And I follow "Test scheduler grouping A" 68 | And I navigate to "Edit settings" in current page administration 69 | And I set the following fields to these values: 70 | | Booking in groups | Yes, in grouping Grouping A | 71 | And I click on "Save and return to course" "button" 72 | And I follow "Test scheduler grouping B" 73 | And I navigate to "Edit settings" in current page administration 74 | And I set the following fields to these values: 75 | | Booking in groups | Yes, in grouping Grouping B | 76 | And I click on "Save and return to course" "button" 77 | And I log out 78 | 79 | @javascript 80 | Scenario: Editing teachers can see and schedule relevant groups 81 | Given I log in as "edteacher1" 82 | And I am on "Course 1" course homepage 83 | 84 | When I am on "Course 1" course homepage 85 | And I follow "Test scheduler no grouping" 86 | Then I should see "Group A1" in the "groupstoschedule" "table" 87 | And I should see "Group A2" in the "groupstoschedule" "table" 88 | And I should see "Group B1" in the "groupstoschedule" "table" 89 | And I should see "Group B2" in the "groupstoschedule" "table" 90 | 91 | When I am on "Course 1" course homepage 92 | And I follow "Test scheduler grouping A" 93 | Then I should see "Group A1" in the "groupstoschedule" "table" 94 | And I should see "Group A2" in the "groupstoschedule" "table" 95 | And I should not see "Group B" in the "groupstoschedule" "table" 96 | 97 | When I am on "Course 1" course homepage 98 | And I follow "Test scheduler grouping B" 99 | Then I should not see "Group A" in the "groupstoschedule" "table" 100 | And I should see "Group B1" in the "groupstoschedule" "table" 101 | And I should see "Group B2" in the "groupstoschedule" "table" 102 | 103 | When I am on "Course 1" course homepage 104 | And I follow "Test scheduler no grouping" 105 | And I click on "Schedule" "link_or_button" in the "Group A1" "table_row" 106 | And I click on "Schedule in slot" "text" in the "Group A1" "table_row" 107 | And I click on "Save changes" "button" 108 | Then I should see "Student 1" in the "slotmanager" "table" 109 | And I should see "Student 2" in the "slotmanager" "table" 110 | And I should see "2 students still need to make an appointment" 111 | And I should not see "Group A1" in the "groupstoschedule" "table" 112 | And I should see "Group A2" in the "groupstoschedule" "table" 113 | And I should not see "Group B1" in the "groupstoschedule" "table" 114 | And I should not see "Group B2" in the "groupstoschedule" "table" 115 | 116 | @javascript 117 | Scenario: Students can book their entire group into a slot 118 | Given I log in as "edteacher1" 119 | And I am on "Course 1" course homepage 120 | And I follow "Test scheduler no grouping" 121 | And I add 8 slots 5 days ahead in "Test scheduler" scheduler and I fill the form with: 122 | | Location | Large office | 123 | | exclusivity | 5 | 124 | And I add 5 slots 6 days ahead in "Test scheduler" scheduler and I fill the form with: 125 | | Location | Small office | 126 | | exclusivity | 1 | 127 | And I log out 128 | 129 | When I log in as "student1" 130 | And I am on "Course 1" course homepage 131 | And I follow "Test scheduler no grouping" 132 | Then the "appointgroup" select box should contain "Myself" 133 | And the "appointgroup" select box should contain "Group A1" 134 | And the "appointgroup" select box should contain "Group B1" 135 | And the "appointgroup" select box should not contain "Group A2" 136 | And the "appointgroup" select box should not contain "Group B2" 137 | 138 | When I set the field "appointgroup" to "Group A1" 139 | And I click on "Book slot" "button" in the "8:00 AM" "table_row" 140 | Then I should see "8:00 AM" in the "Large office" "table_row" 141 | And I log out 142 | 143 | When I log in as "edteacher1" 144 | And I am on "Course 1" course homepage 145 | And I follow "Test scheduler no grouping" 146 | Then I should see "Student 1" in the "8:00 AM" "table_row" 147 | And I should see "Student 2" in the "8:00 AM" "table_row" 148 | And I should see "2 students still need to make an appointment" 149 | And I should not see "Group A1" in the "groupstoschedule" "table" 150 | And I should see "Group A2" in the "groupstoschedule" "table" 151 | And I should not see "Group B1" in the "groupstoschedule" "table" 152 | And I should not see "Group B2" in the "groupstoschedule" "table" 153 | And I log out 154 | 155 | -------------------------------------------------------------------------------- /tests/behat/notes.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: Teachers can write notes on slots and appointments 3 | In order to record details about a meeting 4 | As a teacher 5 | I need to enter notes for the appointment 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | edteacher1 | Editingteacher | 1 | edteacher1@example.com | 11 | | neteacher1 | Nonedteacher | 1 | neteacher1@example.com | 12 | | student1 | Student | 1 | student1@example.com | 13 | And the following "courses" exist: 14 | | fullname | shortname | category | 15 | | Course 1 | C1 | 0 | 16 | And the following "course enrolments" exist: 17 | | user | course | role | 18 | | edteacher1 | C1 | editingteacher | 19 | | neteacher1 | C1 | teacher | 20 | | student1 | C1 | student | 21 | And the following "activities" exist: 22 | | activity | name | intro | course | idnumber | usenotes | 23 | | scheduler | Test scheduler | n | C1 | schedulern | 3 | 24 | And I log in as "edteacher1" 25 | And I am on "Course 1" course homepage 26 | And I add 5 slots 10 days ahead in "Test scheduler" scheduler and I fill the form with: 27 | | Location | Here | 28 | And I log out 29 | 30 | @javascript 31 | Scenario: Teachers can enter slot notes and appointment notes for others to see 32 | When I log in as "edteacher1" 33 | And I am on "Course 1" course homepage 34 | And I follow "Test scheduler" 35 | And I follow "Statistics" 36 | And I follow "All appointments" 37 | And I click on "Edit" "link" in the "4:00 AM" "table_row" 38 | And I set the following fields to these values: 39 | | Comments | Note-for-slot | 40 | And I click on "Save" "button" 41 | Then I should see "slot updated" 42 | When I click on "Edit" "link" in the "4:00 AM" "table_row" 43 | Then I should see "Note-for-slot" 44 | And I log out 45 | 46 | When I log in as "student1" 47 | And I am on "Course 1" course homepage 48 | And I follow "Test scheduler" 49 | Then I should see "Note-for-slot" in the "4:00 AM" "table_row" 50 | When I click on "Book slot" "button" in the "4:00 AM" "table_row" 51 | Then I should see "Note-for-slot" 52 | And I log out 53 | 54 | When I log in as "edteacher1" 55 | And I am on "Course 1" course homepage 56 | And I follow "Test scheduler" 57 | And I follow "Statistics" 58 | And I follow "All appointments" 59 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 60 | Then I should see ", 4:00 AM" in the "Date and time" "table_row" 61 | And I should see "4:45 AM" in the "Date and time" "table_row" 62 | And I should see "Editingteacher 1" in the "Teacher" "table_row" 63 | And I set the following fields to these values: 64 | | Attended | 1 | 65 | | Notes for appointment (visible to student) | note-for-appointment | 66 | | Confidential notes (visible to teacher only) | note-confidential | 67 | And I click on "Save changes" "button" 68 | Then I should see "note-for-appointment" 69 | And I should see "note-confidential" 70 | And I log out 71 | 72 | When I log in as "student1" 73 | And I am on "Course 1" course homepage 74 | And I follow "Test scheduler" 75 | Then I should see "Attended slots" 76 | And I should see "note-for-appointment" 77 | And I should not see "note-confidential" 78 | And I log out 79 | 80 | @javascript 81 | Scenario: Teachers see only the comments fields specified in the configuration 82 | 83 | When I log in as "student1" 84 | And I am on "Course 1" course homepage 85 | And I follow "Test scheduler" 86 | And I click on "Book slot" "button" in the "4:00 AM" "table_row" 87 | Then I should see "Upcoming slots" 88 | And I log out 89 | 90 | When I log in as "edteacher1" 91 | And I am on "Course 1" course homepage 92 | And I follow "Test scheduler" 93 | And I follow "Statistics" 94 | And I follow "All appointments" 95 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 96 | And I set the following fields to these values: 97 | | Notes for appointment (visible to student) | note-for-appointment | 98 | | Confidential notes (visible to teacher only) | note-confidential | 99 | And I click on "Save changes" "button" 100 | Then I should see "note-for-appointment" 101 | And I should see "note-confidential" 102 | 103 | When I follow "Test scheduler" 104 | And I navigate to "Edit settings" in current page administration 105 | And I set the field "Use notes for appointments" to "0" 106 | And I click on "Save and display" "button" 107 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 108 | Then I should not see "Notes for appointment" 109 | And I should not see "note-for-appointment" 110 | And I should not see "Confidential notes" 111 | And I should not see "note-confidential" 112 | And I click on "Save changes" "button" 113 | And I log out 114 | 115 | When I log in as "student1" 116 | And I am on "Course 1" course homepage 117 | And I follow "Test scheduler" 118 | Then I should not see "note-for-appointment" 119 | And I should not see "note-confidential" 120 | And I log out 121 | 122 | When I log in as "edteacher1" 123 | And I am on "Course 1" course homepage 124 | And I follow "Test scheduler" 125 | And I navigate to "Edit settings" in current page administration 126 | And I set the field "Use notes for appointments" to "1" 127 | And I click on "Save and display" "button" 128 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 129 | Then I should see "Notes for appointment" 130 | And I should see "note-for-appointment" 131 | And I should not see "Confidential notes" 132 | And I should not see "note-confidential" 133 | And I click on "Save changes" "button" 134 | And I log out 135 | 136 | When I log in as "student1" 137 | And I am on "Course 1" course homepage 138 | And I follow "Test scheduler" 139 | Then I should see "note-for-appointment" 140 | And I should not see "note-confidential" 141 | And I log out 142 | 143 | When I log in as "edteacher1" 144 | And I am on "Course 1" course homepage 145 | And I follow "Test scheduler" 146 | And I navigate to "Edit settings" in current page administration 147 | And I set the field "Use notes for appointments" to "2" 148 | And I click on "Save and display" "button" 149 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 150 | Then I should not see "Notes for appointment" 151 | And I should not see "note-for-appointment" 152 | And I should see "Confidential notes" 153 | And I should see "note-confidential" 154 | And I click on "Save changes" "button" 155 | And I log out 156 | 157 | When I log in as "student1" 158 | And I am on "Course 1" course homepage 159 | And I follow "Test scheduler" 160 | Then I should not see "note-for-appointment" 161 | And I should not see "note-confidential" 162 | And I log out 163 | 164 | When I log in as "edteacher1" 165 | And I am on "Course 1" course homepage 166 | And I follow "Test scheduler" 167 | And I navigate to "Edit settings" in current page administration 168 | And I set the field "Use notes for appointments" to "3" 169 | And I click on "Save and display" "button" 170 | And I click on "//a[text()='Student 1']" "xpath_element" in the "4:00 AM" "table_row" 171 | Then I should see "Notes for appointment" 172 | And I should see "note-for-appointment" 173 | And I should see "Confidential notes" 174 | And I should see "note-confidential" 175 | And I log out -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Appointment Scheduler for Moodle 2 | 3 | This program is free software; you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation; either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details: 12 | 13 | http://www.gnu.org/copyleft/gpl.html 14 | 15 | 16 | === Description === 17 | 18 | The Scheduler module helps you to schedule appointments with your students. 19 | Teachers specify time slots for meetings, students then choose one of them on Moodle. 20 | Teacher in turn can record the outcome of the meeting - and optionally a grade - 21 | within the scheduler. 22 | 23 | For further information, please see: 24 | http://docs.moodle.org/33/en/Scheduler_module 25 | 26 | (Note that the information there may refer to a previous version of the module.) 27 | 28 | 29 | === Installation instructions === 30 | 31 | Place the code of the module into the mod/scheduler directory of your Moodle 32 | directory root. That is, the present file should be located at: 33 | mod/scheduler/README.txt 34 | 35 | For further installation instructions please see: 36 | http://docs.moodle.org/en/Installing_contributed_modules_or_plugins 37 | 38 | This module is intended for Moodle 3.3 and above. 39 | 40 | 41 | === Authors === 42 | 43 | Current maintainer: 44 | Henning Bostelmann, University of York 45 | 46 | Based on previous work by: 47 | 48 | * Gustav Delius (until Moodle 1.7) 49 | * Valery Fremaux (Moodle 1.8 - Moodle 1.9) 50 | 51 | With further contributions taken from: 52 | 53 | * Vivek Arora (independent migration of the module to 2.0) 54 | * Andriy Semenets (Russian and Ukrainian localization) 55 | * Gaël Mifsud (French localization) 56 | * Various authors of the core Moodle code 57 | 58 | 59 | === Release notes === 60 | 61 | --- Version 3.3 --- 62 | 63 | Intended for Moodle 3.3 and later. 64 | 65 | New features / improvements: 66 | 67 | Optionally, before making an appointment, students now see a booking screen 68 | in which they need to enter text, upload a file, and/or solve a captcha. 69 | 70 | Filter strings (e.g., multilang syntax) are now processed in course shortname, 71 | course fullname, and location fields. 72 | 73 | Export files can now include custom profile fields of students. 74 | 75 | Feature changes: 76 | 77 | For booking in groups, students now need to select explicitly which group 78 | they are booking for, or whether they want to make an individual booking. 79 | Individual bookings can be disabled via a global configuration setting. 80 | 81 | For viewing student's email addresses, the capability 82 | moodle/site:viewuseridentity is now required. 83 | 84 | When allowing an "unlimited" number of appointments, students will no longer 85 | be included in reminder e-mails if they have booked at least one slot. 86 | 87 | Refactoring / API changes: 88 | 89 | The function scheduler_get_user_fields() in customlib.php has changed 90 | signature. If you have customized it in an earlier version, you will want 91 | to edit your code. 92 | 93 | --- Version 3.1 --- 94 | 95 | Intended for Moodle 3.1 and later. 96 | 97 | New features / improvements: 98 | 99 | An additional "confidential note" field is supplied for appointments; 100 | the contents can be read by teachers only. 101 | 102 | Slot notes and appointment notes can now contain attachments. 103 | 104 | Students can now be allowed to see existing bookings of other students. 105 | See https://docs.moodle.org/31/en/Scheduler_Module_capabilities#Student_side 106 | 107 | Feature changes: 108 | 109 | Sending of invitations and reminders is no longer handled via a "mailto" link 110 | but rather via a webform, using Moodle's messaging system. 111 | 112 | The conflict detection feature (when creating new slots) has been reworked slightly. 113 | See https://docs.moodle.org/31/en/Scheduler:_Conflicts 114 | 115 | Refactoring / API changes: 116 | 117 | All email-related features now use the Messaging API. 118 | 119 | Appointment reminders and deletion of past unused slots are now handled via 120 | the Scheduled Tasks API. 121 | 122 | The new Search API is supported for the activity description only. 123 | 124 | --- Version 2.9 --- 125 | 126 | Intended for Moodle 2.9 and later. 127 | 128 | New features / improvements: 129 | 130 | The export screen now allows users to choose the format of the output file, 131 | as well as the data fields to include in the export. File format may 132 | slightly differ from previous versions. 133 | 134 | Improved gradebook integration: Grades overridden in the gradebook will now 135 | show up as such in the scheduler. 136 | 137 | Lists of students to be scheduled now take availability conditions 138 | (groups and groupings) into account. 139 | 140 | Feature changes: 141 | 142 | The handling of "group mode" in Scheduler has changed. The feature of "booking 143 | entire groups into a slot" is now controlled by a setting "Booking in groups" 144 | at the level of each scheduler. The setting "Group mode" in "Common module 145 | settings" is now used in line with usual Moodle conventions - setting it to, 146 | e.g., "Separate groups" will mean that students can only book slots with 147 | teachers in the same group. The old "Group mode" settings are automatically 148 | migrated to "Booking in groups" and the "Group mode" set to "None". 149 | If you have used group scheduling in previous versions, please check your data 150 | after migration. 151 | 152 | The student view has been redesigned. Bookable appointments are now displayed 153 | in pages of 25, and student select a slot by clicking a button "Book slot" 154 | rather then selecting with a radio button and clicking "Save choice". 155 | 156 | For using the Overview screen outside the current scheduler, e.g., for displaying 157 | all slots of a user across the site, users will now need extra permissions; 158 | see CONTRIB-5750 for details. 159 | 160 | Refactoring / API changes: 161 | 162 | Config settings have been migrated to the config_plugins table. 163 | 164 | --- Version 2.7 --- 165 | 166 | Intended for Moodle 2.7 and later. 167 | 168 | New features: 169 | 170 | Students can now be allowed to book several slots at a time. 171 | "Volatile slots" replaced with "guard time" - students cannot change their booking 172 | for slots closer than this time to the current time. 173 | 174 | Feature changes: 175 | 176 | "Notes" field will now be shown to students at booking time. 177 | 178 | Refactoring / API changes: 179 | 180 | Major refactoring of teacher view (slot list), student view (booking screen), 181 | teacher view of individual appointments, as well as of the backend. 182 | Security enhancements (sessionid parameter now used throughout). 183 | Adapted to changes in core API and to the new logging/event system (Event 2). 184 | 185 | --- Version 2.5 --- 186 | 187 | Intended for Moodle 2.5 and later. 188 | 189 | Module adapted to API changes Moodle core. 190 | "Add slot" and "Edit slot" forms refactored, now based on Moodle Forms. 191 | Language packs migrated to AMOS, removed from plugin codebase. 192 | 193 | --- Version 2.3 --- 194 | 195 | Intended for Moodle 2.3 and later; no major functional changes, but API adapted and minor enhancements. 196 | 197 | --- Version 2.0 --- 198 | 199 | No major functional changes over 1.9; bug fixes and API migration only. Requires 1.9 for database upgrades. 200 | 201 | 202 | === Technical notes === 203 | 204 | The code of this module is rather old, some of it still predates even Moodle 1.9. 205 | It has now largely, but not completely, been adapted to the new APIs. 206 | The following aspects have been migrated, that is, malfunction in this respect 207 | should be considered a bug: 208 | 209 | * Gradebook integration 210 | * Moodle 2 backup 211 | * New rich text editor and file API 212 | * Localization / language packs 213 | * Logging / event system 214 | * Scheduler tasks API 215 | * Messaging API 216 | 217 | The module does not use any deprecated API as of Moodle 3.3. 218 | -------------------------------------------------------------------------------- /tests/behat/conflicts.feature: -------------------------------------------------------------------------------- 1 | @mod_scheduler 2 | Feature: Teachers are warned about scheduling conflicts 3 | In order to create useful slots 4 | As a teacher 5 | I need to take care not to create conflicting schedules. 6 | 7 | Background: 8 | Given the following "users" exist: 9 | | username | firstname | lastname | email | 10 | | manager1 | Manager | 1 | manager1@example.com | 11 | | teacher1 | Teacher | 1 | teacher1@example.com | 12 | | teacher2 | Teacher | 2 | teacher2@example.com | 13 | | student1 | Student | 1 | student1@example.com | 14 | And the following "courses" exist: 15 | | fullname | shortname | category | 16 | | Course 1 | C1 | 0 | 17 | And the following "course enrolments" exist: 18 | | user | course | role | 19 | | teacher1 | C1 | editingteacher | 20 | | teacher2 | C1 | editingteacher | 21 | | student1 | C1 | student | 22 | And the following "system role assigns" exist: 23 | | user | role | 24 | | manager1 | manager | 25 | And the following "activities" exist: 26 | | activity | name | intro | course | idnumber | groupmode | schedulermode | maxbookings | 27 | | scheduler | Test scheduler A | n | C1 | schedulerA | 0 | oneonly | 1 | 28 | | scheduler | Test scheduler B | n | C1 | schedulerB | 0 | oneonly | 1 | 29 | 30 | @javascript 31 | Scenario: A teacher edits a single slot and is warned about conflicts 32 | 33 | Given I log in as "teacher1" 34 | And I am on "Course 1" course homepage 35 | And I add 5 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: 36 | | Location | My office | 37 | And I am on "Course 1" course homepage 38 | And I add a slot 5 days ahead at 1000 in "Test scheduler B" scheduler and I fill the form with: 39 | | Location | My office | 40 | 41 | When I am on "Course 1" course homepage 42 | And I follow "Test scheduler A" 43 | And I click on "Edit" "link" in the "2:00 AM" "table_row" 44 | And I set the following fields to these values: 45 | | starttime[minute] | 40 | 46 | And I click on "Save changes" "button" 47 | Then I should see "conflict" 48 | And "Save changes" "button" should exist 49 | And I should see "3:00 AM" 50 | And I should not see "2:00 AM" 51 | 52 | When I set the following fields to these values: 53 | | starttime[hour] | 09 | 54 | | starttime[minute] | 55 | 55 | And I click on "Save changes" "button" 56 | Then I should see "conflict" 57 | And I should see "in course C1, scheduler Test scheduler B" 58 | And I should see "10:00 AM" 59 | And I should not see "2:00 AM" 60 | And "Save changes" "button" should exist 61 | 62 | When I set the following fields to these values: 63 | | starttime[hour] | 09 | 64 | | starttime[minute] | 55 | 65 | | Ignore scheduling conflicts | 1 | 66 | And I click on "Save changes" "button" 67 | Then I should see "slot updated" 68 | And "9:55 AM" "table_row" should exist 69 | And I log out 70 | 71 | @javascript 72 | Scenario: A manager edits slots for several teachers, creating conflicts 73 | 74 | Given I log in as "manager1" 75 | And I follow "Site home" 76 | And I navigate to "Turn editing on" in current page administration 77 | And I add the "Navigation" block if not present 78 | And I click on "Courses" "link" in the "Navigation" "block" 79 | And I am on "Course 1" course homepage 80 | And I add 6 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: 81 | | Location | Office T1 | 82 | | Teacher | Teacher 1 | 83 | And I am on "Course 1" course homepage 84 | And I add 5 slots 5 days ahead in "Test scheduler B" scheduler and I fill the form with: 85 | | Location | Office T2 | 86 | | Teacher | Teacher 2 | 87 | 88 | When I am on "Course 1" course homepage 89 | And I follow "Test scheduler A" 90 | And I click on "Edit" "link" in the "3:00 AM" "table_row" 91 | And I set the following fields to these values: 92 | | starttime[hour] | 6 | 93 | | starttime[minute] | 40 | 94 | | duration | 5 | 95 | And I click on "Save changes" "button" 96 | Then I should see "conflict" 97 | And I should see "6:00 AM" 98 | And I should see "in this scheduler" 99 | And I should not see "3:00 AM" 100 | And "Save changes" "button" should exist 101 | 102 | When I set the following fields to these values: 103 | | starttime[hour] | 5 | 104 | | starttime[minute] | 40 | 105 | | duration | 5 | 106 | | Teacher | Teacher 2 | 107 | And I click on "Save changes" "button" 108 | Then I should see "conflict" 109 | And I should see "5:00 AM" 110 | And I should see "in course C1, scheduler Test scheduler B" 111 | And I should not see "3:00 AM" 112 | And "Save changes" "button" should exist 113 | 114 | When I set the following fields to these values: 115 | | starttime[hour] | 6 | 116 | | starttime[minute] | 40 | 117 | | duration | 5 | 118 | | Teacher | Teacher 2 | 119 | And I click on "Save changes" "button" 120 | Then I should not see "conflict" 121 | And I should see "slot updated" 122 | And "6:40 AM" "table_row" should exist 123 | And "Save changes" "button" should not exist 124 | And I log out 125 | 126 | 127 | @javascript 128 | Scenario: A teacher adds a series of slots, creating conflicts 129 | 130 | Given I log in as "teacher1" 131 | And I am on "Course 1" course homepage 132 | And I add a slot 5 days ahead at 0125 in "Test scheduler A" scheduler and I fill the form with: 133 | | Location | My office | 134 | | duration | 15 | 135 | # Blocks 3 other slots on a 1-hour grid 136 | And I am on "Course 1" course homepage 137 | And I add a slot 5 days ahead at 0225 in "Test scheduler A" scheduler and I fill the form with: 138 | | Location | My office | 139 | | duration | 100 | 140 | # Booked slot - must not be deleted as conflict 141 | And I am on "Course 1" course homepage 142 | And I add a slot 5 days ahead at 0855 in "Test scheduler A" scheduler and I fill the form with: 143 | | Location | My office | 144 | | duration | 10 | 145 | | studentid[0] | Student 1 | 146 | # Slot in other scheduler - must not be deleted as conflict 147 | And I am on "Course 1" course homepage 148 | And I add a slot 5 days ahead at 0605 in "Test scheduler B" scheduler and I fill the form with: 149 | | Location | My office | 150 | | duration | 20 | 151 | 152 | When I am on "Course 1" course homepage 153 | And I add 10 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: 154 | | Location | Lecture hall | 155 | Then I should see "conflicting slots" 156 | And I should not see "deleted" 157 | And I should see "4 slots have been added" 158 | And "1:25 AM" "table_row" should exist 159 | And "2:25 AM" "table_row" should exist 160 | And "8:55 AM" "table_row" should exist 161 | And "1:00 AM" "table_row" should not exist 162 | And "2:00 AM" "table_row" should not exist 163 | And "3:00 AM" "table_row" should not exist 164 | And "4:00 AM" "table_row" should not exist 165 | And "5:00 AM" "table_row" should exist 166 | And "6:00 AM" "table_row" should not exist 167 | And "7:00 AM" "table_row" should exist 168 | And "8:00 AM" "table_row" should exist 169 | And "9:00 AM" "table_row" should not exist 170 | And "10:00 AM" "table_row" should exist 171 | And I am on "Course 1" course homepage 172 | And I follow "Test scheduler B" 173 | And "6:05 AM" "table_row" should exist 174 | 175 | When I am on "Course 1" course homepage 176 | And I add 10 slots 5 days ahead in "Test scheduler A" scheduler and I fill the form with: 177 | | Location | Lecture hall | 178 | | Force when overlap | 1 | 179 | Then I should see "conflicting slots" 180 | And I should see "deleted" 181 | And I should see "8 slots have been added" 182 | And "1:25 AM" "table_row" should not exist 183 | And "2:25 AM" "table_row" should not exist 184 | And "9:55 AM" "table_row" should not exist 185 | And "1:00 AM" "table_row" should exist 186 | And "2:00 AM" "table_row" should exist 187 | And "3:00 AM" "table_row" should exist 188 | And "4:00 AM" "table_row" should exist 189 | And "5:00 AM" "table_row" should exist 190 | And "6:00 AM" "table_row" should not exist 191 | And "7:00 AM" "table_row" should exist 192 | And "8:00 AM" "table_row" should exist 193 | And "9:00 AM" "table_row" should not exist 194 | And "10:00 AM" "table_row" should exist 195 | And I am on "Course 1" course homepage 196 | And I follow "Test scheduler B" 197 | And "6:05 AM" "table_row" should exist 198 | 199 | And I log out 200 | -------------------------------------------------------------------------------- /tests/privacy_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Data provider tests. 19 | * 20 | * @package mod_scheduler 21 | * @category test 22 | * @copyright 2018 Henning Bostelmann 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | global $CFG; 28 | 29 | use core_privacy\tests\provider_testcase; 30 | use mod_scheduler\privacy\provider; 31 | use core_privacy\local\request\approved_contextlist; 32 | use core_privacy\local\request\approved_userlist; 33 | use core_privacy\local\request\writer; 34 | 35 | require_once($CFG->dirroot.'/mod/scheduler/locallib.php'); 36 | 37 | /** 38 | * Data provider testcase class. 39 | * 40 | * @group mod_scheduler 41 | * @copyright 2018 Henning Bostelmann 42 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 | */ 44 | class mod_scheduler_privacy_testcase extends provider_testcase { 45 | 46 | /** 47 | * @var int course_module id used for testing 48 | */ 49 | protected $moduleid; 50 | 51 | /** 52 | * @var the module context used for testing 53 | */ 54 | protected $context; 55 | 56 | /** 57 | * @var int Course id used for testing 58 | */ 59 | protected $courseid; 60 | 61 | /** 62 | * @var int Scheduler id used for testing 63 | */ 64 | protected $schedulerid; 65 | 66 | /** 67 | * @var int One of the slots used for testing 68 | */ 69 | protected $slotid; 70 | 71 | /** 72 | * @var int first student used in testing - a student that has an appointment 73 | */ 74 | protected $student1; 75 | 76 | /** 77 | * @var int second student used in testing - a student that has an appointment 78 | */ 79 | protected $student2; 80 | 81 | /** 82 | * @var array all students (only id) involved in the scheduler 83 | */ 84 | protected $allstudents; 85 | 86 | protected function setUp() { 87 | global $DB, $CFG; 88 | 89 | $this->resetAfterTest(true); 90 | 91 | $course = $this->getDataGenerator()->create_course(); 92 | $this->courseid = $course->id; 93 | 94 | 95 | $this->student1 = $this->getDataGenerator()->create_user(); 96 | $this->student2 = $this->getDataGenerator()->create_user(); 97 | $this->allstudents = [$this->student1->id, $this->student2->id]; 98 | 99 | $options = array(); 100 | $options['slottimes'] = array(); 101 | $options['slotstudents'] = array(); 102 | for ($c = 0; $c < 4; $c++) { 103 | $options['slottimes'][$c] = time() + ($c + 1) * DAYSECS; 104 | $stud = $this->getDataGenerator()->create_user()->id; 105 | $this->allstudents[] = $stud; 106 | $options['slotstudents'][$c] = array($stud); 107 | } 108 | $options['slottimes'][4] = time() + 10 * DAYSECS; 109 | $options['slottimes'][5] = time() + 11 * DAYSECS; 110 | $options['slotstudents'][5] = array( 111 | $this->student1->id, 112 | $this->student2->id 113 | ); 114 | 115 | $scheduler = $this->getDataGenerator()->create_module('scheduler', array('course' => $course->id), $options); 116 | $coursemodule = $DB->get_record('course_modules', array('id' => $scheduler->cmid)); 117 | 118 | $this->schedulerid = $scheduler->id; 119 | $this->moduleid = $coursemodule->id; 120 | $this->context = context_module::instance($scheduler->cmid); 121 | 122 | $recs = $DB->get_records('scheduler_slots', array('schedulerid' => $scheduler->id), 'id DESC'); 123 | $this->slotid = array_keys($recs)[0]; 124 | $this->appointmentids = array_keys($DB->get_records('scheduler_appointment', array('slotid' => $this->slotid))); 125 | } 126 | 127 | /** 128 | * Asserts whether or not an appointment exists in a scheduler for a certian student. 129 | * 130 | * @param int $schedulerid the id of the scheduler to test 131 | * @param int $studentid the user id of the student to test 132 | * @param boolean $expected whether an appointment is expected to exist or not 133 | */ 134 | private function assert_appointment_status($schedulerid, $studentid, $expected) { 135 | global $DB; 136 | 137 | $sql = "SELECT * FROM {scheduler} s 138 | JOIN {scheduler_slots} t ON t.schedulerid = s.id 139 | JOIN {scheduler_appointment} a ON a.slotid = t.id 140 | WHERE s.id = :schedulerid AND a.studentid = :studentid"; 141 | 142 | $params = array('schedulerid' => $schedulerid, 'studentid' => $studentid); 143 | $actual = $DB->record_exists_sql($sql, $params); 144 | $this->assertEquals($expected, $actual, "Checking whether student $studentid has appointment in scheduler $schedulerid"); 145 | } 146 | 147 | /** 148 | * Test getting the contexts for a user. 149 | */ 150 | public function test_get_contexts_for_userid() { 151 | 152 | // Get contexts for the first user. 153 | $contextids = provider::get_contexts_for_userid($this->student1->id)->get_contextids(); 154 | $this->assertEquals([$this->context->id], $contextids, '', 0.0, 10, true); 155 | } 156 | 157 | /** 158 | * Test getting the users within a context. 159 | */ 160 | public function test_get_users_in_context() { 161 | global $DB; 162 | $component = 'mod_scheduler'; 163 | 164 | // Ensure userlist for context contains all users. 165 | $userlist = new \core_privacy\local\request\userlist($this->context, $component); 166 | provider::get_users_in_context($userlist); 167 | 168 | $expected = $this->allstudents; 169 | $expected[] = 2; // The teacher involved. 170 | $actual = $userlist->get_userids(); 171 | sort($expected); 172 | sort($actual); 173 | $this->assertEquals($expected, $actual); 174 | } 175 | 176 | 177 | /** 178 | * Export test for teacher data. 179 | */ 180 | public function test_export_teacher_data() { 181 | global $DB; 182 | 183 | // Export all contexts for the teacher. 184 | $contextids = [$this->context->id]; 185 | $teacher = $DB->get_record('user', array('id' => 2)); 186 | $appctx = new approved_contextlist($teacher, 'mod_scheduler', $contextids); 187 | provider::export_user_data($appctx); 188 | $data = writer::with_context($this->context)->get_data([]); 189 | $this->assertNotEmpty($data); 190 | } 191 | 192 | /** 193 | * Export test for student1's data. 194 | */ 195 | public function test_export_user_data1() { 196 | 197 | // Export all contexts for the first user. 198 | $contextids = [$this->context->id]; 199 | $appctx = new approved_contextlist($this->student1, 'mod_scheduler', $contextids); 200 | provider::export_user_data($appctx); 201 | $data = writer::with_context($this->context)->get_data([]); 202 | $this->assertNotEmpty($data); 203 | } 204 | 205 | /** 206 | * Test for delete_data_for_all_users_in_context(). 207 | */ 208 | public function test_delete_data_for_all_users_in_context() { 209 | provider::delete_data_for_all_users_in_context($this->context); 210 | 211 | foreach($this->allstudents as $u) { 212 | $this->assert_appointment_status($this->schedulerid, $u, false); 213 | } 214 | } 215 | 216 | /** 217 | * Test for delete_data_for_user(). 218 | */ 219 | public function test_delete_data_for_user() { 220 | $appctx = new approved_contextlist($this->student1, 'mod_scheduler', [$this->context->id]); 221 | provider::delete_data_for_user($appctx); 222 | 223 | $this->assert_appointment_status($this->schedulerid, $this->student1->id, false); 224 | $this->assert_appointment_status($this->schedulerid, $this->student2->id, true); 225 | 226 | } 227 | 228 | /** 229 | * Test for delete_data_for_users(). 230 | */ 231 | public function test_delete_data_for_users() { 232 | $component = 'mod_scheduler'; 233 | 234 | $approveduserids = [$this->student1->id, $this->student2->id]; 235 | $approvedlist = new approved_userlist($this->context, $component, $approveduserids); 236 | provider::delete_data_for_users($approvedlist); 237 | 238 | $this->assert_appointment_status($this->schedulerid, $this->student1->id, false); 239 | $this->assert_appointment_status($this->schedulerid, $this->student2->id, false); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /datelist.php: -------------------------------------------------------------------------------- 1 | libdir.'/tablelib.php'); 14 | 15 | $PAGE->set_docs_path('mod/scheduler/datelist'); 16 | 17 | $scope = optional_param('scope', 'activity', PARAM_TEXT); 18 | if (!in_array($scope, array('activity', 'course', 'site'))) { 19 | $scope = 'activity'; 20 | } 21 | $teacherid = optional_param('teacherid', 0, PARAM_INT); 22 | 23 | if ($scope == 'site') { 24 | $scopecontext = context_system::instance(); 25 | } else if ($scope == 'course') { 26 | $scopecontext = context_course::instance($scheduler->courseid); 27 | } else { 28 | $scopecontext = $context; 29 | } 30 | 31 | if (!has_capability('mod/scheduler:seeoverviewoutsideactivity', $context)) { 32 | $scope = 'activity'; 33 | } 34 | if (!has_capability('mod/scheduler:canseeotherteachersbooking', $scopecontext)) { 35 | $teacherid = 0; 36 | } 37 | 38 | $taburl = new moodle_url('/mod/scheduler/view.php', 39 | array('id' => $scheduler->cmid, 'what' => 'datelist', 'scope' => $scope, 'teacherid' => $teacherid)); 40 | $returnurl = new moodle_url('/mod/scheduler/view.php', array('id' => $scheduler->cmid)); 41 | 42 | $PAGE->set_url($taburl); 43 | 44 | echo $output->header(); 45 | 46 | // Print top tabs. 47 | 48 | echo $output->teacherview_tabs($scheduler, $taburl, 'datelist'); 49 | 50 | 51 | // Find active group in case that group mode is in use. 52 | $currentgroupid = 0; 53 | $groupmode = groups_get_activity_groupmode($scheduler->cm); 54 | if ($groupmode) { 55 | $currentgroupid = groups_get_activity_group($scheduler->cm, true); 56 | 57 | echo html_writer::start_div('dropdownmenu'); 58 | groups_print_activity_menu($scheduler->cm, $taburl); 59 | echo html_writer::end_div(); 60 | } 61 | 62 | $scopemenukey = 'scopemenuself'; 63 | if (has_capability('mod/scheduler:canseeotherteachersbooking', $scopecontext)) { 64 | $teachers = $scheduler->get_available_teachers($currentgroupid); 65 | $teachermenu = array(); 66 | foreach ($teachers as $teacher) { 67 | $teachermenu[$teacher->id] = fullname($teacher); 68 | } 69 | $select = $output->single_select($taburl, 'teacherid', $teachermenu, $teacherid, 70 | array(0 => get_string('myself', 'scheduler')), 'teacheridform'); 71 | echo html_writer::div(get_string('teachersmenu', 'scheduler', $select), 'dropdownmenu'); 72 | $scopemenukey = 'scopemenu'; 73 | } 74 | if (has_capability('mod/scheduler:seeoverviewoutsideactivity', $context)) { 75 | $scopemenu = array('activity' => get_string('thisscheduler', 'scheduler'), 76 | 'course' => get_string('thiscourse', 'scheduler'), 77 | 'site' => get_string('thissite', 'scheduler')); 78 | $select = $output->single_select($taburl, 'scope', $scopemenu, $scope, null, 'scopeform'); 79 | echo html_writer::div(get_string($scopemenukey, 'scheduler', $select), 'dropdownmenu'); 80 | } 81 | 82 | // Getting date list. 83 | 84 | $params = array(); 85 | $params['teacherid'] = $teacherid == 0 ? $USER->id : $teacherid; 86 | $params['courseid'] = $scheduler->courseid; 87 | $params['schedulerid'] = $scheduler->id; 88 | 89 | $scopecond = ''; 90 | if ($scope == 'activity') { 91 | $scopecond = ' AND sc.id = :schedulerid'; 92 | } else if ($scope == 'course') { 93 | $scopecond = ' AND c.id = :courseid'; 94 | } 95 | 96 | $sql = "SELECT a.id AS id, ". 97 | user_picture::fields('u1', array('email', 'department'), 'studentid', 'student').", ". 98 | $DB->sql_fullname('u1.firstname', 'u1.lastname')." AS studentfullname, 99 | a.appointmentnote, 100 | a.appointmentnoteformat, 101 | a.teachernote, 102 | a.teachernoteformat, 103 | a.grade, 104 | sc.name, 105 | sc.id AS schedulerid, 106 | sc.scale, 107 | c.shortname AS courseshort, 108 | c.id AS courseid, ". 109 | user_picture::fields('u2', null, 'teacherid').", 110 | s.id AS sid, 111 | s.starttime, 112 | s.duration, 113 | s.appointmentlocation, 114 | s.notes, 115 | s.notesformat 116 | FROM {course} c, 117 | {scheduler} sc, 118 | {scheduler_appointment} a, 119 | {scheduler_slots} s, 120 | {user} u1, 121 | {user} u2 122 | WHERE c.id = sc.course AND 123 | sc.id = s.schedulerid AND 124 | a.slotid = s.id AND 125 | u1.id = a.studentid AND 126 | u2.id = s.teacherid AND 127 | s.teacherid = :teacherid ". 128 | $scopecond; 129 | 130 | $sqlcount = 131 | "SELECT COUNT(*) 132 | FROM {course} c, 133 | {scheduler} sc, 134 | {scheduler_appointment} a, 135 | {scheduler_slots} s 136 | WHERE c.id = sc.course AND 137 | sc.id = s.schedulerid AND 138 | a.slotid = s.id AND 139 | s.teacherid = :teacherid ". 140 | $scopecond; 141 | 142 | $numrecords = $DB->count_records_sql($sqlcount, $params); 143 | 144 | 145 | $limit = 30; 146 | 147 | if ($numrecords) { 148 | 149 | // Make the table of results. 150 | 151 | $coursestr = get_string('course', 'scheduler'); 152 | $schedulerstr = get_string('scheduler', 'scheduler'); 153 | $whenstr = get_string('when', 'scheduler'); 154 | $wherestr = get_string('where', 'scheduler'); 155 | $whostr = get_string('who', 'scheduler'); 156 | $wherefromstr = get_string('department', 'scheduler'); 157 | $whatstr = get_string('what', 'scheduler'); 158 | $whatresultedstr = get_string('whatresulted', 'scheduler'); 159 | $whathappenedstr = get_string('whathappened', 'scheduler'); 160 | 161 | $tablecolumns = array('courseshort', 'schedulerid', 'starttime', 'appointmentlocation', 162 | 'studentfullname', 'studentdepartment', 'notes', 'grade', 'appointmentnote'); 163 | $tableheaders = array($coursestr, $schedulerstr, $whenstr, $wherestr, 164 | $whostr, $wherefromstr, $whatstr, $whatresultedstr, $whathappenedstr); 165 | 166 | $table = new flexible_table('mod-scheduler-datelist'); 167 | $table->define_columns($tablecolumns); 168 | $table->define_headers($tableheaders); 169 | 170 | $table->define_baseurl($taburl); 171 | 172 | $table->sortable(true, 'when'); // Sorted by date by default. 173 | $table->collapsible(true); // Allow column hiding. 174 | $table->initialbars(true); 175 | 176 | $table->column_suppress('courseshort'); 177 | $table->column_suppress('schedulerid'); 178 | $table->column_suppress('starttime'); 179 | $table->column_suppress('studentfullname'); 180 | $table->column_suppress('notes'); 181 | 182 | $table->set_attribute('id', 'dates'); 183 | $table->set_attribute('class', 'datelist'); 184 | 185 | $table->column_class('course', 'datelist_course'); 186 | $table->column_class('scheduler', 'datelist_scheduler'); 187 | 188 | $table->setup(); 189 | 190 | // Get extra query parameters from flexible_table behaviour. 191 | $where = $table->get_sql_where(); 192 | $sort = $table->get_sql_sort(); 193 | $table->pagesize($limit, $numrecords); 194 | 195 | if (!empty($sort)) { 196 | $sql .= " ORDER BY $sort"; 197 | } 198 | 199 | $results = $DB->get_records_sql($sql, $params); 200 | 201 | foreach ($results as $id => $row) { 202 | $courseurl = new moodle_url('/course/view.php', array('id' => $row->courseid)); 203 | $coursedata = html_writer::link($courseurl, format_string($row->courseshort)); 204 | $schedulerurl = new moodle_url('/mod/scheduler/view.php', array('a' => $row->schedulerid)); 205 | $schedulerdata = html_writer::link($schedulerurl, format_string($row->name)); 206 | $a = mod_scheduler_renderer::slotdatetime($row->starttime, $row->duration); 207 | $whendata = get_string('slotdatetime', 'scheduler', $a); 208 | $whourl = new moodle_url('/mod/scheduler/view.php', 209 | array('what' => 'viewstudent', 'a' => $row->schedulerid, 'appointmentid' => $row->id)); 210 | $whodata = html_writer::link($whourl, $row->studentfullname); 211 | $whatdata = $output->format_notes($row->notes, $row->notesformat, $context, 'slotnote', $row->sid); 212 | $gradedata = $row->scale == 0 ? '' : $output->format_grade($row->scale, $row->grade); 213 | 214 | $dataset = array( 215 | $coursedata, 216 | $schedulerdata, 217 | $whendata, 218 | format_string($row->appointmentlocation), 219 | $whodata, 220 | $row->studentdepartment, 221 | $whatdata, 222 | $gradedata, 223 | $output->format_appointment_notes($scheduler, $row) ); 224 | $table->add_data($dataset); 225 | } 226 | $table->print_html(); 227 | echo $output->continue_button($returnurl); 228 | } else { 229 | notice(get_string('noresults', 'scheduler'), $returnurl); 230 | } 231 | 232 | echo $output->footer(); -------------------------------------------------------------------------------- /mailtemplatelib.php: -------------------------------------------------------------------------------- 1 | id) and $course->id != SITEID and !empty($course->lang)) { 32 | // Course language overrides user language. 33 | $return = $course->lang; 34 | } else if (!empty($user->lang)) { 35 | $return = $user->lang; 36 | } else if (isset ($CFG->lang)) { 37 | $return = $CFG->lang; 38 | } else { 39 | $return = 'en'; 40 | } 41 | 42 | return $return; 43 | } 44 | 45 | /** 46 | * Gets the content of an e-mail from language strings. 47 | * 48 | * Looks for the language string email_$template_$format and replaces the parameter values. 49 | * 50 | * @param string $template the template's identified 51 | * @param string $format the mail format ('subject', 'html' or 'plain') 52 | * @param array $parameters an array ontaining pairs of parm => data to replace in template 53 | * @param string $module module to use language strings from 54 | * @param string $lang language to use 55 | * @return a fully resolved template where all data has been injected 56 | * 57 | */ 58 | public static function compile_mail_template($template, $format, $parameters, $module = 'scheduler', $lang = null) { 59 | $params = array (); 60 | foreach ($parameters as $key => $value) { 61 | $params [strtolower($key)] = $value; 62 | } 63 | $mailstr = get_string_manager()->get_string("email_{$template}_{$format}", $module, $params, $lang); 64 | return $mailstr; 65 | } 66 | 67 | /** 68 | * Sends a message based on a template. 69 | * Several template substitution values are automatically filled by this routine. 70 | * 71 | * @uses $CFG 72 | * @uses $SITE 73 | * @param string $modulename 74 | * name of the module that sends the message 75 | * @param string $messagename 76 | * name of the message in messages.php 77 | * @param int $isnotification 78 | * 1 for notifications, 0 for personal messages 79 | * @param user $sender 80 | * A {@link $USER} object describing the sender 81 | * @param user $recipient 82 | * A {@link $USER} object describing the recipient 83 | * @param object $course 84 | * The course that the activity is in. Can be null. 85 | * @param string $template 86 | * the mail template name as in language config file (without "_html" part) 87 | * @param array $parameters 88 | * a hash containing pairs of parm => data to replace in template 89 | * @return bool|int Returns message id if message was sent OK, "false" if there was another sort of error. 90 | */ 91 | public static function send_message_from_template($modulename, $messagename, $isnotification, 92 | stdClass $sender, stdClass $recipient, $course, 93 | $template, array $parameters) { 94 | global $CFG; 95 | global $SITE; 96 | 97 | $lang = self::get_message_language($recipient, $course); 98 | 99 | $defaultvars = array ( 100 | 'SITE' => $SITE->fullname, 101 | 'SITE_SHORT' => $SITE->shortname, 102 | 'SITE_URL' => $CFG->wwwroot, 103 | 'SENDER' => fullname ( $sender ), 104 | 'RECIPIENT' => fullname ( $recipient ) 105 | ); 106 | 107 | if ($course) { 108 | $defaultvars['COURSE_SHORT'] = format_string($course->shortname); 109 | $defaultvars['COURSE'] = format_string($course->fullname); 110 | $defaultvars['COURSE_URL'] = $CFG->wwwroot . '/course/view.php?id=' . $course->id; 111 | } 112 | 113 | $vars = array_merge($defaultvars, $parameters); 114 | 115 | $message = new \core\message\message(); 116 | $message->component = $modulename; 117 | $message->name = $messagename; 118 | $message->userfrom = $sender; 119 | $message->userto = $recipient; 120 | $message->subject = self::compile_mail_template($template, 'subject', $vars, $modulename, $lang); 121 | $message->fullmessage = self::compile_mail_template($template, 'plain', $vars, $modulename, $lang); 122 | $message->fullmessageformat = FORMAT_PLAIN; 123 | $message->fullmessagehtml = self::compile_mail_template ( $template, 'html', $vars, $modulename, $lang ); 124 | $message->notification = '1'; 125 | $message->courseid = $course->id; 126 | $message->contexturl = $defaultvars['COURSE_URL']; 127 | $message->contexturlname = $course->fullname; 128 | 129 | $msgid = message_send($message); 130 | return $msgid; 131 | } 132 | 133 | /** 134 | * Construct an array with subtitution rules for mail templates, relating to 135 | * a single appointment. Any of the parameters can be null. 136 | * 137 | * @param scheduler_instance $scheduler The scheduler instance 138 | * @param scheduler_slot $slot The slot data as an MVC object, may be null 139 | * @param user $teacher A {@link $USER} object describing the attendant (teacher) 140 | * @param user $student A {@link $USER} object describing the attendee (student) 141 | * @param object $course A course object relating to the ontext of the message 142 | * @param object $recipient A {@link $USER} object describing the recipient of the message 143 | * (used for determining the message language) 144 | * @return array A hash with mail template substitutions 145 | */ 146 | public static function get_scheduler_variables(scheduler_instance $scheduler, $slot, 147 | $teacher, $student, $course, $recipient) { 148 | 149 | global $CFG; 150 | 151 | $lang = self::get_message_language($recipient, $course); 152 | // Force any string formatting to happen in the target language. 153 | $oldlang = force_current_language($lang); 154 | 155 | $tz = core_date::get_user_timezone($recipient); 156 | 157 | $vars = array(); 158 | 159 | if ($scheduler) { 160 | $vars['MODULE'] = format_string($scheduler->name); 161 | $vars['STAFFROLE'] = $scheduler->get_teacher_name(); 162 | $vars['SCHEDULER_URL'] = $CFG->wwwroot.'/mod/scheduler/view.php?id='.$scheduler->cmid; 163 | } 164 | if ($slot) { 165 | $vars ['DATE'] = userdate($slot->starttime, get_string('strftimedate'), $tz); 166 | $vars ['TIME'] = userdate($slot->starttime, get_string('strftimetime'), $tz); 167 | $vars ['ENDTIME'] = userdate($slot->endtime, get_string('strftimetime'), $tz); 168 | $vars ['LOCATION'] = format_string($slot->appointmentlocation); 169 | } 170 | if ($teacher) { 171 | $vars['ATTENDANT'] = fullname($teacher); 172 | $vars['ATTENDANT_URL'] = $CFG->wwwroot.'/user/view.php?id='.$teacher->id.'&course='.$scheduler->course; 173 | } 174 | if ($student) { 175 | $vars['ATTENDEE'] = fullname($student); 176 | $vars['ATTENDEE_URL'] = $CFG->wwwroot.'/user/view.php?id='.$student->id.'&course='.$scheduler->course; 177 | } 178 | 179 | // Reset language settings. 180 | force_current_language($oldlang); 181 | 182 | return $vars; 183 | 184 | } 185 | 186 | 187 | /** 188 | * Send a notification message about a scheduler slot. 189 | * 190 | * @param scheduler_slot $slot the slot that the notification relates to 191 | * @param string $messagename name of message as in db/message.php 192 | * @param string $template template name to use (language string up to prefix/postfix) 193 | * @param stdClass $sender user record for sender 194 | * @param stdClass $recipient user record for recipient 195 | * @param stdClass $teacher user record for teacher 196 | * @param stdClass $student user record for student 197 | * @param stdClass $course course record 198 | */ 199 | public static function send_slot_notification(scheduler_slot $slot, $messagename, $template, 200 | stdClass $sender, stdClass $recipient, 201 | stdClass $teacher, stdClass $student, stdClass $course) { 202 | $vars = self::get_scheduler_variables($slot->get_scheduler(), $slot, $teacher, $student, $course, $recipient); 203 | self::send_message_from_template('mod_scheduler', $messagename, 1, $sender, $recipient, $course, $template, $vars); 204 | } 205 | 206 | } --------------------------------------------------------------------------------