gocodebox/lifterlms

View on GitHub
includes/models/model.llms.course.php

Summary

Maintainability
C
1 day
Test Coverage
B
84%
<?php
/**
 * LifterLMS Course Model
 *
 * @package LifterLMS/Models/Classes
 *
 * @since 1.0.0
 * @version 7.2.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_Course model class.
 *
 * @since 1.0.0
 * @since 3.30.3 Explicitly define class properties.
 * @since 4.0.0 Remove previously deprecated class methods.
 * @since 5.2.1 Check for an empty sales page URL or ID.
 * @since 5.3.0 Move audio and video embed methods to `LLMS_Trait_Audio_Video_Embed`.
 *              Move sales page methods to `LLMS_Trait_Sales_Page`.
 * @since 6.0.0 Removed deprecated items.
 *              - `LLMS_Course::sections` property
 *              - `LLMS_Course::sku` property
 *
 * @property string $audio_embed                URL to an oEmbed enable audio URL.
 * @property float  $average_grade              Calculated value of the overall average grade of all *enrolled* students in the course..
 * @property float  $average_progress           Calculated value of the overall average progress of all *enrolled* students in the course..
 * @property int    $capacity                   Number of students who can be enrolled in the course before enrollment closes.
 * @property string $capacity_message           Message displayed when capacity has been reached.
 * @property string $content_restricted_message Message displayed when non-enrolled visitors try to access lessons/quizzes directly.
 * @property string $course_closed_message      Message displayed to visitors when the course is accessed after the Course End Date has passed. Only applicable when $time_period is 'yes'.
 * @property string $course_opens_message       Message displayed to visitors when the course is accessed before the Course Start Date has passed. Only applicable when $time_period is 'yes'.
 * @property string $enable_capacity            Whether capacity restrictions are enabled [yes|no].
 * @property string $enrollment_closed_message  Message displayed to non-enrolled visitors when the course is accessed after the Enrollment End Date has passed. Only applicable when $enrollment_period is 'yes'.
 * @property string $enrollment_end_date        After this date, registration closes.
 * @property string $enrollment_opens_message   Message displayed to non-enrolled visitors when the course is accessed before the Enrollment Start Date has passed. Only applicable when $enrollment_period is 'yes'.
 * @property string $enrollment_period          Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned).
 * @property string $enrollment_start_date      Before this date, registration is closed.
 * @property string $end_date                   Date when a course closes. Students may no longer view content or complete lessons / quizzes after this date..
 * @property string $has_prerequisite           Determine if prerequisites are enabled [yes|no].
 * @property array  $instructors                Course instructor user information.
 * @property int    $prerequisite               WP Post ID of a the prerequisite course.
 * @property int    $prerequisite_track         WP Tax ID of a the prerequisite track.
 * @property string $start_date                 Date when a course is opens. Students may register before this date but can only view content and complete lessons or quizzes after this date..
 * @property string $length                     User defined course length.
 * @property int    $sales_page_content_page_id WP Post ID of the WP page to redirect to when $sales_page_content_type is 'page'.
 * @property string $sales_page_content_type    Sales page behavior [none,content,page,url].
 * @property string $sales_page_content_url     Redirect URL for a sales page, when $sales_page_content_type is 'url'.
 * @property string $tile_featured_video        Displays the featured video instead of the featured image on course tiles [yes|no].
 * @property string $time_period                Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned).
 * @property string $video_embed                URL to an oEmbed enable video URL.
 */
class LLMS_Course extends LLMS_Post_Model implements LLMS_Interface_Post_Instructors {

    use LLMS_Trait_Audio_Video_Embed;
    use LLMS_Trait_Sales_Page;

