1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 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. 8 // 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. 13 // 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/>. 16 17 /** 18 * Data provider. 19 * 20 * @package mod_lesson 21 * @copyright 2018 Frédéric Massart 22 * @author Frédéric Massart <fred@branchup.tech> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_lesson\privacy; 27 defined('MOODLE_INTERNAL') || die(); 28 29 use context; 30 use context_helper; 31 use context_module; 32 use stdClass; 33 use core_privacy\local\metadata\collection; 34 use core_privacy\local\request\approved_contextlist; 35 use core_privacy\local\request\approved_userlist; 36 use core_privacy\local\request\helper; 37 use core_privacy\local\request\transform; 38 use core_privacy\local\request\userlist; 39 use core_privacy\local\request\writer; 40 41 require_once($CFG->dirroot . '/mod/lesson/locallib.php'); 42 require_once($CFG->dirroot . '/mod/lesson/pagetypes/essay.php'); 43 require_once($CFG->dirroot . '/mod/lesson/pagetypes/matching.php'); 44 require_once($CFG->dirroot . '/mod/lesson/pagetypes/multichoice.php'); 45 46 /** 47 * Data provider class. 48 * 49 * @package mod_lesson 50 * @copyright 2018 Frédéric Massart 51 * @author Frédéric Massart <fred@branchup.tech> 52 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 53 */ 54 class provider implements 55 \core_privacy\local\metadata\provider, 56 \core_privacy\local\request\core_userlist_provider, 57 \core_privacy\local\request\plugin\provider, 58 \core_privacy\local\request\user_preference_provider { 59 60 /** 61 * Returns metadata. 62 * 63 * @param collection $collection The initialised collection to add items to. 64 * @return collection A listing of user data stored through this system. 65 */ 66 public static function get_metadata(collection $collection) : collection { 67 $collection->add_database_table('lesson_attempts', [ 68 'userid' => 'privacy:metadata:attempts:userid', 69 'pageid' => 'privacy:metadata:attempts:pageid', 70 'answerid' => 'privacy:metadata:attempts:answerid', 71 'retry' => 'privacy:metadata:attempts:retry', 72 'correct' => 'privacy:metadata:attempts:correct', 73 'useranswer' => 'privacy:metadata:attempts:useranswer', 74 'timeseen' => 'privacy:metadata:attempts:timeseen', 75 ], 'privacy:metadata:attempts'); 76 77 $collection->add_database_table('lesson_grades', [ 78 'userid' => 'privacy:metadata:grades:userid', 79 'grade' => 'privacy:metadata:grades:grade', 80 'completed' => 'privacy:metadata:grades:completed', 81 // The column late is not used. 82 ], 'privacy:metadata:grades'); 83 84 $collection->add_database_table('lesson_timer', [ 85 'userid' => 'privacy:metadata:timer:userid', 86 'starttime' => 'privacy:metadata:timer:starttime', 87 'lessontime' => 'privacy:metadata:timer:lessontime', 88 'completed' => 'privacy:metadata:timer:completed', 89 'timemodifiedoffline' => 'privacy:metadata:timer:timemodifiedoffline', 90 ], 'privacy:metadata:timer'); 91 92 $collection->add_database_table('lesson_branch', [ 93 'userid' => 'privacy:metadata:branch:userid', 94 'pageid' => 'privacy:metadata:branch:pageid', 95 'retry' => 'privacy:metadata:branch:retry', 96 'flag' => 'privacy:metadata:branch:flag', 97 'timeseen' => 'privacy:metadata:branch:timeseen', 98 'nextpageid' => 'privacy:metadata:branch:nextpageid', 99 ], 'privacy:metadata:branch'); 100 101 $collection->add_database_table('lesson_overrides', [ 102 'userid' => 'privacy:metadata:overrides:userid', 103 'available' => 'privacy:metadata:overrides:available', 104 'deadline' => 'privacy:metadata:overrides:deadline', 105 'timelimit' => 'privacy:metadata:overrides:timelimit', 106 'review' => 'privacy:metadata:overrides:review', 107 'maxattempts' => 'privacy:metadata:overrides:maxattempts', 108 'retake' => 'privacy:metadata:overrides:retake', 109 'password' => 'privacy:metadata:overrides:password', 110 ], 'privacy:metadata:overrides'); 111 112 $collection->add_user_preference('lesson_view', 'privacy:metadata:userpref:lessonview'); 113 114 return $collection; 115 } 116 117 /** 118 * Get the list of contexts that contain user information for the specified user. 119 * 120 * @param int $userid The user to search. 121 * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. 122 */ 123 public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist { 124 $contextlist = new \core_privacy\local\request\contextlist(); 125 126 $sql = " 127 SELECT DISTINCT ctx.id 128 FROM {lesson} l 129 JOIN {modules} m 130 ON m.name = :lesson 131 JOIN {course_modules} cm 132 ON cm.instance = l.id 133 AND cm.module = m.id 134 JOIN {context} ctx 135 ON ctx.instanceid = cm.id 136 AND ctx.contextlevel = :modulelevel 137 LEFT JOIN {lesson_attempts} la 138 ON la.lessonid = l.id 139 AND la.userid = :userid1 140 LEFT JOIN {lesson_branch} lb 141 ON lb.lessonid = l.id 142 AND lb.userid = :userid2 143 LEFT JOIN {lesson_grades} lg 144 ON lg.lessonid = l.id 145 AND lg.userid = :userid3 146 LEFT JOIN {lesson_overrides} lo 147 ON lo.lessonid = l.id 148 AND lo.userid = :userid4 149 LEFT JOIN {lesson_timer} lt 150 ON lt.lessonid = l.id 151 AND lt.userid = :userid5 152 WHERE la.id IS NOT NULL 153 OR lb.id IS NOT NULL 154 OR lg.id IS NOT NULL 155 OR lo.id IS NOT NULL 156 OR lt.id IS NOT NULL"; 157 158 $params = [ 159 'lesson' => 'lesson', 160 'modulelevel' => CONTEXT_MODULE, 161 'userid1' => $userid, 162 'userid2' => $userid, 163 'userid3' => $userid, 164 'userid4' => $userid, 165 'userid5' => $userid, 166 ]; 167 $contextlist->add_from_sql($sql, $params); 168 169 return $contextlist; 170 } 171 172 /** 173 * Get the list of users who have data within a context. 174 * 175 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. 176 * 177 */ 178 public static function get_users_in_context(userlist $userlist) { 179 $context = $userlist->get_context(); 180 181 if (!is_a($context, \context_module::class)) { 182 return; 183 } 184 185 $params = [ 186 'lesson' => 'lesson', 187 'modulelevel' => CONTEXT_MODULE, 188 'contextid' => $context->id, 189 ]; 190 191 // Mapping of lesson tables which may contain user data. 192 $joins = [ 193 'lesson_attempts', 194 'lesson_branch', 195 'lesson_grades', 196 'lesson_overrides', 197 'lesson_timer', 198 ]; 199 200 foreach ($joins as $join) { 201 $sql = " 202 SELECT lx.userid 203 FROM {lesson} l 204 JOIN {modules} m 205 ON m.name = :lesson 206 JOIN {course_modules} cm 207 ON cm.instance = l.id 208 AND cm.module = m.id 209 JOIN {context} ctx 210 ON ctx.instanceid = cm.id 211 AND ctx.contextlevel = :modulelevel 212 JOIN {{$join}} lx 213 ON lx.lessonid = l.id 214 WHERE ctx.id = :contextid"; 215 216 $userlist->add_from_sql('userid', $sql, $params); 217 } 218 } 219 220 /** 221 * Export all user data for the specified user, in the specified contexts. 222 * 223 * @param approved_contextlist $contextlist The approved contexts to export information for. 224 */ 225 public static function export_user_data(approved_contextlist $contextlist) { 226 global $DB; 227 228 $user = $contextlist->get_user(); 229 $userid = $user->id; 230 $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { 231 if ($context->contextlevel == CONTEXT_MODULE) { 232 $carry[] = $context->instanceid; 233 } 234 return $carry; 235 }, []); 236 if (empty($cmids)) { 237 return; 238 } 239 240 // If the context export was requested, then let's at least describe the lesson. 241 foreach ($cmids as $cmid) { 242 $context = context_module::instance($cmid); 243 $contextdata = helper::get_context_data($context, $user); 244 helper::export_context_files($context, $user); 245 writer::with_context($context)->export_data([], $contextdata); 246 } 247 248 // Find the lesson IDs. 249 $lessonidstocmids = static::get_lesson_ids_to_cmids_from_cmids($cmids); 250 251 // Prepare the common SQL fragments. 252 list($inlessonsql, $inlessonparams) = $DB->get_in_or_equal(array_keys($lessonidstocmids), SQL_PARAMS_NAMED); 253 $sqluserlesson = "userid = :userid AND lessonid $inlessonsql"; 254 $paramsuserlesson = array_merge($inlessonparams, ['userid' => $userid]); 255 256 // Export the overrides. 257 $recordset = $DB->get_recordset_select('lesson_overrides', $sqluserlesson, $paramsuserlesson); 258 static::recordset_loop_and_export($recordset, 'lessonid', null, function($carry, $record) { 259 // We know that there is only one row per lesson, so no need to use $carry. 260 return (object) [ 261 'available' => $record->available !== null ? transform::datetime($record->available) : null, 262 'deadline' => $record->deadline !== null ? transform::datetime($record->deadline) : null, 263 'timelimit' => $record->timelimit !== null ? format_time($record->timelimit) : null, 264 'review' => $record->review !== null ? transform::yesno($record->review) : null, 265 'maxattempts' => $record->maxattempts, 266 'retake' => $record->retake !== null ? transform::yesno($record->retake) : null, 267 'password' => $record->password, 268 ]; 269 }, function($lessonid, $data) use ($lessonidstocmids) { 270 $context = context_module::instance($lessonidstocmids[$lessonid]); 271 writer::with_context($context)->export_related_data([], 'overrides', $data); 272 }); 273 274 // Export the grades. 275 $recordset = $DB->get_recordset_select('lesson_grades', $sqluserlesson, $paramsuserlesson, 'lessonid, completed'); 276 static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) { 277 $carry[] = (object) [ 278 'grade' => $record->grade, 279 'completed' => transform::datetime($record->completed), 280 ]; 281 return $carry; 282 }, function($lessonid, $data) use ($lessonidstocmids) { 283 $context = context_module::instance($lessonidstocmids[$lessonid]); 284 writer::with_context($context)->export_related_data([], 'grades', (object) ['grades' => $data]); 285 }); 286 287 // Export the timers. 288 $recordset = $DB->get_recordset_select('lesson_timer', $sqluserlesson, $paramsuserlesson, 'lessonid, starttime'); 289 static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) { 290 $carry[] = (object) [ 291 'starttime' => transform::datetime($record->starttime), 292 'lastactivity' => transform::datetime($record->lessontime), 293 'completed' => transform::yesno($record->completed), 294 'timemodifiedoffline' => $record->timemodifiedoffline ? transform::datetime($record->timemodifiedoffline) : null, 295 ]; 296 return $carry; 297 }, function($lessonid, $data) use ($lessonidstocmids) { 298 $context = context_module::instance($lessonidstocmids[$lessonid]); 299 writer::with_context($context)->export_related_data([], 'timers', (object) ['timers' => $data]); 300 }); 301 302 // Export the attempts and branches. 303 $sql = " 304 SELECT " . $DB->sql_concat('lp.id', "':'", 'COALESCE(la.id, 0)', "':'", 'COALESCE(lb.id, 0)') . " AS uniqid, 305 lp.lessonid, 306 307 lp.id AS page_id, 308 lp.qtype AS page_qtype, 309 lp.qoption AS page_qoption, 310 lp.title AS page_title, 311 lp.contents AS page_contents, 312 lp.contentsformat AS page_contentsformat, 313 314 la.id AS attempt_id, 315 la.retry AS attempt_retry, 316 la.correct AS attempt_correct, 317 la.useranswer AS attempt_useranswer, 318 la.timeseen AS attempt_timeseen, 319 320 lb.id AS branch_id, 321 lb.retry AS branch_retry, 322 lb.timeseen AS branch_timeseen, 323 324 lpb.id AS nextpage_id, 325 lpb.title AS nextpage_title 326 327 FROM {lesson_pages} lp 328 LEFT JOIN {lesson_attempts} la 329 ON la.pageid = lp.id 330 AND la.userid = :userid1 331 LEFT JOIN {lesson_branch} lb 332 ON lb.pageid = lp.id 333 AND lb.userid = :userid2 334 LEFT JOIN {lesson_pages} lpb 335 ON lpb.id = lb.nextpageid 336 WHERE lp.lessonid $inlessonsql 337 AND (la.id IS NOT NULL OR lb.id IS NOT NULL) 338 ORDER BY lp.lessonid, lp.id, la.retry, lb.retry, la.id, lb.id"; 339 $params = array_merge($inlessonparams, ['userid1' => $userid, 'userid2' => $userid]); 340 341 $recordset = $DB->get_recordset_sql($sql, $params); 342 static::recordset_loop_and_export($recordset, 'lessonid', [], function($carry, $record) use ($lessonidstocmids) { 343 $context = context_module::instance($lessonidstocmids[$record->lessonid]); 344 $options = ['context' => $context]; 345 346 $take = isset($record->attempt_retry) ? $record->attempt_retry : $record->branch_retry; 347 if (!isset($carry[$take])) { 348 $carry[$take] = (object) [ 349 'number' => $take + 1, 350 'answers' => [], 351 'jumps' => [] 352 ]; 353 } 354 355 $pagefilespath = [get_string('privacy:path:pages', 'mod_lesson'), $record->page_id]; 356 writer::with_context($context)->export_area_files($pagefilespath, 'mod_lesson', 'page_contents', $record->page_id); 357 $pagecontents = format_text( 358 writer::with_context($context)->rewrite_pluginfile_urls( 359 $pagefilespath, 360 'mod_lesson', 361 'page_contents', 362 $record->page_id, 363 $record->page_contents 364 ), 365 $record->page_contentsformat, 366 $options 367 ); 368 369 $pagebase = [ 370 'id' => $record->page_id, 371 'page' => $record->page_title, 372 'contents' => $pagecontents, 373 'contents_files_folder' => implode('/', $pagefilespath) 374 ]; 375 376 if (isset($record->attempt_id)) { 377 $carry[$take]->answers[] = array_merge($pagebase, static::transform_attempt($record, $context)); 378 379 } else if (isset($record->branch_id)) { 380 if (!empty($record->nextpage_id)) { 381 $wentto = $record->nextpage_title . " (id: {$record->nextpage_id})"; 382 } else { 383 $wentto = get_string('endoflesson', 'mod_lesson'); 384 } 385 $carry[$take]->jumps[] = array_merge($pagebase, [ 386 'went_to' => $wentto, 387 'timeseen' => transform::datetime($record->attempt_timeseen) 388 ]); 389 } 390 391 return $carry; 392 393 }, function($lessonid, $data) use ($lessonidstocmids) { 394 $context = context_module::instance($lessonidstocmids[$lessonid]); 395 writer::with_context($context)->export_related_data([], 'attempts', (object) [ 396 'attempts' => array_values($data) 397 ]); 398 }); 399 } 400 401 /** 402 * Export all user preferences for the plugin. 403 * 404 * @param int $userid The userid of the user whose data is to be exported. 405 */ 406 public static function export_user_preferences(int $userid) { 407 $lessonview = get_user_preferences('lesson_view', null, $userid); 408 if ($lessonview !== null) { 409 $value = $lessonview; 410 411 // The code seems to indicate that there also is the option 'simple', but it's not 412 // described nor accessible from anywhere so we won't describe it more than being 'simple'. 413 if ($lessonview == 'full') { 414 $value = get_string('full', 'mod_lesson'); 415 } else if ($lessonview == 'collapsed') { 416 $value = get_string('collapsed', 'mod_lesson'); 417 } 418 419 writer::export_user_preference('mod_lesson', 'lesson_view', $lessonview, 420 get_string('privacy:metadata:userpref:lessonview', 'mod_lesson')); 421 } 422 } 423 424 /** 425 * Delete all data for all users in the specified context. 426 * 427 * @param context $context The specific context to delete data for. 428 */ 429 public static function delete_data_for_all_users_in_context(context $context) { 430 global $DB; 431 432 if ($context->contextlevel != CONTEXT_MODULE) { 433 return; 434 } 435 436 if (!$lessonid = static::get_lesson_id_from_context($context)) { 437 return; 438 } 439 440 $DB->delete_records('lesson_attempts', ['lessonid' => $lessonid]); 441 $DB->delete_records('lesson_branch', ['lessonid' => $lessonid]); 442 $DB->delete_records('lesson_grades', ['lessonid' => $lessonid]); 443 $DB->delete_records('lesson_timer', ['lessonid' => $lessonid]); 444 $DB->delete_records_select('lesson_overrides', 'lessonid = :id AND userid IS NOT NULL', ['id' => $lessonid]); 445 446 $fs = get_file_storage(); 447 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses'); 448 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers'); 449 } 450 451 /** 452 * Delete all user data for the specified user, in the specified contexts. 453 * 454 * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. 455 */ 456 public static function delete_data_for_user(approved_contextlist $contextlist) { 457 global $DB; 458 459 $userid = $contextlist->get_user()->id; 460 $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) { 461 if ($context->contextlevel == CONTEXT_MODULE) { 462 $carry[] = $context->instanceid; 463 } 464 return $carry; 465 }, []); 466 if (empty($cmids)) { 467 return; 468 } 469 470 // Find the lesson IDs. 471 $lessonidstocmids = static::get_lesson_ids_to_cmids_from_cmids($cmids); 472 $lessonids = array_keys($lessonidstocmids); 473 if (empty($lessonids)) { 474 return; 475 } 476 477 // Prepare the SQL we'll need below. 478 list($insql, $inparams) = $DB->get_in_or_equal($lessonids, SQL_PARAMS_NAMED); 479 $sql = "lessonid $insql AND userid = :userid"; 480 $params = array_merge($inparams, ['userid' => $userid]); 481 482 // Delete the attempt files. 483 $fs = get_file_storage(); 484 $recordset = $DB->get_recordset_select('lesson_attempts', $sql, $params, '', 'id, lessonid'); 485 foreach ($recordset as $record) { 486 $cmid = $lessonidstocmids[$record->lessonid]; 487 $context = context_module::instance($cmid); 488 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $record->id); 489 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $record->id); 490 } 491 $recordset->close(); 492 493 // Delete all the things. 494 $DB->delete_records_select('lesson_attempts', $sql, $params); 495 $DB->delete_records_select('lesson_branch', $sql, $params); 496 $DB->delete_records_select('lesson_grades', $sql, $params); 497 $DB->delete_records_select('lesson_timer', $sql, $params); 498 $DB->delete_records_select('lesson_overrides', $sql, $params); 499 } 500 501 /** 502 * Delete multiple users within a single context. 503 * 504 * @param approved_userlist $userlist The approved context and user information to delete information for. 505 */ 506 public static function delete_data_for_users(approved_userlist $userlist) { 507 global $DB; 508 509 $context = $userlist->get_context(); 510 $lessonid = static::get_lesson_id_from_context($context); 511 $userids = $userlist->get_userids(); 512 513 if (empty($lessonid)) { 514 return; 515 } 516 517 // Prepare the SQL we'll need below. 518 list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); 519 $sql = "lessonid = :lessonid AND userid {$insql}"; 520 $params = array_merge($inparams, ['lessonid' => $lessonid]); 521 522 // Delete the attempt files. 523 $fs = get_file_storage(); 524 $recordset = $DB->get_recordset_select('lesson_attempts', $sql, $params, '', 'id, lessonid'); 525 foreach ($recordset as $record) { 526 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $record->id); 527 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $record->id); 528 } 529 $recordset->close(); 530 531 // Delete all the things. 532 $DB->delete_records_select('lesson_attempts', $sql, $params); 533 $DB->delete_records_select('lesson_branch', $sql, $params); 534 $DB->delete_records_select('lesson_grades', $sql, $params); 535 $DB->delete_records_select('lesson_timer', $sql, $params); 536 $DB->delete_records_select('lesson_overrides', $sql, $params); 537 } 538 539 /** 540 * Get a survey ID from its context. 541 * 542 * @param context_module $context The module context. 543 * @return int 544 */ 545 protected static function get_lesson_id_from_context(context_module $context) { 546 $cm = get_coursemodule_from_id('lesson', $context->instanceid); 547 return $cm ? (int) $cm->instance : 0; 548 } 549 550 /** 551 * Return a dict of lesson IDs mapped to their course module ID. 552 * 553 * @param array $cmids The course module IDs. 554 * @return array In the form of [$lessonid => $cmid]. 555 */ 556 protected static function get_lesson_ids_to_cmids_from_cmids(array $cmids) { 557 global $DB; 558 list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED); 559 $sql = " 560 SELECT l.id, cm.id AS cmid 561 FROM {lesson} l 562 JOIN {modules} m 563 ON m.name = :lesson 564 JOIN {course_modules} cm 565 ON cm.instance = l.id 566 AND cm.module = m.id 567 WHERE cm.id $insql"; 568 $params = array_merge($inparams, ['lesson' => 'lesson']); 569 return $DB->get_records_sql_menu($sql, $params); 570 } 571 572 /** 573 * Loop and export from a recordset. 574 * 575 * @param moodle_recordset $recordset The recordset. 576 * @param string $splitkey The record key to determine when to export. 577 * @param mixed $initial The initial data to reduce from. 578 * @param callable $reducer The function to return the dataset, receives current dataset, and the current record. 579 * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset. 580 * @return void 581 */ 582 protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial, 583 callable $reducer, callable $export) { 584 585 $data = $initial; 586 $lastid = null; 587 588 foreach ($recordset as $record) { 589 if ($lastid && $record->{$splitkey} != $lastid) { 590 $export($lastid, $data); 591 $data = $initial; 592 } 593 $data = $reducer($data, $record); 594 $lastid = $record->{$splitkey}; 595 } 596 $recordset->close(); 597 598 if (!empty($lastid)) { 599 $export($lastid, $data); 600 } 601 } 602 603 /** 604 * Transform an attempt. 605 * 606 * @param stdClass $data Data from the database, as per the exporting method. 607 * @param context_module $context The module context. 608 * @return array 609 */ 610 protected static function transform_attempt(stdClass $data, context_module $context) { 611 global $DB; 612 613 $options = ['context' => $context]; 614 $answer = $data->attempt_useranswer; 615 $response = null; 616 $responsefilesfolder = null; 617 618 if ($answer !== null) { 619 if ($data->page_qtype == LESSON_PAGE_ESSAY) { 620 // Essay questions serialise data in the answer field. 621 $info = \lesson_page_type_essay::extract_useranswer($answer); 622 $answerfilespath = [get_string('privacy:path:essayanswers', 'mod_lesson'), $data->attempt_id]; 623 $answer = format_text( 624 writer::with_context($context)->rewrite_pluginfile_urls( 625 $answerfilespath, 626 'mod_lesson', 627 'essay_answers', 628 $data->attempt_id, 629 $info->answer 630 ), 631 $info->answerformat, 632 $options 633 ); 634 writer::with_context($context)->export_area_files($answerfilespath, 'mod_lesson', 635 'essay_answers', $data->page_id); 636 637 if ($info->response !== null) { 638 // We export the files in a subfolder to avoid conflicting files, and tell the user 639 // where those files were exported. That is because we are not using a subfolder for 640 // every single essay response. 641 $responsefilespath = [get_string('privacy:path:essayresponses', 'mod_lesson'), $data->attempt_id]; 642 $responsefilesfolder = implode('/', $responsefilespath); 643 $response = format_text( 644 writer::with_context($context)->rewrite_pluginfile_urls( 645 $responsefilespath, 646 'mod_lesson', 647 'essay_responses', 648 $data->attempt_id, 649 $info->response 650 ), 651 $info->responseformat, 652 $options 653 ); 654 writer::with_context($context)->export_area_files($responsefilespath, 'mod_lesson', 655 'essay_responses', $data->page_id); 656 657 } 658 659 } else if ($data->page_qtype == LESSON_PAGE_MULTICHOICE && $data->page_qoption) { 660 // Multiple choice quesitons with multiple answers encode the answers. 661 list($insql, $inparams) = $DB->get_in_or_equal(explode(',', $answer), SQL_PARAMS_NAMED); 662 $orderby = 'id, ' . $DB->sql_order_by_text('answer') . ', answerformat'; 663 $records = $DB->get_records_select('lesson_answers', "id $insql", $inparams, $orderby); 664 $answer = array_values(array_map(function($record) use ($options) { 665 return format_text($record->answer, $record->answerformat, $options); 666 }, empty($records) ? [] : $records)); 667 668 } else if ($data->page_qtype == LESSON_PAGE_MATCHING) { 669 // Matching questions need sorting. 670 $chosen = explode(',', $answer); 671 $answers = $DB->get_records_select('lesson_answers', 'pageid = :pageid', ['pageid' => $data->page_id], 672 'id', 'id, answer, answerformat', 2); // The two first entries are not options. 673 $i = -1; 674 $answer = array_values(array_map(function($record) use (&$i, $chosen, $options) { 675 $i++; 676 return [ 677 'label' => format_text($record->answer, $record->answerformat, $options), 678 'matched_with' => array_key_exists($i, $chosen) ? $chosen[$i] : null 679 ]; 680 }, empty($answers) ? [] : $answers)); 681 } 682 } 683 684 $result = [ 685 'answer' => $answer, 686 'correct' => transform::yesno($data->attempt_correct), 687 'timeseen' => transform::datetime($data->attempt_timeseen), 688 ]; 689 690 if ($response !== null) { 691 $result['response'] = $response; 692 $result['response_files_folder'] = $responsefilesfolder; 693 } 694 695 return $result; 696 } 697 698 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body