From 6e2aab623e2960438f84dbe649d90cf7d99c3356 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 18 Dec 2020 09:20:35 -0800 Subject: [PATCH] API: Endpoint to get an organization's listings (#11823) * add tests * add route * add controller action * add jbuilder file * update api docs * update query * update jbuilder listing * add organization/user and category to listing * update api docs/spec file --- .../api/v0/organizations_controller.rb | 21 +++- app/views/api/v0/listings/index.json.jbuilder | 2 +- app/views/api/v0/listings/show.json.jbuilder | 2 +- .../v0/organizations/listings.json.jbuilder | 5 + .../_listing.json.jbuilder | 0 config/routes.rb | 1 + docs/api_v0.yml | 106 ++++++++++++++++-- spec/requests/api/v0/organizations_spec.rb | 45 ++++++++ 8 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 app/views/api/v0/organizations/listings.json.jbuilder rename app/views/api/v0/{listings => shared}/_listing.json.jbuilder (100%) diff --git a/app/controllers/api/v0/organizations_controller.rb b/app/controllers/api/v0/organizations_controller.rb index d43537a0e..bd8b4191f 100644 --- a/app/controllers/api/v0/organizations_controller.rb +++ b/app/controllers/api/v0/organizations_controller.rb @@ -1,7 +1,7 @@ module Api module V0 class OrganizationsController < ApiController - before_action :find_organization, only: %i[users] + before_action :find_organization, only: %i[users listings] SHOW_ATTRIBUTES_FOR_SERIALIZATION = %i[ username name summary twitter_username github_username url @@ -15,6 +15,12 @@ module Api ].freeze private_constant :USERS_FOR_SERIALIZATION + LISTINGS_FOR_SERIALIZATION = %i[ + id user_id organization_id title slug body_markdown cached_tag_list + classified_listing_category_id processed_html published + ].freeze + private_constant :LISTINGS_FOR_SERIALIZATION + def show @organization = Organization.select(SHOW_ATTRIBUTES_FOR_SERIALIZATION) .find_by!(username: params[:username]) @@ -28,6 +34,19 @@ module Api @users = @organization.users.select(USERS_FOR_SERIALIZATION).page(page).per(num) end + def listings + per_page = (params[:per_page] || 30).to_i + num = [per_page, 1000].min + page = params[:page] || 1 + + @listings = @organization.listings.published + .select(LISTINGS_FOR_SERIALIZATION).page(page).per(num) + .includes(:user, :taggings, :listing_category) + .order(bumped_at: :desc) + + @listings = @listings.in_category(params[:category]) if params[:category].present? + end + private def find_organization diff --git a/app/views/api/v0/listings/index.json.jbuilder b/app/views/api/v0/listings/index.json.jbuilder index 15f5781dd..aabf401af 100644 --- a/app/views/api/v0/listings/index.json.jbuilder +++ b/app/views/api/v0/listings/index.json.jbuilder @@ -1,5 +1,5 @@ json.array! @listings do |listing| - json.partial! "listing", listing: listing + json.partial! "api/v0/shared/listing", listing: listing json.partial! "api/v0/shared/user", user: listing.user if listing.organization diff --git a/app/views/api/v0/listings/show.json.jbuilder b/app/views/api/v0/listings/show.json.jbuilder index 593a41c8f..b3231d908 100644 --- a/app/views/api/v0/listings/show.json.jbuilder +++ b/app/views/api/v0/listings/show.json.jbuilder @@ -1,4 +1,4 @@ -json.partial! "listing", listing: @listing +json.partial! "api/v0/shared/listing", listing: @listing json.partial! "api/v0/shared/user", user: @listing.user diff --git a/app/views/api/v0/organizations/listings.json.jbuilder b/app/views/api/v0/organizations/listings.json.jbuilder new file mode 100644 index 000000000..0c4a48734 --- /dev/null +++ b/app/views/api/v0/organizations/listings.json.jbuilder @@ -0,0 +1,5 @@ +json.array! @listings do |listing| + json.partial! "api/v0/shared/listing", listing: listing + json.partial! "api/v0/shared/user", user: listing.user + json.partial! "api/v0/shared/organization", organization: @organization +end diff --git a/app/views/api/v0/listings/_listing.json.jbuilder b/app/views/api/v0/shared/_listing.json.jbuilder similarity index 100% rename from app/views/api/v0/listings/_listing.json.jbuilder rename to app/views/api/v0/shared/_listing.json.jbuilder diff --git a/config/routes.rb b/config/routes.rb index cd68928b5..6c871d13c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -201,6 +201,7 @@ Rails.application.routes.draw do resources :profile_images, only: %i[show], param: :username resources :organizations, only: [:show], param: :username do resources :users, only: [:index], to: "organizations#users" + resources :listings, only: [:index], to: "organizations#listings" end namespace :admin do diff --git a/docs/api_v0.yml b/docs/api_v0.yml index ecb91682b..97b4b96fd 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.3" + version: "0.9.4" termsOfService: https://dev.to/terms contact: name: DEV Team @@ -99,6 +99,15 @@ components: minimum: 1 maximum: 1000 default: 80 + listingCategoryParam: + name: category + in: query + description: | + Using this parameter will return listings belonging to the + requested category. + schema: + type: string + example: cfp securitySchemes: api_key: @@ -1580,6 +1589,39 @@ components: website_url: profile_image: https://res.cloudinary.com/practicaldev/image/fetch/s--ggU5WPaT--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--CjladMBD--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg + ListingsByOrganization: + value: + - type_of: listing + id: 1157 + title: TestBash Detroit + slug: testbash-detroit-50gb + body_markdown: "Do you want to learn about automation? Maybe you're interested in + AI-driven testing? Security testing? We have a selection of talks, workshops and + training courses to help you in a wide variety of areas in software testing. Join + us for our very first trip to Detroit MI! \n\nhttps://bit.ly/TBDetroit" + tag_list: events + tags: + - events + category: cfp + processed_html: | +

