<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Sensei Modules Class
*
* Sensei Module Functionality
*
* @package Content
* @author Automattic
*
* @since 1.8.0
*/
class Sensei_Core_Modules {
private $file;
private $order_page_slug;
public $taxonomy;
public function __construct( $file ) {
$this->file = $file;
$this->taxonomy = 'module';
$this->order_page_slug = 'module-order';
// setup taxonomy
add_action( 'init', array( $this, 'setup_modules_taxonomy' ), 10 );
// Manage lesson meta boxes for taxonomy
add_action( 'add_meta_boxes', array( $this, 'modules_metaboxes' ), 20, 2 );
// Save lesson meta box
add_action( 'save_post', array( $this, 'save_lesson_module' ), 10, 1 );
// Frontend styling
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) );
// Admin styling
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_styles' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 20, 2 );
// Handle module completion record
add_action( 'sensei_lesson_status_updated', array( $this, 'update_lesson_status_module_progress' ), 10, 3 );
add_action( 'sensei_user_lesson_reset', array( $this, 'save_lesson_module_progress' ), 10, 2 );
add_action( 'wp', array( $this, 'save_module_progress' ), 10 );
add_action( 'admin_menu', array( $this, 'add_submenus' ) );
add_action( 'admin_post_order_modules', array( $this, 'handle_order_modules' ) );
add_filter( 'manage_course_posts_columns', array( $this, 'course_columns' ), 11, 1 );
add_action( 'manage_course_posts_custom_column', array( $this, 'course_column_content' ), 11, 2 );
add_filter( 'manage_lesson_posts_columns', array( $this, 'add_lesson_columns' ), 11, 1 );
add_action( 'manage_lesson_posts_custom_column', array( $this, 'add_lesson_column_content' ), 11, 2 );
// Ensure modules always show under courses
add_action( 'admin_menu', array( $this, 'remove_lessons_menu_model_taxonomy' ), 10 );
add_action( 'admin_menu', array( $this, 'remove_courses_menu_model_taxonomy' ), 10 );
add_action( 'admin_menu', array( $this, 'redirect_to_lesson_module_taxonomy_to_course' ), 20 );
// Add course field to taxonomy
add_action( $this->taxonomy . '_add_form_fields', array( $this, 'add_module_fields' ), 50, 1 );
add_action( $this->taxonomy . '_edit_form_fields', array( $this, 'edit_module_fields' ), 1, 1 );
add_action( 'created_' . $this->taxonomy, array( $this, 'track_module_creation' ), 10 );
add_action( 'admin_init', array( $this, 'add_module_admin_hooks' ) );
add_action( 'wp_ajax_sensei_json_search_courses', array( $this, 'search_courses_json' ) );
// Manage module taxonomy archive page
add_filter( 'template_include', array( $this, 'module_archive_template' ), 10 );
add_action( 'pre_get_posts', array( $this, 'module_archive_filter' ), 10, 1 );
add_filter( 'sensei_lessons_archive_text', array( $this, 'module_archive_title' ) );
add_action( 'sensei_loop_lesson_inside_before', array( $this, 'module_archive_description' ), 30 );
add_action( 'sensei_taxonomy_module_content_inside_before', array( $this, 'course_signup_link' ), 30 );
add_action( 'sensei_taxonomy_module_content_inside_before', array( $this, 'module_archive_description' ), 30 );
add_filter( 'body_class', array( $this, 'module_archive_body_class' ) );
// Single Course modules actions. Add to single-course/course-modules.php
add_action( 'sensei_single_course_modules_before', array( $this, 'course_modules_title' ), 20 );
// Set up display on single lesson page
add_filter( 'sensei_breadcrumb_output', array( $this, 'module_breadcrumb_link' ), 10, 2 );
// Add 'Modules' columns to Analysis tables
add_filter( 'sensei_analysis_overview_columns', array( $this, 'analysis_overview_column_title' ), 10, 2 );
add_filter( 'sensei_analysis_course_columns', array( $this, 'analysis_course_column_title' ), 10, 2 );
add_filter( 'sensei_analysis_course_column_data', array( $this, 'analysis_course_column_data' ), 10, 3 );
// Manage module taxonomy columns
add_filter( 'manage_edit-' . $this->taxonomy . '_columns', array( $this, 'taxonomy_column_headings' ), 1, 1 );
add_filter( 'manage_' . $this->taxonomy . '_custom_column', array( $this, 'taxonomy_column_content' ), 1, 3 );
add_filter( 'sensei_module_lesson_list_title', array( $this, 'sensei_course_preview_titles' ), 10, 2 );
// store new modules created on the course edit screen
add_action( 'wp_ajax_sensei_add_new_module_term', array( 'Sensei_Core_Modules', 'add_new_module_term' ) );
add_action( 'wp_ajax_sensei_get_course_modules', array( $this, 'ajax_get_course_modules' ) );
add_action( 'wp_ajax_sensei_get_lesson_module_metabox', array( $this, 'handle_get_lesson_module_metabox' ) );
// for non admin users, only show taxonomies that belong to them
add_filter( 'get_terms', array( $this, 'filter_module_terms' ), 20, 3 );
// add the teacher name next to the module term in for admin users
add_filter( 'get_terms', array( $this, 'append_teacher_name_to_module' ), 70, 3 );
add_filter( 'get_object_terms', array( $this, 'filter_course_selected_terms' ), 20, 3 );
// remove the default modules metabox
add_action( 'admin_init', array( 'Sensei_Core_Modules', 'remove_default_modules_box' ) );
// Add custom navigation.
add_action( 'in_admin_header', [ $this, 'add_custom_navigation' ] );
// Update module teacher meta when added to course.
add_action( 'added_term_relationship', [ $this, 'add_teacher_id_in_module_meta_when_added_to_course' ], 10, 3 );
// Remove module teacher meta when removed from course.
add_action( 'delete_term_relationships', [ $this, 'remove_teacher_id_from_module_meta_when_removed_from_course' ], 10, 3 );
// Update module teacher meta on course teacher update.
add_action( 'post_updated', [ $this, 'update_module_teacher_id_meta_on_post_teacher_update' ], 10, 3 );
}
/**
* Add teacher id as term meta when a module is added to a course.
*
* @since 4.9.0
* @access private
*
* @param int $post_ID Post ID.
* @param WP_Post $post_after Post object following the update.
* @param WP_Post $post_before Post object before the update.
*/
public function update_module_teacher_id_meta_on_post_teacher_update( int $post_ID, WP_Post $post_after, WP_Post $post_before ) {
if ( 'course' !== get_post( $post_ID )->post_type ) {
return;
}
if ( $post_after->post_author !== $post_before->post_author ) {
$modules = Sensei()->modules->get_course_modules( $post_ID );
foreach ( $modules as $module ) {
self::update_module_teacher_meta( $module->term_id, $post_after->post_author );
}
}
}
/**
* Add teacher id as term meta when a module is added to a course.
*
* @since 4.9.0
* @access private
*
* @param int $object_id Object ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
*/
public function add_teacher_id_in_module_meta_when_added_to_course( int $object_id, int $tt_id, string $taxonomy ) {
if ( 'module' !== $taxonomy ) {
return;
}
$course = get_post( $object_id );
self::update_module_teacher_meta( $tt_id, $course->post_author );
}
/**
* Remove teacher id from term meta when a module is added to a course.
*
* @since 4.9.0
* @access private
*
* @param int $object_id Object ID.
* @param array $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
*/
public function remove_teacher_id_from_module_meta_when_removed_from_course( int $object_id, array $tt_ids, string $taxonomy ) {
if ( 'module' !== $taxonomy ) {
return;
}
foreach ( $tt_ids as $tt_id ) {
$args = array(
'post_type' => 'course',
'post_status' => array( 'publish', 'draft', 'future', 'private' ),
'posts_per_page' => -1,
'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
array(
'taxonomy' => $this->taxonomy,
'field' => 'id',
'terms' => $tt_id,
),
),
);
$courses = get_posts( $args );
// Don't remove teacher id if this module is still being used in other courses.
if ( count( $courses ) < 2 ) {
delete_term_meta( $tt_id, 'module_author' );
}
}
}
/**
* Highlight the menu item for the modules pages.
*
* @deprecated 4.8.0
*
* @since 4.0.0
* @access private
*
* @param string $submenu_file The submenu file points to the certain item of the submenu.
*
* @return string
*/
public function highlight_menu_item( $submenu_file ) {
_deprecated_function( __METHOD__, '4.8.0' );
$screen = get_current_screen();
if ( $screen && in_array( $screen->id, [ 'edit-module', 'course_page_module-order' ], true ) ) {
$submenu_file = 'edit-tags.php?taxonomy=module&post_type=course';
}
return $submenu_file;
}
/**
* Add custom navigation to the admin pages.
*
* @since 4.0.0
* @access private
*/
public function add_custom_navigation() {
$screen = get_current_screen();
if ( ! $screen ) {
return;
}
if ( ( 'edit-module' === $screen->id ) && ( 'term' !== $screen->base ) ) {
$this->display_modules_navigation( $screen );
}
}
/**
* Display the modules' navigation.
*
* @param WP_Screen $screen WordPress current screen object.
*/
private function display_modules_navigation( WP_Screen $screen ) {
?>
<div id="sensei-custom-navigation" class="sensei-custom-navigation">
<div class="sensei-custom-navigation__heading">
<div class="sensei-custom-navigation__title">
<h1><?php esc_html_e( 'Modules', 'sensei-lms' ); ?></h1>
</div>
<div class="sensei-custom-navigation__links">
<a href="<?php echo esc_url( admin_url( 'admin.php?page=module-order' ) ); ?>"><?php esc_html_e( 'Order Modules', 'sensei-lms' ); ?></a>
</div>
</div>
</div>
<?php
}
/**
* Hook in all meta boxes related tot he modules taxonomy
*
* @since 1.8.0
*
* @param string $post_type
* @param WP_Post $post
*
* @return void
*/
public function modules_metaboxes( $post_type, $post ) {
if ( 'lesson' == $post_type ) {
// Remove default taxonomy meta box from Lesson edit screen
remove_meta_box( $this->taxonomy . 'div', 'lesson', 'side' );
// Add custom meta box to limit module selection to one per lesson
add_meta_box( $this->taxonomy . '_select', __( 'Module', 'sensei-lms' ), array( $this, 'lesson_module_metabox' ), 'lesson', 'side', 'default' );
}
if ( 'course' == $post_type ) {
// Course modules selection metabox
add_meta_box( $this->taxonomy . '_course_mb', __( 'Course Modules', 'sensei-lms' ), array( $this, 'course_module_metabox' ), 'course', 'side', 'core' );
}
}
/**
* Build content for custom module meta box
*
* @since 1.8.0
* @param WP_Post $post Current post object.
* @return void
*/
public function lesson_module_metabox( $post ) {
$course_id = (int) get_post_meta( $post->ID, '_lesson_course', true );
$this->output_lesson_module_metabox( $post, $course_id );
}
/**
* Outputs the lesson module meta box HTML.
*
* @since 3.15.0
*
* @param WP_Post $lesson_post The lesson post object.
* @param int $course_id The course id.
*/
private function output_lesson_module_metabox( WP_Post $lesson_post, int $course_id ) {
// Get current lesson module.
$module_id = $course_id ? $this->get_lesson_module_if_exists( $lesson_post ) : null;
$html = '<div id="lesson-module-metabox-select">';
$html .= $this->render_lesson_module_select_for_course( $course_id, $module_id );
$html .= '</div>';
echo wp_kses(
$html,
array_merge(
wp_kses_allowed_html( 'post' ),
array(
'input' => array(
'id' => array(),
'name' => array(),
'type' => array(),
'value' => array(),
),
'option' => array(
'selected' => array(),
'value' => array(),
),
'select' => array(
'class' => array(),
'id' => array(),
'name' => array(),
'style' => array(),
),
)
)
);
}
/**
* Get the lesson module if it Exists. Defaults to 0 if none found.
*
* @param WP_Post $post The post.
* @return int
*/
public function get_lesson_module_if_exists( $post ) {
// Get existing lesson module.
$lesson_module = 0;
$lesson_module_list = wp_get_post_terms( $post->ID, $this->taxonomy );
if ( is_array( $lesson_module_list ) && count( $lesson_module_list ) > 0 ) {
foreach ( $lesson_module_list as $single_module ) {
$lesson_module = $single_module->term_id;
break;
}
}
return $lesson_module;
}
/**
* Renders the lesson module select input.
*
* @since 3.15.0
*
* @param int|null $course_id The course post ID.
* @param int|null $current_module_id The currently selected module post ID.
*
* @return string The lesson module select HTML.
*/
private function render_lesson_module_select_for_course( int $course_id = null, int $current_module_id = null ): string {
// Get the available modules for this lesson's course.
$modules = $course_id ? $this->get_course_modules( $course_id ) : [];
// Build the HTML.
$input_name = 'lesson_module';
$html = '';
$html .= '<input type="hidden" name="' . esc_attr( 'woo_lesson_' . $this->taxonomy . '_nonce' ) . '" id="' . esc_attr( 'woo_lesson_' . $this->taxonomy . '_nonce' ) . '" value="' . esc_attr( wp_create_nonce( plugin_basename( $this->file ) ) ) . '" />';
if ( $modules ) {
$html .= '<select id="lesson-module-options" name="' . esc_attr( $input_name ) . '" class="widefat" style="width: 100%">' . "\n";
$html .= '<option value="">' . esc_html__( 'None', 'sensei-lms' ) . '</option>';
foreach ( $modules as $module ) {
$html .= '<option value="' . esc_attr( absint( $module->term_id ) ) . '"' . selected( $module->term_id, $current_module_id, false ) . '>' . esc_html( $module->name ) . '</option>' . "\n";
}
$html .= '</select>' . "\n";
} else {
$html .= '<input type="hidden" name="' . esc_attr( $input_name ) . '" value="">';
if ( $course_id ) {
$course_url = admin_url( 'post.php?post=' . $course_id . '&action=edit' );
/*
* translators: The placeholders are as follows:
*
* %1$s - <em>
* %2$s - </em>
* %3$s - Opening <a> tag to link to the Course URL.
* %4$s - </a>
*/
$html .= '<p>' . wp_kses_post( sprintf( __( 'No modules are available for this lesson yet. %1$sPlease add some to %3$sthe course%4$s.%2$s', 'sensei-lms' ), '<em>', '</em>', '<a href="' . esc_url( $course_url ) . '">', '</a>' ) ) . '</p>';
} else {
/*
* translators: The placeholders are as follows:
*
* %1$s - <em>
* %2$s - </em>
*/
$html .= '<p>' . sprintf( __( 'No modules are available for this lesson yet. %1$sPlease select a course first.%2$s', 'sensei-lms' ), '<em>', '</em>' ) . '</p>';
}
}
return $html;
}
/**
* Delete a term if it is childless and not associated with a lesson or course.
*
* @param int $module_term_id Term ID for the module.
*/
public function remove_if_unused( $module_term_id ) {
if ( ! $this->is_term_used( $module_term_id ) ) {
wp_delete_term( $module_term_id, 'module' );
}
}
/**
* Check if term either has children or is associated with a lesson or course.
*
* @param int $module_term_id Term ID for the module.
* @return bool True if term is has children or is associated with a lesson or course.
*/
public function is_term_used( $module_term_id ) {
$term_children = get_term_children( $module_term_id, 'module' );
if ( ! is_wp_error( $term_children ) && ! empty( $term_children ) ) {
return true;
}
$post_query = new WP_Query(
array(
'post_type' => array( 'lesson', 'course' ),
'tax_query' => array(
array(
'taxonomy' => 'module',
'field' => 'id',
'terms' => intval( $module_term_id ),
),
),
'fields' => 'ids',
'posts_per_page' => 1,
)
);
if ( $post_query->found_posts > 0 ) {
return true;
}
return false;
}
/**
* Save module to lesson. This method checks for authorization, and checks
* the incoming nonce.
*
* @since 1.8.0
* @param integer $post_id ID of post
* @return mixed Post ID on permissions failure, boolean true on success
*/
public function save_lesson_module( $post_id ) {
$post = get_post( $post_id );
// Verify post type and nonce
if ( ( get_post_type( $post ) != 'lesson' ) || ! isset( $_POST[ 'woo_lesson_' . $this->taxonomy . '_nonce' ] )
|| ! wp_verify_nonce( $_POST[ 'woo_lesson_' . $this->taxonomy . '_nonce' ], plugin_basename( $this->file ) ) ) {
return $post_id;
}
// Check if user has permissions to edit lessons
$post_type = get_post_type_object( $post->post_type );
if ( ! current_user_can( $post_type->cap->edit_post, $post_id ) ) {
return $post_id;
}
// Check if user has permissions to edit this specific post
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return $post_id;
}
// Get module and course IDs
$lesson_module_id_key = 'lesson_module';
$lesson_course_id_key = 'lesson_course';
$module_id = isset( $_POST[ $lesson_module_id_key ] ) ? $_POST[ $lesson_module_id_key ] : null;
$course_id = isset( $_POST[ $lesson_course_id_key ] ) ? $_POST[ $lesson_course_id_key ] : null;
// Set the module on the lesson
$lesson_modules = new Sensei_Core_Lesson_Modules( $post_id );
$lesson_modules->set_module( $module_id, $course_id );
return true;
}
/**
* Display course field on new module screen
*
* @since 1.8.0
* @param object $taxonomy Taxonomy object
* @return void
*/
public function add_module_fields( $taxonomy ) {
?>
<div class="form-field">
<?php $this->render_module_course_multi_select(); ?>
</div>
<input type="hidden" name="from_page" value="module">
<?php
}
/**
* Render the Course Multi-Select (used by select2)
*
* @param array $module_courses The Module courses.
* @since 1.9.15
* @return void
*/
private function render_module_course_multi_select( $module_courses = array() ) {
?>
<label for="module_courses"><?php echo esc_html__( 'Course(s)', 'sensei-lms' ); ?></label>
<select name="module_courses[]"
id="module_courses"
class="ajax_chosen_select_courses"
multiple="multiple"
data-placeholder="<?php echo esc_attr__( 'Search for courses...', 'sensei-lms' ); ?>"
>
<?php foreach ( $module_courses as $module_course ) { ?>
<option value="<?php echo esc_attr( $module_course['id'] ); ?>" selected="selected">
<?php echo esc_html( $module_course['details'] ); ?>
</option>
<?php } ?>
</select>
<span
class="description"><?php echo esc_html__( 'Search for and select the courses that this module will belong to.', 'sensei-lms' ); ?>
</span>
<?php
}
/**
* Adds hooks for use with editing a taxonomy in WP Admin.
*
* @since 3.6.0
* @access private
*/
public function add_module_admin_hooks() {
add_action( 'edited_' . $this->taxonomy, array( $this, 'save_module_course' ), 10, 2 );
add_action( 'created_' . $this->taxonomy, array( $this, 'save_module_course' ), 10, 2 );
}
/**
* Display course field on module edit screen
*
* @since 1.8.0
* @param object $module Module term object
* @return void
*/
public function edit_module_fields( $module ) {
$module_id = $module->term_id;
// Get module's existing courses
$args = array(
'post_type' => 'course',
'post_status' => array( 'publish', 'draft', 'future', 'private' ),
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => $this->taxonomy,
'field' => 'id',
'terms' => $module_id,
),
),
);
$courses = get_posts( $args );
// build the defaults array
$module_courses = array();
if ( isset( $courses ) && is_array( $courses ) ) {
foreach ( $courses as $course ) {
$module_courses[] = array(
'id' => $course->ID,
'details' => $course->post_title,
);
}
}
?>
<tr class="form-field">
<th scope="row" valign="top"><label
for="module_courses"><?php esc_html_e( 'Course(s)', 'sensei-lms' ); ?></label></th>
<td>
<?php $this->render_module_course_multi_select( $module_courses ); ?>
</td>
</tr>
<?php
}
/**
* Save module course on add/edit
*
* @since 1.8.0
* @param int $module_id ID of module.
* @return void
*/
public function save_module_course( $module_id ) {
/*
* It is safe to ignore nonce verification here because this is called
* from `edited_{$taxonomy}` and `created_{$taxonomy}` on a post to
* `edit-tags.php`, which occur after WordPress performs its own nonce
* verification.
*/
$is_rest_request = defined( 'REST_REQUEST' ) && REST_REQUEST;
// phpcs:ignore WordPress.Security.NonceVerification
if ( $is_rest_request || ( isset( $_POST['action'] ) && 'inline-save-tax' == $_POST['action'] ) ) {
return;
}
// Get module's existing courses
$args = array(
'post_type' => 'course',
'post_status' => array( 'publish', 'draft', 'future', 'private' ),
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => $this->taxonomy,
'field' => 'id',
'terms' => $module_id,
),
),
);
$courses = get_posts( $args );
// Remove module from existing courses
if ( isset( $courses ) && is_array( $courses ) ) {
foreach ( $courses as $course ) {
wp_remove_object_terms( $course->ID, (int) $module_id, $this->taxonomy );
}
}
// Add module to selected courses
// phpcs:ignore WordPress.Security.NonceVerification
if ( isset( $_POST['module_courses'] ) && ! empty( $_POST['module_courses'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification
$course_ids = is_array( $_POST['module_courses'] ) ? $_POST['module_courses'] : explode( ',', $_POST['module_courses'] );
foreach ( $course_ids as $course_id ) {
wp_set_object_terms( absint( $course_id ), $module_id, $this->taxonomy, true );
}
}
}
/**
* Track module creation.
*
* @since 2.1.0
* @access private
*
* @param int $module_id ID of module.
*/
public function track_module_creation( $module_id ) {
$module = get_term( $module_id );
$event_properties = [
// phpcs:ignore WordPress.Security.NonceVerification
'page' => isset( $_REQUEST['from_page'] ) ? $_REQUEST['from_page'] : '',
'parent_id' => -1,
];
if ( $module->parent ) {
$event_properties['parent_id'] = $module->parent;
}
sensei_log_event( 'module_add', $event_properties );
}
/**
* Ajax function to search for courses matching term
*
* @since 1.8.0
* @return void
*/
public function search_courses_json() {
// Security check
check_ajax_referer( 'search-courses', 'security' );
// Set content type
header( 'Content-Type: application/json; charset=utf-8' );
// Get user input
$term = urldecode( stripslashes( $_GET['term'] ) );
// Return nothing if term is empty
if ( empty( $term ) ) {
die();
}
// Set a default if none is given
$default = isset( $_GET['default'] ) ? $_GET['default'] : __( 'No course', 'sensei-lms' );
// Set up array of results
$found_courses = array( '' => $default );
// Fetch results
$args = array(
'post_type' => 'course',
'post_status' => array( 'publish', 'draft', 'future', 'private' ),
'posts_per_page' => -1,
'orderby' => 'title',
's' => $term,
);
$courses = get_posts( $args );
// Add results to array
if ( $courses ) {
foreach ( $courses as $course ) {
$found_courses[ $course->ID ] = $course->post_title;
}
}
// Encode and return results for processing & selection
echo json_encode( $found_courses );
die();
}
public function sensei_course_preview_titles( $title, $lesson_id ) {
global $post, $current_user;
$course_id = $post->ID;
$title_text = '';
if ( method_exists( 'Sensei_Utils', 'is_preview_lesson' ) && Sensei_Utils::is_preview_lesson( $lesson_id ) ) {
$is_user_taking_course = Sensei()->course_progress_repository->has( $course_id, $current_user->ID );
if ( ! $is_user_taking_course ) {
if ( method_exists( 'Sensei_Frontend', 'sensei_lesson_preview_title_text' ) ) {
$title_text = Sensei()->frontend->sensei_lesson_preview_title_text( $course_id );
// Remove brackets for display here
$title_text = str_replace( '(', '', $title_text );
$title_text = str_replace( ')', '', $title_text );
$title_text = '<span class="preview-label">' . $title_text . '</span>';
}
$title .= ' ' . $title_text;
}
}
return $title;
}
public function module_breadcrumb_link( $html, $separator ) {
global $post;
// Lesson
if ( is_singular( 'lesson' ) ) {
if ( has_term( '', $this->taxonomy, $post->ID ) ) {
$module = $this->get_lesson_module( $post->ID );
if ( $module ) {
if ( $this->do_link_to_module( $module ) ) {
$html .= ' ' . $separator . ' <a href="' . esc_url( $module->url ) . '" title="' . __( 'Back to the module', 'sensei-lms' ) . '">' . $module->name . '</a>';
} else {
$html .= ' ' . $separator . ' ' . $module->name;
}
}
}
}
// Module
if ( is_tax( $this->taxonomy ) ) {
if ( isset( $_GET['course_id'] ) && 0 < intval( $_GET['course_id'] ) ) {
$course_id = intval( $_GET['course_id'] );
$html .= '<a href="' . esc_url( get_permalink( $course_id ) ) . '" title="' . __( 'Back to the course', 'sensei-lms' ) . '">' . get_the_title( $course_id ) . '</a>';
}
}
return $html;
}
/**
* Check if we should link to a module in the course outline.
*
* True if there is a module description or if the `taxonomy-module.php` template has been overridden.
*
* @since 1.10.0
*
* @param WP_Term $module
* @param bool $link_to_current Set to true to disable checks for currently displayed module. Default false.
* @return bool
*/
public function do_link_to_module( WP_Term $module, $link_to_current = false ) {
// Perhaps don't link to module when on the module page already.
if ( ! $link_to_current && is_tax( 'module', $module->term_id ) ) {
$do_link_to_module = false;
} else {
$description = trim( $module->description );
if ( ! empty( $description ) ) {
$do_link_to_module = true;
} else {
$do_link_to_module = $this->is_module_tax_template_overridden();
}
}
/**
* Determine if a particular module should be linked to.
*
* @since 1.10.0
*
* @hook sensei_do_link_to_module
*
* @param {bool} $do_link_to_module True if module should be linked to.
* @param {WP_Term} $module Module to check if it should be linked to.
* @param {bool} $link_to_current Allow for linking to the currently displayed module.
* @return {bool} Filtered value of $do_link_to_module.
*/
return apply_filters( 'sensei_do_link_to_module', $do_link_to_module, $module, $link_to_current );
}
/**
* Checks if a module taxonomy template file has been overridden.
*
* @since 1.10.0
*
* @return bool True if taxonomy template has been overridden.
*/
protected function is_module_tax_template_overridden() {
$file = 'taxonomy-module.php';
$find = array( $file, Sensei()->template_url . $file );
$template = locate_template( $find );
return (bool) $template;
}
/**
* Set lesson archive template to display on module taxonomy archive page
*
* @since 1.8.0
* @param string $template Default template
* @return string Modified template
*/
public function module_archive_template( $template ) {
if ( ! is_tax( $this->taxonomy ) ) {
return $template;
}
$file = 'taxonomy-module.php';
$find = array( $file, Sensei()->template_url . $file );
// locate the template file
$template = locate_template( $find );
if ( ! $template ) {
$template = Sensei()->plugin_path() . 'templates/' . $file;
}
return $template;
}
/**
* Modify module taxonomy archive query
*
* @since 1.8.0
* @param object $query The query object passed by reference
* @return void
*/
public function module_archive_filter( $query ) {
// If there is already an "orderby" property set
// then no need to do anything.
if ( $query->get( 'orderby' ) ) {
return;
}
if ( $query->is_main_query() && is_tax( $this->taxonomy ) ) {
// Limit to lessons only
$query->set( 'post_type', 'lesson' );
// Set order of lessons
if ( version_compare( Sensei()->version, '1.6.0', '>=' ) ) {
$module_id = $query->queried_object_id;
$query->set( 'meta_key', '_order_module_' . $module_id );
$query->set( 'orderby', 'meta_value_num date' );
} else {
$query->set( 'orderby', 'menu_order' );
}
$query->set( 'order', 'ASC' );
// Limit to specific course if specified
if ( isset( $_GET['course_id'] ) && 0 < intval( $_GET['course_id'] ) ) {
$course_id = intval( $_GET['course_id'] );
$meta_query = [];
$meta_query[] = array(
'key' => '_lesson_course',
'value' => intval( $course_id ),
);
$query->set( 'meta_query', $meta_query );
}
}
}
/**
* Modify archive page title
*
* @since 1.8.0
* @param string $title Default title
* @return string Modified title
*/
public function module_archive_title( $title ) {
if ( is_tax( $this->taxonomy ) ) {
/**
* Filter the module archive title.
*
* @hook sensei_module_archive_title
*
* @param {string} $title Archive title.
* @return {string} Filtered title.
*/
$title = apply_filters( 'sensei_module_archive_title', get_queried_object()->name );
}
return $title;
}
/**
* Display module description on taxonomy archive page
*
* @since 1.8.0
* @return void
*/
public function module_archive_description() {
// ensure this only shows once on the archive.
remove_action( 'sensei_loop_lesson_before', array( $this, 'module_archive_description' ), 30 );
if ( is_tax( $this->taxonomy ) ) {
$module = get_queried_object();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Safe handling of course ID query var.
$course_id = isset( $_GET['course_id'] ) ? intval( $_GET['course_id'] ) : null;
$user_id = get_current_user_id();
$module_progress = false;
if ( $user_id && ! empty( $course_id ) ) {
$module_progress = $this->get_user_module_progress( $module->term_id, $course_id, $user_id );
}
if ( $module_progress && $module_progress > 0 ) {
$status = __( 'Completed', 'sensei-lms' );
$class = 'completed';
if ( $module_progress < 100 ) {
$status = __( 'In progress', 'sensei-lms' );
$class = 'in-progress';
}
echo '<p class="status ' . esc_attr( $class ) . '">' . esc_html( $status ) . '</p>';
}
if ( $this->can_view_module_content( $module, $course_id, $user_id ) ) {
/**
* Filter the module archive description.
*
* @hook sensei_module_archive_description
*
* @param {string} $description Archive description.
* @param {int} $module_id Module term ID.
* @return {string} Filtered description.
*/
echo '<p class="archive-description module-description">' . wp_kses_post( apply_filters( 'sensei_module_archive_description', nl2br( $module->description ), $module->term_id ) ) . '</p>';
}
}
}
/**
* Check if we can view module content.
*
* @param WP_Term $module Module term object. Defaults to the currently queried term.
* @param int $course_id Course post ID. May not be set if not viewing module in course context.
* @param int $user_id User ID. Defaults to currently logged in user ID.
*
* @return bool
*/
public function can_view_module_content( WP_Term $module = null, $course_id = null, $user_id = null ) {
$can_view_module_content = false;
if ( null === $module ) {
$module = get_queried_object();
}
if ( ! $module instanceof WP_Term ) {
return false;
}
if ( null === $user_id ) {
$user_id = get_current_user_id();
}
if (
! sensei_is_login_required()
|| ( $course_id && Sensei()->course->can_access_course_content( $course_id, $user_id, 'module' ) )
) {
$can_view_module_content = true;
}
/**
* Filter if the user can view module content.
*
* @since 3.0.0
*
* @hook sensei_can_user_view_module
*
* @param {bool} $can_view_module_content True if they can view module content.
* @param {int} $module_term_id Module term ID.
* @param {int} $course_id Course post ID.
* @param {int} $user_id User ID.
* @return {bool} Filtered value of $can_view_module_content.
*/
return apply_filters( 'sensei_can_user_view_module', $can_view_module_content, $module->term_id, $course_id, $user_id );
}
/**
* Outputs the module course sign-up link.
*
* @access private
* @since 3.0.0
*/
public function course_signup_link() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Safe use of retrieving course ID.
$course_id = isset( $_GET['course_id'] ) ? intval( $_GET['course_id'] ) : null;
if ( empty( $course_id ) || 'course' !== get_post_type( $course_id ) ) {
return;
}
$show_course_signup_notice = ! $this->can_view_module_content( null, $course_id );
/**
* Filter for if we should show the course sign up notice on the module page.
*
* @since 3.0.0
*
* @hook sensei_module_show_course_signup_notice
*
* @param {bool} $show_course_signup_notice True if we should show the signup notice to the user.
* @param {int} $course_id Post ID for the course.
* @return {bool} Filtered value of $show_course_signup_notice.
*/
if ( ! apply_filters( 'sensei_module_show_course_signup_notice', $show_course_signup_notice, $course_id ) ) {
return;
}
$course_link = '<a href="' . esc_url( get_permalink( $course_id ) ) . '" title="' . esc_attr__( 'Sign Up', 'sensei-lms' ) . '">';
$course_link .= esc_html__( 'course', 'sensei-lms' );
$course_link .= '</a>';
// translators: Placeholder is a link to the Course.
$message_default = sprintf( esc_html__( 'Please sign up for the %1$s before starting the module.', 'sensei-lms' ), $course_link );
/**
* Filter the course sign up notice message on the module page.
*
* @since 3.0.0
*
* @hook sensei_module_course_signup_notice_message
*
* @param {string} $message Message to show user.
* @param {int} $course_id Post ID for the course.
* @param {string} $course_link Generated HTML link to the course.
* @return {string} Filtered message.
*/
$message = apply_filters( 'sensei_module_course_signup_notice_message', $message_default, $course_id, $course_link );
/**
* Filter the course sign up notice message alert level on the module page.
*
* @since 3.0.0
*
* @hook sensei_module_course_signup_notice_level
*
* @param {string} $notice_level Notice level to use for the shown alert (alert, tick, download, info).
* @param {int} $course_id Post ID for the course.
* @return {string} Filtered notice level.
*/
$notice_level = apply_filters( 'sensei_module_course_signup_notice_level', 'info', $course_id );
Sensei()->notices->add_notice( $message, $notice_level );
}
public function module_archive_body_class( $classes ) {
if ( is_tax( $this->taxonomy ) ) {
$classes[] = 'module-archive';
}
return $classes;
}
/**
* Trigger save_lesson_module_progress() when a lesson status is updated for a specific user
*
* @since 1.8.0
* @param string $status Status of the lesson for the user
* @param integer $user_id ID of user
* @param integer $lesson_id ID of lesson
* @return void
*/
public function update_lesson_status_module_progress( $status = '', $user_id = 0, $lesson_id = 0 ) {
$this->save_lesson_module_progress( $user_id, $lesson_id );
}
/**
* Save lesson's module progress for a specific user
*
* @since 1.8.0
* @param integer $user_id ID of user
* @param integer $lesson_id ID of lesson
* @return void
*/
public function save_lesson_module_progress( $user_id = 0, $lesson_id = 0 ) {
$module = $this->get_lesson_module( $lesson_id );
$course_id = get_post_meta( $lesson_id, '_lesson_course', true );
if ( $module && $course_id ) {
$this->save_user_module_progress( intval( $module->term_id ), intval( $course_id ), intval( $user_id ) );
}
}
/**
* Save progress of module for user
*
* @since 1.8.0
* @return void
*/
public function save_module_progress() {
if ( is_tax( $this->taxonomy ) && is_user_logged_in() && isset( $_GET['course_id'] ) && 0 < intval( $_GET['course_id'] ) ) {
global $current_user;
wp_get_current_user();
$user_id = $current_user->ID;
$module = get_queried_object();
$this->save_user_module_progress( intval( $module->term_id ), intval( $_GET['course_id'] ), intval( $user_id ) );
}
}
/**
* Save module progess for user
*
* @since 1.8.0
*
* @param integer $module_id ID of module
* @param integer $course_id ID of course
* @param integer $user_id ID of user
* @return void
*/
public function save_user_module_progress( $module_id = 0, $course_id = 0, $user_id = 0 ) {
$module_progress = $this->calculate_user_module_progress( $user_id, $module_id, $course_id );
update_user_meta( intval( $user_id ), '_module_progress_' . intval( $course_id ) . '_' . intval( $module_id ), intval( $module_progress ) );
/**
* Action hook triggered after module progress is saved for a user.
*
* @hook sensei_module_save_user_progress
*
* @param {int} $course_id ID of course.
* @param {int} $module_id ID of module.
* @param {int} $user_id ID of user.
* @param {float} $module_progress Module progress percentage.
*/
do_action( 'sensei_module_save_user_progress', $course_id, $module_id, $user_id, $module_progress );
}
/**
* Get module progress for a user
*
* @since 1.8.0
*
* @param integer $module_id ID of module
* @param integer $course_id ID of course
* @param integer $user_id ID of user
* @return mixed Module progress percentage on success, false on failure
*/
public function get_user_module_progress( $module_id = 0, $course_id = 0, $user_id = 0 ) {
$this->save_user_module_progress( $module_id, $course_id, $user_id );
$module_progress = get_user_meta( intval( $user_id ), '_module_progress_' . intval( $course_id ) . '_' . intval( $module_id ), true );
if ( $module_progress ) {
return (float) $module_progress;
}
return false;
}
/**
* Calculate module progess for user
*
* @since 1.8.0
*
* @param integer $user_id ID of user
* @param integer $module_id ID of module
* @param integer $course_id ID of course
* @return integer Module progress percentage
*/
public function calculate_user_module_progress( $user_id = 0, $module_id = 0, $course_id = 0 ) {
$args = array(
'post_type' => 'lesson',
'post_status' => 'publish',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => $this->taxonomy,
'field' => 'id',
'terms' => $module_id,
),
),
'meta_query' => array(
array(
'key' => '_lesson_course',
'value' => $course_id,
),
),
'fields' => 'ids',
);
$lessons = get_posts( $args );
if ( is_wp_error( $lessons ) || ! $lessons ) {
return 0;
}
$completed = false;
$lesson_count = 0;
$completed_count = 0;
$lessons = array_map( 'intval', $lessons );
foreach ( $lessons as $lesson_id ) {
$completed = Sensei_Utils::user_completed_lesson( $lesson_id, $user_id );
++$lesson_count;
if ( $completed ) {
++$completed_count;
}
}
$module_progress = ( $completed_count / $lesson_count ) * 100;
return (float) $module_progress;
}
/**
* Register admin screen for ordering modules
*
* @since 1.8.0
* @deprecated 4.0.0
*
* @return void
*/
public function register_modules_admin_menu_items() {
_deprecated_function( __METHOD__, '4.0.0' );
// add the modules link under the Course main menu
add_submenu_page( 'edit.php?post_type=course', __( 'Modules', 'sensei-lms' ), __( 'Modules', 'sensei-lms' ), 'manage_categories', 'edit-tags.php?taxonomy=module&post_type=course', '' );
// Register new admin page for module ordering.
add_submenu_page( 'edit.php?post_type=course', __( 'Order Modules', 'sensei-lms' ), __( 'Order Modules', 'sensei-lms' ), 'edit_lessons', $this->order_page_slug, array( $this, 'module_order_screen' ) );
}
/**
* Add admin screens.
*
* @since 4.0.0
*/
public function add_submenus() {
add_submenu_page(
'', // Hide the submenu.
__( 'Order Modules', 'sensei-lms' ),
__( 'Order Modules', 'sensei-lms' ),
'edit_courses',
$this->order_page_slug,
array( $this, 'module_order_screen' )
);
}
/**
* Handle the POST request for reordering the Modules.
*
* @since 1.12.2
*/
public function handle_order_modules() {
check_admin_referer( 'order_modules' );
$course_id = isset( $_POST['course_id'] ) ? intval( $_POST['course_id'] ) : 0;
$module_order = isset( $_POST['module-order'] ) ? sanitize_text_field( wp_unslash( $_POST['module-order'] ) ) : '';
if (
! Sensei_Course::can_current_user_edit_course( $course_id )
) {
wp_die( esc_html__( 'Insufficient permissions', 'sensei-lms' ) );
}
$ordered = false;
if ( 0 < strlen( $module_order ) ) {
$ordered = $this->save_course_module_order( esc_attr( $module_order ), $course_id );
}
wp_safe_redirect(
esc_url_raw(
add_query_arg(
array(
'page' => $this->order_page_slug,
'ordered' => $ordered,
'course_id' => $course_id,
),
admin_url( 'admin.php' )
)
)
);
}
/**
* Display Module Order screen
*
* @since 1.8.0
*
* @return void
*/
public function module_order_screen() {
?>
<div id="<?php echo esc_attr( $this->order_page_slug ); ?>"
class="wrap <?php echo esc_attr( $this->order_page_slug ); ?>">
<h1><?php esc_html_e( 'Order Modules', 'sensei-lms' ); ?></h1>
<?php
$html = '';
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- No need to unslash or sanitize in this case.
if ( isset( $_GET['ordered'] ) && $_GET['ordered'] ) {
$html .= '<div class="updated fade">' . "\n";
$html .= '<p>' . esc_html__( 'The module order has been saved for this course.', 'sensei-lms' ) . '</p>' . "\n";
$html .= '</div>' . "\n";
}
$courses = Sensei()->course->get_all_courses();
$html .= '<form action="' . esc_url( admin_url( 'admin.php' ) ) . '" method="get">' . "\n";
$html .= '<input type="hidden" name="post_type" value="course" />' . "\n";
$html .= '<input type="hidden" name="page" value="' . esc_attr( $this->order_page_slug ) . '" />' . "\n";
$html .= '<select id="module-order-course" name="course_id">' . "\n";
$html .= '<option value="">' . esc_html__( 'Select a course', 'sensei-lms' ) . '</option>' . "\n";
foreach ( $courses as $course ) {
if ( has_term( '', $this->taxonomy, $course->ID ) ) {
$course_id = '';
if ( isset( $_GET['course_id'] ) ) {
$course_id = intval( $_GET['course_id'] );
}
$html .= '<option value="' . esc_attr( intval( $course->ID ) ) . '" ' . selected( $course->ID, $course_id, false ) . '>' . esc_html( get_the_title( $course->ID ) ) . '</option>' . "\n";
}
}
$html .= '</select>' . "\n";
$html .= '<input type="submit" class="button-primary module-order-select-course-submit" value="' . esc_attr__( 'Select', 'sensei-lms' ) . '" />' . "\n";
$html .= '</form>' . "\n";
if ( isset( $_GET['course_id'] ) ) {
$course_id = intval( $_GET['course_id'] );
if ( $course_id > 0 ) {
$modules = Sensei_Course_Structure::instance( $course_id )->get( 'edit' );
if ( ! empty( $modules ) ) {
$html .= '<form id="editgrouping" method="post" action="'
. esc_url( admin_url( 'admin-post.php' ) )
. '" class="validate">' . "\n";
$html .= '<ul class="sortable-module-list">' . "\n";
foreach ( $modules as $module ) {
if ( 'module' !== $module['type'] ) {
continue;
}
$html .= '<li class="' . $this->taxonomy . '"><span rel="' . esc_attr( $module['id'] ) . '" style="width: 100%;"> ' . esc_html( $module['title'] ) . '</span></li>' . "\n";
}
$html .= '</ul>' . "\n";
$html .= '<input type="hidden" name="action" value="order_modules" />' . "\n";
$html .= wp_nonce_field( 'order_modules', '_wpnonce', true, false ) . "\n";
$html .= '<input type="hidden" name="module-order" value="" />' . "\n";
$html .= '<input type="hidden" name="course_id" value="' . esc_attr( $course_id ) . '" />' . "\n";
$html .= '<input type="submit" class="button-primary" value="' . esc_attr__( 'Save module order', 'sensei-lms' ) . '" />' . "\n";
$html .= '<a href="' . esc_url( admin_url( 'post.php?post=' . $course_id . '&action=edit' ) ) . '" class="button-secondary">' . esc_html__( 'Edit course', 'sensei-lms' ) . '</a>' . "\n";
$html .= '</form>';
}
}
}
echo wp_kses(
$html,
array_merge(
wp_kses_allowed_html( 'post' ),
array(
// Explicitly allow form tag for WP.com.
'form' => array(
'action' => array(),
'class' => array(),
'id' => array(),
'method' => array(),
),
'input' => array(
'class' => array(),
'name' => array(),
'type' => array(),
'value' => array(),
),
'option' => array(
'selected' => array(),
'value' => array(),
),
'select' => array(
'id' => array(),
'name' => array(),
),
'span' => array(
'rel' => array(),
'style' => array(),
),
)
)
);
?>
</div>
<?php
}
/**
* Add custom columns to course list table.
*
* @since 1.8.0
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function course_columns( $columns = array() ) {
$columns['modules'] = __( 'Modules', 'sensei-lms' );
return $columns;
}
/**
* Load content in the course custom columns.
*
* @since 1.8.0
*
* @param string $column Current column name.
* @param integer $course_id The course ID.
* @return void
*/
public function course_column_content( $column = '', $course_id = 0 ) {
if ( 'modules' === $column ) {
$this->output_course_modules_column( $course_id );
}
}
/**
* Output the course modules column HTML.
*
* @since 4.0.0
*
* @param int $course_id
*/
private function output_course_modules_column( int $course_id ) {
$modules = $this->get_course_modules( $course_id );
if ( ! $modules ) {
return;
}
/**
* Filter to change the number of links in the course modules column.
*
* @since 4.0.0
*
* @hook sensei_module_course_column_max_links_count
*
* @param {int} $max_links_count The number of links.
* @return {int} The filtered number of links.
*/
$max_links_count = apply_filters( 'sensei_module_course_column_max_links_count', 3 );
$links_count = count( $modules );
foreach ( $modules as $index => $module ) {
$is_last = $index + 1 === $links_count;
$should_hide = $index + 1 > $max_links_count;
$module_link = sprintf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
[
'post_type' => 'course',
$this->taxonomy => $module->slug,
],
admin_url( 'edit.php' )
)
),
esc_html( $module->name )
);
?>
<span class="<?php echo esc_attr( $should_hide ? 'hidden' : '' ); ?>">
<?php echo $module_link . ( $is_last ? '' : ', ' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is already escaped. ?>
</span>
<?php if ( $is_last && $should_hide ) { ?>
<a href="#" class="sensei-show-more">
<?php
printf(
/* translators: %d: the number of links to be displayed */
esc_html__( '+%d more', 'sensei-lms' ),
intval( $links_count - $max_links_count )
);
?>
</a>
<?php
}
}
if ( count( $modules ) > 1 ) {
// Output the edit modules order link.
echo sprintf(
'<a class="sensei-wp-list-table-link" href="%s">%s</a>',
esc_url(
add_query_arg(
[
'page' => 'module-order',
'course_id' => $course_id,
],
admin_url( 'admin.php' )
)
),
esc_html__( 'Order Modules', 'sensei-lms' )
);
}
}
/**
* Add custom columns to lesson list table.
*
* @since 4.0.0
* @access private
*
* @param array $columns Existing columns.
* @return array Modified columns.
*/
public function add_lesson_columns( $columns = array() ) {
// The lesson module column id should not be equal to "module".
// @see https://core.trac.wordpress.org/ticket/56185.
$columns['modules'] = __( 'Module', 'sensei-lms' );
return $columns;
}
/**
* Load content in the lesson custom columns.
*
* @since 4.0.0
* @access private
*
* @param string $column Current column name.
* @param integer $lesson_id The lesson ID.
*/
public function add_lesson_column_content( $column = '', $lesson_id = 0 ) {
if ( 'modules' === $column ) {
$modules = wp_get_post_terms( $lesson_id, $this->taxonomy );
$module = $modules && is_array( $modules ) ? $modules[0] : null;
if ( $module ) {
printf(
'<a href="%s">%s</a>',
esc_url(
add_query_arg(
[
'post_type' => 'lesson',
$this->taxonomy => $module->slug,
],
admin_url( 'edit.php' )
)
),
esc_html( $module->name )
);
}
}
}
/**
* Save module order for course
*
* @since 1.8.0
*
* @param string $order_string Comma-separated string of module IDs
* @param integer $course_id ID of course
* @return boolean True on success, false on failure
*/
private function save_course_module_order( $order_string = '', $course_id = 0 ) {
if ( $order_string && $course_id ) {
remove_filter( 'get_terms', array( Sensei()->modules, 'append_teacher_name_to_module' ), 70 );
$course_structure = Sensei_Course_Structure::instance( $course_id )->get( 'edit' );
add_filter( 'get_terms', array( Sensei()->modules, 'append_teacher_name_to_module' ), 70, 3 );
$order = array_map( 'absint', explode( ',', $order_string ) );
$course_structure = Sensei_Course_Structure::sort_structure( $course_structure, $order, 'module' );
if ( true === Sensei_Course_Structure::instance( $course_id )->save( $course_structure ) ) {
return true;
}
}
return false;
}
/**
* Get module order for course
*
* @since 1.8.0
*
* @param integer $course_id ID of course
* @return mixed Module order on success, false if no module order has been saved
*/
public function get_course_module_order( $course_id = 0 ) {
if ( $course_id ) {
$order = get_post_meta( intval( $course_id ), '_module_order', true );
return $order;
}
return false;
}
/**
* Modify module taxonomy columns
*
* @since 1.8.0
*
* @param array $columns Default columns
* @return array Modified columns
*/
public function taxonomy_column_headings( $columns ) {
unset( $columns['posts'] );
$columns['lessons'] = __( 'Lessons', 'sensei-lms' );
return $columns;
}
/**
* Manage content in custom module taxonomy columns
*
* @since 1.8.0
*
* @param string $column_data Default data for column
* @param string $column_name Name of current column
* @param integer $term_id ID of current term
* @return string Modified column data
*/
public function taxonomy_column_content( $column_data, $column_name, $term_id ) {
$args = array(
'post_status' => 'publish',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => $this->taxonomy,
'field' => 'id',
'terms' => intval( $term_id ),
),
),
);
$module = get_term( $term_id, $this->taxonomy );
switch ( $column_name ) {
case 'lessons':
$args['post_type'] = 'lesson';
$lessons = get_posts( $args );
$total_lessons = count( $lessons );
$column_data = '<a href="' . admin_url( 'edit.php?module=' . urlencode( $module->slug ) . '&post_type=lesson' ) . '">' . intval( $total_lessons ) . '</a>';
break;
}
return $column_data;
}
/**
* Add 'Module' columns to Analysis Lesson Overview table
*
* @since 1.8.0
*
* @param array $columns Default columns
* @return array Modified columns
*/
public function analysis_overview_column_title( $columns ) {
if ( isset( $_GET['view'] ) && 'lessons' == $_GET['view'] ) {
$new_columns = array();
if ( is_array( $columns ) && 0 < count( $columns ) ) {
foreach ( $columns as $column => $title ) {
$new_columns[ $column ] = $title;
if ( $column == 'title' ) {
$new_columns['lesson_module'] = __( 'Module', 'sensei-lms' );
}
}
}
if ( 0 < count( $new_columns ) ) {
return $new_columns;
}
}
return $columns;
}
/**
* Data for 'Module' column Analysis Lesson Overview table
*
* @since 1.8.0
* @deprecated 4.3.0
*
* @param array $columns Table column data
* @param WP_Post $lesson
* @return array Updated column data
*/
public function analysis_overview_column_data( $columns, $lesson ) {
_deprecated_function( __METHOD__, '4.3.0' );
if ( isset( $_GET['view'] ) && 'lessons' == $_GET['view'] ) {
$lesson_module = '';
$lesson_module_list = wp_get_post_terms( $lesson->ID, $this->taxonomy );
if ( is_array( $lesson_module_list ) && count( $lesson_module_list ) > 0 ) {
foreach ( $lesson_module_list as $single_module ) {
$lesson_module = '<a href="' . esc_url( admin_url( 'edit-tags.php?action=edit&taxonomy=' . urlencode( $this->taxonomy ) . '&tag_ID=' . urlencode( $single_module->term_id ) ) ) . '">' . $single_module->name . '</a>';
break;
}
}
$columns['lesson_module'] = $lesson_module;
}
return $columns;
}
/**
* Add 'Module' columns to Analysis Course table
*
* @since 1.8.0
*
* @param array $columns Default columns
* @return array Modified columns
*/
public function analysis_course_column_title( $columns ) {
if ( isset( $_GET['view'] ) && 'lessons' == $_GET['view'] ) {
$columns['lesson_module'] = __( 'Module', 'sensei-lms' );
}
return $columns;
}
/**
* Data for 'Module' column in Analysis Course table
*
* @since 1.8.0
*
* @param array $columns Table column data
* @param WP_Post $lesson
* @return array Updated columns data
*/
public function analysis_course_column_data( $columns, $lesson ) {
if ( isset( $_GET['course_id'] ) ) {
$lesson_module = '';
$lesson_module_list = wp_get_post_terms( $lesson->ID, $this->taxonomy );
if ( is_array( $lesson_module_list ) && count( $lesson_module_list ) > 0 ) {
foreach ( $lesson_module_list as $single_module ) {
$lesson_module = '<a href="' . esc_url( admin_url( 'edit-tags.php?action=edit&taxonomy=' . urlencode( $this->taxonomy ) . '&tag_ID=' . urlencode( $single_module->term_id ) ) ) . '">' . $single_module->name . '</a>';
break;
}
}
$columns['lesson_module'] = $lesson_module;
}
return $columns;
}
/**
* Get module for lesson
*
* This function also checks if the module still
* exists on the course before returning it. Although
* the lesson has a module the same module must exist on the
* course for it to be valid.
*
* @since 1.8.0
*
* @param integer $lesson_id ID of lesson
* @return object Module taxonomy term object
*/
public function get_lesson_module( $lesson_id = 0 ) {
$lesson_id = intval( $lesson_id );
if ( ! ( intval( $lesson_id > 0 ) ) ) {
return false;
}
// get taxonomy terms on this lesson
$modules = wp_get_post_terms( $lesson_id, $this->taxonomy );
// check if error returned
if ( empty( $modules )
|| is_wp_error( $modules )
|| isset( $modules['errors'] ) ) {
return false;
}
// get the last item in the array there should be only be 1 really.
// this method works for all php versions.
foreach ( $modules as $module ) {
break;
}
if ( ! isset( $module ) || ! is_object( $module ) || is_wp_error( $module ) ) {
return false;
}
$module->url = get_term_link( $module, $this->taxonomy );
$course_id = intval( get_post_meta( intval( $lesson_id ), '_lesson_course', true ) );
if ( isset( $course_id ) && 0 < $course_id ) {
// the course should contain the same module taxonomy term for this to be valid
if ( ! has_term( $module->term_id, $this->taxonomy, $course_id ) ) {
return false;
}
$module->url = esc_url( add_query_arg( 'course_id', intval( $course_id ), $module->url ) );
}
return $module;
}
/**
* Get ordered array of all modules in course
*
* @since 1.8.0
*
* @param integer $course_id ID of course
* @return WP_Term[] Ordered array of module taxonomy term objects
*/
public function get_course_modules( $course_id = 0 ) {
$course_id = intval( $course_id );
if ( empty( $course_id ) ) {
return array();
}
// Get modules for course
$modules = wp_get_post_terms( $course_id, $this->taxonomy );
// Get custom module order for course
$order = $this->get_course_module_order( $course_id );
if ( ! $order ) {
return $modules;
}
// Sort by custom order
$ordered_modules = array();
$unordered_modules = array();
foreach ( $modules as $module ) {
$order_key = array_search( $module->term_id, $order );
if ( $order_key !== false ) {
$ordered_modules[ $order_key ] = $module;
} else {
$unordered_modules[] = $module;
}
}
// Order modules correctly
ksort( $ordered_modules );
// Append modules that have not yet been ordered
if ( count( $unordered_modules ) > 0 ) {
$ordered_modules = array_merge( $ordered_modules, $unordered_modules );
}
// remove order key but maintain order
$ordered_modules_with_keys_in_sequence = array();
foreach ( $ordered_modules as $key => $module ) {
$ordered_modules_with_keys_in_sequence[] = $module;
}
return $ordered_modules_with_keys_in_sequence;
}
/**
* Load frontend CSS
*
* @since 1.8.0
*
* @return void
*/
public function enqueue_styles() {
$disable_styles = false;
if ( isset( Sensei()->settings->settings['styles_disable'] ) ) {
$disable_styles = Sensei()->settings->settings['styles_disable'];
}
/**
* Filter to disable Sensei styles.
*
* @hook sensei_disable_styles
*
* @param {bool} $disable_styles Whether to disable Sensei styles.
* @return {bool} Whether to disable Sensei styles.
*/
$disable_styles = apply_filters( 'sensei_disable_styles', $disable_styles );
if ( ! $disable_styles ) {
Sensei()->assets->enqueue( $this->taxonomy . '-frontend', 'css/modules-frontend.css' );
}
}
/**
* Load admin Javascript
*
* @since 1.8.0
*
* @return void
*/
public function admin_enqueue_scripts( $hook ) {
$screen = get_current_screen();
/**
* Filter the page hooks where modules admin script can be loaded on.
*
* @param array $white_listed_pages
*/
$script_on_pages_white_list = apply_filters(
'sensei_module_admin_script_page_white_lists',
array( 'admin_page_module-order' )
);
// Only load module scripts when adding, editing or ordering modules or editing course/lesson.
$screen_related =
$screen &&
(
'module' === $screen->taxonomy
|| 'course' === $screen->id
|| 'lesson' === $screen->id
);
if ( ! ( in_array( $hook, $script_on_pages_white_list ) || $screen_related ) ) {
return;
}
wp_enqueue_script( 'sensei-chosen-ajax' );
Sensei()->assets->enqueue(
$this->taxonomy . '-admin',
'js/modules-admin.js',
[ 'jquery', 'sensei-chosen-ajax', 'jquery-ui-sortable', 'sensei-core-select2' ],
true
);
// localized module data
$localize_modulesAdmin = array(
'search_courses_nonce' => wp_create_nonce( 'search-courses' ),
'getLessonModuleMetaBoxNonce' => wp_create_nonce( 'get_lesson_module_metabox_nonce' ),
'selectPlaceholder' => __( 'Search for courses', 'sensei-lms' ),
);
wp_localize_script( $this->taxonomy . '-admin', 'modulesAdmin', $localize_modulesAdmin );
}
/**
* Load admin CSS
*
* @since 1.8.0
*
* @return void
*/
public function admin_enqueue_styles() {
Sensei()->assets->enqueue( $this->taxonomy . '-sortable', 'css/modules-admin.css' );
}
/**
* Show the title modules on the single course template.
*
* Function is hooked into sensei_single_course_modules_before.
*
* @since 1.8.0
* @return void
*/
public function course_modules_title() {
if ( ! sensei_module_has_lessons() || ! Sensei_Utils::show_course_lessons( get_the_ID() ) ) {
return;
}
global $post;
/**
* Filters the module title on the single course page.
*
* @since 2.2.0
*
* @hook sensei_modules_title
*
* @param {string} $html The HTML to be displayed.
* @param {int} $course_id Course ID.
* @return {string} The filtered HTML.
*/
echo wp_kses_post( apply_filters( 'sensei_modules_title', '<header class="modules-title"><h2>' . __( 'Modules', 'sensei-lms' ) . '</h2></header>', $post->ID ) );
}
/**
* Display the single course modules content this will only show
* if the course has modules.
*
* @since 1.8.0
* @return void
*/
public function load_course_module_content_template() {
if ( ! Sensei_Utils::show_course_lessons( get_the_ID() ) ) {
return;
}
// load backwards compatible template name if it exists in the users theme
$located_template = locate_template( Sensei()->template_url . 'single-course/course-modules.php' );
if ( $located_template ) {
Sensei_Templates::get_template( 'single-course/course-modules.php' );
return;
}
Sensei_Templates::get_template( 'single-course/modules.php' );
}
/**
* Returns all lessons for the given module ID
*
* @since 1.8.0
*
* @param $course_id
* @param $term_id
* @return array $lessons
*/
public function get_lessons( $course_id, $term_id ) {
$lesson_query = $this->get_lessons_query( $course_id, $term_id );
if ( isset( $lesson_query->posts ) ) {
return $lesson_query->posts;
} else {
return array();
}
}
/**
* Returns all lessons for the given module ID
*
* @param int $course_id Course post ID.
* @param int $term_id Module term ID.
* @param array|string $course_lessons_post_status Post status for lessons. Can be an array of statuses.
*
* @return WP_Query $lessons_query
* @since 1.8.0
*/
public function get_lessons_query( $course_id, $term_id, $course_lessons_post_status = null ) {
global $wp_query;
if ( empty( $term_id ) || empty( $course_id ) ) {
return array();
}
if ( ! $course_lessons_post_status ) {
$course_lessons_post_status = isset( $wp_query ) && $wp_query->is_preview() ? 'all' : 'publish';
}
$args = array(
'post_type' => 'lesson',
'post_status' => $course_lessons_post_status,
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_lesson_course',
'value' => intval( $course_id ),
'compare' => '=',
),
),
'tax_query' => array(
array(
'taxonomy' => 'module',
'field' => 'id',
'terms' => intval( $term_id ),
),
),
'orderby' => 'menu_order',
'order' => 'ASC',
'suppress_filters' => 0,
);
if ( version_compare( Sensei()->version, '1.6.0', '>=' ) ) {
$args['meta_key'] = '_order_module_' . intval( $term_id );
$args['orderby'] = 'meta_value_num date ID';
}
$lessons_query = new WP_Query( $args );
return $lessons_query;
}
/**
* Find the lesson in the given course that doesn't belong
* to any of the courses modules
*
* @param int $course_id The course id.
* @param string|array $post_status The status of the lessons.
*
* @return array $non_module_lessons
*/
public function get_none_module_lessons( $course_id, $post_status = 'publish' ) {
// Return early if no course was passed.
if ( empty( $course_id ) || 'course' !== get_post_type( $course_id ) ) {
return [];
}
// Fetch terms array which must be excluded from the result.
$course_modules = $this->get_course_modules( $course_id );
$base_args = [];
if ( ! empty( $course_modules ) && is_array( $course_modules ) ) {
$term_ids = wp_list_pluck( $course_modules, 'term_id' );
$base_args['tax_query'] = [
[
'taxonomy' => 'module',
'field' => 'id',
'terms' => $term_ids,
'operator' => 'NOT IN',
],
];
}
return Sensei()->course->course_lessons( $course_id, $post_status, 'all', $base_args );
}
/**
* Register the modules taxonomy
*
* @since 1.8.0
* @since 1.9.7 Added `not_found` label.
*/
public function setup_modules_taxonomy() {
$labels = array(
'name' => __( 'Modules', 'sensei-lms' ),
'singular_name' => __( 'Module', 'sensei-lms' ),
'search_items' => __( 'Search Modules', 'sensei-lms' ),
'all_items' => __( 'All Modules', 'sensei-lms' ),
'parent_item' => __( 'Parent Module', 'sensei-lms' ),
'parent_item_colon' => __( 'Parent Module:', 'sensei-lms' ),
'view_item' => __( 'View Module', 'sensei-lms' ),
'edit_item' => __( 'Edit Module', 'sensei-lms' ),
'update_item' => __( 'Update Module', 'sensei-lms' ),
'add_new_item' => __( 'Add New Module', 'sensei-lms' ),
'new_item_name' => __( 'New Module Name', 'sensei-lms' ),
'menu_name' => __( 'Modules', 'sensei-lms' ),
'not_found' => __( 'No modules found.', 'sensei-lms' ),
'back_to_items' => __( '← Back to Modules', 'sensei-lms' ),
);
/**
* Filter to alter the Sensei Modules rewrite slug
*
* @since 1.8.0
*
* @hook sensei_module_slug
*
* @param {string} $modules_rewrite_slug The rewrite slug for the modules taxonomy.
* @return {string} The filtered rewrite slug for the modules taxonomy.
*/
$modules_rewrite_slug = apply_filters( 'sensei_module_slug', 'modules' );
$args = array(
'public' => true,
'hierarchical' => true,
'show_admin_column' => false,
'capabilities' => array(
'manage_terms' => 'manage_modules',
'edit_terms' => 'manage_modules',
'delete_terms' => 'manage_modules',
'assign_terms' => 'edit_courses',
),
'show_in_nav_menus' => false,
'show_in_quick_edit' => false,
'show_ui' => true,
'rewrite' => array( 'slug' => $modules_rewrite_slug ),
'labels' => $labels,
);
register_taxonomy( 'module', array( 'course', 'lesson' ), $args );
}
/**
* When the wants to edit the lesson modules redirect them to the course modules.
*
* This function is hooked into the admin_menu
*
* @since 1.8.0
* @return void
*/
function redirect_to_lesson_module_taxonomy_to_course() {
global $typenow , $taxnow;
if ( 'lesson' == $typenow && 'module' == $taxnow ) {
wp_safe_redirect( esc_url_raw( 'edit-tags.php?taxonomy=module&post_type=course' ) );
}
}
/**
* Completely remove the module menu item under lessons.
*
* This function is hooked into the admin_menu
*
* @since 1.8.0
* @return void
*/
public function remove_lessons_menu_model_taxonomy() {
global $submenu;
if ( ! isset( $submenu['edit.php?post_type=lesson'] ) || ! is_array( $submenu['edit.php?post_type=lesson'] ) ) {
return; // exit
}
$lesson_main_menu = $submenu['edit.php?post_type=lesson'];
foreach ( $lesson_main_menu as $index => $sub_item ) {
if ( 'edit-tags.php?taxonomy=module&post_type=lesson' == $sub_item[2] ) {
unset( $submenu['edit.php?post_type=lesson'][ $index ] );
}
}
}
/**
* Completely remove the second modules under courses
*
* This function is hooked into the admin_menu
*
* @since 1.8.0
* @return void
*/
public function remove_courses_menu_model_taxonomy() {
global $submenu;
if ( ! isset( $submenu['edit.php?post_type=course'] ) || ! is_array( $submenu['edit.php?post_type=course'] ) ) {
return; // exit
}
$course_main_menu = $submenu['edit.php?post_type=course'];
foreach ( $course_main_menu as $index => $sub_item ) {
if ( 'edit-tags.php?taxonomy=module&post_type=course' == $sub_item[2] ) {
unset( $submenu['edit.php?post_type=course'][ $index ] );
}
}
}
/**
* Determine the author of a module term term by looking at
* the prefixed author id. This function will query the full term object.
* Will return the admin user author could not be determined.
*
* @since 1.8.0
*
* @param string $term_name
* @return array $owners { type WP_User }. Empty array if none if found.
*/
public static function get_term_authors( $term_name ) {
$terms = get_terms(
array( 'module' ),
array(
'name__like' => $term_name,
'hide_empty' => false,
)
);
$owners = array();
if ( empty( $terms ) ) {
return $owners;
}
// setup the admin user
// if there are more handle them appropriately and get the ones we really need that matches the desired name exactly
foreach ( $terms as $term ) {
if ( $term->name == $term_name ) {
// look for the author in the slug
$owners[] = self::get_term_author( $term->slug );
}
}
return $owners;
}
/**
* Looks at a term slug and figures out
* which author created the slug. The author was
* appended when the user saved the module term in the course edit
* screen.
*
* @since 1.8.0
*
* @param $slug
* @return WP_User $author if no author is found or invalid term is passed the admin user will be returned.
*/
public static function get_term_author( $slug = '' ) {
$term_owner = get_user_by( 'email', get_bloginfo( 'admin_email' ) );
// Fallaback in case the admin email does not match a user, otherwise it shows warnings.
if ( ! $term_owner ) {
$site_admins = get_super_admins();
if ( ! empty( $site_admins ) && is_array( $site_admins ) ) {
$term_owner = get_user_by( 'login', $site_admins[0] );
}
}
if ( empty( $slug ) ) {
return $term_owner;
}
// Look for the author in the term meta.
$term = get_term_by( 'slug', $slug, 'module' );
if ( $term ) {
$author_id = (int) get_term_meta( $term->term_id, 'module_author', true );
$author = get_user_by( 'id', $author_id );
if ( $author ) {
return $author;
}
}
// look for the author in the slug.
$slug_parts = explode( '-', $slug );
if (
count( $slug_parts ) > 1
&& is_numeric( $slug_parts[0] )
) {
// get the user data
$possible_user_id = $slug_parts[0];
$author = get_userdata( $possible_user_id );
// if the user doesnt exist for the first part of the slug
// then this slug was also created by admin
if ( is_a( $author, 'WP_User' ) ) {
$term_owner = $author;
}
}
return $term_owner;
}
/**
* Display the Sensei modules taxonomy terms metabox
*
* @since 1.8.0
*
* @hooked into add_meta_box
*
* @param WP_Post $post Post object.
*/
public function course_module_metabox( $post ) {
$tax_name = 'module';
$taxonomy = get_taxonomy( 'module' );
?>
<div id="taxonomy-<?php echo esc_attr( $tax_name ); ?>" class="categorydiv">
<div id="<?php echo esc_attr( $tax_name ); ?>-all" class="tabs-panel">
<?php
$name = ( $tax_name === 'category' ) ? 'post_category' : 'tax_input[' . $tax_name . ']';
echo "<input type='hidden' name='" . esc_attr( $name ) . "[]' value='0' />"; // Allows for an empty term set to be sent. 0 is an invalid Term ID and will be ignored by empty() checks.
?>
<ul id="<?php echo esc_attr( $tax_name ); ?>checklist" data-wp-lists="list:<?php echo esc_attr( $tax_name ); ?>" class="categorychecklist form-no-clear">
<?php
wp_terms_checklist(
$post->ID,
array(
'taxonomy' => $tax_name,
)
);
?>
</ul>
</div>
<?php if ( current_user_can( $taxonomy->cap->edit_terms ) ) : ?>
<div id="<?php echo esc_attr( $tax_name ); ?>-adder" class="wp-hidden-children">
<h4>
<a id="sensei-<?php echo esc_attr( $tax_name ); ?>-add-toggle" href="#<?php echo esc_url( $tax_name ); ?>-add" class="hide-if-no-js">
<?php
/* translators: %s: add new taxonomy label */
printf( esc_html__( '+ %s', 'sensei-lms' ), esc_html( $taxonomy->labels->add_new_item ) );
?>
</a>
</h4>
<p id="sensei-<?php echo esc_attr( $tax_name ); ?>-add" class="category-add wp-hidden-child">
<label class="screen-reader-text" for="new<?php echo esc_attr( $tax_name ); ?>"><?php echo esc_html( $taxonomy->labels->add_new_item ); ?></label>
<input type="text" name="new<?php echo esc_attr( $tax_name ); ?>" id="new<?php echo esc_attr( $tax_name ); ?>" class="form-required form-input-tip" value="<?php echo esc_attr( $taxonomy->labels->new_item_name ); ?>" aria-required="true"/>
<a class="button" id="sensei-<?php echo esc_attr( $tax_name ); ?>-add-submit" class="button category-add-submit"><?php echo esc_attr( $taxonomy->labels->add_new_item ); ?></a>
<?php wp_nonce_field( '_ajax_nonce-add-' . $tax_name, 'add_module_nonce' ); ?>
<span id="<?php echo esc_attr( $tax_name ); ?>-ajax-response"></span>
</p>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Submits a new module term prefixed with the
* the current author id.
*
* @since 1.8.0
*/
public static function add_new_module_term() {
if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], '_ajax_nonce-add-module' ) ) {
wp_send_json_error( array( 'error' => 'wrong security nonce' ) );
}
// get the term an create the new term storing infomration
$term_name = sanitize_text_field( $_POST['newTerm'] );
if ( current_user_can( 'manage_options' ) ) {
$term_slug = str_ireplace( ' ', '-', trim( $term_name ) );
} else {
$term_slug = get_current_user_id() . '-' . str_ireplace( ' ', '-', trim( $term_name ) );
}
$course_id = sanitize_text_field( $_POST['course_id'] );
// save the term
$slug = wp_insert_term( $term_name, 'module', array( 'slug' => $term_slug ) );
// send error for all errors except term exits
if ( is_wp_error( $slug ) ) {
// prepare for possible term name and id to be passed down if term exists
$term_data = array();
// if term exists also send back the term name and id
if ( isset( $slug->errors['term_exists'] ) ) {
$term = get_term_by( 'slug', $term_slug, 'module' );
$term_data['name'] = $term_name;
$term_data['id'] = $term->term_id;
// set the object terms
wp_set_object_terms( $course_id, $term->term_id, 'module', true );
}
wp_send_json_error(
array(
'errors' => $slug->errors,
'term' => $term_data,
)
);
}
// make sure the new term is checked for this course
wp_set_object_terms( $course_id, $slug['term_id'], 'module', true );
// Handle request then generate response using WP_Ajax_Response
wp_send_json_success(
array(
'termId' => $slug['term_id'],
'termName' => $term_name,
)
);
}
/**
* Get course modules
*
* @deprecated 3.15.0
*/
public function ajax_get_course_modules() {
_deprecated_function( __METHOD__, '3.15.0', 'Sensei_Core_Modules::handle_get_lesson_module_metabox' );
// Security check
check_ajax_referer( 'get-course-modules', 'security' );
$course_id = isset( $_POST['course_id'] ) ? absint( $_POST['course_id'] ) : null;
if ( null === $course_id ) {
wp_send_json_error( array( 'error' => 'invalid course id' ) );
}
$html_content = $this->render_lesson_module_select_for_course( $course_id );
wp_send_json_success( array( 'content' => $html_content ) );
}
/**
* Handles the lesson module meta box ajax request by outputting the box content HTML.
*
* @since 3.15.0
*/
public function handle_get_lesson_module_metabox() {
// Security check.
check_ajax_referer( 'get_lesson_module_metabox_nonce', 'security' );
if ( isset( $_GET['lesson_id'] ) && isset( $_GET['course_id'] ) ) {
$this->output_lesson_module_metabox(
get_post( (int) $_GET['lesson_id'] ),
(int) $_GET['course_id']
);
}
wp_die(); // This is required to terminate immediately and return a proper response.
}
/**
* Limit the course module metabox
* term list to only those on courses belonging to current teacher.
*
* Hooked into 'get_terms'
*
* @since 1.8.0
*/
public function filter_module_terms( $terms, $taxonomies, $args ) {
// dont limit for admins and other taxonomies. This should also only apply to admin
if ( current_user_can( 'manage_options' ) || ! $taxonomies || ! in_array( 'module', $taxonomies ) || ! is_admin() ) {
return $terms;
}
// in certain cases the array is passed in as reference to the parent term_id => parent_id
if ( isset( $args['fields'] ) ) {
if ( in_array( $args['fields'], array( 'ids', 'tt_ids' ), true ) ) {
return $terms;
}
// change only scrub the terms ids form the array keys
if ( 'id=>parent' == $args['fields'] ) {
$terms = array_keys( $terms );
}
}
$teachers_terms = $this->filter_terms_by_owner_no_infinite_loop( $terms, get_current_user_id() );
return $teachers_terms;
}
/**
* Call filter_terms_by_owner without infinite loops
*
* @param array $terms Terms.
* @param int $user_id User Id.
* @return array
*/
private function filter_terms_by_owner_no_infinite_loop( $terms, $user_id ) {
// avoid infinite call loop.
remove_filter( 'get_terms', array( $this, 'filter_module_terms' ), 20 );
$teachers_terms = $this->filter_terms_by_owner( $terms, $user_id );
// add filter again as removed above.
add_filter( 'get_terms', array( $this, 'filter_module_terms' ), 20, 3 );
return $teachers_terms;
}
/**
* For the selected items on a course module only return those
* for the current user. This does not apply to admin and super admin users.
*
* hooked into get_object_terms
*
* @since 1.8.0
*/
public function filter_course_selected_terms( $terms, $course_ids_array, $taxonomies ) {
$taxonomies_count = is_countable( $taxonomies ) ? count( $taxonomies ) : 0;
// dont limit for admins and other taxonomies. This should also only apply to admin
if ( current_user_can( 'manage_options' ) || ! is_admin() || empty( $terms )
// only apply this to module only taxonomy queries so 1 taxonomy only:
|| $taxonomies_count > 1 || ! in_array( 'module', $taxonomies ) ) {
return $terms;
}
$term_objects = $this->filter_terms_by_owner( $terms, get_current_user_id() );
// if term objects were passed in send back objects
// if term id were passed in send that back
if ( is_object( $terms[0] ) ) {
return $term_objects;
}
$terms = array();
foreach ( $term_objects as $term_object ) {
$terms[] = $term_object->term_id;
}
return $terms;
}
/**
* Filter the given terms and only return the
* terms that belong to the given user id.
*
* @since 1.8.0
* @param $terms
* @param $user_id
* @return array
*/
public function filter_terms_by_owner( $terms, $user_id ) {
$users_terms = array();
foreach ( $terms as $index => $term ) {
if ( is_numeric( $term ) ) {
// the term id was given, get the term object
$term = get_term( $term, 'module' );
}
$author = self::get_term_author( $term->slug );
if ( $user_id == $author->ID ) {
// add the term to the teachers terms
$users_terms[] = $term;
}
}
/**
* Filters the module terms when ownership is being checked for them.
*
* @since 4.9.0
*
* @hook sensei_filter_module_terms_by_owner
*
* @param {WP_Term[]} $user_terms The terms after applying the filter by owner.
* @param {WP_Term[]|int[]} $terms The original terms before the filtering was applied.
* @param {int} $user_id The user ID to check for ownership.
* @return {WP_Term[]} The final list of terms that must be considered as owner by the given user ID.
*/
return apply_filters( 'sensei_filter_module_terms_by_owner', $users_terms, $terms, $user_id );
}
/**
* Add the teacher name next to modules. Only works in Admin for Admin users.
* This will not add name to terms belonging to admin user.
*
* Hooked into 'get_terms'
*
* @since 1.8.0
*/
public function append_teacher_name_to_module( $terms, $taxonomies, $args ) {
// only for admin users ont he module taxonomy
if ( empty( $terms ) || ! is_array( $taxonomies ) || ! current_user_can( 'manage_options' ) || ! in_array( 'module', $taxonomies ) || ! is_admin() ) {
return $terms;
}
// in certain cases the array is passed in as reference to the parent term_id => parent_id
// In other cases we explicitly require ids (as in 'tt_ids' or 'ids')
// simply return this as wp doesn't need an array of stdObject Term
if ( isset( $args['fields'] ) && in_array( $args['fields'], array( 'id=>parent', 'tt_ids', 'ids' ) ) ) {
return $terms;
}
$users_terms = [];
// loop through and update all terms adding the author name
foreach ( $terms as $index => $term ) {
if ( is_numeric( $term ) ) {
// the term id was given, get the term object
$term = get_term( $term, 'module' );
}
if ( ! $term instanceof WP_Term ) {
continue;
}
if ( 'module' !== $term->taxonomy ) {
$users_terms[] = $term;
continue;
}
$author = self::get_term_author( $term->slug );
if ( ! user_can( $author, 'manage_options' ) && isset( $term->name ) ) {
$term->name = $term->name . ' (' . $author->display_name . ') ';
}
// add the term to the teachers terms
$users_terms[] = $term;
}
return $users_terms;
}
/**
* Remove modules metabox that come by default
* with the modules taxonomy. We are removing this as
* we have created our own custom meta box.
*/
public static function remove_default_modules_box() {
remove_meta_box( 'modulediv', 'course', 'side' );
}
/**
* When a course is save make sure to reset the transient set
* for it when determining the none module lessons.
*
* @since 1.9.0
* @deprecated 3.6.0
*
* @param int $post_id The post ID.
*/
public static function reset_none_modules_transient( $post_id ) {
_deprecated_function( __METHOD__, '3.6.0' );
}
/**
* Setup the single course module loop.
*
* Setup the global $sensei_modules_loop
*
* @since 1.9.0
*/
public static function setup_single_course_module_loop() {
global $sensei_modules_loop, $post;
$course_id = $post->ID;
$modules = Sensei()->modules->get_course_modules( $course_id );
// initial setup
$sensei_modules_loop['total'] = 0;
$sensei_modules_loop['modules'] = array();
$sensei_modules_loop['current'] = -1;
// exit if this course doesn't have modules
if ( ! $modules || empty( $modules ) ) {
return;
}
$lessons_in_all_modules = array();
foreach ( $modules as $term ) {
$lessons_in_this_module = Sensei()->modules->get_lessons( $course_id, $term->term_id );
$lessons_in_all_modules = array_merge( $lessons_in_all_modules, $lessons_in_this_module );
}
// setup all of the modules loop variables
$sensei_modules_loop['total'] = count( $modules );
$sensei_modules_loop['modules'] = $modules;
$sensei_modules_loop['current'] = -1;
$sensei_modules_loop['course_id'] = $course_id;
}
/**
* Tear down the course module loop.
*
* @since 1.9.0
*/
public static function teardown_single_course_module_loop() {
global $sensei_modules_loop;
// reset all of the modules loop variables
$sensei_modules_loop['total'] = 0;
$sensei_modules_loop['modules'] = array();
$sensei_modules_loop['current'] = -1;
// set the current course to be the global post again
wp_reset_query();
}
/**
* Set teacher meta for module.
*
* @since 4.6.0
*
* @param int $module_id Term ID.
* @param int $teacher_id ID of module teacher.
*/
public static function update_module_teacher_meta( $module_id, $teacher_id ) {
if ( user_can( $teacher_id, 'manage_options' ) ) {
delete_term_meta( $module_id, 'module_author' );
} else {
update_term_meta(
$module_id,
'module_author',
$teacher_id
);
}
}
}