Improve API docs for articles, add listings to it and fix bugs (#6413) [deploy]

* Fix articles index state param values

* Update Articles API docs

* Fix ClassifiedListing.cost_by_category with invalid category

* Add docs for GET|POST /listings and fix bugs

* Document PUT /api/listings/:id

* Add docs for /listings/category/{category}

* One variable down

* Improve listings update error handling
This commit is contained in:
rhymes 2020-03-04 15:30:02 +01:00 committed by GitHub
parent 6d7cbfff1f
commit f3e76e7fe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 812 additions and 89 deletions

View file

@ -17,6 +17,10 @@ class Api::V0::ApiController < ApplicationController
error_not_found
end
rescue_from Pundit::NotAuthorizedError do |_exc|
error_unauthorized
end
protected
def error_unprocessable_entity(message)

View file

@ -79,6 +79,8 @@ module Api
render json: { errors: @classified_listing.errors }, status: :unprocessable_entity
end
alias process_unsuccessful_update process_unsuccessful_creation
def process_after_update
render "show", status: :ok
end

View file

@ -92,6 +92,10 @@ class ClassifiedListingsController < ApplicationController
render :new
end
def process_unsuccessful_update
render :edit
end
def process_after_update
redirect_to "/listings"
end

View file

@ -41,7 +41,7 @@ module ClassifiedListingsToolkit
def create
@classified_listing = ClassifiedListing.new(listing_params)
# this will 500 for now if they don't belong in the org
# this will 401 for now if they don't belong in the org
authorize @classified_listing, :authorized_organization_poster? if @classified_listing.organization_id.present?
@classified_listing.user_id = current_user.id
@ -141,7 +141,8 @@ module ClassifiedListingsToolkit
publish_listing
elsif listing_updatable?
update_listing_details
saved = update_listing_details
return process_unsuccessful_update unless saved
end
clear_listings_cache

View file

@ -44,8 +44,8 @@ class ClassifiedListing < ApplicationRecord
scope :published, -> { where(published: true) }
def self.cost_by_category(category = "education")
categories_available[category][:cost]
def self.cost_by_category(category)
categories_available.dig(category, :cost) || 0
end
def author

View file

@ -4,16 +4,8 @@
<h2>Create a Community Listing</h2>
</header>
<div class="classified-form-inner">
<% if classified_listing.errors.any? %>
<div class="classified-errors">
<h2><%= pluralize(classified_listing.errors.count, "error") %> prohibited this listing from being saved:</h2>
<ul>
<% classified_listing.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= render partial: "form_errors", locals: { classified_listing: classified_listing } %>
<div id="listingform-data"
data-listing="<%= classified_listing.to_json(only: %i[id title body_markdown category cached_tag_list]) %>"
data-organizations="<%= @organizations.to_json(only: %i[id name]) %>"

View file

@ -0,0 +1,10 @@
<% if classified_listing.errors.any? %>
<div class="classified-errors">
<h2><%= pluralize(classified_listing.errors.count, "error") %> prohibited this listing from being saved:</h2>
<ul>
<% classified_listing.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

View file

@ -7,6 +7,8 @@
<h2>You can only edit title/body/tags of drafts or within the first 24 hours of listing or bumping</h2>
</header>
<div class="classified-form-inner">
<%= render partial: "form_errors", locals: { classified_listing: @classified_listing } %>
<% if (@classified_listing.bumped_at && @classified_listing.bumped_at > 24.hours.ago) || @classified_listing.updated_at && !@classified_listing.published %>
<div class="field">
<%= f.label "title", "Title" %>

View file

@ -5,7 +5,10 @@ info:
Access DEV articles, users and other resources via API.
All endpoints that don't require authentication are CORS enabled.
version: "0.6.0"
Dates and date times, unless otherwise specified, must be in
the [RFC 3339](https://tools.ietf.org/html/rfc3339) format.
version: "0.6.1"
termsOfService: https://dev.to/terms
contact:
name: DEV Team
@ -158,9 +161,9 @@ components:
type: string
format: date-time
user:
$ref: "#/components/schemas/ArticleUser"
$ref: "#/components/schemas/SharedUser"
organization:
$ref: "#/components/schemas/ArticleOrganization"
$ref: "#/components/schemas/SharedOrganization"
flare_tag:
$ref: "#/components/schemas/ArticleFlareTag"
@ -256,9 +259,9 @@ components:
body_markdown:
type: string
user:
$ref: "#/components/schemas/ArticleUser"
$ref: "#/components/schemas/SharedUser"
organization:
$ref: "#/components/schemas/ArticleOrganization"
$ref: "#/components/schemas/SharedOrganization"
flare_tag:
$ref: "#/components/schemas/ArticleFlareTag"
@ -462,14 +465,185 @@ components:
type: string
format: date-time
user:
$ref: "#/components/schemas/ArticleUser"
$ref: "#/components/schemas/SharedUser"
organization:
$ref: "#/components/schemas/ArticleOrganization"
$ref: "#/components/schemas/SharedOrganization"
flare_tag:
$ref: "#/components/schemas/ArticleFlareTag"
ArticleUser:
description: The article's creator
ArticleFlareTag:
description: Flare tag of the article
type: object
properties:
name:
type: string
bg_color_hex:
description: Background color (hexadecimal)
type: string
text_color_hex:
description: Text color (hexadecimal)
type: string
Listing:
type: object
required:
- type_of
- id
- title
- slug
- body_markdown
- tag_list
- tags
- category
- processed_html
- published
- user
properties:
type_of:
type: string
id:
type: integer
format: int32
title:
type: string
slug:
type: string
body_markdown:
type: string
tag_list:
type: string
tags:
type: array
items:
type: string
category:
$ref: "#/components/schemas/ListingCategory"
processed_html:
type: string
published:
type: boolean
user:
$ref: "#/components/schemas/SharedUser"
organization:
$ref: "#/components/schemas/SharedOrganization"
ListingCategory:
type: string
enum: [cfp, forhire, collabs, education, jobs, mentors, products, mentees, forsale, events, misc]
ListingCreate:
type: object
properties:
classified_listing:
type: object
required:
- title
- body_markdown
- category
properties:
title:
type: string
body_markdown:
description: The body of the listing in Markdown format.
type: string
category:
$ref: "#/components/schemas/ListingCategory"
tags:
description: |
Tags related to the listing.
A maximum of 8 tags are allowed and it takes precedence over `tag_list`.
type: array
items:
type: string
tag_list:
description: |
Comma separated list of tags.
A maximum of 8 tags are allowed.
type: string
expires_at:
description: Date and time of expiration.
type: date
format: date-time
contact_via_connect:
description: |
True if users are allowed to contact the listing's owner
via DEV connect, false otherwise.
Defaults to false.
type: boolean
location:
description: Geographical area or city for the listing.
type: string
organization_id:
description: |
The id of the organization the user is creating the listing for.
Only users belonging to an organization can assign the listing to it.
type: integer
format: int32
action:
description: Set it to "draft" to create an unpublished listing
type: string
enum: [draft]
ListingUpdate:
type: object
properties:
classified_listing:
type: object
properties:
title:
type: string
body_markdown:
description: The body of the listing in Markdown format.
type: string
category:
$ref: "#/components/schemas/ListingCategory"
tags:
description: |
Tags related to the listing.
A maximum of 8 tags are allowed and it takes precedence over `tag_list`.
type: array
items:
type: string
tag_list:
description: |
Comma separated list of tags.
A maximum of 8 tags are allowed.
type: string
expires_at:
description: Date and time of expiration.
type: date
format: date-time
contact_via_connect:
description: |
True if users are allowed to contact the listing's owner
via DEV connect, false otherwise.
Defaults to false.
type: boolean
location:
description: Geographical area or city for the listing.
type: string
action:
description: |
This param can be provided by itself to invoke some actions:
- `bump` bumps the listing and charge either the
organization or the user
- `publish` publishes a draft listing
- `unpublish` unpublishes a published listing
It will take priority on any other param in the payload.
type: string
enum: [bump, publish, unpublish]
SharedUser:
description: The resource creator
type: object
properties:
name:
@ -490,8 +664,8 @@ components:
description: Profile image (90x90)
type: string
ArticleOrganization:
description: The organization the article belongs to
SharedOrganization:
description: The organization the resource belongs to
type: object
properties:
name:
@ -509,19 +683,6 @@ components:
type: string
format: url
ArticleFlareTag:
description: Flare tag of the article
type: object
properties:
name:
type: string
bg_color_hex:
description: Background color (hexadecimal)
type: string
text_color_hex:
description: Text color (hexadecimal)
type: string
WebhookCreate:
description: Webhook creation payload
type: object
@ -569,7 +730,7 @@ components:
type: string
format: date-time
user:
$ref: "#/components/schemas/ArticleUser"
$ref: "#/components/schemas/SharedUser"
WebhookIndex:
description: Webhook
@ -608,10 +769,15 @@ components:
value:
error: unauthorized
status: 401
ErrorPaymentRequired:
value:
error: not enough available credits
status: 402
ErrorUnprocessableEntity:
value:
error: "param is missing or the value is empty: article"
status: 422
ArticlesIndex:
value:
- type_of: article
@ -742,6 +908,96 @@ components:
series: Hello series
canonical_url: https://example.com/blog/hello
organization_id: 1234
Listings:
value:
- type_of: classified_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: |
<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>
<p><a href="https://bit.ly/TBDetroit">https://bit.ly/TBDetroit</a></p>
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
Listing:
value:
type_of: classified_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: |
<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>
<p><a href="https://bit.ly/TBDetroit">https://bit.ly/TBDetroit</a></p>
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
ListingCreate:
value:
classified_listing:
title: ACME Conference
body_markdown: Awesome conference
category: cfp
tags:
- events
ListingCreateOrganization:
value:
classified_listing:
title: ACME Conference
body_markdown: Awesome conference
category: cfp
tags:
- events
organization_id: 1
ListingUpdate:
value:
classified_listing:
title: ACME New Conference
body_markdown: Awesome new conference
ListingUpdateActionBump:
value:
classified_listing:
action: bump
ListingUpdateActionPublish:
value:
classified_listing:
action: publish
ListingUpdateActionUnpublish:
value:
classified_listing:
action: unpublish
WebhookCreate:
value:
webhook_endpoint:
@ -770,6 +1026,8 @@ components:
tags:
- name: articles
description: Articles are all the posts users create on DEV
- name: listings
description: Listings are classified ads
- name: users
description: Users own resources that require authentication
- name: webhooks
@ -790,71 +1048,85 @@ paths:
By default it will return featured, published articles ordered
by descending popularity.
Each page will contain `30` articles.
Responses, according to the combination of params, are cached for 24 hours.
It supports pagination, each page will contain `30` articles by default.
tags:
- articles
parameters:
- name: page
in: query
description: |
Pagination page.
This param can be used in conjuction with all other params
(except when asking for fresh and rising articles by themselves).
description: Pagination page.
schema:
type: integer
format: int32
example: 1
- name: per_page
in: query
description: Page size (defaults to 30 with a maximum of 1000).
schema:
type: integer
format: int32
example: 30
- name: tag
in: query
description: |
Adding this parameter will return articles that contain the
Using this parameter will retrieve articles that contain the
requested tag.
This param can be used by itself, with `page` or with `top`.
Articles will be ordered by descending popularity.
This parameter can be used in conjuction with `top`.
schema:
type: string
example: discuss
- name: username
in: query
description: |
Adding this parameter will return articles belonging to a User or Organization
ordered by descending `published_at`.
Using this parameter will retrieve articles belonging
to a User or Organization ordered by descending publication date.
If `state=all` the number of items returned will be `1000` instead of the default `30`.
This param can be used by itself or only with `page` and `state`.
This parameter can be used in conjuction with `state`.
schema:
type: string
example: ben
- name: state
in: query
description: |
Adding this will allow the client to check which articles are fresh or rising.
Using this parameter will allow the client to check which articles are fresh or rising.
If `state=fresh` the server will return published fresh articles.
If `state=rising` the server will return published rising articles.
If `state=fresh` the server will return fresh articles.
If `state=rising` the server will return rising articles.
This param can only be used by itself or with `username` if set to `all`.
This param can be used in conjuction with `username`, only if set to `all`.
schema:
type: string
example: ben
enum: [fresh, rising, all]
example: fresh
- name: top
in: query
description: |
Adding this will allow the client to return the most popular articles
Using this parameter will allow the client to return the most popular articles
in the last `N` days.
`top` indicates the number of days since publication of the articles
returned.
This param can only be used by itself or with `tag`.
This param can be used in conjuction with `tag`.
schema:
type: integer
format: int32
example: 2
- name: collection_id
in: query
description: |
Adding this will allow the client to return the list of articles
belonging to the requested collection, ordered by ascending
publication date.
schema:
type: integer
format: int32
example: 99
responses:
200:
description: A list of articles
@ -978,8 +1250,6 @@ paths:
description: |
This endpoint allows the client to retrieve a single
published article given its `id`.
Responses are cached for 5 minutes.
tags:
- articles
parameters:
@ -1019,7 +1289,7 @@ paths:
operationId: updateArticle
summary: Update an article
description: |
This endpoint allows the client to updated an existing article.
This endpoint allows the client to update an existing article.
"Articles" are all the posts that users create on DEV that typically
show up in the feed. They can be a blog post, a discussion question,
@ -1045,7 +1315,7 @@ paths:
example: 150589
requestBody:
description: |
Article params to update.
Article params for the update.
*Note: if the article contains a front matter in its body, its front
matter properties will still take precedence over any JSON equivalent
@ -1325,6 +1595,373 @@ paths:
source: |
curl -H "api-key: API_KEY" https://dev.to/api/articles/me/all
/listings:
get:
operationId: getListings
summary: Published listings
description: |
This endpoint allows the client to retrieve a list of listings.
"Listings" are classified ads that users create on DEV.
They can be related to conference announcements, job offers, mentorships,
upcoming events and more.
By default it will return published listings ordered by descending
freshness.
It supports pagination, each page will contain `30` articles by default.
tags:
- listings
parameters:
- name: page
in: query
description: Pagination page.
schema:
type: integer
format: int32
example: 1
- name: per_page
in: query
description: Page size (defaults to 30 with a maximum of 100).
schema:
type: integer
format: int32
example: 30
- name: category
in: query
description: |
Using this parameter will return listings belonging to the
requested category.
schema:
type: string
example: cfp
responses:
200:
description: A list of listings
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Listing"
examples:
listings-success:
$ref: "#/components/examples/Listings"
x-code-samples: # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples
- lang: Shell
label: curl (all listings)
source: |
curl https://dev.to/api/listings
post:
operationId: createListing
summary: Create a new listing
description: |
This endpoint allows the client to create a new listing.
"Listings" are classified ads that users create on DEV.
They can be related to conference announcements, job offers, mentorships,
upcoming events and more.
The user creating the listing or the organization on which behalf the user
is creating for need to have enough creadits for this operation to be
successful. The server will prioritize the organization's credits over
the user's credits.
### Additional resources
- [Rails tests for Listings API](https://github.com/thepracticaldev/dev.to/blob/master/spec/requests/api/v0/classified_listings_spec.rb)
tags:
- listings
requestBody:
description: Listing to create
content:
application/json:
schema:
$ref: "#/components/schemas/ListingCreate"
examples:
listing-create:
$ref: "#/components/examples/ListingCreate"
listing-create-organization:
$ref: "#/components/examples/ListingCreateOrganization"
responses:
201:
description: A newly created Listing
content:
application/json:
schema:
$ref: "#/components/schemas/Listing"
examples:
article-success:
$ref: "#/components/examples/Listing"
400:
description: BadRequest
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
error-bad-request:
$ref: "#/components/examples/ErrorBadRequest"
401:
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
error-unauthorized:
$ref: "#/components/examples/ErrorUnauthorized"
402:
description: PaymentRequired
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
error-unauthorized:
$ref: "#/components/examples/ErrorPaymentRequired"
422:
description: Unprocessable Entity
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
error-unprocessable-entity:
$ref: "#/components/examples/ErrorUnprocessableEntity"
security:
- api_key: []
x-code-samples: # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples
- lang: Shell
label: curl
source: |
curl -X POST -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"classified_listing":{"title":"Title","body_markdown":"Body","category":"cfp"}}' \
https://dev.to/api/listings
- lang: Shell
label: curl (with tags)
source: |
curl -X POST -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"classified_listing":{"title":"Title","body_markdown":"Body","category":"cfp","tags":["python"]}}' \
https://dev.to/api/listings
- lang: Shell
label: curl (draft)
source: |
curl -X POST -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"classified_listing":{"title":"Title","body_markdown":"Body","category":"cfp","action":"draft"}}' \
https://dev.to/api/listings
/listings/category/{category}:
get:
operationId: getListingsByCategory
summary: Published listings by category
description: |
This endpoint allows the client to retrieve a list of listings belonging
to the specified category.
"Listings" are classified ads that users create on DEV.
They can be related to conference announcements, job offers, mentorships,
upcoming events and more.
By default it will return published listings ordered by descending
freshness.
It supports pagination, each page will contain `30` articles by default.
tags:
- listings
parameters:
- name: category
in: path
required: true
description: The category of the listing
schema:
$ref: "#/components/schemas/ListingCategory"
- name: page
in: query
description: Pagination page.
schema:
type: integer
format: int32
example: 1
- name: per_page
in: query
description: Page size (defaults to 30 with a maximum of 100).
schema:
type: integer
format: int32
example: 30
responses:
200:
description: A list of listings
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Listing"
examples:
listings-success:
$ref: "#/components/examples/Listings"
x-code-samples: # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples
- lang: Shell
label: curl (call for papers listings)
source: |
curl https://dev.to/api/listings/category/cfp
/listings/{id}:
get:
operationId: getListingById
summary: A listing
description: |
This endpoint allows the client to retrieve
a single listing given its `id`.
tags:
- listings
parameters:
- name: id
in: path
required: true
description: Id of the listing
schema:
type: integer
format: int32
example: 1
responses:
200:
description: A listing
content:
application/json:
schema:
$ref: "#/components/schemas/Listing"
examples:
article-success:
$ref: "#/components/examples/Listing"
404:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/APIError"
examples:
article-not-found:
$ref: "#/components/examples/ErrorNotFound"
x-code-samples: # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples
- lang: Shell
label: curl
source: |
curl https://dev.to/api/listings/1184
put:
operationId: updateListing
summary: Update a listing
description: |
This endpoint allows the client to update an existing listing.
tags:
- listings
parameters:
- name: id
in: path
required: true
description: Id of the listing
schema:
type: integer
format: int32
example: 1184
requestBody:
description: |
Listing params for the update.
**Note**: except for bumping, publishing and unpublishing there are the
following restrictions on the ability to update listings:
- the payload has to contain at least one param among `title`, `body_markdown` or `tags`/`tag_list`
- the listing can't be updated if it has not been bumped in the last 24 hours
- the listing can't be updated if it has been published but not recently bumped
content:
application/json:
schema:
$ref: "#/components/schemas/ListingUpdate"
examples:
listing-update-action-bump:
$ref: "#/components/examples/ListingUpdateActionBump"
listing-update-action-publish:
$ref: "#/components/examples/ListingUpdateActionPublish"
listing-update-action-unpublish:
$ref: "#/components/examples/ListingUpdateActionUnpublish"
listing-update:
$ref: "#/components/examples/ListingUpdate"
responses:
200:
description: The updated article
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/ArticleShow"
examples:
article-success:
$ref: "#/components/examples/ArticleShow"
400:
description: BadRequest
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/APIError"
examples:
error-bad-request:
$ref: "#/components/examples/ErrorBadRequest"
401:
description: Unauthorized
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/APIError"
examples:
error-unauthorized:
$ref: "#/components/examples/ErrorUnauthorized"
402:
description: PaymentRequired
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/APIError"
examples:
error-unauthorized:
$ref: "#/components/examples/ErrorPaymentRequired"
422:
description: Unprocessable Entity
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/APIError"
examples:
error-unprocessable-entity:
$ref: "#/components/examples/ErrorUnprocessableEntity"
security:
- api_key: []
x-code-samples: # https://github.com/Redocly/redoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples
- lang: Shell
label: curl (bump)
source: |
curl -X PUT -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"classified_listing":{"action":"bump"}' \
https://dev.to/api/listings/{id}
- lang: Shell
label: curl (update)
source: |
curl -X PUT -H "Content-Type: application/json" \
-H "api-key: API_KEY" \
-d '{"classified_listing":{"title":"Title"}' \
https://dev.to/api/listings/{id}
/webhooks:
get:
operationId: getWebhooks

