LLMS_Processor_Course_Data

LLMS_Processor_Course_Data


Description Description

Handle background processing of average progress & average grade for courses.

The background process calculates "expensive" aggregate course data and stores them on the wp_postmeta table so the data can be access later with a single database read.

The process is queued for recalculation when:

  • Students enroll.
  • Students unenroll.
  • Students complete lessons.
  • Students complete quizzes.

Upon completion, the following values can be accessed via the LLMS_Course model to retrieve the aggregate data for the course:

  • Average grade: LLMS_Course::get( 'average_grade' )
  • Average progress: LLMS_Course::get( 'average_progress' )
  • Number of currently enrolled students: LLMS_Course::get( 'enrolled_students' )

Top ↑

Source Source

File: includes/processors/class.llms.processor.course.data.php

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
class LLMS_Processor_Course_Data extends LLMS_Abstract_Processor {
 
    /**
     * Unique identifier for the processor
     *
     * @var string
     */
    protected $id = 'course_data';
 
    /**
     * WP Cron Hook for scheduling the bg process
     *
     * @var string
     */
    private $schedule_hook = 'llms_calculate_course_data';
 
    /**
     * Maximum number of students allowed in a course
     *
     * When enrollment is higher than this number
     * throttling the calculations will be delayed.
     *
     * @var int
     */
    private $throttle_max_students;
 
    /**
     * Frequency of calculation process when the process is throttled.
     *
     * @var int
     */
    private $throttle_frequency;
 
    /**
     * Action triggered to queue queries needed to make the calculation
     *
     * @since 3.15.0
     * @since 4.12.0 Add throttling by course in progress and adjust last_run calculation to be specific to the course.
     *               Improve performance of the student query by removing unneeded sort columns.
     * @since 4.21.0 When there's no students found in the course, run the `task_complete()` method to ensure data
     *               from a previous calculation is cleared.
     * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.
     *
     * @param int $course_id WP Post ID of the course.
     * @return void|null
     */
    public function dispatch_calc( $course_id ) {
 
        $this->log( sprintf( 'Course data calculation dispatched for course %d.', $course_id ) );
 
        // Make sure we have a course.
        $course = llms_get_post( $course_id );
        if ( ! $course instanceof LLMS_Course ) {
            return null;
        }
 
        // Return early if we're already processing data for the given course.
        if ( $this->is_already_processing_course( $course_id ) ) {
            return $this->dispatch_calc_throttled( $course_id );
        }
 
        // Retrieve args.
        $args = $this->get_student_query_args( $course_id );
 
        // Get total number of pages.
        $query = new LLMS_Student_Query( $args );
 
        // No students in the course, run task completion.
        if ( ! $query->get_found_results() ) {
            return $this->task_complete( $course, $this->get_task_data(), true );
        }
 
        // Store the total number of students right away.
        $course->set( 'enrolled_students', $query->get_found_results() );
 
        // Throttle processing.
        if ( $this->maybe_throttle( $query->get_found_results(), $course_id ) ) {
            return $this->dispatch_calc_throttled( $course_id );
        }
 
        // Add each page to the queue.
        while ( $args['page'] <= $query->get_max_pages() ) {
            $this->push_to_queue( $args );
            $args['page']++;
        }
 
        // Save queue and dispatch the process.
        $this->save()->dispatch();
 
    }
 
    /**
     * Schedule data calculation for the future
     *
     * This method is called when data processing is triggered for a course that is currently being processed
     * or for a course that qualifies for process throttling based on the number of students in the course.
     *
     * @since 4.12.0
     *
     * @param int $course_id WP_Post ID of the course.
     * @return void
     */
    protected function dispatch_calc_throttled( $course_id ) {
 
        $this->schedule_calculation( $course_id, $this->get_last_run( $course_id ) + $this->throttle_frequency );
        $this->log( sprintf( 'Course data calculation throttled for course %d.', $course_id ) );
 
    }
 
