How to Use Enums in Rails to Manage Data Stages
This guide walks through how to integrate enums into a Ruby on Rails application, which will allow for our application's blog posts to have a published or draft status. This will enable a number of key features, such as: creating scopes, automatically generating methods for updating the blog post's draft or published status, and much more.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

This guide walks through how to integrate enums into a Ruby on Rails application, which will allow for our application's blog posts to have a published or draft status. This will enable a number of key features, such as: creating scopes, automatically generating methods for updating the blog post's draft or published status, and much more.

In this guide, we're going to start working on the last item on our task list, which is, "Custom actions for updating status". This is going to take more than one guide because the implementation will require us to make changes to our routes, models and controllers. It's not possible to get it all in one video, mainly because I'll be explaining the purpose behind the changes as we build it out.

With this feature, I want to implement a toggle button in our blogs page that will allow us to switch from draft mode to publish mode. Eventually, when we implement a design, there will be a label or note to indicate if the blog is in draft mode or published.

With this plan in place, there are a number of things that need to be implemented. First, we have to update the data model, which means we need to update our schema file and run a migration. That will give us the ability to have a draft mode and a publish mode for the blog posts.

Enums

Now whenever we have data that is going to be changing stages, for example, from published to draft or an application where you have an invoice that needs to go through a full set of approval items from managers, you would create an approval workflow. In these situations, where you have data that changes state, there is a good chance you will want to use a Rails enum.

An enum gives us the ability to change the state of the data very quickly. It is an elegant way to represent this type of workflow. Once we have implemented the enum, we will be ready to create a custom action and a custom route.

Custom Actions

So far in our data flows section, we've covered resources routes, which are the standard CRUD related items such as show, edit, create and delete. We've also talked about integrating static routes with our data flow. One thing we haven't gone into is how we can have custom actions. For example, when we have an action like toggling between statuses, it not only requires custom routes, but we also have to create custom actions, so we will have to build that out.

The first thing we need to do is look at our schema.rb file. Since we have to edit our blogs table, let's run a migration. We're going to add a new attribute called status. This attribute will be an integer and will need a default value. The code for this is:

rails g migration add_post_status_to_blogs status:integer

We can set our defaults inside the migration file. One way to open the migration file, is to use data from the terminal. Copy the line after create, then use ‘command t’ to open a fuzzy search. Paste the file in the search area, and you will be able to quickly open it.

class AddPostStatusToBlogs < ActiveRecord::Migration[5.0]
  def change
    add_column :blogs, :status, :integer
  end
end

Setting a Default

To add the default to the migration, we need to place the code at the end of the add_column line using, default: 0.

- add_column :blogs, :status, :integer
+ add_column :blogs, :status, :integer, default: 0

We will cover the need for defaults later, but essentially, whenever you have stages, such as draft or published, you’ll need to declare what the default state is going to be.

Also, enums work with integers. Don't worry if this concept is a bit puzzling right now, as we have an entire section on data management where we'll talk about things like active_record and why we need to follow this pattern for implementation. For now just follow along and make note of what I'm doing, with the understanding this will make more sense in the active record portion of the course.

Switch back to the terminal and run rails db:migrate.

Now, we should have the status column in our schema.rb file, with a default value of 0.

  create_table "blogs", force: :cascade do |t|
    t.string   "title"
    t.text     "body"
    t.datetime "created_at",             null: false
    t.datetime "updated_at",             null: false
    t.string   "slug"
    t.integer  "status",     default: 0
    t.index ["slug"], name: "index_blogs_on_slug", unique: true, using: :btree
  end

Right now, status is a regular integer attribute, just like an id is an integer attribute. There is nothing special about it yet. To make it an enum, we need to make a change to our model file. Go to app/model/blog.rb

Creating the Enum

We will create an enum inside our class. Our enum needs to know which attribute is the enum, so it will take in the attribute. In this case it is ‘status’. So we begin with enum status: {}. Inside the brackets we can pass key-value pairs. For one we will use draft, which is set to 0, and for the other we will use published, which is set to 1. {draft: 0, published: 1}

class Blog < ApplicationRecord
  enum status: { draft: 0, published: 1 }
  extend FriendlyId
  friendly_id :title, use: :slugged
end

Hopefully now you can understand the reason we set the default to 0. As you can see from the code 0 is going to be mapped to draft. Accordingly, whenever you create a new blog post, by default, it will be in draft mode until the state is changed to publish . Also, this is the only place where we're going to have to talk about 1 and 0. Moving forward, we'll simply say draft and published. This is one of the most impressive things about enums.

To make sure this is working, open the rails console and create a blog. The title and body can be whatever you choose. Blog.create!(title: ”asdfads”, body: “asdfads”)

large

Now that this has been created you will see the last attribute is status and it has a value of draft. By default this was mapped to draft status because it is a new blog post. That is efficient functionality.

Changing the Status

So, now we know that our enum is working. Now, if you want to change the status to published, you can access the last blog and change it like this:

Blog.last.published!

That command will run a query and will update the value of status.

large

Notice, in the SQL code that was generated, it declares ‘update blog status for the blog with an id of 3 (which is the last one) to a 1’. Now, consider, I didn't indicate 1, all that I stated was blog.last.published. This is due to the way we defined in our enum. The system knows that published is mapped to 1. We personally don’t have to do anything to express the 1. We just indicate published and the code does the rest for us.

Now, if you run Blog.last again, you can see that the status is published. I think it’s pretty remarkable that we can run this command and manage items at different stages. When we create our custom route, you will see how this is incredibly effective when we want to make changes on the fly, for example when we want to toggle between a published item or a draft item. It's also a lot easier than manually updating a record and passing in values.

Using .published as a Method

Incidentally, we can treat .published as a method because that is what it actually is. For example, if you use the command Blog.published, it will run a database query on the entire Blogs table and will display all the blogs that have status as published. You can also use the imperative Blog.published.count and this will return a value of one => 1, as we have only one record with this status.

Let’s update another. Using Blog.first.published! will update the first blog to have a status of published. Now, if you run Blog.published.count, you'll see there are two => 2 items.

Blog.published is now a database scope that is built into Rails. Even though Rails didn't know about draft or published before, now, it dynamically created a method for us based on the key-value pairs we gave in our enum.

To verify this, if you run Blog.published in the console, you will get a return that shows it has run a database query that says select from blogs where blogs has a status of 1.

As we know, that is where the enum is mapped to the attribute, and now the query brings back the associated items.

This is really efficient because we only want to show published blogs to the public. Expressly, when people are coming to your portfolio, you don’t want your visitors all view all your blog posts. You will only want to show them the ones you have marked as published and not posts that are still in draft mode. To accomplish this, all we have to do is run Blog.published, and the blogs you have deemed as published will be displayed. As a site owner, you will want to see all the blogs, so you can simply use Blog.all. This is also a good way of handling permissions, though we'll get into implementing that later on.

I hope you can appreciate how elegant this is. Years ago when I started doing development, this functionality enum status: { draft: 0, published: 1 } would have required hundreds of lines of code to write from scratch. Now, we are able to do a complete mapping across different stages with just a single line of code. Essentially we created a state machine which also created database queries, and custom scopes. Simply stated, this is an amazing amount of functionality.

Finally, we need to add these altered files to our GitHub branch. In the terminal lets run a git status, git add. and git commit -m “Updated status for enum”. Finish with git push origin portfolio-feature.

In the next guide we will walk through making changes to the view that will mark items as draft or published.

Resources