Batch processing for retroactively enrolling members to courses. - #108
Batch processing for retroactively enrolling members to courses.#108andrewlimaza wants to merge 5 commits into
Conversation
* FEATURE: Added support for batch enrollment for course integrations that use "enrollment".
flintfromthebasement
left a comment
There was a problem hiding this comment.
PR: #108 — Batch processing for retroactively enrolling members to courses.
andrewlimaza → dev | 15 files, +450 -46 lines
#108
Summary
Solid foundation for retroactive batch enrollment via Action Scheduler — the class structure, pagination via offset chaining, and per-course processed-levels tracking are all sound. One correctness issue undermines the stated goal for old courses; a few minor things to clean up before merge. Not blocking, but the major issue should be fixed.
Issues
-
Major
includes/batch-enrollment.php:206-218(get_published_course_ids_for_post_type) — The daily cron is supposed to retroactively enroll members in courses that existed before this plugin version, but the query filtersAND p.post_modified >= %s(last 24 hours). This means any course that hasn't been touched in more than a day is silently skipped every single daily run, forever. ThePROCESSED_LEVELS_METAalready prevents re-processing enrolled levels — the time filter is redundant and actively breaks the retroactive goal. Remove thepost_modifiedfilter entirely, or query for courses that don't yet have_pmpro_courses_batch_enrollment_levelsset and let the metadata be the deduplication gate. -
Minor
includes/modules/learndash.php:228(retroactive_enroll_user) —ld_update_course_access()does not have a documented return value and returnsvoidin some LearnDash versions.if ( ! $result )will always be truthy when it returnsnull/false, logging a spurious error on every successful enrollment. Either drop the return-value check or verify the current LD version actually returns a boolean here. -
Minor
includes/batch-enrollment.php:96-109(schedule) vsprocess_batch:163-172— The first batch task goes throughPMPro_Action_Scheduler::instance()->maybe_add_task()(presumably deduplication-aware), but chained batches callas_enqueue_async_action()directly. This inconsistency is probably intentional (chained offsets shouldn't deduplicate), but a comment explaining why the two paths differ would prevent someone from "fixing" it later. -
Minor
includes/batch-enrollment.php:278-280(init guard) — Theclass_exists('PMPro_Action_Scheduler')check runs at include-time. Sincebatch-enrollment.phpisrequire_once'd directly inpmpro-courses.php(beforeplugins_loadedfires), this guard could fail if PMPro loads after pmpro-courses depending on alphabetical plugin order. Wrapping theinit()call inadd_action('plugins_loaded', ...)with a late priority would make this robust.
Looks Good
get_active_members() — correct use of ORDER BY user_id before LIMIT/OFFSET ensures consistent pagination across batch runs. array_map('intval', $level_ids) + sort() before building placeholders prevents both injection and unstable query plans.
maybe_schedule_for_course() — the autosave/revision guards and the post_status === 'publish' check are all in the right place. Clean.
Questions
-
The PR description notes this "won't re-run batches whenever a member is added to a level programmatically and bypasses any WordPress hooks." Is that known gap documented anywhere user-facing, or does it need a notice in the admin or release notes?
-
tutorlms.php:retroactive_enroll_user—do_enroll( $user_id, 0, $course_id )matches the pre-existing pattern at line 295, but TutorLMS's documented signature isdo_enroll($course_id, $order_id, $user_id). Can you confirm this arg order is correct for the TutorLMS version being targeted? The pre-existing code may have a latent bug here that this PR is replicating.
|
Hey Andrew — going to mark this draft for now while we rethink the approach. The architecture is solid as a starting point, but after looking at how PMPro core handles the same problem for the LifterLMS streamline mode (
The shape I'd suggest mirroring:
One more thing while we're here: Going to convert this to Draft for now. |
- Trigger off pmpro_after_updating_post_level_restrictions instead of save_post/daily cron. - Fan out one Action Scheduler task per member (halt/resume) instead of offset-chained batches. - Drop the processed-levels post meta; each per-user repair is idempotent. - Consolidate module enroll/unenroll logic into repair_user_enrollments(). - Fix Tutor LMS do_enroll() argument order. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Reworked in 91442b9 per the feedback above — now hooks |
Resolve conflicts: fold LearnDash group enrollment (strangerstudios#111) into repair_user_enrollments(), drop the Tutor do_enroll changelog line already shipped in 2.1.3 (strangerstudios#135). Co-Authored-By: Claude Code <noreply@anthropic.com>
Resolves #72.
Approach
Reworked per @dparker1005's review to mirror PMPro core's LifterLMS streamline pattern (
includes/compatibility/lifterlms.php):pmpro_after_updating_post_level_restrictions(PMPro 3.6). Fires from the Require Membership meta box, the REST API, and programmaticpmpro_update_post_level_restrictions()calls. No moresave_posthook or daily cron.pmpro_courses_repair_all_enrollments_callbacktask per course, which runsPMPro_Action_Scheduler::halt()→ onepmpro_courses_repair_user_enrollmentstask per member →resume(). Keyset pagination onuser_id, no sharedOFFSETcursor._pmpro_courses_batch_enrollment_levelspost meta is gone. Each LMS module has a singlerepair_user_enrollments( $user_id )that diffs the user's current-level courses against all level-restricted courses and enrolls/unenrolls the difference, checkingis_enrolledfirst.pmpro_after_all_membership_level_changesnow calls the same method, so both paths share one code path.pmpro_courses_enrollment_course_post_typesfilter and hookPMPro_Courses_Batch_Enrollment::AS_HOOK_USER.Note: the fan-out queues everyone who has ever held a level, not just members of the course's current levels. The hook fires after the change, so a level that was just removed from the course is no longer visible — its members would otherwise never be unenrolled. Core makes the same trade.
Behavior change to be aware of: the repair unenrolls a user from any level-restricted course their current levels don't grant (previously only courses tied to levels they lost). A student manually enrolled in a restricted course without the level will be unenrolled on the next repair. This is what makes it self-healing and matches core's LifterLMS behavior.
How to test the changes in this Pull Request:
pmpro_courses_repair_all_enrollments_callbacktask should appear, followed by onepmpro_courses_repair_user_enrollmentstask per member (grouppmpro_courses_enrollment). Run them (or wait for cron).Tested with LearnDash; other modules follow the same structure but should be verified before release.
LearnDash:
repair_user_enrollments()also reconciles Group enrollment (from #111), and thegroupspost type triggers a repair when its level restrictions change.