From afafd4780d8e81f82837835811cb94035fe751f6 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Fri, 14 Mar 2025 12:50:54 +0000 Subject: [PATCH 1/7] update civix version --- info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.xml b/info.xml index 10dd804..d9f7376 100644 --- a/info.xml +++ b/info.xml @@ -25,7 +25,7 @@ Information related to the completion of Moodle units is pulled back to CiviCRM.</comments> <civix> <namespace>CRM/Civimoodle</namespace> - <format>24.09.1</format> + <format>25.01.1</format> <angularModule>crmCivimoodle</angularModule> </civix> <mixins> -- GitLab From 73ab907eb1e18afbbfd92997a0d6bc4efc632896 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 13:49:52 +0000 Subject: [PATCH 2/7] fix duplicate menu item --- civimoodle.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/civimoodle.php b/civimoodle.php index 07b7e79..415c104 100644 --- a/civimoodle.php +++ b/civimoodle.php @@ -409,20 +409,3 @@ function civimoodle_civicrm_tabset($tabsetName, &$tabs, $context) { 'weight' => 200, ]; } - -/** - * Implements hook_civicrm_navigationMenu(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_navigationMenu/ - */ -function civimoodle_civicrm_navigationMenu(&$menu) { - _civimoodle_civix_insert_navigation_menu($menu, 'Administer/System Settings', [ - 'label' => E::ts('Moodle integration'), - 'name' => 'moodle_admin', - 'url' => 'civicrm/admin/setting/moodle', - 'permission' => 'administer CiviCRM', - 'operator' => 'OR', - 'separator' => 0, - ]); - _civimoodle_civix_navigationMenu($menu); -} -- GitLab From 61a77c7259606d768dec0884fc78cda055fbbd95 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 13:50:28 +0000 Subject: [PATCH 3/7] add check course status to api helper --- CRM/Civimoodle/API.php | 49 +++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/CRM/Civimoodle/API.php b/CRM/Civimoodle/API.php index 36a70e5..6aa6a6b 100644 --- a/CRM/Civimoodle/API.php +++ b/CRM/Civimoodle/API.php @@ -100,6 +100,24 @@ class CRM_Civimoodle_API { return $this->sendRequest('enrol_manual_enrol_users'); } + /** + * Function to core_completion_get_course_completion_status from the moodle server + * + * NOTE: passing search params here is different from other functions, but the params + * are requied + * + * @param $courseId the course id to get status for + * @param $userId the user id to get status for + * + * @return array the api result + */ + public function getCourseCompletionStatus($courseId, $userId) { + return $this->sendRequest('core_completion_get_course_completion_status', [ + 'courseid' => $courseId, + 'userid' => $userId, + ]); + } + /** * Function used to make Moodle API request * @@ -108,7 +126,7 @@ class CRM_Civimoodle_API { * * @return array */ - public function sendRequest($apiFunc) { + public function sendRequest($apiFunc, array $apiParams = []) { $searchArgs = array( 'wstoken=' . $this->_wsToken, 'wsfunction=' . $apiFunc, @@ -118,30 +136,37 @@ class CRM_Civimoodle_API { switch ($apiFunc) { case 'core_user_get_users': // expects search params to be in array('key' => 'firstname', 'value' => 'Adam') format - foreach (array('key', 'value') as $arg) { + foreach (['key', 'value'] as $arg) { $searchArgs[] = "criteria[0][$arg]=" . $this->_searchParams[$arg]; } break; case 'core_user_create_users': - foreach (array('username', 'password', 'firstname', 'lastname', 'email') as $arg) { - $searchArgs[] = "users[0][$arg]=" . $this->_searchParams[$arg]; - } - break; + foreach (['username', 'password', 'firstname', 'lastname', 'email'] as $arg) { + $searchArgs[] = "users[0][$arg]=" . $this->_searchParams[$arg]; + } + break; case 'core_user_update_users': - foreach (array('id', 'firstname', 'lastname', 'email') as $arg) { - if (!empty($this->_searchParams[$arg])) { - $searchArgs[] = "users[0][$arg]=" . $this->_searchParams[$arg]; + foreach (['id', 'firstname', 'lastname', 'email'] as $arg) { + if (!empty($this->_searchParams[$arg])) { + $searchArgs[] = "users[0][$arg]=" . $this->_searchParams[$arg]; + } } - } - break; + break; case 'enrol_manual_enrol_users': - foreach (array('roleid', 'userid', 'courseid') as $arg) { + foreach (['roleid', 'userid', 'courseid'] as $arg) { $searchArgs[] = "enrolments[0][$arg]=" . $this->_searchParams[$arg]; } break; + + case 'core_completion_get_course_completion_status': + foreach (['userid', 'courseid'] as $arg) { + $searchArgs[] = "$arg=" . $apiParams[$arg]; + } + break; + default: //do nothing break; -- GitLab From 902808cf5abba1f6375f5a27f51febd8ffc9484d Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 13:50:55 +0000 Subject: [PATCH 4/7] add custom fieldset for moodle enrollments --- ...stomGroup_moodle_course_enrollment.mgd.php | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 managed/CustomGroup_moodle_course_enrollment.mgd.php diff --git a/managed/CustomGroup_moodle_course_enrollment.mgd.php b/managed/CustomGroup_moodle_course_enrollment.mgd.php new file mode 100644 index 0000000..40d9d33 --- /dev/null +++ b/managed/CustomGroup_moodle_course_enrollment.mgd.php @@ -0,0 +1,137 @@ +<?php +use CRM_Civimoodle_ExtensionUtil as E; + +return [ + [ + 'name' => 'CustomGroup_moodle_course_enrollment', + 'entity' => 'CustomGroup', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'moodle_course_enrollment', + 'title' => E::ts('Moodle Courses'), + 'style' => 'Tab with table', + 'weight' => 6, + 'is_multiple' => TRUE, + 'collapse_adv_display' => TRUE, + 'created_date' => '2025-03-20 06:11:05', + ], + 'match' => ['name'], + ], + ], + [ + 'name' => 'CustomGroup_moodle_course_enrollments_CustomField_course_id', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'moodle_course_enrollment', + 'name' => 'course_id', + 'label' => E::ts('Course'), + 'data_type' => 'Int', + 'html_type' => 'Select', + 'is_required' => TRUE, + 'in_selector' => TRUE, + 'option_group_id.name' => 'available_courses', + ], + 'match' => [ + 'name', + 'custom_group_id', + ], + ], + ], + [ + 'name' => 'CustomGroup_moodle_course_enrollments_CustomField_enroll_date', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'moodle_course_enrollment', + 'name' => 'enroll_date', + 'label' => E::ts('Enrollment Date'), + 'data_type' => 'Date', + 'html_type' => 'Select Date', + 'is_required' => TRUE, + 'date_format' => 'dd/mm/yy', + 'in_selector' => TRUE, + ], + 'match' => [ + 'name', + 'custom_group_id', + ], + ], + ], + [ + 'name' => 'CustomGroup_moodle_course_enrollments_CustomField_completion_date', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'moodle_course_enrollment', + 'name' => 'completion_date', + 'label' => E::ts('Completion Date'), + 'data_type' => 'Date', + 'html_type' => 'Select Date', + 'date_format' => 'dd/mm/yy', + 'in_selector' => TRUE, + ], + 'match' => [ + 'name', + 'custom_group_id', + ], + ], + ], + [ + 'name' => 'CustomGroup_moodle_course_enrollments_CustomField_registration_participant_id', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'moodle_course_enrollment', + 'name' => 'courseregistration_participant_id', + 'label' => E::ts('Participant ID'), + 'data_type' => 'EntityReference', + 'html_type' => 'Autocomplete-Select', + 'fk_entity' => 'Participant', + 'is_required' => TRUE, + ], + 'match' => [ + 'name', + 'custom_group_id', + ], + ], + ], + [ + 'name' => 'CustomGroup_moodle_course_enrollments_CustomField_completed_by', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'moodle_course_enrollment', + 'name' => 'completed_by', + 'label' => E::ts('Completed By'), + 'data_type' => 'EntityReference', + 'html_type' => 'Autocomplete-Select', + 'fk_entity' => 'Contact', + 'is_required' => TRUE, + 'in_selector' => TRUE, + ], + 'match' => [ + 'name', + 'custom_group_id', + ], + ], + ], +]; -- GitLab From cff1a5417c84285ad5b60f7962222ead6a5ac517 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 14:32:45 +0000 Subject: [PATCH 5/7] add hook to create enrollments when participants are registered --- CRM/Civimoodle/Util.php | 16 ++++++++++++++++ civimoodle.php | 24 +++++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CRM/Civimoodle/Util.php b/CRM/Civimoodle/Util.php index fe29177..869af75 100644 --- a/CRM/Civimoodle/Util.php +++ b/CRM/Civimoodle/Util.php @@ -203,4 +203,20 @@ class CRM_Civimoodle_Util { } return $options; } + + public static function createLocalEnrollmentRecords(int $contactID, array $courses, int $participantID, $date = 'now') { + $enrollmentCreate = \Civi\Api4\CustomValue::save('moodle_course_enrollment', FALSE); + + foreach ($courses as $course) { + $enrollmentCreate->addRecord([ + 'course_id' => $course, + 'entity_id' => $contactID, + 'registration_participant_id' => $participantID, + 'enroll_date' => $date, + ]); + } + + $enrollmentCreate->execute(); + } + } diff --git a/civimoodle.php b/civimoodle.php index 415c104..6ca14d8 100644 --- a/civimoodle.php +++ b/civimoodle.php @@ -53,14 +53,15 @@ function civimoodle_civicrm_container(\Symfony\Component\DependencyInjection\Con * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_fieldOptions */ function civimoodle_civicrm_fieldOptions($entity, $field, &$options, $params) { - if ($entity == 'Event') { - // TODO: This backward-compat for legacy custom field names can be removed when dropping support for CiviCRM < 5.79 - if ($field == 'moodle_courses.courses' || $field == CRM_Civimoodle_Util::getCustomFieldKey('courses')) { - // fetch available Moodle courses in array('id' => 'fullname') format - $courses = CRM_Civimoodle_Util::getAvailableCourseNames(); - if (isset($courses) && count($courses)) { - $options = $courses; - } + // TODO: This backward-compat for legacy custom field names can be removed when dropping support for CiviCRM < 5.79 + if ( + (($entity === 'Event') && ($field == 'moodle_courses.courses' || $field == CRM_Civimoodle_Util::getCustomFieldKey('courses'))) + || ($field === 'moodle_course_enrollment.course_id') + ) { + // fetch available Moodle courses in array('id' => 'fullname') format + $courses = CRM_Civimoodle_Util::getAvailableCourseNames(); + if (isset($courses) && count($courses)) { + $options = $courses; } } } @@ -116,6 +117,11 @@ function civimoodle_civicrm_post($op, $objectName, $objectId, &$objectRef) { // fetch courses from given event ID $courses = CRM_Civimoodle_Util::getCoursesFromEvent($objectRef->event_id); if (isset($courses) && count($courses) > 0) { + + \CRM_Civimoodle_Util::createLocalEnrollmentRecords($objectRef->contact_id, $courses, $objectId, $objectRef->register_date); + + // now register the partipant at Moodle + //Changed 'user_load' to directory '\Drupal\user\Entity\User' for Drupal 10 compatibility if (Civi::settings()->get('moodle_cms_credential')) { $user = \Drupal\user\Entity\User::load($ufID); @@ -322,7 +328,7 @@ function updateCMSUserDetails($ufID, $contactParams, $create = FALSE){ return get_userdata($ufID); break; - + /* For Joomla implementation when needed case 'Joomla': break; -- GitLab From 3054ec94e5762946926432c8c6018e7437d4f1d3 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 14:34:58 +0000 Subject: [PATCH 6/7] add upgrader for backfilling moodle registration records --- CRM/Civimoodle/Upgrader.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CRM/Civimoodle/Upgrader.php b/CRM/Civimoodle/Upgrader.php index 9836fe6..50ad961 100644 --- a/CRM/Civimoodle/Upgrader.php +++ b/CRM/Civimoodle/Upgrader.php @@ -32,4 +32,30 @@ class CRM_Civimoodle_Upgrader extends CRM_Extension_Upgrader_Base { return TRUE; } + /** + * Add enrollment records for pre-existing registrations + */ + public function upgrade_1010() { + $participants = \Civi\Api4\Participant::get(FALSE) + ->addSelect('id', 'contact_id', 'moodle_courses.courses', 'register_date') + ->addWhere('moodle_courses.courses', 'IS NOT EMPTY') + ->execute(); + + foreach ($participants as $participant) { + try { + \CRM_Civimoodle_Util::createLocalEnrollmentRecords( + $participant['contact_id'], + $participant['moodle_courses.courses'], + $participant['id'], + $participant['register_date'] + ); + } + catch (\Exception $e) { + \Civi::log()->debug('Error creating Moodle registration records for participant ID ' . $participant['id']); + } + } + + return TRUE; + } + } -- GitLab From 44d738c311b8ca18615a3c417355cdfe7f0bb8c9 Mon Sep 17 00:00:00 2001 From: ben <ben.walpole@jmaconsulting.biz> Date: Thu, 20 Mar 2025 13:52:46 +0000 Subject: [PATCH 7/7] add job to fetch course completions --- CRM/Civimoodle/FetchCourseCompletions.php | 101 ++++++++++++++++++++++ api/v3/Job/Fetchcoursecompletions.mgd.php | 21 +++++ api/v3/Job/Fetchcoursecompletions.php | 35 ++++++++ 3 files changed, 157 insertions(+) create mode 100644 CRM/Civimoodle/FetchCourseCompletions.php create mode 100644 api/v3/Job/Fetchcoursecompletions.mgd.php create mode 100644 api/v3/Job/Fetchcoursecompletions.php diff --git a/CRM/Civimoodle/FetchCourseCompletions.php b/CRM/Civimoodle/FetchCourseCompletions.php new file mode 100644 index 0000000..ebaf08b --- /dev/null +++ b/CRM/Civimoodle/FetchCourseCompletions.php @@ -0,0 +1,101 @@ +<?php + +class CRM_Civimoodle_FetchCourseCompletions { + + public static function run() { + $results = []; + $counts = [ + 'completed' => 0, + 'errors' => 0, + ]; + + $enrollmentsToCheck = \Civi\Api4\CustomValue::get('moodle_course_enrollment', FALSE) + ->addWhere('completion_date', 'IS EMPTY') + ->addSelect('id', 'course_id', 'entity_id', 'entity_id.moodle_credential.user_id') + ->execute(); + + foreach ($enrollmentsToCheck as $enrollment) { + $result = self::checkAndUpdateEnrollment($enrollment); + $results[] = $result; + + if ($result['is_complete']) { + $counts['completed'] += 1; + } + if ($result['is_error']) { + $counts['errors'] += 1; + } + } + + // add the total count + $counts['checked'] = count($results); + + return [ + 'records' => $results, + 'counts' => $counts, + ]; + } + + protected static function checkAndUpdateEnrollment(array $enrollment) { + $enrollmentId = $enrollment['id']; + $courseId = $enrollment['course_id']; + $moodleUserId = $enrollment['entity_id.moodle_credential.user_id']; + + try { + $remoteDetails = \CRM_Civimoodle_API::singleton()->getCourseCompletionStatus($courseId, $moodleUserId); + + $parsedDetails = json_decode($remoteDetails[1], TRUE)['completionstatus']; + + if ($parsedDetails['completed'] ?? FALSE) { + + $completionDate = self::extractCompletionDate($parsedDetails['completions'] ?? []) ?? 'now'; + + // unfortunately moodle api doesn't seem to provide a user + // $completedBy = self::getCompletedBy($parsedDetails['completions']); + + $updateEnrollment = \Civi\Api4\CustomValue::update('moodle_course_enrollment', FALSE) + ->addWhere('id', '=', $enrollmentId) + ->addValue('completion_date', $completionDate); + + if ($completedBy) { + $updateEnrollment->addValue('completed_by', $completedBy); + } + + $updateEnrollment->execute(); + + return [ + 'id' => $enrollmentId, + 'is_complete' => TRUE, + 'is_error' => FALSE, + ]; + } + + return [ + 'id' => $enrollmentId, + 'is_complete' => FALSE, + 'is_error' => FALSE, + ]; + } + catch (\Exception $e) { + return [ + 'id' => $enrollment, + 'is_error' => TRUE, + 'error_message' => $e->getMessage(), + 'is_complete' => FALSE, + ]; + } + } + + /** + * Try to find a completion date in the Moodle API response + */ + protected static function extractCompletionDate(array $completions) { + foreach ($completions as $completion) { + if ($completion['timecompleted']) { + return $completion['timecompleted']; + } + } + + return NULL; + } + +} diff --git a/api/v3/Job/Fetchcoursecompletions.mgd.php b/api/v3/Job/Fetchcoursecompletions.mgd.php new file mode 100644 index 0000000..4a9c354 --- /dev/null +++ b/api/v3/Job/Fetchcoursecompletions.mgd.php @@ -0,0 +1,21 @@ +<?php + +// This file declares a managed database record of type "Job". +// The record will be automatically inserted, updated, or deleted from the +// database as appropriate. For more details, see "hook_civicrm_managed" at: +// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed +return [ + [ + 'name' => 'Cron:Job.Fetchcoursecompletions', + 'entity' => 'Job', + 'params' => [ + 'version' => 3, + 'name' => 'Call Job.Fetchcoursecompletions API', + 'description' => 'Call Job.Fetchcoursecompletions API', + 'run_frequency' => 'Daily', + 'api_entity' => 'Job', + 'api_action' => 'Fetchcoursecompletions', + 'parameters' => '', + ], + ], +]; diff --git a/api/v3/Job/Fetchcoursecompletions.php b/api/v3/Job/Fetchcoursecompletions.php new file mode 100644 index 0000000..8cbfb91 --- /dev/null +++ b/api/v3/Job/Fetchcoursecompletions.php @@ -0,0 +1,35 @@ +<?php +use CRM_Civimoodle_ExtensionUtil as E; + +/** + * Job.Updatecoursestatus API specification (optional) + * This is used for documentation and validation. + * + * @param array $spec description of fields supported by this API call + * + * @see https://docs.civicrm.org/dev/en/latest/framework/api-architecture/ + */ +function _civicrm_api3_job_Fetchcoursecompletions_spec(&$spec) { + // $spec['magicword']['api.required'] = 1; +} + +/** + * Job.Updatecoursestatus API + * + * @param array $params + * + * @return array + * API result descriptor + * + * @see civicrm_api3_create_success + * + * @throws CRM_Core_Exception + */ +function civicrm_api3_job_Fetchcoursecompletions($params) { + $result = \CRM_Civimoodle_FetchCourseCompletions::run(); + + if ($result['counts']['errors']) { + return civicrm_api3_create_error($result, $params, 'Job', 'Updatecoursestatus'); + } + return civicrm_api3_create_success($result, $params, 'Job', 'Updatecoursestatus'); +} -- GitLab