This commit is contained in:
Mark Moser 2018-11-11 10:12:43 -06:00
parent 8a7b3d8ae0
commit 869a9fc048
16 changed files with 412 additions and 13 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@
# Ignore application configuration
/config/application.yml
coverage/
erd.pdf

View 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
View 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

View File

@ -20,6 +20,8 @@
class User < ApplicationRecord
has_secure_password
has_many :blogs, dependent: :destroy
validates :display_name, presence: true
validates :email, presence: true, email_format: true, uniqueness: true
validates :password_confirmation, presence: true, if: ->(m) { m.password.present? }

View 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

View 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

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.array! @blogs, partial: 'v1/blogs/blog', as: :blog

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.partial! "v1/blogs/blog", blog: @blog

View File

@ -4,6 +4,7 @@ Rails.application.routes.draw do
concern :api_base do
post 'authenticate', to: 'authentication#authenticate', as: :authenticate
resources :users
resources :blogs
end
namespace :v1, defaults: { format: :json } do

View 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

View File

@ -10,7 +10,17 @@
#
# 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|
t.string "display_name", null: false

View 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
View 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
View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'test_helper'
class BlogTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View 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

View File

@ -2,15 +2,9 @@
An API for a micro blogging platform
Guests can only view published content and author list
Authors can edit their profile & blogs
Admins can view the list of authors and edit any blog
blogs:
title
article
published_date
author_id
* Guests can view published content and author list
* Authors can edit their profile & blogs
* Admins can view the list of authors and edit any blog
## Tasks
@ -19,9 +13,14 @@ blogs:
As an author, I want to be able to write a short bio that
is displayed with each of my postings.
### BUG: Publishing
It has been reported that authors are not able to publish
their blogs
### 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.
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
As an author, I would like to be able to assign as category
to my posting from a known list of options.
As an author, I would like to be able to assign a category
to my posting from an existing list of options.
As an admin, I can manage the list of categories available
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.