REST API: Allow for the posts endpoint include/exclude terms query to include_children.

For example the `categories` or `categories_exclude` parameters can now optionally accept an object with a `terms` property that accepts the list of term ids and a new `include_children` property which controls the Tax Query `include_children` field.

Props jason_the_adams, jnylen0, birgire, dlh.
Fixes #39494.


git-svn-id: https://develop.svn.wordpress.org/trunk@50157 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Timothy Jacobs 2021-02-02 19:23:08 +00:00
parent 051c135c6e
commit 032e946633
3 changed files with 571 additions and 97 deletions

View File

@ -280,35 +280,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
}
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
if ( ! empty( $request['tax_relation'] ) ) {
$args['tax_query'] = array( 'relation' => $request['tax_relation'] );
}
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$tax_exclude = $base . '_exclude';
if ( ! empty( $request[ $base ] ) ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $request[ $base ],
'include_children' => false,
);
}
if ( ! empty( $request[ $tax_exclude ] ) ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $request[ $tax_exclude ],
'include_children' => false,
'operator' => 'NOT IN',
);
}
}
$args = $this->prepare_tax_query( $args, $request );
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
@ -2799,39 +2771,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
);
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
if ( ! empty( $taxonomies ) ) {
$query_params['tax_relation'] = array(
'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
);
}
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$query_params[ $base ] = array(
/* translators: %s: Taxonomy name. */
'description' => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params[ $base . '_exclude' ] = array(
/* translators: %s: Taxonomy name. */
'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
}
$query_params = $this->prepare_taxonomy_limit_schema( $query_params );
if ( 'post' === $this->post_type ) {
$query_params['sticky'] = array(
@ -2899,4 +2839,168 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
return $statuses;
}
/**
* Prepares the 'tax_query' for a collection of posts.
*
* @since 5.7.0
*
* @param array $args WP_Query arguments.
* @param WP_REST_Request $request Full details about the request.
* @return array Updated query arguments.
*/
private function prepare_tax_query( array $args, WP_REST_Request $request ) {
$relation = $request['tax_relation'];
if ( $relation ) {
$args['tax_query'] = array( 'relation' => $relation );
}
$taxonomies = wp_list_filter(
get_object_taxonomies( $this->post_type, 'objects' ),
array( 'show_in_rest' => true )
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$tax_include = $request[ $base ];
$tax_exclude = $request[ $base . '_exclude' ];
if ( $tax_include ) {
$terms = array();
$include_children = false;
if ( rest_is_array( $tax_include ) ) {
$terms = $tax_include;
} elseif ( rest_is_object( $tax_include ) ) {
$terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
$include_children = ! empty( $tax_include['include_children'] );
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
);
}
}
if ( $tax_exclude ) {
$terms = array();
$include_children = false;
if ( rest_is_array( $tax_exclude ) ) {
$terms = $tax_exclude;
} elseif ( rest_is_object( $tax_exclude ) ) {
$terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
$include_children = ! empty( $tax_exclude['include_children'] );
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
'operator' => 'NOT IN',
);
}
}
}
return $args;
}
/**
* Prepares the collection schema for including and excluding items by terms.
*
* @since 5.7.0
*
* @param array $query_params Collection schema.
* @return array Updated schema.
*/
private function prepare_taxonomy_limit_schema( array $query_params ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
if ( ! $taxonomies ) {
return $query_params;
}
$query_params['tax_relation'] = array(
'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
);
$limit_schema = array(
'type' => array( 'object', 'array' ),
'oneOf' => array(
array(
'title' => __( 'Term ID List' ),
'description' => __( 'Match terms with the listed IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
array(
'title' => __( 'Term ID Taxonomy Query' ),
'description' => __( 'Perform an advanced term query.' ),
'type' => 'object',
'properties' => array(
'terms' => array(
'description' => __( 'Term IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
),
'include_children' => array(
'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
'type' => 'boolean',
'default' => false,
),
),
'additionalProperties' => false,
),
),
);
$include_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
$exclude_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$base_exclude = $base . '_exclude';
$query_params[ $base ] = $include_schema;
$query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );
$query_params[ $base_exclude ] = $exclude_schema;
$query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );
if ( ! $taxonomy->hierarchical ) {
unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
}
}
return $query_params;
}
}