    /**
     * Meta properties.
     *
     * @var array
     */
    protected $properties = array(

        // Public.
        'average_grade'              => 'float',
        'average_progress'           => 'float',
        'capacity'                   => 'absint',
        'capacity_message'           => 'text',
        'course_closed_message'      => 'text',
        'course_opens_message'       => 'text',
        'content_restricted_message' => 'text',
        'enable_capacity'            => 'yesno',
        'end_date'                   => 'text',
        'enrolled_students'          => 'absint',
        'enrollment_closed_message'  => 'text',
        'enrollment_end_date'        => 'text',
        'enrollment_opens_message'   => 'text',
        'enrollment_period'          => 'yesno',
        'enrollment_start_date'      => 'text',
        'has_prerequisite'           => 'yesno',
        'instructors'                => 'array',
        'length'                     => 'text',
        'prerequisite'               => 'absint',
        'prerequisite_track'         => 'absint',
        'tile_featured_video'        => 'yesno',
        'time_period'                => 'yesno',
        'start_date'                 => 'text',
        'lesson_drip'                => 'yesno',
        'drip_method'                => 'text',
        'ignore_lessons'             => 'absint',
        'days_before_available'      => 'absint',

        // Private.
        'temp_calc_data'             => 'array',
        'last_data_calc_run'         => 'absint',

    );

    /**
     * Default property values
     *
     * @var array
     */
    protected $property_defaults = array(
        'enrolled_students' => 0,
    );

    /**
     * DB post type name.
     *
     * @var string
     */
    protected $db_post_type = 'course';

    /**
     * Model post type name.
     *
     * @var string
     */
    protected $model_post_type = 'course';

    /**
     * Constructor for this class and the traits it uses.
     *
     * @since 5.3.0
     *
     * @param string|int|LLMS_Post_Model|WP_Post $model 'new', WP post id, instance of an extending class, instance of WP_Post.
     * @param array                              $args  Args to create the post, only applies when $model is 'new'.
     */
    public function __construct( $model, $args = array() ) {

        $this->construct_audio_video_embed();
        $this->construct_sales_page();
        parent::__construct( $model, $args );
    }

    /**
     * Retrieve an instance of the Post Instructors model
     *
     * @since 3.13.0
     *
     * @return LLMS_Post_Instructors
     */
    public function instructors() {
        return new LLMS_Post_Instructors( $this );
    }

    /**
     * Retrieve the total points available for the course
     *
     * @since 3.24.0
     *
     * @return int
     */
    public function get_available_points() {
        $points = 0;
        foreach ( $this->get_lessons() as $lesson ) {
            $points += $lesson->get( 'points' );
        }

        /**
         * Filters the total available points for the course.
         *
         * @since 3.24.0
         *
         * @param int         $points Number of available points.
         * @param LLMS_Course $course Course object.
         */
        return apply_filters( 'llms_course_get_available_points', $points, $this );
    }

    /**
     * Get course's prerequisite id based on the type of prerequisite
     *
     * @since 3.0.0
     * @since 3.7.3 Unknown.
     *
     * @param string $type Optional. Type of prereq to retrieve id for [course|track]. Default is 'course'.
     * @return int|false Post ID of a course, taxonomy ID of a track, or false if none found.
.     */
    public function get_prerequisite_id( $type = 'course' ) {

        if ( $this->has_prerequisite( $type ) ) {

            switch ( $type ) {

                case 'course':
                    $key = 'prerequisite';
                    break;

                case 'course_track':
                    $key = 'prerequisite_track';
                    break;

            }

            if ( isset( $key ) ) {
                return $this->get( $key );
            }
        }

        return false;

    }

    /**
     * Retrieve course categories
     *
     * @since 3.3.0
     *
     * @param array $args Array of args passed to wp_get_post_terms.
     * @return array
     */
    public function get_categories( $args = array() ) {
        return wp_get_post_terms( $this->get( 'id' ), 'course_cat', $args );
    }