View file

@ -4,8 +4,8 @@ title: Contributing to the API Specification Docs
# Contributing to API spec docs
The API docs follow the [OpenAPI 3
specification](http://spec.openapis.org/oas/v3.0.2).
The API docs follow the
[OpenAPI 3 specification](https://spec.openapis.org/oas/v3.0.3).
## Where the docs are located
@ -32,7 +32,5 @@ will reload the doc after every modification of the spec file.
If you have Visual Studio Code, we suggest you install the following extensions
that enable validation and navigation within the spec file:
- [OpenAPI (Swagger)
editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi)
- [openapi-designer live
preview](https://marketplace.visualstudio.com/items?itemName=philosowaffle.openapi-designer)
- [OpenAPI (Swagger) editor](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi)
- [openapi-designer live preview](https://marketplace.visualstudio.com/items?itemName=philosowaffle.openapi-designer)

View file

@ -123,4 +123,15 @@ RSpec.describe ClassifiedListing, type: :model do
expect(Search::ClassifiedListing).to have_received(:find_document)
end
end
describe ".cost_by_category" do
it "returns the cost per category" do
expected_cost = described_class::CATEGORIES_AVAILABLE.dig("cfp", "cost")
expect(described_class.cost_by_category("cfp")).to eq(expected_cost)
end
it "returns 0 with invalid category" do
expect(described_class.cost_by_category("invalid")).to eq(0)
end
end
end

View file

@ -12,9 +12,7 @@ RSpec.describe "Api::V0::Listings" do
{
title: "Title",
body_markdown: "Markdown text",
category: "cfp",
tags: [],
contact_via_connect: true
category: "cfp"
}
end
let(:draft_params) do
@ -22,8 +20,6 @@ RSpec.describe "Api::V0::Listings" do
title: "Title draft",
body_markdown: "Markdown draft text",
category: "cfp",
tags: [],
contact_via_connect: true,
action: "draft"
}
end
@ -141,7 +137,7 @@ RSpec.describe "Api::V0::Listings" do
org
end
describe "user cannot proceed if not properly unauthorizedœ" do
describe "user cannot proceed if not properly unauthorized" do
let(:api_secret) { create(:api_secret) }
it "fails with no api key" do
@ -181,11 +177,22 @@ RSpec.describe "Api::V0::Listings" do
expect(response).to have_http_status(:unprocessable_entity)
end
it "fails if a mandatory param is missing" do
it "fails if body_markdown is missing" do
post_classified_listing(invalid_params)
expect(response).to have_http_status(:unprocessable_entity)
end
it "fails if category is missing" do
post_classified_listing(title: "Title", body_markdown: "body")
expect(response).to have_http_status(:unprocessable_entity)
end
it "fails if category is invalid" do
post_classified_listing(title: "Title", body_markdown: "body", category: "unknown")
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body.dig("errors", "category").first).to match(/not a valid category/)
end
it "does not subtract credits or create a listing if the listing is not valid" do
expect do
post_classified_listing(invalid_params)
@ -200,6 +207,8 @@ RSpec.describe "Api::V0::Listings" do
it "properly deducts the amount of credits" do
post_classified_listing(listing_params)
expect(response).to have_http_status(:created)
listing_cost = ClassifiedListing.categories_available[:cfp][:cost]
expect(user.credits.spent.size).to eq(listing_cost)
end
@ -221,12 +230,18 @@ RSpec.describe "Api::V0::Listings" do
it "does not create a listing draft for an org not belonging to the user" do
org = create(:organization)
expect { post_classified_listing(draft_params.merge(organization_id: org.id)) }.to raise_error(Pundit::NotAuthorizedError)
expect do
post_classified_listing(draft_params.merge(organization_id: org.id))
expect(response).to have_http_status(:unauthorized)
end.to change(ClassifiedListing, :count).by(0)
end
it "does not create a listing for an org not belonging to the user" do
org = create(:organization)
expect { post_classified_listing(listing_params.merge(organization_id: org.id)) }.to raise_error(Pundit::NotAuthorizedError)
expect do
post_classified_listing(listing_params.merge(organization_id: org.id))
expect(response).to have_http_status(:unauthorized)
end.to change(ClassifiedListing, :count).by(0)
end
it "assigns the spent credits to the listing" do
@ -278,9 +293,12 @@ RSpec.describe "Api::V0::Listings" do
post_classified_listing(listing_params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(response.parsed_body["id"]).title).to eq("Title")
expect(ClassifiedListing.find(response.parsed_body["id"]).body_markdown).to eq("Markdown text")
expect(ClassifiedListing.find(response.parsed_body["id"]).category).to eq("cfp")
listing = ClassifiedListing.find(response.parsed_body["id"])
expect(listing.title).to eq(listing_params[:title])
expect(listing.body_markdown).to eq(listing_params[:body_markdown])
expect(listing.category).to eq(listing_params[:category])
end
it "creates a classified listing with a location" do
@ -298,8 +316,11 @@ RSpec.describe "Api::V0::Listings" do
post_classified_listing(params)
expect(response).to have_http_status(:created)
end.to change(ClassifiedListing, :count).by(1)
expect(ClassifiedListing.find(response.parsed_body["id"]).cached_tag_list).to eq("discuss, javascript")
expect(ClassifiedListing.find(response.parsed_body["id"]).contact_via_connect).to eq(true)
listing = ClassifiedListing.find(response.parsed_body["id"])
expect(listing.cached_tag_list).to eq("discuss, javascript")
expect(listing.contact_via_connect).to be(true)
end
end
@ -325,7 +346,7 @@ RSpec.describe "Api::V0::Listings" do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let(:listing) { create(:classified_listing, user_id: user.id) }
let!(:listing) { create(:classified_listing, user: user) }
let(:another_user_listing) { create(:classified_listing, user_id: another_user.id) }
let(:listing_draft) { create(:classified_listing, user: user) }
let(:organization) { create(:organization) }
@ -337,7 +358,7 @@ RSpec.describe "Api::V0::Listings" do
org_listing_draft.update_columns(bumped_at: nil, published: false)
end
describe "user cannot proceed if not properly unauthorizedœ" do
describe "user cannot proceed if not properly unauthorized" do
let(:api_secret) { create(:api_secret) }
it "fails with no api key" do
@ -481,11 +502,17 @@ RSpec.describe "Api::V0::Listings" do
include_context "when user is authorized"
include_context "when user has enough credit"
it "returns HTTP 422 if no params given" do
it "fails if no params have been given" do
put_classified_listing(listing.id)
expect(response).to have_http_status(:unprocessable_entity)
end
it "fails if category is invalid" do
put_classified_listing(listing.id, title: "New title", category: "unknown")
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body.dig("errors", "category").first).to match(/not a valid category/)
end
it "updates the title of his listing" do
put_classified_listing(listing.id, title: "This is a new title")
expect(response).to have_http_status(:ok)
@ -507,9 +534,36 @@ RSpec.describe "Api::V0::Listings" do
end
it "cannot update another user listing" do
expect do
put_classified_listing(another_user_listing.id, title: "Test for a new title")
end.to raise_error(Pundit::NotAuthorizedError)
put_classified_listing(another_user_listing.id, title: "Test for a new title")
expect(response).to have_http_status(:unauthorized)
end
it "updates details if the listing has been bumped in the last 24 hours" do
listing.update!(bumped_at: 3.minutes.ago)
new_title = Faker::Book.title
put_classified_listing(listing.id, title: new_title)
expect(listing.reload.title).to eq(new_title)
end
it "does not update details if the listing hasn't been bumped in the last 24 hours" do
listing.update!(bumped_at: 24.hours.ago)
new_title = Faker::Book.title
put_classified_listing(listing.id, title: new_title)
expect(listing.reload.title).to eq(listing.title)
end
it "does not update a published listing" do
listing.update!(bumped_at: nil, published: true)
old_title = listing.title
new_title = Faker::Book.title
put_classified_listing(listing.id, title: new_title)
expect(listing.reload.title).to eq(old_title)
end
end
end

View file

@ -389,6 +389,14 @@ RSpec.describe "ClassifiedListings", type: :request do
expect(listing.reload.published).to eq(false)
end
end
context "when an update is attempted" do
it "does not update with an empty body markdown" do
put "/listings/#{listing.id}", params: { classified_listing: { body_markdown: "" } }
expect(response.body).to include(CGI.escapeHTML("can't be blank"))
expect(listing.reload.body_markdown).not_to be_empty
end
end
end
describe "DEL /listings/:id" do