View File

@ -1115,6 +1115,185 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te
$this->assertSame( $id1, $data[2]['id'] );
}
/**
* @ticket 39494
*/
public function test_get_items_with_category_including_children() {
$taxonomy = get_taxonomy( 'category' );
$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
$cat2 = static::factory()->term->create(
array(
'taxonomy' => $taxonomy->name,
'parent' => $cat1,
)
);
$post_ids = array(
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat1 ),
)
),
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat2 ),
)
),
);
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param(
$taxonomy->rest_base,
array(
'terms' => array( $cat1 ),
'include_children' => true,
)
);
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertEqualSets( $post_ids, array_column( $data, 'id' ) );
}
/**
* @ticket 39494
*/
public function test_get_items_with_category_excluding_children() {
$taxonomy = get_taxonomy( 'category' );
$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
$cat2 = static::factory()->term->create(
array(
'taxonomy' => $taxonomy->name,
'parent' => $cat1,
)
);
$post_ids = array(
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat1 ),
)
),
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat2 ),
)
),
);
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param(
$taxonomy->rest_base,
array(
'terms' => array( $cat1 ),
'include_children' => false,
)
);
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertCount( 1, $data );
$this->assertEquals( $post_ids[0], $data[0]['id'] );
}
/**
* @ticket 39494
*/
public function test_get_items_without_category_or_its_children() {
$taxonomy = get_taxonomy( 'category' );
$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
$cat2 = static::factory()->term->create(
array(
'taxonomy' => $taxonomy->name,
'parent' => $cat1,
)
);
$post_ids = array(
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat1 ),
)
),
static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat2 ),
)
),
);
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param(
$taxonomy->rest_base . '_exclude',
array(
'terms' => array( $cat1 ),
'include_children' => true,
)
);
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$this->assertEmpty(
array_intersect(
$post_ids,
array_column( $data, 'id' )
)
);
}
/**
* @ticket 39494
*/
public function test_get_items_without_category_but_allowing_its_children() {
$taxonomy = get_taxonomy( 'category' );
$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
$cat2 = static::factory()->term->create(
array(
'taxonomy' => $taxonomy->name,
'parent' => $cat1,
)
);
$p1 = static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat1 ),
)
);
$p2 = static::factory()->post->create(
array(
'post_status' => 'publish',
'post_category' => array( $cat2 ),
)
);
$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
$request->set_param(
$taxonomy->rest_base . '_exclude',
array(
'terms' => array( $cat1 ),
'include_children' => false,
)
);
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
$found_ids = array_column( $data, 'id' );
$this->assertNotContains( $p1, $found_ids );
$this->assertContains( $p2, $found_ids );
}
/**
* @ticket 44326
*/

View File

