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

3

13

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
Cherey answered 29/1, 2016 at 0:24 Comment(5)
Where do you call Pundit's authorize?Expedient
Good point. When I add authorise@article to the publish action, I get an error that says: Statesman::TransitionFailedError in ArticlesController#publish Cannot transition from 'publish' to 'publish'Cherey
Then it seems there is no problem on Pundit's part. The reason should be somewhere in the exception message/stack trace. BTW the bang method transition_to! raises exceptions instead of returning false. 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).Expedient
The error in the logs says: Completed 500 Internal Server Error in 5ms (ActiveRecord: 0.8ms) NoMethodError (undefined method in_state?' for #<ArticleStateMachine:0x007ff37ff65098>): app/policies/article_policy.rb:51:in publish?' app/controllers/articles_controller.rb:157:in `set_article'Cherey
Can you update the code to exactly the same version that gives that error? It would be much better if we could see the whole picture.Expedient
U
3

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.

Uptodate answered 11/2, 2016 at 16:38 Comment(0)
C
2

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

Chantry answered 9/2, 2016 at 9:50 Comment(3)
Good point Guilherme, I tried that, but it gives this error: NoMethodError in ArticlesController#publish undefined method `in_state' for #<ArticleStateMachine:0x007ff3642a8ca8>Cherey
According to the docs shouldn't you be using in_state? with a question mark. Machine#in_state?(:state_1, :state_2, ...)Chantry
It gives this error; NoMethodError in ArticlesController#publish undefined method `in_state?' for #<ArticleStateMachine:0x007ff37ea21f10>Cherey
P
2

You are missing out the ? at the end of the method:

the in_state method is actually a class method and behaves like a scope.

You need to use the in_state? method, which is an instance method, like this:

article.state_machine.in_state?(:approve)

Priapus answered 11/2, 2016 at 13:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.