    /**
     * Retrieve arguments used to perform an LLMS_Student_Query for background data processing
     *
     * @since 4.12.0
     *
     * @param int $course_id WP_Post ID of the course.
     * @return array Array of arguments passed to an LLMS_Student_Query.
     */
    protected function get_student_query_args( $course_id ) {
 
        /**
         * Filter the query arguments used when calculating course data
         *
         * @since 4.12.0
         *
         * @param array                      $args      Query arguments passed to LLMS_Student_Query.
         * @param LLMS_Processor_Course_Data $processor Instance of the data processor class.
         */
        return apply_filters(
            'llms_data_processor_course_data_student_query_args',
            array(
                'post_id'  => $course_id,
                'statuses' => array( 'enrolled' ),
                'page'     => 1,
                'per_page' => 100,
                'sort'     => array(
                    'id' => 'ASC',
                ),
            ),
            $this
        );
 
    }
 
    /**
     * Retrieve a timestamp for the last time data calculation was completed for a given course
     *
     * @since 4.12.0
     *
     * @param int $course_id WP_Post ID of the course.
     * @return int The timestamp of the last run. Returns `0` when no data recorded.
     */
    protected function get_last_run( $course_id ) {
        return absint( get_post_meta( $course_id, '_llms_last_data_calc_run', true ) );
    }
 
    /**
     * Retrieve structured task data array.
     *
     * Ensures the expected required array keys are found on the task array
     * and optionally merges in an existing array of day with the (empty) defaults.
     *
     * @since 4.21.0
     *
     * @param array $data Existing array of day (from a previous task).
     * @return array
     */
    protected function get_task_data( $data = array() ) {
        return wp_parse_args(
            $data,
            array(
                'students' => 0,
                'progress' => 0,
                'quizzes'  => 0,
                'grade'    => 0,
            )
        );
    }
 
    /**
     * Initializer
     *
     * @since 3.15.0
     *
     * @return void
     */
    protected function init() {
 
        // For the cron.
        add_action( $this->schedule_hook, array( $this, 'dispatch_calc' ), 10, 1 );
 
        // For LifterLMS actions which trigger recalculation.
        $this->actions = array(
            'llms_course_calculate_data'    => array(
                'arguments' => 1,
                'callback'  => 'schedule_calculation',
                'priority'  => 10,
            ),
            'llms_user_enrolled_in_course'  => array(
                'arguments' => 2,
                'callback'  => 'schedule_from_course',
                'priority'  => 10,
            ),
            'llms_user_removed_from_course' => array(
                'arguments' => 2,
                'callback'  => 'schedule_from_course',
                'priority'  => 10,
            ),
            'lifterlms_lesson_completed'    => array(
                'arguments' => 2,
                'callback'  => 'schedule_from_lesson',
                'priority'  => 10,
            ),
            'lifterlms_quiz_completed'      => array(
                'arguments' => 3,
                'callback'  => 'schedule_from_quiz',
                'priority'  => 10,
            ),
        );
 
        /**
         * Throttles course data processing based on the number of a students in a course.
         *
         * If the number of students in a course is greater than or equal to this number, the background
         * process will be throttled to run only once every N hours where N is equal to the number of hours
         * defined by the `llms_data_processor_course_data_throttle_frequency` filter.
         *
         * @since 3.15.0
         * @since 4.12.0 Reduced default value of `$number_students` from 2500 to 500.
         *
         * @see llms_data_processor_course_data_throttle_frequency
         *
         * @param int                        $number_students The number of students. Default is `500`.
         * @param LLMS_Processor_Course_Data $processor       Instance of the data processor class.
         */
        $this->throttle_max_students = apply_filters( 'llms_data_processor_course_data_throttle_count', 500, $this );
 
        /**
         * Frequency to run the processor for a given course when processing is throttled
         *
         * @since 3.15.0
         *
         * @see llms_data_processor_course_data_throttle_count
         *
         * @param int                        $frequency Frequency of the calculation process in seconds. Default `HOUR_IN_SECONDS * 4`.
         * @param LLMS_Processor_Course_Data $processor Instance of the data processor class.
         */
        $this->throttle_frequency = apply_filters( 'llms_data_processor_course_data_throttle_frequency', HOUR_IN_SECONDS * 4, $this );
 
    }
 
