Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403]

   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  namespace core_competency\privacy;
  18  
  19  use context;
  20  use context_course;
  21  use context_helper;
  22  use context_module;
  23  use context_system;
  24  use context_user;
  25  use moodle_recordset;
  26  use core_competency\api;
  27  use core_competency\competency;
  28  use core_competency\competency_framework;
  29  use core_competency\course_competency;
  30  use core_competency\course_competency_settings;
  31  use core_competency\course_module_competency;
  32  use core_competency\evidence;
  33  use core_competency\plan;
  34  use core_competency\plan_competency;
  35  use core_competency\related_competency;
  36  use core_competency\template;
  37  use core_competency\template_cohort;
  38  use core_competency\template_competency;
  39  use core_competency\user_competency;
  40  use core_competency\user_competency_course;
  41  use core_competency\user_competency_plan;
  42  use core_competency\user_evidence;
  43  use core_competency\user_evidence_competency;
  44  use core_competency\external\performance_helper;
  45  use core_privacy\local\metadata\collection;
  46  use core_privacy\local\request\approved_userlist;
  47  use core_privacy\local\request\contextlist;
  48  use core_privacy\local\request\approved_contextlist;
  49  use core_privacy\local\request\transform;
  50  use core_privacy\local\request\userlist;
  51  use core_privacy\local\request\writer;
  52  
  53  /**
  54   * Data provider class.
  55   *
  56   * @package    core_competency
  57   * @copyright  2018 Frédéric Massart
  58   * @author     Frédéric Massart <fred@branchup.tech>
  59   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  60   */
  61  class provider implements
  62      \core_privacy\local\metadata\provider,
  63      \core_privacy\local\request\core_userlist_provider,
  64      \core_privacy\local\request\subsystem\provider {
  65  
  66      /**
  67       * Returns metadata.
  68       *
  69       * @param collection $collection The initialised collection to add items to.
  70       * @return collection A listing of user data stored through this system.
  71       */
  72      public static function get_metadata(collection $collection) : collection {
  73  
  74          // Tables not related to users aside from the editing information.
  75          $collection->add_database_table('competency', [
  76              'timecreated' => 'privacy:metadata:timecreated',
  77              'timemodified' => 'privacy:metadata:timemodified',
  78              'usermodified' => 'privacy:metadata:usermodified',
  79          ], 'privacy:metadata:competency');
  80  
  81          $collection->add_database_table('competency_coursecompsetting', [
  82              'timecreated' => 'privacy:metadata:timecreated',
  83              'timemodified' => 'privacy:metadata:timemodified',
  84              'usermodified' => 'privacy:metadata:usermodified',
  85          ], 'privacy:metadata:competency_coursecompsetting');
  86  
  87          $collection->add_database_table('competency_framework', [
  88              'timecreated' => 'privacy:metadata:timecreated',
  89              'timemodified' => 'privacy:metadata:timemodified',
  90              'usermodified' => 'privacy:metadata:usermodified',
  91          ], 'privacy:metadata:competency_framework');
  92  
  93          $collection->add_database_table('competency_coursecomp', [
  94              'timecreated' => 'privacy:metadata:timecreated',
  95              'timemodified' => 'privacy:metadata:timemodified',
  96              'usermodified' => 'privacy:metadata:usermodified',
  97          ], 'privacy:metadata:competency_coursecomp');
  98  
  99          $collection->add_database_table('competency_template', [
 100              'timecreated' => 'privacy:metadata:timecreated',
 101              'timemodified' => 'privacy:metadata:timemodified',
 102              'usermodified' => 'privacy:metadata:usermodified',
 103          ], 'privacy:metadata:competency_template');
 104  
 105          $collection->add_database_table('competency_templatecomp', [
 106              'timecreated' => 'privacy:metadata:timecreated',
 107              'timemodified' => 'privacy:metadata:timemodified',
 108              'usermodified' => 'privacy:metadata:usermodified',
 109          ], 'privacy:metadata:competency_templatecomp');
 110  
 111          $collection->add_database_table('competency_templatecohort', [
 112              'timecreated' => 'privacy:metadata:timecreated',
 113              'timemodified' => 'privacy:metadata:timemodified',
 114              'usermodified' => 'privacy:metadata:usermodified',
 115          ], 'privacy:metadata:competency_templatecohort');
 116  
 117          $collection->add_database_table('competency_relatedcomp', [
 118              'timecreated' => 'privacy:metadata:timecreated',
 119              'timemodified' => 'privacy:metadata:timemodified',
 120              'usermodified' => 'privacy:metadata:usermodified',
 121          ], 'privacy:metadata:competency_relatedcomp');
 122  
 123          $collection->add_database_table('competency_modulecomp', [
 124              'timecreated' => 'privacy:metadata:timecreated',
 125              'timemodified' => 'privacy:metadata:timemodified',
 126              'usermodified' => 'privacy:metadata:usermodified',
 127          ], 'privacy:metadata:competency_modulecomp');
 128  
 129          // Tables containing user data.
 130          $collection->add_database_table('competency_plan', [
 131              'name' => 'privacy:metadata:plan:name',
 132              'description' => 'privacy:metadata:plan:description',
 133              'userid' => 'privacy:metadata:plan:userid',
 134              'status' => 'privacy:metadata:plan:status',
 135              'duedate' => 'privacy:metadata:plan:duedate',
 136              'reviewerid' => 'privacy:metadata:plan:reviewerid',
 137              'timecreated' => 'privacy:metadata:timecreated',
 138              'timemodified' => 'privacy:metadata:timemodified',
 139              'usermodified' => 'privacy:metadata:usermodified',
 140          ], 'privacy:metadata:competency_plan');
 141  
 142          $collection->add_database_table('competency_usercomp', [
 143              'userid' => 'privacy:metadata:usercomp:userid',
 144              'status' => 'privacy:metadata:usercomp:status',
 145              'reviewerid' => 'privacy:metadata:usercomp:reviewerid',
 146              'proficiency' => 'privacy:metadata:usercomp:proficiency',
 147              'grade' => 'privacy:metadata:usercomp:grade',
 148              'timecreated' => 'privacy:metadata:timecreated',
 149              'timemodified' => 'privacy:metadata:timemodified',
 150              'usermodified' => 'privacy:metadata:usermodified',
 151          ], 'privacy:metadata:competency_usercomp');
 152  
 153          $collection->add_database_table('competency_usercompcourse', [
 154              'userid' => 'privacy:metadata:usercomp:userid',
 155              'proficiency' => 'privacy:metadata:usercomp:proficiency',
 156              'grade' => 'privacy:metadata:usercomp:grade',
 157              'timecreated' => 'privacy:metadata:timecreated',
 158              'timemodified' => 'privacy:metadata:timemodified',
 159              'usermodified' => 'privacy:metadata:usermodified',
 160          ], 'privacy:metadata:competency_usercompcourse');
 161  
 162          $collection->add_database_table('competency_usercompplan', [
 163              'userid' => 'privacy:metadata:usercomp:userid',
 164              'proficiency' => 'privacy:metadata:usercomp:proficiency',
 165              'grade' => 'privacy:metadata:usercomp:grade',
 166              'timecreated' => 'privacy:metadata:timecreated',
 167              'timemodified' => 'privacy:metadata:timemodified',
 168              'usermodified' => 'privacy:metadata:usermodified',
 169          ], 'privacy:metadata:competency_usercompplan');
 170  
 171          $collection->add_database_table('competency_plancomp', [
 172              'timecreated' => 'privacy:metadata:timecreated',
 173              'timemodified' => 'privacy:metadata:timemodified',
 174              'usermodified' => 'privacy:metadata:usermodified',
 175          ], 'privacy:metadata:competency_plancomp');
 176  
 177          $collection->add_database_table('competency_evidence', [
 178              'action' => 'privacy:metadata:evidence:action',
 179              'actionuserid' => 'privacy:metadata:evidence:actionuserid',
 180              'descidentifier' => 'privacy:metadata:evidence:descidentifier',
 181              'desccomponent' => 'privacy:metadata:evidence:desccomponent',
 182              'desca' => 'privacy:metadata:evidence:desca',
 183              'url' => 'privacy:metadata:evidence:url',
 184              'grade' => 'privacy:metadata:evidence:grade',
 185              'note' => 'privacy:metadata:evidence:note',
 186              'timecreated' => 'privacy:metadata:timecreated',
 187              'timemodified' => 'privacy:metadata:timemodified',
 188              'usermodified' => 'privacy:metadata:usermodified',
 189          ], 'privacy:metadata:competency_evidence');
 190  
 191          $collection->add_database_table('competency_userevidence', [
 192              'name' => 'privacy:metadata:userevidence:name',
 193              'description' => 'privacy:metadata:userevidence:description',
 194              'url' => 'privacy:metadata:userevidence:url',
 195              'timecreated' => 'privacy:metadata:timecreated',
 196              'timemodified' => 'privacy:metadata:timemodified',
 197              'usermodified' => 'privacy:metadata:usermodified',
 198          ], 'privacy:metadata:competency_userevidence');
 199  
 200          $collection->add_database_table('competency_userevidencecomp', [
 201              'timecreated' => 'privacy:metadata:timecreated',
 202              'timemodified' => 'privacy:metadata:timemodified',
 203              'usermodified' => 'privacy:metadata:usermodified',
 204          ], 'privacy:metadata:competency_userevidencecomp');
 205  
 206          // Comments can be left on learning plans and competencies.
 207          $collection->link_subsystem('core_comment', 'privacy:metadata:core_comments');
 208  
 209          return $collection;
 210      }
 211  
 212      /**
 213       * Get the list of contexts that contain user information for the specified user.
 214       *
 215       * @param int $userid The user to search.
 216       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
 217       */
 218      public static function get_contexts_for_userid(int $userid) : contextlist {
 219          global $DB;
 220          $contextlist = new \core_privacy\local\request\contextlist();
 221  
 222          // Find the contexts of the frameworks, and related data, modified by the user.
 223          $sql = "
 224               SELECT DISTINCT ctx.id
 225                 FROM {context} ctx
 226                 JOIN {" . competency_framework::TABLE . "} cf
 227                   ON cf.contextid = ctx.id
 228            LEFT JOIN {" . competency::TABLE . "} c
 229                   ON c.competencyframeworkid = cf.id
 230            LEFT JOIN {" . related_competency::TABLE . "} cr
 231                   ON cr.competencyid = c.id
 232                WHERE cf.usermodified = :userid1
 233                   OR c.usermodified = :userid2
 234                   OR cr.usermodified = :userid3";
 235          $params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
 236          $contextlist->add_from_sql($sql, $params);
 237  
 238          // Find the contexts of the templates, and related data, modified by the user.
 239          $sql = "
 240               SELECT DISTINCT ctx.id
 241                 FROM {context} ctx
 242                 JOIN {" . template::TABLE . "} tpl
 243                   ON tpl.contextid = ctx.id
 244            LEFT JOIN {" . template_cohort::TABLE . "} tch
 245                   ON tch.templateid = tpl.id
 246                  AND tch.usermodified = :userid2
 247            LEFT JOIN {" . template_competency::TABLE . "} tc
 248                   ON tc.templateid = tpl.id
 249                  AND tc.usermodified = :userid3
 250                WHERE tpl.usermodified = :userid1
 251                   OR tch.id IS NOT NULL
 252                   OR tc.id IS NOT NULL";
 253          $params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
 254          $contextlist->add_from_sql($sql, $params);
 255  
 256          // Find the possible course contexts.
 257          $sql = "
 258               SELECT DISTINCT ctx.id
 259                 FROM {" . course_competency::TABLE . "} cc
 260                 JOIN {context} ctx
 261                   ON ctx.instanceid = cc.courseid
 262                  AND ctx.contextlevel = :courselevel
 263                WHERE cc.usermodified = :userid";
 264          $params = ['courselevel' => CONTEXT_COURSE, 'userid' => $userid];
 265          $contextlist->add_from_sql($sql, $params);
 266  
 267          $sql = "
 268               SELECT DISTINCT ctx.id
 269                 FROM {" . course_competency_settings::TABLE . "} ccs
 270                 JOIN {context} ctx
 271                   ON ctx.instanceid = ccs.courseid
 272                  AND ctx.contextlevel = :courselevel
 273                WHERE ccs.usermodified = :userid";
 274          $params = ['courselevel' => CONTEXT_COURSE, 'userid' => $userid];
 275          $contextlist->add_from_sql($sql, $params);
 276  
 277          $sql = "
 278               SELECT DISTINCT ctx.id
 279                 FROM {" . user_competency_course::TABLE . "} ucc
 280                 JOIN {context} ctx
 281                   ON ctx.instanceid = ucc.courseid
 282                  AND ctx.contextlevel = :courselevel
 283                WHERE ucc.usermodified = :userid";
 284          $params = ['courselevel' => CONTEXT_COURSE, 'userid' => $userid];
 285          $contextlist->add_from_sql($sql, $params);
 286  
 287          // Find the possible module contexts.
 288          $sql = "
 289               SELECT DISTINCT ctx.id
 290                 FROM {" . course_module_competency::TABLE . "} cmc
 291                 JOIN {context} ctx
 292                   ON ctx.instanceid = cmc.cmid
 293                  AND ctx.contextlevel = :modulelevel
 294                WHERE cmc.usermodified = :userid";
 295          $params = ['modulelevel' => CONTEXT_MODULE, 'userid' => $userid];
 296          $contextlist->add_from_sql($sql, $params);
 297  
 298          // Add user contexts through usermodified/reviewing of plan related data.
 299          $sql = "
 300               SELECT DISTINCT ctx.id
 301                 FROM {" . plan::TABLE . "} p
 302                 JOIN {context} ctx
 303                   ON ctx.instanceid = p.userid
 304                  AND ctx.contextlevel = :userlevel
 305            LEFT JOIN {" . plan_competency::TABLE . "} pc
 306                   ON pc.planid = p.id
 307                  AND pc.usermodified = :userid3
 308            LEFT JOIN {" . user_competency_plan::TABLE . "} upc
 309                   ON upc.planid = p.id
 310                  AND upc.usermodified = :userid4
 311                WHERE p.usermodified = :userid1
 312                   OR p.reviewerid = :userid2
 313                   OR pc.id IS NOT NULL
 314                   OR upc.id IS NOT NULL";
 315          $params = [
 316              'userlevel' => CONTEXT_USER,
 317              'userid1' => $userid,
 318              'userid2' => $userid,
 319              'userid3' => $userid,
 320              'userid4' => $userid,
 321          ];
 322          $contextlist->add_from_sql($sql, $params);
 323  
 324          // Add user contexts through usermodified/reviewing of competency data.
 325          $sql = "
 326               SELECT DISTINCT ctx.id
 327                 FROM {context} ctx
 328            LEFT JOIN {" . user_competency::TABLE . "} uc
 329                   ON uc.userid = ctx.instanceid
 330                  AND ctx.contextlevel = :userlevel1
 331            LEFT JOIN {" . evidence::TABLE . "} e
 332                   ON e.usercompetencyid = uc.id
 333                  AND (e.usermodified = :userid3 OR e.actionuserid = :userid4)
 334            LEFT JOIN {" . user_evidence::TABLE . "} ue
 335                   ON ue.userid = ctx.instanceid
 336                  AND ctx.contextlevel = :userlevel2
 337                  AND ue.usermodified = :userid5
 338            LEFT JOIN {" . user_evidence_competency::TABLE . "} uec
 339                   ON uec.userevidenceid = ue.id
 340                  AND uec.usermodified = :userid6
 341                WHERE uc.usermodified = :userid1
 342                   OR uc.reviewerid = :userid2
 343                   OR e.id IS NOT NULL
 344                   OR ue.id IS NOT NULL
 345                   OR uec.id IS NOT NULL";
 346          $params = [
 347              'userlevel1' => CONTEXT_USER,
 348              'userlevel2' => CONTEXT_USER,
 349              'userid1' => $userid,
 350              'userid2' => $userid,
 351              'userid3' => $userid,
 352              'userid4' => $userid,
 353              'userid5' => $userid,
 354              'userid6' => $userid,
 355          ];
 356          $contextlist->add_from_sql($sql, $params);
 357  
 358          // Now, the easy part, we fetch the user context for user plans and competencies.
 359          // We also fetch the course context for the state of competencies for the user in courses.
 360          $sql = "
 361               SELECT DISTINCT ctx.id
 362                 FROM {context} ctx
 363            LEFT JOIN {" . plan::TABLE . "} p
 364                   ON p.userid = ctx.instanceid
 365                  AND ctx.contextlevel = :userlevel1
 366            LEFT JOIN {" . user_competency::TABLE . "} uc
 367                   ON uc.userid = ctx.instanceid
 368                  AND ctx.contextlevel = :userlevel2
 369                  AND uc.userid = :userid2
 370            LEFT JOIN {" . user_evidence::TABLE . "} ue
 371                   ON ue.userid = ctx.instanceid
 372                  AND ctx.contextlevel = :userlevel3
 373                  AND ue.userid = :userid3
 374            LEFT JOIN {" . user_competency_course::TABLE . "} ucc
 375                   ON ucc.courseid = ctx.instanceid
 376                  AND ctx.contextlevel = :courselevel
 377                  AND ucc.userid = :userid4
 378                WHERE p.userid = :userid1
 379                   OR uc.id IS NOT NULL
 380                   OR ue.id IS NOT NULL
 381                   OR ucc.id IS NOT NULL";
 382          $params = [
 383              'userlevel1' => CONTEXT_USER,
 384              'userlevel2' => CONTEXT_USER,
 385              'userlevel3' => CONTEXT_USER,
 386              'courselevel' => CONTEXT_COURSE,
 387              'userid1' => $userid,
 388              'userid2' => $userid,
 389              'userid3' => $userid,
 390              'userid4' => $userid,
 391          ];
 392          $contextlist->add_from_sql($sql, $params);
 393  
 394          // Include the user contexts in which the user commented.
 395          $sql = "
 396              SELECT ctx.id
 397                FROM {context} ctx
 398                JOIN {comments} c
 399                  ON c.contextid = ctx.id
 400               WHERE c.component = :component
 401                 AND c.commentarea IN (:planarea, :usercomparea)
 402                 AND c.userid = :userid";
 403          $params = [
 404              'component' => 'competency',    // Must not be core_competency.
 405              'planarea' => 'plan',
 406              'usercomparea' => 'user_competency',
 407              'userid' => $userid
 408          ];
 409          $contextlist->add_from_sql($sql, $params);
 410  
 411          return $contextlist;
 412      }
 413  
 414      /**
 415       * Get the list of users who have data within a context.
 416       *
 417       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 418       */
 419      public static function get_users_in_context(userlist $userlist) {
 420          $context = $userlist->get_context();
 421          $params = ['contextid' => $context->id];
 422  
 423          // Add users who have modified the frameworks and related data in the context.
 424          $sql = "
 425               SELECT cf.usermodified
 426                 FROM {" . competency_framework::TABLE . "} cf
 427                WHERE cf.contextid = :contextid";
 428          $userlist->add_from_sql('usermodified', $sql, $params);
 429  
 430          $sql = "
 431               SELECT c.usermodified
 432                 FROM {" . competency_framework::TABLE . "} cf
 433                 JOIN {" . competency::TABLE . "} c
 434                   ON c.competencyframeworkid = cf.id
 435                WHERE cf.contextid = :contextid";
 436          $userlist->add_from_sql('usermodified', $sql, $params);
 437  
 438          $sql = "
 439               SELECT cr.usermodified
 440                 FROM {" . competency_framework::TABLE . "} cf
 441                 JOIN {" . competency::TABLE . "} c
 442                   ON c.competencyframeworkid = cf.id
 443                 JOIN {" . related_competency::TABLE . "} cr
 444                   ON cr.competencyid = c.id
 445                WHERE cf.contextid = :contextid";
 446          $userlist->add_from_sql('usermodified', $sql, $params);
 447  
 448          // Add users who have modified the templates and related data in the context.
 449          $sql = "
 450               SELECT tpl.usermodified
 451                 FROM {" . template::TABLE . "} tpl
 452                WHERE tpl.contextid = :contextid";
 453          $userlist->add_from_sql('usermodified', $sql, $params);
 454  
 455          $sql = "
 456               SELECT tch.usermodified
 457                 FROM {" . template::TABLE . "} tpl
 458                 JOIN {" . template_cohort::TABLE . "} tch
 459                   ON tch.templateid = tpl.id
 460                WHERE tpl.contextid = :contextid";
 461          $userlist->add_from_sql('usermodified', $sql, $params);
 462  
 463          $sql = "
 464               SELECT tc.usermodified
 465                 FROM {" . template::TABLE . "} tpl
 466                 JOIN {" . template_competency::TABLE . "} tc
 467                   ON tc.templateid = tpl.id
 468                WHERE tpl.contextid = :contextid";
 469          $userlist->add_from_sql('usermodified', $sql, $params);
 470  
 471          // Add users if userlist is in course context.
 472          if (is_a($context, \context_course::class)) {
 473              $params = ['courseid' => $context->instanceid];
 474  
 475              $sql = "
 476                   SELECT cc.usermodified
 477                     FROM {" . course_competency::TABLE . "} cc
 478                    WHERE cc.courseid = :courseid";
 479              $userlist->add_from_sql('usermodified', $sql, $params);
 480  
 481              $sql = "
 482                   SELECT ccs.usermodified
 483                     FROM {" . course_competency_settings::TABLE . "} ccs
 484                    WHERE ccs.courseid = :courseid";
 485              $userlist->add_from_sql('usermodified', $sql, $params);
 486  
 487              $sql = "
 488                   SELECT ucc.userid, ucc.usermodified
 489                     FROM {" . user_competency_course::TABLE . "} ucc
 490                    WHERE ucc.courseid = :courseid";
 491              $userlist->add_from_sql('userid', $sql, $params);
 492              $userlist->add_from_sql('usermodified', $sql, $params);
 493  
 494          } else if (is_a($context, \context_module::class)) {
 495              // Add users if userlist is in module context.
 496              $params = ['moduleid' => $context->instanceid];
 497  
 498              $sql = "
 499                   SELECT cmc.usermodified
 500                     FROM {" . course_module_competency::TABLE . "} cmc
 501                    WHERE cmc.cmid = :moduleid";
 502              $userlist->add_from_sql('usermodified', $sql, $params);
 503  
 504          } else if (is_a($context, \context_user::class)) {
 505              $params = ['userid' => $context->instanceid];
 506  
 507              // Add users through plan related data.
 508              $sql = "
 509                   SELECT p.userid, p.usermodified, p.reviewerid
 510                     FROM {" . plan::TABLE . "} p
 511                    WHERE p.userid = :userid";
 512              $userlist->add_from_sql('userid', $sql, $params);
 513              $userlist->add_from_sql('usermodified', $sql, $params);
 514              $userlist->add_from_sql('reviewerid', $sql, $params);
 515  
 516              $sql = "
 517                   SELECT pc.usermodified
 518                     FROM {" . plan::TABLE . "} p
 519                     JOIN {" . plan_competency::TABLE . "} pc
 520                       ON pc.planid = p.id
 521                    WHERE p.userid = :userid";
 522              $userlist->add_from_sql('usermodified', $sql, $params);
 523  
 524              $sql = "
 525                   SELECT upc.usermodified
 526                     FROM {" . user_competency_plan::TABLE . "} upc
 527                    WHERE upc.userid = :userid";
 528              $userlist->add_from_sql('usermodified', $sql, $params);
 529  
 530              // Add users through competency data.
 531              $sql = "
 532                   SELECT uc.userid, uc.usermodified, uc.reviewerid
 533                     FROM {" . user_competency::TABLE . "} uc
 534                    WHERE uc.userid = :userid";
 535              $userlist->add_from_sql('userid', $sql, $params);
 536              $userlist->add_from_sql('usermodified', $sql, $params);
 537              $userlist->add_from_sql('reviewerid', $sql, $params);
 538  
 539              $sql = "
 540                   SELECT e.usermodified, e.actionuserid
 541                     FROM {" . user_competency::TABLE . "} uc
 542                     JOIN {" . evidence::TABLE . "} e
 543                       ON e.usercompetencyid = uc.id
 544                    WHERE uc.userid = :userid";
 545              $userlist->add_from_sql('usermodified', $sql, $params);
 546              $userlist->add_from_sql('actionuserid', $sql, $params);
 547  
 548              // Add users through evidence data.
 549              $sql = "
 550                   SELECT ue.userid, ue.usermodified
 551                     FROM {" . user_evidence::TABLE . "} ue
 552                    WHERE ue.userid = :userid";
 553              $userlist->add_from_sql('userid', $sql, $params);
 554              $userlist->add_from_sql('usermodified', $sql, $params);
 555  
 556              $sql = "
 557                   SELECT ue.usermodified
 558                     FROM {" . user_evidence::TABLE . "} ue
 559                     JOIN {" . user_evidence_competency::TABLE . "} uec
 560                       ON uec.userevidenceid = ue.id
 561                    WHERE ue.userid = :userid";
 562              $userlist->add_from_sql('usermodified', $sql, $params);
 563          }
 564  
 565          // Add users who commented in the context.
 566          // Note: Comment component must be competency and not core_competency.
 567          \core_comment\privacy\provider::get_users_in_context_from_sql(
 568                  $userlist, 'com', 'competency', 'plan', $context->id);
 569  
 570          \core_comment\privacy\provider::get_users_in_context_from_sql(
 571                  $userlist, 'com', 'competency', 'user_competency', $context->id);
 572      }
 573  
 574      /**
 575       * Export all user data for the specified user, in the specified contexts.
 576       *
 577       * We skip the enabled check for competencies, as there could be historical data. This avoids exceptions thrown from
 578       * the {@see api::require_enabled} method, which is called at various points during export via the competency API
 579       *
 580       * @param approved_contextlist $contextlist The approved contexts to export information for.
 581       */
 582      public static function export_user_data(approved_contextlist $contextlist) {
 583  
 584          // Export even if competencies are not currently enabled.
 585          api::skip_enabled();
 586  
 587          $user = $contextlist->get_user();
 588          $userid = $user->id;
 589  
 590          // Re-arrange the contexts by context level.
 591          $groupedcontexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 592              $contextlevel = $context->contextlevel;
 593              if (!in_array($contextlevel, [CONTEXT_USER, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_SYSTEM, CONTEXT_COURSECAT])) {
 594                  return $carry;
 595              }
 596              $carry[$contextlevel][] = $context;
 597              return $carry;
 598          }, [
 599              CONTEXT_COURSE => [],
 600              CONTEXT_COURSECAT => [],
 601              CONTEXT_MODULE => [],
 602              CONTEXT_SYSTEM => [],
 603              CONTEXT_USER => [],
 604          ]);
 605  
 606          // Process module contexts.
 607          static::export_user_data_in_module_contexts($userid, $groupedcontexts[CONTEXT_MODULE]);
 608  
 609          // Process course contexts.
 610          static::export_user_data_in_course_contexts($userid, $groupedcontexts[CONTEXT_COURSE]);
 611  
 612          // Process course categories context.
 613          static::export_user_data_in_category_contexts($userid, $groupedcontexts[CONTEXT_COURSECAT]);
 614  
 615          // Process system context.
 616          if (!empty($groupedcontexts[CONTEXT_SYSTEM])) {
 617              static::export_user_data_in_system_context($userid);
 618          }
 619  
 620          // Process user contexts.
 621          static::export_user_data_in_user_contexts($userid, $groupedcontexts[CONTEXT_USER]);
 622      }
 623  
 624      /**
 625       * Delete all data for all users in the specified context.
 626       *
 627       * @param context $context The specific context to delete data for.
 628       */
 629      public static function delete_data_for_all_users_in_context(context $context) {
 630          global $DB;
 631  
 632          switch ($context->contextlevel) {
 633              case CONTEXT_USER:
 634                  $userid = $context->instanceid;
 635                  static::delete_user_evidence_of_prior_learning($userid);
 636                  static::delete_user_plans($userid);
 637                  static::delete_user_competencies($userid);
 638                  break;
 639  
 640              case CONTEXT_COURSE:
 641                  static::delete_user_competencies_in_course($context->instanceid);
 642                  break;
 643          }
 644      }
 645  
 646      /**
 647       * Delete all user data for the specified user, in the specified contexts.
 648       *
 649       * Here we only delete the private data of user, not whether they modified, are reviewing,
 650       * or are associated with the record on at a second level. Only data directly linked to the
 651       * user will be affected.
 652       *
 653       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 654       */
 655      public static function delete_data_for_user(approved_contextlist $contextlist) {
 656          $user = $contextlist->get_user();
 657          $userid = $user->id;
 658  
 659          foreach ($contextlist as $context) {
 660              switch ($context->contextlevel) {
 661                  case CONTEXT_USER:
 662                      if ($context->instanceid == $userid) {
 663                          // Only delete the user's information when they requested their context to be deleted. We
 664                          // do not take any action on other user's contexts because we don't own the data there.
 665                          static::delete_user_evidence_of_prior_learning($userid);
 666                          static::delete_user_plans($userid);
 667                          static::delete_user_competencies($userid);
 668                      }
 669                      break;
 670  
 671                  case CONTEXT_COURSE:
 672                      static::delete_user_competencies_in_course($context->instanceid, [$userid]);
 673                      break;
 674              }
 675          }
 676      }
 677  
 678      /**
 679       * Delete multiple users within a single context.
 680       *
 681       * Here we only delete the private data of users, not whether they modified, are reviewing,
 682       * or are associated with the record on at a second level. Only data directly linked to the
 683       * user will be affected.
 684       *
 685       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 686       */
 687      public static function delete_data_for_users(approved_userlist $userlist) {
 688          $context = $userlist->get_context();
 689          $userids = $userlist->get_userids();
 690  
 691          switch ($context->contextlevel) {
 692              case CONTEXT_USER:
 693                  // Only delete the user's information when their context is being deleted.
 694                  // We do not take any action on other user's contexts because we don't own the data there.
 695                  if (in_array($context->instanceid, $userids)) {
 696                      static::delete_user_evidence_of_prior_learning($context->instanceid);
 697                      static::delete_user_plans($context->instanceid);
 698                      static::delete_user_competencies($context->instanceid);
 699                  }
 700  
 701                  break;
 702  
 703              case CONTEXT_COURSE:
 704                  static::delete_user_competencies_in_course($context->instanceid, $userids);
 705                  break;
 706          }
 707      }
 708  
 709      /**
 710       * Delete evidence of prior learning.
 711       *
 712       * @param int $userid The user ID.
 713       * @return void
 714       */
 715      protected static function delete_user_evidence_of_prior_learning($userid) {
 716          global $DB;
 717  
 718          $usercontext = context_user::instance($userid);
 719          $ueids = $DB->get_fieldset_select(user_evidence::TABLE, 'id', 'userid = :userid', ['userid' => $userid]);
 720          if (empty($ueids)) {
 721              return;
 722          }
 723          list($insql, $inparams) = $DB->get_in_or_equal($ueids, SQL_PARAMS_NAMED);
 724  
 725          // Delete competencies associated with user evidence.
 726          $DB->delete_records_select(user_evidence_competency::TABLE, "userevidenceid $insql", $inparams);
 727  
 728          // Delete the user evidence.
 729          $DB->delete_records_select(user_evidence::TABLE, "id $insql", $inparams);
 730  
 731          // Delete the user evidence files.
 732          $fs = get_file_storage();
 733          $fs->delete_area_files($usercontext->id, 'core_competency', 'userevidence');
 734      }
 735  
 736      /**
 737       * User plans.
 738       *
 739       * @param int $userid The user ID.
 740       * @return void
 741       */
 742      protected static function delete_user_plans($userid) {
 743          global $DB;
 744          $usercontext = context_user::instance($userid);
 745  
 746          // Remove all the comments made on plans.
 747          \core_comment\privacy\provider::delete_comments_for_all_users($usercontext, 'competency', 'plan');
 748  
 749          // Find the user plan IDs.
 750          $planids = $DB->get_fieldset_select(plan::TABLE, 'id', 'userid = :userid', ['userid' => $userid]);
 751          if (empty($planids)) {
 752              return;
 753          }
 754          list($insql, $inparams) = $DB->get_in_or_equal($planids, SQL_PARAMS_NAMED);
 755  
 756          // Delete all the competencies proficiency in the plans.
 757          $DB->delete_records_select(user_competency_plan::TABLE, "planid $insql", $inparams);
 758  
 759          // Delete all the competencies in the plans.
 760          $DB->delete_records_select(plan_competency::TABLE, "planid $insql", $inparams);
 761  
 762          // Delete all the plans.
 763          $DB->delete_records_select(plan::TABLE, "id $insql", $inparams);
 764      }
 765  
 766      /**
 767       * Delete user competency data.
 768       *
 769       * @param int $userid The user ID.
 770       * @return void
 771       */
 772      protected static function delete_user_competencies($userid) {
 773          global $DB;
 774          $usercontext = context_user::instance($userid);
 775  
 776          // Remove all the comments made on user competencies.
 777          \core_comment\privacy\provider::delete_comments_for_all_users($usercontext, 'competency', 'user_competency');
 778  
 779          // Find the user competency IDs.
 780          $ucids = $DB->get_fieldset_select(user_competency::TABLE, 'id', 'userid = :userid', ['userid' => $userid]);
 781          if (empty($ucids)) {
 782              return;
 783          }
 784          list($insql, $inparams) = $DB->get_in_or_equal($ucids, SQL_PARAMS_NAMED);
 785  
 786          // Delete all the evidence associated with competencies.
 787          $DB->delete_records_select(evidence::TABLE, "usercompetencyid $insql", $inparams);
 788  
 789          // Delete all the record of competency.
 790          $DB->delete_records_select(user_competency::TABLE, "id $insql", $inparams);
 791      }
 792  
 793      /**
 794       * Delete the record of competencies for user(s) in a course.
 795       *
 796       * @param int $courseid The course ID.
 797       * @param int[] $userids The user IDs, if deleting for specific user(s).
 798       * @return void
 799       */
 800      protected static function delete_user_competencies_in_course($courseid, $userids = []) {
 801          global $DB;
 802  
 803          $params = ['courseid' => $courseid];
 804          $where = "courseid = :courseid";
 805  
 806          if (!empty($userids)) {
 807              list($insql, $inparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
 808              $params = $params + $inparams;
 809  
 810              $where .= " AND userid {$insql}";
 811          }
 812  
 813          $DB->delete_records_select(user_competency_course::TABLE, $where, $params);
 814      }
 815  
 816      /**
 817       * Export the user data in user context.
 818       *
 819       * @param int $userid The user ID.
 820       * @param array $contexts The contexts.
 821       * @return void
 822       */
 823      protected static function export_user_data_in_user_contexts($userid, array $contexts) {
 824          global $DB;
 825  
 826          $mycontext = context_user::instance($userid);
 827          $contextids = array_map(function($context) {
 828              return $context->id;
 829          }, $contexts);
 830          $exportowncontext = in_array($mycontext->id, $contextids);
 831          $othercontexts = array_filter($contextids, function($contextid) use ($mycontext) {
 832              return $contextid != $mycontext->id;
 833          });
 834  
 835          if ($exportowncontext) {
 836              static::export_user_data_learning_plans($mycontext);
 837              static::export_user_data_competencies($mycontext);
 838              static::export_user_data_user_evidence($mycontext);
 839          }
 840  
 841          foreach ($othercontexts as $contextid) {
 842              static::export_user_data_learning_plans_related_to_me($userid, context::instance_by_id($contextid));
 843              static::export_user_data_competencies_related_to_me($userid, context::instance_by_id($contextid));
 844              static::export_user_data_user_evidence_related_to_me($userid, context::instance_by_id($contextid));
 845          }
 846      }
 847  
 848      /**
 849       * Export the user data in systen context.
 850       *
 851       * @param int $userid The user ID.
 852       * @return void
 853       */
 854      protected static function export_user_data_in_system_context($userid) {
 855          static::export_user_data_frameworks_in_context($userid, context_system::instance());
 856          static::export_user_data_templates_in_context($userid, context_system::instance());
 857      }
 858  
 859      /**
 860       * Export the user data in category contexts.
 861       *
 862       * @param int $userid The user ID.
 863       * @param array $contexts The contexts.
 864       * @return void
 865       */
 866      protected static function export_user_data_in_category_contexts($userid, array $contexts) {
 867          $contexts = array_filter($contexts, function($context) {
 868              return $context->contextlevel == CONTEXT_COURSECAT;
 869          });
 870          if (empty($contexts)) {
 871              return;
 872          }
 873  
 874          foreach ($contexts as $context) {
 875              static::export_user_data_frameworks_in_context($userid, $context);
 876              static::export_user_data_templates_in_context($userid, $context);
 877          }
 878      }
 879  
 880      /**
 881       * Export the user data in course contexts.
 882       *
 883       * @param int $userid The user whose data we're exporting.
 884       * @param array $contexts A list of contexts.
 885       * @return void
 886       */
 887      protected static function export_user_data_in_course_contexts($userid, array $contexts) {
 888          global $DB;
 889  
 890          $contexts = array_filter($contexts, function($context) {
 891              return $context->contextlevel == CONTEXT_COURSE;
 892          });
 893          if (empty($contexts)) {
 894              return;
 895          }
 896  
 897          $helper = new performance_helper();
 898          $path = [get_string('competencies', 'core_competency')];
 899          $courseids = array_map(function($context) {
 900              return $context->instanceid;
 901          }, $contexts);
 902  
 903          // Fetch all the records of competency proficiency in the course.
 904          $ffields = competency_framework::get_sql_fields('f', 'f_');
 905          $compfields = competency::get_sql_fields('c', 'c_');
 906          $uccfields = user_competency_course::get_sql_fields('ucc', 'ucc_');
 907          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
 908          list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 909          $sql = "
 910              SELECT $ffields, $compfields, $uccfields, $ctxfields
 911                FROM {" . user_competency_course::TABLE . "} ucc
 912                JOIN {" . competency::TABLE . "} c
 913                  ON c.id = ucc.competencyid
 914                JOIN {" . competency_framework::TABLE . "} f
 915                  ON f.id = c.competencyframeworkid
 916                JOIN {context} ctx
 917                  ON ctx.id = f.contextid
 918               WHERE ucc.userid = :userid
 919                 AND ucc.courseid $insql
 920            ORDER BY ucc.courseid, c.id";
 921          $params = array_merge($inparams, ['userid' => $userid]);
 922  
 923          // Export data.
 924          $recordset = $DB->get_recordset_sql($sql, $params);
 925          static::recordset_loop_and_export($recordset, 'ucc_courseid', [], function($carry, $record) use ($helper) {
 926              context_helper::preload_from_record($record);
 927              $framework = new competency_framework(null, competency_framework::extract_record($record, 'f_'));
 928              $competency = new competency(null, competency::extract_record($record, 'c_'));
 929              $ucc = new user_competency_course(null, user_competency_course::extract_record($record, 'ucc_'));
 930              $helper->ingest_framework($framework);
 931  
 932              $carry[] = array_merge(static::transform_competency_brief($competency), [
 933                  'rating' => [
 934                      'rating' => static::transform_competency_grade($competency, $ucc->get('grade'), $helper),
 935                      'proficient' => static::transform_proficiency($ucc->get('proficiency')),
 936                      'timecreated' => transform::datetime($ucc->get('timecreated')),
 937                      'timemodified' => transform::datetime($ucc->get('timemodified')),
 938                  ]
 939              ]);
 940              return $carry;
 941  
 942          }, function($courseid, $data) use ($path) {
 943              $context = context_course::instance($courseid);
 944              writer::with_context($context)->export_data($path, (object) ['ratings' => $data]);
 945          });
 946  
 947          // Export usermodified data.
 948          static::export_user_data_in_course_contexts_associations($userid, $courseids, $path);
 949          static::export_user_data_in_course_contexts_settings($userid, $courseids, $path);
 950          static::export_user_data_in_course_contexts_rated_by_me($userid, $courseids, $path, $helper);
 951      }
 952  
 953      /**
 954       * Export the ratings given in a course.
 955       *
 956       * @param int $userid The user ID.
 957       * @param array $courseids The course IDs.
 958       * @param array $path The root path.
 959       * @param performance_helper $helper The performance helper.
 960       * @return void
 961       */
 962      protected static function export_user_data_in_course_contexts_rated_by_me($userid, $courseids, $path,
 963              performance_helper $helper) {
 964          global $DB;
 965  
 966          // Fetch all the records of competency proficiency in the course.
 967          $ffields = competency_framework::get_sql_fields('f', 'f_');
 968          $compfields = competency::get_sql_fields('c', 'c_');
 969          $uccfields = user_competency_course::get_sql_fields('ucc', 'ucc_');
 970          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
 971          list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 972          $sql = "
 973              SELECT $ffields, $compfields, $uccfields, $ctxfields
 974                FROM {" . user_competency_course::TABLE . "} ucc
 975                JOIN {" . competency::TABLE . "} c
 976                  ON c.id = ucc.competencyid
 977                JOIN {" . competency_framework::TABLE . "} f
 978                  ON f.id = c.competencyframeworkid
 979                JOIN {context} ctx
 980                  ON ctx.id = f.contextid
 981               WHERE ucc.usermodified = :userid
 982                 AND ucc.courseid $insql
 983            ORDER BY ucc.courseid, ucc.id";
 984          $params = array_merge($inparams, ['userid' => $userid]);
 985  
 986          // Export the data.
 987          static::recordset_loop_and_export($DB->get_recordset_sql($sql, $params), 'ucc_courseid', [],
 988              function($carry, $record) use ($helper) {
 989                  context_helper::preload_from_record($record);
 990  
 991                  $framework = new competency_framework(null, competency_framework::extract_record($record, 'f_'));
 992                  $competency = new competency(null, competency::extract_record($record, 'c_'));
 993                  $ucc = new user_competency_course(null, user_competency_course::extract_record($record, 'ucc_'));
 994                  $helper->ingest_framework($framework);
 995  
 996                  $carry[] = array_merge(static::transform_competency_brief($competency), [
 997                      'rating' => [
 998                          'userid' => transform::user($ucc->get('userid')),
 999                          'rating' => static::transform_competency_grade($competency, $ucc->get('grade'), $helper),
1000                          'proficient' => static::transform_proficiency($ucc->get('proficiency')),
1001                          'timemodified' => transform::datetime($ucc->get('timemodified')),
1002                      ]
1003                  ]);
1004                  return $carry;
1005  
1006              }, function($courseid, $data) use ($path) {
1007                  $context = context_course::instance($courseid);
1008                  writer::with_context($context)->export_related_data($path, 'rated_by_me', (object) [
1009                      'ratings' => $data
1010                  ]);
1011              }
1012          );
1013      }
1014  
1015      /**
1016       * Export user data in course contexts related to linked competencies.
1017       *
1018       * @param int $userid The user ID.
1019       * @param array $courseids The course IDs.
1020       * @param array $path The root path to export at.
1021       * @return void
1022       */
1023      protected static function export_user_data_in_course_contexts_associations($userid, $courseids, $path) {
1024          global $DB;
1025  
1026          // Fetch all the courses with associations we created or modified.
1027          $compfields = competency::get_sql_fields('c', 'c_');
1028          $ccfields = course_competency::get_sql_fields('cc', 'cc_');
1029          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
1030          list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
1031          $sql = "
1032              SELECT $compfields, $ccfields, $ctxfields
1033                FROM {" . course_competency::TABLE . "} cc
1034                JOIN {" . competency::TABLE . "} c
1035                  ON c.id = cc.competencyid
1036                JOIN {" . competency_framework::TABLE . "} f
1037                  ON f.id = c.competencyframeworkid
1038                JOIN {context} ctx
1039                  ON ctx.id = f.contextid
1040               WHERE cc.usermodified = :userid
1041                 AND cc.courseid $insql
1042            ORDER BY cc.courseid, c.id";
1043          $params = array_merge($inparams, ['userid' => $userid]);
1044          $recordset = $DB->get_recordset_sql($sql, $params);
1045  
1046          // Export the data.
1047          static::recordset_loop_and_export($recordset, 'cc_courseid', [], function($carry, $record) {
1048              context_helper::preload_from_record($record);
1049              $competency = new competency(null, competency::extract_record($record, 'c_'));
1050              $cc = new course_competency(null, course_competency::extract_record($record, 'cc_'));
1051              $carry[] = array_merge(static::transform_competency_brief($competency), [
1052                  'timemodified' => transform::datetime($cc->get('timemodified')),
1053                  'created_or_modified_by_you' => transform::yesno(true)
1054              ]);
1055              return $carry;
1056  
1057          }, function($courseid, $data) use ($path, $userid, $DB) {
1058              $context = context_course::instance($courseid);
1059              writer::with_context($context)->export_related_data($path, 'associations', (object) ['competencies' => $data]);
1060          });
1061      }
1062  
1063      /**
1064       * Export user data in course contexts related to course settings.
1065       *
1066       * @param int $userid The user ID.
1067       * @param array $courseids The course IDs.
1068       * @param array $path The root path to export at.
1069       * @return void
1070       */
1071      protected static function export_user_data_in_course_contexts_settings($userid, $courseids, $path) {
1072          global $DB;
1073  
1074          // Fetch all the courses with associations we created or modified.
1075          $ccsfields = course_competency_settings::get_sql_fields('ccs', 'ccs_');
1076          list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
1077          $sql = "
1078              SELECT $ccsfields
1079                FROM {" . course_competency_settings::TABLE . "} ccs
1080               WHERE ccs.usermodified = :userid
1081                 AND ccs.courseid $insql
1082            ORDER BY ccs.courseid";
1083          $params = array_merge($inparams, ['userid' => $userid]);
1084          $recordset = $DB->get_recordset_sql($sql, $params);
1085  
1086          // Export the data.
1087          static::recordset_loop_and_export($recordset, 'ccs_courseid', [], function($carry, $record) {
1088              $ccs = new course_competency_settings(null, course_competency_settings::extract_record($record, 'ccs_'));
1089              return [
1090                  'timemodified' => transform::datetime($ccs->get('timemodified')),
1091                  'created_or_modified_by_you' => transform::yesno(true)
1092              ];
1093          }, function($courseid, $data) use ($path, $userid, $DB) {
1094              $context = context_course::instance($courseid);
1095              writer::with_context($context)->export_related_data($path, 'settings', (object) $data);
1096          });
1097      }
1098  
1099      /**
1100       * Export the user data in module contexts.
1101       *
1102       * @param int $userid The user whose data we're exporting.
1103       * @param array $contexts A list of contexts.
1104       * @return void
1105       */
1106      protected static function export_user_data_in_module_contexts($userid, array $contexts) {
1107          global $DB;
1108  
1109          $contexts = array_filter($contexts, function($context) {
1110              return $context->contextlevel == CONTEXT_MODULE;
1111          });
1112          if (empty($contexts)) {
1113              return;
1114          }
1115  
1116          $path = [get_string('competencies', 'core_competency')];
1117          $cmids = array_map(function($context) {
1118              return $context->instanceid;
1119          }, $contexts);
1120  
1121          // Fetch all the modules with associations we created or modified.
1122          $compfields = competency::get_sql_fields('c', 'c_');
1123          $cmcfields = course_module_competency::get_sql_fields('cmc', 'cmc_');
1124          $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
1125          list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
1126          $sql = "
1127              SELECT $compfields, $cmcfields, $ctxfields
1128                FROM {" . course_module_competency::TABLE . "} cmc
1129                JOIN {" . competency::TABLE . "} c
1130                  ON c.id = cmc.competencyid
1131                JOIN {" . competency_framework::TABLE . "} f
1132                  ON f.id = c.competencyframeworkid
1133                JOIN {context} ctx
1134                  ON ctx.id = f.contextid
1135               WHERE cmc.usermodified = :userid
1136                 AND cmc.cmid $insql
1137            ORDER BY cmc.cmid";
1138          $params = array_merge($inparams, ['userid' => $userid]);
1139  
1140          // Export the data.
1141          $recordset = $DB->get_recordset_sql($sql, $params);
1142          static::recordset_loop_and_export($recordset, 'cmc_cmid', [], function($carry, $record) {
1143              context_helper::preload_from_record($record);
1144              $competency = new competency(null, competency::extract_record($record, 'c_'));
1145              $cmc = new course_module_competency(null, course_module_competency::extract_record($record, 'cmc_'));
1146              $carry[] = array_merge(static::transform_competency_brief($competency), [
1147                  'timecreated' => transform::datetime($cmc->get('timecreated')),
1148                  'timemodified' => transform::datetime($cmc->get('timemodified')),
1149                  'created_or_modified_by_you' => transform::yesno(true)
1150              ]);
1151              return $carry;
1152  
1153          }, function($cmid, $data) use ($path) {
1154              $context = context_module::instance($cmid);
1155              writer::with_context($context)->export_data($path, (object) ['associations' => $data]);
1156          });
1157      }
1158  
1159      /**
1160       * Export a user's competencies.
1161       *
1162       * @param context_user $context The context of the user requesting the export.
1163       * @return void
1164       */
1165      protected static function export_user_data_competencies(context_user $context) {
1166          global $DB;
1167  
1168          $userid = $context->instanceid;
1169          $path = [get_string('competencies', 'core_competency'), get_string('competencies', 'core_competency')];
1170          $helper = new performance_helper();
1171          $cfields = competency::get_sql_fields('c', 'c_');
1172          $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1173          $efields = evidence::get_sql_fields('e', 'e_');
1174  
1175          $makecomppath = function($competencyid, $data) use ($path) {
1176              return array_merge($path, [$data['name'] . ' (' . $competencyid . ')']);
1177          };
1178  
1179          $sql = "
1180              SELECT $cfields, $ucfields, $efields
1181                FROM {" . user_competency::TABLE . "} uc
1182                JOIN {" . competency::TABLE . "} c
1183                  ON c.id = uc.competencyid
1184           LEFT JOIN {" . evidence::TABLE . "} e
1185                  ON uc.id = e.usercompetencyid
1186               WHERE uc.userid = :userid
1187            ORDER BY c.id, e.timecreated DESC, e.id DESC";
1188          $params = ['userid' => $userid];
1189  
1190          $recordset = $DB->get_recordset_sql($sql, $params);
1191          static::recordset_loop_and_export($recordset, 'c_id', null, function($carry, $record)
1192                  use ($context, $userid, $helper, $makecomppath) {
1193  
1194              $competency = new competency(null, competency::extract_record($record, 'c_'));
1195  
1196              if ($carry === null) {
1197                  $uc = new user_competency(null, user_competency::extract_record($record, 'uc_'));
1198                  $carry = array_merge(static::transform_competency_brief($competency), [
1199                      'rating' => static::transform_user_competency($userid, $uc, $competency, $helper),
1200                      'evidence' => []
1201                  ]);
1202                  \core_comment\privacy\provider::export_comments($context, 'competency', 'user_competency',
1203                      $uc->get('id'), $makecomppath($competency->get('id'), $carry), false);
1204              }
1205  
1206              // There is an evidence in this record.
1207              if (!empty($record->e_id)) {
1208                  $evidence = new evidence(null, evidence::extract_record($record, 'e_'));
1209                  $carry['evidence'][] = static::transform_evidence($userid, $evidence, $competency, $helper);
1210              }
1211  
1212              return $carry;
1213  
1214          }, function($competencyid, $data) use ($makecomppath, $context) {
1215              writer::with_context($context)->export_data($makecomppath($competencyid, $data), (object) $data);
1216          });
1217      }
1218  
1219      /**
1220       * Export a user's learning plans.
1221       *
1222       * @param context_user $context The context of the user requesting the export.
1223       * @return void
1224       */
1225      protected static function export_user_data_learning_plans(context_user $context) {
1226          global $DB;
1227  
1228          $userid = $context->instanceid;
1229          $path = [get_string('competencies', 'core_competency'), get_string('privacy:path:plans', 'core_competency')];
1230          $helper = new performance_helper();
1231          $pfields = plan::get_sql_fields('p', 'p_');
1232          $pcfields = plan_competency::get_sql_fields('pc', 'pc_');
1233          $cfields = competency::get_sql_fields('c', 'c_');
1234          $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1235          $ucpfields = user_competency_plan::get_sql_fields('ucp', 'ucp_');
1236  
1237          // The user's learning plans.
1238          $sql = "
1239              SELECT $pfields, $pcfields, $cfields, $ucfields, $ucpfields
1240                FROM {" . plan::TABLE . "} p
1241           LEFT JOIN {" . plan_competency::TABLE . "} pc
1242                  ON p.id = pc.planid
1243                 AND p.templateid IS NULL
1244                 AND p.status != :complete1
1245           LEFT JOIN {" . template_competency::TABLE . "} tc
1246                  ON tc.templateid = p.templateid
1247                 AND p.templateid IS NOT NULL
1248                 AND p.status != :complete2
1249           LEFT JOIN {" . user_competency_plan::TABLE . "} ucp
1250                  ON ucp.planid = p.id
1251                 AND p.status = :complete3
1252           LEFT JOIN {" . competency::TABLE . "} c
1253                  ON c.id = pc.competencyid
1254                  OR c.id = tc.competencyid
1255                  OR c.id = ucp.competencyid
1256           LEFT JOIN {" . user_competency::TABLE . "} uc
1257                  ON uc.userid = p.userid
1258                 AND (uc.competencyid = pc.competencyid OR uc.competencyid = tc.competencyid)
1259               WHERE p.userid = :userid
1260            ORDER BY p.id, c.id";
1261          $params = [
1262              'userid' => $userid,
1263              'complete1' => plan::STATUS_COMPLETE,
1264              'complete2' => plan::STATUS_COMPLETE,
1265              'complete3' => plan::STATUS_COMPLETE,
1266          ];
1267  
1268          $recordset = $DB->get_recordset_sql($sql, $params);
1269          static::recordset_loop_and_export($recordset, 'p_id', null, function($carry, $record) use ($userid, $helper, $context) {
1270              $iscomplete = $record->p_status == plan::STATUS_COMPLETE;
1271  
1272              if ($carry === null) {
1273                  $plan = new plan(null, plan::extract_record($record, 'p_'));
1274                  $options = ['context' => $context];
1275                  $carry = [
1276                      'name' => format_string($plan->get('name'), true, $options),
1277                      'description' => format_text($plan->get('description'), $plan->get('descriptionformat'), $options),
1278                      'status' => $plan->get_statusname(),
1279                      'duedate' => $plan->get('duedate') ? transform::datetime($plan->get('duedate')) : '-',
1280                      'reviewerid' => $plan->get('reviewerid') ? transform::user($plan->get('reviewerid')) : '-',
1281                      'timecreated' => transform::datetime($plan->get('timecreated')),
1282                      'timemodified' => transform::datetime($plan->get('timemodified')),
1283                      'competencies' => [],
1284                  ];
1285              }
1286  
1287              // The plan is empty.
1288              if (empty($record->c_id)) {
1289                  return $carry;
1290              }
1291  
1292              $competency = new competency(null, competency::extract_record($record, 'c_'));
1293              $rating = null;
1294  
1295              if ($iscomplete) {
1296                  // When the plan is complete, we should always found the user_competency_plan.
1297                  $ucp = new user_competency_plan(null, user_competency_plan::extract_record($record, 'ucp_'));
1298                  $rating = static::transform_user_competency($userid, $ucp, $competency, $helper);
1299  
1300              } else if (!empty($record->uc_id)) {
1301                  // When the plan is complete, there are still records of user_competency but we do not
1302                  // export them here, we export them as part of the competencies structure. The reason why
1303                  // we try to get the user_competency when the plan is not complete is to give the most accurate
1304                  // representation of the plan as possible.
1305                  $uc = new user_competency(null, user_competency::extract_record($record, 'uc_'));
1306                  $rating = static::transform_user_competency($userid, $uc, $competency, $helper);
1307              }
1308  
1309              $carry['competencies'][] = array_merge(static::transform_competency_brief($competency), ['rating' => $rating]);
1310              return $carry;
1311  
1312          }, function($planid, $data) use ($context, $path) {
1313              $planpath = array_merge($path, [$data['name'] . ' (' . $planid . ')']);
1314              \core_comment\privacy\provider::export_comments($context, 'competency', 'plan', $planid, $planpath, false);
1315              writer::with_context($context)->export_data($planpath, (object) $data);
1316          });
1317      }
1318  
1319      /**
1320       * Export a user's data related to learning plans.
1321       *
1322       * @param int $userid The user ID we're exporting for.
1323       * @param context_user $context The context of the user in which we're gathering data.
1324       * @return void
1325       */
1326      protected static function export_user_data_learning_plans_related_to_me($userid, context_user $context) {
1327          global $DB;
1328  
1329          $path = [
1330              get_string('competencies', 'core_competency'),
1331              get_string('privacy:path:relatedtome', 'core_competency'),
1332              get_string('privacy:path:plans', 'core_competency'),
1333          ];
1334          $plans = [];
1335          $helper = new performance_helper();
1336          $pfields = plan::get_sql_fields('p', 'p_');
1337          $pcfields = plan_competency::get_sql_fields('pc', 'pc_');
1338          $cfields = competency::get_sql_fields('c', 'c_');
1339          $ucpfields = user_competency_plan::get_sql_fields('ucp', 'ucp_');
1340  
1341          // Function to initialise a plan record.
1342          $initplan = function($record) use ($context, $userid, &$plans) {
1343              $plan = new plan(null, plan::extract_record($record, 'p_'));
1344              $options = ['context' => $context];
1345              $plans[$plan->get('id')] = [
1346                  'name' => format_string($plan->get('name'), true, $options),
1347                  'reviewer_is_you' => transform::yesno($plan->get('reviewerid') == $userid),
1348                  'timecreated' => transform::datetime($plan->get('timecreated')),
1349                  'timemodified' => transform::datetime($plan->get('timemodified')),
1350                  'created_or_modified_by_you' => transform::yesno($plan->get('usermodified') == $userid),
1351                  'competencies' => [],
1352              ];
1353          };
1354  
1355          $initcompetency = function($record, $planid) use (&$plans) {
1356              $competency = new competency(null, competency::extract_record($record, 'c_'));
1357              $plans[$planid]['competencies'][$competency->get('id')] = static::transform_competency_brief($competency);
1358          };
1359  
1360          // Look for associations that were created.
1361          $sql = "
1362              SELECT $pfields, $pcfields, $cfields
1363                FROM {" . plan_competency::TABLE . "} pc
1364                JOIN {" . plan::TABLE . "} p
1365                  ON p.id = pc.planid
1366                JOIN {" . competency::TABLE . "} c
1367                  ON c.id = pc.competencyid
1368               WHERE p.userid = :targetuserid
1369                 AND pc.usermodified = :userid
1370            ORDER BY p.id, c.id";
1371          $params = [
1372              'targetuserid' => $context->instanceid,
1373              'userid' => $userid,
1374          ];
1375  
1376          $recordset = $DB->get_recordset_sql($sql, $params);
1377          foreach ($recordset as $record) {
1378              $planid = $record->p_id;
1379              if (!isset($plans[$planid])) {
1380                  $initplan($record);
1381              }
1382  
1383              $initcompetency($record, $planid);
1384              $pc = new plan_competency(null, plan_competency::extract_record($record, 'pc_'));
1385              $plans[$planid]['competencies'][$pc->get('competencyid')] = array_merge(
1386                  $plans[$planid]['competencies'][$pc->get('competencyid')], [
1387                      'timemodified' => $pc->get('timemodified') ? transform::datetime($pc->get('timemodified')) : '-',
1388                      'timecreated' => $pc->get('timecreated') ? transform::datetime($pc->get('timecreated')) : '-',
1389                      'created_or_modified_by_you' => transform::yesno($pc->get('usermodified') == $userid),
1390                  ]
1391              );
1392          }
1393          $recordset->close();
1394  
1395          // Look for final grades that were given.
1396          $sql = "
1397              SELECT $pfields, $ucpfields, $cfields
1398                FROM {" . user_competency_plan::TABLE . "} ucp
1399                JOIN {" . plan::TABLE . "} p
1400                  ON p.id = ucp.planid
1401                JOIN {" . competency::TABLE . "} c
1402                  ON c.id = ucp.competencyid
1403               WHERE p.userid = :targetuserid
1404                 AND ucp.usermodified = :userid
1405            ORDER BY p.id, c.id";
1406          $params = [
1407              'targetuserid' => $context->instanceid,
1408              'userid' => $userid,
1409          ];
1410  
1411          $recordset = $DB->get_recordset_sql($sql, $params);
1412          foreach ($recordset as $record) {
1413              $planid = $record->p_id;
1414              $competencyid = $record->c_id;
1415  
1416              if (!isset($plans[$planid])) {
1417                  $initplan($record);
1418              }
1419  
1420              if (!isset($plans[$planid]['competencies'][$competencyid])) {
1421                  $initcompetency($record, $planid);
1422              }
1423  
1424              $competency = new competency(null, competency::extract_record($record, 'c_'));
1425              $ucp = new user_competency_plan(null, user_competency_plan::extract_record($record, 'ucp_'));
1426              $plans[$planid]['competencies'][$competencyid]['rating'] = static::transform_user_competency($userid, $ucp,
1427                  $competency, $helper);
1428          }
1429          $recordset->close();
1430  
1431          // Find the plans that were modified or reviewed.
1432          $insql = " > 0";
1433          $inparams = [];
1434          if (!empty($plans)) {
1435              list($insql, $inparams) = $DB->get_in_or_equal(array_keys($plans), SQL_PARAMS_NAMED, 'param', false);
1436          }
1437          $sql = "
1438              SELECT $pfields
1439                FROM {" . plan::TABLE . "} p
1440           LEFT JOIN {comments} c
1441                  ON c.contextid = :contextid
1442                 AND c.commentarea = :planarea
1443                 AND c.component = :competency
1444                 AND c.itemid = p.id
1445               WHERE p.userid = :targetuserid
1446                 AND (p.usermodified = :userid1
1447                  OR p.reviewerid = :userid2
1448                  OR c.userid = :userid3)
1449                 AND p.id $insql
1450            ORDER BY p.id";
1451          $params = array_merge($inparams, [
1452              'targetuserid' => $context->instanceid,
1453              'userid1' => $userid,
1454              'userid2' => $userid,
1455              'userid3' => $userid,
1456              'contextid' => $context->id,
1457              'planarea' => 'plan',
1458              'competency' => 'competency'
1459          ]);
1460  
1461          $recordset = $DB->get_recordset_sql($sql, $params);
1462          foreach ($recordset as $record) {
1463              $planid = $record->p_id;
1464              if (!isset($plans[$planid])) {
1465                  $initplan($record);
1466              }
1467          }
1468          $recordset->close();
1469  
1470          // Export each plan on its own.
1471          foreach ($plans as $planid => $plan) {
1472              $planpath = array_merge($path, ["{$plan['name']} ({$planid})"]);
1473              $plan['competencies'] = array_values($plan['competencies']);    // Drop the keys.
1474  
1475              writer::with_context($context)->export_data($planpath, (object) $plan);
1476              \core_comment\privacy\provider::export_comments($context, 'competency', 'plan', $planid, $planpath, true);
1477          }
1478      }
1479  
1480      /**
1481       * Export a user's data related to competencies.
1482       *
1483       * @param int $userid The user ID we're exporting for.
1484       * @param context_user $context The context of the user in which we're gathering data.
1485       * @return void
1486       */
1487      protected static function export_user_data_competencies_related_to_me($userid, context_user $context) {
1488          global $DB;
1489  
1490          $path = [
1491              get_string('competencies', 'core_competency'),
1492              get_string('privacy:path:relatedtome', 'core_competency'),
1493              get_string('competencies', 'core_competency'),
1494          ];
1495          $competencies = [];
1496          $helper = new performance_helper();
1497          $cfields = competency::get_sql_fields('c', 'c_');
1498          $ucfields = user_competency::get_sql_fields('uc', 'uc_');
1499          $efields = evidence::get_sql_fields('e', 'e_');
1500  
1501          $initcompetency = function($record) use (&$competencies) {
1502              $competency = new competency(null, competency::extract_record($record, 'c_'));
1503              $competencies[$competency->get('id')] = array_merge(static::transform_competency_brief($competency), [
1504                  'evidence' => []
1505              ]);
1506          };
1507  
1508          $initusercomp = function($competency, $record) use (&$competencies, $userid, $helper) {
1509              $competencyid = $competency->get('id');
1510              $uc = new user_competency(null, user_competency::extract_record($record, 'uc_'));
1511              $competencies[$competencyid]['uc_id'] = $uc->get('id');
1512              $competencies[$competencyid]['rating'] = static::transform_user_competency($userid, $uc, $competency, $helper);
1513          };
1514  
1515          // Look for evidence.
1516          $sql = "
1517              SELECT $efields, $ucfields, $cfields
1518                FROM {" . evidence::TABLE . "} e
1519                JOIN {" . user_competency::TABLE . "} uc
1520                  ON uc.id = e.usercompetencyid
1521                JOIN {" . competency::TABLE . "} c
1522                  ON c.id = uc.competencyid
1523               WHERE uc.userid = :targetuserid
1524                 AND (e.usermodified = :userid1
1525                  OR e.actionuserid = :userid2)
1526            ORDER BY c.id, e.id";
1527          $params = [
1528              'targetuserid' => $context->instanceid,
1529              'userid1' => $userid,
1530              'userid2' => $userid,
1531          ];
1532          $recordset = $DB->get_recordset_sql($sql, $params);
1533          foreach ($recordset as $record) {
1534              $competencyid = $record->c_id;
1535              $competency = new competency(null, competency::extract_record($record, 'c_'));
1536  
1537              if (!isset($competencies[$competencyid])) {
1538                  $initcompetency($record);
1539              }
1540  
1541              if (!array_key_exists('rating', $competencies[$competencyid])) {
1542                  $competencies[$competencyid]['rating'] = null;
1543                  if ($record->uc_reviewerid == $userid || $record->uc_usermodified == $userid) {
1544                      $initusercomp($competency, $record);
1545                  }
1546              }
1547  
1548              $evidence = new evidence(null, evidence::extract_record($record, 'e_'));
1549              $competencies[$competencyid]['evidence'][] = static::transform_evidence($userid, $evidence, $competency, $helper);
1550          }
1551          $recordset->close();
1552  
1553          // Look for user competency we modified and didn't catch.
1554          $insql = ' > 0';
1555          $inparams = [];
1556          if (!empty($competencies)) {
1557              list($insql, $inparams) = $DB->get_in_or_equal(array_keys($competencies), SQL_PARAMS_NAMED, 'param', false);
1558          }
1559          $sql = "
1560              SELECT $ucfields, $cfields
1561                FROM {" . user_competency::TABLE . "} uc
1562                JOIN {" . competency::TABLE . "} c
1563                  ON c.id = uc.competencyid
1564           LEFT JOIN {comments} cmt
1565                  ON cmt.contextid = :contextid
1566                 AND cmt.commentarea = :ucarea
1567                 AND cmt.component = :competency
1568                 AND cmt.itemid = uc.id
1569               WHERE uc.userid = :targetuserid
1570                 AND (uc.usermodified = :userid1
1571                  OR uc.reviewerid = :userid2
1572                  OR cmt.userid = :userid3)
1573                 AND uc.competencyid $insql
1574            ORDER BY c.id, uc.id";
1575          $params = array_merge($inparams, [
1576              'targetuserid' => $context->instanceid,
1577              'userid1' => $userid,
1578              'userid2' => $userid,
1579              'userid3' => $userid,
1580              'contextid' => $context->id,
1581              'ucarea' => 'user_competency',
1582              'competency' => 'competency',
1583          ]);
1584  
1585          $recordset = $DB->get_recordset_sql($sql, $params);
1586          foreach ($recordset as $record) {
1587              $competency = new competency(null, competency::extract_record($record, 'c_'));
1588              if (!isset($competencies[$competency->get('id')])) {
1589                  $initcompetency($record);
1590                  $initusercomp($competency, $record);
1591              }
1592          }
1593          $recordset->close();
1594  
1595          // Export each competency on its own.
1596          foreach ($competencies as $competencyid => $competency) {
1597              $comppath = array_merge($path, ["{$competency['name']} ({$competencyid})"]);
1598              $ucid = isset($competency['uc_id']) ? $competency['uc_id'] : null;
1599              unset($competency['uc_id']);
1600  
1601              // Send to writer.
1602              writer::with_context($context)->export_data($comppath, (object) $competency);
1603              if ($ucid) {
1604                  \core_comment\privacy\provider::export_comments($context, 'competency', 'user_competency', $ucid, $comppath, true);
1605              }
1606          }
1607      }
1608  
1609      /**
1610       * Export a user's data related to evidence of prior learning.
1611       *
1612       * @param int $userid The user ID we're exporting for.
1613       * @param context_user $context The context of the user in which we're gathering data.
1614       * @return void
1615       */
1616      protected static function export_user_data_user_evidence_related_to_me($userid, context_user $context) {
1617          global $DB;
1618  
1619          $path = [
1620              get_string('competencies', 'core_competency'),
1621              get_string('privacy:path:relatedtome', 'core_competency'),
1622              get_string('privacy:path:userevidence', 'core_competency'),
1623          ];
1624          $evidence = [];
1625          $helper = new performance_helper();
1626          $cfields = competency::get_sql_fields('c', 'c_');
1627          $uecfields = user_evidence_competency::get_sql_fields('uec', 'uec_');
1628          $uefields = user_evidence::get_sql_fields('ue', 'ue_');
1629  
1630          $initevidence = function($record) use (&$evidence, $userid) {
1631              $ue = new user_evidence(null, user_evidence::extract_record($record, 'ue_'));
1632              $evidence[$ue->get('id')] = static::transform_user_evidence($userid, $ue);
1633          };
1634  
1635          // Look for evidence.
1636          $sql = "
1637              SELECT $uefields, $uecfields, $cfields
1638                FROM {" . user_evidence_competency::TABLE . "} uec
1639                JOIN {" . user_evidence::TABLE . "} ue
1640                  ON ue.id = uec.userevidenceid
1641                JOIN {" . competency::TABLE . "} c
1642                  ON c.id = uec.competencyid
1643               WHERE ue.userid = :targetuserid
1644                 AND uec.usermodified = :userid
1645            ORDER BY ue.id, c.id";
1646          $params = [
1647              'targetuserid' => $context->instanceid,
1648              'userid' => $userid,
1649          ];
1650          $recordset = $DB->get_recordset_sql($sql, $params);
1651          foreach ($recordset as $record) {
1652              $ueid = $record->ue_id;
1653              if (!isset($evidence[$ueid])) {
1654                  $initevidence($record);
1655              }
1656  
1657              $competency = new competency(null, competency::extract_record($record, 'c_'));
1658              $uec = new user_evidence_competency(null, user_evidence_competency::extract_record($record, 'uec_'));
1659              $evidence[$ueid]['competencies'][] = array_merge(static::transform_competency_brief($competency), [
1660                  'timemodified' => $uec->get('timemodified') ? transform::datetime($uec->get('timemodified')) : '-',
1661                  'timecreated' => $uec->get('timecreated') ? transform::datetime($uec->get('timecreated')) : '-',
1662                  'created_or_modified_by_you' => transform::yesno($uec->get('usermodified'))
1663              ]);
1664          }
1665          $recordset->close();
1666  
1667          // Look for user evidence we modified or reviewed and didn't catch.
1668          $insql = ' > 0';
1669          $inparams = [];
1670          if (!empty($evidence)) {
1671              list($insql, $inparams) = $DB->get_in_or_equal(array_keys($evidence), SQL_PARAMS_NAMED, 'param', false);
1672          }
1673          $sql = "
1674              SELECT $uefields
1675                FROM {" . user_evidence::TABLE . "} ue
1676               WHERE ue.userid = :targetuserid
1677                 AND ue.usermodified = :userid
1678                 AND ue.id $insql
1679            ORDER BY ue.id";
1680          $params = array_merge($inparams, [
1681              'targetuserid' => $context->instanceid,
1682              'userid' => $userid,
1683          ]);
1684  
1685          $recordset = $DB->get_recordset_sql($sql, $params);
1686          foreach ($recordset as $record) {
1687              $initevidence($record);
1688          }
1689          $recordset->close();
1690  
1691          // Export files, then content.
1692          foreach ($evidence as $ueid => $data) {
1693              $uepath = array_merge($path, ["{$data['name']} ({$ueid})"]);
1694              writer::with_context($context)->export_area_files($uepath, 'core_competency', 'userevidence', $ueid);
1695              writer::with_context($context)->export_data($uepath, (object) $data);
1696          }
1697      }
1698  
1699      /**
1700       * Export the evidence of prior learning of a user.
1701       *
1702       * @param context_user $context The context of the user we're exporting for.
1703       * @return void
1704       */
1705      protected static function export_user_data_user_evidence(context_user $context) {
1706          global $DB;
1707  
1708          $userid = $context->instanceid;
1709          $path = [get_string('competencies', 'core_competency'), get_string('privacy:path:userevidence', 'core_competency')];
1710          $uefields = user_evidence::get_sql_fields('ue', 'ue_');
1711          $cfields = competency::get_sql_fields('c', 'c_');
1712  
1713          $sql = "
1714              SELECT $uefields, $cfields
1715                FROM {" . user_evidence::TABLE . "} ue
1716           LEFT JOIN {" . user_evidence_competency::TABLE . "} uec
1717                  ON uec.userevidenceid = ue.id
1718           LEFT JOIN {" . competency::TABLE . "} c
1719                  ON c.id = uec.competencyid
1720               WHERE ue.userid = :userid
1721            ORDER BY ue.id";
1722          $params = ['userid' => $userid];
1723  
1724          $recordset = $DB->get_recordset_sql($sql, $params);
1725          static::recordset_loop_and_export($recordset, 'ue_id', null, function($carry, $record) use ($userid, $context){
1726              if ($carry === null) {
1727                  $ue = new user_evidence(null, user_evidence::extract_record($record, 'ue_'));
1728                  $carry = static::transform_user_evidence($userid, $ue);
1729              }
1730  
1731              if (!empty($record->c_id)) {
1732                  $competency = new competency(null, competency::extract_record($record, 'c_'));
1733                  $carry['competencies'][] = static::transform_competency_brief($competency);
1734              }
1735  
1736              return $carry;
1737          }, function($ueid, $data) use ($context, $path) {
1738              $finalpath = array_merge($path, [$data['name'] . ' (' . $ueid . ')']);
1739              writer::with_context($context)->export_area_files($finalpath, 'core_competency', 'userevidence', $ueid);
1740              writer::with_context($context)->export_data($finalpath, (object) $data);
1741          });
1742      }
1743  
1744      /**
1745       * Export the user data related to frameworks in context.
1746       *
1747       * @param int $userid The user ID.
1748       * @param context $context The context.
1749       * @return void
1750       */
1751      protected static function export_user_data_frameworks_in_context($userid, context $context) {
1752          global $DB;
1753  
1754          $ffields = competency_framework::get_sql_fields('f', 'f_');
1755          $cfields = competency::get_sql_fields('c', 'c_');
1756          $c2fields = competency::get_sql_fields('c2', 'c2_');
1757          $rcfields = related_competency::get_sql_fields('rc', 'rc_');
1758  
1759          $frameworks = [];
1760          $initframework = function($record) use (&$frameworks, $userid) {
1761              $framework = new competency_framework(null, competency_framework::extract_record($record, 'f_'));
1762              $frameworks[$framework->get('id')] = array_merge(static::transform_framework_brief($framework), [
1763                  'timemodified' => transform::datetime($framework->get('timemodified')),
1764                  'created_or_modified_by_you' => transform::yesno($framework->get('usermodified') == $userid),
1765                  'competencies' => []
1766              ]);
1767          };
1768          $initcompetency = function($record, $prefix) use (&$frameworks, $userid) {
1769              $competency = new competency(null, competency::extract_record($record, $prefix));
1770              $frameworks[$competency->get('competencyframeworkid')]['competencies'][$competency->get('id')] = array_merge(
1771                  static::transform_competency_brief($competency),
1772                  [
1773                      'timemodified' => transform::datetime($competency->get('timemodified')),
1774                      'created_or_modified_by_you' => transform::yesno($competency->get('usermodified') == $userid),
1775                      'related_competencies' => []
1776                  ]
1777              );
1778          };
1779  
1780          // Start by looking for related competencies.
1781          $sql = "
1782              SELECT $ffields, $cfields, $c2fields, $rcfields
1783                FROM {" . related_competency::TABLE . "} rc
1784                JOIN {" . competency::TABLE . "} c
1785                  ON c.id = rc.competencyid
1786                JOIN {" . competency::TABLE . "} c2
1787                  ON c2.id = rc.relatedcompetencyid
1788                JOIN {" . competency_framework::TABLE . "} f
1789                  ON f.id = c.competencyframeworkid
1790               WHERE rc.usermodified = :userid
1791                 AND f.contextid = :contextid
1792            ORDER BY rc.id, c.id";
1793          $params = ['userid' => $userid, 'contextid' => $context->id];
1794  
1795          $recordset = $DB->get_recordset_sql($sql, $params);
1796          foreach ($recordset as $record) {
1797              $frameworkid = $record->f_id;
1798              $comp1id = $record->c_id;
1799              $comp2id = $record->c2_id;
1800  
1801              if (!isset($frameworks[$frameworkid])) {
1802                  $initframework($record);
1803              }
1804  
1805              foreach (['c_', 'c2_'] as $key) {
1806                  $competencyid = $record->{$key . 'id'};
1807                  if (!isset($frameworks[$frameworkid]['competencies'][$competencyid])) {
1808                      $initcompetency($record, $key);
1809                  }
1810              }
1811  
1812              $relcomp = new related_competency(null, related_competency::extract_record($record, 'rc_'));
1813              foreach (['c_' => 'c2_', 'c2_' => 'c_'] as $key => $relatedkey) {
1814                  $competencyid = $record->{$key . 'id'};
1815                  $competency = new competency(null, competency::extract_record($record, $relatedkey));
1816                  $frameworks[$frameworkid]['competencies'][$competencyid]['related_competencies'][] = [
1817                      'name' => $competency->get('shortname'),
1818                      'idnumber' => $competency->get('idnumber'),
1819                      'timemodified' => transform::datetime($relcomp->get('timemodified')),
1820                      'created_or_modified_by_you' => transform::yesno($relcomp->get('usermodified') == $userid),
1821                  ];
1822              }
1823          }
1824          $recordset->close();
1825  
1826          // Now look for competencies, but skip the ones we've already seen.
1827          $competencyids = array_reduce($frameworks, function($carry, $framework) {
1828              return array_merge($carry, array_keys($framework['competencies']));
1829          }, []);
1830          $insql = ' IS NOT NULL';
1831          $inparams = [];
1832          if (!empty($competencyids)) {
1833              list($insql, $inparams) = $DB->get_in_or_equal($competencyids, SQL_PARAMS_NAMED, 'param', false);
1834          }
1835          $sql = "
1836              SELECT $ffields, $cfields
1837                FROM {" . competency::TABLE . "} c
1838                JOIN {" . competency_framework::TABLE . "} f
1839                  ON f.id = c.competencyframeworkid
1840               WHERE c.usermodified = :userid
1841                 AND f.contextid = :contextid
1842                 AND c.id $insql
1843            ORDER BY c.id";
1844          $params = array_merge($inparams, ['userid' => $userid, 'contextid' => $context->id]);
1845          $recordset = $DB->get_recordset_sql($sql, $params);
1846          foreach ($recordset as $record) {
1847              $frameworkid = $record->f_id;
1848              if (!isset($frameworks[$frameworkid])) {
1849                  $initframework($record);
1850              }
1851              $initcompetency($record, 'c_');
1852          }
1853          $recordset->close();
1854  
1855          // Now look for frameworks, but skip the ones we've already seen.
1856          $frameworkids = array_keys($frameworks);
1857          $insql = ' IS NOT NULL';
1858          $inparams = [];
1859          if (!empty($frameworkids)) {
1860              list($insql, $inparams) = $DB->get_in_or_equal($frameworkids, SQL_PARAMS_NAMED, 'param', false);
1861          }
1862          $sql = "
1863              SELECT $ffields
1864                FROM {" . competency_framework::TABLE . "} f
1865               WHERE f.usermodified = :userid
1866                 AND f.contextid = :contextid
1867                 AND f.id $insql
1868            ORDER BY f.id";
1869          $params = array_merge($inparams, ['userid' => $userid, 'contextid' => $context->id]);
1870          $recordset = $DB->get_recordset_sql($sql, $params);
1871          foreach ($recordset as $record) {
1872              context_helper::preload_from_record($record);
1873              $initframework($record);
1874          }
1875          $recordset->close();
1876  
1877          // Export all the things!
1878          writer::with_context($context)->export_related_data(
1879              [get_string('competencies', 'core_competency')],
1880              'frameworks',
1881              (object) [
1882                  // Drop the temporary IDs.
1883                  'frameworks' => array_reduce($frameworks, function($carry, $item) {
1884                      $item['competencies'] = array_values($item['competencies']);
1885                      $carry[] = $item;
1886                      return $carry;
1887                  }, [])
1888              ]
1889          );
1890      }
1891  
1892      /**
1893       * Export the user data related to templates in contexts.
1894       *
1895       * @param int $userid The user ID.
1896       * @param context $context The context.
1897       * @return void
1898       */
1899      protected static function export_user_data_templates_in_context($userid, context $context) {
1900          global $DB;
1901  
1902          $tfields = template::get_sql_fields('t', 't_');
1903          $cfields = competency::get_sql_fields('c', 'c_');
1904          $tcfields = template_competency::get_sql_fields('tc', 'tc_');
1905          $tchfields = template_cohort::get_sql_fields('tch', 'tch_');
1906  
1907          $templates = [];
1908          $inittemplate = function($record) use (&$templates, $userid) {
1909              $template = new template(null, template::extract_record($record, 't_'));
1910              $templates[$template->get('id')] = array_merge(static::transform_template_brief($template), [
1911                  'timemodified' => transform::datetime($template->get('timemodified')),
1912                  'created_or_modified_by_you' => transform::yesno($template->get('usermodified') == $userid),
1913                  'competencies' => [],
1914                  'cohorts' => []
1915              ]);
1916          };
1917  
1918          // Find the template competencies.
1919          $sql = "
1920              SELECT $tfields, $cfields, $tcfields
1921                FROM {" . template_competency::TABLE . "} tc
1922                JOIN {" . template::TABLE . "} t
1923                  ON t.id = tc.templateid
1924                JOIN {" . competency::TABLE . "} c
1925                  ON c.id = tc.competencyid
1926               WHERE t.contextid = :contextid
1927                 AND tc.usermodified = :userid
1928            ORDER BY t.id, tc.id";
1929          $params = ['userid' => $userid, 'contextid' => $context->id];
1930          $recordset = $DB->get_recordset_sql($sql, $params);
1931          foreach ($recordset as $record) {
1932              $templateid = $record->t_id;
1933              if (!isset($templates[$templateid])) {
1934                  $inittemplate($record);
1935              }
1936              $tplcomp = new template_competency(null, template_competency::extract_record($record, 'tc_'));
1937              $competency = new competency(null, competency::extract_record($record, 'c_'));
1938              $templates[$templateid]['competencies'][] = array_merge(
1939                  static::transform_competency_brief($competency),
1940                  [
1941                      'timemodified' => transform::datetime($tplcomp->get('timemodified')),
1942                      'created_or_modified_by_you' => transform::yesno($tplcomp->get('usermodified') == $userid)
1943                  ]
1944              );
1945          }
1946          $recordset->close();
1947  
1948          // Find the template cohorts.
1949          $sql = "
1950              SELECT $tfields, $tchfields, c.name AS cohortname
1951                FROM {" . template_cohort::TABLE . "} tch
1952                JOIN {" . template::TABLE . "} t
1953                  ON t.id = tch.templateid
1954                JOIN {cohort} c
1955                  ON c.id = tch.cohortid
1956               WHERE t.contextid = :contextid
1957                 AND tch.usermodified = :userid
1958            ORDER BY t.id, tch.id";
1959          $params = ['userid' => $userid, 'contextid' => $context->id];
1960          $recordset = $DB->get_recordset_sql($sql, $params);
1961          foreach ($recordset as $record) {
1962              $templateid = $record->t_id;
1963              if (!isset($templates[$templateid])) {
1964                  $inittemplate($record);
1965              }
1966              $tplcohort = new template_cohort(null, template_cohort::extract_record($record, 'tch_'));
1967              $templates[$templateid]['cohorts'][] = [
1968                  'name' => $record->cohortname,
1969                  'timemodified' => transform::datetime($tplcohort->get('timemodified')),
1970                  'created_or_modified_by_you' => transform::yesno($tplcohort->get('usermodified') == $userid)
1971              ];
1972          }
1973          $recordset->close();
1974  
1975          // Find the modified templates which we haven't been found yet.
1976          $templateids = array_keys($templates);
1977          $insql = "IS NOT NULL";
1978          $inparams = [];
1979          if (!empty($templateids)) {
1980              list($insql, $inparams) = $DB->get_in_or_equal($templateids, SQL_PARAMS_NAMED, 'param', false);
1981          }
1982          $sql = "
1983              SELECT $tfields
1984                FROM {" . template::TABLE . "} t
1985               WHERE t.contextid = :contextid
1986                 AND t.usermodified = :userid
1987                 AND t.id $insql
1988            ORDER BY t.id";
1989          $params = array_merge($inparams, ['userid' => $userid, 'contextid' => $context->id]);
1990          $recordset = $DB->get_recordset_sql($sql, $params);
1991          foreach ($recordset as $record) {
1992              $inittemplate($record);
1993          }
1994          $recordset->close();
1995  
1996          // Export all the things!
1997          writer::with_context($context)->export_related_data([get_string('competencies', 'core_competency')],
1998              'templates', (object) ['templates' => array_values($templates)]);
1999      }
2000  
2001      /**
2002       * Transform a competency into a brief description.
2003       *
2004       * @param competency $competency The competency.
2005       * @return array
2006       */
2007      protected static function transform_competency_brief(competency $competency) {
2008          global $OUTPUT;
2009          $exporter = new \core_competency\external\competency_exporter($competency, ['context' => $competency->get_context()]);
2010          $data = $exporter->export($OUTPUT);
2011          return [
2012              'idnumber' => $data->idnumber,
2013              'name' => $data->shortname,
2014              'description' => $data->description
2015          ];
2016      }
2017  
2018      /**
2019       * Transform a competency rating.
2020       *
2021       * @param competency $competency The competency.
2022       * @param int $grade The grade.
2023       * @param performance_helper $helper The performance helper.
2024       * @return string
2025       */
2026      protected static function transform_competency_grade(competency $competency, $grade, performance_helper $helper) {
2027          if ($grade === null) {
2028              return '-';
2029          }
2030          $scale = $helper->get_scale_from_competency($competency);
2031          return $scale->scale_items[$grade - 1];
2032      }
2033  
2034      /**
2035       * Transform an evidence.
2036       *
2037       * @param int $userid The user ID we are exporting for.
2038       * @param evidence $evidence The evidence.
2039       * @param competency $competency The associated competency.
2040       * @param performance_helper $helper The performance helper.
2041       * @return array
2042       */
2043      protected static function transform_evidence($userid, evidence $evidence, competency $competency, performance_helper $helper) {
2044          $action = $evidence->get('action');
2045          $actiontxt = '?';
2046          if ($action == evidence::ACTION_LOG) {
2047              $actiontxt = get_string('privacy:evidence:action:log', 'core_competency');
2048          } else if ($action == evidence::ACTION_COMPLETE) {
2049              $actiontxt = get_string('privacy:evidence:action:complete', 'core_competency');
2050          } else if ($action == evidence::ACTION_OVERRIDE) {
2051              $actiontxt = get_string('privacy:evidence:action:override', 'core_competency');
2052          }
2053  
2054          $actionuserid = $evidence->get('actionuserid');
2055  
2056          return [
2057              'action' => $actiontxt,
2058              'actionuserid' => $actionuserid ? transform::user($actionuserid) : '-',
2059              'acting_user_is_you' => transform::yesno($userid == $actionuserid),
2060              'description' => (string) $evidence->get_description(),
2061              'url' => $evidence->get('url'),
2062              'grade' => static::transform_competency_grade($competency, $evidence->get('grade'), $helper),
2063              'note' => $evidence->get('note'),
2064              'timecreated' => transform::datetime($evidence->get('timecreated')),
2065              'timemodified' => transform::datetime($evidence->get('timemodified')),
2066              'created_or_modified_by_you' => transform::yesno($userid == $evidence->get('usermodified'))
2067          ];
2068      }
2069  
2070      /**
2071       * Transform a framework into a brief description.
2072       *
2073       * @param competency_framework $framework The framework.
2074       * @return array
2075       */
2076      protected static function transform_framework_brief(competency_framework $framework) {
2077          global $OUTPUT;
2078          $exporter = new \core_competency\external\competency_framework_exporter($framework);
2079          $data = $exporter->export($OUTPUT);
2080          return [
2081              'name' => $data->shortname,
2082              'idnumber' => $data->idnumber,
2083              'description' => $data->description
2084          ];
2085      }
2086  
2087      /**
2088       * Transform a template into a brief description.
2089       *
2090       * @param template $template The Template.
2091       * @return array
2092       */
2093      protected static function transform_template_brief(template $template) {
2094          global $OUTPUT;
2095          $exporter = new \core_competency\external\template_exporter($template);
2096          $data = $exporter->export($OUTPUT);
2097          return [
2098              'name' => $data->shortname,
2099              'description' => $data->description
2100          ];
2101      }
2102  
2103      /**
2104       * Transform proficiency.
2105       *
2106       * @param null|bool $proficiency The proficiency.
2107       * @return string
2108       */
2109      protected static function transform_proficiency($proficiency) {
2110          return $proficiency !== null ? transform::yesno($proficiency) : '-';
2111      }
2112  
2113      /**
2114       * Transform user competency.
2115       *
2116       * @param int $userid The user ID we are exporting for.
2117       * @param user_competency|user_competency_plan|user_competency_course $uc The user competency.
2118       * @param competency $competency The associated competency.
2119       * @param performance_helper $helper The performance helper.
2120       * @return array
2121       */
2122      protected static function transform_user_competency($userid, $uc, competency $competency, performance_helper $helper) {
2123          $data = [
2124              'proficient' => static::transform_proficiency($uc->get('proficiency')),
2125              'rating' => static::transform_competency_grade($competency, $uc->get('grade'), $helper),
2126              'timemodified' => $uc->get('timemodified') ? transform::datetime($uc->get('timemodified')) : '-',
2127              'timecreated' => $uc->get('timecreated') ? transform::datetime($uc->get('timecreated')) : '-',
2128              'created_or_modified_by_you' => transform::yesno($uc->get('usermodified') == $userid),
2129          ];
2130  
2131          if ($uc instanceof user_competency) {
2132              $reviewer = $uc->get('reviewerid');
2133              $data['status'] = (string) user_competency::get_status_name($uc->get('status'));
2134              $data['reviewerid'] = $reviewer ? transform::user($reviewer) : '-';
2135              $data['reviewer_is_you'] = transform::yesno($reviewer == $userid);
2136          }
2137  
2138          return $data;
2139      }
2140  
2141      /**
2142       * Transform a user evidence.
2143       *
2144       * @param int $userid The user we are exporting for.
2145       * @param user_evidence $ue The evidence of prior learning.
2146       * @return array
2147       */
2148      protected static function transform_user_evidence($userid, user_evidence $ue) {
2149          $options = ['context' => $ue->get_context()];
2150          return [
2151              'name' => format_string($ue->get('name'), true, $options),
2152              'description' => format_text($ue->get('description'), $ue->get('descriptionformat'), $options),
2153              'url' => $ue->get('url'),
2154              'timecreated' => $ue->get('timecreated') ? transform::datetime($ue->get('timecreated')) : '-',
2155              'timemodified' => $ue->get('timemodified') ? transform::datetime($ue->get('timemodified')) : '-',
2156              'created_or_modified_by_you' => transform::yesno($ue->get('usermodified') == $userid),
2157              'competencies' => []
2158          ];
2159      }
2160  
2161      /**
2162       * Loop and export from a recordset.
2163       *
2164       * @param moodle_recordset $recordset The recordset.
2165       * @param string $splitkey The record key to determine when to export.
2166       * @param mixed $initial The initial data to reduce from.
2167       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
2168       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
2169       * @return void
2170       */
2171      protected static function recordset_loop_and_export(moodle_recordset $recordset, $splitkey, $initial,
2172              callable $reducer, callable $export) {
2173  
2174          $data = $initial;
2175          $lastid = null;
2176  
2177          foreach ($recordset as $record) {
2178              if ($lastid && $record->{$splitkey} != $lastid) {
2179                  $export($lastid, $data);
2180                  $data = $initial;
2181              }
2182              $data = $reducer($data, $record);
2183              $lastid = $record->{$splitkey};
2184          }
2185          $recordset->close();
2186  
2187          if (!empty($lastid)) {
2188              $export($lastid, $data);
2189          }
2190      }
2191  }