| @@ -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 | } |