[deploy] API - New endpoint to retrieve the articles in the reading list of the authenticated user (#10540)

This commit is contained in:
Jean-Francis Bastien 2020-10-14 14:20:03 -04:00 committed by GitHub
parent b5f20c0a46
commit 9ff7f82664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 299 additions and 110 deletions

View file

@ -18,7 +18,6 @@ module Api
public_reactions_count created_at edited_at last_comment_at published
updated_at video_thumbnail_url
].freeze
private_constant :INDEX_ATTRIBUTES_FOR_SERIALIZATION
SHOW_ATTRIBUTES_FOR_SERIALIZATION = [
*INDEX_ATTRIBUTES_FOR_SERIALIZATION, :body_markdown, :processed_html

View file

@ -0,0 +1,45 @@
module Api
module V0
class ReadinglistController < ApiController
before_action :authenticate!
before_action -> { doorkeeper_authorize! :public }, only: %w[index], if: -> { doorkeeper_token }
INDEX_REACTIONS_ATTRIBUTES_FOR_SERIALIZATION = %i[id reactable_id created_at status].freeze
private_constant :INDEX_REACTIONS_ATTRIBUTES_FOR_SERIALIZATION
PER_PAGE_MAX = 100
private_constant :PER_PAGE_MAX
def index
per_page = (params[:per_page] || 30).to_i
num = [per_page, PER_PAGE_MAX].min
@readinglist = Reaction
.select(INDEX_REACTIONS_ATTRIBUTES_FOR_SERIALIZATION)
.readinglist
.where(user_id: @user.id)
.where.not(status: "archived")
.order(created_at: :desc)
.page(params[:page])
.per(num)
articles = Article
.includes(:organization)
.select(ArticlesController::INDEX_ATTRIBUTES_FOR_SERIALIZATION)
.where(id: @readinglist.map(&:reactable_id))
.decorate
@users_by_id = User
.select(UsersController::SHOW_ATTRIBUTES_FOR_SERIALIZATION)
.find(articles.map(&:user_id))
.index_by(&:id)
articles_by_id = articles.index_by(&:id)
@articles_by_reaction_ids = @readinglist.each_with_object({}) do |reaction, result|
result[reaction.id] = articles_by_id[reaction.reactable_id]
end
end
end
end
end

View file

@ -8,7 +8,6 @@ module Api
id username name summary twitter_username github_username website_url
location created_at profile_image registered
].freeze
private_constant :SHOW_ATTRIBUTES_FOR_SERIALIZATION
def show
relation = User.select(SHOW_ATTRIBUTES_FOR_SERIALIZATION)

View file

@ -0,0 +1,14 @@
json.array! @readinglist do |reaction|
json.type_of "readinglist"
json.extract!(reaction, :id, :status)
json.created_at utc_iso_timestamp(reaction.created_at)
json.article do
article = @articles_by_reaction_ids[reaction.id]
json.partial! "api/v0/articles/article", article: article
json.tags article.cached_tag_list
json.partial! "api/v0/shared/user", user: @users_by_id[article.user_id]
if article.organization
json.partial! "api/v0/shared/organization", organization: article.organization
end
end
end

View file

@ -176,6 +176,7 @@ Rails.application.routes.draw do
get :users
get :organizations
end
resources :readinglist, only: [:index]
resources :webhooks, only: %i[index create show destroy]
resources :listings, only: %i[index show create update]

View file

