Manual Audience Segments API read endpoints (#19542)

* getting and listing segments

* get list of users in segment

* docs???
This commit is contained in:
PJ 2023-05-26 15:10:26 +01:00 committed by GitHub
parent bbb9fd2e1e
commit 453c2eeccb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 562 additions and 9 deletions

View file

@ -1,12 +1,30 @@
module Api
module V1
class AudienceSegmentsController < ApiController
DEFAULT_PER_PAGE = 30
MAX_USER_IDS = 10_000
USER_ATTRIBUTES_FOR_SERIALIZATION = %i[
id username name twitter_username github_username
profile_image website_url location summary created_at
].freeze
before_action :authenticate_with_api_key!
before_action :require_admin
before_action :restrict_user_ids, only: %i[add_users remove_users]
def index
page, per_page = pagination_params
@segments = scope.including_user_counts.order(id: :desc).page(page).per(per_page)
render json: @segments
end
def show
@segment = scope.including_user_counts.find(params[:id])
render json: @segment
end
def create
@segment = AudienceSegment.create!(type_of: "manual")
@ -25,6 +43,13 @@ module Api
end
end
def users
@segment = scope.find(params[:id])
page, per_page = pagination_params
@users = @segment.users.joins(:profile).select(USER_ATTRIBUTES_FOR_SERIALIZATION).page(page).per(per_page)
end
def add_users
@segment = scope.find(params[:id])
@ -50,6 +75,14 @@ module Api
render json: { error: "Too many user IDs provided" }, status: :unprocessable_entity
end
def pagination_params
per_page_param = params[:per_page] || DEFAULT_PER_PAGE
max_per_page = ApplicationConfig["API_PER_PAGE_MAX"] || 1000
per_page = [per_page_param, max_per_page].map(&:to_i).min
[params[:page].to_i, per_page]
end
def scope
AudienceSegment.manual
end

View file

@ -58,6 +58,12 @@ class AudienceSegment < ApplicationRecord
.map { |(id, type)| [I18n.t("models.#{model_name.i18n_key}.type_ofs.#{type}"), id] }
end
def self.including_user_counts
left_joins(:segmented_users)
.select("audience_segments.*, COUNT(segmented_users.id) AS user_count")
.group("audience_segments.id")
end
def persist_recently_active_users
self.users = self.class.recently_active_users_in_segment(type_of)
end

View file

@ -0,0 +1,3 @@
json.array! @users do |user|
json.partial! "api/v1/shared/user_show", user: user
end

View file

@ -55,7 +55,8 @@ Rails.application.routes.draw do
put "unpublish", on: :member
end
resources :segments, controller: "audience_segments", only: %i[create destroy] do
resources :segments, controller: "audience_segments", only: %i[index show create destroy] do
get "users", on: :member
put "add_users", on: :member
put "remove_users", on: :member
end

View file

@ -46,6 +46,87 @@ RSpec.describe "Api::V1::AudienceSegments" do
end
end
describe "GET /api/segments" do
subject(:make_request) { get api_segments_path, headers: headers }
let(:first_manual_segment) { AudienceSegment.create!(type_of: "manual") }
let(:second_manual_segment) { AudienceSegment.create!(type_of: "manual") }
let(:automatic_segment) { AudienceSegment.create!(type_of: "trusted") }
let(:users) { create_list(:user, 3) }
it_behaves_like "an admin-only protected resource"
it "returns only manual segments including their user counts" do
first_manual_segment.users << users
expected_data = [
hash_including(
"id" => first_manual_segment.id,
"type_of" => "manual",
"user_count" => 3,
),
hash_including(
"id" => second_manual_segment.id,
"type_of" => "manual",
"user_count" => 0,
),
]
excluded_data = hash_including("id" => automatic_segment.id)
make_request
expect(response).to have_http_status(:success)
expect(response.media_type).to eq("application/json")
expect(response.parsed_body).to include(*expected_data)
expect(response.parsed_body).not_to include(excluded_data)
end
it "supports pagination" do
5.times.each { AudienceSegment.create!(type_of: :manual) }
get api_segments_path, params: { page: 1, per_page: 3 }, headers: headers
expect(response.parsed_body.length).to eq(3)
get api_segments_path, params: { page: 2, per_page: 4 }, headers: headers
expect(response.parsed_body.length).to eq(1)
end
it "respects API_PER_PAGE_MAX env variable limit" do
5.times.each { AudienceSegment.create!(type_of: :manual) }
allow(ApplicationConfig).to receive(:[]).and_return(nil)
allow(ApplicationConfig).to receive(:[]).with("APP_PROTOCOL").and_return("http://")
allow(ApplicationConfig).to receive(:[]).with("API_PER_PAGE_MAX").and_return(3)
get api_segments_path, params: { per_page: 10 }, headers: headers
expect(response.parsed_body.length).to eq(3)
end
end
describe "GET /api/segments/:id" do
subject(:make_request) { get api_segment_path(segment.id), headers: headers }
let(:segment) { AudienceSegment.create!(type_of: "manual") }
let(:users) { create_list(:user, 3) }
it_behaves_like "an admin-only protected resource"
it_behaves_like "an endpoint for only manual audience segments"
it "returns the segment including its user count" do
segment.users << users
make_request
expect(response).to have_http_status(:success)
expect(response.media_type).to eq("application/json")
expect(response.parsed_body).to include(
"id" => segment.id,
"type_of" => "manual",
"user_count" => 3,
)
end
end
describe "POST /api/segments" do
subject(:make_request) { post api_segments_path, headers: headers }
@ -66,6 +147,60 @@ RSpec.describe "Api::V1::AudienceSegments" do
end
end
describe "GET /api/segments/:id/users" do
subject(:make_request) { get users_api_segment_path(segment.id), headers: headers }
let(:segment) { AudienceSegment.create!(type_of: "manual") }
let(:users) { create_list(:user, 5) }
it_behaves_like "an admin-only protected resource"
it_behaves_like "an endpoint for only manual audience segments"
it "returns a list of users in the segment" do
segment.users << users
expected_data = users.map do |user|
hash_including(
"id" => user.id,
"username" => user.username,
"name" => user.name,
"twitter_username" => user.twitter_username,
"github_username" => user.github_username,
"summary" => user.profile.summary,
"location" => user.profile.location,
"website_url" => user.profile.website_url,
)
end
make_request
expect(response).to have_http_status(:success)
expect(response.media_type).to eq("application/json")
expect(response.parsed_body).to match_array(expected_data)
end
it "supports pagination" do
segment.users << users
get users_api_segment_path(segment.id), params: { page: 1, per_page: 3 }, headers: headers
expect(response.parsed_body.length).to eq(3)
get users_api_segment_path(segment.id), params: { page: 2, per_page: 4 }, headers: headers
expect(response.parsed_body.length).to eq(1)
end
it "respects API_PER_PAGE_MAX env variable limit" do
segment.users << users
allow(ApplicationConfig).to receive(:[]).and_return(nil)
allow(ApplicationConfig).to receive(:[]).with("APP_PROTOCOL").and_return("http://")
allow(ApplicationConfig).to receive(:[]).with("API_PER_PAGE_MAX").and_return(3)
get users_api_segment_path(segment.id), params: { per_page: 5 }, headers: headers
expect(response.parsed_body.length).to eq(3)
end
end
describe "PUT /api/segments/:id/add_users" do
subject(:make_request) do
put add_users_api_segment_path(segment.id), params: { user_ids: user_ids }, headers: headers, as: :json

View file

@ -25,11 +25,59 @@ RSpec.describe "Api::V1::Docs::AudienceSegments" do
admin_api_secret.user.add_role(:admin)
end
describe "POST /segments" do
path "/api/segments" do
path "/api/segments" do
describe "GET /segments" do
get "Manually managed audience segments" do
tags "segments"
description "This endpoint allows the client to retrieve a list of audience segments.
An audience segment is a group of users that can be targeted by a Billboard. This API only permits managing segments you create and maintain yourself.
The endpoint supports pagination, and each page will contain `30` segments by default."
operationId "getSegments"
produces "application/json"
consumes "application/json"
parameter "$ref": "#/components/parameters/perPageParam30to1000"
response "200", "A List of manually managed audience segments" do
let(:"api-key") { admin_api_secret.secret }
let(:second_segment) { AudienceSegment.create!(type_of: "manual") }
schema type: :array,
items: { "$ref": "#/components/schemas/Segment" }
before do
segment.users << users
second_segment.users << create(:user)
end
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { nil }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { regular_api_secret.secret }
add_examples
run_test!
end
end
end
describe "POST /segments" do
post "Create a manually managed audience segment" do
tags "segments"
description "This endpoint allows the client to create a new audience segment.\n\nAn audience segment is a group of users that can be targeted by a DisplayAd/Billboard/Widget. This API only permits managing segments you create and maintain yourself."
description "This endpoint allows the client to create a new audience segment.\n\nAn audience segment is a group of users that can be targeted by a Billboard. This API only permits managing segments you create and maintain yourself."
operationId "createSegment"
produces "application/json"
consumes "application/json"
@ -61,11 +109,65 @@ RSpec.describe "Api::V1::Docs::AudienceSegments" do
end
end
describe "DELETE /segments/:id" do
path "/api/segments/{id}" do
path "/api/segments/{id}" do
describe "GET /segments/:id" do
get "A manually managed audience segment" do
tags "segments"
description "This endpoint allows the client to retrieve a single manually-managed audience segment specified by ID."
operationId "getSegment"
produces "application/json"
consumes "application/json"
parameter name: :id, in: :path, required: true, schema: id_schema
response "200", "The audience segment" do
let(:"api-key") { admin_api_secret.secret }
let(:id) { segment.id }
schema type: :object,
items: { "$ref": "#/components/schemas/Segment" }
before do
segment.users << users
end
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { nil }
let(:id) { segment.id }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { regular_api_secret.secret }
let(:id) { segment.id }
add_examples
run_test!
end
response "404", "Audience Segment Not Found" do
let(:"api-key") { admin_api_secret.secret }
let(:id) { automatic_segment.id }
add_examples
run_test!
end
end
end
describe "DELETE /segments/:id" do
delete "Delete a manually managed audience segment" do
tags "segments"
description "This endpoint allows the client to delete an audience segment specified by ID.\n\nAudience segments cannot be deleted if there are still any DisplayAds/Billboards/Widgets using them."
description "This endpoint allows the client to delete an audience segment specified by ID.\n\nAudience segments cannot be deleted if there are still any Billboards using them."
operationId "deleteSegment"
produces "application/json"
consumes "application/json"
@ -125,6 +227,63 @@ RSpec.describe "Api::V1::Docs::AudienceSegments" do
end
end
describe "GET /segments/:id/users" do
path "/api/segments/{id}/users" do
get "Users in a manually managed audience segment" do
tags "segments"
description "This endpoint allows the client to retrieve a list of the users in an audience segment specified by ID. The endpoint supports pagination, and each page will contain `30` users by default."
operationId "getUsersInSegment"
produces "application/json"
consumes "application/json"
parameter name: :id, in: :path, required: true, schema: id_schema
parameter "$ref": "#/components/parameters/perPageParam30to1000"
response "200", "A List of users in the audience segment" do
let(:"api-key") { admin_api_secret.secret }
let(:id) { segment.id }
schema type: :array,
items: { "$ref": "#/components/schemas/User" }
before do
segment.users << users
end
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { nil }
let(:id) { segment.id }
add_examples
run_test!
end
response "401", "Unauthorized" do
let(:"api-key") { regular_api_secret.secret }
let(:id) { segment.id }
add_examples
run_test!
end
response "404", "Audience Segment Not Found" do
let(:"api-key") { admin_api_secret.secret }
let(:id) { automatic_segment.id }
add_examples
run_test!
end
end
end
end
describe "PUT /segments/:id/add_users" do
path "/api/segments/{id}/add_users" do
put "Add users to a manually managed audience segment" do

View file

@ -929,10 +929,63 @@
}
},
"/api/segments": {
"get": {
"summary": "Manually managed audience segments",
"tags": ["segments"],
"description": "This endpoint allows the client to retrieve a list of audience segments.\n\nAn audience segment is a group of users that can be targeted by a Billboard. This API only permits managing segments you create and maintain yourself.\n\nThe endpoint supports pagination, and each page will contain `30` segments by default.",
"operationId": "getSegments",
"parameters": [
{
"$ref": "#/components/parameters/perPageParam30to1000"
}
],
"responses": {
"200": {
"description": "A List of manually managed audience segments",
"content": {
"application/json": {
"example": [
{
"id": 127,
"created_at": "2023-05-19T13:29:19.013-05:00",
"type_of": "manual",
"updated_at": "2023-05-19T13:29:19.013-05:00",
"user_count": 1
},
{
"id": 126,
"created_at": "2023-05-19T13:29:19.013-05:00",
"type_of": "manual",
"updated_at": "2023-05-19T13:29:19.013-05:00",
"user_count": 3
}
],
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Segment"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
}
}
},
"post": {
"summary": "Create a manually managed audience segment",
"tags": ["segments"],
"description": "This endpoint allows the client to create a new audience segment.\n\nAn audience segment is a group of users that can be targeted by a DisplayAd/Billboard/Widget. This API only permits managing segments you create and maintain yourself.",
"description": "This endpoint allows the client to create a new audience segment.\n\nAn audience segment is a group of users that can be targeted by a Billboard. This API only permits managing segments you create and maintain yourself.",
"operationId": "createSegment",
"responses": {
"201": {
@ -963,10 +1016,72 @@
}
},
"/api/segments/{id}": {
"get": {
"summary": "A manually managed audience segment",
"tags": ["segments"],
"description": "This endpoint allows the client to retrieve a single manually-managed audience segment specified by ID.",
"operationId": "getSegment",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1
}
}
],
"responses": {
"200": {
"description": "The audience segment",
"content": {
"application/json": {
"example": {
"id": 131,
"created_at": "2023-05-19T13:29:19.481-05:00",
"type_of": "manual",
"updated_at": "2023-05-19T13:29:19.481-05:00",
"user_count": 3
},
"schema": {
"type": "object",
"items": {
"$ref": "#/components/schemas/Segment"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
},
"404": {
"description": "Audience Segment Not Found",
"content": {
"application/json": {
"example": {
"error": "not found",
"status": 404
}
}
}
}
}
},
"delete": {
"summary": "Delete a manually managed audience segment",
"tags": ["segments"],
"description": "This endpoint allows the client to delete an audience segment specified by ID.\n\nAudience segments cannot be deleted if there are still any DisplayAds/Billboards/Widgets using them.",
"description": "This endpoint allows the client to delete an audience segment specified by ID.\n\nAudience segments cannot be deleted if there are still any Billboards using them.",
"operationId": "deleteSegment",
"parameters": [
{
@ -1029,6 +1144,107 @@
}
}
},
"/api/segments/{id}/users": {
"get": {
"summary": "Users in a manually managed audience segment",
"tags": ["segments"],
"description": "This endpoint allows the client to retrieve a list of the users in an audience segment specified by ID. The endpoint supports pagination, and each page will contain `30` users by default.",
"operationId": "getUsersInSegment",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32",
"minimum": 1
}
},
{
"$ref": "#/components/parameters/perPageParam30to1000"
}
],
"responses": {
"200": {
"description": "A List of users in the audience segment",
"content": {
"application/json": {
"example": [
{
"type_of": "user",
"id": 1032,
"username": "username586",
"name": "Katrina \"Kirsten\" \\:/ Schiller",
"twitter_username": "twitter586",
"github_username": "github586",
"summary": null,
"location": null,
"website_url": null,
"joined_at": "May 19, 2023",
"profile_image": "/uploads/user/profile_image/1947/02641cc1-4268-4203-8f2d-59cf53d4433a.jpeg"
},
{
"type_of": "user",
"id": 1033,
"username": "username587",
"name": "Breanne \"Steve\" \\:/ Marquardt",
"twitter_username": "twitter587",
"github_username": "github587",
"summary": null,
"location": null,
"website_url": null,
"joined_at": "May 19, 2023",
"profile_image": "/uploads/user/profile_image/1948/621613c2-2990-4b92-8a26-8dac49725a95.jpeg"
},
{
"type_of": "user",
"id": 1034,
"username": "username588",
"name": "Eugene \"Jamar\" \\:/ Keebler",
"twitter_username": "twitter588",
"github_username": "github588",
"summary": null,
"location": null,
"website_url": null,
"joined_at": "May 19, 2023",
"profile_image": "/uploads/user/profile_image/1949/7d5a54bc-faf2-408b-a988-3ea5b8ef40f1.jpeg"
}
],
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"example": {
"error": "unauthorized",
"status": 401
}
}
}
},
"404": {
"description": "Audience Segment Not Found",
"content": {
"application/json": {
"example": {
"error": "not found",
"status": 404
}
}
}
}
}
}
},
"/api/segments/{id}/add_users": {
"put": {
"summary": "Add users to a manually managed audience segment",