diff --git a/.changelogs/feature_embed-linked-models-student-progress-webhook-1.yml b/.changelogs/feature_embed-linked-models-student-progress-webhook-1.yml new file mode 100644 index 00000000..211e2445 --- /dev/null +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook-1.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: Allow several endpoints to be "embeddable" along with their links. diff --git a/.changelogs/feature_embed-linked-models-student-progress-webhook-2.yml b/.changelogs/feature_embed-linked-models-student-progress-webhook-2.yml new file mode 100644 index 00000000..86c3ce1b --- /dev/null +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook-2.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: Ability to list progresses for a student via /students//progress. diff --git a/.changelogs/feature_embed-linked-models-student-progress-webhook.yml b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml new file mode 100644 index 00000000..c66e519d --- /dev/null +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: New Quiz, Quiz Attempt, Certificate, Awarded Certificate, and Order endpoints. diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index 6b6fddae..d05ec2fb 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -57,7 +57,6 @@ private function __construct() { // Load everything else. add_action( 'plugins_loaded', array( $this, 'init' ), 10 ); - } /** @@ -70,6 +69,10 @@ private function __construct() { */ public function includes() { + if ( ! class_exists( 'LLMS_Abstract_Database_Store' ) ) { + return; + } + // Abstracts. include_once LLMS_REST_API_PLUGIN_DIR . 'includes/abstracts/class-llms-rest-database-resource.php'; include_once LLMS_REST_API_PLUGIN_DIR . 'includes/abstracts/class-llms-rest-webhook-data.php'; @@ -98,7 +101,6 @@ public function includes() { add_action( 'rest_api_init', array( $this, 'rest_api_includes' ), 5 ); add_action( 'rest_api_init', array( $this, 'rest_api_controllers_init' ), 10 ); - } /** @@ -140,6 +142,11 @@ public function rest_api_includes() { 'server/class-llms-rest-api-keys-controller', 'server/class-llms-rest-access-plans-controller', 'server/class-llms-rest-courses-controller', + 'server/class-llms-rest-quizzes-controller', + 'server/class-llms-rest-quiz-attempts-controller', + 'server/class-llms-rest-certificates-controller', + 'server/class-llms-rest-awarded-certificates-controller', + 'server/class-llms-rest-orders-controller', 'server/class-llms-rest-sections-controller', 'server/class-llms-rest-lessons-controller', 'server/class-llms-rest-memberships-controller', @@ -172,6 +179,11 @@ public function rest_api_controllers_init() { 'LLMS_REST_Courses_Controller', 'LLMS_REST_Sections_Controller', 'LLMS_REST_Lessons_Controller', + 'LLMS_REST_Quizzes_Controller', + 'LLMS_REST_Quiz_Attempts_Controller', + 'LLMS_REST_Certificates_Controller', + 'LLMS_REST_Awarded_Certificates_Controller', + 'LLMS_REST_Orders_Controller', 'LLMS_REST_Memberships_Controller', 'LLMS_REST_Instructors_Controller', 'LLMS_REST_Students_Controller', @@ -185,7 +197,6 @@ public function rest_api_controllers_init() { $controller_instance = new $controller(); $controller_instance->register_routes(); } - } /** @@ -216,7 +227,6 @@ public function init() { add_action( 'init', array( $this->webhooks(), 'load' ), 6 ); add_action( 'deleted_user', array( $this, 'on_user_deletion' ) ); - } /** @@ -278,7 +288,6 @@ public function load_textdomain() { // Load from the plugin's language file directory. load_textdomain( 'lifterlms', LLMS_REST_API_PLUGIN_DIR . '/i18n/lifterlms-rest-' . $locale . '.mo' ); - } /** @@ -293,5 +302,4 @@ public function load_textdomain() { public function webhooks() { return LLMS_REST_Webhooks::instance(); } - } diff --git a/includes/abstracts/class-llms-rest-posts-controller.php b/includes/abstracts/class-llms-rest-posts-controller.php index e2a87f0b..59cb14d8 100644 --- a/includes/abstracts/class-llms-rest-posts-controller.php +++ b/includes/abstracts/class-llms-rest-posts-controller.php @@ -109,7 +109,6 @@ public function get_delete_item_args() { 'default' => false, ), ); - } /** @@ -132,7 +131,6 @@ public function get_get_item_params() { } return $params; - } /** @@ -163,7 +161,6 @@ public function get_items_permissions_check( $request ) { } return true; - } /** @@ -189,7 +186,6 @@ protected function get_pagination_data_from_query( $query, $prepared, $request ) $total_pages = (int) ceil( $total_results / (int) $query->get( 'posts_per_page' ) ); return compact( 'current_page', 'total_results', 'total_pages' ); - } /** @@ -411,7 +407,6 @@ public function get_collection_params() { } return $query_params; - } /** @@ -437,7 +432,6 @@ protected function prepare_collection_query_args( $request ) { $query_args = $this->prepare_items_query( $prepared, $request ); return $query_args; - } /** @@ -622,7 +616,6 @@ public function update_item( $request ) { do_action( "llms_rest_after_insert_{$this->post_type}", $object, $request, $schema, false ); return $this->prepare_item_for_response( $object, $request ); - } /** @@ -673,7 +666,6 @@ public function delete_item_permissions_check( $request ) { } return true; - } /** @@ -747,7 +739,6 @@ public function delete_item( $request ) { } return $response; - } /** @@ -786,7 +777,6 @@ protected function is_trash_supported() { protected function get_objects_query( $prepared, $request ) { return new WP_Query( $prepared ); - } /** @@ -801,7 +791,6 @@ protected function get_objects_query( $prepared, $request ) { protected function get_objects_from_query( $query ) { return $query->posts; - } /** @@ -830,7 +819,6 @@ protected function prepare_collection_items_for_response( $objects, $request ) { } return $items; - } /** @@ -880,7 +868,6 @@ protected function prepare_object_for_response( $object, $request ) { ); return $data; - } /** @@ -925,7 +912,6 @@ protected function prepare_object_data_for_response( $object, $request ) { wp_reset_postdata(); return $data; - } /** @@ -956,7 +942,6 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul } return $query_args; - } /** @@ -985,7 +970,6 @@ protected function prepare_items_query_orderby_mappings( $query_args, $request ) } return $query_args; - } /** @@ -1093,7 +1077,6 @@ protected function prepare_item_for_database( $request ) { } return $prepared_item; - } /** @@ -1150,7 +1133,7 @@ protected function get_item_schema_base() { 'title' => array( 'description' => __( 'Post title.', 'lifterlms' ), 'type' => 'object', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). @@ -1165,7 +1148,7 @@ protected function get_item_schema_base() { 'rendered' => array( 'description' => __( 'Rendered title.', 'lifterlms' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), ), @@ -1231,7 +1214,7 @@ protected function get_item_schema_base() { 'description' => __( 'Post URL.', 'lifterlms' ), 'type' => 'string', 'format' => 'uri', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), 'slug' => array( @@ -1246,7 +1229,7 @@ protected function get_item_schema_base() { 'description' => __( 'LifterLMS custom post type', 'lifterlms' ), 'type' => 'string', 'readonly' => true, - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ), 'status' => array( 'description' => __( 'The publication status of the post.', 'lifterlms' ), @@ -1290,7 +1273,6 @@ protected function get_item_schema_base() { ); return $schema; - } /** @@ -1421,7 +1403,6 @@ protected function prepare_links( $object, $request ) { } return $links; - } /** @@ -1454,7 +1435,6 @@ protected function maybe_remove_filters_for_response( $object ) { } return $filters_removed; - } /** @@ -1504,7 +1484,6 @@ protected function get_filters_to_be_removed_for_response( $object ) { * @param LLMS_Post_Model $object LLMS_Post_Model object. */ return apply_filters( "llms_rest_{$this->post_type}_filters_removed_for_response", array(), $object ); - } /** @@ -1574,7 +1553,6 @@ protected function handle_featured_media( $featured_media, $object_id ) { } else { return delete_post_thumbnail( $object_id ); } - } /** @@ -1655,7 +1633,6 @@ protected function check_assign_terms_permission( $request ) { protected function get_taxonomy_rest_base( $taxonomy ) { return ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - } /** @@ -1669,7 +1646,6 @@ protected function check_create_permission() { $post_type = get_post_type_object( $this->post_type ); return current_user_can( $post_type->cap->publish_posts ); - } /** @@ -1684,7 +1660,6 @@ protected function check_update_permission( $object = null ) { $post_type = get_post_type_object( $this->post_type ); return is_null( $object ) ? current_user_can( $post_type->cap->edit_posts ) : current_user_can( $post_type->cap->edit_post, $object->get( 'id' ) ); - } /** @@ -1699,7 +1674,6 @@ protected function check_delete_permission( $object ) { $post_type = get_post_type_object( $this->post_type ); return current_user_can( $post_type->cap->delete_post, $object->get( 'id' ) ); - } /** @@ -1750,7 +1724,6 @@ protected function check_read_permission( $object ) { } return false; - } @@ -1850,5 +1823,4 @@ public function sanitize_post_statuses( $statuses, $request, $parameter ) { return $statuses; } - } diff --git a/includes/abstracts/class-llms-rest-webhook-data.php b/includes/abstracts/class-llms-rest-webhook-data.php index b064d9c7..ab51adb7 100644 --- a/includes/abstracts/class-llms-rest-webhook-data.php +++ b/includes/abstracts/class-llms-rest-webhook-data.php @@ -71,7 +71,6 @@ public function __construct( $id = null, $hydrate = true ) { // Adds created and updated dates on instantiation. parent::__construct(); - } @@ -92,7 +91,6 @@ public function get_delete_link() { ), LLMS_REST_API()->keys()->get_admin_url() ); - } /** @@ -134,7 +132,6 @@ public function get_delivery_signature( $payload ) { $hash = hash_hmac( $hash_algo, $message, $this->get( 'secret' ) ); return sprintf( 't=%1$d,v1=%2$s', $ts, $hash ); - } /** @@ -165,7 +162,6 @@ public function get_event() { $topic = explode( '.', $this->get( 'topic' ) ); return apply_filters( 'llms_rest_webhook_get_event', isset( $topic[1] ) ? $topic[1] : '', $this->get( 'id' ) ); - } /** @@ -186,7 +182,6 @@ public function get_hooks() { } return apply_filters( 'llms_rest_webhook_get_hooks', $hooks, $this->get( 'id' ) ); - } /** @@ -227,11 +222,19 @@ protected function get_payload( $args ) { $endpoint = sprintf( '/llms/v1/students/%1$d/enrollments/%2$d', $args[0], $args[1] ); } elseif ( 'progress' === $resource ) { $endpoint = sprintf( '/llms/v1/students/%1$d/progress/%2$d', $args[0], $args[1] ); + } elseif ( 'quiz-attempt' === $resource ) { + $endpoint = sprintf( '/llms/v1/quiz-attempts/%d', $args[2]->get( 'id' ) ); } else { - $endpoint = sprintf( '/llms/v1/%1$ss/%2$d', $resource, $args[0] ); + // Cast as string first in case $args[0] is an object with a __toString() method. + $endpoint = sprintf( '/llms/v1/%1$ss/%2$d', $resource, intval( (string) $args[0] ) ); } - $payload = llms_rest_get_api_endpoint_data( $endpoint ); + $payload = llms_rest_get_api_endpoint_data( + $endpoint, + array( + '_embed' => true, + ) + ); } @@ -250,7 +253,6 @@ protected function get_payload( $args ) { * @param LLMS_REST_Webhook $this Webhook object. */ return apply_filters( 'llms_rest_webhook_get_payload', $payload, $resource, $event, $args, $this ); - } /** @@ -264,7 +266,6 @@ public function get_resource() { $topic = explode( '.', $this->get( 'topic' ) ); return apply_filters( 'llms_rest_webhook_get_resource', $topic[0], $this->get( 'id' ) ); - } /** @@ -317,7 +318,5 @@ protected function set_delivery_failure() { } return $this; - } - } diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index 87b7ccfb..ef2fe6ba 100644 --- a/includes/class-llms-rest-webhooks.php +++ b/includes/class-llms-rest-webhooks.php @@ -82,7 +82,6 @@ public function create( $data ) { unset( $data['failure_count'] ); return $this->save( new $this->model(), $data ); - } /** @@ -137,7 +136,6 @@ public function get_default_column_values() { ); return parent::get_default_column_values(); - } /** @@ -175,7 +173,6 @@ public function get_statuses() { 'disabled' => __( 'Disabled', 'lifterlms' ), ) ); - } /** @@ -199,46 +196,47 @@ public function get_topics() { return apply_filters( 'llms_rest_webhook_topics', array( - 'course.created' => __( 'Course created', 'lifterlms' ), - 'course.updated' => __( 'Course updated', 'lifterlms' ), - 'course.deleted' => __( 'Course deleted', 'lifterlms' ), - 'course.restored' => __( 'Course restored', 'lifterlms' ), - 'section.created' => __( 'Section created', 'lifterlms' ), - 'section.updated' => __( 'Section updated', 'lifterlms' ), - 'section.deleted' => __( 'Section deleted', 'lifterlms' ), - 'lesson.created' => __( 'Lesson created', 'lifterlms' ), - 'lesson.updated' => __( 'Lesson updated', 'lifterlms' ), - 'lesson.deleted' => __( 'Lesson deleted', 'lifterlms' ), - 'lesson.restored' => __( 'Lesson restored', 'lifterlms' ), - 'membership.created' => __( 'Membership created', 'lifterlms' ), - 'membership.updated' => __( 'Membership updated', 'lifterlms' ), - 'membership.deleted' => __( 'Membership deleted', 'lifterlms' ), - 'membership.restored' => __( 'Membership restored', 'lifterlms' ), - 'access_plan.created' => __( 'Access Plan created', 'lifterlms' ), - 'access_plan.updated' => __( 'Access Plan updated', 'lifterlms' ), - 'access_plan.deleted' => __( 'Access Plan deleted', 'lifterlms' ), - 'order.created' => __( 'Order created', 'lifterlms' ), - 'order.updated' => __( 'Order updated', 'lifterlms' ), - 'order.deleted' => __( 'Order deleted', 'lifterlms' ), - 'order.restored' => __( 'Order restored', 'lifterlms' ), - 'transaction.created' => __( 'Transaction created', 'lifterlms' ), - 'transaction.updated' => __( 'Transaction updated', 'lifterlms' ), - 'transaction.deleted' => __( 'Transaction deleted', 'lifterlms' ), - 'student.created' => __( 'Student created', 'lifterlms' ), - 'student.updated' => __( 'Student updated', 'lifterlms' ), - 'student.deleted' => __( 'Student deleted', 'lifterlms' ), - 'enrollment.created' => __( 'Enrollment created', 'lifterlms' ), - 'enrollment.updated' => __( 'Enrollment updated', 'lifterlms' ), - 'enrollment.deleted' => __( 'Enrollment deleted', 'lifterlms' ), - 'progress.updated' => __( 'Progress updated', 'lifterlms' ), - 'progress.deleted' => __( 'Progress deleted', 'lifterlms' ), - 'instructor.created' => __( 'Instructor created', 'lifterlms' ), - 'instructor.updated' => __( 'Instructor updated', 'lifterlms' ), - 'instructor.deleted' => __( 'Instructor deleted', 'lifterlms' ), - 'action' => __( 'Action', 'lifterlms' ), + 'course.created' => __( 'Course created', 'lifterlms' ), + 'course.updated' => __( 'Course updated', 'lifterlms' ), + 'course.deleted' => __( 'Course deleted', 'lifterlms' ), + 'course.restored' => __( 'Course restored', 'lifterlms' ), + 'section.created' => __( 'Section created', 'lifterlms' ), + 'section.updated' => __( 'Section updated', 'lifterlms' ), + 'section.deleted' => __( 'Section deleted', 'lifterlms' ), + 'lesson.created' => __( 'Lesson created', 'lifterlms' ), + 'lesson.updated' => __( 'Lesson updated', 'lifterlms' ), + 'lesson.deleted' => __( 'Lesson deleted', 'lifterlms' ), + 'lesson.restored' => __( 'Lesson restored', 'lifterlms' ), + 'membership.created' => __( 'Membership created', 'lifterlms' ), + 'membership.updated' => __( 'Membership updated', 'lifterlms' ), + 'membership.deleted' => __( 'Membership deleted', 'lifterlms' ), + 'membership.restored' => __( 'Membership restored', 'lifterlms' ), + 'access_plan.created' => __( 'Access Plan created', 'lifterlms' ), + 'access_plan.updated' => __( 'Access Plan updated', 'lifterlms' ), + 'access_plan.deleted' => __( 'Access Plan deleted', 'lifterlms' ), + 'order.created' => __( 'Order created', 'lifterlms' ), + 'order.updated' => __( 'Order updated', 'lifterlms' ), + 'order.deleted' => __( 'Order deleted', 'lifterlms' ), + 'order.restored' => __( 'Order restored', 'lifterlms' ), + 'transaction.created' => __( 'Transaction created', 'lifterlms' ), + 'transaction.updated' => __( 'Transaction updated', 'lifterlms' ), + 'transaction.deleted' => __( 'Transaction deleted', 'lifterlms' ), + 'student.created' => __( 'Student created', 'lifterlms' ), + 'student.updated' => __( 'Student updated', 'lifterlms' ), + 'student.deleted' => __( 'Student deleted', 'lifterlms' ), + 'enrollment.created' => __( 'Enrollment created', 'lifterlms' ), + 'enrollment.updated' => __( 'Enrollment updated', 'lifterlms' ), + 'enrollment.deleted' => __( 'Enrollment deleted', 'lifterlms' ), + 'progress.updated' => __( 'Progress updated', 'lifterlms' ), + 'progress.deleted' => __( 'Progress deleted', 'lifterlms' ), + 'instructor.created' => __( 'Instructor created', 'lifterlms' ), + 'instructor.updated' => __( 'Instructor updated', 'lifterlms' ), + 'instructor.deleted' => __( 'Instructor deleted', 'lifterlms' ), + 'quiz-attempt.updated' => __( 'Quiz Attempt completed', 'lifterlms' ), + 'awarded-certificate.updated' => __( 'Awarded Certificate updated', 'lifterlms' ), + 'action' => __( 'Action', 'lifterlms' ), ) ); - } /** @@ -255,145 +253,152 @@ public function get_hooks() { $hooks = array( // Courses. - 'course.created' => array( + 'course.created' => array( 'save_post_course' => 2, ), - 'course.updated' => array( + 'course.updated' => array( 'edit_post_course' => 2, ), - 'course.deleted' => array( + 'course.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), - 'course.restored' => array( + 'course.restored' => array( 'untrashed_post' => 1, ), // Sections. - 'section.created' => array( + 'section.created' => array( 'save_post_section' => 2, ), - 'section.updated' => array( + 'section.updated' => array( 'edit_post_section' => 2, ), - 'section.deleted' => array( + 'section.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), // Lessons. - 'lesson.created' => array( + 'lesson.created' => array( 'save_post_lesson' => 2, ), - 'lesson.updated' => array( + 'lesson.updated' => array( 'edit_post_lesson' => 2, ), - 'lesson.deleted' => array( + 'lesson.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), - 'lesson.restored' => array( + 'lesson.restored' => array( 'untrashed_post' => 1, ), // Memberships. - 'membership.created' => array( + 'membership.created' => array( 'save_post_llms_membership' => 2, ), - 'membership.updated' => array( + 'membership.updated' => array( 'edit_post_llms_membership' => 2, ), - 'membership.deleted' => array( + 'membership.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), - 'membership.restored' => array( + 'membership.restored' => array( 'untrashed_post' => 1, ), // Access Plans. - 'access_plan.created' => array( + 'access_plan.created' => array( 'save_post_llms_access_plan' => 2, ), - 'access_plan.updated' => array( + 'access_plan.updated' => array( 'edit_post_llms_access_plan' => 2, ), - 'access_plan.deleted' => array( + 'access_plan.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), // Orders. - 'order.created' => array( + 'order.created' => array( 'save_post_llms_order' => 2, ), - 'order.updated' => array( + 'order.updated' => array( 'edit_post_llms_order' => 2, ), - 'order.deleted' => array( + 'order.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), // Transactions. - 'transaction.created' => array( + 'transaction.created' => array( 'save_post_llms_transaction' => 2, ), - 'transaction.updated' => array( + 'transaction.updated' => array( 'edit_post_llms_transaction' => 2, ), - 'transaction.deleted' => array( + 'transaction.deleted' => array( 'wp_trash_post' => 1, 'delete_post' => 1, ), // Students. - 'student.created' => array( + 'student.created' => array( 'user_register' => 1, 'lifterlms_user_registered' => 1, ), - 'student.updated' => array( + 'student.updated' => array( 'profile_update' => 1, 'lifterlms_user_updated' => 1, ), - 'student.deleted' => array( + 'student.deleted' => array( 'delete_user' => 1, ), // Instructors. - 'instructor.created' => array( + 'instructor.created' => array( 'user_register' => 1, ), - 'instructor.updated' => array( + 'instructor.updated' => array( 'profile_update' => 1, ), - 'instructor.deleted' => array( + 'instructor.deleted' => array( 'delete_user' => 1, ), - 'enrollment.created' => array( + 'enrollment.created' => array( 'llms_user_course_enrollment_created' => 2, 'llms_user_membership_enrollment_created' => 2, ), - 'enrollment.updated' => array( + 'enrollment.updated' => array( 'llms_user_course_enrollment_updated' => 2, 'llms_user_membership_enrollment_updated' => 2, 'llms_user_removed_from_course' => 2, 'llms_user_removed_from_membership' => 2, ), - 'enrollment.deleted' => array( + 'enrollment.deleted' => array( 'llms_user_enrollment_deleted' => 2, ), - 'progress.updated' => array( + 'progress.updated' => array( 'llms_mark_complete' => 2, 'llms_mark_incomplete' => 2, ), + 'quiz-attempt.updated' => array( + 'lifterlms_quiz_completed' => 3, + ), + + 'awarded-certificate.updated' => array( + 'edit_post_llms_my_certificate' => 2, + ), + ); return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks ); - } /** @@ -422,7 +427,6 @@ public function get_post_type_resources() { 'llms_transaction', ) ); - } /** @@ -457,7 +461,6 @@ protected function is_data_valid( $data ) { } return true; - } /** @@ -479,7 +482,6 @@ public function is_topic_valid( $topic ) { } return false; - } /** @@ -513,11 +515,10 @@ public function load() { $loaded = 0; foreach ( $hooks->get_webhooks() as $hook ) { $hook->enqueue(); - $loaded++; + ++$loaded; } return $loaded; - } /** @@ -543,7 +544,6 @@ protected function save( $obj, $data ) { $obj->setup( $data )->save(); return $obj; - } /** @@ -580,7 +580,5 @@ protected function update_prepare( $data ) { $data['updated'] = llms_current_time( 'mysql' ); return $data; - } - } diff --git a/includes/llms-rest-functions.php b/includes/llms-rest-functions.php index 37bda9f2..74213d80 100644 --- a/includes/llms-rest-functions.php +++ b/includes/llms-rest-functions.php @@ -42,7 +42,6 @@ function llms_rest_deliver_webhook_async( $webhook_id, $args ) { if ( $webhook ) { $webhook->deliver( $args ); } - } add_action( 'lifterlms_rest_deliver_webhook_async', 'llms_rest_deliver_webhook_async', 10, 2 ); @@ -64,10 +63,9 @@ function llms_rest_get_api_endpoint_data( $endpoint, $params = array() ) { $res = rest_do_request( $req ); $server = rest_get_server(); - $json = wp_json_encode( $server->response_to_data( $res, false ) ); + $json = wp_json_encode( $server->response_to_data( $res, true ) ); return json_decode( $json, true ); - } /** diff --git a/includes/server/class-llms-rest-awarded-certificates-controller.php b/includes/server/class-llms-rest-awarded-certificates-controller.php new file mode 100644 index 00000000..e40ece53 --- /dev/null +++ b/includes/server/class-llms-rest-awarded-certificates-controller.php @@ -0,0 +1,191 @@ + __( 'The ID of the certificate template.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ); + + $schema['properties']['student_id'] = array( + 'description' => __( 'The student ID the certificate was awarded to.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ); + + // Update defaults. + $schema['properties']['content']['required'] = false; + + // Remove unnecessary props. + $remove = array( + 'comment_status', + 'password', + 'ping_status', + 'post_type', + 'featured_media', + ); + foreach ( $remove as $prop ) { + unset( $schema['properties'][ $prop ] ); + } + + return $schema; + } + + public function register_routes() { + + // Only registering read-only routes for this controller. + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + + protected function get_object( $id_or_object ) { + return new LLMS_User_Certificate( $id_or_object ); + } + + /** + * Prepare a single object output for response. + * + * @since [version] + * + * @param LLMS_User_Certificate $certificate Certificate object. + * @param WP_REST_Request $request Full details about the request. + * + * @return array + */ + protected function prepare_object_for_response( $certificate, $request ) { + + $data = parent::prepare_object_for_response( $certificate, $request ); + + $data['certificate_id'] = $certificate->get( 'parent' ); + $data['student_id'] = $certificate->get( 'author' ); + + /** + * Filters the assignment data for a response. + * + * @param array $data Array of assignment properties prepared for response. + * @param LLMS_Assignment $certificate Assignment object. + * @param WP_REST_Request $request Full details about the request. + * + *@since [version] + */ + return apply_filters( 'llms_rest_prepare_assignment_object_response', $data, $certificate, $request ); + } + + /** + * Prepare links for the request. + * + * @since [version] + * + * @param LLMS_User_Certificate $certificate Certificate oblect. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $certificate, $request ) { + + $links = parent::prepare_links( $certificate, $request ); + + $links['student'] = array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'students', $certificate->get( 'author' ) ) + ), + 'embeddable' => true, + ); + + unset( $links['content'] ); + + return $links; + } +} diff --git a/includes/server/class-llms-rest-certificates-controller.php b/includes/server/class-llms-rest-certificates-controller.php new file mode 100644 index 00000000..bd787786 --- /dev/null +++ b/includes/server/class-llms-rest-certificates-controller.php @@ -0,0 +1,182 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Prepare a single object output for response. + * + * @param $certificate Certificate object. + * @param WP_REST_Request $request Full details about the request. + * + * @return array + * @since [version] + */ + protected function prepare_object_for_response( $certificate, $request ) { + + $data = parent::prepare_object_for_response( $certificate, $request ); + + /** + * Filters the assignment data for a response. + * + * @param array $data Array of assignment properties prepared for response. + * @param LLMS_Assignment $certificate Assignment object. + * @param WP_REST_Request $request Full details about the request. + * + *@since [version] + */ + return apply_filters( 'llms_rest_prepare_certificate_object_response', $data, $certificate, $request ); + } + + protected function get_object( $id ) { + return get_post( $id, OBJECT_K ); + } + + protected function check_read_permission( $object ) { + if ( current_user_can( 'edit_post', $object->ID ) ) { + return true; + } + + return false; + } + + protected function prepare_object_data_for_response( $object, $request ) { + + return array( + 'id' => $object->ID, + 'title' => $object->post_title, + ); + } + + /** + * Prepare links for the request. + * + * @since [version] + * + * @param LLMS_Assignment $assignment Assignment oblect. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $certificate, $request ) { + + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $certificate->ID ) ), + ), + ); + + return $links; + } +} diff --git a/includes/server/class-llms-rest-courses-controller.php b/includes/server/class-llms-rest-courses-controller.php index 3936240e..83842cb7 100644 --- a/includes/server/class-llms-rest-courses-controller.php +++ b/includes/server/class-llms-rest-courses-controller.php @@ -110,7 +110,6 @@ public function __construct() { $this->sections_controller = new LLMS_REST_Sections_Controller( '' ); $this->sections_controller->set_collection_params( $this->get_course_content_collection_params() ); - } /** @@ -163,7 +162,6 @@ public function register_routes() { 'schema' => array( $this->sections_controller, 'get_public_item_schema' ), ) ); - } /** @@ -178,7 +176,6 @@ protected function get_object_id( $object ) { // For example. return $object->get( 'id' ); - } /** @@ -192,6 +189,8 @@ protected function get_item_schema_base() { $schema = (array) parent::get_item_schema_base(); + $schema['properties']['title']['context'][] = 'embed'; + $course_properties = array( 'catalog_visibility' => array( 'description' => __( 'Visibility of the course in catalogs and search results.', 'lifterlms' ), @@ -539,7 +538,6 @@ protected function get_item_schema_base() { $schema['properties'] = array_merge( (array) $schema['properties'], $course_properties ); return $schema; - } /** @@ -682,7 +680,6 @@ protected function prepare_object_for_response( $course, $request ) { * @param WP_REST_Request $request Full details about the request. */ return apply_filters( 'llms_rest_prepare_course_object_response', $data, $course, $request ); - } /** @@ -786,7 +783,6 @@ protected function prepare_item_for_database( $request ) { * @param array $schema The item schema. */ return apply_filters( 'llms_rest_pre_insert_course', $prepared_item, $request, $schema ); - } /** @@ -993,7 +989,6 @@ protected function update_additional_object_fields( $course, $request, $schema, } return ! empty( $to_set ); - } /** @@ -1016,7 +1011,6 @@ protected function get_taxonomy_rest_base( $taxonomy ) { ); return isset( $taxonomy_base_map[ $base ] ) ? $taxonomy_base_map[ $base ] : $base; - } /** @@ -1098,7 +1092,6 @@ protected function get_filters_to_be_removed_for_response( $course ) { * @param LLMS_Course $course Course object. */ return apply_filters( 'llms_rest_course_filters_removed_for_response', $filters, $course ); - } /** @@ -1225,7 +1218,6 @@ public function get_course_content_collection_params() { unset( $query_params['parent'] ); return $query_params; - } /** @@ -1247,7 +1239,5 @@ public function get_course_content_items( $request ) { } return $result; - } - } diff --git a/includes/server/class-llms-rest-enrollments-controller.php b/includes/server/class-llms-rest-enrollments-controller.php index d48933a0..318a5333 100644 --- a/includes/server/class-llms-rest-enrollments-controller.php +++ b/includes/server/class-llms-rest-enrollments-controller.php @@ -100,7 +100,6 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE } return $args; - } /** @@ -166,7 +165,6 @@ public function register_routes() { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - } /** @@ -191,7 +189,6 @@ public function get_items_permissions_check( $request ) { } return true; - } /** @@ -213,7 +210,6 @@ public function get_items( $request ) { } return $response; - } /** @@ -261,7 +257,6 @@ public function get_item( $request ) { $response = $this->prepare_item_for_response( $object, $request ); return $response; - } /** @@ -352,7 +347,6 @@ public function create_item( $request ) { ); return $response; - } /** @@ -376,7 +370,6 @@ public function update_item_permissions_check( $request ) { } return true; - } /** @@ -455,7 +448,6 @@ public function update_item( $request ) { $response = $this->prepare_item_for_response( $enrollment, $request ); return $response; - } /** @@ -492,7 +484,6 @@ public function delete_item_permissions_check( $request ) { } return true; - } /** @@ -528,7 +519,6 @@ public function delete_item( $request ) { } return rest_ensure_response( $response ); - } /** @@ -563,7 +553,6 @@ protected function enrollment_exists( $student_id, $post_id, $trigger = 'any', $ } return true; - } /** @@ -616,7 +605,6 @@ protected function prepare_object_query_args( $student_id, $post_id ) { $args = $this->prepare_items_query( $args ); return $args; - } /** @@ -741,7 +729,6 @@ protected function get_item_schema_base() { '[version]', "llms_rest_{$this->get_object_type( $schema )}_item_schema" ); - } /** @@ -755,7 +742,6 @@ protected function get_item_schema_base() { protected function get_objects_from_query( $query ) { return $query->items; - } /** @@ -809,7 +795,6 @@ protected function get_pagination_data_from_query( $query, $prepared, $request ) $total_pages = (int) ceil( $total_results / (int) $prepared['per_page'] ); return compact( 'current_page', 'total_results', 'total_pages' ); - } /** @@ -833,7 +818,6 @@ protected function prepare_collection_query_args( $request ) { $prepared['page'] = ! isset( $prepared['page'] ) ? 1 : $prepared['page']; return $this->prepare_items_query( $prepared, $request ); - } /** @@ -879,7 +863,6 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul $query_args['is_students_route'] = $request ? false !== stristr( $request->get_route(), '/students/' ) : true; return $query_args; - } /** @@ -1026,7 +1009,6 @@ protected function get_objects_query( $query_args, $request = null ) { $query->found_results = empty( $query_args['no_found_rows'] ) ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : $count; // no-cache ok. return $query; - } /** @@ -1091,28 +1073,31 @@ public function prepare_links( $enrollment, $request ) { ), ), 'student' => array( - 'href' => rest_url( + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'students', $enrollment->student_id ) ), + 'embeddable' => true, ), ); switch ( get_post_type( $enrollment->post_id ) ) : case 'course': $links['post'] = array( - 'type' => 'course', - 'href' => rest_url( + 'type' => 'course', + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $enrollment->post_id ) ), + 'embeddable' => true, ); break; case 'llms_membership': $links['post'] = array( - 'type' => 'llms_membership', - 'href' => rest_url( + 'type' => 'llms_membership', + 'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'memberships', $enrollment->post_id ) ), + 'embeddable' => true, ); break; endswitch; @@ -1126,7 +1111,6 @@ public function prepare_links( $enrollment, $request ) { * @param stdClass $enrollment Enrollment object. */ return apply_filters( 'llms_rest_enrollment_links', $links, $enrollment ); - } /** @@ -1156,7 +1140,6 @@ protected function handle_status_update( $student, $post_id, $status, $trigger ) endswitch; return $updated; - } @@ -1262,7 +1245,5 @@ protected function check_read_permission( $enrollment ) { } return current_user_can( 'view_students', $enrollment->student_id ); - } - } diff --git a/includes/server/class-llms-rest-lessons-controller.php b/includes/server/class-llms-rest-lessons-controller.php index 5b37ff09..2b120b73 100644 --- a/includes/server/class-llms-rest-lessons-controller.php +++ b/includes/server/class-llms-rest-lessons-controller.php @@ -322,7 +322,7 @@ protected function get_item_schema_base() { 'parent_id' => array( 'description' => __( 'WordPress post ID of the parent item. Must be a Section ID. 0 indicates an "orphaned" lesson which can be edited and viewed by instructors and admins but cannot be read by students.', 'lifterlms' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'absint', ), @@ -330,7 +330,7 @@ protected function get_item_schema_base() { 'course_id' => array( 'description' => __( 'WordPress post ID of the lesson\'s parent course.', 'lifterlms' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'absint', ), @@ -375,7 +375,7 @@ protected function get_item_schema_base() { 'video_embed' => array( 'description' => __( 'URL to an oEmbed enable video URL.', 'lifterlms' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'format' => 'uri', 'arg_options' => array( 'sanitize_callback' => 'esc_url_raw', diff --git a/includes/server/class-llms-rest-orders-controller.php b/includes/server/class-llms-rest-orders-controller.php new file mode 100644 index 00000000..38ab365c --- /dev/null +++ b/includes/server/class-llms-rest-orders-controller.php @@ -0,0 +1,197 @@ + rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'students', $object->get( 'user_id' ) ) + ), + 'embeddable' => true, + ); + + $product = $object->get_product(); + if ( ! $product ) { + return $links; + } + + switch ( $product->get( 'type' ) ) { + case 'course': + $links['product'] = array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $object->get( 'product_id' ) ) + ), + 'embeddable' => true, + ); + break; + case 'llms_membership': + $links['product'] = array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'memberships', $object->get( 'product_id' ) ) + ), + 'embeddable' => true, + ); + break; + } + + return $links; + } + + /** + * Prepare a single object output for response. + * + * @since [version] + * + * @param LLMS_Order $order Lesson object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_object_for_response( $order, $request ) { + + $data = parent::prepare_object_for_response( $order, $request ); + + $data['status'] = str_replace( 'llms-', '', $data['status'] ); + $data['billing_email'] = $order->get( 'billing_email' ); + $data['payment_gateway'] = $order->get( 'payment_gateway' ); + $data['coupon_amount'] = $order->get( 'coupon_amount' ); + $data['original_total'] = $order->get( 'original_total' ); + $data['product_id'] = $order->get( 'product_id' ); + $data['sale_value'] = $order->get( 'sale_value' ); + + return $data; + } + + /** + * Get the order's schema, conforming to JSON Schema. + * + * @since [version] + * + * @return array Item schema data. + */ + protected function get_item_schema_base() { + + $schema = (array) parent::get_item_schema_base(); + + $order_properties = array( + 'billing_email' => array( + 'description' => __( 'Billing email address for the order.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'payment_gateway' => array( + 'description' => __( 'Payment gateway used for the order.', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'coupon_amount' => array( + 'description' => __( 'Total amount of any coupons applied to the order.', 'lifterlms' ), + 'type' => 'number', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'original_total' => array( + 'description' => __( 'Original total amount of the order before any discounts or coupons.', 'lifterlms' ), + 'type' => 'number', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'product_id' => array( + 'description' => __( 'ID of the product associated with the order.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'sale_value' => array( + 'description' => __( 'Sale value of the order.', 'lifterlms' ), + 'type' => 'number', + 'context' => array( 'view', 'edit', 'embed' ), + ), + ); + + $schema['properties'] = array_merge( (array) $schema['properties'], $order_properties ); + + return $schema; + } + + /** + * Register routes. + * + * @since [version] + * + * @return void + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + function check_read_permission( $object ) { + return current_user_can( 'manage_lifterlms' ); + } +} diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php new file mode 100644 index 00000000..dc32278b --- /dev/null +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -0,0 +1,586 @@ +collection_params = $this->build_collection_params(); + } + + /** + * Register routes. + * + * @since [version] + * + * @return void + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'post_id' => array( + 'description' => __( 'Unique quiz attempt ID.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $this->get_get_item_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check if a given request has access to read items. + * + * @since [version] + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + + if ( ! $this->check_read_permission( $request ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + } + + /** + * Get a collection of quiz attempts. + * + * @since [version] + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + + $response = parent::get_items( $request ); + // Specs require 404 when no quiz attempts are found. + if ( ! is_wp_error( $response ) && empty( $response->data ) ) { + return llms_rest_not_found_error(); + } + + return $response; + } + + /** + * Check if a given request has access to read an item. + * + * @since [version] + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + + if ( ! $this->check_read_permission( $request ) ) { + return llms_rest_authorization_required_error(); + } + + return true; + } + + /** + * Get a single item. + * + * @since [version] + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + + $object = $this->get_object( (int) $request['id'] ); + if ( is_wp_error( $object ) ) { + return $object; + } + + $response = $this->prepare_item_for_response( $object, $request ); + + return $response; + } + + /** + * Get object. + * + * @since [version] + * + * @param int $attempt_id Quiz attempt ID. + * @return object|WP_Error + */ + protected function get_object( $attempt_id ) { + + if ( empty( $attempt_id ) ) { + return llms_rest_bad_request_error(); + } + + $query_args = $this->prepare_object_query_args( $attempt_id ); + $query = $this->get_objects_query( $query_args ); + $items = $this->get_objects_from_query( $query ); + + if ( $items ) { + return $items[0]; + } + + return llms_rest_not_found_error(); + } + + /** + * Prepare quiz attempts objects query. + * + * @since [version] + * + * @param int $attempt_id Attempt ID. + * @return array + */ + protected function prepare_object_query_args( $attempt_id ) { + + $args = array(); + + $args['id'] = $attempt_id; + $args['no_found_rows'] = true; + $args['per_page'] = 1; + + $args = $this->prepare_items_query( $args ); + + return $args; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since [version] + * + * @return array The quiz attempt collection parameters. + */ + public function get_collection_params() { + return $this->collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since [version] + * + * @param array $collection_params The quiz attempt collection parameters to be set. + * @return void + */ + public function set_collection_params( $collection_params ) { + $this->collection_params = $collection_params; + } + + /** + * Build the query params for the objects collection. + * + * @since [version] + * + * @return array Collection parameters. + */ + protected function build_collection_params() { + + $query_params = parent::get_collection_params(); + + unset( $query_params['include'], $query_params['exclude'] ); + + $query_params['status'] = array( + 'description' => __( 'Filter results to records matching the specified status.', 'lifterlms' ), + 'enum' => array_keys( llms_get_quiz_attempt_statuses() ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + $query_params['post'] = array( + 'description' => __( 'Limit results to a specific lesson or a list of lessons. Accepts a single post id or a comma separated list of post ids.', 'lifterlms' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $query_params; + } + + /** + * Get the Quiz Attempt's schema, conforming to JSON Schema. + * + * @since [version] + * + * @return array + */ + protected function get_item_schema_base() { + + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'quiz-attempts', + 'type' => 'object', + 'properties' => array( + 'student_id' => array( + 'description' => __( 'The ID of the student.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'quiz_id' => array( + 'description' => __( 'The ID of the quiz.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'lesson_id' => array( + 'description' => __( 'The ID of the lesson.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'start_date' => array( + 'description' => __( 'Start date. Format: `Y-m-d H:i:s`', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'update_date' => array( + 'description' => __( 'Date last modified. Format: `Y-m-d H:i:s`', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'end_date' => array( + 'description' => __( 'End date. Format: `Y-m-d H:i:s`', 'lifterlms' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'status' => array( + 'description' => __( 'The status of the quiz attempt.', 'lifterlms' ), + 'enum' => array_keys( llms_get_quiz_attempt_statuses() ), + 'context' => array( 'view', 'edit', 'embed' ), + 'type' => 'string', + ), + 'attempt' => array( + 'description' => __( 'The attempt number of the quiz.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'grade' => array( + 'description' => __( 'The grade of the quiz attempt.', 'lifterlms' ), + 'type' => 'number', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'can_be_resumed' => array( + 'description' => __( 'Whether the quiz attempt can be resumed.', 'lifterlms' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ); + } + + /** + * Retrieve an array of objects from the result of $this->get_objects_query(). + * + * @since [version] + * + * @param WP_Query $query Query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + return $query->get_attempts(); + } + + /** + * Prepare collection items for response. + * + * @since [version] + * + * @param array $objects Array of objects to be prepared for response. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + + foreach ( $objects as $object ) { + + if ( ! $this->check_read_permission( $object ) ) { + continue; + } + + $item = $this->prepare_item_for_response( $object, $request ); + if ( ! is_wp_error( $item ) ) { + $items[] = $this->prepare_response_for_collection( $item ); + } + } + + return $items; + } + + /** + * Retrieve pagination information from an objects query. + * + * @since [version] + * + * @param stdClass $query Objects query result returned by {@see LLMS_REST_Quiz_Attempts_Controller::get_objects_query()}. + * @param array $prepared Array of collection arguments. + * @param WP_REST_Request $request Request object. + * @return array { + * Array of pagination information. + * + * @type int $current_page Current page number. + * @type int $total_results Total number of results. + * @type int $total_pages Total number of results pages. + * } + */ + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + + $total_results = (int) $query->found_results; + $current_page = isset( $prepared['page'] ) ? (int) $prepared['page'] : 1; + $total_pages = (int) ceil( $total_results / (int) $prepared['per_page'] ); + + return compact( 'current_page', 'total_results', 'total_pages' ); + } + + /** + * Prepare quiz attempts objects query + * + * @since [version] + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + protected function prepare_collection_query_args( $request ) { + + $prepared = parent::prepare_collection_query_args( $request ); + if ( is_wp_error( $prepared ) ) { + return $prepared; + } + + $prepared['id'] = $request['id']; + $prepared['page'] = ! isset( $prepared['page'] ) ? 1 : $prepared['page']; + + return $this->prepare_items_query( $prepared, $request ); + } + + /** + * Determines the allowed query_vars for a get_items() response and prepares + * them for WP_Query. + * + * @since [version] + * + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param WP_REST_Request $request Optional. Full details about the request. + * @return array Items query arguments. + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $query_args = array(); + + foreach ( $prepared_args as $key => $value ) { + $query_args[ $key ] = $value; + } + + // Filters. + if ( isset( $query_args['student'] ) && ! is_array( $query_args['student'] ) ) { + $query_args['student'] = array_map( 'absint', explode( ',', $query_args['student'] ) ); + } + if ( isset( $query_args['post'] ) && ! is_array( $query_args['post'] ) ) { + $query_args['post'] = array_map( 'absint', explode( ',', $query_args['post'] ) ); + } + + // $query_args['is_students_route'] = $request ? false !== stristr( $request->get_route(), '/students/' ) : true; + + return $query_args; + } + + /** + * Get quiz attempts query. + * + * @since [version] + * + * @param array $query_args Array of collection arguments. + * @param WP_REST_Request $request Optional. Full details about the request. Default null. + * @return LLMS_Query_Quiz_Attempt + */ + protected function get_objects_query( $query_args, $request = null ) { + + $args = array(); + if ( isset( $query_args['orderby'], $query_args['order'] ) ) { + $args['sort'] = array( + $query_args['orderby'] => $query_args['order'], + ); + } + + if ( isset( $query_args['status'] ) ) { + $args['status'] = $query_args['status']; + } + + return new LLMS_Query_Quiz_Attempt( $args ); + } + + /** + * Prepare a single object output for response. + * + * @since [version] + * + * @param LLMS_Quiz_Attempt $attempt Attempt object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + public function prepare_object_for_response( $attempt, $request ) { + + // Filter data including only schema props. + $prepared_quiz_attempt = array( + 'student_id' => (int) $attempt->get( 'student_id' ), + 'quiz_id' => (int) $attempt->get( 'quiz_id' ), + 'lesson_id' => (int) $attempt->get( 'lesson_id' ), + 'start_date' => $attempt->get( 'start_date' ), + 'update_date' => $attempt->get( 'update_date' ), + 'end_date' => $attempt->get( 'end_date' ), + 'attempt' => (int) $attempt->get( 'attempt' ), + 'status' => $attempt->get( 'status' ), + 'grade' => (float) $attempt->get( 'grade' ), + 'can_be_resumed' => (bool) $attempt->get( 'can_be_resumed' ), + ); + + $data = array_intersect_key( $prepared_quiz_attempt, array_flip( $this->get_fields_for_response( $request ) ) ); + + /** + * Filters the quiz attempt data for a response. + * + * @since [version] + * + * @param array $data Array of quiz attempt properties prepared for response. + * @param stdClass $attempt Attempt object. + * @param WP_REST_Request $request Full details about the request. + */ + return apply_filters( 'llms_rest_prepare_quiz_attempt_object_response', $data, $attempt, $request ); + } + + /** + * Prepare quiz attempt links for the request. + * + * @since [version] + * + * @param LLMS_Quiz_Attempt $attempt Attempt object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + public function prepare_links( $attempt, $request ) { + + $links = array( + 'self' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'quiz-attempts', $attempt->get( 'id' ) ) + ), + ), + 'collection' => array( + 'href' => rest_url( + sprintf( '/%s/%s', 'llms/v1', 'quiz-attempts' ) + ), + ), + 'student' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'students', $attempt->get( 'student_id' ) ) + ), + 'embeddable' => true, + ), + 'quiz' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'quizzes', $attempt->get( 'quiz_id' ) ) + ), + 'embeddable' => true, + ), + 'lesson' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'lessons', $attempt->get( 'lesson_id' ) ) + ), + 'embeddable' => true, + ), + ); + + /** + * Filters the quiz attempt's links. + * + * @since [version] + * + * @param array $links Links for the given quiz attempt. + * @param stdClass $attempt Attempt object. + */ + return apply_filters( 'llms_rest_quiz_attempt_links', $links, $attempt ); + } + + /** + * Checks if a quiz attempt can be read. + * + * @since [version] + * + * @param WP_REST_Request $request The request array. + * @return bool Whether the quiz attempt can be read. + */ + protected function check_read_permission( $request ) { + + return current_user_can( 'manage_lifterlms' ); + } +} diff --git a/includes/server/class-llms-rest-quizzes-controller.php b/includes/server/class-llms-rest-quizzes-controller.php new file mode 100644 index 00000000..ff49eb3e --- /dev/null +++ b/includes/server/class-llms-rest-quizzes-controller.php @@ -0,0 +1,52 @@ +content_controller = new $this->content_controller_class(); $this->content_controller->set_collection_params( $this->get_content_collection_params() ); } - } /** @@ -225,7 +224,6 @@ protected function prepare_item_for_database( $request ) { } return $prepared_item; - } /** @@ -241,12 +239,13 @@ public function get_item_schema_base() { // Section's title. $schema['properties']['title']['description'] = __( 'Section Title', 'lifterlms' ); + $schema['properties']['title']['context'][] = 'embed'; // Section's parent id. $schema['properties']['parent_id'] = array( 'description' => __( 'WordPress post ID of the parent item. Must be a Course ID.', 'lifterlms' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => 'absint', ), @@ -285,7 +284,6 @@ public function get_item_schema_base() { } return $schema; - } /** @@ -352,7 +350,6 @@ protected function prepare_object_for_response( $section, $request ) { $data['order'] = $section->get( 'order' ); return $data; - } /** @@ -490,7 +487,6 @@ protected function check_read_permission( $section ) { } return parent::check_read_permission( $section ); - } /** @@ -525,7 +521,6 @@ public function get_content_collection_params() { unset( $query_params['parent'] ); return $query_params; - } /** @@ -547,7 +542,5 @@ public function get_content_items( $request ) { } return $result; - } - } diff --git a/includes/server/class-llms-rest-students-controller.php b/includes/server/class-llms-rest-students-controller.php index 316d2dc8..4b0ad890 100644 --- a/includes/server/class-llms-rest-students-controller.php +++ b/includes/server/class-llms-rest-students-controller.php @@ -133,8 +133,13 @@ public function get_collection_params() { */ public function get_item_schema_base() { - $schema = parent::get_item_schema_base(); - $schema['properties']['roles']['default'] = array( 'student' ); + $schema = parent::get_item_schema_base(); + $schema['properties']['roles']['default'] = array( 'student' ); + $schema['properties']['id']['context'][] = 'embed'; + $schema['properties']['email']['context'][] = 'embed'; + $schema['properties']['name']['context'][] = 'embed'; + $schema['properties']['first_name']['context'][] = 'embed'; + $schema['properties']['last_name']['context'][] = 'embed'; return $schema; } @@ -369,7 +374,8 @@ protected function prepare_links( $object, $request ) { 'href' => sprintf( '%s/enrollments', $links['self']['href'] ), ); $links['progress'] = array( - 'href' => sprintf( '%s/progress', $links['self']['href'] ), + 'href' => sprintf( '%s/progress', $links['self']['href'] ), + 'embeddable' => true, ); return $links; diff --git a/includes/server/class-llms-rest-students-progress-controller.php b/includes/server/class-llms-rest-students-progress-controller.php index 8a48c094..d050cac4 100644 --- a/includes/server/class-llms-rest-students-progress-controller.php +++ b/includes/server/class-llms-rest-students-progress-controller.php @@ -24,7 +24,7 @@ class LLMS_REST_Students_Progress_Controller extends LLMS_REST_Controller { * * @var string */ - protected $rest_base = 'students/(?P[\d]+)/progress/(?P[\d]+)'; + protected $rest_base = 'students/(?P[\d]+)/progress'; /** * Schema properties available for ordering the collection. @@ -32,9 +32,7 @@ class LLMS_REST_Students_Progress_Controller extends LLMS_REST_Controller { * @var string[] */ protected $orderby_properties = array( - 'date_created', - 'date_updated', - 'progress', + 'updated_date', ); /** @@ -53,12 +51,19 @@ protected function check_read_item_permissions( $request ) { } // Must be able to edit post and student to view other's progress. - if ( current_user_can( 'edit_post', $request['post_id'] ) && current_user_can( 'edit_students', $request['id'] ) ) { + if ( ! current_user_can( 'edit_students', $request['id'] ) ) { + return false; + } + + if ( ! $request['post_id'] && current_user_can( 'edit_posts' ) ) { return true; } - return false; + if ( current_user_can( 'edit_post', $request['post_id'] ) ) { + return true; + } + return false; } /** @@ -76,7 +81,6 @@ public function delete_item_permissions_check( $request ) { } return true; - } /** @@ -116,7 +120,6 @@ protected function delete_object( $object, $request ) { } return true; - } /** @@ -162,7 +165,6 @@ protected function get_date( $student, $post, $order ) { } return null; - } /** @@ -179,7 +181,32 @@ public function get_item( $request ) { $response = $this->prepare_item_for_response( $object, $request ); return rest_ensure_response( $response ); + } + + /** + * Determine if current user has permission to list all progress for a student. + * + * @since [version]] + * + * @param WP_REST_Request $request Request object. + * @return true|WP_Error + */ + public function get_items_permissions_check( $request ) { + + if ( get_current_user_id() === $request['id'] ) { + return true; + } + + if ( ! current_user_can( 'edit_posts' ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to view all progress.', 'lifterlms' ) ); + } + + // Must be able to edit post and student to view other's progress. + if ( ! current_user_can( 'edit_students', $request['id'] ) ) { + return llms_rest_authorization_required_error( __( 'You are not allowed to view progress for this student.', 'lifterlms' ) ); + } + return true; } /** @@ -216,14 +243,16 @@ protected function get_item_schema_base() { 'student_id' => array( 'description' => __( 'The ID of the student.', 'lifterlms' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, + 'embeddable' => true, ), 'post_id' => array( 'description' => __( 'The ID of the course/membership.', 'lifterlms' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, + 'embeddable' => true, ), 'date_created' => array( 'description' => __( 'Creation date. Format: Y-m-d H:i:s', 'lifterlms' ), @@ -257,7 +286,6 @@ protected function get_item_schema_base() { ), ), ); - } /** @@ -307,7 +335,75 @@ protected function get_object( $ids ) { $obj->date_created = $this->get_date( $student, $post, 'ASC' ); return $obj; + } + + protected function get_pagination_data_from_query( $query, $prepared, $request ) { + global $wpdb; + + $total_results = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ); + $current_page = isset( $prepared['paged'] ) ? (int) $prepared['paged'] : 1; + $total_pages = absint( (int) ceil( $total_results / (int) $prepared['per_page'] ) ); + + return compact( 'current_page', 'total_results', 'total_pages' ); + } + + protected function get_objects_from_query( $query ) { + // The query is the array of objects via $wpdb->get_results() in this case. + return $query; + } + + protected function prepare_collection_items_for_response( $objects, $request ) { + + $items = array(); + foreach ( $objects as $obj ) { + $object = $this->get_object( array( $request['id'], $obj->id ) ); + + if ( ! $this->check_read_object_permissions( $object ) ) { + continue; + } + + $item = $this->prepare_item_for_response( $object, $request ); + if ( ! is_wp_error( $item ) ) { + $items[] = $this->prepare_response_for_collection( $item ); + } + } + + return $items; + } + + protected function get_objects_query( $prepared, $request ) { + global $wpdb; + + $args = array( + 'per_page' => $prepared['per_page'] ?? 10, + 'page' => $prepared['page'] ?? 1, + ); + + // TODO: Switch to get collection of courses, sections, or lessons for a student. + + return $wpdb->get_results( + $wpdb->prepare( + "SELECT SQL_CALC_FOUND_ROWS DISTINCT upm.post_id AS id + FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm + JOIN {$wpdb->posts} AS p ON p.ID = upm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND upm.meta_key = '_status' + AND upm.user_id = %d + ORDER BY {$prepared['orderby']} {$prepared['order']} + LIMIT %d, %d; + ", + array( + 'course', + $request['id'], + $args['per_page'] * ( $args['page'] - 1 ), + $args['per_page'], + ) + ), + 'OBJECT_K' + ); // db call ok; no-cache ok. + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** @@ -321,7 +417,6 @@ protected function get_object( $ids ) { protected function get_object_id( $object ) { return array( $object->student_id, $object->post_id ); - } @@ -339,7 +434,6 @@ protected function prepare_item_for_database( $request ) { $prepared['id'] = $request['id']; return $prepared; - } /** @@ -370,19 +464,20 @@ protected function prepare_links( $object, $request ) { $links = array( 'self' => array( - 'href' => $base, + 'href' => $base . '/' . $object->post_id, ), 'post' => array( - 'type' => $post_type, - 'href' => rest_url( sprintf( '/%1$s/%2$ss/%3$d', $this->namespace, $post_type, $object->post_id ) ), + 'type' => $post_type, + 'href' => rest_url( sprintf( '/%1$s/%2$ss/%3$d', $this->namespace, $post_type, $object->post_id ) ), + 'embeddable' => true, ), 'student' => array( - 'href' => rest_url( sprintf( '/%1$s/students/%2$d', $this->namespace, $object->student_id ) ), + 'href' => rest_url( sprintf( '/%1$s/students/%2$d', $this->namespace, $object->student_id ) ), + 'embeddable' => true, ), ); return $links; - } /** @@ -397,7 +492,6 @@ protected function prepare_links( $object, $request ) { protected function prepare_object_for_response( $object, $request ) { return (array) $object; - } /** @@ -412,6 +506,32 @@ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base, + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the student. The WP User ID.', 'lifterlms' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', array( 'args' => array( 'id' => array( @@ -445,7 +565,6 @@ public function register_routes() { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - } /** @@ -463,7 +582,6 @@ public function update_item_permissions_check( $request ) { } return true; - } /** @@ -494,7 +612,6 @@ protected function update_object( $prepared, $request ) { } return $this->get_object( array( $prepared['id'], $prepared['post_id'] ) ); - } /** @@ -543,5 +660,4 @@ public function validate_post_id( $value, $request, $param ) { return true; } - }