2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * Core Report class of basic reporting plugin
18 * @package scormreport
19 * @subpackage interactions
20 * @author Dan Marsden and Ankit Kumar Agarwal
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 namespace scormreport_interactions;
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot.'/mod/scorm/report/interactions/responsessettings_form.php');
30 class report extends \mod_scorm\report {
32 * displays the full report
33 * @param \stdClass $scorm full SCORM object
34 * @param \stdClass $cm - full course_module object
35 * @param \stdClass $course - full course object
36 * @param string $download - type of download being requested
38 public function display($scorm, $cm, $course, $download) {
39 global $CFG, $DB, $OUTPUT, $PAGE;
41 $contextmodule = \context_module::instance($cm->id);
42 $action = optional_param('action', '', PARAM_ALPHA);
43 $attemptids = optional_param_array('attemptid', array(), PARAM_RAW);
44 $attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT);
45 $PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode)));
47 if ($action == 'delete' && has_capability('mod/scorm:deleteresponses', $contextmodule) && confirm_sesskey()) {
48 if (scorm_delete_responses($attemptids, $scorm)) { // Delete responses.
49 echo $OUTPUT->notification(get_string('scormresponsedeleted', 'scorm'), 'notifysuccess');
52 // Find out current groups mode.
53 $currentgroup = groups_get_activity_group($cm, true);
56 $mform = new \mod_scorm_report_interactions_settings($PAGE->url, compact('currentgroup'));
57 if ($fromform = $mform->get_data()) {
58 $pagesize = $fromform->pagesize;
59 $includeqtext = $fromform->qtext;
60 $includeresp = $fromform->resp;
61 $includeright = $fromform->right;
62 $includeresult = $fromform->result;
63 set_user_preference('scorm_report_pagesize', $pagesize);
64 set_user_preference('scorm_report_interactions_qtext', $includeqtext);
65 set_user_preference('scorm_report_interactions_resp', $includeresp);
66 set_user_preference('scorm_report_interactions_right', $includeright);
67 set_user_preference('scorm_report_interactions_result', $includeresult);
69 $pagesize = get_user_preferences('scorm_report_pagesize', 0);
70 $includeqtext = get_user_preferences('scorm_report_interactions_qtext', 0);
71 $includeresp = get_user_preferences('scorm_report_interactions_resp', 1);
72 $includeright = get_user_preferences('scorm_report_interactions_right', 0);
73 $includeresult = get_user_preferences('scorm_report_interactions_result', 0);
76 $pagesize = SCORM_REPORT_DEFAULT_PAGE_SIZE;
80 $displayoptions = array();
81 $displayoptions['attemptsmode'] = $attemptsmode;
82 $displayoptions['qtext'] = $includeqtext;
83 $displayoptions['resp'] = $includeresp;
84 $displayoptions['right'] = $includeright;
85 $displayoptions['result'] = $includeresult;
87 $mform->set_data($displayoptions + array('pagesize' => $pagesize));
88 if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used.
90 groups_print_activity_menu($cm, new \moodle_url($PAGE->url, $displayoptions));
93 $formattextoptions = array('context' => \context_course::instance($course->id));
95 // We only want to show the checkbox to delete attempts
96 // if the user has permissions and if the report mode is showing attempts.
97 $candelete = has_capability('mod/scorm:deleteresponses', $contextmodule)
98 && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
99 // Select the students.
102 if (empty($currentgroup)) {
103 // All users who can attempt scoes.
104 if (!$students = get_users_by_capability($contextmodule, 'mod/scorm:savetrack', 'u.id', '', '', '', '', '', false)) {
105 echo $OUTPUT->notification(get_string('nostudentsyet'));
109 $allowedlist = array_keys($students);
113 // All users who can attempt scoes and who are in the currently selected group.
114 if (!$groupstudents = get_users_by_capability($contextmodule,
115 'mod/scorm:savetrack', 'u.id', '', '', '',
116 $currentgroup, '', false)) {
117 echo $OUTPUT->notification(get_string('nostudentsingroup'));
119 $groupstudents = array();
121 $allowedlist = array_keys($groupstudents);
122 unset($groupstudents);
124 if ( !$nostudents ) {
125 // Now check if asked download of data.
126 $coursecontext = \context_course::instance($course->id);
128 $filename = clean_filename("$course->shortname ".format_string($scorm->name, true, $formattextoptions));
131 // Define table columns.
134 if (!$download && $candelete) {
135 $columns[] = 'checkbox';
138 if (!$download && $CFG->grade_report_showuserimage) {
139 $columns[] = 'picture';
142 $columns[] = 'fullname';
143 $headers[] = get_string('name');
145 $extrafields = get_extra_user_fields($coursecontext);
146 foreach ($extrafields as $field) {
148 $headers[] = get_user_field_name($field);
150 $columns[] = 'attempt';
151 $headers[] = get_string('attempt', 'scorm');
152 $columns[] = 'start';
153 $headers[] = get_string('started', 'scorm');
154 $columns[] = 'finish';
155 $headers[] = get_string('last', 'scorm');
156 $columns[] = 'score';
157 $headers[] = get_string('score', 'scorm');
158 $scoes = $DB->get_records('scorm_scoes', array("scorm" => $scorm->id), 'sortorder, id');
159 foreach ($scoes as $sco) {
160 if ($sco->launch != '') {
161 $columns[] = 'scograde'.$sco->id;
162 $headers[] = format_string($sco->title, '', $formattextoptions);
167 list($usql, $params) = $DB->get_in_or_equal($allowedlist, SQL_PARAMS_NAMED);
168 // Construct the SQL.
169 $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').' AS uniqueid, ';
170 $select .= 'st.scormid AS scormid, st.attempt AS attempt, ' .
171 \user_picture::fields('u', array('idnumber'), 'userid') .
172 get_extra_user_fields_sql($coursecontext, 'u', '', array('email', 'idnumber')) . ' ';
174 // This part is the same for all cases - join users and scorm_scoes_track tables.
175 $from = 'FROM {user} u ';
176 $from .= 'LEFT JOIN {scorm_scoes_track} st ON st.userid = u.id AND st.scormid = '.$scorm->id;
177 switch ($attemptsmode) {
178 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH:
179 // Show only students with attempts.
180 $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NOT NULL';
182 case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
183 // Show only students without attempts.
184 $where = ' WHERE u.id ' .$usql. ' AND st.userid IS NULL';
186 case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS:
187 // Show all students with or without attempts.
188 $where = ' WHERE u.id ' .$usql. ' AND (st.userid IS NOT NULL OR st.userid IS NULL)';
192 $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(st.attempt, 0)').')) AS nbresults, ';
193 $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'st.attempt').')) AS nbattempts, ';
194 $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers ';
195 $countsql .= $from.$where;
196 $questioncount = get_scorm_question_count($scorm->id);
197 $nbmaincolumns = count($columns);
198 for ($id = 0; $id < $questioncount; $id++) {
199 if ($displayoptions['qtext']) {
200 $columns[] = 'question' . $id;
201 $headers[] = get_string('questionx', 'scormreport_interactions', $id);
203 if ($displayoptions['resp']) {
204 $columns[] = 'response' . $id;
205 $headers[] = get_string('responsex', 'scormreport_interactions', $id);
207 if ($displayoptions['right']) {
208 $columns[] = 'right' . $id;
209 $headers[] = get_string('rightanswerx', 'scormreport_interactions', $id);
211 if ($displayoptions['result']) {
212 $columns[] = 'result' . $id;
213 $headers[] = get_string('resultx', 'scormreport_interactions', $id);
218 $table = new \flexible_table('mod-scorm-report');
220 $table->define_columns($columns);
221 $table->define_headers($headers);
222 $table->define_baseurl($PAGE->url);
224 $table->sortable(true);
225 $table->collapsible(true);
227 // This is done to prevent redundant data, when a user has multiple attempts.
228 $table->column_suppress('picture');
229 $table->column_suppress('fullname');
230 foreach ($extrafields as $field) {
231 $table->column_suppress($field);
234 $table->no_sorting('start');
235 $table->no_sorting('finish');
236 $table->no_sorting('score');
237 $table->no_sorting('checkbox');
238 $table->no_sorting('picture');
240 for ($id = 0; $id < $questioncount; $id++) {
241 if ($displayoptions['qtext']) {
242 $table->no_sorting('question'.$id);
244 if ($displayoptions['resp']) {
245 $table->no_sorting('response'.$id);
247 if ($displayoptions['right']) {
248 $table->no_sorting('right'.$id);
250 if ($displayoptions['result']) {
251 $table->no_sorting('result'.$id);
255 foreach ($scoes as $sco) {
256 if ($sco->launch != '') {
257 $table->no_sorting('scograde'.$sco->id);
261 $table->column_class('picture', 'picture');
262 $table->column_class('fullname', 'bold');
263 $table->column_class('score', 'bold');
265 $table->set_attribute('cellspacing', '0');
266 $table->set_attribute('id', 'attempts');
267 $table->set_attribute('class', 'generaltable generalbox');
269 // Start working -- this is necessary as soon as the niceties are over.
271 } else if ($download == 'ODS') {
272 require_once("$CFG->libdir/odslib.class.php");
275 // Creating a workbook.
276 $workbook = new \MoodleODSWorkbook("-");
277 // Sending HTTP headers.
278 $workbook->send($filename);
279 // Creating the first worksheet.
280 $sheettitle = get_string('report', 'scorm');
281 $myxls = $workbook->add_worksheet($sheettitle);
283 $format = $workbook->add_format();
284 $format->set_bold(0);
285 $formatbc = $workbook->add_format();
286 $formatbc->set_bold(1);
287 $formatbc->set_align('center');
288 $formatb = $workbook->add_format();
289 $formatb->set_bold(1);
290 $formaty = $workbook->add_format();
291 $formaty->set_bg_color('yellow');
292 $formatc = $workbook->add_format();
293 $formatc->set_align('center');
294 $formatr = $workbook->add_format();
295 $formatr->set_bold(1);
296 $formatr->set_color('red');
297 $formatr->set_align('center');
298 $formatg = $workbook->add_format();
299 $formatg->set_bold(1);
300 $formatg->set_color('green');
301 $formatg->set_align('center');
302 // Here starts workshhet headers.
305 foreach ($headers as $item) {
306 $myxls->write(0, $colnum, $item, $formatbc);
310 } else if ($download == 'Excel') {
311 require_once("$CFG->libdir/excellib.class.php");
314 // Creating a workbook.
315 $workbook = new \MoodleExcelWorkbook("-");
316 // Sending HTTP headers.
317 $workbook->send($filename);
318 // Creating the first worksheet.
319 $sheettitle = get_string('report', 'scorm');
320 $myxls = $workbook->add_worksheet($sheettitle);
322 $format = $workbook->add_format();
323 $format->set_bold(0);
324 $formatbc = $workbook->add_format();
325 $formatbc->set_bold(1);
326 $formatbc->set_align('center');
327 $formatb = $workbook->add_format();
328 $formatb->set_bold(1);
329 $formaty = $workbook->add_format();
330 $formaty->set_bg_color('yellow');
331 $formatc = $workbook->add_format();
332 $formatc->set_align('center');
333 $formatr = $workbook->add_format();
334 $formatr->set_bold(1);
335 $formatr->set_color('red');
336 $formatr->set_align('center');
337 $formatg = $workbook->add_format();
338 $formatg->set_bold(1);
339 $formatg->set_color('green');
340 $formatg->set_align('center');
343 foreach ($headers as $item) {
344 $myxls->write(0, $colnum, $item, $formatbc);
348 } else if ($download == 'CSV') {
349 $csvexport = new \csv_export_writer("tab");
350 $csvexport->set_filename($filename, ".txt");
351 $csvexport->add_data($headers);
355 $sort = $table->get_sql_sort();
359 // Fix some wired sorting.
361 $sort = ' ORDER BY uniqueid';
363 $sort = ' ORDER BY '.$sort;
367 // Add extra limits due to initials bar.
368 list($twhere, $tparams) = $table->get_sql_where();
370 $where .= ' AND '.$twhere; // Initial bar.
371 $params = array_merge($params, $tparams);
374 if (!empty($countsql)) {
375 $count = $DB->get_record_sql($countsql, $params);
376 $totalinitials = $count->nbresults;
378 $countsql .= ' AND '.$twhere;
380 $count = $DB->get_record_sql($countsql, $params);
381 $total = $count->nbresults;
384 $table->pagesize($pagesize, $total);
386 echo \html_writer::start_div('scormattemptcounts');
387 if ( $count->nbresults == $count->nbattempts ) {
388 echo get_string('reportcountattempts', 'scorm', $count);
389 } else if ( $count->nbattempts > 0 ) {
390 echo get_string('reportcountallattempts', 'scorm', $count);
392 echo $count->nbusers.' '.get_string('users');
394 echo \html_writer::end_div();
397 // Fetch the attempts.
399 $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params,
400 $table->get_page_start(), $table->get_page_size());
401 echo \html_writer::start_div('', array('id' => 'scormtablecontainer'));
404 $strreallydel = addslashes_js(get_string('deleteattemptcheck', 'scorm'));
405 echo \html_writer::start_tag('form', array('id' => 'attemptsform', 'method' => 'post',
406 'action' => $PAGE->url->out(false),
407 'onsubmit' => 'return confirm("'.$strreallydel.'");'));
408 echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'delete'));
409 echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
410 echo \html_writer::start_div('', array('style' => 'display: none;'));
411 echo \html_writer::input_hidden_params($PAGE->url);
412 echo \html_writer::end_div();
413 echo \html_writer::start_div();
415 $table->initialbars($totalinitials > 20); // Build table rows.
417 $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params);
420 foreach ($attempts as $scouser) {
422 if (!empty($scouser->attempt)) {
423 $timetracks = scorm_get_sco_runtime($scorm->id, false, $scouser->userid, $scouser->attempt);
427 if (in_array('checkbox', $columns)) {
428 if ($candelete && !empty($timetracks->start)) {
429 $row[] = \html_writer::checkbox('attemptid[]', $scouser->userid . ':' . $scouser->attempt, false);
430 } else if ($candelete) {
434 if (in_array('picture', $columns)) {
435 $user = new \stdClass();
436 $additionalfields = explode(',', \user_picture::fields());
437 $user = username_load_fields_from_object($user, $scouser, null, $additionalfields);
438 $user->id = $scouser->userid;
439 $row[] = $OUTPUT->user_picture($user, array('courseid' => $course->id));
442 $url = new \moodle_url('/user/view.php', array('id' => $scouser->userid, 'course' => $course->id));
443 $row[] = \html_writer::link($url, fullname($scouser));
445 $row[] = fullname($scouser);
447 foreach ($extrafields as $field) {
448 $row[] = s($scouser->{$field});
450 if (empty($timetracks->start)) {
457 $url = new \moodle_url('/mod/scorm/report/userreport.php',
458 array('id' => $cm->id,
459 'user' => $scouser->userid,
460 'attempt' => $scouser->attempt));
461 $row[] = \html_writer::link($url, $scouser->attempt);
463 $row[] = $scouser->attempt;
465 if ($download == 'ODS' || $download == 'Excel' ) {
466 $row[] = userdate($timetracks->start, get_string("strftimedatetime", "langconfig"));
468 $row[] = userdate($timetracks->start);
470 if ($download == 'ODS' || $download == 'Excel' ) {
471 $row[] = userdate($timetracks->finish, get_string('strftimedatetime', 'langconfig'));
473 $row[] = userdate($timetracks->finish);
475 $row[] = scorm_grade_user_attempt($scorm, $scouser->userid, $scouser->attempt);
477 // Print out all scores of attempt.
478 $emptyrow = $download ? '' : ' ';
479 foreach ($scoes as $sco) {
480 if ($sco->launch != '') {
481 if ($trackdata = scorm_get_tracks($sco->id, $scouser->userid, $scouser->attempt)) {
482 if ($trackdata->status == '') {
483 $trackdata->status = 'notattempted';
485 $strstatus = get_string($trackdata->status, 'scorm');
486 // If raw score exists, print it.
487 if ($trackdata->score_raw != '') {
488 $score = $trackdata->score_raw;
489 // Add max score if it exists.
490 if (isset($trackdata->score_max)) {
491 $score .= '/'.$trackdata->score_max;
493 } else { // Else print out status.
497 $url = new \moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $cm->id,
498 'scoid' => $sco->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt));
499 $row[] = $OUTPUT->pix_icon($trackdata->status, $strstatus, 'scorm') . '<br>' .
500 \html_writer::link($url, $score, array('title' => get_string('details', 'scorm')));
505 for ($i = 0; $i < $questioncount; $i++) {
506 if ($displayoptions['qtext']) {
507 $element = 'cmi.interactions_'.$i.'.id';
508 if (isset($trackdata->$element)) {
509 $row[] = s($trackdata->$element);
514 if ($displayoptions['resp']) {
515 $element = 'cmi.interactions_'.$i.'.student_response';
516 if (isset($trackdata->$element)) {
517 $row[] = s($trackdata->$element);
522 if ($displayoptions['right']) {
524 $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern';
526 if (isset($trackdata->$element)) {
527 while (isset($trackdata->$element)) {
531 $rightans .= s($trackdata->$element);
533 $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern';
540 if ($displayoptions['result']) {
541 $element = 'cmi.interactions_'.$i.'.result';
542 if (isset($trackdata->$element)) {
543 $row[] = s($trackdata->$element);
549 // End of interaction data.
551 // If we don't have track data, we haven't attempted yet.
552 $strstatus = get_string('notattempted', 'scorm');
554 $row[] = $OUTPUT->pix_icon('notattempted', $strstatus, 'scorm') . '<br>' . $strstatus;
558 // Complete the empty cells.
559 for ($i = 0; $i < count($columns) - $nbmaincolumns; $i++) {
567 $table->add_data($row);
568 } else if ($download == 'Excel' or $download == 'ODS') {
570 foreach ($row as $item) {
571 $myxls->write($rownum, $colnum, $item, $format);
575 } else if ($download == 'CSV') {
576 $csvexport->add_data($row);
580 $table->finish_output();
582 echo \html_writer::start_tag('table', array('id' => 'commands'));
583 echo \html_writer::start_tag('tr').\html_writer::start_tag('td');
584 echo \html_writer::link('#', get_string('selectall', 'scorm'), array('id' => 'checkattempts'));
586 echo \html_writer::link('#', get_string('selectnone', 'scorm'), array('id' => 'uncheckattempts'));
587 $PAGE->requires->js_amd_inline("
588 require(['jquery'], function($) {
589 $('#checkattempts').click(function(e) {
590 $('#attemptsform').find('input:checkbox').prop('checked', true);
593 $('#uncheckattempts').click(function(e) {
594 $('#attemptsform').find('input:checkbox').prop('checked', false);
599 echo \html_writer::empty_tag('input', array('type' => 'submit',
600 'value' => get_string('deleteselected', 'scorm'),
601 'class' => 'btn btn-secondary'));
602 echo \html_writer::end_tag('td').\html_writer::end_tag('tr').\html_writer::end_tag('table');
604 echo \html_writer::end_tag('div');
605 echo \html_writer::end_tag('form');
607 echo \html_writer::end_div();
608 if (!empty($attempts)) {
609 echo \html_writer::start_tag('table', array('class' => 'boxaligncenter')).\html_writer::start_tag('tr');
610 echo \html_writer::start_tag('td');
611 echo $OUTPUT->single_button(new \moodle_url($PAGE->url,
612 array('download' => 'ODS') + $displayoptions),
613 get_string('downloadods'),
615 ['class' => 'mt-1']);
616 echo \html_writer::end_tag('td');
617 echo \html_writer::start_tag('td');
618 echo $OUTPUT->single_button(new \moodle_url($PAGE->url,
619 array('download' => 'Excel') + $displayoptions),
620 get_string('downloadexcel'),
622 ['class' => 'mt-1']);
623 echo \html_writer::end_tag('td');
624 echo \html_writer::start_tag('td');
625 echo $OUTPUT->single_button(new \moodle_url($PAGE->url,
626 array('download' => 'CSV') + $displayoptions),
627 get_string('downloadtext'),
629 ['class' => 'mt-1']);
630 echo \html_writer::end_tag('td');
631 echo \html_writer::start_tag('td');
632 echo \html_writer::end_tag('td');
633 echo \html_writer::end_tag('tr').\html_writer::end_tag('table');
637 if ($candelete && !$download) {
638 echo \html_writer::end_div();
639 echo \html_writer::end_tag('form');
640 $table->finish_output();
642 echo \html_writer::end_div();
644 // Show preferences form irrespective of attempts are there to report or not.
646 $mform->set_data(compact('pagesize', 'attemptsmode'));
649 if ($download == 'Excel' or $download == 'ODS') {
652 } else if ($download == 'CSV') {
653 $csvexport->download_file();
657 echo $OUTPUT->notification(get_string('noactivity', 'scorm'));