From 16c883c71167fab87fc2f7426a06248c097924f8 Mon Sep 17 00:00:00 2001 From: Davo Smith Date: Thu, 25 Jun 2015 10:56:53 +0100 Subject: [PATCH] Allow temporary users to be created and have their attendance taken --- db/access.php | 15 +++- db/install.xml | 17 ++++ db/upgrade.php | 36 ++++++++ lang/en/attendance.php | 55 +++++++++++++ locallib.php | 113 ++++++++++++++++++++++++- pix/ghost.png | Bin 0 -> 48062 bytes renderables.php | 5 ++ renderer.php | 33 ++++++-- temp_form.php | 68 +++++++++++++++ tempedit.php | 115 ++++++++++++++++++++++++++ tempedit_form.php | 71 ++++++++++++++++ tempmerge.php | 104 +++++++++++++++++++++++ tempmerge_form.php | 40 +++++++++ tempusers.php | 127 +++++++++++++++++++++++++++++ tests/behat/extra_features.feature | 112 +++++++++++++++++++++++++ version.php | 2 +- 16 files changed, 903 insertions(+), 10 deletions(-) create mode 100644 pix/ghost.png create mode 100644 temp_form.php create mode 100644 tempedit.php create mode 100644 tempedit_form.php create mode 100644 tempmerge.php create mode 100644 tempmerge_form.php create mode 100644 tempusers.php create mode 100644 tests/behat/extra_features.feature diff --git a/db/access.php b/db/access.php index 90ae65a..b7580a7 100644 --- a/db/access.php +++ b/db/access.php @@ -117,5 +117,18 @@ $capabilities = array( 'archetypes' => array( 'student' => CAP_ALLOW ) - ) + ), + + // Allow teachers to manage temporary users. + 'mod/attendance:managetemporaryusers' => array( + 'riskbitmask' => RISK_DATALOSS, + + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), ); diff --git a/db/install.xml b/db/install.xml index ca4e6b0..65e43d5 100644 --- a/db/install.xml +++ b/db/install.xml @@ -80,5 +80,22 @@ + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/db/upgrade.php b/db/upgrade.php index 12a0b95..8ec0802 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -85,5 +85,41 @@ function xmldb_attendance_upgrade($oldversion=0) { upgrade_plugin_savepoint($result, 2014112001, 'mod', 'attendance'); } + if ($oldversion < 2015040501) { + // Define table attendance_tempusers to be created. + $table = new xmldb_table('attendance_tempusers'); + + // Adding fields to table attendance_tempusers. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('studentid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('fullname', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('email', XMLDB_TYPE_CHAR, '100', null, null, null, null); + $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + + // Adding keys to table attendance_tempusers. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for attendance_tempusers. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Conditionally launch add index courseid. + $index = new xmldb_index('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Conditionally launch add index studentid. + $index = new xmldb_index('studentid', XMLDB_INDEX_UNIQUE, array('studentid')); + if (!$dbman->index_exists($table, $index)) { + $dbman->add_index($table, $index); + } + + // Attendance savepoint reached. + upgrade_mod_savepoint(true, 2015040501, 'attendance'); + } + return $result; } diff --git a/lang/en/attendance.php b/lang/en/attendance.php index 3dcb964..5ac7897 100644 --- a/lang/en/attendance.php +++ b/lang/en/attendance.php @@ -24,6 +24,7 @@ $string['attendance:addinstance'] = 'Add a new attendance activity'; $string['Aacronym'] = 'A'; +$string['adduser'] = 'Add user'; $string['Afull'] = 'Absent'; $string['Eacronym'] = 'E'; $string['Efull'] = 'Excused'; @@ -51,6 +52,7 @@ $string['attendance:changepreferences'] = 'Changing Preferences'; $string['attendance:changeattendances'] = 'Changing Attendances'; $string['attendance:export'] = 'Export Reports'; $string['attendance:manageattendances'] = 'Manage Attendances'; +$string['attendance:managetemporaryusers'] = 'Manage temporary users'; $string['attendance:takeattendances'] = 'Taking Attendances'; $string['attendance:view'] = 'Viewing Attendances'; $string['attendance:viewreports'] = 'Viewing Reports'; @@ -69,6 +71,7 @@ $string['column'] = 'column'; $string['columns'] = 'columns'; $string['commonsession'] = 'Common'; $string['commonsessions'] = 'Common'; +$string['confirmdeleteuser'] = 'Are you sure you want to delete user \'{$a->fullname}\' ({$a->email})?
All of their attendance records will be permanently deleted.'; $string['countofselected'] = 'Count of selected'; $string['copyfrom'] = 'Copy attendance data from'; $string['createmultiplesessions'] = 'Create multiple sessions'; @@ -88,6 +91,7 @@ $string['deletelogs'] = 'Delete attendance data'; $string['deleteselected'] = 'Delete selected'; $string['deletesession'] = 'Delete session'; $string['deletesessions'] = 'Delete all sessions'; +$string['deleteuser'] = 'Delete user'; $string['deletingsession'] = 'Deleting session for the course'; $string['deletingstatus'] = 'Deleting status for the course'; $string['description'] = 'Description'; @@ -99,6 +103,7 @@ $string['downloadtext'] = 'Download in text format'; $string['donotusepaging'] = 'Do not use paging'; $string['duration'] = 'Duration'; $string['editsession'] = 'Edit Session'; +$string['edituser'] = 'Edit user'; $string['endtime'] = 'Session end time'; $string['endofperiod'] = 'End of period'; $string['enrolmentend'] = 'User enrolment ends {$a}'; @@ -125,6 +130,7 @@ $string['indetail'] = 'In detail...'; $string['invalidsessionenddate'] = 'The session end date can not be earlier than the session start date'; $string['invalidaction'] = 'You must select an action'; $string['jumpto'] = 'Jump to'; +$string['mergeuser'] = 'Merge user'; $string['modulename'] = 'Attendance'; $string['modulename_help'] = 'The attendance activity module enables a teacher to take attendance during class and students to view their own attendance record. @@ -151,6 +157,7 @@ $string['nosessionsselected'] = 'No sessions selected'; $string['notfound'] = 'Attendance activity not found in this course!'; $string['noupgradefromthisversion'] = 'The Attendance module cannot upgrade from the version of attforblock you have installed. - please delete attforblock or upgrade it to the latest version before isntalling the new attendance module'; $string['olddate'] = 'Old date'; +$string['participant'] = 'Participant'; $string['period'] = 'Frequency'; $string['pluginname'] = 'Attendance'; $string['pluginadministration'] = 'Attendance administration'; @@ -158,6 +165,41 @@ $string['remark'] = 'Remark for: {$a}'; $string['remarks'] = 'Remarks'; $string['report'] = 'Report'; $string['required'] = 'Required*'; +$string['requiredentries'] = ' Temporary records overwrite participant attendance records'; +$string['requiredentry'] = ' Temporary user merge help guide'; +$string['requiredentry_help'] = '

