Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 4 with Pundit & Statesman gem - policy when an object is in a state

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
like image 230
Mel Avatar asked Jan 29 '16 00:01

Mel


2 Answers

The version of statesman gem you are using does not have in_state? defined. You can update the gem. Or you can define it yourself using similar codes as linked by smallbuttoncom

https://github.com/gocardless/statesman/blob/1fd4ee84c87765b7855688b8eb5dddea7ddddbdd/lib/statesman/machine.rb#L180-L182

However, for your case, a simple check should be enough. Try following code in your policy

article.state_machine.current_state == "approve"

Hope that helps.

like image 179
Rajesh Sharma Avatar answered Nov 16 '22 00:11

Rajesh Sharma


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'.

Have you tried to change article.in_state?(:approve) to article.state_machine.in_state?(:approve) in your policy?.

like image 45
Guilherme Avatar answered Nov 16 '22 00:11

Guilherme