LLMS_Student
LLMS_Student model class.
Description Description
Source Source
File: includes/models/model.llms.student.php
class LLMS_Student extends LLMS_Abstract_User_Data { /** * Retrieve an instance of the LLMS_Instructor model for the current user * @return obj|false * @since 3.14.0 * @version 3.14.0 */ public function instructor() { if ( $this->is_instructor() ) { return llms_get_instructor( $this->get_id() ); } return false; } /** * Retrieve an instance of the student quiz data model * @return LLMS_Student_Quizzes * @since 3.9.0 * @version 3.9.0 */ public function quizzes() { return new LLMS_Student_Quizzes( $this->get_id() ); } /** * Add the student to a LifterLMS Membership * @param int $membership_id WP Post ID of the membership * @return void * * @since 2.2.3 */ private function add_membership_level( $membership_id ) { // add the user to the membership level $membership_levels = $this->get_membership_levels(); array_push( $membership_levels, $membership_id ); update_user_meta( $this->get_id(), '_llms_restricted_levels', $membership_levels ); // if there's auto-enroll courses, enroll the user in those courses $autoenroll_courses = get_post_meta( $membership_id, '_llms_auto_enroll', true ); if ( $autoenroll_courses ) { foreach ( $autoenroll_courses as $course_id ) { $this->enroll( $course_id, 'membership_' . $membership_id ); } } } /** * Enroll the student in a course or membership * @param int $product_id WP Post ID of the course or membership * @param string $trigger String describing the reason for enrollment * @return boolean * * @see llms_enroll_student() calls this function without having to instantiate the LLMS_Student class first * * @since 2.2.3 * @version 3.17.0 */ public function enroll( $product_id, $trigger = 'unspecified' ) { do_action( 'before_llms_user_enrollment', $this->get_id(), $product_id ); // can only be enrolled in the following post types $product_type = get_post_type( $product_id ); if ( ! in_array( $product_type, array( 'course', 'llms_membership' ) ) ) { return false; } // check enrollment before enrolling // this will prevent duplicate enrollments if ( llms_is_user_enrolled( $this->get_id(), $product_id ) ) { return false; } // if the student has been previously enrolled, simply update don't run a full enrollment if ( $this->get_enrollment_status( $product_id, false ) ) { $insert = $this->insert_status_postmeta( $product_id, 'enrolled', $trigger ); } else { $insert = $this->insert_enrollment_postmeta( $product_id, $trigger ); } // add the user postmeta for the enrollment if ( ! empty( $insert ) ) { // update the cache $this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), 'enrolled' ); $this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) ); $this->cache_delete( sprintf( 'date_updated_%d', $product_id ) ); // trigger additional actions based off post type switch ( get_post_type( $product_id ) ) { case 'course': do_action( 'llms_user_enrolled_in_course', $this->get_id(), $product_id ); break; case 'llms_membership': $this->add_membership_level( $product_id ); do_action( 'llms_user_added_to_membership_level', $this->get_id(), $product_id ); break; } return true; } return false; } /** * Retrieve achievements that a user has earned * @param string $orderby field to order the returned results by * @param string $order ordering method for returned results (ASC or DESC) * @param string $return return type * obj => array of objects from $wpdb->get_results * achievements => array of LLMS_User_Achievement instances * @return array * @since 2.4.0 * @version 3.14.0 */ public function get_achievements( $orderby = 'updated_date', $order = 'DESC', $return = 'obj' ) { $orderby = esc_sql( $orderby ); $order = esc_sql( $order ); global $wpdb; $query = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value AS achievement_id, updated_date AS earned_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d and meta_key = '_achievement_earned' ORDER BY $orderby $order", $this->get_id() ) ); if ( 'achievements' === $return ) { $ret = array(); foreach ( $query as $obj ) { $ret[] = new LLMS_User_Achievement( $obj->achievement_id ); } return $ret; } return $query; } public function get_avatar( $size = 96 ) { return '<span class="llms-student-avatar">' . get_avatar( $this->get_id(), $size, null, $this->get_name() ) . '</span>'; } /** * Retrieve the order which enrolled a student in a given course or membership * Retrieves the most recently updated order for the given product * * @param int $product_id WP Post ID of the LifterLMS Product (course, lesson, or membership) * @return obj|false Instance of the LLMS_Order or false if none found * @since 3.0.0 * @version 3.0.0 */ public function get_enrollment_order( $product_id ) { // if a lesson id was passed in, cascade up to the course for order retrieval if ( 'lesson' === get_post_type( $product_id ) ) { $lesson = new LLMS_Lesson( $product_id ); $product_id = $lesson->get_parent_course(); } // attempt to locate the order via the enrollment trigger $trigger = $this->get_enrollment_trigger( $product_id ); if ( strpos( $trigger, 'order_' ) !== false ) { $id = str_replace( array( 'order_', 'wc_' ), '', $trigger ); if ( is_numeric( $id ) ) { if ( 'llms_order' === get_post_type( $id ) ) { return new LLMS_Order( $id ); } else { return get_post( $id ); } } } // couldn't find via enrollment trigger, do a WP_Query $q = new WP_Query( array( 'order' => 'DESC', 'orderby' => 'modified', 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_llms_user_id', 'value' => $this->get_id(), ), array( 'key' => '_llms_product_id', 'value' => $product_id, ), ), 'posts_per_page' => 1, // 'post_status' => $statuses, 'post_type' => 'llms_order', ) ); if ( $q->have_posts() ) { return new LLMS_Order( $q->posts[0] ); } // couldn't find an order, return false return false; } /** * Retrieve certificates that a user has earned * @param string $orderby field to order the returned results by * @param string $order ordering method for returned results (ASC or DESC) * @param string $return return type * obj => array of objects from $wpdb->get_results * certificates => array of LLMS_User_Certificate instances * @return array * @since 2.4.0 * @version 3.14.1 */ public function get_certificates( $orderby = 'updated_date', $order = 'DESC', $return = 'obj' ) { $orderby = esc_sql( $orderby ); $order = esc_sql( $order ); global $wpdb; $query = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value AS certificate_id, updated_date AS earned_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d and meta_key = '_certificate_earned' ORDER BY $orderby $order", $this->get_id() ) ); if ( 'certificates' === $return ) { $ret = array(); foreach ( $query as $obj ) { $ret[] = new LLMS_User_Certificate( $obj->certificate_id ); } return $ret; } return $query; } /** * Retrieve IDs of user's courses based on supplied criteria * @param array $args see `get_enrollments` * @return array * @since 3.0.0 * @version 3.15.0 */ public function get_courses( $args = array() ) { return $this->get_enrollments( 'course', $args ); } /** * Retrieve IDs of courses a user has completed * * @param array $args query arguments * @arg int $limit number of courses to return * @arg string $orderby table reference and field to order results by * @arg string $order result order (DESC, ASC) * @arg int $skip number of results to skip for pagination purposes * @return array "courses" will contain an array of course ids * "more" will contain a boolean determining whether or not more courses are available beyond supplied limit/skip criteria * @since ?? * @version 3.24.0 */ public function get_completed_courses( $args = array() ) { global $wpdb; $args = array_merge( array( 'limit' => 20, 'orderby' => 'upm.updated_date', 'order' => 'DESC', 'skip' => 0, ), $args ); // add one to the limit to see if there's pagination $args['limit']++; // the query $q = $wpdb->get_results( $wpdb->prepare( "SELECT upm.post_id AS id FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm JOIN {$wpdb->posts} AS p ON p.ID = upm.post_id WHERE p.post_type = 'course' AND upm.meta_key = '_is_complete' AND upm.meta_value = 'yes' AND upm.user_id = %d ORDER BY {$args['orderby']} {$args['order']} LIMIT %d, %d; ", array( $this->get_id(), $args['skip'], $args['limit'], ) ), 'OBJECT_K' ); $ids = array_keys( $q ); $more = false; // if we hit our limit we have too many results, pop the last one if ( count( $ids ) === $args['limit'] ) { array_pop( $ids ); $more = true; } // reset args to pass back for pagination $args['limit']--; $r = array( 'limit' => $args['limit'], 'more' => $more, 'results' => $ids, 'skip' => $args['skip'], ); return $r; } /** * Get the formatted date when a course or lesson was completed by the student * @param int $object_id WP Post ID of a course or lesson * @param string $format date format as accepted by php date() * @return false|string will return false if the user is not enrolled * @since ?? * @version ?? */ public function get_completion_date( $object_id, $format = 'F d, Y' ) { global $wpdb; $q = $wpdb->get_var( $wpdb->prepare( "SELECT updated_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_is_complete' AND meta_value = 'yes' AND user_id = %d AND post_id = %d ORDER BY updated_date DESC LIMIT 1", array( $this->get_id(), $object_id ) ) ); return ( $q ) ? date_i18n( $format, strtotime( $q ) ) : false; } /** * Retrieve IDs of user's enrollments by post type (and additional criteria) * @param string $post_type name of the post type (course|membership) * @param array $args query arguments * @arg int $limit number of courses to return * @arg string $orderby table reference and field to order results by * @arg string $order result order (DESC, ASC) * @arg int $skip number of results to skip for pagination purposes * @arg string $status filter results by enrollment status, "any", "enrolled", "cancelled", or "expired" * @return array "results" will contain an array of course ids * "more" will contain a boolean determining whether or not more courses are available beyond supplied limit/skip criteria * "found" will contain the total possible FOUND_ROWS() for the query * @since 3.0.0 * @version 3.15.1 */ public function get_enrollments( $post_type = 'course', $args = array() ) { global $wpdb; $args = wp_parse_args( $args, array( 'limit' => 20, 'orderby' => 'upm.updated_date', 'order' => 'DESC', 'skip' => 0, 'status' => 'any', // any, enrolled, cancelled, expired ) ); // prefix membership if ( 'membership' === $post_type ) { $post_type = 'llms_membership'; } // sanitize order & orderby $args['orderby'] = preg_replace( '/[^a-zA-Z_.]/', '', $args['orderby'] ); $args['order'] = preg_replace( '/[^a-zA-Z_.]/', '', $args['order'] ); // allow "short" orderby's to be passed in without a table reference switch ( $args['orderby'] ) { case 'date': $args['orderby'] = 'upm.updated_date'; break; case 'order': $args['orderby'] = 'p.menu_order'; break; case 'title': $args['orderby'] = 'p.post_title'; break; } // prepare additional status AND clauses if ( 'any' !== $args['status'] ) { $status = $wpdb->prepare( " AND upm.meta_value = %s AND upm.updated_date = ( SELECT MAX( upm2.updated_date ) FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2 WHERE upm2.meta_key = '_status' AND upm2.user_id = %d AND upm2.post_id = upm.post_id )", $args['status'], $this->get_id() ); } else { $status = ''; } // the query $query = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS DISTINCT upm.post_id AS id FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm JOIN {$wpdb->posts} AS p ON p.ID = upm.post_id WHERE p.post_type = %s AND p.post_status = 'publish' AND upm.meta_key = '_status' AND upm.user_id = %d {$status} ORDER BY {$args['orderby']} {$args['order']} LIMIT %d, %d; ", array( $post_type, $this->get_id(), $args['skip'], $args['limit'], ) ), 'OBJECT_K' ); $found = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ); return array( 'found' => $found, 'limit' => $args['limit'], 'more' => ( $found > ( ( $args['skip'] / $args['limit'] + 1 ) * $args['limit'] ) ), 'skip' => $args['skip'], 'results' => array_keys( $query ), ); } /** * Get the formatted date when a user initially enrolled in a product or when they were last updated * @param int $product_id WP Post ID of a course or membership * @param string $date "enrolled" will get the most recent start date, "updated" will get the most recent status change date * @param string $format date format as accepted by php date(), if none supplied uses the WP core "date_format" option * @return false|string will return false if the user is not enrolled * @since 3.0.0 * @version 3.17.0 */ public function get_enrollment_date( $product_id, $date = 'enrolled', $format = null ) { if ( ! $format ) { $format = get_option( 'date_format', 'M d, Y' ); } $cache_key = sprintf( 'date_%1$s_%2$s', $date, $product_id ); $res = $this->cache_get( $cache_key ); if ( false === $res ) { $key = ( 'enrolled' === $date ) ? '_start_date' : '_status'; global $wpdb; // get the oldest recorded Enrollment date $res = $wpdb->get_var( $wpdb->prepare( "SELECT updated_date FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '$key' AND user_id = %d AND post_id = %d ORDER BY updated_date DESC LIMIT 1", array( $this->get_id(), $product_id ) ) ); $this->cache_set( $cache_key, $res ); } return ( $res ) ? date_i18n( $format, strtotime( $res ) ) : false; } /** * Get the current enrollment status of a student for a particular product * * @param int $product_id WP Post ID of a Course, Lesson, or Membership * @param bool $use_cache If true, returns cached data if available, if false will run a db query * @return false|string * @since 3.0.0 * @version 3.17.0 */ public function get_enrollment_status( $product_id, $use_cache = true ) { $status = false; $product_type = get_post_type( $product_id ); // only check the following post types if ( ! in_array( $product_type, array( 'course', 'lesson', 'llms_membership' ) ) ) { return apply_filters( 'llms_get_enrollment_status', $status, $this->get_id(), $product_id ); } // get course ID if we're looking at a lesson if ( 'lesson' === $product_type ) { $lesson = new LLMS_Lesson( $product_id ); $product_id = $lesson->get_parent_course(); } if ( $use_cache ) { $status = $this->cache_get( sprintf( 'enrollment_status_%d', $product_id ) ); } // status will be: // + false if there was nothing in the cache -- run a query! // + a string if there was a status -- don't run query // + null if there's no status -- don't run query if ( false === $status ) { global $wpdb; // get the most recent recorded status $status = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE meta_key = '_status' AND user_id = %d AND post_id = %d ORDER BY updated_date DESC LIMIT 1", array( $this->get_id(), $product_id ) ) ); // null will be stored if the student has no status $this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), $status ); } $status = ( $status ) ? $status : false; return apply_filters( 'llms_get_enrollment_status', $status, $this->get_id(), $product_id ); } /** * Get the enrollment trigger for a the student's enrollment in a course * @param int $product_id WP Post ID of the course or membership * @return string|false * @since ?? * @version 3.21.0 */ public function get_enrollment_trigger( $product_id ) { $trigger = llms_get_user_postmeta( $this->get_id(), $product_id, '_enrollment_trigger', true ); return $trigger ? $trigger : false; } /** * Get the enrollment trigger id for a the student's enrollment in a course * @param int $product_id WP Post ID of the course or membership * @return int|false * @since 3.0.0 * @version 3.17.2 */ public function get_enrollment_trigger_id( $product_id ) { $trigger = $this->get_enrollment_trigger( $product_id ); $id = false; if ( $trigger && false !== strpos( $trigger, 'order_' ) ) { $trigger_obj = $this->get_enrollment_order( $product_id ); if ( $trigger_obj instanceof LLMS_Order ) { $id = $trigger_obj->get( 'id' ); } elseif ( $trigger_obj instanceof WP_Post ) { $id = $trigger_obj->ID; } } elseif ( $trigger && false !== strpos( $trigger, 'admin_' ) ) { $id = absint( str_replace( 'admin_', '', $trigger ) ); } return $id; } /** * Retrieve postmeta events related to the student * @param array $args default args, see LLMS_Query_User_Postmeta * @return array * @since 3.15.0 * @version 3.15.0 */ public function get_events( $args = array() ) { $query = new LLMS_Query_User_Postmeta( wp_parse_args( $args, array( 'types' => 'all', 'per_page' => 10, 'user_id' => $this->get_id(), ) ) ); return $query->get_metas(); } /** * Get the students grade for a lesson / course * All grades are based on quizzes assigned to lessons * @param int $object_id WP Post ID of a course or lesson * @param bool $use_cache If true, uses cached results * @return mixed * @since ?? * @version 3.24.0 */ public function get_grade( $object_id, $use_cache = true ) { $grade = LLMS()->grades()->get_grade( $object_id, $this, $use_cache ); if ( is_null( $grade ) ) { $grade = _x( 'N/A', 'Grade to display when no quizzes taken or available', 'lifterlms' ); } return apply_filters( 'llms_student_get_grade', $grade, $this, $object_id, get_post_type( $object_id ) ); } /** * Retrieve IDs of user's memberships based on supplied criteria * @param array $args see `get_enrollments` * @return array * @since 3.15.0 * @version 3.15.0 */ public function get_memberships( $args = array() ) { return $this->get_enrollments( 'membership', $args ); } /** * Retrieve a user's notification subscription preferences for a given type & trigger * @param string $type notification type: email, basic, etc... * @param string $trigger notification trigger: eg purchase_receipt, lesson_complete, etc... * @param string $default value to return if no setting is saved in the db * @return string yes or no * @since 3.10.0 * @version 3.10.0 */ public function get_notification_subscription( $type, $trigger, $default = 'no' ) { $prefs = $this->get( 'notification_subscriptions' ); if ( ! $prefs ) { $prefs = array(); } if ( isset( $prefs[ $type ] ) && isset( $prefs[ $type ][ $trigger ] ) ) { return $prefs[ $type ][ $trigger ]; } return $default; } /** * Retrieve the student's overall grade * Grade = sum of grades for all courses divided by number of enrolled courses * if a course has no quizzes in it, it cannot be graded and is therefore excluded from the calculation * * cached data is automatically cleared when a student completes a quiz * * @param boolean $use_cache if false, calculates the grade, otherwise utilizes cached data (if available) * @return float|string grade as float or "N/A" * @since 3.2.0 * @version 3.2.0 */ public function get_overall_grade( $use_cache = true ) { $grade = null; // attempt to pull from the cache first if ( $use_cache ) { $grade = $this->get( $this->meta_prefix . 'overall_grade' ); if ( is_numeric( $grade ) ) { $grade = floatval( $grade ); } } // cache disabled or no cached data available if ( ! $use_cache || null === $grade || '' === $grade ) { $grades = array(); // get courses $courses = $this->get_courses( array( 'limit' => 9999, ) ); // loop through courses foreach ( $courses['results'] as $course_id ) { // get course grade $g = $this->get_grade( $course_id ); // if an actual grade (not N/A) is returned if ( is_numeric( $g ) ) { array_push( $grades, $g ); } } // if we have at least one grade $count = count( $grades ); if ( $count ) { $grade = round( array_sum( $grades ) / $count, 2 ); } else { $grade = _x( 'N/A', 'overall grade when no quizzes', 'lifterlms' ); } // cache the grade $this->set( 'overall_grade', $grade ); }// End if(). return apply_filters( 'llms_student_get_overall_grade', $grade, $this ); } /** * Retrieve a student's overall progress * Overall progress is the total percentage completed based on all courses the student is enrolled in * Cached data is cleared every time the student completes a lesson * * @param boolean $use_cache if false, calculates the progress, otherwise utilizes cached data (if available) * @return float * @since 3.2.0 * @version 3.2.0 */ public function get_overall_progress( $use_cache = true ) { $progress = null; // attempt to pull from the cache first if ( $use_cache ) { $progress = $this->get( $this->meta_prefix . 'overall_progress' ); if ( is_numeric( $progress ) ) { $progress = floatval( $progress ); } } // cache disabled or no cached data available if ( ! $use_cache || null === $progress || '' === $progress ) { $progresses = array(); // get courses $courses = $this->get_courses( array( 'limit' => 9999, ) ); // loop through courses foreach ( $courses['results'] as $course_id ) { array_push( $progresses, $this->get_progress( $course_id, 'course' ) ); } $count = count( $progresses ); if ( $count ) { $progress = round( array_sum( $progresses ) / $count, 2 ); } else { $progress = 0; } // cache the grade $this->set( 'overall_progress', $progress ); } return apply_filters( 'llms_student_get_overall_progress', $progress, $this ); } /** * Get the students last completed lesson in a course * @param int $course_id WP_Post ID of the course * @return int WP_Post ID of the lesson or false if no progress has been made * @since 3.0.0 * @version 3.0.0 */ public function get_last_completed_lesson( $course_id ) { $course = new LLMS_Course( $course_id ); $lessons = array_reverse( $course->get_lessons( 'ids' ) ); foreach ( $lessons as $lesson ) { if ( $this->is_complete( $lesson, 'lesson' ) ) { return $lesson; } } return false; } /** * Retrieve an array of Membership Levels for a user * @return array * @since 2.2.3 * @version 2.2.3 */ public function get_membership_levels() { $levels = get_user_meta( $this->get_id(), '_llms_restricted_levels', true ); if ( empty( $levels ) ) { $levels = array(); } return $levels; } /** * Get the full name of a student * @return string * @since 3.0.4 * @version 3.5.1 */ public function get_name() { $name = trim( $this->get( 'first_name' ) . ' ' . $this->get( 'last_name' ) ); if ( ! $name ) { $name = $this->display_name; } return apply_filters( 'llms_student_get_name', $name, $this->get_id(), $this ); } /** * Get the next lesson a student needs to complete in a course * @param int $course_id WP_Post ID of the course * @return int WP_Post ID of the lesson or false if all courses are complete * @since 3.0.1 * @version 3.0.1 */ public function get_next_lesson( $course_id ) { $course = new LLMS_Course( $course_id ); $lessons = $course->get_lessons( 'ids' ); foreach ( $lessons as $lesson ) { if ( ! $this->is_complete( $lesson, 'lesson' ) ) { return $lesson; } } return false; } public function get_orders( $params = array() ) { $params = wp_parse_args( $params, array( 'count' => 25, 'page' => 1, 'statuses' => array_keys( llms_get_order_statuses() ), ) ); extract( $params ); $q = new WP_Query( array( 'order' => 'DESC', 'orderby' => 'date', 'meta_query' => array( array( 'key' => '_llms_user_id', 'value' => $this->get_id(), ), ), 'paged' => $page, 'posts_per_page' => $count, 'post_status' => $statuses, 'post_type' => 'llms_order', ) ); $orders = array(); if ( $q->have_posts() ) { foreach ( $q->posts as $post ) { $orders[ $post->ID ] = new LLMS_Order( $post ); } } return array( 'count' => count( $q->posts ), 'page' => $page, 'pages' => $q->max_num_pages, 'orders' => $orders, ); } /** * Get students progress through a course or track * @param int $object_id course or track id * @param string $type object type [course|course_track|section] * @param boolean $use_cache if true, will use cached data from the usermeta table (if available) * if false, will bypass cached data and recalculate the progress from scratch * @return float * @since 3.0.0 * @version 3.24.0 */ public function get_progress( $object_id, $type = 'course', $use_cache = true ) { $ret = 0; $cache_key = sprintf( '%1$s_%2$d_progress', $type, $object_id ); $cached = $use_cache ? $this->get( $cache_key ) : ''; if ( '' === $cached ) { $total = 0; $completed = 0; if ( 'course' === $type ) { $course = new LLMS_Course( $object_id ); $lessons = $course->get_lessons( 'ids' ); $total = count( $lessons ); foreach ( $lessons as $lesson ) { if ( $this->is_complete( $lesson, 'lesson' ) ) { $completed++; } } } elseif ( 'course_track' === $type ) { $track = new LLMS_Track( $object_id ); $courses = $track->get_courses(); $total = count( $courses ); foreach ( $courses as $course ) { if ( $this->is_complete( $course->ID, 'course' ) ) { $completed++; } } } elseif ( 'section' === $type ) { $section = new LLMS_Section( $object_id ); $lessons = $section->get_lessons( 'ids' ); $total = count( $lessons ); foreach ( $lessons as $lesson ) { if ( $this->is_complete( $lesson, 'lesson' ) ) { $completed++; } } } $ret = ( ! $completed || ! $total ) ? 0 : round( 100 / ( $total / $completed ), 2 ); $this->set( $cache_key, $ret ); } else { $ret = $cached; }// End if(). /** * @filter llms_student_get_progress * Filters the return of get_progress method * @param float $ret student's progress * @param int $object_id WP_Post ID of the object * @param string $type object post type [course|course_track|section] * @param int $user_id WP_User ID of the student * @since unknown * @version 3.24.0 */ return apply_filters( 'llms_student_get_progress', $ret, $object_id, $type, $this->get_id() ); } /** * Retrieve the Students original registration date in chosen format * @param string $format any date format that can be passed to date() * @return string * @since ?? * @version 3.14.0 */ public function get_registration_date( $format = '' ) { if ( ! $format ) { $format = get_option( 'date_format' ); } return date_i18n( $format, strtotime( $this->get( 'user_registered' ) ) ); } /** * Determine if the student is active in at least one course or membership * @return boolean * @since 3.14.0 * @version 3.14.0 */ public function is_active() { // this is a simpler, faster query, check first if ( $this->get_membership_levels() ) { return true; } // check for at least one enrolled course $courses = $this->get_courses( array( 'limit' => 1, 'status' => 'enrolled', ) ); if ( $courses['results'] ) { return true; } // not active return false; } /** * Determine if the student has completed a course, track, or lesson * * @param int $object_id WP Post ID of a course or lesson or section or the term id of the track * @param string $type Object type (course, lesson, section, or track) * @return boolean * @since 3.0.0 * @version 3.24.0 */ public function is_complete( $object_id, $type = 'course' ) { // check tracks by progress // this is done because tracks can have the same id as another object... // @todo tracks should have a different table or format since the post_id col won't guarantee uniqueness... if ( 'course_track' === $type ) { $ret = ( 100 == $this->get_progress( $object_id, $type ) ); // everything else can be checked on the postmeta table } else { $query = new LLMS_Query_User_Postmeta( array( 'types' => 'completion', 'include_post_children' => false, 'user_id' => $this->get_id(), 'post_id' => $object_id, 'per_page' => 1, ) ); $ret = $query->has_results(); } return apply_filters( 'llms_is_' . $type . '_complete', $ret, $object_id, $type, $this ); } /** * Determine if the student is a LifterLMS Instructor (of any kind) * Can be admin, manager, instructor, assistant * @return boolean * @since 3.14.0 * @version 3.14.0 */ public function is_instructor() { return $this->user->has_cap( 'lifterlms_instructor' ); } /** * Add student postmeta data for completion of a lesson, section, course or track * @param int $object_id WP Post ID of the lesson, section, course or track * @param string $trigger String describing the reason for mark completion * @return boolean * @since 3.3.1 * @version 3.21.0 */ private function insert_completion_postmeta( $object_id, $trigger = 'unspecified' ) { // add info to the user postmeta table $user_metadatas = array( '_is_complete' => 'yes', '_completion_trigger' => $trigger, ); $update = llms_bulk_update_user_postmeta( $this->get_id(), $object_id, $user_metadatas, false ); // returns an array with errored keys or true on success return is_array( $update ) ? false : true; } /** * Add student postmeta data for incompletion of a lesson, section, course or track * An "_is_complete" value of "no" is inserted into postmeta * @param int $object_id WP Post ID of the lesson, section, course or track * @param string $trigger String describing the reason for mark incompletion * @return boolean * @since 3.5.0 * @version 3.24.0 */ private function insert_incompletion_postmeta( $object_id, $trigger = 'unspecified' ) { global $wpdb; // add '_is_complete' to the user postmeta table for object $user_metadatas = array( '_is_complete' => 'no', '_completion_trigger' => $trigger, ); foreach ( $user_metadatas as $key => $value ) { // It's too difficult to keep track of multiple postmetas for each lesson incomplete // Instead, I'm just replacing the old '_is_complete' value with 'no' // // lessons that have never been complete will not have an '_is_complete' record, // lessons that were completed will have an '_is_complete' record of 'yes', // lessons that have been completed once but were marked incomplete will have an '_is_complete' record of 'no' $update = $wpdb->update( $wpdb->prefix . 'lifterlms_user_postmeta', array( 'user_id' => $this->get_id(), 'post_id' => $object_id, 'meta_key' => $key, 'meta_value' => $value, 'updated_date' => current_time( 'mysql' ), ), array( 'user_id' => $this->get_id(), 'post_id' => $object_id, 'meta_key' => $key, ), array( '%d', '%d', '%s', '%s', '%s' ) ); if ( false === $update ) { return false; } } return true; } /** * Add student postmeta data for enrollment into a course or membership * @param int $product_id WP Post ID of the course or membership * @param string $trigger String describing the reason for enrollment * @return boolean * @since 2.2.3 * @version 3.21.0 */ private function insert_enrollment_postmeta( $product_id, $trigger = 'unspecified' ) { // add info to the user postmeta table $user_metadatas = array( '_enrollment_trigger' => $trigger, '_start_date' => 'yes', '_status' => 'enrolled', ); $update = llms_bulk_update_user_postmeta( $this->get_id(), $product_id, $user_metadatas, false ); // returns an array with errored keys or true on success return is_array( $update ) ? false : true; } /** * Remove student enrollment postmeta for a given product. * * @since 3.33.0 * * @param int $product_id WP Post ID of the course or membership. * @param string $trigger Optional. String the reason for enrollment. Default `null` * @return boolean Whether or not the enrollment records have been succesfully removed. */ private function delete_enrollment_postmeta( $product_id, $trigger = null ) { // delete info from the user postmeta table $user_metadatas = array( '_enrollment_trigger' => $trigger, '_start_date' => null, '_status' => null, ); $delete = llms_bulk_delete_user_postmeta( $this->get_id(), $product_id, $user_metadatas ); return is_array( $delete ) ? false : true; } /** * Add a new status record to the user postmeta table for a specific product * @param int $product_id WP Post ID of the course or membership * @param string $status string describing the new status * @param string $trigger String describing the reason for enrollment (optional) * @return boolean * @since 3.0.0 * @version 3.21.0 */ private function insert_status_postmeta( $product_id, $status = '', $trigger = null ) { $update = llms_update_user_postmeta( $this->get_id(), $product_id, '_status', $status, false ); if ( $update && $trigger ) { $update = llms_update_user_postmeta( $this->get_id(), $product_id, '_enrollment_trigger', $trigger, false ); } return $update; } /** * Determine if a student is enrolled in a Course or Membership. * * @see llms_is_user_enrolled() * * @param int|array $product_id WP Post ID of a Course, Lesson, or Membership or array of multiple IDs. * @param string $relation Comparator for enrollment check. * All = user must be enrolled in all $product_ids. * Any = user must be enrolled in at least one of the $product_ids. * @param bool $use_cache If true, returns cached data if available, if false will run a db query. * * @return boolean * * @since 3.0.0 * @version 3.25.0 */ public function is_enrolled( $product_ids = null, $relation = 'all', $use_cache = true ) { // Assume enrollment unless we find otherwise. $ret = true; // Allow a single product ID to be submitted (backwards compat) $product_ids = ! is_array( $product_ids ) ? array( $product_ids ) : $product_ids; foreach ( $product_ids as $id ) { $enrolled = ( 'enrolled' === strtolower( $this->get_enrollment_status( $id, $use_cache ) ) ); // If use must be enrolled in all products and one is not enrolled: quit the loop & return false. if ( 'all' === $relation && ! $enrolled ) { $ret = false; break; // If user must be enrolled in any } elseif ( 'any' === $relation ) { // If we find an enrollment: return true and quit the loop. if ( $enrolled ) { $ret = true; break; // If not switch return to false but keep looking. } else { $ret = false; } } } return apply_filters( 'llms_is_user_enrolled', $ret, $this, $product_ids, $relation, $use_cache ); } /** * Mark a lesson, section, course, or track complete for the given user * @param int $object_id WP Post ID of the lesson, section, course, or track * @param string $object_type object type [lesson|section|course|track] * @param string $trigger String describing the reason for marking complete * @return boolean * * @see llms_mark_complete() calls this function without having to instantiate the LLMS_Student class first * * @since 3.3.1 * @version 3.17.1 */ public function mark_complete( $object_id, $object_type, $trigger = 'unspecified' ) { // short circuit if it's already completed if ( $this->is_complete( $object_id, $object_type ) ) { return true; } return $this->update_completion_status( 'complete', $object_id, $object_type, $trigger ); } /** * Mark a lesson, section, course, or track incomplete for the given user * Gives an "_is_complete" value of "no" for the given object * @param int $object_id WP Post ID of the lesson, section, course, or track * @param string $object_type object type [lesson|section|course|track] * @param string $trigger String describing the reason for marking incomplete * @return boolean * * @see llms_mark_incomplete() calls this function without having to instantiate the LLMS_Student class first * * @since 3.5.0 * @version 3.17.0 */ public function mark_incomplete( $object_id, $object_type, $trigger = 'unspecified' ) { return $this->update_completion_status( 'incomplete', $object_id, $object_type, $trigger ); } /** * Remove a student from a membership level * @param int $membership_id WP Post ID of the membership * @param string $status status to update the removal to * @return void * @since 2.7 * @version 3.7.5 */ private function remove_membership_level( $membership_id, $status = 'expired' ) { // remove the user from the membership level $membership_levels = $this->get_membership_levels(); $key = array_search( $membership_id, $membership_levels ); if ( false !== $key ) { unset( $membership_levels[ $key ] ); } update_user_meta( $this->get_id(), '_llms_restricted_levels', $membership_levels ); global $wpdb; // locate all enrollments triggered by this membership level $q = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}lifterlms_user_postmeta WHERE user_id = %d AND meta_key = '_enrollment_trigger' AND meta_value = %s", array( $this->get_id(), 'membership_' . $membership_id ) ), 'OBJECT_K' ); $courses = array_keys( $q ); if ( $courses ) { // loop through all the courses and update the enrollment status foreach ( $courses as $course_id ) { $this->unenroll( $course_id, 'membership_' . $membership_id, $status ); } } } /** * Remove a student from a LifterLMS course or membership * * @param int $product_id WordPress Post ID of the course or membership * @param string $trigger only remove the student if the original enrollment trigger matches the submitted value * "any" will remove regardless of enrollment trigger * @param string $new_status the value to update the new status with after removal is complete * @return boolean * * @see llms_unenroll_student() calls this function without having to instantiate the LLMS_Student class first * * @since 3.0.0 * @version 3.26.0 */ public function unenroll( $product_id, $trigger = 'any', $new_status = 'expired' ) { // can only unenroll those are a currently enrolled if ( ! $this->is_enrolled( $product_id, 'all', false ) ) { return false; } // assume we can't unenroll $update = false; // if trigger is "any" we'll just unenroll regardless of the trigger if ( 'any' === $trigger ) { $update = true; } // End if(). else { $enrollment_trigger = $this->get_enrollment_trigger( $product_id ); // no enrollment trigger exists b/c pre 3.0.0 enrollment, unenroll the user if ( ! $enrollment_trigger ) { $update = apply_filters( 'lifterlms_legacy_unenrollment_action', true ); } // End if(). elseif ( $enrollment_trigger == $trigger ) { $update = true; } } // update if we can if ( $update ) { // update enrollment for the product if ( $this->insert_status_postmeta( $product_id, $new_status ) ) { // update the cache $this->cache_set( sprintf( 'enrollment_status_%d', $product_id ), $new_status ); $this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) ); $this->cache_delete( sprintf( 'date_updated_%d', $product_id ) ); // trigger actions based on product type switch ( get_post_type( $product_id ) ) { case 'course': do_action( 'llms_user_removed_from_course', $this->get_id(), $product_id ); break; case 'llms_membership': // also physically remove from the membership level & perform unenrollment // on related products $this->remove_membership_level( $product_id, $new_status ); do_action( 'llms_user_removed_from_membership_level', $this->get_id(), $product_id ); break; } return true; } } // return false if we didn't update return false; } /** * Delete a student enrollment. * * @since 3.33.0 * * @see llms_delete_student_enrollment() calls this function without having to instantiate the LLMS_Student class first. * * @param int $product_id WP Post ID of the course or membership. * @param string $trigger Optional. Only delete the student's enrollment if the original enrollment trigger matches the submitted value * "any" will remove regardless of enrollment trigger. Default "any". * @return boolean Whether or not the enrollment records have been succesfully removed. */ public function delete_enrollment( $product_id, $trigger = 'any' ) { // assume we can't delete the enrollment $delete = false; // if trigger is "any" we'll just delete the enrollment regardless of the trigger. if ( 'any' === $trigger ) { $delete = true; } // End if(). else { $enrollment_trigger = $this->get_enrollment_trigger( $product_id ); // no enrollment trigger exists b/c pre 3.0.0 enrollment, delete the user's enrollment. if ( ! $enrollment_trigger ) { $delete = apply_filters( 'lifterlms_legacy_delete_enrollment_action', true ); } // End if(). elseif ( $enrollment_trigger === $trigger ) { $delete = true; } } // delete if we can if ( $delete ) { // delete enrollment for the product if ( $this->delete_enrollment_postmeta( $product_id ) ) { // clean the cache $this->cache_delete( sprintf( 'enrollment_status_%d', $product_id ) ); $this->cache_delete( sprintf( 'date_enrolled_%d', $product_id ) ); $this->cache_delete( sprintf( 'date_updated_%d', $product_id ) ); // trigger action do_action( 'llms_user_enrollment_deleted', $this->get_id(), $product_id ); return true; } } // return false if we didn't remove anything return false; } /** * Update the completion status of a track, course, section, or lesson for the current student * Cascades up to parents and clears progress caches for parents * Triggers actions for completion/incompletion * Inserts / updates necessary user postmeta data * @param string $status new status to update to [complete|incomplete] * @param int $object_id WP Post ID of the lesson, section, course, or track * @param string $object_type object type [lesson|section|course|course_track] * @param string $trigger String describing the reason for marking complete * @return boolean * @since 3.17.0 * @version 3.17.0 */ private function update_completion_status( $status, $object_id, $object_type, $trigger = 'unspecified' ) { /** * Before hook * @action before_llms_mark_complete * @action before_llms_mark_incomplete */ do_action( 'before_llms_mark_' . $status, $this->get_id(), $object_id, $object_type, $trigger ); // can only be marked incomplete in the following post types if ( in_array( $object_type, apply_filters( 'llms_completable_post_types', array( 'course', 'lesson', 'section' ) ) ) ) { $object = llms_get_post( $object_id ); } elseif ( 'course_track' === $object_type ) { $object = get_term( $object_id, 'course_track' ); } else { return false; } // parent(s) to cascade up and check for incompletion // lessons -> section -> course -> track(s) $parent_ids = array(); $parent_type = false; // lessons are complete / incomplete automatically // other object types are dependent on their children's statuses // so the other object types need to check progress manually (bypassing cache) to see if it's complete / incomplete $complete = ( 'lesson' === $object_type ) ? ( 'complete' === $status ) : ( 100 == $this->get_progress( $object_id, $object_type, false ) ); // get the immediate parent so we can cascade up and maybe mark the parent as incomplete as well switch ( $object_type ) { case 'lesson': $parent_ids = array( $object->get( 'parent_section' ) ); $parent_type = 'section'; break; case 'section': $parent_ids = array( $object->get( 'parent_course' ) ); $parent_type = 'course'; break; case 'course': $parent_ids = wp_list_pluck( $object->get_tracks(), 'term_id' ); $parent_type = 'course_track'; break; } // reset the cached progress for any objects with children if ( 'lesson' !== $object_type ) { $this->set( sprintf( '%1$s_%2$d_progress', $object_type, $object_id ), '' ); } // reset cache for all parents if ( $parent_ids && $parent_type ) { foreach ( $parent_ids as $pid ) { $this->set( sprintf( '%1$s_%2$d_progress', $parent_type, $pid ), '' ); } } // determine if an update should be made $update = ( 'complete' === $status && $complete ) || ( 'incomplete' === $status && ! $complete ); if ( $update ) { // insert meta data if ( 'complete' === $status ) { $this->insert_completion_postmeta( $object_id, $trigger ); } elseif ( 'incomplete' === $status ) { $this->insert_incompletion_postmeta( $object_id, $trigger ); } /** * Generic hook * @action llms_mark_complete * @action llms_mark_incomplete */ do_action( 'llms_mark_' . $status, $this->get_id(), $object_id, $object_type, $trigger ); /** * Specific hook * Also backwards compatible * @action lifterlms_{$object_type}_completed * @action lifterlms_{$object_type}_incompleted */ do_action( 'lifterlms_' . $object_type . '_' . $status . 'd', $this->get_id(), $object_id ); // cascade up for parents if ( $parent_ids && $parent_type ) { foreach ( $parent_ids as $pid ) { $this->update_completion_status( $status, $pid, $parent_type, $trigger ); } } /** * Generic after hook * @action after_llms_mark_complete * @action after_llms_mark_incomplete */ do_action( 'after_llms_mark_' . $status, $this->get_id(), $object_id, $object_type, $trigger ); }// End if(). return $update; } /* /$$ /$$ /$$ | $$ | $$ | $$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$_____/ |____ $$|_ $$_/ /$$__ $$ /$$__ $$ | $$ | $$| $$$$$$$$| $$ \ $$| $$ \__/| $$$$$$$$| $$ /$$$$$$$ | $$ | $$$$$$$$| $$ | $$ | $$ | $$| $$_____/| $$ | $$| $$ | $$_____/| $$ /$$__ $$ | $$ /$$| $$_____/| $$ | $$ | $$$$$$$| $$$$$$$| $$$$$$$/| $$ | $$$$$$$| $$$$$$$| $$$$$$$ | $$$$/| $$$$$$$| $$$$$$$ \_______/ \_______/| $$____/ |__/ \_______/ \_______/ \_______/ \___/ \_______/ \_______/ | $$ | $$ |__/ */ /** * Remove Student Quiz attempts * @param int $quiz_id WP Post ID of a Quiz * @param int $lesson_id WP Post ID of a lesson * @param int $attempt optional attempt number, if omitted all attempts for quiz & lesson will be deleted * @return array updated array quiz data for the student * @since 3.4.4 * @version 3.9.0 */ public function delete_quiz_attempt( $quiz_id, $lesson_id, $attempt = null ) { llms_deprecated_function( 'LLMS_Student->delete_quiz_attempt()', '3.9.0', 'LLMS_Student->quizzes()->delete_attempt()' ); return $this->quizzes()->delete_attempt( $quiz_id, $lesson_id, $attempt ); } /** * Get the quiz attempt with the highest grade for a given quiz and lesson combination * @param int $quiz_id WP Post ID of a Quiz * @param int $lesson_id WP Post ID of a lesson * @return array * @since 3.9.0 * @version 3.9.0 */ public function get_best_quiz_attempt( $quiz = null, $lesson = null ) { llms_deprecated_function( 'LLMS_Student->get_best_quiz_attempt()', '3.9.0', 'LLMS_Student->quizzes()->get_best_attempt()' ); return $this->quizzes()->get_best_attempt( $quiz, $lesson ); } /** * Retrieve quiz data for a student for a lesson / quiz combination * @param int $quiz WP Post ID of a Quiz * @param int $lesson WP Post ID of a lesson * @return array * @since 3.2.0 * @version 3.9.0 */ public function get_quiz_data( $quiz = null, $lesson = null ) { llms_deprecated_function( 'LLMS_Student->get_quiz_data()', '3.9.0', 'LLMS_Student->quizzes()->get_all()' ); return $this->quizzes()->get_all( $quiz, $lesson ); } /** * Determine if a student has access to a product's content * @param int $product_id WP Post ID of a course or membership * @return boolean * @since 3.0.0 * @version 3.12.2 * @deprecated 3.12.2 This function previously differed from $this->is_enrolled() by * checking the status of an order and only returning true when * the order status and enrollment status were both true * this causes issues when a student is expired from a limited-access product * and is then manually re-enrolled by an admin * there is no way to change the access expiration information * and the enrollment status says "Enrolled" but the student still cannot * access the content * * Additionally redundant due to the fact that access is expired automatically * via action scheduler `do_action( 'llms_access_plan_expiration', $order_id );` * This action changes the enrollment status thereby rendering this additional * access check redundant, confusing, unnecessary */ public function has_access( $product_id ) { llms_deprecated_function( 'LLMS_Student::has_access()', '3.12.2', 'LLMS_Student::is_enrolled()' ); return $this->is_enrolled( $product_id ); } }
Expand full source code Collapse full source code View on GitHub
Changelog Changelog
Version | Description |
---|---|
3.33.0 | Added the delete_enrollment_postmeta private method that allows student's enrollment postmeta deletion. |
2.2.3 | Introduced. |
Methods Methods
- add_membership_level — Add the student to a LifterLMS Membership
- delete_enrollment — Delete a student enrollment.
- delete_enrollment_postmeta — Remove student enrollment postmeta for a given product.
- delete_quiz_attempt — Remove Student Quiz attempts
- enroll — Enroll the student in a course or membership
- get_achievements — Retrieve achievements that a user has earned
- get_avatar
- get_best_quiz_attempt — Get the quiz attempt with the highest grade for a given quiz and lesson combination
- get_certificates — Retrieve certificates that a user has earned
- get_completed_courses — Retrieve IDs of courses a user has completed
- get_completion_date — Get the formatted date when a course or lesson was completed by the student
- get_courses — Retrieve IDs of user's courses based on supplied criteria
- get_enrollment_date — Get the formatted date when a user initially enrolled in a product or when they were last updated
- get_enrollment_order — Retrieve the order which enrolled a student in a given course or membership Retrieves the most recently updated order for the given product
- get_enrollment_status — Get the current enrollment status of a student for a particular product
- get_enrollment_trigger — Get the enrollment trigger for a the student's enrollment in a course
- get_enrollment_trigger_id — Get the enrollment trigger id for a the student's enrollment in a course
- get_enrollments — Retrieve IDs of user's enrollments by post type (and additional criteria)
- get_events — Retrieve postmeta events related to the student
- get_grade — Get the students grade for a lesson / course All grades are based on quizzes assigned to lessons
- get_last_completed_lesson — Get the students last completed lesson in a course
- get_membership_levels — Retrieve an array of Membership Levels for a user
- get_memberships — Retrieve IDs of user's memberships based on supplied criteria
- get_name — Get the full name of a student
- get_next_lesson — Get the next lesson a student needs to complete in a course
- get_notification_subscription — Retrieve a user's notification subscription preferences for a given type & trigger
- get_orders
- get_overall_grade — Retrieve the student's overall grade Grade = sum of grades for all courses divided by number of enrolled courses if a course has no quizzes in it, it cannot be graded and is therefore excluded from the calculation
- get_overall_progress — Retrieve a student's overall progress Overall progress is the total percentage completed based on all courses the student is enrolled in Cached data is cleared every time the student completes a lesson
- get_progress — Get students progress through a course or track
- get_quiz_data — Retrieve quiz data for a student for a lesson / quiz combination
- get_registration_date — Retrieve the Students original registration date in chosen format
- has_access — Determine if a student has access to a product's content — deprecated
- insert_completion_postmeta — Add student postmeta data for completion of a lesson, section, course or track
- insert_enrollment_postmeta — Add student postmeta data for enrollment into a course or membership
- insert_incompletion_postmeta — Add student postmeta data for incompletion of a lesson, section, course or track An "_is_complete" value of "no" is inserted into postmeta
- insert_status_postmeta — Add a new status record to the user postmeta table for a specific product
- instructor — Retrieve an instance of the LLMS_Instructor model for the current user
- is_active — Determine if the student is active in at least one course or membership
- is_complete — Determine if the student has completed a course, track, or lesson
- is_enrolled — Determine if a student is enrolled in a Course or Membership.
- is_instructor — Determine if the student is a LifterLMS Instructor (of any kind) Can be admin, manager, instructor, assistant
- mark_complete — Mark a lesson, section, course, or track complete for the given user
- mark_incomplete — Mark a lesson, section, course, or track incomplete for the given user Gives an "_is_complete" value of "no" for the given object
- quizzes — Retrieve an instance of the student quiz data model
- remove_membership_level — Remove a student from a membership level
- unenroll — Remove a student from a LifterLMS course or membership
- update_completion_status — Update the completion status of a track, course, section, or lesson for the current student Cascades up to parents and clears progress caches for parents Triggers actions for completion/incompletion Inserts / updates necessary user postmeta data
User Contributed Notes User Contributed Notes
Permalink: