diff --git a/includes/batch-enrollment.php b/includes/batch-enrollment.php new file mode 100644 index 0000000..0d51203 --- /dev/null +++ b/includes/batch-enrollment.php @@ -0,0 +1,282 @@ +post_type !== $post_type || $post->post_status !== 'publish' ) { + return; + } + + // Get the level IDs currently associated with this course. + $current_levels = self::get_level_ids_for_post( $post_id ); + if ( empty( $current_levels ) ) { + return; + } + + // Get level IDs we have already processed for retroactive enrollment. + $processed_levels = get_post_meta( $post_id, self::PROCESSED_LEVELS_META, true ); + if ( ! is_array( $processed_levels ) ) { + $processed_levels = array(); + } + + // Only act on newly associated levels. + $new_levels = array_values( array_diff( array_map( 'intval', $current_levels ), array_map( 'intval', $processed_levels ) ) ); + if ( empty( $new_levels ) ) { + return; + } + + self::schedule( $post_id, $new_levels, $module_slug ); + + // Mark all current levels as processed so future saves don't re-queue. + update_post_meta( $post_id, self::PROCESSED_LEVELS_META, array_map( 'intval', $current_levels ) ); + } + + /** + * Schedule the first batch task for a course. + * + * @param int $course_id Course post ID. + * @param array $level_ids Level IDs to enroll members from. + * @param string $module_slug Module slug. + */ + public static function schedule( $course_id, $level_ids, $module_slug ) { + if ( empty( $level_ids ) || empty( $course_id ) ) { + return; + } + + if ( ! class_exists( 'PMPro_Action_Scheduler' ) || ! function_exists( 'as_enqueue_async_action' ) ) { + error_log( 'PMPro Courses: Action Scheduler not available — retroactive enrollment disabled. Requires PMPro 3.5+.' ); + return; + } + + PMPro_Action_Scheduler::instance()->maybe_add_task( + self::AS_HOOK, + array( + array( + 'course_id' => $course_id, + 'level_ids' => $level_ids, + 'module_slug' => $module_slug, + 'offset' => 0, + ), + ), + self::AS_GROUP, + null, + true // run_asap — enqueue for immediate async execution. + ); + } + + /** + * Action Scheduler callback — unwraps task data and runs one batch. + * + * @param array $data Task data array with keys: course_id, level_ids, module_slug, offset. + */ + public static function process_batch_task( $data ) { + if ( empty( $data['course_id'] ) || empty( $data['level_ids'] ) || empty( $data['module_slug'] ) ) { + return; + } + + self::process_batch( + (int) $data['course_id'], + (array) $data['level_ids'], + (string) $data['module_slug'], + (int) $data['offset'] + ); + } + + /** + * Process one batch of enrollments. + * + * Fires `pmpro_courses_{module_slug}_retroactive_enroll_user` for each + * user in the batch. If the batch is full, chains the next batch as a + * new async task. + * + * @param int $course_id + * @param array $level_ids + * @param string $module_slug + * @param int $offset + */ + public static function process_batch( $course_id, $level_ids, $module_slug, $offset ) { + $user_ids = self::get_active_members( $level_ids, self::BATCH_SIZE, $offset ); + + if ( empty( $user_ids ) ) { + return; + } + + foreach ( $user_ids as $user_id ) { + /** + * Fires to enroll a single user in a course retroactively. + * + * Each LMS module hooks into this to perform its own enrollment call. + * The hook name is: pmpro_courses_{module_slug}_retroactive_enroll_user + * + * @param int $user_id The user to enroll. + * @param int $course_id The course to enroll them in. + */ + do_action( "pmpro_courses_{$module_slug}_retroactive_enroll_user", (int) $user_id, (int) $course_id ); + } + + // If the batch was full there may be more users — chain the next batch. + if ( count( $user_ids ) === self::BATCH_SIZE && function_exists( 'as_enqueue_async_action' ) ) { + as_enqueue_async_action( + self::AS_HOOK, + array( + array( + 'course_id' => $course_id, + 'level_ids' => $level_ids, + 'module_slug' => $module_slug, + 'offset' => $offset + self::BATCH_SIZE, + ), + ), + self::AS_GROUP + ); + } + } + + /** + * Schedule retroactive enrollment for all published course posts daily. + */ + public static function schedule_daily_retroactive_enrollment() { + $module_post_types = get_option( 'pmpro_courses_modules', array() ); + + foreach ( $module_post_types as $module_slug => $post_type ) { + if ( ! pmpro_courses_is_module_active( $module_slug ) ) { + continue; + } + + foreach ( self::get_published_course_ids_for_post_type( $post_type ) as $course_id ) { + self::maybe_schedule_for_course( $course_id, $post_type, $module_slug ); + } + } + } + + /** + * Get published course post IDs for the given post type. + * + * @param string $post_type + * @return array + */ + private static function get_published_course_ids_for_post_type( $post_type ) { + global $wpdb; + + return $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT p.ID + FROM {$wpdb->posts} p + INNER JOIN {$wpdb->pmpro_memberships_pages} mp ON mp.page_id = p.ID + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND p.post_modified >= %s", + $post_type, + gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) ) + ) + ); + } + + /** + * Get the PMPro membership level IDs associated with a post. + * + * @param int $post_id + * @return array Level IDs (integers as strings from DB). + */ + public static function get_level_ids_for_post( $post_id ) { + global $wpdb; + + return $wpdb->get_col( + $wpdb->prepare( + "SELECT membership_id FROM {$wpdb->pmpro_memberships_pages} WHERE page_id = %d", + $post_id + ) + ); + } + + /** + * Get active member user IDs for a set of level IDs, paginated. + * + * @param array $level_ids + * @param int $limit + * @param int $offset + * @return array User IDs. + */ + private static function get_active_members( $level_ids, $limit, $offset ) { + global $wpdb; + + if ( empty( $level_ids ) ) { + return array(); + } + + $level_ids = array_map( 'intval', $level_ids ); + sort( $level_ids ); + $placeholders = implode( ', ', array_fill( 0, count( $level_ids ), '%d' ) ); + $args = array_merge( $level_ids, array( $limit, $offset ) ); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql = "SELECT DISTINCT user_id + FROM {$wpdb->pmpro_memberships_users} + WHERE membership_id IN ($placeholders) + AND status = 'active' + ORDER BY user_id + LIMIT %d OFFSET %d"; + + return $wpdb->get_col( $wpdb->prepare( $sql, $args ) ); + } +} + +if ( class_exists( 'PMPro_Action_Scheduler' ) && function_exists( 'as_enqueue_async_action' ) ) { + PMPro_Courses_Batch_Enrollment::init(); +} diff --git a/includes/common.php b/includes/common.php index 3f557be..63fee62 100644 --- a/includes/common.php +++ b/includes/common.php @@ -423,7 +423,7 @@ function pmpro_courses_get_lessons_html( $course_id ) { * Get the lessons dropdown HTML with all PMPro lessons that are "available" * This is used for the the lesson settings. * - * @since TBD + * @since 2.0 * */ function pmpro_courses_lessons_settings( $exclude_lessons = array(), $parent_id = 0 ) { diff --git a/includes/courses.php b/includes/courses.php index 25d72e0..e07ae94 100644 --- a/includes/courses.php +++ b/includes/courses.php @@ -134,7 +134,7 @@ function pmpro_courses_update_course_callback() { /** * Ajax function to allow creation and assignment of a draft lesson. * - * @since TBD + * @since 2.0 */ function pmpro_courses_create_lesson_cb() { diff --git a/includes/lessons.php b/includes/lessons.php index 6959663..fd64839 100644 --- a/includes/lessons.php +++ b/includes/lessons.php @@ -145,7 +145,7 @@ function pmpro_courses_lessons_pre_get_posts_table_sorting( $query ) { /** * Add a "Course" dropdown filter to the Lessons list table. * - * @since TBD + * @since 2.0 */ function pmpro_courses_lessons_filter_dropdown() { // Only show on the pmpro_lesson list screen. @@ -184,7 +184,7 @@ function pmpro_courses_lessons_filter_dropdown() { /** * Apply the Course filter to the Lessons query based on the dropdown. * - * @since TBD + * @since 2.0 */ function pmpro_courses_lessons_filter_query( WP_Query $query ) { if ( ! is_admin() || ! $query->is_main_query() ) { @@ -207,7 +207,7 @@ function pmpro_courses_lessons_filter_query( WP_Query $query ) { /** * Bypass any level restrictions for a PMPro Lesson CPT and mark it as "Free/Public" * - * @since TBD + * @since 2.0 */ function pmpro_lessons_bypass_check($hasaccess, $post, $user, $levels) { diff --git a/includes/modules/default.php b/includes/modules/default.php index dcfd6e5..38a6e15 100644 --- a/includes/modules/default.php +++ b/includes/modules/default.php @@ -113,7 +113,7 @@ public function init_active() { /** * Load the Member Edit Panel if the class exists. * - * @since TBD + * @since 2.0 */ static public function pmpro_courses_pmpro_member_edit_panels( $panels ) { diff --git a/includes/modules/learndash.php b/includes/modules/learndash.php index 3d84c7a..06cd84a 100644 --- a/includes/modules/learndash.php +++ b/includes/modules/learndash.php @@ -30,7 +30,11 @@ public function init_active() { add_filter( 'pmpro_has_membership_access_filter', array( 'PMPro_Courses_LearnDash', 'pmpro_has_membership_access_filter' ), 10, 4 ); add_action( 'template_redirect', array( 'PMPro_Courses_LearnDash', 'template_redirect' ) ); add_filter( 'pmpro_membership_content_filter', array( 'PMPro_Courses_LearnDash', 'pmpro_membership_content_filter' ), 10, 2 ); - add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_LearnDash', 'pmpro_after_all_membership_level_changes' ) ); + add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_LearnDash', 'pmpro_after_all_membership_level_changes' ) ); + + // Retroactive batch enrollment when a course is published with level associations. + add_action( 'save_post', array( 'PMPro_Courses_LearnDash', 'on_course_save' ), 20 ); + add_action( 'pmpro_courses_learndash_retroactive_enroll_user', array( 'PMPro_Courses_LearnDash', 'retroactive_enroll_user' ), 10, 2 ); } /** @@ -207,6 +211,32 @@ public static function pmpro_membership_content_filter( $filtered_content, $orig } } + /** + * Trigger retroactive enrollment when a LearnDash course is saved as published. + * + * Runs at save_post priority 20 so PMPro has already persisted level associations. + * + * @param int $post_id The saved post ID. + */ + public static function on_course_save( $post_id ) { + PMPro_Courses_Batch_Enrollment::maybe_schedule_for_course( $post_id, 'sfwd-courses', 'learndash' ); + } + + /** + * Enroll a single user in a LearnDash course during retroactive batch processing. + * + * @param int $user_id User to enroll. + * @param int $course_id LearnDash course post ID. + */ + public static function retroactive_enroll_user( $user_id, $course_id ) { + if ( ! ld_course_check_user_access( $course_id, $user_id ) ) { + $result = ld_update_course_access( $user_id, $course_id ); + if ( ! $result ) { + error_log( sprintf( 'PMPro Courses (LearnDash): Failed to enroll user %d in course %d.', $user_id, $course_id ) ); + } + } + } + /** * Get courses associated with a level. */ diff --git a/includes/modules/lifterlms.php b/includes/modules/lifterlms.php index 7c3070a..b3e7834 100644 --- a/includes/modules/lifterlms.php +++ b/includes/modules/lifterlms.php @@ -29,6 +29,10 @@ public function init_active() { add_filter( 'pmpro_membership_content_filter', array( 'PMPro_Courses_LifterLMS', 'pmpro_membership_content_filter' ), 10, 2 ); add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_LifterLMS', 'pmpro_after_all_membership_level_changes' ) ); + + // Retroactive batch enrollment when a course is published with level associations. + add_action( 'save_post', array( 'PMPro_Courses_LifterLMS', 'on_course_save' ), 20 ); + add_action( 'pmpro_courses_lifterlms_retroactive_enroll_user', array( 'PMPro_Courses_LifterLMS', 'retroactive_enroll_user' ), 10, 2 ); } /** @@ -86,37 +90,63 @@ public static function pmpro_membership_content_filter( $filtered_content, $orig } } + /** + * Trigger retroactive enrollment when a LifterLMS course is saved as published. + * + * Runs at save_post priority 20 so PMPro has already persisted level associations. + * + * @param int $post_id The saved post ID. + */ + public static function on_course_save( $post_id ) { + PMPro_Courses_Batch_Enrollment::maybe_schedule_for_course( $post_id, 'course', 'lifterlms' ); + } + + /** + * Enroll a single user in a LifterLMS course during retroactive batch processing. + * + * @param int $user_id User to enroll. + * @param int $course_id LifterLMS course post ID. + */ + public static function retroactive_enroll_user( $user_id, $course_id ) { + if ( ! llms_is_user_enrolled( $user_id, $course_id ) ) { + $result = llms_enroll_student( $user_id, $course_id ); + if ( ! $result ) { + error_log( sprintf( 'PMPro Courses (LifterLMS): Failed to enroll user %d in course %d.', $user_id, $course_id ) ); + } + } + } + /** * Get courses associated with a level. */ public static function get_courses_for_levels( $level_ids ) { global $wpdb; - + // In case a level object was passed in. if ( is_object( $level_ids ) ) { $level_ids = $level_ids->ID; } - + // Make sure we have an array of ids. if ( ! is_array( $level_ids ) ) { $level_ids = array( $level_ids ); } - + if ( empty( $level_ids ) ) { return array(); } - + $sql = " - SELECT mp.page_id - FROM $wpdb->pmpro_memberships_pages mp - LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID - WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") - AND p.post_type = 'course' - AND p.post_status = 'publish' + SELECT mp.page_id + FROM $wpdb->pmpro_memberships_pages mp + LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID + WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") + AND p.post_type = 'course' + AND p.post_status = 'publish' GROUP BY mp.page_id "; $course_ids = $wpdb->get_col( call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $level_ids ) ) ); - + return $course_ids; } diff --git a/includes/modules/senseilms.php b/includes/modules/senseilms.php index 6796d7f..fc654ad 100644 --- a/includes/modules/senseilms.php +++ b/includes/modules/senseilms.php @@ -35,6 +35,10 @@ public function init_active() { add_action( 'template_redirect', array( 'PMPro_Courses_SenseiLMS', 'template_redirect' ) ); add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_SenseiLMS', 'pmpro_after_all_membership_level_changes' ) ); + + // Retroactive batch enrollment when a course is published with level associations. + add_action( 'save_post', array( 'PMPro_Courses_SenseiLMS', 'on_course_save' ), 20 ); + add_action( 'pmpro_courses_senseilms_retroactive_enroll_user', array( 'PMPro_Courses_SenseiLMS', 'retroactive_enroll_user' ), 10, 2 ); } /** @@ -222,6 +226,33 @@ public static function pmpro_membership_content_filter( $filtered_content, $orig } } + /** + * Trigger retroactive enrollment when a Sensei LMS course is saved as published. + * + * Runs at save_post priority 20 so PMPro has already persisted level associations. + * + * @param int $post_id The saved post ID. + */ + public static function on_course_save( $post_id ) { + PMPro_Courses_Batch_Enrollment::maybe_schedule_for_course( $post_id, 'course', 'senseilms' ); + } + + /** + * Enroll a single user in a Sensei LMS course during retroactive batch processing. + * + * @param int $user_id User to enroll. + * @param int $course_id Sensei course post ID. + */ + public static function retroactive_enroll_user( $user_id, $course_id ) { + if ( ! Sensei_Course::is_user_enrolled( $course_id, $user_id ) ) { + $manual_enrolment_provider = Sensei_Course_Enrolment_Manager::instance()->get_manual_enrolment_provider(); + $result = $manual_enrolment_provider->enrol_learner( $user_id, $course_id ); + if ( ! $result ) { + error_log( sprintf( 'PMPro Courses (Sensei): Failed to enroll user %d in course %d.', $user_id, $course_id ) ); + } + } + } + /** * Get courses associated with a level. */ @@ -243,12 +274,12 @@ public static function get_courses_for_levels( $level_ids ) { } $sql = " - SELECT mp.page_id - FROM $wpdb->pmpro_memberships_pages mp - LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID - WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") - AND p.post_type = 'course' - AND p.post_status = 'publish' + SELECT mp.page_id + FROM $wpdb->pmpro_memberships_pages mp + LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID + WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") + AND p.post_type = 'course' + AND p.post_status = 'publish' GROUP BY mp.page_id "; $course_ids = $wpdb->get_col( call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $level_ids ) ) ); diff --git a/includes/modules/tutorlms.php b/includes/modules/tutorlms.php index 835cc5e..bcd6769 100644 --- a/includes/modules/tutorlms.php +++ b/includes/modules/tutorlms.php @@ -34,7 +34,11 @@ public function init_active() { add_filter( 'pmpro_membership_content_filter', array( 'PMPro_Courses_TutorLMS', 'pmpro_membership_content_filter' ), 10, 2 ); add_action( 'template_redirect', array( 'PMPro_Courses_TutorLMS', 'template_redirect' ) ); - add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_TutorLMS', 'pmpro_after_all_membership_level_changes' ) ); + add_action( 'pmpro_after_all_membership_level_changes', array( 'PMPro_Courses_TutorLMS', 'pmpro_after_all_membership_level_changes' ) ); + + // Retroactive batch enrollment when a course is published with level associations. + add_action( 'save_post', array( 'PMPro_Courses_TutorLMS', 'on_course_save' ), 20 ); + add_action( 'pmpro_courses_tutorlms_retroactive_enroll_user', array( 'PMPro_Courses_TutorLMS', 'retroactive_enroll_user' ), 10, 2 ); } /** @@ -226,37 +230,63 @@ public static function pmpro_membership_content_filter( $filtered_content, $orig return $filtered_content; // In case we don't get here. } + /** + * Trigger retroactive enrollment when a Tutor LMS course is saved as published. + * + * Runs at save_post priority 20 so PMPro has already persisted level associations. + * + * @param int $post_id The saved post ID. + */ + public static function on_course_save( $post_id ) { + PMPro_Courses_Batch_Enrollment::maybe_schedule_for_course( $post_id, 'courses', 'tutorlms' ); + } + + /** + * Enroll a single user in a Tutor LMS course during retroactive batch processing. + * + * @param int $user_id User to enroll. + * @param int $course_id Tutor LMS course post ID. + */ + public static function retroactive_enroll_user( $user_id, $course_id ) { + if ( ! tutor_utils()->is_enrolled( $course_id, $user_id ) ) { + $result = tutor_utils()->do_enroll( $user_id, 0, $course_id ); + if ( ! $result ) { + error_log( sprintf( 'PMPro Courses (TutorLMS): Failed to enroll user %d in course %d.', $user_id, $course_id ) ); + } + } + } + /** * Get courses associated with a level. */ public static function get_courses_for_levels( $level_ids ) { global $wpdb; - + // In case a level object was passed in. if ( is_object( $level_ids ) ) { $level_ids = $level_ids->ID; } - + // Make sure we have an array of ids. if ( ! is_array( $level_ids ) ) { $level_ids = array( $level_ids ); } - + if ( empty( $level_ids ) ) { return array(); } - + $sql = " - SELECT mp.page_id - FROM $wpdb->pmpro_memberships_pages mp - LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID - WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") - AND p.post_type = 'courses' - AND p.post_status = 'publish' + SELECT mp.page_id + FROM $wpdb->pmpro_memberships_pages mp + LEFT JOIN $wpdb->posts p ON mp.page_id = p.ID + WHERE mp.membership_id IN(".implode(', ', array_fill(0, count($level_ids), '%s')).") + AND p.post_type = 'courses' + AND p.post_status = 'publish' GROUP BY mp.page_id "; $course_ids = $wpdb->get_col( call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $level_ids ) ) ); - + return $course_ids; } diff --git a/includes/post-types/courses.php b/includes/post-types/courses.php index 620e87c..3e6163a 100644 --- a/includes/post-types/courses.php +++ b/includes/post-types/courses.php @@ -94,7 +94,7 @@ function pmpro_courses_course_cpt_define_meta_boxes() { /** * Always show the "Course Outline" metabox on a page for PMPro Courses as it's required. * - * @since TBD + * @since 2.0 */ function pmpro_courses_unhide_course_outline_meta_box( $hidden, $screen ) { if ( $screen->post_type == 'pmpro_course' ) { @@ -205,7 +205,7 @@ function pmpro_courses_get_lessons_table_html( $lessons, $section_id = 1 ){ * Save PMPro Course sections + lessons as a normalized array. * Runs only when saving the pmpro_course post type. * - * @since TBD + * @since 2.0 */ function pmpro_courses_save_course_sections( $post_id, $post, $update ) { diff --git a/includes/progress.php b/includes/progress.php index bf25a43..b8069ea 100644 --- a/includes/progress.php +++ b/includes/progress.php @@ -3,7 +3,7 @@ class PMPro_Courses_User_Progress { /** * Toggle progress for lesson completion (complete or reset/clear) * - * @since TBD + * @since 2.0 * * @param int $lesson_id The lesson ID. * @param int $user_id The user ID. Optional. @@ -82,7 +82,7 @@ public static function get_user_lesson_status( $lesson_id, $user_id = null ) { /** * Get the user's progress for a specific course. * - * @since TBD + * @since 2.0 * * @param int $course_id The course ID. * @param int $user_id The user ID. Optional. @@ -165,7 +165,7 @@ public static function get_completed_lessons_for_a_course( $course_id, $user_id /** * Get a list of courses for a user that have no progress at all. * - * @since TBD + * @since 2.0 * * @param int|null $user_id * @return array|false Array of WP_Post objects (pmpro_course) or false if no user. @@ -317,7 +317,7 @@ function pmpro_courses_toggle_lesson_progress_ajax(){ /** * Retroactively migrate any user progress from user meta to the new progress table. * - * @since TBD + * @since 2.0 */ function pmpro_courses_migrate_course_progress() { global $wpdb; diff --git a/includes/shortcodes/courses-outline.php b/includes/shortcodes/courses-outline.php index ad57c85..528943a 100644 --- a/includes/shortcodes/courses-outline.php +++ b/includes/shortcodes/courses-outline.php @@ -6,7 +6,7 @@ * - course_id: The ID(s) of the course(s) to display. Can be a single ID, an array of IDs, or a comma-separated string of IDs. * - show_course_title: Whether to display the course title. Default is true. * - * @since TBD + * @since 2.0 */ // Exit if accessed directly. diff --git a/includes/updates/upgrade_1_3.php b/includes/updates/upgrade_1_3.php index 6da123c..0107cc1 100644 --- a/includes/updates/upgrade_1_3.php +++ b/includes/updates/upgrade_1_3.php @@ -12,7 +12,7 @@ function pmpro_courses_upgrade_1_3() { /** * Create the table for progress tracking. * - * @since TBD + * @since 2.0 * @return void */ function pmpro_courses_create_progress_table() { diff --git a/js/admin.js b/js/admin.js index 1dfa492..48b8a7e 100644 --- a/js/admin.js +++ b/js/admin.js @@ -6,7 +6,7 @@ /** * Remove a lesson from the lesson table for a course. * - * @since TBD + * @since 2.0 */ function pmpro_courses_remove_lesson(lesson_id, section_id) { let $row, $section, $table; @@ -111,7 +111,7 @@ function pmpro_courses_remove_lesson(lesson_id, section_id) { * * AJAX is needed, because we need to get the WP_POST object for the lesson when building the table row. * - * @since TBD + * @since 2.0 */ function pmpro_courses_update_post(button_element) { var button = jQuery(button_element); diff --git a/pmpro-courses.php b/pmpro-courses.php index 2a59b27..c2499a7 100644 --- a/pmpro-courses.php +++ b/pmpro-courses.php @@ -23,6 +23,7 @@ require_once PMPRO_COURSES_DIR . '/includes/admin.php'; require_once PMPRO_COURSES_DIR . '/includes/settings.php'; require_once PMPRO_COURSES_DIR . '/includes/blocks.php'; +require_once PMPRO_COURSES_DIR . '/includes/batch-enrollment.php'; // Modules. function pmpro_courses_setup_modules() {