    /**
     * Get Difficulty
     *
     * @since 1.0.0
     * @since 3.24.0 Unknown.
     * @since 7.2.0 Added support for showing multiple difficulties.
     *
     * @param string $field Optional. Which field to return from the available term fields.
     *                      Any public variables from a WP_Term object are acceptable: term_id, name, slug, and more.
     *                      Default is 'name'.
     * @return string
     */
    public function get_difficulty( $field = 'name' ) {

        $terms = get_the_terms( $this->get( 'id' ), 'course_difficulty' );

        if ( false === $terms ) {
            return '';
        }

        $difficulties = wp_list_pluck( $terms, $field );
        return implode( ', ', $difficulties );

    }

    /**
     * Retrieve course instructor information
     *
     * @since 3.13.0
     *
     * @param boolean $exclude_hidden Optional. If true, excludes hidden instructors from the return array. Default is `false`.
     * @return array
     */
    public function get_instructors( $exclude_hidden = false ) {

        /**
         * Filters the course's instructors list
         *
         * @since 3.13.0
         *
         * @param array       $instructors    Instructor data array.
         * @param LLMS_Course $course         Course object.
         * @param boolearn    $exclude_hidden If true, excludes hidden instructors from the return array.
         */
        return apply_filters(
            'llms_course_get_instructors',
            $this->instructors()->get_instructors( $exclude_hidden ),
            $this,
            $exclude_hidden
        );

    }

    /**
     * Get course lessons
     *
     * @since 3.0.0
     * @since 3.24.0 Unknown.
     *
     * @param string $return Optional. Type of return [ids|posts|lessons]. Default is 'lessons'.
     * @return int[]|WP_Post[]|LLMS_Lesson[] The type depends on value of `$return`.
     */
    public function get_lessons( $return = 'lessons' ) {

        $lessons = array();
        foreach ( $this->get_sections( 'sections' ) as $section ) {
            $lessons = array_merge( $lessons, $section->get_lessons( 'posts' ) );
        }

        if ( 'ids' === $return ) {
            $ret = wp_list_pluck( $lessons, 'ID' );
        } elseif ( 'posts' === $return ) {
            $ret = $lessons;
        } else {
            $ret = array_map( 'llms_get_post', $lessons );
        }
        return $ret;

    }

    /**
     * Retrieve the number of course's lessons.
     *
     * This is less expensive than counting the result of {@see LLMS_Course::get_lessons()},
     * and should be preferred when you only need to count the number of lessons of a course.
     *
     * @since 7.1.0
     *
     * @return int
     */
    public function get_lessons_count() {

        $query = new WP_Query(
            array(
                'meta_key'               => '_llms_parent_course',
                'meta_value'             => $this->get( 'id' ),
                'post_type'              => 'lesson',
                'posts_per_page'         => -1,
                'no_found_rows'          => true,
                'update_post_meta_cache' => false,
                'update_post_term_cache' => false,
                'fields'                 => 'ids',
                'orderby'                => 'ID',
                'order'                  => 'ASC',
            )
        );

        return $query->post_count;

    }

    /**
     * Retrieve an array of quizzes within a course
     *
     * @since 3.12.0
     * @since 3.16.0 Unknown.
     *
     * @return int[] Array of WP_Post IDs of the quizzes.
     */
    public function get_quizzes() {

        $quizzes = array();
        foreach ( $this->get_lessons( 'lessons' ) as $lesson ) {
            if ( $lesson->has_quiz() ) {
                $quizzes[] = $lesson->get( 'quiz' );
            }
        }
        return $quizzes;

    }

    /**
     * Get course sections
     *
     * @since 3.0.0
     * @since 3.24.0 Unknown.
     *
     * @param string $return Optional. Type of return [ids|posts|sections]. Default is 'sections'.
     * @return int[]|WP_Post[]|LLMS_Section[] The type depends on value of `$return`.
     */
    public function get_sections( $return = 'sections' ) {

        $q = new WP_Query(
            array(
                'meta_key'       => '_llms_order',
                'meta_query'     => array(
                    array(
                        'key'   => '_llms_parent_course',
                        'value' => $this->id,
                    ),
                ),
                'order'          => 'ASC',
                'orderby'        => 'meta_value_num',
                'post_type'      => 'section',
                'posts_per_page' => 500,
            )
        );

        if ( 'ids' === $return ) {
            $r = wp_list_pluck( $q->posts, 'ID' );
        } elseif ( 'posts' === $return ) {
            $r = $q->posts;
        } else {
            $r = array();
            foreach ( $q->posts as $p ) {
                $r[] = new LLMS_Section( $p );
            }
        }

        return $r;

    }