    /**
     * Determines if the supplied course is already being processed.
     *
     * If it's already being processed we'll throttle the processing so we'll wait until the course
     * completes its current data processing and start again later.
     *
     * @since 4.12.0
     *
     * @param int $course_id WP_Post ID of the course.
     * @return boolean
     */
    protected function is_already_processing_course( $course_id ) {
        return llms_parse_bool( get_post_meta( $course_id, '_llms_temp_calc_data_lock', true ) );
    }
 
    /**
     * For large courses, only recalculate once every 4 hours
     *
     * @since 3.15.0
     * @since 4.12.0 Adjusted access from private to protected.
     *               Pull last run data on a per-course basis.
     *               Added parameter `$course_id`.
     *
     * @param int $num_students Number of students in the current course.
     * @param int $course_id    WP_Post ID of the course.
     * @return boolean When `true` the dispatch is throttled and when `false` it will run.
     */
    protected function maybe_throttle( $num_students, $course_id ) {
 
        $throttled = false;
 
        if ( $num_students >= $this->throttle_max_students ) {
 
            $throttled = ( time() - $this->get_last_run( $course_id ) <= $this->throttle_frequency );
 
        }
 
        /**
         * Filters whether or not data processing is throttled for a request
         *
         * @since 4.12.0
         *
         * @param boolean $throttled    If `true`, the processing for the current request is throttled, otherwise data processing will begin.
         * @param int     $num_students Number of students in the current course.
         * @param int     $course_id    WP_Post ID of the course.
         * $param int     $max_students Maximum number of students in the course before processing is throttled.
         */
        return apply_filters( 'llms_data_processor_course_data_throttled', $throttled, $num_students, $course_id, $this->throttle_max_students );
 
    }
 
    /**
     * Schedule recalculation from actions triggered against a course
     *
     * @since 3.15.0
     *
     * @param int $user_id   WP user id of the student.
     * @param int $course_id WP Post ID of the course.
     * @return void
     */
    public function schedule_from_course( $user_id, $course_id ) {
        $this->schedule_calculation( $course_id );
    }
 
    /**
     * Schedule recalculation from actions triggered against a lesson
     *
     * @since 3.15.0
     *
     * @param int $user_id   WP user id of the student.
     * @param int $lesson_id WP Post ID of the lesson.
     * @return void
     */
    public function schedule_from_lesson( $user_id, $lesson_id ) {
        $lesson = llms_get_post( $lesson_id );
        $this->schedule_calculation( $lesson->get( 'parent_course' ) );
    }
 
    /**
     * Schedule recalculation from actions triggered against a quiz
     *
     * @since 3.15.0
     *
     * @param int               $user_id WP user id of the student.
     * @param int               $quiz_id WP Post ID of the quiz.
     * @param LLMS_Quiz_Attempt $attempt Quiz attempt object.
     * @return void
     */
    public function schedule_from_quiz( $user_id, $quiz_id, $attempt ) {
        $this->schedule_from_lesson( $user_id, $attempt->get( 'lesson_id' ) );
    }
 