@ -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.8.1"
version: "0.9.0"
termsOfService: https://dev.to/terms
contact:
name: DEV Team
@ -77,6 +77,17 @@ components:
minimum: 1
maximum: 1000
default: 30
perPageParam30to100:
in: query
name: per_page
required: false
description: Page size (the number of items to return per page).
schema:
type: integer
format: int32
minimum: 1
maximum: 100
default: 30
perPageParam80to1000:
in: query
name: per_page
@ -829,6 +840,34 @@ components:
type: string
enum: [bump, publish, unpublish]
ReadingList:
type: object
required:
- type_of
- id
- status
- created_at
- article
properties:
type_of:
type: string
id:
description: Follow id
type: integer
format: int32
status:
type: string
enum:
- valid
- invalid
- confirmed
- archived
created_at:
type: string
format: date-time
article:
$ref: "#/components/schemas/ArticleIndex"
PodcastEpisode:
type: object
required:
@ -1088,10 +1127,10 @@ components:
readable_publish_date: Oct 24
social_image: https://res.cloudinary.com/practicaldev/image/fetch/s--SeMxdKIa--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://res.cloudinary.com/practicaldev/image/fetch/s--xU8cbIK4--/c_imagga_scale%2Cf_auto%2Cfl_progressive%2Ch_420%2Cq_auto%2Cw_1000/https://thepracticaldev.s3.amazonaws.com/i/8a39dzf3oovzc2snl7iv.png
tag_list:
- meta
- changelog
- css
- ux
- meta
- changelog
- css
- ux
tags: meta, changelog, css, ux
slug: there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk
path: "/devteam/there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk"
@ -1259,25 +1298,25 @@ components:
profile_image: https://res.cloudinary.com/...png
profile_image_90: https://res.cloudinary.com/...png
children:
- type_of: comment
id_code: m35m
created_at: 2020-08-01T11:59:40Z
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
created_at: 2020-08-01T11:59:40Z
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
CommentsDeleted:
value:
- type_of: comment
@ -1285,24 +1324,24 @@ components:
body_html: <p>[deleted]</p>
user: {}
children:
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
CommentsHidden:
value:
- type_of: comment
@ -1310,24 +1349,24 @@ components:
body_html: <p>[hidden by post author]</p>
user: {}
children:
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/practicaldev/image/fetch/s--SC90PuMi--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/2693/146201.jpeg
children: []
Comment:
value:
type_of: comment
@ -1350,25 +1389,25 @@ components:
profile_image: https://res.cloudinary.com/...png
profile_image_90: https://res.cloudinary.com/...png
children:
- type_of: comment
id_code: m35m
created_at: 2020-07-02T17:19:40Z
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
created_at: 2020-07-02T17:19:40Z
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/....jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/....jpeg
children: []
CommentDeleted:
value:
type_of: comment
@ -1376,24 +1415,24 @@ components:
body_html: <p>[deleted]</p>
user: {}
children:
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/...jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/...jpeg
children: []
CommentHidden:
value:
type_of: comment
@ -1401,24 +1440,24 @@ components:
body_html: <p>[hidden by post author]</p>
user: {}
children:
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
- type_of: comment
id_code: m35m
body_html: |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>...</p>
<p>...</p>
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/...jpeg
children: []
</body></html>
user:
name: rhymes
username: rhymes
twitter_username:
github_username:
website_url:
profile_image: https://res.cloudinary.com/...jpeg
profile_image_90: https://res.cloudinary.com/...jpeg
children: []
Followers:
value:
@ -1447,7 +1486,7 @@ components:
us for our very first trip to Detroit MI! \n\nhttps://bit.ly/TBDetroit"
tag_list: events
tags:
- events
- events
category: cfp
processed_html: |
<p>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! </p>
@ -1474,7 +1513,7 @@ components:
us for our very first trip to Detroit MI! \n\nhttps://bit.ly/TBDetroit"
tag_list: events
tags:
- events
- events
category: cfp
processed_html: |
<p>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! </p>
@ -1496,7 +1535,7 @@ components:
body_markdown: Awesome conference
category: cfp
tags:
- events
- events
ListingCreateOrganization:
value:
listing:
@ -1504,7 +1543,7 @@ components:
body_markdown: Awesome conference
category: cfp
tags:
- events
- events
organization_id: 1
ListingUpdate:
value:
@ -1616,6 +1655,8 @@ tags:
description: Listings are classified ads
- name: podcast-episodes
description: Podcast episodes
- name: readinglist
description: User's reading list
- name: tags
description: Tags for articles
- name: users
@ -2808,6 +2849,50 @@ paths:
-d '{"listing":{"title":"Title"}' \
https://dev.to/api/listings/{id}
/readinglist:
get:
operationId: getReadinglist
summary: User's reading list
description: |
This endpoint allows the client to retrieve a list of readinglist reactions along with the related article for the authenticated user.
Reading list will be in reverse chronological order base
on the creation of the reaction.
It will return paginated reading list items along with the articles
they refer to. By default a page will contain `30` items
tags:
- readinglist
parameters:
- $ref: '#/components/parameters/pageParam'
- $ref: '#/components/parameters/perPageParam30to100'
responses:
"200":
description: The reading list with a overwiew of the article
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ReadingList"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
error-unauthorized:
$ref: "#/components/examples/ErrorUnauthorized"
security:
- api_key: []
- oauth2: []
x-code-samples:
- lang: Shell
label: curl
source: |
curl -H "api-key: API_KEY" https://dev.to/api/readinglist
/podcast_episodes:
get:
operationId: getPodcastEpisodes

View file

@ -481,7 +481,7 @@ RSpec.describe "Api::V0::Articles", type: :request do
expect(response).to have_http_status(:ok)
end
it "returns success when requesting publiched articles with public token" do
it "returns success when requesting published articles with public token" do
public_token = create(:doorkeeper_access_token, resource_owner: user, scopes: "public")
get me_api_articles_path(status: :published), params: { access_token: public_token.token }
expect(response.media_type).to eq("application/json")

View file

@ -0,0 +1,46 @@
require "rails_helper"
RSpec.describe "Api::V0::ReadingList", type: :request do
describe "GET /api/readinglist" do
let(:readinglist) { create_list(:reading_reaction, 3, user: user) }
context "when request is unauthenticated" do
it "return unauthorized" do
get api_readinglist_index_path
expect(response).to have_http_status(:unauthorized)
end
end
context "when request is authenticated" do
let(:access_token) { create :doorkeeper_access_token, resource_owner: user, scopes: "public read_articles" }
let(:user) { create(:user) }
it "works with bearer authorization" do
headers = { "authorization" => "Bearer #{access_token.token}", "content-type" => "application/json" }
get api_readinglist_index_path, headers: headers
expect(response.media_type).to eq("application/json")
expect(response).to have_http_status(:ok)
end
it "returns proper response specification" do
get api_readinglist_index_path, params: { access_token: access_token.token }
expect(response.media_type).to eq("application/json")
expect(response).to have_http_status(:ok)
end
it "doesn't return archived reactions" do
create(:reading_reaction, user: user)
create(:reading_reaction, user: user, status: :archived)
get api_readinglist_index_path, params: { access_token: access_token.token }
expect(response.parsed_body.length).to eq(1)
end
it "supports pagination" do
create_list(:reading_reaction, 3, user: user)
get api_readinglist_index_path, params: { access_token: access_token.token, page: 2, per_page: 2 }
expect(response.parsed_body.length).to eq(1)
end
end
end
end