Learn How to Lock Records in Rails to Prevent Editing
In this guide we are going to create one more rule: If an admin user has approved a post, then users cannot edit that post. Essentially, we have to lock approved records so they can't edit be edited.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

Let's get back to our approval workflow now. We have quite a number of rules in place, such as:

  • A user cannot edit a post they've not created
  • An admin can only change the status of a post
  • And so on

In this guide we are going to create one more rule:

  • If an admin user has approved a post, then users cannot edit that post

This is a requirement since we don't want a user to change the number of hours after a post was approved. Essentially, we have to lock approved records so they can't edit be edited.

To start, let's open the approval_workflow_spec.rb, and add the following test:

# spec/features/approval_workflow_spec.rb

it 'should not be editable by the post creator if status is approved' do
  logout(:user)
  user = FactoryGirl.create(:user)
  login_as(user, :scope => :user)

  @post.update(user_id: user.id, status: 'approved')

  visit edit_post_path(@post)

  expect(current_path).to eq(root_path)
end

In this code, we are running the following simulation:

  1. Logging out the existing user
  2. Creating a new user
  3. Logging in that new user
  4. Changing the status of the associated post to approved
  5. We're directing the user to the edit page
  6. Finally, we testing to ensure that the user is redirected to the homepage

If you run rspec it will obviously fail because the test expected the homepage, but got a page where users can still edit their post.

To implement this feature, open the post_policy.rb file that contains our post permissions. Currently, in the update method we are asking it to return true if the user id of the record is the same as that of the current user or if the user is of type admin. Let's update it to look like this:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def update?
    return true if record.approved? && admin_types.include?(user.type)
    return true if (record.user_id == user.id || admin_types.include?(user.type) && !record.approved?)
  end
end

Here, we are asking the method to return true if the record has a status approved and if the user is of type admin. If this is true, then nothing else in the method will execute. If that's not true, it'll move onto the next line where we are asking the method to return true if the current user is the owner of the record or if the user is an admin, AND if the record does not have a status approved.

If you think this is horrible code, you're right! We'll refactor it next to have a cleaner implementation.

If you run the tests they'll be passing now.

With that working we can now refactor the implementation code. I'm going to extract each component into its own method that we can call them each separately. The fully refactored policy code is below:

# app/policies/post_policy.rb

class PostPolicy < ApplicationPolicy
  def update?
    return true if post_approved? && admin?
    return true if user_or_admin && !post_approved?
  end

  private

    def user_or_admin
      record.user_id == user.id || admin?
    end

    def admin?
      admin_types.include?(user.type)
    end

    def post_approved?
      record.approved?
    end
end

Even though this code works the same as the previous implementation it's a significant improvement. Notice how it almost reads like English? This code will be easier to manage moving forward and when I come to look at it months from now its purpose will immediately make sense.

If you run rspec again you'll see that all of the tests are passing and if you open this up in the browser you'll find that the system now has the behavior we're looking for.

large

Before we finish up this guide I want to quickly tell you why I'm spending so much time in making my test cases so explicit. If you type rspec --format documentation in the console, this is what you'll get.

large

This command will print out each of our tests in a readable format. When you use the documentation format flag it will give you a report that can be understood by both technical and non-technical users. After running this command all that you have to do is copy-paste it in a word document and hand it over to a manager or quality assurance team, who in turn can check if your tests are comprehensive.

Resources