includes/models/model.llms.course.php
<?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;
}
}