    /**
     * Retrieve the number of enrolled students in the course
     *
     * The cached value is calculated in the `LLMS_Processor_Course_Data` background processor.
     *
     * If, for whatever reason, it's not found, it will be calculated on demand and saved for later use.
     *
     * @since 3.15.0
     * @since 4.12.0 Use cached value where possible.
     * @since 6.0.0 Don't access `LLMS_Student_Query` properties directly.
     *
     * @param boolean $skip_cache Default: `false`. Whether or not to bypass the cache. If `true`, bypasses the cache.
     * @return int
     */
    public function get_student_count( $skip_cache = false ) {

        $count = ! $skip_cache ? $this->get( 'enrolled_students' ) : false;

        /**
         * Query enrolled students when `$skip_cache=true` or when there's no stored meta data.
         *
         * The second condition is necessary to disambiguate between a cached `0` and a `0` that's
         * returned as the default value when the metadata doesn't exist.
         */
        if ( false === $count || ! isset( $this->enrolled_students ) ) {

            $query = new LLMS_Student_Query(
                array(
                    'post_id'  => $this->get( 'id' ),
                    'statuses' => array( 'enrolled' ),
                    'per_page' => 1,
                    'sort'     => array(
                        'id' => 'ASC',
                    ),
                )
            );

            $count = $query->get_found_results();

            // Cache result for later use.
            $this->set( 'enrolled_students', $count );

        }

        /**
         * Filter the number of actively enrolled students in the course
         *
         * @since 4.12.0
         *
         * @param int         $count  Number of students enrolled in the course.
         * @param LLMS_Course $course Instance of the course object.
         */
        $count = apply_filters( 'llms_course_get_student_count', $count, $this );

        return absint( $count );

    }

    /**
     * Get an array of student IDs based on enrollment status in the course
     *
     * @since 3.0.0
     *
     * @param string|string[] $statuses Optional. List of enrollment statuses to query by. Students matching at least one of the provided statuses will be returned. Default is 'enrolled'.
     * @param integer         $limit    Optional. Number of results. Default is `50`.
     * @param integer         $skip     Optional. Number of results to skip (for pagination). Default is `0`.
     * @return array
     */
    public function get_students( $statuses = 'enrolled', $limit = 50, $skip = 0 ) {
        return llms_get_enrolled_students( $this->get( 'id' ), $statuses, $limit, $skip );
    }

    /**
     * Retrieve course tags
     *
     * @since 3.3.0
     *
     * @param array $args Array of args passed to wp_get_post_terms.
     * @return array
     */
    public function get_tags( $args = array() ) {
        return wp_get_post_terms( $this->get( 'id' ), 'course_tag', $args );
    }

    /**
     * Get the properties that will be explicitly excluded from the array representation of the model.
     *
     * This stub can be overloaded by an extending class and the property list is filterable via the
     * {@see llms_get_{$this->model_post_type}_excluded_to_array_properties} filter.
     *
     * @since 5.4.1
     *
     * @return string[]
     */
    protected function get_to_array_excluded_properties() {

        /**
         * Disable course property exclusion while running `toArray()`.
         *
         * This hook is intended to allow developers to retain the functionality implemented
         * prior to the introduction of this hook.
         *
         * The LifterLMS developers consider the presence of these properties to be a bug but
         * acknowledge that the removal of these properties could be seen as a backwards incompatible
         * "feature" removal.
         *
         * This hook disables the exclusion of the following properties: 'average_grade', 'average_progress',
         * 'enrolled_students', 'last_data_calc_run', and 'temp_calc_data'. Any excluded properties added in the
         * future will not be excluded when using this hook.
         *
         * @example `add_filter( 'llms_course_to_array_disable_prop_exclusion', '__return_true' );`
         *
         * @since 5.4.1
         *
         * @param boolean $disable Whether or not to disable property exclusions.
         */
        $disable = apply_filters( 'llms_course_to_array_disable_prop_exclusion', false );
        if ( $disable ) {
            return array();
        }

        return array(
            'average_grade',
            'average_progress',
            'enrolled_students',
            'last_data_calc_run',
            'temp_calc_data',
        );
    }

