- Read Tutorial
Let's move onto one of our more straightforward models, the Topic
model. Even though the model itself is quite straightforward, there is some custom behavior that it needs to perform. Namely it has to override the default behavior for a show
action in Rails. Let's look at a basic example of how routing works by default in Rails:
If you have a Topic
model and you want to see a specific topic, you would need to go to a route such as:
/topics/123
This has a few negatives:
It's not a great looking route, I typically prefer URLs to be more descriptive
It's not helpful for search engine optimization. Google highly values the words found in a URL, so it's preferable to list out keywords instead of simply an ID
Extending our example from above, what we want the Topic
URL too look like might be:
/topics/coding
Doesn't that look better? It's also easier for users to remember and more search engine friendly. For a real world example, notice how the category URLs are structured on Reddit.com?
Isn't that easier to remember than something like reddit.com/r/782634893/
Creating the model
First create a git branch so this work can be isolated. Run git checkout -b add-topic
. Now let's run the generator for our Topic
feature, we know that topics need to have a single attribute: title
. I don't create tests in advanced for this since RSpec automatically creates a model spec file for us after running a generator. For topics
I'm going to use the resource
generator since I want:
A database table
An ActiveRecord model file
An empty controller
An empty
views
directoryAll of the basic CRUD routes
Run the following command in the terminal:
rails g resource Topic title:string
This will create a skeleton for our Topic
, run rake db:migrate
and we'll be ready to go!
Implementing Friendly ID
We're going to leverage the Friendly ID ruby gem to accomplish the custom slug feature. So let's add the gem to the Gemfile:
gem 'friendly_id', '~> 5.1'
After running bundle
we now have access to all of the custom slug functionality that comes with friendly_id
. Let's create some specs to outline the behavior that we want topics
to have in the application. We don't have to setup base case tests to check on things such as Topic can be created. Our other specs are going to test this behavior thoroughly so I find it a waste of time to create tests for functionality like that. First we'll setup our model specs:
# spec/models/topic_spec.rb require 'rails_helper' RSpec.describe Topic, type: :model do before do @topic = Topic.create(title: "Sports") end describe 'validations' do xit 'cannot be created without a title' do end xit 'cannot be created without a slug' do end end describe 'callbacks' do xit 'automatically sets the slug value even with a nil value submitted' do end end end
If you're unfamiliar with the syntax, by putting x
in front of each of the specs it will mark them as pending. I like to do this since I prefer to create a group of specs but then I prefer to implement their functionality one at a time. If you run rspec
right now you'll see that it lists out each of the specs as pending instead of failing.
Before we even get into implementation you'll notice that we're already missing an attribute: slug
. Let's create a migration to add this to the topics
table:
rails g migration add_slug_to_topics slug:string
After running rake db:migrate
we can now remove the x
from each of the validation specs (you could do it one at a time, but I'm not a fan of wasting time on basic specs like this so I'll combine specs for nearly identical functionality). The updated specs should look like this:
# spec/models/topic_spec.rb describe 'validations' do it 'cannot be created without a title' do @topic.title = nil expect(@topic).to_not be_valid end it 'cannot be created without a slug' do @topic.slug = nil expect(@topic).to_not be_valid end end
Running these specs will give us an error, as expected. To fix this add in the validation to the model file:
# app/models/topic.rb class Topic < ActiveRecord::Base validates_presence_of :title, :slug end
Running rspec
will now pass those tests. Now let's implement the friendly
slug feature. Update the spec like below:
# spec/models/topic_spec.rb describe 'callbacks' do it 'automatically sets the slug value even with a nil value submitted' do expect(@topic.slug).to_not eq(nil) end end
With the way that the friendly_id
gem works, if no default URL is given for the slug it will auto-generate one, so this spec essentially tests to ensure that friendly ID is working properly, even though our topics will most likely always have a custom slug supplied. Running the tests will give a failure since we haven't implemented the feature yet. Let's do that now. Open up the Topic
model file and update it like below:
# app/models/topic.rb class Topic < ActiveRecord::Base extend FriendlyId friendly_id :title, use: :slugged validates_uniqueness_of :slug validates_presence_of :title, :slug end
This is all we need to do to get the slug functionality working. Open up the rails console by running rails c
and test it out with a few different topics:
Topic.create!(title: "Baseball") Topic.create!(title: "Star Wars")
Pretty cool, right? Notice how it automatically makes the slugs URL friendly (hence the name)? We'll need to use a special method to override the default ID lookup method that Rails utilizes, however that will wait for when we're building out the views.
It looks like we ran into a little unexpected behavior from the Gem, since it runs the slug generation process after our callbacks our slug
validation spec is always going to fail. If you run rspec
you'll see it returns a failure:
Failures: 1) Topic validations cannot be created without a slug Failure/Error: expect(@topic).to_not be_valid expected #<Topic id: 2, title: "Sports", created_at: "2016-02-10 01:01:43", updated_at: "2016-02-10 01:01:43", slug: "sports"> not to be valid
I left this in the guide so you could see how the gem works, even setting the slug
value to nil will not cause an error or leave the slug value blank. So we can simply remove the slug
validation spec and we'll be back to all green.
Lastly, did you see how I included validates_uniqueness_of :slug
in the model file? If you open up the console and try running:
Topic.create!(title: "Star Wars")
It creates a slug of "star-wars-7f3801a6-4795-4d17-b11a-58b2d5d10bdb"
, this is the gem placing a hash at the end of the name to ensure it remains unique. This is helpful since it ensures that you'll never end up with two identical slugs, however it's not a great URL. For this application we're going to have a small number of topics, so it wouldn't make sense to spend the time on the advanced implementation, however we'll implement a more advanced version of friendly ID for our posts that will create a better looking URL that still remains unique.