From de3d62bc40abb73e10bb37504430b2c3154c68af Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 25 Nov 2020 03:50:57 -0800 Subject: [PATCH] API: Endpoint to get an organization's users (#11514) * add tests * add route * add users action to organizations controller * add jbuilder file * update api docs * add recource routes * make updates to organizations controller * remove org_users from api docs * update spec * add users show jbuilder * bug fix for user show --- .../api/v0/organizations_controller.rb | 24 ++++++- .../api/v0/organizations/users.json.jbuilder | 3 + .../api/v0/shared/_user_show.json.jbuilder | 16 +++++ app/views/api/v0/users/show.json.jbuilder | 17 +---- config/routes.rb | 6 +- docs/api_v0.yml | 68 ++++++++++++++++++- spec/requests/api/v0/organizations_spec.rb | 42 +++++++++++- 7 files changed, 151 insertions(+), 25 deletions(-) create mode 100644 app/views/api/v0/organizations/users.json.jbuilder create mode 100644 app/views/api/v0/shared/_user_show.json.jbuilder diff --git a/app/controllers/api/v0/organizations_controller.rb b/app/controllers/api/v0/organizations_controller.rb index 182028723..d43537a0e 100644 --- a/app/controllers/api/v0/organizations_controller.rb +++ b/app/controllers/api/v0/organizations_controller.rb @@ -1,15 +1,37 @@ module Api module V0 class OrganizationsController < ApiController + before_action :find_organization, only: %i[users] + SHOW_ATTRIBUTES_FOR_SERIALIZATION = %i[ username name summary twitter_username github_username url location created_at profile_image tech_stack tag_line story ].freeze private_constant :SHOW_ATTRIBUTES_FOR_SERIALIZATION + USERS_FOR_SERIALIZATION = %i[ + id username name twitter_username github_username + profile_image website_url location summary created_at + ].freeze + private_constant :USERS_FOR_SERIALIZATION + def show @organization = Organization.select(SHOW_ATTRIBUTES_FOR_SERIALIZATION) - .find_by!(username: params[:org_username]) + .find_by!(username: params[:username]) + end + + def users + per_page = (params[:per_page] || 30).to_i + num = [per_page, 1000].min + page = params[:page] || 1 + + @users = @organization.users.select(USERS_FOR_SERIALIZATION).page(page).per(num) + end + + private + + def find_organization + @organization = Organization.find_by!(username: params[:organization_username]) end end end diff --git a/app/views/api/v0/organizations/users.json.jbuilder b/app/views/api/v0/organizations/users.json.jbuilder new file mode 100644 index 000000000..b7b8fcb55 --- /dev/null +++ b/app/views/api/v0/organizations/users.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @users do |user| + json.partial! "api/v0/shared/user_show", user: user +end diff --git a/app/views/api/v0/shared/_user_show.json.jbuilder b/app/views/api/v0/shared/_user_show.json.jbuilder new file mode 100644 index 000000000..7c01a27bd --- /dev/null +++ b/app/views/api/v0/shared/_user_show.json.jbuilder @@ -0,0 +1,16 @@ +json.type_of "user" + +json.extract!( + user, + :id, + :username, + :name, + :summary, + :twitter_username, + :github_username, + :website_url, + :location, +) + +json.joined_at user.created_at.strftime("%b %e, %Y") +json.profile_image Images::Profile.call(user.profile_image_url, length: 320) diff --git a/app/views/api/v0/users/show.json.jbuilder b/app/views/api/v0/users/show.json.jbuilder index 44bce87e5..76f8211b0 100644 --- a/app/views/api/v0/users/show.json.jbuilder +++ b/app/views/api/v0/users/show.json.jbuilder @@ -1,16 +1 @@ -json.type_of "user" - -json.extract!( - @user, - :id, - :username, - :name, - :summary, - :twitter_username, - :github_username, - :website_url, - :location, -) - -json.joined_at @user.created_at.strftime("%b %e, %Y") -json.profile_image Images::Profile.call(@user.profile_image_url, length: 320) +json.partial! "api/v0/shared/user_show", user: @user diff --git a/config/routes.rb b/config/routes.rb index ea1caa665..1a1fe866a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -203,10 +203,8 @@ Rails.application.routes.draw do end resources :profile_images, only: %i[show], param: :username - resources :organizations, only: [] do - collection do - get "/:org_username", to: "organizations#show" - end + resources :organizations, only: [:show], param: :username do + resources :users, only: [:index], to: "organizations#users" end end end diff --git a/docs/api_v0.yml b/docs/api_v0.yml index bc56547d7..5174009b9 100644 --- a/docs/api_v0.yml +++ b/docs/api_v0.yml @@ -17,7 +17,7 @@ info: Dates and date times, unless otherwise specified, must be in the [RFC 3339](https://tools.ietf.org/html/rfc3339) format. - version: "0.9.2" + version: "0.9.3" termsOfService: https://dev.to/terms contact: name: DEV Team @@ -1683,6 +1683,20 @@ components: joined_at: Jan 1, 2017 profile_image: https://res.cloudinary.com/...jpeg + Users: + value: + - type_of: user + id: 1234 + username: bob + name: bob + summary: Hello, world + twitter_username: bob + github_username: bob + website_url: + location: New York + joined_at: Jan 1, 2017 + profile_image: https://res.cloudinary.com/...jpeg + WebhookCreate: value: webhook_endpoint: @@ -2986,7 +3000,7 @@ paths: source: | curl -H "api-key: API_KEY" https://dev.to/api/readinglist - /organizations/{org_username}: + /organizations/{username}: get: operationId: getOrganization summary: An organization @@ -2995,7 +3009,7 @@ paths: tags: - organizations parameters: - - name: org_username + - name: username in: path required: true description: | @@ -3028,6 +3042,54 @@ paths: source: | curl https://dev.to/api/organizations/ecorp + /organizations/{username}/users: + get: + operationId: getOrgUsers + summary: Organization's users + description: | + This endpoint allows the client to retrieve a list of users belonging to the organization + + It supports pagination, each page will contain `30` users by default. + tags: + - organizations + parameters: + - name: username + in: path + required: true + description: | + Username of the organization + schema: + type: string + example: "ecorp" + - $ref: '#/components/parameters/pageParam' + - $ref: '#/components/parameters/perPageParam30to1000' + responses: + "200": + description: A list of users belonging to the organization + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + examples: + user-success: + $ref: "#/components/examples/Users" + "404": + description: Resource not found + content: + application/json: + schema: + $ref: "#/components/schemas/APIError" + examples: + user-not-found: + $ref: "#/components/examples/ErrorNotFound" + x-code-samples: + - lang: Shell + label: curl + source: | + curl https://dev.to/api/organizations/ecorp/users + /podcast_episodes: get: operationId: getPodcastEpisodes diff --git a/spec/requests/api/v0/organizations_spec.rb b/spec/requests/api/v0/organizations_spec.rb index 2e4be8024..f39a86ed0 100644 --- a/spec/requests/api/v0/organizations_spec.rb +++ b/spec/requests/api/v0/organizations_spec.rb @@ -10,7 +10,7 @@ RSpec.describe "Api::V0::Organizations", type: :request do end it "returns the correct json representation of the organization", :aggregate_failures do - get "/api/organizations/#{organization.username}" + get api_organization_path(organization.username) response_organization = response.parsed_body @@ -25,4 +25,44 @@ RSpec.describe "Api::V0::Organizations", type: :request do expect(response_organization["joined_at"]).to eq(organization.created_at.utc.iso8601) end end + + describe "GET /api/organizations/:username/users" do + let!(:org_user) { create(:user, :org_member) } + let(:organization) { org_user.organizations.first } + + it "returns 404 if the organizations username is not found" do + get "/api/organizations/invalid-username/users" + expect(response).to have_http_status(:not_found) + end + + it "supports pagination" do + create(:organization_membership, user: create(:user), organization: organization) + + get api_organization_users_path(organization.username), params: { page: 1, per_page: 1 } + expect(response.parsed_body.length).to eq(1) + + get api_organization_users_path(organization.username), params: { page: 2, per_page: 1 } + expect(response.parsed_body.length).to eq(1) + + get api_organization_users_path(organization.username), params: { page: 3, per_page: 1 } + expect(response.parsed_body.length).to eq(0) + end + + it "returns the correct json representation of the organizations users", :aggregate_failures do + get api_organization_users_path(organization.username) + + response_org_users = response.parsed_body.first + + expect(response_org_users["type_of"]).to eq("user") + + %w[ + id username name summary twitter_username github_username website_url location + ].each do |attr| + expect(response_org_users[attr]).to eq(org_user.public_send(attr)) + end + + expect(response_org_users["joined_at"]).to eq(org_user.created_at.strftime("%b %e, %Y")) + expect(response_org_users["profile_image"]).to eq(Images::Profile.call(org_user.profile_image_url, length: 320)) + end + end end