Do you want to learn about automation? Maybe you're interested in AI-driven testing? Security testing? We have a selection of talks, workshops and training courses to help you in a wide variety of areas in software testing. Join us for our very first trip to Detroit MI!

+ +

https://bit.ly/TBDetroit

+ published: true + user: + name: Heather + username: heatherr + twitter_username: + github_username: Heather-R + website_url: + profile_image: https://res.cloudinary.com/practicaldev/image/fetch/s--ggU5WPaT--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg + profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--CjladMBD--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg + organization: + name: E Corp + username: ecorp + slug: ecorp + profile_image: https://res.cloudinary.com/practicaldev/image/fetch/s--ggU5WPaT--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg + profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--CjladMBD--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/136256/11cced64-afd2-4421-91e1-c5f7b216d49b.jpeg Listing: value: type_of: listing @@ -2674,14 +2716,7 @@ paths: parameters: - $ref: '#/components/parameters/pageParam' - $ref: '#/components/parameters/perPageParam30to1000' - - name: category - in: query - description: | - Using this parameter will return listings belonging to the - requested category. - schema: - type: string - example: cfp + - $ref: '#/components/parameters/listingCategoryParam' responses: "200": description: A list of listings @@ -3128,6 +3163,59 @@ paths: source: | curl https://dev.to/api/organizations/ecorp/users + /organizations/{username}/listings: + get: + operationId: getOrgListings + summary: Organization's listings + description: | + This endpoint allows the client to retrieve a list of listings belonging to the organization + + It supports pagination, each page will contain `30` listing 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' + - $ref: '#/components/parameters/listingCategoryParam' + responses: + "200": + description: A list of listings belonging to the organization + content: + application/json: + schema: + type: array + items: + allOf: + - $ref: "#/components/schemas/Listing" + - required: + - organization + examples: + user-success: + $ref: "#/components/examples/ListingsByOrganization" + "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/listings + + /podcast_episodes: get: operationId: getPodcastEpisodes diff --git a/spec/requests/api/v0/organizations_spec.rb b/spec/requests/api/v0/organizations_spec.rb index f39a86ed0..32d67a13f 100644 --- a/spec/requests/api/v0/organizations_spec.rb +++ b/spec/requests/api/v0/organizations_spec.rb @@ -65,4 +65,49 @@ RSpec.describe "Api::V0::Organizations", type: :request do expect(response_org_users["profile_image"]).to eq(Images::Profile.call(org_user.profile_image_url, length: 320)) end end + + describe "GET /api/organizations/:username/listings" do + let(:org_user) { create(:user, :org_member) } + let(:organization) { org_user.organizations.first } + let!(:listing) { create(:listing, user: org_user, organization: organization) } + + it "returns 404 if the organizations username is not found" do + get "/api/organizations/invalid-username/listings" + expect(response).to have_http_status(:not_found) + end + + it "supports pagination" do + create(:listing, user: org_user, organization: organization) + + get api_organization_listings_path(organization.username), params: { page: 1, per_page: 1 } + expect(response.parsed_body.length).to eq(1) + + get api_organization_listings_path(organization.username), params: { page: 2, per_page: 1 } + expect(response.parsed_body.length).to eq(1) + + get api_organization_listings_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 listings", :aggregate_failures do + get api_organization_listings_path(organization.username) + response_listing = response.parsed_body.first + expect(response_listing["type_of"]).to eq("listing") + + %w[id title slug body_markdown category processed_html published listing_category_id].each do |attr| + expect(response_listing[attr]).to eq(listing.public_send(attr)) + end + + expect(response_listing["tag_list"]).to eq(listing.cached_tag_list) + expect(response_listing["tags"]).to match_array(listing.tag_list) + + %w[name username twitter_username github_username website_url].each do |attr| + expect(response_listing["user"][attr]).to eq(org_user.public_send(attr)) + end + + %w[name username slug].each do |attr| + expect(response_listing["organization"][attr]).to eq(organization.public_send(attr)) + end + end + end end