    /**
     * Retrieve course tracks
     *
     * @since 3.3.0
     *
     * @param array $args Array of args passed to wp_get_post_terms.
     * @return array
     */
    public function get_tracks( $args = array() ) {
        return wp_get_post_terms( $this->get( 'id' ), 'course_track', $args );
    }

    /**
     * Retrieve an array of students currently enrolled in the course
     *
     * @since 1.0.0
     * @since 3.0.0 Use `LLMS_Course::get_students()`.
     *
     * @param integer $limit Number of results.
     * @param integer $skip Number of results to skip (for pagination).
     * @return array
     */
    public function get_enrolled_students( $limit, $skip ) {
        return $this->get_students( 'enrolled', $limit, $skip );
    }

    /**
     * Get a user's percentage completion through the course
     *
     * @since 1.0.0
     * @since 3.17.2 Unknown.
     *
     * @return float
     */
    public function get_percent_complete( $user_id = '' ) {

        $student = llms_get_student( $user_id );
        if ( ! $student ) {
            return 0;
        }
        return $student->get_progress( $this->get( 'id' ), 'course' );

    }

    /**
     * Retrieve an instance of the LLMS_Product for this course
     *
     * @since 3.3.0
     *
     * @return LLMS_Product
     */
    public function get_product() {
        return new LLMS_Product( $this->get( 'id' ) );
    }

    /**
     * Compare a course meta info date to the current date and get a bool
     *
     * @since 3.0.0
     *
     * @param string $date_key Property key, eg "start_date" or "enrollment_end_date".
     * @return boolean Returns `true` when the date is in the past and `false` when the date is in the future.
     */
    public function has_date_passed( $date_key ) {

        $now  = current_time( 'timestamp' );
        $date = $this->get_date( $date_key, 'U' );

        /**
         * If there's no date, we can't make a comparison
         * so assume it's unset and unnecessary
         * so return 'false'.
         */
        if ( ! $date ) {
            return false;

        }

        return $now > $date;

    }

    /**
     * Determine if the course is at capacity based on course capacity settings
     *
     * @since 3.0.0
     * @since 3.15.0 Unknown.
     *
     * @return boolean Returns `true` if not at capacity & `false` if at or over capacity.
     */
    public function has_capacity() {

        // Capacity disabled, so there is capacity.
        if ( 'yes' !== $this->get( 'enable_capacity' ) ) {
            return true;
        }

        $capacity = $this->get( 'capacity' );
        // No capacity restriction set, so it has capacity.
        if ( ! $capacity ) {
            return true;
        }

        // Compare results.
        return ( $this->get_student_count() < $capacity );

    }

    /**
     * Determine if prerequisites are enabled and there are prereqs configured
     *
     * @since 3.0.0
     * @since 3.7.5 Unknown.
     *
     * @param string $type Determine if a specific type of prereq exists [any|course|track].
     * @return boolean Returns true if prereq is enabled and there is a prerequisite course or track.
     */
    public function has_prerequisite( $type = 'any' ) {

        if ( 'yes' === $this->get( 'has_prerequisite' ) ) {

            if ( 'any' === $type ) {

                return ( $this->get( 'prerequisite' ) || $this->get( 'prerequisite_track' ) );

            } elseif ( 'course' === $type ) {

                return ( $this->get( 'prerequisite' ) ) ? true : false;

            } elseif ( 'course_track' === $type ) {

                return ( $this->get( 'prerequisite_track' ) ) ? true : false;

            }
        }

        return false;

    }

