- Read Tutorial
- Watch Guide Video
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:
- Logging out the existing user
- Creating a new user
- Logging in that new user
- Changing the status of the associated post to
approved
- We're directing the user to the
edit
page - 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.
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.
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.