Attendance

+

Merge Accounts

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Moodle UserTemporary UserAction
Attendance dataAttendance dataTemporary user will override Moodle user
No attendance dataAttendance dataTemporary user attendance will be transfered to Moodle user
Attendance dataNo attendance dataTemporary user will be deleted
No attendance dataNo attendance dataTemporary user will be deleted
+ +

+

Temporay user will be deleted in all cases after merge action

'; $string['resetdescription'] = 'Remember that deleting attendance data will erase information from database. You can just hide older sessions having changed start date of course!'; $string['resetstatuses'] = 'Reset statuses to default'; $string['restoredefaults'] = 'Restore defaults'; @@ -206,9 +248,22 @@ $string['strftimehm'] = '%H:%M'; // Line added to allow display of time. $string['strftimeshortdate'] = '%d.%m.%Y'; $string['studentid'] = 'Student ID'; $string['takeattendance'] = 'Take attendance'; +$string['tempaddform'] = 'Add temporary user'; +$string['tempexists'] = 'There is already a temporary user with this email address'; +$string['tempusers'] = 'Temporary users'; $string['thiscourse'] = 'This course'; $string['tablerenamefailed'] = 'Rename of old attforblock table to attendance failed'; +$string['tactions'] = 'Action'; +$string['tcreated'] = 'Created'; +$string['temptable'] = 'List of temporary users'; +$string['tempuser'] = 'Temporary user'; +$string['tempusersedit'] = 'Edit temporary user'; +$string['tempuserslist'] = 'Temporary users'; +$string['tempusermerge'] = 'Merge temporary user'; +$string['tuseremail'] = 'Email'; +$string['tusername'] = 'Full name'; $string['update'] = 'Update'; +$string['userexists'] = 'There is already a real user with this email address'; $string['variable'] = 'variable'; $string['variablesupdated'] = 'Variables successfully updated'; $string['versionforprinting'] = 'version for printing'; diff --git a/locallib.php b/locallib.php index f003c73..09c8b89 100644 --- a/locallib.php +++ b/locallib.php @@ -44,6 +44,7 @@ class attendance_permissions { private $cantake; private $canchange; private $canmanage; + private $canmanagetemp; // Can manage temporary users. private $canchangepreferences; private $canexport; private $canbelisted; @@ -122,6 +123,18 @@ class attendance_permissions { public function require_manage_capability() { require_capability('mod/attendance:manageattendances', $this->context); } + + // Check to see if the user can manage temporary users. + public function can_managetemp() { + if (is_null($this->canmanagetemp)) { + $this->canmanagetemp = has_capability('mod/attendance:managetemporaryusers', $this->context); + } + return $this->canmanagetemp; + } + + public function require_managetemp_capability() { + require_capability('mod/attendance:managetemporaryusers', $this->context); + } public function can_change_preferences() { if (is_null($this->canchangepreferences)) { @@ -588,7 +601,7 @@ class attendance { $this->cm = $cm; $this->course = $course; if (is_null($context)) { - $this->context = context_module::instance_by_id($this->cm->id); + $this->context = context_module::instance($this->cm->id); } else { $this->context = $context; } @@ -738,6 +751,42 @@ class attendance { return new moodle_url('/mod/attendance/manage.php', $params); } + /** + * @param array $params optional + * @return moodle_url of tempusers.php for attendance instance + */ + public function url_managetemp($params=array()) { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempusers.php', $params); + } + + /** + * @param array $params optional + * @return moodle_url of tempdelete.php for attendance instance + */ + public function url_tempdelete($params=array()) { + $params = array_merge(array('id' => $this->cm->id, 'action' => 'delete'), $params); + return new moodle_url('/mod/attendance/tempedit.php', $params); + } + + /** + * @param array $params optional + * @return moodle_url of tempedit.php for attendance instance + */ + public function url_tempedit($params=array()) { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempedit.php', $params); + } + + /** + * @param array $params optional + * @return moodle_url of tempedit.php for attendance instance + */ + public function url_tempmerge($params=array()) { + $params = array_merge(array('id' => $this->cm->id), $params); + return new moodle_url('/mod/attendance/tempmerge.php', $params); + } + /** * @return moodle_url of sessions.php for attendance instance */ @@ -1058,17 +1107,53 @@ class attendance { $users[$user->id]->enrolmentstatus = $enrolments[$user->id]->status; $users[$user->id]->enrolmentstart = $enrolments[$user->id]->mintime; $users[$user->id]->enrolmentend = $enrolments[$user->id]->maxtime; + $users[$user->id]->type = 'standard'; // Mark as a standard (not a temporary) user. } } + // Add the 'temporary' users to this list. + $tempusers = $DB->get_records('attendance_tempusers', array('courseid' => $this->course->id)); + foreach ($tempusers as $tempuser) { + $users[] = self::tempuser_to_user($tempuser); + } + return $users; } + // Convert a tempuser record into a user object. + protected static function tempuser_to_user($tempuser) { + $ret = (object)array( + 'id' => $tempuser->studentid, + 'firstname' => $tempuser->fullname, + 'email' => $tempuser->email, + 'username' => '', + 'enrolmentstatus' => 0, + 'enrolmentstart' => 0, + 'enrolmentend' => 0, + 'picture' => 0, + 'type' => 'temporary', + ); + foreach (get_all_user_name_fields() as $namefield) { + if (!isset($ret->$namefield)) { + $ret->$namefield = ''; + } + } + return $ret; + } + public function get_user($userid) { global $DB; $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); + // Look for 'temporary' users and return their details from the attendance_tempusers table. + if ($user->idnumber == 'tempghost') { + $tempuser = $DB->get_record('attendance_tempusers', array('studentid' => $userid), '*', MUST_EXIST); + return self::tempuser_to_user($tempuser); + } + + $user->type = 'standard'; + // CONTRIB-4868 $mintime = 'MIN(CASE WHEN (ue.timestart > :zerotime) THEN ue.timestart ELSE ue.timecreated END)'; $maxtime = 'MAX(ue.timeend)'; @@ -1516,8 +1601,34 @@ class attendance { $event->trigger(); } + /** + * Check if the email address is already in use by either another temporary user, + * or a real user. + * + * @param string $email the address to check for + * @param int $tempuserid optional the ID of the temporary user (to avoid matching against themself) + * @return null|string the error message to display, null if there is no error + */ + public static function check_existing_email($email, $tempuserid = 0) { + global $DB; + + if (empty($email)) { + return null; // Fine to create temporary users without an email address. + } + if ($tempuser = $DB->get_record('attendance_tempusers', array('email' => $email), 'id')) { + if ($tempuser->id != $tempuserid) { + return get_string('tempexists', 'attendance'); + } + } + if ($DB->record_exists('user', array('email' => $email))) { + return get_string('userexists', 'attendance'); + } + + return null; + } } + function att_get_statuses($attid, $onlyvisible=true) { global $DB; diff --git a/pix/ghost.png b/pix/ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..b199efa48aee951d390b92d90730c748d6d893f3 GIT binary patch literal 48062 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 Z{~trw!<*0$T?YUF002ovPDHLkV1l(d-0}bb literal 0 HcmV?d00001 diff --git a/renderables.php b/renderables.php index 5dd2223..ac9b37e 100644 --- a/renderables.php +++ b/renderables.php @@ -42,6 +42,7 @@ class attendance_tabs implements renderable { const TAB_REPORT = 3; const TAB_EXPORT = 4; const TAB_PREFERENCES = 5; + const TAB_TEMPORARYUSERS = 6; // Tab for managing temporary users. public $currenttab; @@ -92,6 +93,10 @@ class attendance_tabs implements renderable { $toprow[] = new tabobject(self::TAB_PREFERENCES, $this->att->url_preferences()->out(), get_string('settings', 'attendance')); } + if ($this->att->perm->can_managetemp()) { + $toprow[] = new tabobject(self::TAB_TEMPORARYUSERS, $this->att->url_managetemp()->out(), + get_string('tempusers', 'attendance')); + } return array($toprow); } diff --git a/renderer.php b/renderer.php index ac4a378..46b4617 100644 --- a/renderer.php +++ b/renderer.php @@ -495,7 +495,7 @@ class mod_attendance_renderer extends plugin_renderer_base { $row = new html_table_row(); $row->cells[] = $i; $fullname = html_writer::link($takedata->url_view(array('studentid' => $user->id)), fullname($user)); - $fullname = $this->output->user_picture($user).$fullname; + $fullname = $this->user_picture($user).$fullname; // Show different picture if it is a temporary user. $ucdata = $this->construct_take_user_controls($takedata, $user); if (array_key_exists('warning', $ucdata)) { @@ -540,7 +540,7 @@ class mod_attendance_renderer extends plugin_renderer_base { $i = 0; $row = new html_table_row(); foreach ($takedata->users as $user) { - $celltext = $this->output->user_picture($user, array('size' => 100)); + $celltext = $this->user_picture($user, array('size' => 100)); // Show different picture if it is a temporary user. $celltext .= html_writer::empty_tag('br'); $fullname = html_writer::link($takedata->url_view(array('studentid' => $user->id)), fullname($user)); $celltext .= html_writer::tag('span', $fullname, array('class' => 'fullname')); @@ -656,7 +656,7 @@ class mod_attendance_renderer extends plugin_renderer_base { $table->attributes['class'] = 'userinfobox'; $table->colclasses = array('left side', ''); - $table->data[0][] = $this->output->user_picture($userdata->user, array('size' => 100)); + $table->data[0][] = $this->user_picture($userdata->user, array('size' => 100)); // Show different picture if it is a temporary user. $table->data[0][] = $this->construct_user_data($userdata); $o .= html_writer::table($table); @@ -671,9 +671,12 @@ class mod_attendance_renderer extends plugin_renderer_base { $userdata->url()->out(true, array('mode' => att_view_page_params::MODE_THIS_COURSE)), get_string('thiscourse', 'attendance')); - $tabs[] = new tabobject(att_view_page_params::MODE_ALL_COURSES, - $userdata->url()->out(true, array('mode' => att_view_page_params::MODE_ALL_COURSES)), - get_string('allcourses', 'attendance')); + // Skip the 'all courses' tab for 'temporary' users. + if ($userdata->user->type == 'standard') { + $tabs[] = new tabobject(att_view_page_params::MODE_ALL_COURSES, + $userdata->url()->out(true, array('mode' => att_view_page_params::MODE_ALL_COURSES)), + get_string('allcourses', 'attendance')); + } return print_tabs(array($tabs), $userdata->pageparams->mode, null, null, true); } @@ -842,7 +845,7 @@ class mod_attendance_renderer extends plugin_renderer_base { foreach ($reportdata->users as $user) { $row = new html_table_row(); - $row->cells[] = $this->output->user_picture($user); + $row->cells[] = $this->user_picture($user); // Show different picture if it is a temporary user. $row->cells[] = html_writer::link($reportdata->url_view(array('studentid' => $user->id)), fullname($user)); $cellsgenerator = new user_sessions_cells_html_generator($reportdata, $user); $row->cells = array_merge($row->cells, $cellsgenerator->get_cells()); @@ -1006,4 +1009,20 @@ class mod_attendance_renderer extends plugin_renderer_base { return html_writer::empty_tag('input', $attributes); } + // Show different picture if it is a temporary user. + protected function user_picture($user, array $opts = null) { + if ($user->type == 'temporary') { + $attrib = array( + 'width' => '35', + 'height' => '35', + 'class' => 'userpicture defaultuserpic', + ); + if (isset($opts['size'])) { + $attrib['width'] = $attrib['height'] = $opts['size']; + } + return $this->output->pix_icon('ghost', '', 'mod_attendance', $attrib); + } + + return $this->output->user_picture($user, $opts); + } } diff --git a/temp_form.php b/temp_form.php new file mode 100644 index 0000000..1fd5eec --- /dev/null +++ b/temp_form.php @@ -0,0 +1,68 @@ +. + +/** + * Form for creating temporary users. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir.'/formslib.php'); + +class temp_form extends moodleform { + function definition() { + + $mform = $this->_form; + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempaddform', 'attendance')); + $mform->addElement('text', 'tname', get_string('tusername', 'attendance')); + $mform->addRule('tname', 'Required', 'required', null, 'client'); + $mform->setType('tname', PARAM_TEXT); + + $mform->addElement('text', 'temail', get_string('tuseremail', 'attendance')); + $mform->addRule('temail', 'Email', 'email', null, 'client'); + $mform->addRule('temail', '', 'callback', null, 'server'); + $mform->setType('temail', PARAM_EMAIL); + + $mform->addElement('submit', 'submitbutton', get_string('adduser', 'attendance')); + $mform->closeHeaderBefore('submit'); + } + + function definition_after_data() { + $mform = $this->_form; + $mform->applyFilter('tname', 'trim'); + } + + function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($err = attendance::check_existing_email($data['temail'])) { + $errors['temail'] = $err; + } + + return $errors; + } + +} + diff --git a/tempedit.php b/tempedit.php new file mode 100644 index 0000000..15349df --- /dev/null +++ b/tempedit.php @@ -0,0 +1,115 @@ +. + +/** + * Attendance tempedit + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); + +global $CFG, $DB, $PAGE, $OUTPUT; +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); +require_once($CFG->dirroot.'/mod/attendance/tempedit_form.php'); + +$id = required_param('id', PARAM_INT); +$userid = required_param('userid', PARAM_INT); +$action = optional_param('action', null, PARAM_ALPHA); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); +$tempuser = $DB->get_record('attendance_tempusers', array('id' => $userid), '*', MUST_EXIST); + +$att = new attendance($att, $cm, $course); + +$params = array('userid' => $tempuser->id); +if ($action) { + $params['action'] = $action; +} +$PAGE->set_url($att->url_tempedit($params)); + +require_login($course, true, $cm); + +$att->perm->require_managetemp_capability(); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusersedit', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('tempusersedit', 'attendance')); + +/** @var mod_attendance_renderer $output */ +$output = $PAGE->get_renderer('mod_attendance'); + +if ($action == 'delete') { + if (optional_param('confirm', false, PARAM_BOOL)) { + require_sesskey(); + + // Remove the user from the grades table, the attendance log and the tempusers table. + $DB->delete_records('grade_grades', array('userid' => $tempuser->studentid)); + $DB->delete_records('attendance_log', array('studentid' => $tempuser->studentid)); + $DB->delete_records('attendance_tempusers', array('id' => $tempuser->id)); + + redirect($att->url_managetemp()); + } else { + + $info = (object)array( + 'fullname' => $tempuser->fullname, + 'email' => $tempuser->email, + ); + $msg = get_string('confirmdeleteuser', 'attendance', $info); + $continue = new moodle_url($PAGE->url, array('confirm' => 1, 'sesskey' => sesskey())); + + echo $output->header(); + echo $output->confirm($msg, $continue, $att->url_managetemp()); + echo $output->footer(); + + die(); + } +} + +$formdata = new stdClass(); +$formdata->id = $cm->id; +$formdata->tname = $tempuser->fullname; +$formdata->userid = $tempuser->id; +$formdata->temail = $tempuser->email; + +$mform = new tempedit_form(); +$mform->set_data($formdata); + +if ($mform->is_cancelled()) { + redirect($att->url_managetemp()); +} else if ($tempuser = $mform->get_data()) { + global $DB; + $updateuser = new stdClass(); + $updateuser->id = $tempuser->userid; + $updateuser->fullname = $tempuser->tname; + $updateuser->email = $tempuser->temail; + $DB->update_record('attendance_tempusers', $updateuser); + redirect($att->url_managetemp()); +} + +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +echo $output->header(); +echo $output->heading(get_string('tempusersedit', 'attendance').' : '.$course->fullname); +echo $output->render($tabs); +$mform->display(); +echo $output->footer($course); + diff --git a/tempedit_form.php b/tempedit_form.php new file mode 100644 index 0000000..362b196 --- /dev/null +++ b/tempedit_form.php @@ -0,0 +1,71 @@ +. + +/** + * Form for editing temporary users. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir.'/formslib.php'); + +class tempedit_form extends moodleform { + + function definition() { + + $mform = $this->_form; + + $mform->addElement('hidden', 'userid', 0); + $mform->setType('userid', PARAM_INT); + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempusersedit', 'attendance')); + $mform->addElement('text', 'tname', get_string('tusername', 'attendance')); + $mform->addRule('tname', 'Required', 'required', null, 'client'); + $mform->setType('tname', PARAM_TEXT); + + $mform->addElement('text', 'temail', get_string('tuseremail', 'attendance')); + $mform->addRule('temail', 'Email', 'email', null, 'client'); + $mform->setType('temail', PARAM_EMAIL); + + $buttonarray = array( + $mform->createElement('submit', 'submitbutton', get_string('edituser', 'attendance')), + $mform->createElement('cancel'), + ); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('submit'); + } + + function definition_after_data() { + $mform = $this->_form; + $mform->applyFilter('tname', 'trim'); + } + + function validation($data, $files) { + $errors = parent::validation($data, $files); + + if ($err = attendance::check_existing_email($data['temail'], $data['userid'])) { + $errors['temail'] = $err; + } + return $errors; + } +} diff --git a/tempmerge.php b/tempmerge.php new file mode 100644 index 0000000..216d07a --- /dev/null +++ b/tempmerge.php @@ -0,0 +1,104 @@ +. + +/** + * Merge temporary user with real user. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); + +global $CFG, $DB, $PAGE, $OUTPUT; +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); +require_once($CFG->dirroot.'/mod/attendance/tempmerge_form.php'); + +$id = required_param('id', PARAM_INT); +$userid = required_param('userid', PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); +$tempuser = $DB->get_record('attendance_tempusers', array('id' => $userid), '*', MUST_EXIST); + +$att = new attendance($att, $cm, $course); +$params = array('userid' => $tempuser->id); +$PAGE->set_url($att->url_tempmerge($params)); + +require_login($course, true, $cm); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusermerge', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->set_button($OUTPUT->update_module_button($cm->id, 'attendance')); +$PAGE->navbar->add(get_string('tempusermerge', 'attendance')); + +$formdata = (object)array( + 'id' => $cm->id, + 'userid' => $tempuser->id, +); + +$custom = array( + 'description' => format_string($tempuser->fullname).' ('.format_string($tempuser->email).')', +); +$mform = new tempmerge_form(null, $custom); +$mform->set_data($formdata); + +if ($mform->is_cancelled()) { + redirect($att->url_managetemp()); + +} else if ($data = $mform->get_data()) { + + $sql = "SELECT s.id, lr.id AS reallogid, lt.id AS templogid + FROM {attendance_sessions} s + LEFT JOIN {attendance_log} lr ON lr.sessionid = s.id AND lr.studentid = :realuserid + LEFT JOIN {attendance_log} lt ON lt.sessionid = s.id AND lt.studentid = :tempuserid + WHERE s.attendanceid = :attendanceid AND lt.id IS NOT NULL + ORDER BY s.id"; + $params = array( + 'realuserid' => $data->participant, + 'tempuserid' => $tempuser->studentid, + 'attendanceid' => $att->id, + ); + $logs = $DB->get_recordset_sql($sql, $params); + + foreach ($logs as $log) { + if (!is_null($log->reallogid)) { + // Remove the existing attendance for the real user for this session. + $DB->delete_records('attendance_log', array('id' => $log->reallogid)); + } + // Adjust the 'temp user' attendance record to point at the real user. + $DB->set_field('attendance_log', 'studentid', $data->participant, array('id' => $log->templogid)); + } + + // Delete the temp user. + $DB->delete_records('attendance_tempusers', array('id' => $tempuser->id)); + $att->update_users_grade(array($data->participant)); // Update the gradebook after the merge. + + redirect($att->url_managetemp()); +} + +/** @var mod_attendance_renderer $output */ +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +echo $output->header(); +echo $output->heading(get_string('tempusermerge', 'attendance').' : '.$course->fullname); +echo $output->render($tabs); +$mform->display(); +echo $output->footer($course); \ No newline at end of file diff --git a/tempmerge_form.php b/tempmerge_form.php new file mode 100644 index 0000000..5e5de29 --- /dev/null +++ b/tempmerge_form.php @@ -0,0 +1,40 @@ +libdir.'/formslib.php'); + +class tempmerge_form extends moodleform { + + function definition() { + global $COURSE; + + $context = context_course::instance($COURSE->id); + $namefields = get_all_user_name_fields(true, 'u'); + $students = get_enrolled_users($context, 'mod/attendance:canbelisted', 0, 'u.id,'.$namefields.',u.email', + 'u.lastname, u.firstname', 0, 0, true); + $partarray = array(); + foreach ($students as $student) { + $partarray[$student->id] = fullname($student).' ('.$student->email.')'; + } + + $mform = $this->_form; + $description = $this->_customdata['description']; + + $mform->addElement('hidden', 'id', 0); + $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'userid', 0); + $mform->setType('userid', PARAM_INT); + + $mform->addElement('header', 'attheader', get_string('tempusermerge', 'attendance')); + $mform->addElement('static', 'description', get_string('tempuser', 'attendance'), $description); + + $mform->addElement('select', 'participant', get_string('participant', 'attendance'), $partarray); + + $mform->addElement('static', 'requiredentries', '', get_string('requiredentries', 'attendance')); + $mform->addHelpButton('requiredentries', 'requiredentry', 'attendance'); + + $this->add_action_buttons(true, get_string('mergeuser', 'attendance')); + } +} \ No newline at end of file diff --git a/tempusers.php b/tempusers.php new file mode 100644 index 0000000..df1733f --- /dev/null +++ b/tempusers.php @@ -0,0 +1,127 @@ +. + +/** + * Temporary user management. + * + * @package mod_attendance + * @copyright 2013 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../config.php'); +global $CFG, $DB, $OUTPUT, $PAGE, $COURSE; +require_once($CFG->dirroot.'/mod/attendance/locallib.php'); +require_once($CFG->dirroot.'/mod/attendance/temp_form.php'); + +$id = required_param('id', PARAM_INT); + +$cm = get_coursemodule_from_id('attendance', $id, 0, false, MUST_EXIST); +$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); +$att = $DB->get_record('attendance', array('id' => $cm->instance), '*', MUST_EXIST); + +$att = new attendance($att, $cm, $course); +$PAGE->set_url($att->url_managetemp()); + +require_login($course, true, $cm); + +$att->perm->require_managetemp_capability(); + +$PAGE->set_title($course->shortname.": ".$att->name.' - '.get_string('tempusers', 'attendance')); +$PAGE->set_heading($course->fullname); +$PAGE->set_cacheable(true); +$PAGE->navbar->add(get_string('tempusers', 'attendance')); + +/** @var mod_attendance_renderer $output */ +$output = $PAGE->get_renderer('mod_attendance'); +$tabs = new attendance_tabs($att, attendance_tabs::TAB_TEMPORARYUSERS); + +$formdata = (object)array( + 'id' => $cm->id, +); +$mform = new temp_form(); +$mform->set_data($formdata); + +if ($data = $mform->get_data()) { + // Create temp user in main user table. + $user = new stdClass(); + $user->auth = 'manual'; + $user->confirmed = 1; + $user->deleted = 1; + $user->email = time().'@ghost.user.de'; + $user->username = time().'@ghost.user.de'; + $user->idnumber = 'tempghost'; + $studentid = $DB->insert_record('user', $user); + + // Create the temporary user record. + $newtempuser = new stdClass(); + $newtempuser->fullname = $data->tname; + $newtempuser->courseid = $COURSE->id; + $newtempuser->email = $data->temail; + $newtempuser->created = time(); + $newtempuser->studentid = $studentid; + $DB->insert_record('attendance_tempusers', $newtempuser); + + redirect($att->url_managetemp()); +} + +/// Output starts here +echo $output->header(); +echo $output->heading(get_string('tempusers', 'attendance').' : '.$course->fullname); +echo $output->render($tabs); +$mform->display(); + +$tempusers = $DB->get_records('attendance_tempusers', array('courseid' => $course->id), 'fullname, email'); + +echo '
'; +echo '

'.get_string('tempuserslist', 'attendance').'

'; +if ($tempusers) { + print_tempusers($tempusers, $att); +} +echo '
'; +echo $output->footer($course); + +function print_tempusers($tempusers, attendance $att) { + echo '

'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + $even = false; // used to colour rows + foreach ($tempusers as $tempuser) { + if ($even) { + echo ''; + } else { + echo ''; + } + $even = !$even; + echo ''; + echo ''; + echo ''; + $params = array('userid' => $tempuser->id); + $editlink = html_writer::link($att->url_tempedit($params), get_string('edituser', 'attendance')); + $deletelink = html_writer::link($att->url_tempdelete($params), get_string('deleteuser', 'attendance')); + $mergelink = html_writer::link($att->url_tempmerge($params), get_string('mergeuser', 'attendance')); + echo ''; + echo ''; + } + echo '
'.get_string('tusername', 'attendance').''.get_string('tuseremail', 'attendance').''.get_string('tcreated', 'attendance').''.get_string('tactions', 'attendance').'
'.format_string($tempuser->fullname).''.format_string($tempuser->email).''.userdate($tempuser->created, get_string('strftimedatetime')).''.$editlink.' | '.$deletelink.' | '.$mergelink.'
'; +} + + diff --git a/tests/behat/extra_features.feature b/tests/behat/extra_features.feature new file mode 100644 index 0000000..6c03d2b --- /dev/null +++ b/tests/behat/extra_features.feature @@ -0,0 +1,112 @@ +@mod @mod_attendance @javascript +Feature: Test the various new features in the attendance module + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | student4 | Student | 4 | student4@example.com | + | student5 | Student | 5 | student5@example.com | + And the following "course enrolments" exist: + | course | user | role | + | C1 | teacher1 | editingteacher | + | C1 | student1 | student | + | C1 | student2 | student | + | C1 | student3 | student | + And I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "Attendance" to section "1" and I fill the form with: + | Name | Test attendance | + And I log out + + Scenario: A teacher can create and update temporary users + Given I log in as "teacher1" + And I follow "Course 1" + And I follow "Test attendance" + And I follow "Temporary users" + + When I set the following fields to these values: + | Full name | Temporary user 1 | + | Email | | + And I press "Add user" + And I set the following fields to these values: + | Full name | Temporary user test 2 | + | Email | tempuser2test@example.com | + And I press "Add user" + Then I should see "Temporary user 1" + And "tempuser2test@example.com" "text" should exist in the "Temporary user test 2" "table_row" + + When I click on "Edit user" "link" in the "Temporary user test 2" "table_row" + And the following fields match these values: + | Full name | Temporary user test 2 | + | Email | tempuser2test@example.com | + And I set the following fields to these values: + | Full name | Temporary user 2 | + | Email | tempuser2@example.com | + And I press "Edit user" + Then "tempuser2@example.com" "text" should exist in the "Temporary user 2" "table_row" + + When I click on "Delete user" "link" in the "Temporary user 1" "table_row" + And I press "Continue" + Then I should not see "Temporary user 1" + And I should see "Temporary user 2" + + Scenario: A teacher can take attendance for temporary users + Given I log in as "teacher1" + And I follow "Course 1" + And I follow "Test attendance" + And I follow "Temporary users" + And I set the following fields to these values: + | Full name | Temporary user 1 | + | Email | | + And I press "Add user" + And I set the following fields to these values: + | Full name | Temporary user 2 | + | Email | tempuser2@example.com | + And I press "Add user" + + And I follow "Add" + And I set the following fields to these values: + | Create multiple sessions | 0 | + And I click on "submitbutton" "button" + And I follow "Sessions" + + When I follow "Take attendance" + # Present + And I click on "td.c2 input" "css_element" in the "Student 1" "table_row" + # Late + And I click on "td.c3 input" "css_element" in the "Student 2" "table_row" + # Excused + And I click on "td.c4 input" "css_element" in the "Temporary user 1" "table_row" + # Absent + And I click on "td.c5 input" "css_element" in the "Temporary user 2" "table_row" + And I press "Save attendance" + And I follow "Report" + Then "P" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "E" "text" should exist in the "Temporary user 1" "table_row" + And "A" "text" should exist in the "Temporary user 2" "table_row" + + When I follow "Temporary user 2" + Then I should see "Absent" + + # Merge user. + When I follow "Test attendance" + And I follow "Temporary users" + And I click on "Merge user" "link" in the "Temporary user 2" "table_row" + And I set the field "Participant" to "Student 3" + And I press "Merge user" + And I follow "Report" + + Then "P" "text" should exist in the "Student 1" "table_row" + And "L" "text" should exist in the "Student 2" "table_row" + And "E" "text" should exist in the "Temporary user 1" "table_row" + And "A" "text" should exist in the "Student 3" "table_row" + And I should not see "Temporary user 2" diff --git a/version.php b/version.php index d021803..9f2dff3 100644 --- a/version.php +++ b/version.php @@ -22,7 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$plugin->version = 2015040500; +$plugin->version = 2015040501; $plugin->requires = 2014042900; $plugin->release = '2.9.1'; $plugin->maturity = MATURITY_STABLE;