<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* This class is loaded int WP by the shortcode loader class.
*
* Renders the [sensei_user_courses] shortcode to all courses the current user is taking
*
* Shortcode parameters:
* number - how many courses you'd like to show
* orderby - the same as the WordPress orderby query parameter
* order - ASC | DESC
* status - complete | active if none specified it will default to all
*
* If all courses for a given user is shown, there will also be a toggle link between active and complete. Please note
* that the number you specify will be respected.
*
* @class Sensei_Shortcode_User_Courses
*
* @package Content
* @subpackage Shortcode
* @author Automattic
*
* @since 1.9.0
*/
class Sensei_Shortcode_User_Courses implements Sensei_Shortcode_Interface {
/**
* The name of the status filter HTTP query param.
*
* @var string
*/
const MY_COURSES_STATUS_FILTER = 'my_courses_status';
/**
* @var WP_Query to help setup the query needed by the render method.
*/
protected $query;
/**
* @var string number of items to show on the current page
*/
protected $number;
/**
* @var string ordery by course field
* Default: date
*/
protected $orderby;
/**
* @var string ASC or DESC
* Default: 'DESC'
*/
protected $order;
/**
* @var status can be completed or active. If none is specified all will be shown
*/
protected $status;
/**
* Rendering options.
*
* @var array
*/
protected $options;
/**
* Are we in my-courses?
*
* @var bool
*/
private $is_shortcode_initial_status_all = false;
/**
* Current Page ID
*
* @var int
*/
private $page_id = 0;
/**
* Rendering as a block.
*
* @var bool
*/
private $is_block = false;
/**
* Setup the shortcode object
*
* @since 1.9.0
* @param array $attributes
* @param string $content
* @param string $shortcode the shortcode that was called for this instance
*/
public function __construct( $attributes, $content, $shortcode ) {
global $wp_query;
$this->is_shortcode_initial_status_all = ! isset( $attributes['status'] ) || 'all' === $attributes['status'];
$attributes = shortcode_atts(
array(
'number' => null,
'status' => 'all',
'orderby' => 'title',
'order' => 'ASC',
'options' => [],
),
$attributes,
$shortcode
);
if ( $this->is_shortcode_initial_status_all && $wp_query->is_main_query() ) {
// Check if we should filter the courses.
if ( isset( $_GET[ self::MY_COURSES_STATUS_FILTER ] ) ) {
$course_filter_by_status = sanitize_text_field( $_GET[ self::MY_COURSES_STATUS_FILTER ] );
if ( ! empty( $course_filter_by_status ) && in_array( $course_filter_by_status, array_keys( $this->get_filter_options() ), true ) ) {
$attributes['status'] = $course_filter_by_status;
}
}
}
$this->page_id = $wp_query->get_queried_object_id();
$per_page = 10;
if (
isset( Sensei()->settings->settings['my_course_amount'] )
&& 0 < absint( Sensei()->settings->settings['my_course_amount'] )
) {
$per_page = absint( Sensei()->settings->settings['my_course_amount'] );
}
// set up all argument need for constructing the course query
$this->number = isset( $attributes['number'] ) ? absint( $attributes['number'] ) : $per_page;
$this->orderby = isset( $attributes['orderby'] ) ? $attributes['orderby'] : 'title';
$this->status = isset( $attributes['status'] ) ? $attributes['status'] : 'all';
// set the default for menu_order to be ASC
if ( 'menu_order' === $this->orderby && ! isset( $attributes['order'] ) ) {
$this->order = 'ASC';
} else {
// for everything else use the value passed or the default DESC
$this->order = isset( $attributes['order'] ) ? $attributes['order'] : 'ASC';
}
$this->is_block = ! empty( $attributes['options'] );
$this->options = wp_parse_args(
$attributes['options'],
[
'featuredImageEnabled' => true,
'courseCategoryEnabled' => true,
'courseDescriptionEnabled' => true,
'progressBarEnabled' => true,
'columns' => 2,
'layoutView' => 'list',
]
);
}
private function is_my_courses() {
global $wp_query;
return $wp_query->is_page() && $wp_query->get_queried_object_id() === Sensei()->settings->get_my_courses_page_id();
}
private function should_filter_course_by_status( $course_status, $user_id ) {
/**
* Filters courses processed by the course query in the
* [sensei_user_courses] shortcode.
*
* @param bool $should_filter Whether the course should be filtered out.
* @param WP_Comment $course_status The current course status record.
* @param int $user_id The user ID.
* @return bool
*/
return (bool) apply_filters(
'sensei_setup_course_query_should_filter_course_by_status',
false,
$course_status,
$user_id
);
}
/**
* Sets up the object course query
* that will be used in the render method.
*
* @since 1.9.0
*/
protected function setup_course_query() {
$learner_manager = Sensei_Learner::instance();
$user_id = get_current_user_id();
$empty_callback = [ $this, 'no_course_message_output' ];
$number_of_posts = $this->number;
$query_var_paged = get_query_var( 'paged' );
$base_query_args = array(
'orderby' => $this->orderby,
'order' => $this->order,
'paged' => empty( $query_var_paged ) ? 1 : $query_var_paged,
'posts_per_page' => $number_of_posts,
);
/**
* Filters the query which fetches the user courses.
*
* @since 3.13.3
*
* @hook sensei_user_courses_query
*
* @param {null} $query
* @param {int} $user_id The user id.
* @param {string} $status Status of query to run.
* @param {array} $base_query_args Base query args.
* @return {WP_Query} The query.
*/
$filtered_query = apply_filters( 'sensei_user_courses_query', null, $user_id, $this->status, $base_query_args );
if ( ! empty( $filtered_query ) ) {
$this->query = $filtered_query;
} elseif ( 'complete' === $this->status ) {
$this->query = $learner_manager->get_enrolled_completed_courses_query( $user_id, $base_query_args );
} elseif ( 'active' === $this->status ) {
$this->query = $learner_manager->get_enrolled_active_courses_query( $user_id, $base_query_args );
} else {
$this->query = $learner_manager->get_enrolled_courses_query( $user_id, $base_query_args );
}
if ( 'complete' === $this->status ) {
$empty_callback = [ $this, 'completed_no_course_message_output' ];
} elseif ( 'active' === $this->status ) {
$empty_callback = [ $this, 'active_no_course_message_output' ];
}
if ( empty( $this->query->found_posts ) ) {
add_action( 'sensei_loop_course_inside_before', $empty_callback );
}
}
/**
* Output the message that tells the user they have
* no courses.
*
* @since 3.0.0
*/
public function no_course_message_output() {
?>
<li class="user-active">
<div class="sensei-message info">
<?php esc_html_e( 'You have no active or completed courses.', 'sensei-lms' ); ?>
<a href="<?php echo esc_attr( Sensei_Course::get_courses_page_url() ); ?>">
<?php esc_html_e( 'Start a Course!', 'sensei-lms' ); ?>
</a>
</div>
</li>
<?php
}
/**
* Output the message that tells the user they have
* no completed courses.
*
* @since 1.9.0
*/
public function completed_no_course_message_output() {
?>
<li class="user-completed">
<div class="sensei-message info">
<?php esc_html_e( 'You have not completed any courses yet.', 'sensei-lms' ); ?>
</div>
</li>
<?php
}
/**
* Output the message that tells the user they have
* no active courses.
*
* @since 1.9.0
*/
public function active_no_course_message_output() {
?>
<li class="user-active">
<div class="sensei-message info">
<?php esc_html_e( 'You have no active courses.', 'sensei-lms' ); ?>
<a href="<?php echo esc_attr( Sensei_Course::get_courses_page_url() ); ?>">
<?php esc_html_e( 'Start a Course!', 'sensei-lms' ); ?>
</a>
</div>
</li>
<?php
}
/**
* Rendering the shortcode this class is responsible for.
*
* @return string $content
*/
public function render() {
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
global $wp_query;
global $sensei_is_block;
$sensei_is_block = $this->is_block;
if ( false === is_user_logged_in() ) {
// show the login form
return $this->render_login_form();
}
// setup the course query that will be used when rendering
$this->setup_course_query();
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Mocking loop for shortcode. Reset below.
$wp_query = $this->query;
$this->attach_shortcode_hooks();
// Mostly hooks added for legacy and backwards compatiblity sake.
/**
* Fires before the user courses are displayed.
*
* @hook sensei_my_courses_before
*/
do_action( 'sensei_my_courses_before' );
/**
* Fires before the user course content is displayed.
*
* @hook sensei_before_user_course_content
*
* @param {WP_User} $user The user object.
*/
do_action( 'sensei_before_user_course_content', wp_get_current_user() );
ob_start();
echo '<section id="sensei-user-courses">';
if ( ! $sensei_is_block ) {
Sensei_Messages::the_my_messages_link();
}
/**
* Fires before the user course content is displayed inside a container.
*
* @hook sensei_my_courses_content_inside_before
*/
do_action( 'sensei_my_courses_content_inside_before' );
Sensei_Templates::get_template( 'loop-course.php' );
/**
* Fires after the user course content is displayed inside a container.
*
* @hook sensei_my_courses_content_inside_after
*/
do_action( 'sensei_my_courses_content_inside_after' );
Sensei_Templates::get_template( 'globals/pagination.php' );
echo '</section>';
// Mostly hooks added for legacy and backwards compatiblity sake.
/**
* Fires after the user course content is displayed.
*
* @hook sensei_after_user_course_content
*
* @param {WP_User} $user The user object.
*/
do_action( 'sensei_after_user_course_content', wp_get_current_user() );
/**
* Fires after the user courses are displayed.
*
* @hook sensei_my_courses_after
*/
do_action( 'sensei_my_courses_after' );
$shortcode_output = ob_get_clean();
// phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- wp_reset_postdata() is not a good alternative.
wp_reset_query();
$this->detach_shortcode_hooks();
$sensei_is_block = false;
return $shortcode_output;
}
/**
* Add hooks for the shortcode
*
* @since 1.9.0
*/
public function attach_shortcode_hooks() {
// Don't show the toggle action if the user specified complete or active for this shortcode.
if ( $this->is_shortcode_initial_status_all ) {
add_action( 'sensei_loop_course_before', array( $this, 'course_toggle_actions' ) );
}
add_filter( 'sensei_course_loop_content_class', array( $this, 'course_status_class_tagging' ), 20, 2 );
if ( $this->is_block ) {
// Remove default WordPress theme hook that overrides Sensei styles.
remove_filter( 'wp_get_attachment_image_attributes', 'twenty_twenty_one_get_attachment_image_attributes', 10 );
remove_action( 'sensei_course_content_inside_before', array( Sensei()->course, 'the_course_meta' ) );
remove_action( 'sensei_course_content_inside_before', array( Sensei()->course, 'course_image' ), 30 );
if ( $this->options['featuredImageEnabled'] ) {
add_action( 'sensei_course_content_inside_before', array( Sensei()->course, 'course_image' ), 1 );
}
add_action( 'sensei_course_content_inside_before', array( $this, 'add_course_details_wrapper_start' ), 2 );
if ( $this->options['courseCategoryEnabled'] ) {
add_action( 'sensei_course_content_inside_before', array( $this, 'course_category' ), 6 );
}
if ( ! $this->options['courseDescriptionEnabled'] ) {
add_filter( 'get_the_excerpt', '__return_false' );
}
if ( $this->options['progressBarEnabled'] ) {
add_action( 'sensei_course_content_inside_after', array( $this, 'attach_course_progress' ), 20 );
}
}
add_action( 'sensei_course_content_inside_after', array( $this, 'attach_course_buttons' ), 30 );
$this->is_block && add_action( 'sensei_course_content_inside_after', array( $this, 'add_course_details_wrapper_end' ), 40 );
}
/**
* Remove hooks for the shortcode
*
* @since 1.9.0
*/
public function detach_shortcode_hooks() {
// Remove all hooks after the output is generated.
remove_action( 'sensei_course_content_inside_before', array( $this, 'course_category' ), 3 );
remove_action( 'sensei_course_content_inside_after', array( $this, 'attach_course_progress' ), 20 );
remove_action( 'sensei_course_content_inside_after', array( $this, 'attach_course_buttons' ), 30 );
remove_filter( 'sensei_course_loop_content_class', array( $this, 'course_status_class_tagging' ), 20 );
remove_action( 'sensei_loop_course_before', array( $this, 'course_toggle_actions' ) );
remove_filter( 'get_the_excerpt', '__return_false' );
if ( false === $this->options['featuredImageEnabled'] ) {
add_action( 'sensei_course_content_inside_before', array( Sensei()->course, 'course_image' ), 30, 1 );
}
if ( $this->is_block ) {
add_action( 'sensei_course_content_inside_before', array( Sensei()->course, 'the_course_meta' ) );
}
}
/**
* Hooks into sensei_course_content_inside_after
*
* @param int $course_id Course ID.
*/
public function attach_course_progress( $course_id ) {
if ( $this->is_block ) {
$progress_block = ( new Sensei_Course_Progress_Block() )->render_course_progress( [ 'postId' => $course_id ] );
echo wp_kses_post( $progress_block );
} else {
$percentage = Sensei()->course->get_completion_percentage( $course_id, get_current_user_id() );
echo wp_kses_post( Sensei()->course->get_progress_meter( $percentage ) );
}
}
/**
* Hooked into sensei_course_content_inside_after
*
* Prints out the course action buttons
*
* @param integer $course_id
*/
public function attach_course_buttons( $course_id ) {
Sensei()->course->the_course_action_buttons( get_post( $course_id ) );
}
/**
* Display course categories.
*
* @param int|WP_Post $course
*/
public function course_category( $course ) {
$category_output = get_the_term_list( $course, 'course-category', '', ', ', '' );
echo '<span class="wp-block-sensei-lms-learner-courses__courses-list__category">
' . wp_kses_post( $category_output ) . '
</span>';
}
/**
* Add an opening wrapper element around the course details.
*/
public function add_course_details_wrapper_start() {
echo '<div class="wp-block-sensei-lms-learner-courses__courses-list__details">';
}
/**
* Add a closing wrapper element around the course details.
*/
public function add_course_details_wrapper_end() {
echo '</div>';
}
/**
* Add a the user status class for the given course.
*
* @since 1.9
*
* @param array $classes
* @param WP_Post $course
* @return array $classes
*/
public function course_status_class_tagging( $classes, $course ) {
if ( Sensei_Utils::user_completed_course( $course, get_current_user_id() ) ) {
$classes[] = 'user-completed';
} else {
$classes[] = 'user-active';
}
return $classes;
}
/**
* Output the course toggle functionality
*/
public function course_toggle_actions() {
/**
* Determine if we should display course toggles on User Courses Shortcode.
*
* @since 1.9.18
*
* @hook sensei_shortcode_user_courses_display_course_toggle_actions
*
* @param {bool} $should_display_course_toggles Should we display the course toggles.
* @return {bool} Filtered value.
*/
$should_display_course_toggles = (bool) apply_filters( 'sensei_shortcode_user_courses_display_course_toggle_actions', true );
if ( false === $should_display_course_toggles ) {
return;
}
$active_filter_options = $this->get_filter_options();
$base_url = get_page_link( $this->page_id );
?>
<section id="user-course-status-toggle">
<?php
foreach ( $active_filter_options as $key => $option ) {
$css_class = $key === $this->status ? 'active' : 'inactive';
$url = add_query_arg( self::MY_COURSES_STATUS_FILTER, $key, $base_url );
?>
<a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $css_class ); ?>"><?php echo esc_html( $option ); ?></a>
<?php } ?>
</section>
<?php
}
/**
* Get the filter options.
*
* @return array The filter options.
*/
private function get_filter_options() {
$filter_options = [
'all' => __( 'All', 'sensei-lms' ),
'active' => __( 'Active', 'sensei-lms' ),
'complete' => __( 'Completed', 'sensei-lms' ),
];
/**
* Filters the the user courses filter options.
*
* @since 3.13.3
*
* @hook sensei_user_courses_filter_options
*
* @param {array} $filter_options The filter options.
* @return {array} The filter options.
*/
return apply_filters( 'sensei_user_courses_filter_options', $filter_options );
}
/**
* Load the javascript for the toggle functionality
*
* @since 1.9.0
*/
function print_course_toggle_actions_inline_script() {
?>
<script type="text/javascript">
var buttonContainer = jQuery('#user-course-status-toggle');
var courseList = jQuery('ul.course-container');
///
/// EVENT LISTENERS
///
buttonContainer.on('click','a#sensei-user-courses-active-action', function( e ){
e.preventDefault();
sensei_user_courses_hide_all_completed();
sensei_user_courses_show_all_active();
sensei_users_courses_toggle_button_active( e );
});
buttonContainer.on('click', 'a#sensei-user-courses-complete-action', function( e ){
e.preventDefault();
sensei_user_courses_hide_all_active();
sensei_user_courses_show_all_completed();
sensei_users_courses_toggle_button_active( e );
});
///
// Set initial state
///
jQuery( 'a#sensei-user-courses-active-action').trigger( 'click' );
///
/// FUNCTIONS
///
function sensei_user_courses_hide_all_completed(){
courseList.children('li.user-completed').hide();
}
function sensei_user_courses_hide_all_active(){
courseList.children('li.user-active').hide();
}
function sensei_user_courses_show_all_completed(){
courseList.children('li.user-completed').show();
}
function sensei_user_courses_show_all_active(){
courseList.children('li.user-active').show();
}
/**
* Toggle buttons active a classes
*/
function sensei_users_courses_toggle_button_active( e ){
//reset both buttons
buttonContainer.children('a').removeClass( 'active' );
buttonContainer.children('a').addClass( 'inactive' );
// setup the curent clicked button
jQuery( e.target).addClass( 'active' ) ;
jQuery( e.target).removeClass( 'inactive' ) ;
}
</script>
<?php
}
/**
* @return string
*/
private function render_login_form() {
ob_start();
Sensei()->frontend->sensei_login_form();
$shortcode_output = ob_get_clean();
return $shortcode_output;
}
}