@ -427,39 +427,149 @@ mockedApiResponse.Schema = {
"required": false
},
"categories": {
"description": "Limit result set to all items that have the specified term assigned in the categories taxonomy.",
"type": "array",
"items": {
"type": "integer"
},
"default": [],
"description": "Limit result set to items with specific terms assigned in the categories taxonomy.",
"type": [
"object",
"array"
],
"oneOf": [
{
"title": "Term ID List",
"description": "Match terms with the listed IDs.",
"type": "array",
"items": {
"type": "integer"
}
},
{
"title": "Term ID Taxonomy Query",
"description": "Perform an advanced term query.",
"type": "object",
"properties": {
"terms": {
"description": "Term IDs.",
"type": "array",
"items": {
"type": "integer"
},
"default": []
},
"include_children": {
"description": "Whether to include child terms in the terms limiting the result set.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
],
"required": false
},
"categories_exclude": {
"description": "Limit result set to all items except those that have the specified term assigned in the categories taxonomy.",
"type": "array",
"items": {
"type": "integer"
},
"default": [],
"description": "Limit result set to items except those with specific terms assigned in the categories taxonomy.",
"type": [
"object",
"array"
],
"oneOf": [
{
"title": "Term ID List",
"description": "Match terms with the listed IDs.",
"type": "array",
"items": {
"type": "integer"
}
},
{
"title": "Term ID Taxonomy Query",
"description": "Perform an advanced term query.",
"type": "object",
"properties": {
"terms": {
"description": "Term IDs.",
"type": "array",
"items": {
"type": "integer"
},
"default": []
},
"include_children": {
"description": "Whether to include child terms in the terms limiting the result set.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
],
"required": false
},
"tags": {
"description": "Limit result set to all items that have the specified term assigned in the tags taxonomy.",
"type": "array",
"items": {
"type": "integer"
},
"default": [],
"description": "Limit result set to items with specific terms assigned in the tags taxonomy.",
"type": [
"object",
"array"
],
"oneOf": [
{
"title": "Term ID List",
"description": "Match terms with the listed IDs.",
"type": "array",
"items": {
"type": "integer"
}
},
{
"title": "Term ID Taxonomy Query",
"description": "Perform an advanced term query.",
"type": "object",
"properties": {
"terms": {
"description": "Term IDs.",
"type": "array",
"items": {
"type": "integer"
},
"default": []
}
},
"additionalProperties": false
}
],
"required": false
},
"tags_exclude": {
"description": "Limit result set to all items except those that have the specified term assigned in the tags taxonomy.",
"type": "array",
"items": {
"type": "integer"
},
"default": [],
"description": "Limit result set to items except those with specific terms assigned in the tags taxonomy.",
"type": [
"object",
"array"
],
"oneOf": [
{
"title": "Term ID List",
"description": "Match terms with the listed IDs.",
"type": "array",
"items": {
"type": "integer"
}
},
{
"title": "Term ID Taxonomy Query",
"description": "Perform an advanced term query.",
"type": "object",
"properties": {
"terms": {
"description": "Term IDs.",
"type": "array",
"items": {
"type": "integer"
},
"default": []
}
},
"additionalProperties": false
}
],
"required": false
},
"sticky": {
@ -3128,8 +3238,95 @@ mockedApiResponse.Schema = {
"POST"
],
"args": {
"src": {
"description": "URL to the edited image file.",
"type": "string",
"format": "uri",
"required": true
},
"modifiers": {
"description": "Array of image edits.",
"type": "array",
"minItems": 1,
"items": {
"description": "Image edit.",
"type": "object",
"required": [
"type",
"args"
],
"oneOf": [
{
"title": "Rotation",
"properties": {
"type": {
"description": "Rotation type.",
"type": "string",
"enum": [
"rotate"
]
},
"args": {
"description": "Rotation arguments.",
"type": "object",
"required": [
"angle"
],
"properties": {
"angle": {
"description": "Angle to rotate clockwise in degrees.",
"type": "number"
}
}
}
}
},
{
"title": "Crop",
"properties": {
"type": {
"description": "Crop type.",
"type": "string",
"enum": [
"crop"
]
},
"args": {
"description": "Crop arguments.",
"type": "object",
"required": [
"left",
"top",
"width",
"height"
],
"properties": {
"left": {
"description": "Horizontal position from the left to begin the crop as a percentage of the image width.",
"type": "number"
},
"top": {
"description": "Vertical position from the top to begin the crop as a percentage of the image height.",
"type": "number"
},
"width": {
"description": "Width of the crop as a percentage of the image width.",
"type": "number"
},
"height": {
"description": "Height of the crop as a percentage of the image height.",
"type": "number"
}
}
}
}
}
]
},
"required": false
},
"rotation": {
"description": "The amount to rotate the image clockwise in degrees.",
"description": "The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.",
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true,
@ -3138,38 +3335,32 @@ mockedApiResponse.Schema = {
"required": false
},
"x": {
"description": "As a percentage of the image, the x position to start the crop from.",
"description": "As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.",
"type": "number",
"minimum": 0,
"maximum": 100,
"required": false
},
"y": {
"description": "As a percentage of the image, the y position to start the crop from.",
"description": "As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.",
"type": "number",
"minimum": 0,
"maximum": 100,
"required": false
},
"width": {
"description": "As a percentage of the image, the width to crop the image to.",
"description": "As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.",
"type": "number",
"minimum": 0,
"maximum": 100,
"required": false
},
"height": {
"description": "As a percentage of the image, the height to crop the image to.",
"description": "As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.",
"type": "number",
"minimum": 0,
"maximum": 100,
"required": false
},
"src": {
"description": "URL to the edited image file.",
"type": "string",
"format": "uri",
"required": true
}
}
}