    /**
     * Determine if students can access course content based on the current date
     *
     * @since 3.0.0
     * @since 3.7.0 Unknown.
     *
     * @return boolean
     */
    public function is_enrollment_open() {

        // If no period is set, enrollment is automatically open.
        if ( 'yes' !== $this->get( 'enrollment_period' ) ) {

            $is_open = true;

        } else {

            $is_open = ( $this->has_date_passed( 'enrollment_start_date' ) && ! $this->has_date_passed( 'enrollment_end_date' ) );

        }

        /**
         * Filters whether or not course enrollment is open.
         *
         * @since Unknown
         *
         * @param boolean     $is_open Whether or not enrollment is open.
         * @param LLMS_Course $course  Course object.
         */
        return apply_filters( 'llms_is_course_enrollment_open', $is_open, $this );

    }

    /**
     * Determine if students can access course content based on the current date
     *
     * Note that enrollment does not affect the outcome of this check as regardless
     * of enrollment, once a course closes content is locked.
     *
     * @since 3.0.0
     * @since 3.7.0 Unknown.
     *
     * @return boolean
     */
    public function is_open() {

        // If a course time period is not enabled, just return true (content is accessible).
        if ( 'yes' !== $this->get( 'time_period' ) ) {

            $is_open = true;

        } else {

            $is_open = ( $this->has_date_passed( 'start_date' ) && ! $this->has_date_passed( 'end_date' ) );

        }

        /**
         * Filters whether or not the course is considered open based on the current date.
         *
         * @since Unknown
         *
         * @param boolean     $is_open Whether or not enrollment is open.
         * @param LLMS_Course $course  Course object.
         */
        return apply_filters( 'llms_is_course_open', $is_open, $this );

    }

    /**
     * Determine if a prerequisite is completed for a student
     *
     * @since 3.0.0
     *
     * @param string $type Type of prereq [course|track].
     * @return boolean
     */
    public function is_prerequisite_complete( $type = 'course', $student_id = null ) {

        if ( ! $student_id ) {
            $student_id = get_current_user_id();
        }

        // No user or no prereqs so no reason to proceed.
        if ( ! $student_id || ! $this->has_prerequisite( $type ) ) {
            return false;
        }

        $prereq_id = $this->get_prerequisite_id( $type );

        // No prereq id of this type, no need to proceed.
        if ( ! $prereq_id ) {
            return false;
        }

        // Setup student.
        $student = new LLMS_Student( $student_id );

        return $student->is_complete( $prereq_id, $type );

    }

    /**
     * Save instructor information
     *
     * @since 3.13.0
     *
     * @param array $instructors Array of course instructor information.
     */
    public function set_instructors( $instructors = array() ) {

        return $this->instructors()->set_instructors( $instructors );

    }

    /**
     * Add data to the course model when converted to array
     *
     * Called before data is sorted and returned by $this->jsonSerialize().
     *
     * @since 3.3.0
     * @since 3.8.0 Unknown.
     *
     * @param array $arr Data to be serialized.
     * @return array
     */
    public function toArrayAfter( $arr ) {

        $product             = $this->get_product();
        $arr['access_plans'] = array();
        foreach ( $product->get_access_plans( false, false ) as $p ) {
            $arr['access_plans'][] = $p->toArray();
        }

        $arr['sections'] = array();
        foreach ( $this->get_sections() as $s ) {
            $arr['sections'][] = $s->toArray();
        }

        $arr['categories'] = $this->get_categories(
            array(
                'fields' => 'names',
            )
        );
        $arr['tags']       = $this->get_tags(
            array(
                'fields' => 'names',
            )
        );
        $arr['tracks']     = $this->get_tracks(
            array(
                'fields' => 'names',
            )
        );

        $arr['difficulty'] = $this->get_difficulty();

        return $arr;

    }

}