Blogs
This commit is contained in:
parent
8a7b3d8ae0
commit
869a9fc048
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@
|
|||||||
# Ignore application configuration
|
# Ignore application configuration
|
||||||
/config/application.yml
|
/config/application.yml
|
||||||
coverage/
|
coverage/
|
||||||
|
erd.pdf
|
||||||
|
49
app/controllers/v1/blogs_controller.rb
Normal file
49
app/controllers/v1/blogs_controller.rb
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module V1
|
||||||
|
class BlogsController < ApplicationController
|
||||||
|
before_action :set_blog, only: %i[show update destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@blogs = policy_scope Blog.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@blog = Blog.new(blog_params)
|
||||||
|
@blog.user_id = current_user.id
|
||||||
|
|
||||||
|
authorize @blog
|
||||||
|
|
||||||
|
if @blog.save
|
||||||
|
render :show, status: :created, location: v1_blogs_url(@blog)
|
||||||
|
else
|
||||||
|
render json: @blog.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @blog.update(blog_params)
|
||||||
|
render :show, status: :ok, location: v1_blogs_url(@blog)
|
||||||
|
else
|
||||||
|
render json: @blog.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@blog.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_blog
|
||||||
|
@blog = Blog.find(params[:id])
|
||||||
|
authorize @blog
|
||||||
|
end
|
||||||
|
|
||||||
|
def blog_params
|
||||||
|
params.require(:blog).permit(policy(Blog).permitted_attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
app/models/blog.rb
Normal file
30
app/models/blog.rb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: blogs
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# article :text not null
|
||||||
|
# published_date :string default(""), not null
|
||||||
|
# title :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :integer
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_blogs_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
|
||||||
|
class Blog < ApplicationRecord
|
||||||
|
belongs_to :author, class_name: "User", foreign_key: :user_id, inverse_of: :blogs
|
||||||
|
|
||||||
|
scope :published, -> { where("published_date <= ?", Time.zone.now) }
|
||||||
|
|
||||||
|
def published?
|
||||||
|
return false if published_date.empty?
|
||||||
|
|
||||||
|
Time.zone.parse(published_date) <= Time.zone.now
|
||||||
|
end
|
||||||
|
end
|
@ -20,6 +20,8 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
has_secure_password
|
has_secure_password
|
||||||
|
|
||||||
|
has_many :blogs, dependent: :destroy
|
||||||
|
|
||||||
validates :display_name, presence: true
|
validates :display_name, presence: true
|
||||||
validates :email, presence: true, email_format: true, uniqueness: true
|
validates :email, presence: true, email_format: true, uniqueness: true
|
||||||
validates :password_confirmation, presence: true, if: ->(m) { m.password.present? }
|
validates :password_confirmation, presence: true, if: ->(m) { m.password.present? }
|
||||||
|
45
app/policies/blog_policy.rb
Normal file
45
app/policies/blog_policy.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BlogPolicy < ApplicationPolicy
|
||||||
|
def show?
|
||||||
|
return true if update?
|
||||||
|
|
||||||
|
record.published?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
return true if user&.acts_as_admin?
|
||||||
|
|
||||||
|
record.user_id == user&.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
user&.acts_as_author?
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_attributes
|
||||||
|
return base_attributes + %i[user_id] if user&.acts_as_admin?
|
||||||
|
|
||||||
|
base_attributes
|
||||||
|
end
|
||||||
|
|
||||||
|
def base_attributes
|
||||||
|
%i[
|
||||||
|
title
|
||||||
|
article
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
class Scope < Scope
|
||||||
|
def resolve
|
||||||
|
return scope if user&.acts_as_admin?
|
||||||
|
return scope.published.or(user.blogs) if user&.acts_as_author?
|
||||||
|
|
||||||
|
scope.published
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
app/views/v1/blogs/_blog.json.jbuilder
Normal file
14
app/views/v1/blogs/_blog.json.jbuilder
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.url v1_blog_url(blog, format: :json)
|
||||||
|
|
||||||
|
json.extract! blog,
|
||||||
|
:title,
|
||||||
|
:article,
|
||||||
|
:published_date,
|
||||||
|
:id
|
||||||
|
|
||||||
|
json.author do
|
||||||
|
json.name blog.author.display_name
|
||||||
|
json.url v1_user_url(blog.author, format: :json)
|
||||||
|
end
|
3
app/views/v1/blogs/index.json.jbuilder
Normal file
3
app/views/v1/blogs/index.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.array! @blogs, partial: 'v1/blogs/blog', as: :blog
|
3
app/views/v1/blogs/show.json.jbuilder
Normal file
3
app/views/v1/blogs/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.partial! "v1/blogs/blog", blog: @blog
|
@ -4,6 +4,7 @@ Rails.application.routes.draw do
|
|||||||
concern :api_base do
|
concern :api_base do
|
||||||
post 'authenticate', to: 'authentication#authenticate', as: :authenticate
|
post 'authenticate', to: 'authentication#authenticate', as: :authenticate
|
||||||
resources :users
|
resources :users
|
||||||
|
resources :blogs
|
||||||
end
|
end
|
||||||
|
|
||||||
namespace :v1, defaults: { format: :json } do
|
namespace :v1, defaults: { format: :json } do
|
||||||
|
15
db/migrate/20181111150322_create_blogs.rb
Normal file
15
db/migrate/20181111150322_create_blogs.rb
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateBlogs < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_table :blogs do |t|
|
||||||
|
t.string :title, null: false
|
||||||
|
t.text :article, null: false
|
||||||
|
t.string :published_date, null: false, default: ""
|
||||||
|
t.integer :user_id
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :blogs, :user_id
|
||||||
|
end
|
||||||
|
end
|
12
db/schema.rb
12
db/schema.rb
@ -10,7 +10,17 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2018_11_10_183741) do
|
ActiveRecord::Schema.define(version: 2018_11_11_150322) do
|
||||||
|
|
||||||
|
create_table "blogs", force: :cascade do |t|
|
||||||
|
t.string "title", null: false
|
||||||
|
t.text "article", null: false
|
||||||
|
t.string "published_date", default: "", null: false
|
||||||
|
t.integer "user_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["user_id"], name: "index_blogs_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "display_name", null: false
|
t.string "display_name", null: false
|
||||||
|
97
test/controllers/v1/blogs_controller_test.rb
Normal file
97
test/controllers/v1/blogs_controller_test.rb
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class BlogsControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
test "anyone can index published blogs" do
|
||||||
|
blogs = Blog.published
|
||||||
|
get v1_blogs_url
|
||||||
|
body = JSON.parse response.body
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
assert_equal blogs.count, body.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admins can index ALL blogs" do
|
||||||
|
get v1_blogs_url, headers: auth_headers(users(:admin))
|
||||||
|
body = JSON.parse response.body
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
assert_equal Blog.count, body.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can index ALL his blogs plus published" do
|
||||||
|
author = users(:author)
|
||||||
|
blogs = Blog.published.or(author.blogs)
|
||||||
|
|
||||||
|
get v1_blogs_url, headers: auth_headers(author)
|
||||||
|
body = JSON.parse response.body
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
assert_equal blogs.count, body.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sally can not index authors unpublished blogs" do
|
||||||
|
bad_blog = blogs(:author2)
|
||||||
|
sally = users(:sally)
|
||||||
|
|
||||||
|
get v1_blogs_url, headers: auth_headers(sally)
|
||||||
|
body = JSON.parse response.body
|
||||||
|
blog_ids = body.each_with_object([]) { |blog, memo| memo << blog["id"] }
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
assert_not blog_ids.include?(bad_blog)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "guests can view a published blog" do
|
||||||
|
blog = blogs(:author1)
|
||||||
|
get v1_blog_url(blog)
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_match blog.title, response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
test "guests CANNOT view an unpublished blog" do
|
||||||
|
get v1_blog_url(blogs(:author2))
|
||||||
|
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
test "authors can create and recieve a new blog" do
|
||||||
|
assert_difference('Blog.count') do
|
||||||
|
post v1_blogs_url, params: { blog: {
|
||||||
|
title: "This is my blog",
|
||||||
|
article: "I don't have much to say"
|
||||||
|
} }, headers: auth_headers(users(:michelle))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :created
|
||||||
|
assert_match(/this is my blog/i, response.body)
|
||||||
|
assert_match(/michelle/i, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "author can update blog" do
|
||||||
|
patch v1_blog_url(blogs(:author1)), params: { blog: {
|
||||||
|
title: "a new title"
|
||||||
|
} }, headers: auth_headers(users(:author))
|
||||||
|
|
||||||
|
assert_response :ok
|
||||||
|
assert_match(/a new title/i, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can destroy a blog" do
|
||||||
|
assert_difference('Blog.count', -1) do
|
||||||
|
delete v1_blog_url(blogs(:author1)), headers: auth_headers(users(:admin))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
test "sally can destroy her blogs" do
|
||||||
|
assert_difference('Blog.count', -1) do
|
||||||
|
delete v1_blog_url(blogs(:sally1)), headers: auth_headers(users(:sally))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_response :no_content
|
||||||
|
end
|
||||||
|
end
|
47
test/fixtures/blogs.yml
vendored
Normal file
47
test/fixtures/blogs.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: blogs
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# article :text not null
|
||||||
|
# published_date :string default(""), not null
|
||||||
|
# title :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# user_id :integer
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_blogs_on_user_id (user_id)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
author1:
|
||||||
|
title: My Opus
|
||||||
|
article: "Donec sed odio dui. Nulla vitae elit libero, a pharetra augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue."
|
||||||
|
published_date: <%= (Time.zone.now - 8.days).to_s %>
|
||||||
|
author: author
|
||||||
|
|
||||||
|
author2:
|
||||||
|
title: A Work in Progress
|
||||||
|
article: "Donec sed odio dui. Nulla vitae elit libero, a pharetra augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue."
|
||||||
|
author: author
|
||||||
|
|
||||||
|
sally1:
|
||||||
|
title: Vehicula Fringilla Consectetur Elit
|
||||||
|
article: "Donec sed odio dui. Nulla vitae elit libero, a pharetra augue. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue."
|
||||||
|
published_date: <%= (Time.zone.now - 15.days).to_s %>
|
||||||
|
author: sally
|
||||||
|
|
||||||
|
sally2:
|
||||||
|
title: Tristique Malesuada Dapibus Euismod
|
||||||
|
article: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur blandit tempus porttitor. Donec sed odio dui. Nulla vitae elit libero, a pharetra augue."
|
||||||
|
published_date: <%= (Time.zone.now - 5.days).to_s %>
|
||||||
|
author: sally
|
||||||
|
|
||||||
|
sally3:
|
||||||
|
title: Tellus Quam Euismod Aenean
|
||||||
|
article: "Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec ullamcorper nulla non metus auctor fringilla."
|
||||||
|
author: sally
|
||||||
|
|
9
test/models/blog_test.rb
Normal file
9
test/models/blog_test.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class BlogTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
64
test/policies/blog_policy_test.rb
Normal file
64
test/policies/blog_policy_test.rb
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class BlogPolicyTest < PolicyAssertions::Test
|
||||||
|
test 'anyone can view a published blog' do
|
||||||
|
assert_permit nil, blogs(:author1), :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'must authenticate for modification' do
|
||||||
|
assert_raise Pundit::NotAuthorizedError do
|
||||||
|
%w[create update destroy].each do |action|
|
||||||
|
UserPolicy.new(nil, User.new).send("#{action}?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# show
|
||||||
|
test 'author can show his unpublished blog' do
|
||||||
|
assert_permit users(:author), blogs(:author2), :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'admin can show anothers unpublishd blog' do
|
||||||
|
assert_permit users(:admin), blogs(:author2), :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'sally CANNOT show authors unpublishd blog' do
|
||||||
|
assert_not_permitted users(:sally), blogs(:author2), :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
# update
|
||||||
|
test 'author can update his unpublished blog' do
|
||||||
|
assert_permit users(:author), blogs(:author2), :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'admin can update anothers unpublishd blog' do
|
||||||
|
assert_permit users(:admin), blogs(:author2), :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'sally CANNOT update authors unpublishd blog' do
|
||||||
|
assert_not_permitted users(:sally), blogs(:author2), :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
# create
|
||||||
|
test 'users can create a new blog' do
|
||||||
|
assert_permit users(:admin), Blog.new, :create?
|
||||||
|
assert_permit users(:author), Blog.new, :create?
|
||||||
|
assert_permit users(:sally), Blog.new, :create?
|
||||||
|
assert_permit users(:michelle), Blog.new, :create?
|
||||||
|
end
|
||||||
|
|
||||||
|
# destroy
|
||||||
|
test 'authors can destroy their own blogs' do
|
||||||
|
assert_permit users(:author), blogs(:author1), :destroy?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'admins can destroy any blogs' do
|
||||||
|
assert_permit users(:admin), blogs(:author1), :destroy?
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'users CANOT destroy another authors blogs' do
|
||||||
|
assert_not_permitted users(:sally), blogs(:author1), :destroy?
|
||||||
|
end
|
||||||
|
end
|
@ -2,15 +2,9 @@
|
|||||||
|
|
||||||
An API for a micro blogging platform
|
An API for a micro blogging platform
|
||||||
|
|
||||||
Guests can only view published content and author list
|
* Guests can view published content and author list
|
||||||
Authors can edit their profile & blogs
|
* Authors can edit their profile & blogs
|
||||||
Admins can view the list of authors and edit any blog
|
* Admins can view the list of authors and edit any blog
|
||||||
|
|
||||||
blogs:
|
|
||||||
title
|
|
||||||
article
|
|
||||||
published_date
|
|
||||||
author_id
|
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@ -19,9 +13,14 @@ blogs:
|
|||||||
As an author, I want to be able to write a short bio that
|
As an author, I want to be able to write a short bio that
|
||||||
is displayed with each of my postings.
|
is displayed with each of my postings.
|
||||||
|
|
||||||
|
### BUG: Publishing
|
||||||
|
|
||||||
|
It has been reported that authors are not able to publish
|
||||||
|
their blogs
|
||||||
|
|
||||||
### Moderation
|
### Moderation
|
||||||
|
|
||||||
As an admin, I want to be able to put a blog it in a state
|
As an admin, I want to be able to put a blog in a state
|
||||||
of moderation that also unpublishes it.
|
of moderation that also unpublishes it.
|
||||||
|
|
||||||
Once a blog post has this state, the author must edit and
|
Once a blog post has this state, the author must edit and
|
||||||
@ -29,8 +28,18 @@ submit for another review before an admin will republish.
|
|||||||
|
|
||||||
### Categories
|
### Categories
|
||||||
|
|
||||||
As an author, I would like to be able to assign as category
|
As an author, I would like to be able to assign a category
|
||||||
to my posting from a known list of options.
|
to my posting from an existing list of options.
|
||||||
|
|
||||||
As an admin, I can manage the list of categories available
|
As an admin, I can manage the list of categories available
|
||||||
to the authors.
|
to the authors.
|
||||||
|
|
||||||
|
### Registration
|
||||||
|
|
||||||
|
As a guest, I should be able to register so that I may start
|
||||||
|
producing content.
|
||||||
|
|
||||||
|
### Author Titles
|
||||||
|
|
||||||
|
As a user, when I look up an author, I should also get a list
|
||||||
|
of titles and links to her published articles.
|
||||||
|
Loading…
Reference in New Issue
Block a user