<?php /** * Quizzes Reporting Table * * @package LifterLMS/Admin/Reporting/Tables/Classes * * @since 3.16.0 * @version 6.0.0 */ defined( 'ABSPATH' ) || exit; /** * Quizzes Reporting Table class * * @since 3.16.0 * @since 3.36.1 Fixed an issue that allow instructors, who can only see their own reports, * to see all the quizzes when they had no courses or courses with no lessons. * @since 3.37.8 Allow orphaned quizzes to be deleted. * Output quiz IDs as plain text (no link) when they cannot be edited and link to the quiz within the course builder when they can. * @since 3.37.12 Fixed the 'actions' column name. * @since 4.2.0 Added deep checks on whether the quiz is associated to a lesson. */ class LLMS_Table_Quizzes extends LLMS_Admin_Table { /** * Unique ID for the Table * * @var string */ protected $id = 'quizzes'; /** * Value of the field being filtered by * Only applicable if $filterby is set * * @var string */ protected $filter = 'any'; /** * Field results are filtered by * * @var string */ protected $filterby = 'instructor'; /** * Is the Table Exportable? * * @var boolean */ protected $is_exportable = true; /** * Determine if the table is filterable * * @var boolean */ protected $is_filterable = true; /** * If true, tfoot will add ajax pagination links * * @var boolean */ protected $is_paginated = true; /** * Determine of the table is searchable * * @var boolean */ protected $is_searchable = true; /** * Results sort order * 'ASC' or 'DESC'. * Only applicable of $orderby is not set. * * @var string */ protected $order = 'ASC'; /** * Field results are sorted by * * @var string */ protected $orderby = 'title'; /** * Get HTML for buttons in the actions cell of the table * * @since 3.37.8 * @since 4.2.0 Added a deep check on whether the quiz is associated to a lesson. * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly. * * @param LLMS_Quiz $quiz Quiz object. * @return string */ private function get_actions_html( $quiz ) { if ( ! $quiz->is_orphan( true ) && $quiz->get_course() ) { return ''; } // If there are quiz attempts for the quiz let the admin know they're going to delete the attempts also. $query = new LLMS_Query_Quiz_Attempt( array( 'quiz_id' => $quiz->get( 'id' ), 'per_page' => 1, ) ); $msg = $query->has_results() ? __( 'Are you sure you want to delete this quiz and all associated student attempts?', 'lifterlms' ) : __( 'Are you sure you want to delete this quiz?', 'lifterlms' ); $msg .= ' ' . __( 'This action cannot be undone!', 'lifterlms' ); ob_start(); ?> <form action="" method="POST" style="display:inline;"> <button type="submit" class="llms-button-danger small" id="llms-del-quiz-<?php echo $quiz->get( 'id' ); ?>" name="llms_del_quiz" value="<?php echo $quiz->get( 'id' ); ?>"> <?php _e( 'Delete', 'lifterlms' ); ?> <i class="fa fa-trash" aria-hidden="true"></i> </button> <input type="hidden" name="_llms_quiz_actions_nonce" value="<?php echo wp_create_nonce( 'llms-quiz-actions' ); ?>"> </form> <script>document.getElementById( 'llms-del-quiz-<?php echo $quiz->get( 'id' ); ?>' ).onclick = function( e ) { return window.confirm( '<?php echo esc_attr( $msg ); ?>' ); };</script> <?php return ob_get_clean(); } /** * Retrieve data for a cell * * @since 3.16.0 * @since 3.24.0 Unknown. * @since 3.37.8 Add actions column that allows deletion of orphaned quizzes. * ID column displays as plain text if the quiz is not editable and directs to the quiz within the course builder when it is. * @since 4.2.0 Added a deep check on whether the quiz is associated to a lesson. * @since 6.0.0 Don't access `LLMS_Query_Quiz_Attempt` properties directly. * * @param string $key The column id / key. * @param mixed $data Object / array of data that the function can use to extract the data. * @return mixed */ protected function get_data( $key, $data ) { $quiz = llms_get_post( $data ); switch ( $key ) { case 'actions': $value = $this->get_actions_html( $quiz ); break; case 'attempts': $query = new LLMS_Query_Quiz_Attempt( array( 'quiz_id' => $quiz->get( 'id' ), 'per_page' => 1, ) ); $url = LLMS_Admin_Reporting::get_current_tab_url( array( 'tab' => 'quizzes', 'stab' => 'attempts', 'quiz_id' => $quiz->get( 'id' ), ) ); $value = '<a href="' . $url . '">' . $query->get_found_results() . '</a>'; break; case 'average': $grade = 0; $query = new LLMS_Query_Quiz_Attempt( array( 'quiz_id' => $quiz->get( 'id' ), 'per_page' => 1000, ) ); $attempts = $query->get_number_results(); if ( ! $attempts ) { $value = 0; } else { foreach ( $query->get_attempts() as $attempt ) { $grade += $attempt->get( 'grade' ); } $value = round( $grade / $attempts, 3 ) . '%'; } break; case 'course': $value = '—'; $course = $quiz->get_course(); if ( $course ) { $url = LLMS_Admin_Reporting::get_current_tab_url( array( 'tab' => 'courses', 'course_id' => $course->get( 'id' ), ) ); $value = '<a href="' . esc_url( $url ) . '">' . $course->get( 'title' ) . '</a>'; } break; case 'id': $id = $quiz->get( 'id' ); $value = $id; $course = $quiz->get_course(); if ( ! $quiz->is_orphan( true ) && $course ) { $url = add_query_arg( array( 'page' => 'llms-course-builder', 'course_id' => $course->get( 'id' ), ), admin_url( 'admin.php' ) ); $url .= sprintf( '#lesson:%d:quiz', $quiz->get( 'lesson_id' ) ); $value = '<a href="' . esc_url( $url ) . '">' . $id . '</a>'; } break; case 'lesson': $value = '—'; $lesson = $quiz->get_lesson(); if ( $lesson ) { $value = $lesson->get( 'title' ); } break; case 'title': $value = $quiz->get( 'title' ); $url = LLMS_Admin_Reporting::get_current_tab_url( array( 'tab' => 'quizzes', 'quiz_id' => $quiz->get( 'id' ), ) ); $value = '<a href="' . esc_url( $url ) . '">' . $quiz->get( 'title' ) . '</a>'; break; default: $value = $key; } return $this->filter_get_data( $value, $key, $data ); } /** * Retrieve a list of Instructors to be used for Filtering * * @since 3.16.0 * * @return array */ private function get_instructor_filters() { $query = get_users( array( 'fields' => array( 'ID', 'display_name' ), 'meta_key' => 'last_name', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'orderby' => 'meta_value', 'role__in' => array( 'administrator', 'lms_manager', 'instructor', 'instructors_assistant' ), ) ); $instructors = wp_list_pluck( $query, 'display_name', 'ID' ); return $instructors; } /** * Execute a query to retrieve results from the table * * @since 3.16.0 * @since 3.36.1 Fixed an issue that allow instructors, who can only see their own reports, * to see all the quizzes when they had no courses or courses with no lessons. * * @param array $args Array of query args. * @return void */ public function get_results( $args = array() ) { $this->title = __( 'Quizzes', 'lifterlms' ); $args = $this->clean_args( $args ); if ( isset( $args['page'] ) ) { $this->current_page = absint( $args['page'] ); } $per = apply_filters( 'llms_reporting_' . $this->id . '_per_page', 25 ); $this->order = isset( $args['order'] ) ? $args['order'] : $this->order; $this->orderby = isset( $args['orderby'] ) ? $args['orderby'] : $this->orderby; $this->filter = isset( $args['filter'] ) ? $args['filter'] : $this->get_filter(); $this->filterby = isset( $args['filterby'] ) ? $args['filterby'] : $this->get_filterby(); $query_args = array( 'order' => $this->order, 'orderby' => $this->orderby, 'paged' => $this->current_page, 'post_status' => 'publish', 'post_type' => 'llms_quiz', 'posts_per_page' => $per, ); if ( isset( $args['search'] ) ) { $query_args['s'] = sanitize_text_field( $args['search'] ); } // if you can view others reports, make a regular query. if ( current_user_can( 'view_others_lifterlms_reports' ) ) { $query = new WP_Query( $query_args ); // user can only see their own reports, get a list of their students. } elseif ( current_user_can( 'view_lifterlms_reports' ) ) { $instructor = llms_get_instructor(); if ( ! $instructor ) { return; } $lessons = array(); $courses = $instructor->get_courses( array( 'posts_per_page' => -1, ) ); foreach ( $courses as $course ) { $lessons = array_merge( $lessons, $course->get_lessons( 'ids' ) ); } if ( empty( $lessons ) ) { return; } $query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'compare' => 'IN', 'key' => '_llms_lesson_id', 'value' => $lessons, ), ); $query = new WP_Query( $query_args ); } else { return; } $this->max_pages = $query->max_num_pages; if ( $this->max_pages > $this->current_page ) { $this->is_last_page = false; } $this->tbody_data = $query->posts; } /** * Get the Text to be used as the placeholder in a searchable tables search input * * @since 3.16.0 * * @return string */ public function get_table_search_form_placeholder() { /** * Filter the placeholder used in the search input on the quizzes reporting table. * * @since 3.16.0 * * @param string $placeholder The placeholder string. */ return apply_filters( 'llms_table_get_quizzes_search_placeholder', __( 'Search quizzes...', 'lifterlms' ) ); } /** * Define the structure of arguments used to pass to the get_results method * * @since 3.16.0 * * @return array */ public function set_args() { return array(); } /** * Define the structure of the table * * @since 3.16.0 * @since 3.16.10 Unknown. * @since 3.37.8 Added the 'actions' column. * @since 3.37.12 Fixed the 'actions' column name. * * @return array */ protected function set_columns() { return array( 'id' => array( 'exportable' => true, 'title' => __( 'ID', 'lifterlms' ), 'sortable' => true, ), 'title' => array( 'exportable' => true, 'title' => __( 'Title', 'lifterlms' ), 'sortable' => true, ), 'course' => array( 'exportable' => true, 'title' => __( 'Course', 'lifterlms' ), 'sortable' => false, ), 'lesson' => array( 'exportable' => true, 'title' => __( 'Lesson', 'lifterlms' ), 'sortable' => false, ), 'attempts' => array( 'exportable' => true, 'title' => __( 'Total Attempts', 'lifterlms' ), 'sortable' => false, ), 'average' => array( 'exportable' => true, 'title' => __( 'Average Grade', 'lifterlms' ), 'sortable' => false, ), 'actions' => array( 'exportable' => false, 'title' => __( 'Actions', 'lifterlms' ), 'sortable' => false, ), ); } }