I am trying to make an app in Rails 4.
I am trying to use statesman gem for states and then pundit for policies.
My gemfile has:
gem 'statesman', '~> 1.3', '>= 1.3.1'
gem 'pundit'
I have an article model and an article transitions model and an article_state_machine model.
My objective is to define a publish policy (using pundit) in my articles policy which allows a user who owns an article to publish that article if it is in state 'approved'.
I am trying this in my pundit article policy:
class ArticlePolicy < ApplicationPolicy
def publish?
user.present? && user == article.user
# if requires approval, then approved
# and article.in_state(:approve) - why doesnt this work - see statesman docs?
# user && user.article.exists?(article.id)
end
end
When I try to check if the article is in state :approve (as commented out above), I get an error message that says undefined method 'in_state'.
How can I use state machine in the policy? Or is it intended that the policy allows the user to publish at all times but you only show the button on the article show page when the article is in state approve (although I thought that was the point of pundit).
Article.rb
class Article < ActiveRecord::Base
include Statesman::Adapters::ActiveRecordQueries
has_many :transitions, class_name: "ArticleTransition", autosave: false
def state_machine
@state_machine ||= ArticleStateMachine.new(self, transition_class: ArticleTransition, association_name: :transitions)
end
# delegate :can_transition_to?. :trans
# def reindex_articles
# article.reindex_async
# end
private
def self.transition_name
:transitions
end
def self.transition_class
ArticleTransition
end
def self.initial_state
# ArticleTransition.initial_state
:draft
end
end
Article state machine model:
class ArticleStateMachine
include Statesman::Machine
state :draft, initial: :true #while author is drafting
state :review #while approver comments are being addressed (really still in draft)
state :reject # not suitable for publication
state :approve # suitable for publication
state :publish #published
state :remove # destroyed
# state :spotlight
transition from: :draft, to: [:reject, :approve, :publish, :remove]
# transition from: :review, to: [:rejected, :approved, :removed]
transition from: :reject, to: [:draft, :remove]
transition from: :approve, to: [:publish, :remove]
transition from: :publish, to: :remove
end
Article transition model:
class ArticleTransition < ActiveRecord::Base
include Statesman::Adapters::ActiveRecordTransition
belongs_to :article, inverse_of: :article_transitions
end
Article controller:
def approve
article = Article.find(params[:id])
if article.state_machine.transition_to!(:approve)
flash[:notice] = "This article has been approved for publication"
redirect_to action: :show, id: article_id
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to approve this article"
redirect_to action: :show, id: article_id
end
end
def publish
article = Article.find(params[:id])
authorize @article
if article.state_machine.transition_to!(:publish)
redirect_to action: :show, id: article_id
# how do you catch the date the state became published?
else
flash[:error] = "You're not able to publish this article"
redirect_to action: :show, id: article_id
end
end
Can anyone see what I've done wrong?
The entire articles controller has:
class ArticlesController < ApplicationController
before_action :set_article, only: [:show, :edit, :update, :destroy, :reject, :approve, :publish, :remove]
before_action :authenticate_user!, except: [:index, :show, :search, :reject, :approve, :publish, :remove]
respond_to :html, :json
# GET /articles
# GET /articles.json
def index
@articles = policy_scope(Article)
# query = params[:query].presence || "*"
# @articles = Article.search(query)
end
# def index
# if params[:query].present?
# @books = Book.search(params[:query], page: params[:page])
# else
# @books = Book.all.page params[:page]
# end
# end
# GET /articles/1
# GET /articles/1.json
def show
end
# GET /articles/new
def new
@article = Article.new
@article.comments.build
end
# GET /articles/1/edit
def edit
authorize @article
end
# POST /articles
# POST /articles.json
def create
# before_action :authenticate_user!
# authorize @article
@article = current_user.articles.new(article_params)
respond_to do |format|
if @article.save
format.html { redirect_to(@article) }
format.json { render :show, status: :created, location: @article }
else
format.html { render :new }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
end
def search
if params[:search].present?
@articless = Article.search(params[:search])
else
@articles = Articles.all
end
end
# PATCH/PUT /articles/1
# PATCH/PUT /articles/1.json
def update
# before_action :authenticate_user!
authorize @article
respond_to do |format|
# if @article.update(article_params)
# format.json { render :show, status: :ok, location: @article }
# else
# format.html { render :edit }
# format.json { render json: @article.errors, status: :unprocessable_entity }
# end
# end
if @article.update(article_params)
format.html { redirect_to(@article) }
format.json { render :show, status: :ok, location: @article }
else
format.json { render json: @article.errors, status: :unprocessable_entity }
end
format.html { render :edit }
end
end
# DELETE /articles/1
# DELETE /articles/1.json
def destroy
before_action :authenticate_user!
authorize @article
@article.destroy
respond_to do |format|
format.json { head :no_content }
end
end
# def review
# article = Article.find(params[:id])
# if article.state_machine.transition_to!(:review)
# flash[:notice] = "Comments on this article have been made for your review"
# redirect_to action: :show, id: article_id
# else
# flash[:error] = "You're not able to review this article"
# redirect_to action: :show, id: article_id
# end
# end
def reject
end
def approve
article = Article.find(params[:id])
if article.state_machine.transition_to!(:approve)
flash[:notice] = "This article has been approved for publication"
redirect_to action: :show, id: article_id
# add mailer to send message to article owner that article has been approved
else
flash[:error] = "You're not able to approve this article"
redirect_to action: :show, id: article_id
end
end
def publish
article = Article.find(params[:id])
if article.state_machine.transition_to!(:publish)
redirect_to action: :show, id: article_id
# how do you catch the date the state became published?
else
flash[:error] = "You're not able to publish this article"
redirect_to action: :show, id: article_id
end
end
def remove
article = Article.find(params[:id])
if article.state_machine.transition_to!(:remove)
redirect_to root_path
else
flash[:error] = "You're not able to destroy this article"
redirect_to action: :show, id: article_id
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_article
@article = Article.find(params[:id])
authorize @article
end
# Never trust parameters from the scary internet, only allow the white list through.
def article_params
params.require(:article).permit(:body, :title, :image, :tag_list,
comment_attributes: [:opinion])
end
end
authorize
? – Expedienttransition_to!
raises exceptions instead of returningfalse
. It might be a good idea to to use the non-bang method and print the appropriate error message if you're not going the catch those exceptions (using exceptions for program flow is not a good idea anyway). – Expedientin_state?' for #<ArticleStateMachine:0x007ff37ff65098>): app/policies/article_policy.rb:51:in
publish?' app/controllers/articles_controller.rb:157:in `set_article' – Cherey