From 7288ab1d8c1d6c0681814e38350e65ec2d7b6d15 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 17 Jun 2025 17:33:20 -0400 Subject: [PATCH 01/30] WIP: Add embeddable to the post and student. Will likely want to specify additional fields as being available within the "embed" context, and decide if we want to auto embed for all endpoints. Since we only specified embeddable --- .../class-llms-rest-webhook-data.php | 16 ++++------ includes/llms-rest-functions.php | 4 +-- .../class-llms-rest-courses-controller.php | 14 ++------- .../class-llms-rest-lessons-controller.php | 6 ++-- .../class-llms-rest-sections-controller.php | 11 ++----- .../class-llms-rest-students-controller.php | 6 ++-- ...llms-rest-students-progress-controller.php | 29 ++++++------------- 7 files changed, 27 insertions(+), 59 deletions(-) diff --git a/includes/abstracts/class-llms-rest-webhook-data.php b/includes/abstracts/class-llms-rest-webhook-data.php index b064d9c7..338cf24f 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' ) ); - } /** @@ -231,7 +226,12 @@ protected function get_payload( $args ) { $endpoint = sprintf( '/llms/v1/%1$ss/%2$d', $resource, $args[0] ); } - $payload = llms_rest_get_api_endpoint_data( $endpoint ); + $payload = llms_rest_get_api_endpoint_data( + $endpoint, + array( + '_embed' => true, + ) + ); } @@ -250,7 +250,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 +263,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 +315,5 @@ protected function set_delivery_failure() { } return $this; - } - } 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-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-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-sections-controller.php b/includes/server/class-llms-rest-sections-controller.php index e597d9b9..e7550db8 100644 --- a/includes/server/class-llms-rest-sections-controller.php +++ b/includes/server/class-llms-rest-sections-controller.php @@ -92,7 +92,6 @@ public function __construct( $content_controller_class = 'LLMS_REST_Lessons_Cont $this->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..e2362667 100644 --- a/includes/server/class-llms-rest-students-controller.php +++ b/includes/server/class-llms-rest-students-controller.php @@ -133,8 +133,10 @@ 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'; return $schema; } diff --git a/includes/server/class-llms-rest-students-progress-controller.php b/includes/server/class-llms-rest-students-progress-controller.php index 8a48c094..26584937 100644 --- a/includes/server/class-llms-rest-students-progress-controller.php +++ b/includes/server/class-llms-rest-students-progress-controller.php @@ -58,7 +58,6 @@ protected function check_read_item_permissions( $request ) { } return false; - } /** @@ -76,7 +75,6 @@ public function delete_item_permissions_check( $request ) { } return true; - } /** @@ -116,7 +114,6 @@ protected function delete_object( $object, $request ) { } return true; - } /** @@ -162,7 +159,6 @@ protected function get_date( $student, $post, $order ) { } return null; - } /** @@ -179,7 +175,6 @@ public function get_item( $request ) { $response = $this->prepare_item_for_response( $object, $request ); return rest_ensure_response( $response ); - } /** @@ -216,14 +211,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 +254,6 @@ protected function get_item_schema_base() { ), ), ); - } /** @@ -307,7 +303,6 @@ protected function get_object( $ids ) { $obj->date_created = $this->get_date( $student, $post, 'ASC' ); return $obj; - } /** @@ -321,7 +316,6 @@ protected function get_object( $ids ) { protected function get_object_id( $object ) { return array( $object->student_id, $object->post_id ); - } @@ -339,7 +333,6 @@ protected function prepare_item_for_database( $request ) { $prepared['id'] = $request['id']; return $prepared; - } /** @@ -373,16 +366,17 @@ protected function prepare_links( $object, $request ) { 'href' => $base, ), '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 +391,6 @@ protected function prepare_links( $object, $request ) { protected function prepare_object_for_response( $object, $request ) { return (array) $object; - } /** @@ -445,7 +438,6 @@ public function register_routes() { 'schema' => array( $this, 'get_public_item_schema' ), ) ); - } /** @@ -463,7 +455,6 @@ public function update_item_permissions_check( $request ) { } return true; - } /** @@ -494,7 +485,6 @@ protected function update_object( $prepared, $request ) { } return $this->get_object( array( $prepared['id'], $prepared['post_id'] ) ); - } /** @@ -543,5 +533,4 @@ public function validate_post_id( $value, $request, $param ) { return true; } - } From f7a4c05f4ccf808e0d3f4480ec4237417696c1be Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Wed, 18 Jun 2025 08:17:26 -0400 Subject: [PATCH 02/30] Changelog. --- .../feature_embed-linked-models-student-progress-webhook.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelogs/feature_embed-linked-models-student-progress-webhook.yml 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..497f6dfe --- /dev/null +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml @@ -0,0 +1,4 @@ +significance: patch +type: added +entry: Augmented the student progress endpoint to return embedded data (student + and post). From 7d9f3fbfa3a1628fb013176ec51ebf28949bd306 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Wed, 18 Jun 2025 10:55:11 -0400 Subject: [PATCH 03/30] Adding more data to show in webhooks and when _embed query param added to requests. --- ...linked-models-student-progress-webhook.yml | 4 -- .../class-llms-rest-posts-controller.php | 36 ++---------------- ...class-llms-rest-enrollments-controller.php | 38 +++++-------------- .../class-llms-rest-students-controller.php | 3 ++ 4 files changed, 17 insertions(+), 64 deletions(-) delete mode 100644 .changelogs/feature_embed-linked-models-student-progress-webhook.yml diff --git a/.changelogs/feature_embed-linked-models-student-progress-webhook.yml b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml deleted file mode 100644 index 497f6dfe..00000000 --- a/.changelogs/feature_embed-linked-models-student-progress-webhook.yml +++ /dev/null @@ -1,4 +0,0 @@ -significance: patch -type: added -entry: Augmented the student progress endpoint to return embedded data (student - and post). 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/server/class-llms-rest-enrollments-controller.php b/includes/server/class-llms-rest-enrollments-controller.php index d48933a0..05d76e7d 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; - } /** @@ -1086,33 +1068,37 @@ public function prepare_links( $enrollment, $request ) { ), ), 'collection' => array( - 'href' => rest_url( + 'href' => rest_url( sprintf( '/%s/%s/%d/%s', 'llms/v1', 'students', $enrollment->student_id, 'enrollments' ) ), + 'embeddable' => true, ), '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 +1112,6 @@ public function prepare_links( $enrollment, $request ) { * @param stdClass $enrollment Enrollment object. */ return apply_filters( 'llms_rest_enrollment_links', $links, $enrollment ); - } /** @@ -1156,7 +1141,6 @@ protected function handle_status_update( $student, $post_id, $status, $trigger ) endswitch; return $updated; - } @@ -1262,7 +1246,5 @@ protected function check_read_permission( $enrollment ) { } return current_user_can( 'view_students', $enrollment->student_id ); - } - } diff --git a/includes/server/class-llms-rest-students-controller.php b/includes/server/class-llms-rest-students-controller.php index e2362667..7ea93b28 100644 --- a/includes/server/class-llms-rest-students-controller.php +++ b/includes/server/class-llms-rest-students-controller.php @@ -137,6 +137,9 @@ public function 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; } From b49702e61c44d2bf191a9a447d514a44a3a7675b Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Wed, 18 Jun 2025 10:59:53 -0400 Subject: [PATCH 04/30] Removing embed from collections --- includes/server/class-llms-rest-enrollments-controller.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/server/class-llms-rest-enrollments-controller.php b/includes/server/class-llms-rest-enrollments-controller.php index 05d76e7d..318a5333 100644 --- a/includes/server/class-llms-rest-enrollments-controller.php +++ b/includes/server/class-llms-rest-enrollments-controller.php @@ -1068,10 +1068,9 @@ public function prepare_links( $enrollment, $request ) { ), ), 'collection' => array( - 'href' => rest_url( + 'href' => rest_url( sprintf( '/%s/%s/%d/%s', 'llms/v1', 'students', $enrollment->student_id, 'enrollments' ) ), - 'embeddable' => true, ), 'student' => array( 'href' => rest_url( From c1b26e2525e4fc274f4bbe93fd5ebd4b94e1764a Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 20 Jun 2025 13:07:46 -0400 Subject: [PATCH 05/30] Avoid error if core LifterLMS is deactivated. --- class-lifterlms-rest-api.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index 6b6fddae..8bc376cf 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 ); - } /** @@ -185,7 +187,6 @@ public function rest_api_controllers_init() { $controller_instance = new $controller(); $controller_instance->register_routes(); } - } /** @@ -216,7 +217,6 @@ public function init() { add_action( 'init', array( $this->webhooks(), 'load' ), 6 ); add_action( 'deleted_user', array( $this, 'on_user_deletion' ) ); - } /** @@ -278,7 +278,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 +292,4 @@ public function load_textdomain() { public function webhooks() { return LLMS_REST_Webhooks::instance(); } - } From 9cee0548ecbfca9ec0bacd2cbd0e1711fbcdf77e Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 24 Jun 2025 14:33:22 -0400 Subject: [PATCH 06/30] Allow retrieving all progress for a student rather than just a specific course ID. --- .../class-llms-rest-students-controller.php | 3 +- ...llms-rest-students-progress-controller.php | 130 +++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/includes/server/class-llms-rest-students-controller.php b/includes/server/class-llms-rest-students-controller.php index 7ea93b28..4b0ad890 100644 --- a/includes/server/class-llms-rest-students-controller.php +++ b/includes/server/class-llms-rest-students-controller.php @@ -374,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 26584937..1eb7ac1e 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,7 +51,15 @@ 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; + } + + if ( current_user_can( 'edit_post', $request['post_id'] ) ) { return true; } @@ -177,6 +183,32 @@ public function get_item( $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; + } + /** * Check if a given request has access to read an item. * @@ -305,6 +337,68 @@ protected function get_object( $ids ) { 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; + + 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'], + $prepared['per_page'] * ( $prepared['page'] - 1 ), + $prepared['per_page'], + ) + ), + 'OBJECT_K' + ); // db call ok; no-cache ok. + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + /** * Retrieve an ID from the object * @@ -405,6 +499,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( From 9469f55c81aa79cec884c69c56f21149bda3f1c4 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 24 Jun 2025 14:35:48 -0400 Subject: [PATCH 07/30] Fixing warning for no pagination args. --- .../class-llms-rest-students-progress-controller.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/server/class-llms-rest-students-progress-controller.php b/includes/server/class-llms-rest-students-progress-controller.php index 1eb7ac1e..74527475 100644 --- a/includes/server/class-llms-rest-students-progress-controller.php +++ b/includes/server/class-llms-rest-students-progress-controller.php @@ -375,6 +375,11 @@ protected function prepare_collection_items_for_response( $objects, $request ) { protected function get_objects_query( $prepared, $request ) { global $wpdb; + $args = array( + 'per_page' => $prepared['per_page'] ?? 10, + 'page' => $prepared['page'] ?? 1, + ); + return $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS DISTINCT upm.post_id AS id @@ -390,8 +395,8 @@ protected function get_objects_query( $prepared, $request ) { array( 'course', $request['id'], - $prepared['per_page'] * ( $prepared['page'] - 1 ), - $prepared['per_page'], + $args['per_page'] * ( $args['page'] - 1 ), + $args['per_page'], ) ), 'OBJECT_K' From 562a93b923d7f2e9df10db5cf728837046aa9123 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 24 Jun 2025 15:09:37 -0400 Subject: [PATCH 08/30] Fixing individual link. --- .../server/class-llms-rest-students-progress-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/server/class-llms-rest-students-progress-controller.php b/includes/server/class-llms-rest-students-progress-controller.php index 74527475..660ff8f2 100644 --- a/includes/server/class-llms-rest-students-progress-controller.php +++ b/includes/server/class-llms-rest-students-progress-controller.php @@ -462,7 +462,7 @@ protected function prepare_links( $object, $request ) { $links = array( 'self' => array( - 'href' => $base, + 'href' => $base . '/' . $object->post_id, ), 'post' => array( 'type' => $post_type, From cb4c5f1a8c15161517bc796df453966c5945966b Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 24 Jun 2025 15:14:28 -0400 Subject: [PATCH 09/30] Adding note about maybe changing to getting more items to check progress on. --- .../server/class-llms-rest-students-progress-controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/server/class-llms-rest-students-progress-controller.php b/includes/server/class-llms-rest-students-progress-controller.php index 660ff8f2..d050cac4 100644 --- a/includes/server/class-llms-rest-students-progress-controller.php +++ b/includes/server/class-llms-rest-students-progress-controller.php @@ -380,6 +380,8 @@ protected function get_objects_query( $prepared, $request ) { '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 a65132db0bbb5323b090be978b072d5a70734ff4 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Tue, 24 Jun 2025 15:18:20 -0400 Subject: [PATCH 10/30] WIP: Initial quizzes controller. --- class-lifterlms-rest-api.php | 2 + .../class-llms-rest-quizzes-controller.php | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 includes/server/class-llms-rest-quizzes-controller.php diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index 8bc376cf..e8516d70 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -142,6 +142,7 @@ 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-sections-controller', 'server/class-llms-rest-lessons-controller', 'server/class-llms-rest-memberships-controller', @@ -174,6 +175,7 @@ public function rest_api_controllers_init() { 'LLMS_REST_Courses_Controller', 'LLMS_REST_Sections_Controller', 'LLMS_REST_Lessons_Controller', + 'LLMS_REST_Quizzes_Controller', 'LLMS_REST_Memberships_Controller', 'LLMS_REST_Instructors_Controller', 'LLMS_REST_Students_Controller', 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 @@ + Date: Thu, 26 Jun 2025 11:54:16 -0400 Subject: [PATCH 11/30] Quiz attempts controller. --- class-lifterlms-rest-api.php | 2 + ...ass-llms-rest-quiz-attempts-controller.php | 657 ++++++++++++++++++ 2 files changed, 659 insertions(+) create mode 100644 includes/server/class-llms-rest-quiz-attempts-controller.php diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index e8516d70..b7643382 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -143,6 +143,7 @@ public function rest_api_includes() { '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-sections-controller', 'server/class-llms-rest-lessons-controller', 'server/class-llms-rest-memberships-controller', @@ -176,6 +177,7 @@ public function rest_api_controllers_init() { 'LLMS_REST_Sections_Controller', 'LLMS_REST_Lessons_Controller', 'LLMS_REST_Quizzes_Controller', + 'LLMS_REST_Quiz_Attempts_Controller', 'LLMS_REST_Memberships_Controller', 'LLMS_REST_Instructors_Controller', 'LLMS_REST_Students_Controller', 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..815c6631 --- /dev/null +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -0,0 +1,657 @@ +collection_params = $this->build_collection_params(); + } + + /** + * Register routes. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.7 Fixed description of the `post_id` path parameter. + * @since 1.0.0-beta.10 Add `trigger` param for create/update/delete endpoints. + * Use backticks in args descriptions. + * + * @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 1.0.0-beta.1 + * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although + * the single enrollment permission will be checked in + * `LLMS_REST_Enrollments_Controller::get_objects()`. + * + * @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 enrollments. + * + * @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 1.0.0-beta.1 + * + * @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 1.0.0-beta.1 + * + * @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 1.0.0-beta.1 + * @since 1.0.0-beta.4 Fix call to undefined function llms_rest_bad_request(), + * must be llms_rest_bad_request_error(). + * + * @param int $student_id Student ID. + * @param int $post_id The course/membership 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 enrollments objects query. + * + * @since 1.0.0-beta.7 + * @since 1.0.0-beta.10 Set query limit to 1. + * + * @param int $attempt_id Student 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 1.0.0-beta.1 + * + * @return array The Enrollments collection parameters. + */ + public function get_collection_params() { + return $this->collection_params; + } + + /** + * Retrieves the query params for the objects collection. + * + * @since 1.0.0-beta.1 + * + * @param array $collection_params The Enrollments 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' => 'students-enrollments', + '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 1.0.0-beta.7 + * + * @param WP_Query $query Query result. + * @return obj[] + */ + protected function get_objects_from_query( $query ) { + + return $query->items; + } + + /** + * Prepare collection items for response. + * + * @since 1.0.0-beta.7 + * + * @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 1.0.0-beta.7 + * + * @param stdClass $query Objects query result returned by {@see LLMS_REST_Enrollments_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 enrollments 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 enrollments 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 stdClass An object with two fields: 'items' an array of OBJECT result of the query; 'found_results' the total found items. + */ + protected function get_objects_query( $query_args, $request = null ) { + + global $wpdb; + + // Maybe limit the query results depending on the page param. + if ( isset( $query_args['page'] ) ) { + $skip = $query_args['page'] > 1 ? ( $query_args['page'] - 1 ) * $query_args['per_page'] : 0; + $limit = $wpdb->prepare( + 'LIMIT %d, %d', + array( + $skip, + $query_args['per_page'], + ) + ); + } else { + $limit = $wpdb->prepare( + 'LIMIT %d', + $query_args['per_page'] + ); + } + + $filter = ''; + + if ( isset( $query_args['id'] ) && ! empty( $query_args['id'] ) ) { + $filter .= $wpdb->prepare( ' AND qa.student_id = %d', $query_args['id'] ); + } + + if ( isset( $query_args['status'] ) ) { + $filter .= $wpdb->prepare( ' AND qa.status = %s', $query_args['status'] ); + } + + if ( isset( $query_args['orderby'], $query_args['order'] ) ) { + $order = sprintf( 'ORDER BY %1$s %2$s', esc_sql( $query_args['orderby'] ), esc_sql( $query_args['order'] ) ); + } else { + $order = ''; + } + + $query = new stdClass(); + + $select_found_rows = empty( $query_args['no_found_rows'] ) ? esc_sql( 'SQL_CALC_FOUND_ROWS' ) : ''; + + // the query. + $query->items = $wpdb->get_results( + $wpdb->prepare( + " + SELECT {$select_found_rows} DISTINCT id, student_id, quiz_id, lesson_id, start_date, update_date, end_date, status, attempt, grade, can_be_resumed + FROM {$wpdb->prefix}lifterlms_quiz_attempts AS qa + WHERE 1=1 + {$filter} + {$order} + {$limit}; + ", + array( + $query_args['id'], + ) + ) + );// no-cache ok. + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + $count = count( $query->items ); + + if ( $count ) { + foreach ( $query->items as $key => $item ) { + $query->items[ $key ]->lesson_id = (int) $item->lesson_id; + $query->items[ $key ]->student_id = (int) $item->student_id; + $query->items[ $key ]->quiz_id = (int) $item->quiz_id; + } + } + + $query->found_results = empty( $query_args['no_found_rows'] ) ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : $count; // no-cache ok. + + return $query; + } + + /** + * Prepare a single object output for response. + * + * @since [version] + * + * @param stdClass $attempt Attempt object. + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + public function prepare_object_for_response( $attempt, $request ) { + + $prepared_quiz_attempt = get_object_vars( $attempt ); + + // Apply filters. + $prepared_quiz_attempt['status'] = apply_filters( + 'llms_get_quiz_attempt_status', + $prepared_quiz_attempt['status'], + $prepared_quiz_attempt['student_id'], + $prepared_quiz_attempt['quiz_id'], + $prepared_quiz_attempt['lesson_id'] + ); + + // Filter data including only schema props. + $data = array_intersect_key( $prepared_quiz_attempt, array_flip( $this->get_fields_for_response( $request ) ) ); + + /** + * Filters the enrollment data for a response. + * + * @since 1.0.0-beta.10 + * + * @param array $data Array of enrollment properties prepared for response. + * @param stdClass $enrollment Enrollment 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 enrollments links for the request. + * + * @since [version] + * + * @param object $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->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->student_id ) + ), + 'embeddable' => true, + ), + 'quiz' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'quizzes', $attempt->quiz_id ) + ), + 'embeddable' => true, + ), + 'lesson' => array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'lessons', $attempt->lesson_id ) + ), + 'embeddable' => true, + ), + ); + + /** + * Filters the enrollment's links. + * + * @since [version] + * + * @param array $links Links for the given enrollment. + * @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 enrollment can be read. + */ + protected function check_read_permission( $request ) { + + return current_user_can( 'manage_lifterlms' ); + } +} From 745a7eaf889fbd912c82a23f99b01dc58b09ff47 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 12:44:36 -0400 Subject: [PATCH 12/30] Refactor to use LLMS_Query_Quiz_Attempt. --- ...ass-llms-rest-quiz-attempts-controller.php | 114 +++++------------- 1 file changed, 29 insertions(+), 85 deletions(-) diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php index 815c6631..d499d0d0 100644 --- a/includes/server/class-llms-rest-quiz-attempts-controller.php +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -361,7 +361,7 @@ protected function get_item_schema_base() { */ protected function get_objects_from_query( $query ) { - return $query->items; + return $query->get_attempts(); } /** @@ -476,80 +476,22 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul * * @param array $query_args Array of collection arguments. * @param WP_REST_Request $request Optional. Full details about the request. Default null. - * @return stdClass An object with two fields: 'items' an array of OBJECT result of the query; 'found_results' the total found items. + * @return LLMS_Query_Quiz_Attempt */ protected function get_objects_query( $query_args, $request = null ) { - global $wpdb; - - // Maybe limit the query results depending on the page param. - if ( isset( $query_args['page'] ) ) { - $skip = $query_args['page'] > 1 ? ( $query_args['page'] - 1 ) * $query_args['per_page'] : 0; - $limit = $wpdb->prepare( - 'LIMIT %d, %d', - array( - $skip, - $query_args['per_page'], - ) - ); - } else { - $limit = $wpdb->prepare( - 'LIMIT %d', - $query_args['per_page'] + $args = array(); + if ( isset( $query_args['orderby'], $query_args['order'] ) ) { + $args['sort'] = array( + $query_args['orderby'] => $query_args['order'], ); } - $filter = ''; - - if ( isset( $query_args['id'] ) && ! empty( $query_args['id'] ) ) { - $filter .= $wpdb->prepare( ' AND qa.student_id = %d', $query_args['id'] ); - } - if ( isset( $query_args['status'] ) ) { - $filter .= $wpdb->prepare( ' AND qa.status = %s', $query_args['status'] ); - } - - if ( isset( $query_args['orderby'], $query_args['order'] ) ) { - $order = sprintf( 'ORDER BY %1$s %2$s', esc_sql( $query_args['orderby'] ), esc_sql( $query_args['order'] ) ); - } else { - $order = ''; + $args['status'] = $query_args['status']; } - $query = new stdClass(); - - $select_found_rows = empty( $query_args['no_found_rows'] ) ? esc_sql( 'SQL_CALC_FOUND_ROWS' ) : ''; - - // the query. - $query->items = $wpdb->get_results( - $wpdb->prepare( - " - SELECT {$select_found_rows} DISTINCT id, student_id, quiz_id, lesson_id, start_date, update_date, end_date, status, attempt, grade, can_be_resumed - FROM {$wpdb->prefix}lifterlms_quiz_attempts AS qa - WHERE 1=1 - {$filter} - {$order} - {$limit}; - ", - array( - $query_args['id'], - ) - ) - );// no-cache ok. - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - $count = count( $query->items ); - - if ( $count ) { - foreach ( $query->items as $key => $item ) { - $query->items[ $key ]->lesson_id = (int) $item->lesson_id; - $query->items[ $key ]->student_id = (int) $item->student_id; - $query->items[ $key ]->quiz_id = (int) $item->quiz_id; - } - } - - $query->found_results = empty( $query_args['no_found_rows'] ) ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : $count; // no-cache ok. - - return $query; + return new LLMS_Query_Quiz_Attempt( $args ); } /** @@ -557,24 +499,26 @@ protected function get_objects_query( $query_args, $request = null ) { * * @since [version] * - * @param stdClass $attempt Attempt object. - * @param WP_REST_Request $request Full details about the request. + * @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 ) { - $prepared_quiz_attempt = get_object_vars( $attempt ); - - // Apply filters. - $prepared_quiz_attempt['status'] = apply_filters( - 'llms_get_quiz_attempt_status', - $prepared_quiz_attempt['status'], - $prepared_quiz_attempt['student_id'], - $prepared_quiz_attempt['quiz_id'], - $prepared_quiz_attempt['lesson_id'] + // 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' ), ); - // Filter data including only schema props. $data = array_intersect_key( $prepared_quiz_attempt, array_flip( $this->get_fields_for_response( $request ) ) ); /** @@ -582,7 +526,7 @@ public function prepare_object_for_response( $attempt, $request ) { * * @since 1.0.0-beta.10 * - * @param array $data Array of enrollment properties prepared for response. + * @param array $data Array of quiz attempt properties prepared for response. * @param stdClass $enrollment Enrollment object. * @param WP_REST_Request $request Full details about the request. */ @@ -594,8 +538,8 @@ public function prepare_object_for_response( $attempt, $request ) { * * @since [version] * - * @param object $attempt Attempt object data. - * @param WP_REST_Request $request Request object. + * @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 ) { @@ -603,7 +547,7 @@ public function prepare_links( $attempt, $request ) { $links = array( 'self' => array( 'href' => rest_url( - sprintf( '/%s/%s/%d', 'llms/v1', 'quiz-attempts', $attempt->id ) + sprintf( '/%s/%s/%d', 'llms/v1', 'quiz-attempts', $attempt->get( 'id' ) ) ), ), 'collection' => array( @@ -613,19 +557,19 @@ public function prepare_links( $attempt, $request ) { ), 'student' => array( 'href' => rest_url( - sprintf( '/%s/%s/%d', 'llms/v1', 'students', $attempt->student_id ) + 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->quiz_id ) + 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->lesson_id ) + sprintf( '/%s/%s/%d', 'llms/v1', 'lessons', $attempt->get( 'lesson_id' ) ) ), 'embeddable' => true, ), From db78f15496ae88c1fd84de4758dc9cd6448f5476 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 12:55:26 -0400 Subject: [PATCH 13/30] WIP: Adding quiz attempts endpoint. --- .../class-llms-rest-webhook-data.php | 2 + includes/class-llms-rest-webhooks.php | 160 +++++++++--------- 2 files changed, 78 insertions(+), 84 deletions(-) diff --git a/includes/abstracts/class-llms-rest-webhook-data.php b/includes/abstracts/class-llms-rest-webhook-data.php index 338cf24f..f7b4cb90 100644 --- a/includes/abstracts/class-llms-rest-webhook-data.php +++ b/includes/abstracts/class-llms-rest-webhook-data.php @@ -222,6 +222,8 @@ 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] ); } diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index 87b7ccfb..06e286d7 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,46 @@ 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.completed' => __( 'Quiz Attempt completed', 'lifterlms' ), + 'action' => __( 'Action', 'lifterlms' ), ) ); - } /** @@ -255,145 +252,147 @@ 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, + ), ); return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks ); - } /** @@ -422,7 +421,6 @@ public function get_post_type_resources() { 'llms_transaction', ) ); - } /** @@ -457,7 +455,6 @@ protected function is_data_valid( $data ) { } return true; - } /** @@ -479,7 +476,6 @@ public function is_topic_valid( $topic ) { } return false; - } /** @@ -513,11 +509,10 @@ public function load() { $loaded = 0; foreach ( $hooks->get_webhooks() as $hook ) { $hook->enqueue(); - $loaded++; + ++$loaded; } return $loaded; - } /** @@ -543,7 +538,6 @@ protected function save( $obj, $data ) { $obj->setup( $data )->save(); return $obj; - } /** @@ -580,7 +574,5 @@ protected function update_prepare( $data ) { $data['updated'] = llms_current_time( 'mysql' ); return $data; - } - } From 7524e4ef331f52181f704f644fb7fad601b0368e Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 13:47:29 -0400 Subject: [PATCH 14/30] Quiz attempt completed webhook. --- includes/class-llms-rest-webhooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index 06e286d7..7191f763 100644 --- a/includes/class-llms-rest-webhooks.php +++ b/includes/class-llms-rest-webhooks.php @@ -232,7 +232,7 @@ public function get_topics() { 'instructor.created' => __( 'Instructor created', 'lifterlms' ), 'instructor.updated' => __( 'Instructor updated', 'lifterlms' ), 'instructor.deleted' => __( 'Instructor deleted', 'lifterlms' ), - 'quiz_attempt.completed' => __( 'Quiz Attempt completed', 'lifterlms' ), + 'quiz_attempt.updated' => __( 'Quiz Attempt completed', 'lifterlms' ), 'action' => __( 'Action', 'lifterlms' ), ) ); From 8200ff73c1ce48321ce88f8480aeb2496bead99b Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 13:57:36 -0400 Subject: [PATCH 15/30] Changelogs. --- .../feature_embed-linked-models-student-progress-webhook-1.yml | 3 +++ .../feature_embed-linked-models-student-progress-webhook-2.yml | 3 +++ .../feature_embed-linked-models-student-progress-webhook.yml | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 .changelogs/feature_embed-linked-models-student-progress-webhook-1.yml create mode 100644 .changelogs/feature_embed-linked-models-student-progress-webhook-2.yml create mode 100644 .changelogs/feature_embed-linked-models-student-progress-webhook.yml 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..6a822199 --- /dev/null +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml @@ -0,0 +1,3 @@ +significance: minor +type: added +entry: New Quiz and Quiz Attempt endpoints. From 7c1ef12e110267c59a5b7b51fe0e8f31cfcb08b8 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 14:52:27 -0400 Subject: [PATCH 16/30] WIP: Initial orders endpoint. --- class-lifterlms-rest-api.php | 2 ++ .../class-llms-rest-orders-controller.php | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 includes/server/class-llms-rest-orders-controller.php diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index b7643382..7df5f18c 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -144,6 +144,7 @@ public function rest_api_includes() { 'server/class-llms-rest-courses-controller', 'server/class-llms-rest-quizzes-controller', 'server/class-llms-rest-quiz-attempts-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', @@ -178,6 +179,7 @@ public function rest_api_controllers_init() { 'LLMS_REST_Lessons_Controller', 'LLMS_REST_Quizzes_Controller', 'LLMS_REST_Quiz_Attempts_Controller', + 'LLMS_REST_Orders_Controller', 'LLMS_REST_Memberships_Controller', 'LLMS_REST_Instructors_Controller', 'LLMS_REST_Students_Controller', 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..9eccf848 --- /dev/null +++ b/includes/server/class-llms-rest-orders-controller.php @@ -0,0 +1,33 @@ + Date: Thu, 26 Jun 2025 15:05:01 -0400 Subject: [PATCH 17/30] Filter "llms-" out of status. Read-only for order items and collection. --- .../class-llms-rest-orders-controller.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/includes/server/class-llms-rest-orders-controller.php b/includes/server/class-llms-rest-orders-controller.php index 9eccf848..e67b15fd 100644 --- a/includes/server/class-llms-rest-orders-controller.php +++ b/includes/server/class-llms-rest-orders-controller.php @@ -27,6 +27,97 @@ function prepare_collection_query_args( $request ) { return $query_args; } + /** + * Prepare links for the request. + * + * @since 1.0.0-beta.1 + * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`. + * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. + * @since 1.0.0-beta.7 `self` and `collection` links prepared in the parent class. + * Fix wp:featured_media link, we don't expose any embeddable field. + * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route. + * @since 1.0.0-beta.14 Added $request parameter. + * + * @param LLMS_Post_Model $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given object. + */ + protected function prepare_links( $object, $request ) { + + $links = parent::prepare_links( $object, $request ); + + $links['student'] = array( + 'href' => rest_url( + sprintf( '/%s/%s/%d', 'llms/v1', 'students', $object->get( 'user_id' ) ) + ), + 'embeddable' => true, + ); + + 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'] ); + + return $data; + } + + /** + * 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' ); } From f5a10fc21aa6d0ee6369460fa2e06dbdff0e897b Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 15:05:37 -0400 Subject: [PATCH 18/30] Fixing comment. --- includes/server/class-llms-rest-orders-controller.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/includes/server/class-llms-rest-orders-controller.php b/includes/server/class-llms-rest-orders-controller.php index e67b15fd..ecb1619e 100644 --- a/includes/server/class-llms-rest-orders-controller.php +++ b/includes/server/class-llms-rest-orders-controller.php @@ -30,15 +30,9 @@ function prepare_collection_query_args( $request ) { /** * Prepare links for the request. * - * @since 1.0.0-beta.1 - * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`. - * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`. - * @since 1.0.0-beta.7 `self` and `collection` links prepared in the parent class. - * Fix wp:featured_media link, we don't expose any embeddable field. - * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route. - * @since 1.0.0-beta.14 Added $request parameter. + * @since [version] * - * @param LLMS_Post_Model $object Object data. + * @param LLMS_Order $object Object data. * @param WP_REST_Request $request Request object. * @return array Links for the given object. */ From 133bd2a2eb05894fc77c7d0ae859902c3dc457c5 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 15:10:01 -0400 Subject: [PATCH 19/30] Adding billing email. --- .../class-llms-rest-orders-controller.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/includes/server/class-llms-rest-orders-controller.php b/includes/server/class-llms-rest-orders-controller.php index ecb1619e..1b3e2cb4 100644 --- a/includes/server/class-llms-rest-orders-controller.php +++ b/includes/server/class-llms-rest-orders-controller.php @@ -63,11 +63,36 @@ protected function prepare_object_for_response( $order, $request ) { $data = parent::prepare_object_for_response( $order, $request ); - $data['status'] = str_replace( 'llms-', '', $data['status'] ); + $data['status'] = str_replace( 'llms-', '', $data['status'] ); + $data['billing_email'] = $order->get( 'billing_email' ); 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' ), + ), + ); + + $schema['properties'] = array_merge( (array) $schema['properties'], $order_properties ); + + return $schema; + } + /** * Register routes. * From deba91adb384df3d3cce03f775e37487ed4c1785 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 26 Jun 2025 16:03:53 -0400 Subject: [PATCH 20/30] WIP: Adding additional fields --- .../class-llms-rest-orders-controller.php | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/includes/server/class-llms-rest-orders-controller.php b/includes/server/class-llms-rest-orders-controller.php index 1b3e2cb4..38ab365c 100644 --- a/includes/server/class-llms-rest-orders-controller.php +++ b/includes/server/class-llms-rest-orders-controller.php @@ -47,6 +47,30 @@ protected function prepare_links( $object, $request ) { '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; } @@ -63,8 +87,13 @@ 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['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; } @@ -81,11 +110,36 @@ protected function get_item_schema_base() { $schema = (array) parent::get_item_schema_base(); $order_properties = array( - 'billing_email' => 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 ); From d336c97ab90978aa2b02b540319e4f6a5bab15e5 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 11:46:00 -0400 Subject: [PATCH 21/30] Fixing comments. --- ...ass-llms-rest-quiz-attempts-controller.php | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php index d499d0d0..ecb6a6eb 100644 --- a/includes/server/class-llms-rest-quiz-attempts-controller.php +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -41,11 +41,6 @@ class LLMS_REST_Quiz_Attempts_Controller extends LLMS_REST_Controller { 'end_date', ); - /** - * Constructor. - * - * @since 1.0.0-beta.1 - */ public function __construct() { $this->collection_params = $this->build_collection_params(); } @@ -53,10 +48,7 @@ public function __construct() { /** * Register routes. * - * @since 1.0.0-beta.1 - * @since 1.0.0-beta.7 Fixed description of the `post_id` path parameter. - * @since 1.0.0-beta.10 Add `trigger` param for create/update/delete endpoints. - * Use backticks in args descriptions. + * @since [version] * * @return void */ @@ -100,10 +92,7 @@ public function register_routes() { /** * Check if a given request has access to read items. * - * @since 1.0.0-beta.1 - * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although - * the single enrollment permission will be checked in - * `LLMS_REST_Enrollments_Controller::get_objects()`. + * @since [version] * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -139,7 +128,7 @@ public function get_items( $request ) { /** * Check if a given request has access to read an item. * - * @since 1.0.0-beta.1 + * @since [version] * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|boolean @@ -156,7 +145,7 @@ public function get_item_permissions_check( $request ) { /** * Get a single item. * - * @since 1.0.0-beta.1 + * @since [version] * * @param WP_REST_Request $request Full details about the request. * @return WP_Error|WP_REST_Response @@ -176,9 +165,7 @@ public function get_item( $request ) { /** * Get object. * - * @since 1.0.0-beta.1 - * @since 1.0.0-beta.4 Fix call to undefined function llms_rest_bad_request(), - * must be llms_rest_bad_request_error(). + * @since [version] * * @param int $student_id Student ID. * @param int $post_id The course/membership ID. @@ -204,8 +191,7 @@ protected function get_object( $attempt_id ) { /** * Prepare enrollments objects query. * - * @since 1.0.0-beta.7 - * @since 1.0.0-beta.10 Set query limit to 1. + * @since [version] * * @param int $attempt_id Student ID. * @return array @@ -226,7 +212,7 @@ protected function prepare_object_query_args( $attempt_id ) { /** * Retrieves the query params for the objects collection. * - * @since 1.0.0-beta.1 + * @since [version] * * @return array The Enrollments collection parameters. */ @@ -237,7 +223,7 @@ public function get_collection_params() { /** * Retrieves the query params for the objects collection. * - * @since 1.0.0-beta.1 + * @since [version] * * @param array $collection_params The Enrollments collection parameters to be set. * @return void @@ -354,7 +340,7 @@ protected function get_item_schema_base() { /** * Retrieve an array of objects from the result of $this->get_objects_query(). * - * @since 1.0.0-beta.7 + * @since [version] * * @param WP_Query $query Query result. * @return obj[] @@ -367,7 +353,7 @@ protected function get_objects_from_query( $query ) { /** * Prepare collection items for response. * - * @since 1.0.0-beta.7 + * @since [version] * * @param array $objects Array of objects to be prepared for response. * @param WP_REST_Request $request Full details about the request. @@ -395,7 +381,7 @@ protected function prepare_collection_items_for_response( $objects, $request ) { /** * Retrieve pagination information from an objects query. * - * @since 1.0.0-beta.7 + * @since [version] * * @param stdClass $query Objects query result returned by {@see LLMS_REST_Enrollments_Controller::get_objects_query()}. * @param array $prepared Array of collection arguments. From aa4c21f517b089ae1567989aafcb1cb396de9e79 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 11:47:34 -0400 Subject: [PATCH 22/30] Fixing comments. --- includes/server/class-llms-rest-quiz-attempts-controller.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php index ecb6a6eb..fffd793b 100644 --- a/includes/server/class-llms-rest-quiz-attempts-controller.php +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -167,8 +167,7 @@ public function get_item( $request ) { * * @since [version] * - * @param int $student_id Student ID. - * @param int $post_id The course/membership ID. + * @param int $attempt_id Quiz attempt ID. * @return object|WP_Error */ protected function get_object( $attempt_id ) { @@ -193,7 +192,7 @@ protected function get_object( $attempt_id ) { * * @since [version] * - * @param int $attempt_id Student ID. + * @param int $attempt_id Attempt ID. * @return array */ protected function prepare_object_query_args( $attempt_id ) { From d0ec7ed8209aff899fabf7f3a8c689cc20686d0d Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 11:50:41 -0400 Subject: [PATCH 23/30] FIx schema base. --- includes/server/class-llms-rest-quiz-attempts-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php index fffd793b..e6b53cba 100644 --- a/includes/server/class-llms-rest-quiz-attempts-controller.php +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -271,7 +271,7 @@ protected function get_item_schema_base() { return array( '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'students-enrollments', + 'title' => 'quiz-attempts', 'type' => 'object', 'properties' => array( 'student_id' => array( From 20f861f73cf9505ceaa635485ed759abf63956c4 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 11:57:20 -0400 Subject: [PATCH 24/30] More comment fixes. --- ...ass-llms-rest-quiz-attempts-controller.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/includes/server/class-llms-rest-quiz-attempts-controller.php b/includes/server/class-llms-rest-quiz-attempts-controller.php index e6b53cba..dc32278b 100644 --- a/includes/server/class-llms-rest-quiz-attempts-controller.php +++ b/includes/server/class-llms-rest-quiz-attempts-controller.php @@ -107,7 +107,7 @@ public function get_items_permissions_check( $request ) { } /** - * Get a collection of enrollments. + * Get a collection of quiz attempts. * * @since [version] * @@ -188,7 +188,7 @@ protected function get_object( $attempt_id ) { } /** - * Prepare enrollments objects query. + * Prepare quiz attempts objects query. * * @since [version] * @@ -213,7 +213,7 @@ protected function prepare_object_query_args( $attempt_id ) { * * @since [version] * - * @return array The Enrollments collection parameters. + * @return array The quiz attempt collection parameters. */ public function get_collection_params() { return $this->collection_params; @@ -224,7 +224,7 @@ public function get_collection_params() { * * @since [version] * - * @param array $collection_params The Enrollments collection parameters to be set. + * @param array $collection_params The quiz attempt collection parameters to be set. * @return void */ public function set_collection_params( $collection_params ) { @@ -382,7 +382,7 @@ protected function prepare_collection_items_for_response( $objects, $request ) { * * @since [version] * - * @param stdClass $query Objects query result returned by {@see LLMS_REST_Enrollments_Controller::get_objects_query()}. + * @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 { @@ -403,7 +403,7 @@ protected function get_pagination_data_from_query( $query, $prepared, $request ) } /** - * Prepare enrollments objects query + * Prepare quiz attempts objects query * * @since [version] * @@ -455,7 +455,7 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul } /** - * Get enrollments query. + * Get quiz attempts query. * * @since [version] * @@ -507,19 +507,19 @@ public function prepare_object_for_response( $attempt, $request ) { $data = array_intersect_key( $prepared_quiz_attempt, array_flip( $this->get_fields_for_response( $request ) ) ); /** - * Filters the enrollment data for a response. + * Filters the quiz attempt data for a response. * - * @since 1.0.0-beta.10 + * @since [version] * * @param array $data Array of quiz attempt properties prepared for response. - * @param stdClass $enrollment Enrollment object. + * @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 enrollments links for the request. + * Prepare quiz attempt links for the request. * * @since [version] * @@ -561,11 +561,11 @@ public function prepare_links( $attempt, $request ) { ); /** - * Filters the enrollment's links. + * Filters the quiz attempt's links. * * @since [version] * - * @param array $links Links for the given enrollment. + * @param array $links Links for the given quiz attempt. * @param stdClass $attempt Attempt object. */ return apply_filters( 'llms_rest_quiz_attempt_links', $links, $attempt ); @@ -577,7 +577,7 @@ public function prepare_links( $attempt, $request ) { * @since [version] * * @param WP_REST_Request $request The request array. - * @return bool Whether the enrollment can be read. + * @return bool Whether the quiz attempt can be read. */ protected function check_read_permission( $request ) { From 0819f3b198be68e0ddf45c532c3a9fd31041f404 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 15:11:47 -0400 Subject: [PATCH 25/30] Removing add-on topic from core. Allow generic webhook rest api building using object. --- .../class-llms-rest-webhook-data.php | 5 +- includes/class-llms-rest-webhooks.php | 76 ++++++++++--------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/includes/abstracts/class-llms-rest-webhook-data.php b/includes/abstracts/class-llms-rest-webhook-data.php index f7b4cb90..ab51adb7 100644 --- a/includes/abstracts/class-llms-rest-webhook-data.php +++ b/includes/abstracts/class-llms-rest-webhook-data.php @@ -222,10 +222,11 @@ 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 ) { + } 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( diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index 7191f763..f032a3c9 100644 --- a/includes/class-llms-rest-webhooks.php +++ b/includes/class-llms-rest-webhooks.php @@ -232,7 +232,7 @@ public function get_topics() { 'instructor.created' => __( 'Instructor created', 'lifterlms' ), 'instructor.updated' => __( 'Instructor updated', 'lifterlms' ), 'instructor.deleted' => __( 'Instructor deleted', 'lifterlms' ), - 'quiz_attempt.updated' => __( 'Quiz Attempt completed', 'lifterlms' ), + 'quiz-attempt.updated' => __( 'Quiz Attempt completed', 'lifterlms' ), 'action' => __( 'Action', 'lifterlms' ), ) ); @@ -252,144 +252,148 @@ 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( + 'quiz-attempt.updated' => array( 'lifterlms_quiz_completed' => 3, ), + + 'assignment-submission.created' => array( + 'llms_assignment_submitted' => 1, + ), ); return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks ); From d7c814ff29a417f62d999c57cae847e48eb21944 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 15:17:10 -0400 Subject: [PATCH 26/30] Adding assignment webhook topic and hooks within the add-on. --- includes/class-llms-rest-webhooks.php | 73 +++++++++++++-------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index f032a3c9..f8473bce 100644 --- a/includes/class-llms-rest-webhooks.php +++ b/includes/class-llms-rest-webhooks.php @@ -252,148 +252,145 @@ 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( + 'quiz-attempt.updated' => array( 'lifterlms_quiz_completed' => 3, ), - 'assignment-submission.created' => array( - 'llms_assignment_submitted' => 1, - ), ); return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks ); From 76f3b86a8355d039cffe31dd16367b28a7a1e94a Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 16:41:18 -0400 Subject: [PATCH 27/30] Base certificate endpoint. --- class-lifterlms-rest-api.php | 2 + ...lass-llms-rest-certificates-controller.php | 182 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 includes/server/class-llms-rest-certificates-controller.php diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index 7df5f18c..38c2aac1 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -144,6 +144,7 @@ public function rest_api_includes() { '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-orders-controller', 'server/class-llms-rest-sections-controller', 'server/class-llms-rest-lessons-controller', @@ -179,6 +180,7 @@ public function rest_api_controllers_init() { 'LLMS_REST_Lessons_Controller', 'LLMS_REST_Quizzes_Controller', 'LLMS_REST_Quiz_Attempts_Controller', + 'LLMS_REST_Certificates_Controller', 'LLMS_REST_Orders_Controller', 'LLMS_REST_Memberships_Controller', 'LLMS_REST_Instructors_Controller', 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; + } +} From 16f41428eef8d61ead64f402dd12b48c929e5b77 Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Fri, 27 Jun 2025 16:59:35 -0400 Subject: [PATCH 28/30] awarded-certificates endpoint. --- class-lifterlms-rest-api.php | 2 + ...s-rest-awarded-certificates-controller.php | 178 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 includes/server/class-llms-rest-awarded-certificates-controller.php diff --git a/class-lifterlms-rest-api.php b/class-lifterlms-rest-api.php index 38c2aac1..d05ec2fb 100644 --- a/class-lifterlms-rest-api.php +++ b/class-lifterlms-rest-api.php @@ -145,6 +145,7 @@ public function rest_api_includes() { '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', @@ -181,6 +182,7 @@ public function rest_api_controllers_init() { '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', 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..8c081297 --- /dev/null +++ b/includes/server/class-llms-rest-awarded-certificates-controller.php @@ -0,0 +1,178 @@ + __( 'The ID of the certificate template.', 'lifterlms' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ); + + // Update defaults. + $schema['properties']['content']['required'] = false; + + // Remove unnecessary props. + $remove = array( + 'status', + '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['post'] = $certificate->get( 'post_id' ); + $data['certificate'] = $certificate->get( 'parent' ); + + /** + * 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 ); + + unset( $links['content'] ); + + return $links; + } +} From 46ff3385cca959b18123da8b2c535cf4c35a12bf Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 3 Jul 2025 09:15:44 -0400 Subject: [PATCH 29/30] Awarded-certificates webhook and API mods. --- includes/class-llms-rest-webhooks.php | 151 +++++++++--------- ...s-rest-awarded-certificates-controller.php | 19 ++- 2 files changed, 94 insertions(+), 76 deletions(-) diff --git a/includes/class-llms-rest-webhooks.php b/includes/class-llms-rest-webhooks.php index f8473bce..ef2fe6ba 100644 --- a/includes/class-llms-rest-webhooks.php +++ b/includes/class-llms-rest-webhooks.php @@ -196,44 +196,45 @@ 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' ), - 'quiz-attempt.updated' => __( 'Quiz Attempt completed', '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' ), ) ); } @@ -252,145 +253,149 @@ 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( + '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 ); diff --git a/includes/server/class-llms-rest-awarded-certificates-controller.php b/includes/server/class-llms-rest-awarded-certificates-controller.php index 8c081297..e40ece53 100644 --- a/includes/server/class-llms-rest-awarded-certificates-controller.php +++ b/includes/server/class-llms-rest-awarded-certificates-controller.php @@ -67,12 +67,18 @@ public function get_item_schema() { '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( - 'status', 'comment_status', 'password', 'ping_status', @@ -143,8 +149,8 @@ protected function prepare_object_for_response( $certificate, $request ) { $data = parent::prepare_object_for_response( $certificate, $request ); - $data['post'] = $certificate->get( 'post_id' ); - $data['certificate'] = $certificate->get( 'parent' ); + $data['certificate_id'] = $certificate->get( 'parent' ); + $data['student_id'] = $certificate->get( 'author' ); /** * Filters the assignment data for a response. @@ -171,6 +177,13 @@ 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; From 6762e682e150184aaaaf51e7acbe1f16e3eaf3da Mon Sep 17 00:00:00 2001 From: Brian Hogg Date: Thu, 3 Jul 2025 09:16:08 -0400 Subject: [PATCH 30/30] Changelog. --- .../feature_embed-linked-models-student-progress-webhook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelogs/feature_embed-linked-models-student-progress-webhook.yml b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml index 6a822199..c66e519d 100644 --- a/.changelogs/feature_embed-linked-models-student-progress-webhook.yml +++ b/.changelogs/feature_embed-linked-models-student-progress-webhook.yml @@ -1,3 +1,3 @@ significance: minor type: added -entry: New Quiz and Quiz Attempt endpoints. +entry: New Quiz, Quiz Attempt, Certificate, Awarded Certificate, and Order endpoints.