    /**
     * Schedule a calculation to execute
     *
     * This will schedule an event that will setup the queue of items for the background process.
     *
     * @since 3.15.0
     * @since 4.21.0 Force `$course_id` to an absolute integer to avoid duplicate scheduling resulting from loose variable typing.
     *
     * @param int $course_id WP Post ID of the course.
     * @param int $time      Optionally pass a timestamp for when the event should be run.
     * @return void
     */
    public function schedule_calculation( $course_id, $time = null ) {
 
        $course_id = absint( $course_id );
 
        $this->log( sprintf( 'Course data calculation triggered for course %d.', $course_id ) );
 
        $args = array( $course_id );
 
        if ( ! wp_next_scheduled( $this->schedule_hook, $args ) ) {
 
            $time = ! $time ? time() : $time;
 
            wp_schedule_single_event( $time, $this->schedule_hook, $args );
            $this->log( sprintf( 'Course data calculation scheduled for course %d.', $course_id ) );
 
        }
 
    }
 
 
    /**
     * Execute calculation for each item in the queue until all students in the course have been polled
     *
     * Stores the data in the postmeta table to be accessible via LLMS_Course.
     *
     * @since 3.15.0
     * @since 4.12.0 Moved task completion logic to `task_complete()`.
     * @since 4.16.0 Fix log string to properly record the post_id.
     * @since 4.21.0 Use `get_task_data()` to merge/retrieve aggregate task data.
     *               Return early for non-courses.
     *
     * @param array $args Query arguments passed to LLMS_Student_Query.
     * @return boolean Always returns `false` to remove the item from the queue when processing is complete.
     */
    public function task( $args ) {
 
        $this->log( sprintf( 'Course data calculation task called for course %1$d with args: %2$s', $args['post_id'], wp_json_encode( $args ) ) );
 
        $course = llms_get_post( $args['post_id'] );
 
        // Only process existing courses.
        if ( ! $course instanceof LLMS_Course ) {
            $this->log( sprintf( 'Course data calculation task skipped for course %1$d.', $args['post_id'] ) );
            return false;
        }
 
        // Lock the course against duplicate processing.
        $course->set( 'temp_calc_data_lock', 'yes' );
 
        // Get saved data or empty array when on first page.
        $data = ( 1 !== $args['page'] ) ? $course->get( 'temp_calc_data' ) : array();
 
        // Merge with the defaults.
        $data = $this->get_task_data( $data );
 
        // Perform the query.
        $query = new LLMS_Student_Query( $args );
 
        foreach ( $query->get_students() as $student ) {
 
            // Progress, all students counted here.
            $data['students']++;
            $data['progress'] = $data['progress'] + $student->get_progress( $args['post_id'] );
 
            // Grades only counted when a student has taken a quiz.
            // If a student hasn't taken it, we don't count it as a 0 on the quiz.
            $grade = $student->get_grade( $args['post_id'] );
 
            // Only check actual quiz grades.
            if ( is_numeric( $grade ) ) {
                $data['quizzes']++;
                $data['grade'] = $data['grade'] + $grade;
            }
        }
 
        return $this->task_complete( $course, $data, $query->is_last_page() );
 
    }
 
    /**
     * Complete a task
     *
     * Stores the current (incomplete) array of course data on the postmeta table for use
     * by the next task in the queue.
     *
     * Upon completion, uses the data array to calculate the final aggregate values and store
     * them on the postmeta table for the course for quick retrieval later.
     *
     * @since 4.12.0
     * @since 4.16.0 Fix log string to properly log the course id.
     *
     * @param LLMS_Course $course    Course object.
     * @param array       $data      Aggregate calculation data array.
     * @param boolean     $last_page Whether or not this is the last page set of students for the process.
     * @return boolean Always returns false.
     */
    protected function task_complete( $course, $data, $last_page ) {
 
        $this->log( sprintf( 'Course data calculation task completed for course %1$d with data: %2$s', $course->get( 'id' ), wp_json_encode( $data ) ) );
 
        // Save our work on the last run.
        if ( $last_page ) {
 
            // Calculate.
            $grade    = $data['quizzes'] ? round( $data['grade'] / $data['quizzes'], 2 ) : 0;
            $progress = $data['students'] ? round( $data['progress'] / $data['students'], 2 ) : 0;
 
            // Save the data to the course.
            $course->set( 'average_grade', $grade );
            $course->set( 'average_progress', $progress );
            $course->set( 'enrolled_students', $data['students'] );
            $course->set( 'last_data_calc_run', time() );
 
            // Delete the temporary data so its fresh for next time.
            delete_post_meta( $course->get( 'id' ), '_llms_temp_calc_data' );
 
            // Unlock the course.
            delete_post_meta( $course->get( 'id' ), '_llms_temp_calc_data_lock' );
 
            $this->log( sprintf( 'Course data calculation completed for course %d.', $course->get( 'id' ) ) );
 
        } else {
 
            // Save temporary data so it can be used by the next run in the process.
            $course->set( 'temp_calc_data', $data );
 
        }
 
        return false;
 
    }
 
}


Top ↑

Methods Methods


Top ↑

Changelog Changelog

Changelog
Version Description
4.12.0 Remove (protected) method LLMS_Processor_Course_Data::complete(), the override of the parent method is no longer needed.
3.15.0 Introduced.

Top ↑

User Contributed Notes User Contributed Notes

You must log in before being able to contribute a note or feedback.