PluginProbe
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 All 191 releases
← All changes | classes/Course.php +462 -195 3.9.114.0.0 View file →
@@ -9,22 +9,26 @@
9 9 */
10 10
11 11 namespace TUTOR;
12 12
13 -if ( ! defined( 'ABSPATH' ) ) {
14 - exit;
15 -}
13 +defined( 'ABSPATH' ) || exit;
16 14
17 -use stdClass;
18 -use TUTOR\Input;
15 +use Tutor\Components\Button;
16 +use Tutor\Components\Constants\Size;
17 +use Tutor\Components\Constants\Variant;
18 +use Tutor\Components\Progress;
19 19 use Tutor\Ecommerce\Tax;
20 20 use Tutor\Models\QuizModel;
21 21 use Tutor\Helpers\HttpHelper;
22 22 use Tutor\Models\CourseModel;
23 +use Tutor\Components\Tooltip;
23 24 use Tutor\Ecommerce\Ecommerce;
25 +use Tutor\Helpers\DateTimeHelper;
24 26 use Tutor\Traits\JsonResponse;
25 27 use Tutor\Helpers\ValidationHelper;
26 -use TutorPro\CourseBundle\Models\BundleModel;
28 +use Tutor\Models\EnrollmentModel;
29 +use Tutor\Options_V2;
30 +use TUTOR_ASSIGNMENTS\Assignments;
27 31
28 32 /**
29 33 * Course Class
30 34 *
@@ -94,8 +98,15 @@
94 98 const PUBLIC_COURSE_META = '_tutor_is_public_course';
95 99 const COURSE_SETTINGS_META = '_tutor_course_settings';
96 100 const COURSE_LEVEL_META = '_tutor_course_level';
97 101
102 + /**
103 + * Meta keys used to identify and store Tutor course order data.
104 + *
105 + * @since 4.0.0
106 + */
107 + const IS_TUTOR_ORDER_FOR_COURSE_META = '_is_tutor_order_for_course';
108 + const TUTOR_ORDER_FOR_COURSE_ID_META = '_tutor_order_for_course_id_';
98 109
99 110 /**
100 111 * Additional course meta info
101 112 *
@@ -149,15 +160,8 @@
149 160 */
150 161 add_action( 'save_tutor_course', array( $this, 'attach_product_with_course' ), 10, 2 );
151 162
152 163 /**
153 - * Add course level to course settings
154 - *
155 - * @since v.1.4.1
156 - */
157 - add_filter( 'tutor_course_settings_tabs', array( $this, 'add_course_level_to_settings' ) );
158 -
159 - /**
160 164 * Enable Disable Course Details Page Feature
161 165 *
162 166 * @since v.1.4.8
163 167 */
@@ -298,9 +302,13 @@
298 302 add_filter( 'tutor_user_list_args', array( $this, 'user_list_args_for_instructor' ) );
299 303
300 304 add_filter( 'template_include', array( $this, 'handle_password_protected' ) );
301 305 add_action( 'login_form_postpass', array( $this, 'handle_password_submit' ) );
306 + add_filter( 'post_password_required', array( $this, 'bypass_password_for_enrolled' ), 10, 2 );
302 307 add_filter( 'the_preview', array( $this, 'handle_schedule_courses' ) );
308 +
309 + add_action( 'tutor_course_action_btn', array( $this, 'render_course_action_btn' ) );
310 + add_action( 'wp_ajax_tutor_complete_course', array( $this, 'ajax_tutor_complete_course' ) );
303 311 }
304 312
305 313 /**
306 314 * Handle schedule courses preview for instructors.
@@ -342,8 +350,31 @@
342 350 }
343 351 }
344 352
345 353 /**
354 + * Bypass password protection for enrolled users.
355 + *
356 + * @since 4.0.0
357 + *
358 + * @param bool $required Whether the password is required.
359 + * @param WP_Post $post The post object.
360 + *
361 + * @return bool false if the current user is enrolled, original value otherwise.
362 + */
363 + public function bypass_password_for_enrolled( $required, $post ) {
364 + if ( ! $required ) {
365 + return $required;
366 + }
367 +
368 + $post_types = array( tutor()->course_post_type, tutor()->bundle_post_type );
369 + if ( in_array( $post->post_type, $post_types, true ) && EnrollmentModel::is_enrolled( $post->ID ) ) {
370 + return false;
371 + }
372 +
373 + return $required;
374 + }
375 +
376 + /**
346 377 * Handle password protected course/bundle.
347 378 *
348 379 * @since 3.0.0
349 380 *
@@ -881,9 +912,9 @@
881 912
882 913 update_post_meta( $course_id, self::COURSE_PRICE_TYPE_META, self::PRICE_TYPE_FREE );
883 914
884 915 $link = admin_url( 'admin.php?page=create-course' );
885 - if ( Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
916 + if ( tutor()->has_pro && Input::post( 'from_dashboard', false, Input::TYPE_BOOL ) ) {
886 917 $link = tutor_utils()->tutor_dashboard_url( 'create-course' );
887 918 }
888 919
889 920 $link = add_query_arg( array( 'course_id' => $course_id ), $link );
@@ -901,15 +932,14 @@
901 932 * Get course list
902 933 *
903 934 * @since 3.0.0
904 935 *
905 - * @since 3.2.0
936 + * @since 3.2.0 Refactor the arguments & response as per new design.
906 937 *
907 - * Refactor the arguments & response as per new design
908 - *
909 938 * @return void
910 939 */
911 940 public function ajax_course_list() {
941 + tutor_utils()->check_nonce();
912 942 $this->check_access();
913 943
914 944 $limit = Input::post( 'limit', 10, Input::TYPE_INT );
915 945 $offset = Input::post( 'offset', 0, Input::TYPE_INT );
@@ -929,14 +959,9 @@
929 959 );
930 960
931 961 $exclude = Input::post( 'exclude', array(), Input::TYPE_ARRAY );
932 962 if ( count( $exclude ) ) {
933 - $exclude = array_filter(
934 - $exclude,
935 - function ( $id ) {
936 - return is_numeric( $id );
937 - }
938 - );
963 + $exclude = array_filter( $exclude, fn( $id ) => is_numeric( $id ) && $id > 0 );
939 964 $args['post__not_in'] = $exclude;
940 965 }
941 966
942 967 $courses = CourseModel::get_courses_by_args( $args );
@@ -1076,8 +1101,12 @@
1076 1101 )
1077 1102 );
1078 1103
1079 1104 $course_id = (int) $params['course_id'];
1105 + if ( ! $course_id ) {
1106 + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1107 + }
1108 +
1080 1109 $this->check_access( $course_id );
1081 1110
1082 1111 $errors = array();
1083 1112 $validation = $this->validate_inputs( $params );
@@ -1166,11 +1195,15 @@
1166 1195 public function ajax_unlink_page_builder() {
1167 1196 tutor_utils()->check_nonce();
1168 1197
1169 1198 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1170 - $builder = Input::post( 'builder' );
1199 + if ( ! $course_id ) {
1200 + $this->response_bad_request( __( 'Invalid course id', 'tutor' ) );
1201 + }
1202 +
1171 1203 $this->check_access( $course_id );
1172 1204
1205 + $builder = Input::post( 'builder' );
1173 1206 if ( 'elementor' === $builder ) {
1174 1207 delete_post_meta( $course_id, '_elementor_edit_mode' );
1175 1208 } elseif ( 'droip' === $builder ) {
1176 1209 delete_post_meta( $course_id, 'droip_editor_mode' );
@@ -1243,20 +1276,23 @@
1243 1276 /**
1244 1277 * Get course contents
1245 1278 *
1246 1279 * @since 3.0.0
1280 + *
1281 + * @return void
1247 1282 */
1248 1283 public function ajax_course_contents() {
1249 1284 tutor_utils()->check_nonce();
1250 1285
1286 + $errors = array();
1251 1287 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1252 1288
1253 - $this->check_access( $course_id );
1254 -
1255 - if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1289 + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1256 1290 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1257 1291 }
1258 1292
1293 + $this->check_access( $course_id );
1294 +
1259 1295 if ( ! empty( $errors ) ) {
1260 1296 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1261 1297 }
1262 1298
@@ -1280,14 +1316,14 @@
1280 1316
1281 1317 $errors = array();
1282 1318 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1283 1319
1284 - $this->check_access( $course_id );
1285 -
1286 - if ( tutor()->course_post_type !== get_post_type( $course_id ) ) {
1320 + if ( ! $course_id || tutor()->course_post_type !== get_post_type( $course_id ) ) {
1287 1321 $errors['course_id'] = __( 'Invalid course id', 'tutor' );
1288 1322 }
1289 1323
1324 + $this->check_access( $course_id );
1325 +
1290 1326 if ( ! empty( $errors ) ) {
1291 1327 $this->json_response( __( 'Invalid input', 'tutor' ), $errors, HttpHelper::STATUS_UNPROCESSABLE_ENTITY );
1292 1328 }
1293 1329
@@ -1298,9 +1334,9 @@
1298 1334 $price = 0;
1299 1335 $sale_price = 0;
1300 1336 $product_id = tutor_utils()->get_course_product_id( $course_id );
1301 1337
1302 - if ( 'wc' === $monetize_by ) {
1338 + if ( WooCommerce::MONETIZE_BY === $monetize_by ) {
1303 1339 $product = wc_get_product( $product_id );
1304 1340 if ( $product ) {
1305 1341 $product_name = $product->get_name();
1306 1342 $price = $product->get_regular_price();
@@ -1307,9 +1343,9 @@
1307 1343 $sale_price = $product->get_sale_price();
1308 1344 }
1309 1345 }
1310 1346
1311 - if ( 'tutor' === $monetize_by ) {
1347 + if ( Ecommerce::MONETIZE_BY === $monetize_by ) {
1312 1348 $price = get_post_meta( $course_id, self::COURSE_PRICE_META, true );
1313 1349 $sale_price = get_post_meta( $course_id, self::COURSE_SALE_PRICE_META, true );
1314 1350 }
1315 1351
@@ -1491,8 +1527,9 @@
1491 1527 /**
1492 1528 * Localized only options to protect sensitive info like API keys.
1493 1529 */
1494 1530 $required_options = array(
1531 + 'learning_mode',
1495 1532 'monetize_by',
1496 1533 'enable_course_marketplace',
1497 1534 'course_permalink_base',
1498 1535 'supported_video_sources',
@@ -1759,9 +1796,11 @@
1759 1796 /**
1760 1797 * Restrict media
1761 1798 *
1762 1799 * @since 1.0.0
1800 + *
1763 1801 * @param string $where where clause.
1802 + *
1764 1803 * @return string
1765 1804 */
1766 1805 public function restrict_media( $where ) {
1767 1806 $action = Input::post( 'action' );
@@ -1792,9 +1831,9 @@
1792 1831 }
1793 1832
1794 1833 $i = 0;
1795 1834 foreach ( $sort_order as $topic ) {
1796 - $i++;
1835 + ++$i;
1797 1836 $wpdb->update(
1798 1837 $wpdb->posts,
1799 1838 array( 'menu_order' => $i ),
1800 1839 array( 'ID' => $topic['topic_id'] )
@@ -2118,9 +2157,9 @@
2118 2157 // Process purchase.
2119 2158
2120 2159 } else {
2121 2160 // Free enroll.
2122 - tutor_utils()->do_enroll( $course_id );
2161 + EnrollmentModel::do_enroll( $course_id );
2123 2162 }
2124 2163
2125 2164 $referer_url = wp_get_referer();
2126 2165 wp_safe_redirect( tutor_utils()->get_nocache_url( $referer_url ) );
@@ -2154,9 +2193,9 @@
2154 2193 if ( ! $user_id ) {
2155 2194 die( esc_html__( 'Please Sign-In', 'tutor' ) );
2156 2195 }
2157 2196
2158 - if ( ! tutor_utils()->is_enrolled( $course_id, $user_id ) ) {
2197 + if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2159 2198 die( esc_html__( 'User is not enrolled in course', 'tutor' ) );
2160 2199 }
2161 2200
2162 2201 /**
@@ -2163,12 +2202,14 @@
2163 2202 * Filter hook provided to restrict course completion. This is useful
2164 2203 * for specific cases like prerequisites. WP_Error should be returned
2165 2204 * from the filter value to prevent the completion.
2166 2205 */
2167 - $can_complete = apply_filters( 'tutor_user_can_complete_course', true, $user_id, $course_id );
2206 + $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2168 2207
2169 2208 if ( is_wp_error( $can_complete ) ) {
2170 2209 tutor_utils()->redirect_to( $permalink, $can_complete->get_error_message(), 'error' );
2210 + } elseif ( ! $can_complete ) {
2211 + tutor_utils()->redirect_to( $permalink, __( 'You do not have permission to complete this course.', 'tutor' ), 'error' );
2171 2212 } else {
2172 2213 CourseModel::mark_course_as_completed( $course_id, $user_id );
2173 2214 // Set temporary identifier to show review pop up.
2174 2215 self::set_review_popup_data( $user_id, $course_id, $permalink );
@@ -2178,8 +2219,52 @@
2178 2219 }
2179 2220 }
2180 2221
2181 2222 /**
2223 + * Ajax course complete handler
2224 + *
2225 + * @since 4.0.0
2226 + *
2227 + * @return void
2228 + */
2229 + public function ajax_tutor_complete_course() {
2230 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
2231 +
2232 + // Checking nonce.
2233 + if ( ! tutor_utils()->is_nonce_verified() ) {
2234 + $this->response_bad_request( tutor_utils()->error_message( 'nonce' ) );
2235 + }
2236 +
2237 + if ( ! $course_id ) {
2238 + $this->response_bad_request( __( 'Invalid course', 'tutor' ) );
2239 + }
2240 +
2241 + $user_id = get_current_user_id();
2242 + if ( ! EnrollmentModel::is_enrolled( $course_id, $user_id ) ) {
2243 + $this->response_bad_request( __( 'You are not enrolled in this course', 'tutor' ) );
2244 + }
2245 +
2246 + /**
2247 + * Filter hook provided to restrict course completion. This is useful
2248 + * for specific cases like prerequisites. WP_Error should be returned
2249 + * from the filter value to prevent the completion.
2250 + */
2251 + $can_complete = apply_filters( 'tutor_user_can_complete_course', CourseModel::can_complete_course( $course_id, $user_id ), $user_id, $course_id );
2252 +
2253 + if ( is_wp_error( $can_complete ) ) {
2254 + $this->response_bad_request( $can_complete->get_error_message() );
2255 + } elseif ( ! $can_complete ) {
2256 + $this->response_bad_request( __( 'You do not have permission to complete this course.', 'tutor' ) );
2257 + } else {
2258 + CourseModel::mark_course_as_completed( $course_id, $user_id );
2259 + // Set temporary identifier to show review pop up.
2260 + self::set_review_popup_data( $user_id, $course_id );
2261 +
2262 + $this->response_success( __( 'Course completed successfully', 'tutor' ) );
2263 + }
2264 + }
2265 +
2266 + /**
2182 2267 * Set data for review popup.
2183 2268 *
2184 2269 * @since 2.2.5
2185 2270 * @since 2.4.0 removed $permalink param. store user meta instead of option data.
@@ -2205,18 +2290,34 @@
2205 2290 * @since 1.0.0
2206 2291 * @return void
2207 2292 */
2208 2293 public function popup_review_form() {
2209 - if ( is_user_logged_in() ) {
2210 - $user_id = get_current_user_id();
2211 - $course_id = get_the_ID();
2212 - $meta_key = User::get_review_popup_meta( $course_id );
2213 - $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2294 + if ( ! is_user_logged_in() ) {
2295 + return;
2296 + }
2214 2297
2215 - if ( is_single() && $course_id === $review_course_id ) {
2216 - include tutor()->path . 'views/modal/review.php';
2217 - }
2298 + $is_learning_area = tutor_utils()->is_learning_area();
2299 + $is_legacy_learning = tutor_utils()->is_legacy_learning_mode();
2300 + if ( $is_legacy_learning && $is_learning_area ) {
2301 + return;
2218 2302 }
2303 +
2304 + $course_id = get_the_ID();
2305 +
2306 + if ( $is_learning_area && ! Input::has( 'subpage' ) ) {
2307 + $course_id = wp_get_post_parent_id( wp_get_post_parent_id( get_the_ID() ) );
2308 + }
2309 +
2310 + if ( empty( $course_id ) ) {
2311 + return;
2312 + }
2313 +
2314 + $user_id = get_current_user_id();
2315 + $meta_key = User::get_review_popup_meta( $course_id );
2316 + $review_course_id = (int) get_user_meta( $user_id, $meta_key, true );
2317 + if ( is_single() && $course_id === $review_course_id ) {
2318 + include tutor()->path . 'views/modal/review.php';
2319 + }
2219 2320 }
2220 2321
2221 2322 /**
2222 2323 * Review popup data clear
@@ -2244,8 +2345,9 @@
2244 2345 /**
2245 2346 * Delete course delete from frontend dashboard
2246 2347 *
2247 2348 * @since 2.0.0
2349 + *
2248 2350 * @return void
2249 2351 */
2250 2352 public function tutor_delete_dashboard_course() {
2251 2353 tutor_utils()->checking_nonce();
@@ -2264,9 +2366,9 @@
2264 2366 wp_send_json_error( array( 'message' => __( 'Only main instructor can delete this course', 'tutor' ) ) );
2265 2367 }
2266 2368
2267 2369 // Check if user is only an instructor.
2268 - if ( ! current_user_can( 'administrator' ) ) {
2370 + if ( ! User::is_admin() ) {
2269 2371 // Check if instructor can trash course.
2270 2372 $can_trash_post = tutor_utils()->get_option( 'instructor_can_delete_course' );
2271 2373
2272 2374 if ( ! $can_trash_post ) {
@@ -2276,9 +2378,9 @@
2276 2378
2277 2379 $trash_course = wp_update_post(
2278 2380 array(
2279 2381 'ID' => $course_id,
2280 - 'post_status' => 'trash',
2382 + 'post_status' => CourseModel::STATUS_TRASH,
2281 2383 )
2282 2384 );
2283 2385
2284 2386 if ( $trash_course ) {
@@ -2453,33 +2555,8 @@
2453 2555 }
2454 2556 }
2455 2557
2456 2558 /**
2457 - * Add Course level to course settings
2458 - *
2459 - * @since 1.4.1
2460 - *
2461 - * @param array $args arguments.
2462 - * @return array
2463 - */
2464 - public function add_course_level_to_settings( $args ) {
2465 - $course_id = get_the_ID();
2466 - $levels = tutor_utils()->course_levels();
2467 - $course_level = get_post_meta( $course_id, '_tutor_course_level', true );
2468 -
2469 - $args['general']['fields']['_tutor_course_level'] = array(
2470 - 'type' => 'select',
2471 - 'label' => __( 'Difficulty Level', 'tutor' ),
2472 - 'label_title' => __( 'Enable', 'tutor' ),
2473 - 'options' => $levels,
2474 - 'value' => $course_level ? $course_level : 'intermediate',
2475 - 'desc' => __( 'Course difficulty level', 'tutor' ),
2476 - );
2477 -
2478 - return $args;
2479 - }
2480 -
2481 - /**
2482 2559 * Check if course starting
2483 2560 *
2484 2561 * @since 1.4.8
2485 2562 * @return void
@@ -2499,8 +2576,9 @@
2499 2576 /**
2500 2577 * Add Course level to course settings
2501 2578 *
2502 2579 * @since 1.4.8
2580 + *
2503 2581 * @return void
2504 2582 */
2505 2583 public function course_elements_enable_disable() {
2506 2584 add_filter( 'tutor_course/single/completing-progress-bar', array( $this, 'enable_disable_course_progress_bar' ) );
@@ -2533,8 +2611,9 @@
2533 2611 *
2534 2612 * @since 1.4.8
2535 2613 *
2536 2614 * @param string $html HTML string.
2615 + *
2537 2616 * @return string
2538 2617 */
2539 2618 public function enable_disable_material_includes( $html ) {
2540 2619 $disable_option = ! (bool) get_tutor_option( 'enable_course_material', true, true );
@@ -2549,8 +2628,9 @@
2549 2628 *
2550 2629 * @since 1.4.8
2551 2630 *
2552 2631 * @param string $html HTML string.
2632 + *
2553 2633 * @return string
2554 2634 */
2555 2635 public function enable_disable_course_content( $html ) {
2556 2636 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_description', true, true );
@@ -2565,8 +2645,9 @@
2565 2645 *
2566 2646 * @since 1.4.8
2567 2647 *
2568 2648 * @param string $html HTML string.
2649 + *
2569 2650 * @return string
2570 2651 */
2571 2652 public function enable_disable_course_benefits( $html ) {
2572 2653 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_benefits', true, true );
@@ -2581,8 +2662,9 @@
2581 2662 *
2582 2663 * @since 1.4.8
2583 2664 *
2584 2665 * @param string $html HTML string.
2666 + *
2585 2667 * @return string
2586 2668 */
2587 2669 public function enable_disable_course_requirements( $html ) {
2588 2670 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_requirements', true, true );
@@ -2597,8 +2679,9 @@
2597 2679 *
2598 2680 * @since 1.4.8
2599 2681 *
2600 2682 * @param string $html HTML string.
2683 + *
2601 2684 * @return string
2602 2685 */
2603 2686 public function enable_disable_course_target_audience( $html ) {
2604 2687 $disable_option = ! (bool) tutor_utils()->get_option( 'enable_course_target_audience', true, true );
@@ -2643,9 +2726,9 @@
2643 2726 unset( $items['reviews'] );
2644 2727 }
2645 2728
2646 2729 // Whether enrollment require.
2647 - $is_enrolled = tutor_utils()->is_enrolled();
2730 + $is_enrolled = EnrollmentModel::is_enrolled();
2648 2731
2649 2732 return array_filter(
2650 2733 $items,
2651 2734 function ( $item ) use ( $is_enrolled ) {
@@ -2660,8 +2743,9 @@
2660 2743 /**
2661 2744 * Filter product in shop page
2662 2745 *
2663 2746 * @since 1.4.9
2747 + *
2664 2748 * @return void|null
2665 2749 */
2666 2750 public function filter_product_in_shop_page() {
2667 2751 $hide_course_from_shop_page = (bool) get_tutor_option( 'hide_course_from_shop_page' );
@@ -2677,8 +2761,9 @@
2677 2761 /**
2678 2762 * Tutor product meta query
2679 2763 *
2680 2764 * @since 1.4.9
2765 + *
2681 2766 * @return array
2682 2767 */
2683 2768 public function tutor_product_meta_query() {
2684 2769 $meta_query = array(
@@ -2693,8 +2778,9 @@
2693 2778 *
2694 2779 * @since 1.4.9
2695 2780 *
2696 2781 * @param \WP_Query $wp_query WP Query instance.
2782 + *
2697 2783 * @return \WP_Query
2698 2784 */
2699 2785 public function filter_woocommerce_product_query( $wp_query ) {
2700 2786 $product_ids = $this->get_connected_wc_product_ids();
@@ -2736,8 +2822,9 @@
2736 2822 *
2737 2823 * @since 1.4.9
2738 2824 *
2739 2825 * @param \WP_Query $query WP Query instance.
2826 + *
2740 2827 * @return \WP_Query
2741 2828 */
2742 2829 public function filter_edd_downloads_query( $query ) {
2743 2830 $query['meta_query'][] = $this->tutor_product_meta_query();
@@ -2749,8 +2836,9 @@
2749 2836 *
2750 2837 * @since 1.4.9
2751 2838 *
2752 2839 * @param \WP_Query $wp_query WP Query instance.
2840 + *
2753 2841 * @return \WP_Query
2754 2842 */
2755 2843 public function filter_archive_meta_query( $wp_query ) {
2756 2844 if ( ! is_admin() && $wp_query->is_archive && $wp_query->get( 'post_type' ) === 'download' ) {
@@ -2764,8 +2852,9 @@
2764 2852 *
2765 2853 * @since 1.5.8
2766 2854 *
2767 2855 * @param string $html HTML string.
2856 + *
2768 2857 * @return string
2769 2858 */
2770 2859 public function remove_price_if_enrolled( $html ) {
2771 2860 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
@@ -2771,9 +2860,9 @@
2771 2860 $should_removed = apply_filters( 'should_remove_price_if_enrolled', true );
2772 2861
2773 2862 if ( $should_removed ) {
2774 2863 $course_id = get_the_ID();
2775 - $enrolled = tutor_utils()->is_enrolled( $course_id );
2864 + $enrolled = EnrollmentModel::is_enrolled( $course_id );
2776 2865 if ( $enrolled ) {
2777 2866 $html = '';
2778 2867 }
2779 2868 }
@@ -2780,118 +2869,119 @@
2780 2869 return $html;
2781 2870 }
2782 2871
2783 2872 /**
2784 - * Check if all lessons and quizzes done before mark course complete.
2873 + * Get course completion missing requirements message.
2785 2874 *
2786 - * @since 1.5.8
2875 + * @since 4.0.0
2787 2876 *
2788 - * @param string $html HTML string.
2789 - * @return string
2877 + * @param int $course_id Course ID.
2878 + * @param int $user_id User ID.
2879 + *
2880 + * @return string|null
2790 2881 */
2791 - public function tutor_lms_hide_course_complete_btn( $html ) {
2882 + public static function get_course_completion_restrict_msg( $course_id = 0, $user_id = 0 ) {
2883 + $course_id = tutor_utils()->get_post_id( $course_id );
2884 + $user_id = tutor_utils()->get_user_id( $user_id );
2792 2885
2793 - $completion_mode = tutor_utils()->get_option( 'course_completion_process' );
2794 - if ( 'strict' !== $completion_mode ) {
2795 - return $html;
2886 + if ( 'strict' !== tutor_utils()->get_option( 'course_completion_process' ) ) {
2887 + return null;
2796 2888 }
2797 2889
2798 - $completed_lesson = tutor_utils()->get_completed_lesson_count_by_course();
2799 - $lesson_count = tutor_utils()->get_lesson_count_by_course();
2890 + $completed_lessons = tutor_utils()->get_completed_lesson_count_by_course( $course_id, $user_id );
2891 + $total_lessons = tutor_utils()->get_lesson_count_by_course( $course_id );
2800 2892
2801 - if ( $completed_lesson < $lesson_count ) {
2802 - return '<div class="tutor-alert tutor-warning tutor-mt-28">
2803 - <div class="tutor-alert-text">
2804 - <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2805 - <span>' . __( 'Complete all lessons to mark this course as complete', 'tutor' ) . '</span>
2806 - </div>
2807 - </div>';
2893 + if ( $completed_lessons < $total_lessons ) {
2894 + return __( 'Complete all lessons to mark this course as complete', 'tutor' );
2808 2895 }
2809 2896
2810 - $quizzes = array();
2811 - $assignments = array();
2897 + $course_contents = tutor_utils()->get_course_contents_by_id( $course_id );
2812 2898
2813 - $course_contents = tutor_utils()->get_course_contents_by_id();
2814 - if ( tutor_utils()->count( $course_contents ) ) {
2815 - foreach ( $course_contents as $content ) {
2816 - if ( 'tutor_quiz' === $content->post_type ) {
2817 - $quizzes[] = $content;
2818 - }
2819 - if ( 'tutor_assignments' === $content->post_type ) {
2820 - $assignments[] = $content;
2821 - }
2822 - }
2823 - }
2899 + $required_quiz_pass_count = 0;
2900 + $required_assignment_pass_count = 0;
2824 2901
2825 - $required_assignment_pass = 0;
2902 + $is_assignment_addon_enabled = tutor_utils()->is_addon_enabled( 'tutor-assignments' );
2826 2903
2827 - foreach ( $assignments as $row ) {
2904 + foreach ( $course_contents as $content ) {
2828 2905
2829 - $assignment_submission = tutor_utils()->is_assignment_submitted( $row->ID );
2830 - $is_reviewed_by_instructor = ! count( $assignment_submission )
2831 - ? false
2832 - : get_comment_meta( $assignment_submission[0]->comment_ID, 'evaluate_time', true );
2906 + if ( tutor()->quiz_post_type === $content->post_type ) {
2907 + if ( ! QuizModel::is_quiz_passed( $content->ID, $user_id ) ) {
2908 + ++$required_quiz_pass_count;
2909 + }
2910 + }
2833 2911
2834 - if ( $assignment_submission && $is_reviewed_by_instructor ) {
2835 - $pass_mark = tutor_utils()->get_assignment_option( $row->ID, 'pass_mark' );
2836 - $has_passed = false;
2837 - foreach ( $assignment_submission as $submission ) {
2838 - $given_mark = (int) get_comment_meta( $submission->comment_ID, 'assignment_mark', true );
2839 - if ( $given_mark >= $pass_mark ) {
2840 - $has_passed = true;
2841 - break;
2842 - }
2912 + if ( $is_assignment_addon_enabled && tutor()->assignment_post_type === $content->post_type ) {
2913 + if ( ! Assignments::is_assignment_passed( $content->ID, $user_id ) ) {
2914 + ++$required_assignment_pass_count;
2843 2915 }
2844 - if ( ! $has_passed ) {
2845 - $required_assignment_pass++;
2846 - }
2847 - } else {
2848 - $required_assignment_pass++;
2849 2916 }
2850 2917 }
2851 2918
2852 - $is_quiz_pass = true;
2853 - $required_quiz_pass = 0;
2919 + if ( ! $required_quiz_pass_count && ! $required_assignment_pass_count ) {
2920 + return null;
2921 + }
2854 2922
2855 - if ( tutor_utils()->count( $quizzes ) ) {
2856 - foreach ( $quizzes as $quiz ) {
2923 + return self::get_course_completion_requirement_message(
2924 + $required_quiz_pass_count,
2925 + $required_assignment_pass_count
2926 + );
2927 + }
2857 2928
2858 - $attempt = tutor_utils()->get_quiz_attempt( $quiz->ID );
2859 - if ( $attempt ) {
2860 - $passing_grade = tutor_utils()->get_quiz_option( $quiz->ID, 'passing_grade', 0 );
2861 - $earned_percentage = QuizModel::calculate_attempt_earned_percentage( $attempt );
2929 + /**
2930 + * Build missing course completion requirements message.
2931 + *
2932 + * @since 4.0.0
2933 + *
2934 + * @param int $quiz_count Total quiz count.
2935 + * @param int $assignment_count Total assignment count.
2936 + *
2937 + * @return string
2938 + */
2939 + private static function get_course_completion_requirement_message( $quiz_count, $assignment_count ) {
2940 + $quiz_label = _n( 'quiz', 'quizzes', $quiz_count, 'tutor' );
2941 + $assignment_label = _n( 'assignment', 'assignments', $assignment_count, 'tutor' );
2862 2942
2863 - if ( $earned_percentage < $passing_grade ) {
2864 - $required_quiz_pass++;
2865 - $is_quiz_pass = false;
2866 - }
2867 - } else {
2868 - $required_quiz_pass++;
2869 - $is_quiz_pass = false;
2870 - }
2871 - }
2943 + if ( $quiz_count && ! $assignment_count ) {
2944 + return sprintf(
2945 + /* translators: %1$s: item count; %2$s: item label */
2946 + __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2947 + $quiz_count,
2948 + $quiz_label
2949 + );
2872 2950 }
2873 2951
2874 - if ( ! $is_quiz_pass || $required_assignment_pass > 0 ) {
2875 - $_msg = '';
2876 - $quiz_str = _n( 'quiz', 'quizzes', $required_quiz_pass, 'tutor' );
2877 - $assignment_str = _n( 'assignment', 'assignments', $required_assignment_pass, 'tutor' );
2952 + if ( ! $quiz_count && $assignment_count ) {
2953 + return sprintf(
2954 + /* translators: %1$s: item count; %2$s: item label */
2955 + __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ),
2956 + $assignment_count,
2957 + $assignment_label
2958 + );
2959 + }
2878 2960
2879 - if ( ! $is_quiz_pass && 0 == $required_assignment_pass ) {
2880 - /* translators: %1$s: number of quiz/assignment pass required; %2$s: quiz/assignment string */
2881 - $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str );
2882 - }
2961 + return sprintf(
2962 + /* translators: %1$s: quiz count; %2$s: quiz label; %3$s: assignment count; %4$s: assignment label */
2963 + __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ),
2964 + $quiz_count,
2965 + $quiz_label,
2966 + $assignment_count,
2967 + $assignment_label
2968 + );
2969 + }
2883 2970
2884 - if ( $is_quiz_pass && $required_assignment_pass > 0 ) {
2885 - //phpcs:ignore
2886 - $_msg = sprintf( __( 'You have to pass %1$s %2$s to complete this course.', 'tutor' ), $required_assignment_pass, $assignment_str );
2887 - }
2971 + /**
2972 + * Check if all lessons and quizzes done before mark course complete.
2973 + *
2974 + * @since 1.5.8
2975 + *
2976 + * @param string $html HTML string.
2977 + *
2978 + * @return string
2979 + */
2980 + public function tutor_lms_hide_course_complete_btn( $html ) {
2981 + $_msg = self::get_course_completion_restrict_msg();
2888 2982
2889 - if ( ! $is_quiz_pass && $required_assignment_pass > 0 ) {
2890 - /* translators: %1$s: number of quiz pass required; %2$s: quiz string; %3$s: number of assignment pass required; %4$s: assignment string */
2891 - $_msg = sprintf( __( 'You have to pass %1$s %2$s and %3$s %4$s to complete this course.', 'tutor' ), $required_quiz_pass, $quiz_str, $required_assignment_pass, $assignment_str );
2892 - }
2893 -
2983 + if ( $_msg ) {
2894 2984 return '<div class="tutor-alert tutor-warning tutor-mt-28">
2895 2985 <div class="tutor-alert-text">
2896 2986 <span class="tutor-alert-icon tutor-fs-4 tutor-icon-circle-info tutor-mr-12"></span>
2897 2987 <span>' . $_msg . '</span>
@@ -2907,8 +2997,9 @@
2907 2997 *
2908 2998 * @since 1.5.8
2909 2999 *
2910 3000 * @param string $html HTML string.
3001 + *
2911 3002 * @return string
2912 3003 */
2913 3004 public function get_generate_greadbook( $html ) {
2914 3005 if ( ! tutor_utils()->is_completed_course() ) {
@@ -2920,8 +3011,9 @@
2920 3011 /**
2921 3012 * Add social share content in header
2922 3013 *
2923 3014 * @since 1.6.3
3015 + *
2924 3016 * @return void
2925 3017 */
2926 3018 public function social_share_content() {
2927 3019 global $wp_query, $post;
@@ -2945,8 +3037,9 @@
2945 3037 *
2946 3038 * @since 1.8.2
2947 3039 *
2948 3040 * @param integer $post_id post ID.
3041 + *
2949 3042 * @return void
2950 3043 */
2951 3044 public function delete_associated_enrollment( $post_id ) {
2952 3045 global $wpdb;
@@ -2957,11 +3050,12 @@
2957 3050 post_id
2958 3051 FROM
2959 3052 {$wpdb->postmeta}
2960 3053 WHERE
2961 - meta_key='_tutor_enrolled_by_order_id'
3054 + meta_key=%s
2962 3055 AND meta_value = %d
2963 3056 ",
3057 + EnrollmentModel::ENROLLMENT_ORDER_ID_META,
2964 3058 $post_id
2965 3059 )
2966 3060 );
2967 3061
@@ -2977,21 +3071,35 @@
2977 3071 /**
2978 3072 * Reset course progress.
2979 3073 *
2980 3074 * @since 1.5.8
3075 + *
2981 3076 * @return void
2982 3077 */
2983 3078 public function tutor_reset_course_progress() {
2984 3079 tutor_utils()->checking_nonce();
2985 - $course_id = Input::post( 'course_id' );
3080 + $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3081 + $context = Input::post( 'context', '' );
3082 + $course_reset_progress = tutor_utils()->get_option( 'course_reset_progress', false );
3083 + $course_retake_feature = tutor_utils()->get_option( 'course_retake_feature', false );
2986 3084
2987 - if ( ! $course_id || ! is_numeric( $course_id ) || ! tutor_utils()->is_enrolled( $course_id ) ) {
2988 - wp_send_json_error( array( 'message' => __( 'Invalid Course ID or Access Denied.', 'tutor' ) ) );
3085 + if ( ! $course_reset_progress && 'learning-area-sidebar' === $context ) {
3086 + $this->response_bad_request( __( 'You do not have permission to reset this course.', 'tutor' ) );
2989 3087 return;
2990 3088 }
2991 3089
3090 + if ( ! $course_retake_feature && ( 'course-landing' === $context || 'learning-area' === $context ) ) {
3091 + $this->response_bad_request( __( 'You do not have permission to retake this course.', 'tutor' ) );
3092 + return;
3093 + }
3094 +
3095 + if ( ! $course_id || ! is_numeric( $course_id ) || ! EnrollmentModel::is_enrolled( $course_id ) ) {
3096 + $this->response_bad_request( __( 'Invalid Course ID or Access Denied.', 'tutor' ) );
3097 + return;
3098 + }
3099 +
2992 3100 tutor_utils()->delete_course_progress( $course_id );
2993 - wp_send_json_success( array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
3101 + $this->json_response( '', array( 'redirect_to' => tutor_utils()->get_course_first_lesson( $course_id ) ) );
2994 3102 }
2995 3103
2996 3104 /**
2997 3105 * Do enroll if guest attempt to enroll and course is free
@@ -2999,9 +3107,9 @@
2999 3107 * @since 1.9.8
3000 3108 *
3001 3109 * @param integer $course_id course ID.
3002 3110 * @param integer $user_id user ID.
3003 -
3111 + *
3004 3112 * @return void
3005 3113 */
3006 3114 public function enroll_after_login_if_attempt( int $course_id, int $user_id ) {
3007 3115 $course_id = sanitize_text_field( $course_id );
@@ -3009,9 +3117,9 @@
3009 3117
3010 3118 if ( $course_id && $is_allowed ) {
3011 3119 $is_purchasable = tutor_utils()->is_course_purchasable( $course_id );
3012 3120 if ( ! $is_purchasable ) {
3013 - tutor_utils()->do_enroll( $course_id, $order_id = 0, $user_id );
3121 + EnrollmentModel::do_enroll( $course_id, $order_id = 0, $user_id );
3014 3122 do_action( 'guest_attempt_after_enrollment', $course_id );
3015 3123 }
3016 3124 }
3017 3125 }
@@ -3019,8 +3127,9 @@
3019 3127 /**
3020 3128 * Handle course enrollment
3021 3129 *
3022 3130 * @since 2.1.0
3131 + *
3023 3132 * @return void
3024 3133 */
3025 3134 public function course_enrollment() {
3026 3135 tutor_utils()->checking_nonce();
@@ -3027,46 +3136,46 @@
3027 3136
3028 3137 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
3029 3138 $user_id = get_current_user_id();
3030 3139
3031 - if ( $course_id ) {
3032 - $password_protected = post_password_required( $course_id );
3033 - if ( $password_protected ) {
3034 - wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3035 - }
3140 + if ( ! $course_id || ! $user_id ) {
3141 + wp_send_json_error( tutor_utils()->error_message( 'invalid_req' ) );
3142 + }
3036 3143
3037 - $course = get_post( $course_id );
3144 + $password_protected = post_password_required( $course_id );
3145 + if ( $password_protected ) {
3146 + wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
3147 + }
3038 3148
3039 - if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3040 - wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3041 - }
3149 + $course = get_post( $course_id );
3042 3150
3043 - /**
3044 - * This check was added to address a security issue where users could
3045 - * enroll in a course via an AJAX call without purchasing it.
3046 - *
3047 - * To prevent this, we now verify whether the course is paid.
3048 - * Additionally, we check if the user is already enrolled, since
3049 - * Tutor's default behavior enrolls users automatically upon purchase.
3050 - *
3051 - * @since 3.9.4
3052 - */
3053 - if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3054 - $is_enrolled = (bool) tutor_utils()->is_enrolled( $course_id, $user_id );
3151 + if ( CourseModel::STATUS_PRIVATE === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
3152 + wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
3153 + }
3055 3154
3056 - if ( ! $is_enrolled ) {
3057 - wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3058 - }
3155 + /**
3156 + * This check was added to address a security issue where users could
3157 + * enroll in a course via an AJAX call without purchasing it.
3158 + *
3159 + * To prevent this, we now verify whether the course is paid.
3160 + * Additionally, we check if the user is already enrolled, since
3161 + * Tutor's default behavior enrolls users automatically upon purchase.
3162 + *
3163 + * @since 3.9.4
3164 + */
3165 + if ( tutor_utils()->is_course_purchasable( $course_id ) ) {
3166 + $is_enrolled = (bool) EnrollmentModel::is_enrolled( $course_id, $user_id );
3167 +
3168 + if ( ! $is_enrolled ) {
3169 + wp_send_json_error( __( 'Please purchase the course before enrolling', 'tutor' ) );
3059 3170 }
3171 + }
3060 3172
3061 - $enroll = tutor_utils()->do_enroll( $course_id, 0, $user_id );
3062 - if ( $enroll ) {
3063 - wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3064 - } else {
3065 - wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3066 - }
3173 + $enroll = EnrollmentModel::do_enroll( $course_id, 0, $user_id );
3174 + if ( $enroll ) {
3175 + wp_send_json_success( __( 'Enrollment successfully done!', 'tutor' ) );
3067 3176 } else {
3068 - wp_send_json_error( __( 'Invalid course ID', 'tutor' ) );
3177 + wp_send_json_error( __( 'Enrollment failed, please try again!', 'tutor' ) );
3069 3178 }
3070 3179 }
3071 3180
3072 3181 /**
@@ -3245,8 +3354,9 @@
3245 3354 * @since 3.8.2
3246 3355 *
3247 3356 * @param int $post_ID The WordPress post ID of the course.
3248 3357 * @param int $product_id The WooCommerce product ID to associate with the course.
3358 + *
3249 3359 * @return void
3250 3360 */
3251 3361 public static function sync_course_with_wc_product( $post_ID, $product_id ) {
3252 3362
@@ -3271,6 +3381,163 @@
3271 3381
3272 3382 // Set course regular & sale price.
3273 3383 update_post_meta( $post_ID, self::COURSE_PRICE_META, $regular_price );
3274 3384 update_post_meta( $post_ID, self::COURSE_SALE_PRICE_META, $sale_price );
3385 + }
3386 +
3387 + /**
3388 + * Render start/resume button in enrolled course action btn.
3389 + *
3390 + * @since 4.0.0
3391 + *
3392 + * @param int $course_id course id.
3393 + *
3394 + * @return void
3395 + */
3396 + public function render_course_action_btn( int $course_id ) {
3397 + $is_completed = tutor_utils()->is_completed_course( $course_id );
3398 + if ( $is_completed ) {
3399 + $certificate_addon_active = tutor_utils()->is_addon_enabled( 'tutor-certificate' );
3400 + if ( ! $certificate_addon_active ) {
3401 + Button::make()
3402 + ->tag( 'a' )
3403 + ->label( __( 'Completed', 'tutor' ) )
3404 + ->icon( Icon::COMPLETED_CIRCLE )
3405 + ->variant( Variant::PRIMARY )
3406 + ->size( Size::X_SMALL )
3407 + ->attr( 'href', esc_url( get_the_permalink( $course_id ) ) )
3408 + ->render();
3409 + }
3410 + return;
3411 + }
3412 +
3413 + $course_progress = tutor_utils()->get_course_completed_percent( $course_id, 0, true );
3414 + $button_text = $course_progress['completed_percent'] > 0 ? __( 'Resume', 'tutor' ) : __( 'Start', 'tutor' );
3415 + $button_url = tutor_utils()->get_course_first_lesson( $course_id );
3416 +
3417 + if ( ! $button_url ) {
3418 + $button_url = get_the_permalink( $course_id );
3419 + }
3420 +
3421 + Button::make()
3422 + ->tag( 'a' )
3423 + ->label( $button_text )
3424 + ->icon( Icon::PLAY_2 )
3425 + ->variant( Variant::PRIMARY )
3426 + ->size( Size::X_SMALL )
3427 + ->attr( 'href', esc_url( $button_url ) )
3428 + ->render();
3429 + }
3430 +
3431 + /**
3432 + * Get the content for the course completion modal.
3433 + *
3434 + * This method returns HTML for displaying a modal that informs the user
3435 + * that they have not completed all required lessons and assessments. It includes
3436 + * the user's current progress shown as a percentage and a progress bar.
3437 + *
3438 + * @since 4.0.0
3439 + *
3440 + * @param float $course_progress The completion percentage of the course.
3441 + *
3442 + * @return string The rendered HTML content for the modal.
3443 + */
3444 + public static function get_complete_modal_content( float $course_progress = 0 ): string {
3445 + ob_start();
3446 + ?>
3447 + <div>
3448 + <p class="tutor-p3 tutor-text-secondary tutor-text-center tutor-mb-7 tutor-px-11">
3449 + <?php esc_html_e( 'You have not completed all required lessons and assessments. ', 'tutor' ); ?>
3450 + </p>
3451 + <div class="tutor-finish-course-progress tutor-border tutor-p-5 tutor-flex tutor-flex-column tutor-gap-4 tutor-surface-base tutor-rounded-md">
3452 + <div class="tutor-flex tutor-items-center tutor-justify-between">
3453 + <div class="tutor-p3 tutor-text-secondary">
3454 + <?php esc_html_e( 'Your Progress', 'tutor' ); ?>
3455 + </div>
3456 + <div class="tutor-p1 tutor-font-bold">
3457 + <?php echo esc_html( number_format_i18n( $course_progress ) . '%' ); ?>
3458 + </div>
3459 + </div>
3460 + <?php
3461 + Progress::make()->type( 'bar' )->value( $course_progress )->render();
3462 + ?>
3463 + </div>
3464 + </div>
3465 + <?php
3466 + return ob_get_clean();
3467 + }
3468 +
3469 + /**
3470 + * Render the course complete button.
3471 + *
3472 + * Displays a button that allows the user to complete the course. If the course
3473 + * progress is less than 100%, clicking the button shows a modal informing the user
3474 + * that not all requirements are fulfilled. If the course progress is 100%,
3475 + * clicking the button attempts to complete the course.
3476 + *
3477 + * @since 4.0.0
3478 + *
3479 + * @param string $modal_id The HTML ID of the modal to display if not complete.
3480 + * @param int $course_id The ID of the course.
3481 + * @param float $course_progress The current completion percentage of the course.
3482 + * @param string $size The button size.
3483 + * @param string $tooltip Optional. Tooltip message.
3484 + * @param bool $block Optional. Whether the button is full-width.
3485 + *
3486 + * @return void
3487 + */
3488 + public static function render_course_complete_btn( string $modal_id, int $course_id, float $course_progress = 0, string $size = Size::MEDIUM, string $tooltip = '', bool $block = false ): void {
3489 + $button = Button::make()
3490 + ->variant( Variant::SECONDARY )
3491 + ->label( __( 'Complete the Course', 'tutor' ) )
3492 + ->icon( Icon::TICK_MARK )
3493 + ->size( $size )
3494 + ->block( $block )
3495 + ->attr( 'type', 'button' );
3496 +
3497 + if ( ! empty( $tooltip ) ) {
3498 + $button->disabled();
3499 + }
3500 +
3501 + if ( $course_progress < 100 ) {
3502 + $button->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" );
3503 + } else {
3504 + $button->attr( '@click', "handleCourseComplete({$course_id})" );
3505 + $button->attr( ':class', "courseCompleteMutation?.isPending ? 'tutor-btn-loading' : ''" );
3506 + $button->attr( ':disabled', 'courseCompleteMutation?.isPending' );
3507 + }
3508 +
3509 + if ( ! empty( $tooltip ) ) {
3510 + Tooltip::make()
3511 + ->content( $tooltip )
3512 + ->placement( Tooltip::PLACEMENT_BOTTOM )
3513 + ->arrow( Tooltip::ARROW_CENTER )
3514 + ->trigger_element( $button->get() )
3515 + ->render();
3516 + } else {
3517 + $button->render();
3518 + }
3519 + }
3520 +
3521 + /**
3522 + * Render course retake button
3523 + *
3524 + * @since 4.0.0
3525 + *
3526 + * @param string $modal_id Modal id.
3527 + * @param string $size The button size.
3528 + * @param bool $block Optional. Whether the button is full-width.
3529 + *
3530 + * @return void
3531 + */
3532 + public static function render_course_retake_btn( string $modal_id, string $size = Size::MEDIUM, bool $block = false ): void {
3533 + Button::make()
3534 + ->variant( Variant::SECONDARY )
3535 + ->label( __( 'Retake this Course', 'tutor' ) )
3536 + ->icon( Icon::RELOAD_4 )
3537 + ->size( $size )
3538 + ->block( $block )
3539 + ->attr( 'type', 'button' )
3540 + ->attr( '@click', "TutorCore.modal.showModal('{$modal_id}')" )
3541 + ->render();
3275 3542 }
3276 3543 }