-
Notifications
You must be signed in to change notification settings - Fork 0
02 Posts Index
Now it's time to add some code that's actually specific the project at hand.
The piece of a data at the center of our app is a post. On the site that serves as our inspiration, there are links and text posts. As far as we're concerned, they're the same type of thing; they just present slightly differently.
Let's go ahead and create a controller, so we can get an empty view to show up quickly. emember, posts is plural in both the filename (posts_controller.rb) and the name of the Ruby class (PostsController
). We'll add one action to our controller: index
.
Create PostsController: app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
end
end
Now let's create the view itself. We'll first need to create a new directory: app/views/posts
Then we'll create the view:
app/views/posts/index.html.erb
<h1>Posts</h1>
Since we didn't use a generator to create our controller, we'll need to configure a route manually. We'll set the root URL to posts#index
.
config/routes.rb
root 'posts#index'
If you refresh http://localhost:3000
, you should now a page that is empty aside from the "Posts" heading.
Create migration create_posts
rails generate migration create_posts
create_posts will look like this:
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
end
end
end
Add to the create_table
block as follows:
create_table :posts do |t|
t.string :title
t.string :link
t.text :body
t.timestamps
end
$ bin/rake db:migrate
== 20141028190724 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0242s
== 20141028190724 CreatePosts: migrated (0.0243s) =============================
Let's check out the table we just created. First, open a psql session.
$ psql -d bluit_development
OR
$ bin/rails dbconsole
You should see a psql
prompt.
psql (9.3.2)
Type "help" for help.
bluit_development=#
OR
At the prompt, use the \d
command to show all tables and similar objects in the bluit_development database.
bluit_development=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+--------
public | posts | table | dstrus
public | posts_id_seq | sequence | dstrus
public | schema_migrations | table | dstrus
(3 rows)
Now, let's check out the columns of the posts table using the same command with an argument.
bluit_development=# \d posts
Table "public.posts"
Column | Type | Modifiers
------------+-----------------------------+----------------------------------------------------
id | integer | not null default nextval('posts_id_seq'::regclass)
title | character varying(255) |
link | character varying(255) |
body | text |
created_at | timestamp without time zone |
updated_at | timestamp without time zone |
Indexes:
"posts_pkey" PRIMARY KEY, btree (id)
We see the three columns we defined explicitly: title, link, and body.
We also see the id column that Rails adds automatically. We also see created_at and updated_at, which we created by calling t.timestamps
. We will never need to explicitly set the data in these three columns. They will be set automatically as records are created and updated.
Looking at the details of the posts table, I see one change I'd like to make. The two columns that we defined with t.string
are limited to 255 characters. URLs can be considerably longer than that, so let's go ahead and allow more characters in that column.
We could alter the existing table to make that change, but let's instead undo our migration, and then migrate again after we've made the appropriate change to create_posts
.
Use the \q
command to exit psql...
bluit_development=# \q
and rollback the migration.
$ bin/rake db:rollback
== 20141028190724 CreatePosts: reverting ======================================
-- drop_table(:posts)
-> 0.0017s
== 20141028190724 CreatePosts: reverted (0.0042s) =============================
Let's edit our migration again. While the HTTP protocol does not dictate a limit to URL length, some web browsers will choke on URLs of a certain length. A quick Google search will reveal that limiting them to 2000 characters is a safe bet.
If you'd like to allow titles to be longer or shorter than 255 characters, you can change that now as well.
create_table :posts do |t|
t.string :title
t.string :link, limit: 2000
t.text :body
t.timestamps
end
Now let's run the migration again.
bin/rake db:migrate
== 20141028190724 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0050s
== 20141028190724 CreatePosts: migrated (0.0051s) =============================
Let's open up psql again and check it out.
$ psql -d bluit_development
psql (9.3.2)
Type "help" for help.
bluit_development=#
Again, we'll inspect our posts table.
bluit_development=# \d posts
Table "public.posts"
Column | Type | Modifiers
------------+-----------------------------+----------------------------------------------------
id | integer | not null default nextval('posts_id_seq'::regclass)
title | character varying(255) |
link | character varying(2000) |
body | text |
created_at | timestamp without time zone |
updated_at | timestamp without time zone |
Indexes:
"posts_pkey" PRIMARY KEY, btree (id)
And we see that the link column now has a limit of 2000. The details of SQL syntax is beyond the scope of this course, but let's use a simple SQL query to confirm that the posts table has no rows. That is, there are no post records in the database.
bluit_development=# select * from posts;
id | title | link | body | created_at | updated_at
----+-------+------+------+------------+------------
(0 rows)
Most of the time, you'll looks at such data through the Rails model, rather than querying the database directly.
Speaking of which, now is a good time to create a Post model to go with our new table.
Don't forget to quit psql with the \q
command.
Create Post: app/models/post.rb
With models, we use the singular form of the noun post in both the filename and the Ruby class name: post.rb and Post
.
class Post < ActiveRecord::Base
end
For the moment, our model doesn't need to be any more complicated than that. That's enough that we can now store and retrieve post records from the Rails console.
Let's give it a shot. First, open a Rails console.
$ bin/rails console
Loading development environment (Rails 4.1.6)
2.2.3 :001 >
Let's instantiate a new, empty Post
object. If all is well, we'll see that the new object contains all of the expected properties with nil values.
2.2.3 :001 > post = Post.new
=> #<Post id: nil, title: nil, link: nil, body: nil, created_at: nil, updated_at: nil>
Let's give our post values for title
, link
, and body
.
2.2.3 :002 > post.title = "TIL there's an immersive coding experience in Indy."
=> "TIL there's an immersive coding experience in Indy."
2.2.3 :003 > post.link = 'https://elevenfifty.com/'
=> "https://elevenfifty.com/"
2.2.3 :004 > post.body = 'You should totally check this out. They have a T. rex skull.'
=> "You should totally check this out. They have a T. rex skull."
As always, the return value of each Ruby statement should appear after you hit return/enter on your keyboard. If it doesn't, you may have a problem in the statement you've written. For example, notice I used double-quotes around my string value in the first statement. Had I used single-quotes, I'd have had a problem, since my string contains an apostrophe.
Let's have a look at our post in its current state. We should see that the value for title
, link
, and body
are no longer nil.
2.2.3 :005 > post
=> #<Post id: nil, title: "TIL there's an immersive coding experience in Indy...", link: "https://elevenfifty.com/", body: "You should totally check this out. They have a T. ...", created_at: nil, updated_at: nil>
Now let's save it!
2.2.3 :007 > post.save
(0.2ms) BEGIN
SQL (1.6ms) INSERT INTO "posts" ("body", "created_at", "link", "title", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["body", "You should totally check this out. They have a T. rex skull."], ["created_at", "2014-10-29 18:40:38.373217"], ["link", "https://elevenfifty.com/"], ["title", "TIL there's an immersive coding experience in Indy."], ["updated_at", "2014-10-29 18:40:38.373217"]]
(1.1ms) COMMIT
=> true
A successful call to save
will return true. Let's check out our post again.
2.2.3 :008 > post
=> #<Post id: 1, title: "TIL there's an immersive coding experience in Indy...", link: "https://elevenfifty.com/", body: "You should totally check this out. They have a T. ...", created_at: "2014-10-29 18:40:38", updated_at: "2014-10-29 18:40:38">
And if we retrieve all posts, we'll see that record as well.
2.2.3 :009 > Post.all
Post Load (0.4ms) SELECT "posts".* FROM "posts"
=> #<ActiveRecord::Relation [#<Post id: 1, title: "TIL there's an immersive coding experience in Indy...", link: "https://elevenfifty.com/", body: "You should totally check this out. They have a T. ...", created_at: "2014-10-29 18:40:38", updated_at: "2014-10-29 18:40:38">]>
The id
, created_at
, and updated_at
values were set automatically. Hot dog!
So now we've got a post in the database. That doesn't do us a whole lot of good yet, as our app isn't displaying that anywhere. Let's go back to PostsController
and retrieve all the post records in the index
action. We'll assign that to an instance variable called @posts
.
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
Now we can use that in our view. Let's start by showing the post title as an item in a bulleted list.
<h1>Posts</h1>
<ul>
<% @posts.each do |post| %>
<li><%= post.title %></li>
<% end %>
</ul>
Ultimately, posts will be links or text posts. So we'll let them submit values for link
or body
, but not both. Let's stick with links for now.
- Add a few more post records (body values optional) from the console.
- Update the view so that the posts titles act as links.
Remember that you can create posts with a one-liner:
2.2.3 :010 > Post.create title: "It's Google!", link: 'http://google.com'
Post.create
even saves the record to the database.
app/views/posts/index.html.erb:
<ul>
<% @posts.each do |post| %>
<li><a href="<%= post.link %>"><%= post.title %></a></li>
<% end %>
</ul>
You could add target="blank"
to the link to force the links to open in new tabs/windows. That's now how some similar sites work, and our links won't all be external, so I suggest you not do that.
I also got rid of the h1
element. We don't really need it.
Before we wrap up this view, let's add information about when each post was saved. Let's put this in a div
right below each link. While we're at it, and before these items get too jammed with data, let's put the title in a div
as well.
<ul>
<% @posts.each do |post| %>
<li>
<div class="title"><a href="<%= post.link %>"><%= post.title %></a></div>
<div class="tagline">submitted at <%= post.created_at %></div>
</li>
<% end %>
</ul>
The time shows up in UTC format, so it reads something like submitted at 2014-10-29 18:40:38 UTC
. Let's make that a little more readable.
Rails includes a helper called time_ago_in_words
. We can pass any Time
object—such as post.created_at
—and it will return a string describing how long ago it was. Let's try it out in the console.
If you already have the console running, go ahead and quit (type exit
) and start it back up with bin/rails console
. We've made some changes to our bundle since we last started it up.
[1] pry(main)> time_ago_in_words Time.now
NoMethodError: undefined method `time_ago_in_words' for main:Object
from (pry):1:in `__pry__'
Our console doesn't have the same context as our view, so we can't call a helper just like that. Thankfully, the Rails console does give us an ActionView::Base object called helper
, and we can call helpers from there.
[2] pry(main)> helper.time_ago_in_words Time.now
=> "less than a minute"
Notice that the string returned by time_ago_in_words
would sound perfect when followed by the word ago. We've tried it with Time.now
. Let's try it with some other times.
[2] pry(main)> helper.time_ago_in_words Time.now
=> "less than a minute"
[3] pry(main)> helper.time_ago_in_words 3.hours.ago
=> "about 3 hours"
[4] pry(main)> helper.time_ago_in_words 8.days.ago
=> "8 days"
[5] pry(main)> helper.time_ago_in_words 43.minutes.ago
=> "43 minutes"
[6] pry(main)> helper.time_ago_in_words 3.weeks.ago
=> "21 days"
Now let's try it with one of our posts.
[7] pry(main)> post = Post.last
Post Load (1.7ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT 1
=> #<Post id: 9, title: "The news", link: "http://cnn.com", body: nil, created_at: "2014-11-03 00:34:40", updated_at: "2014-11-03 00:34:40">
[8] pry(main)> helper.time_ago_in_words post.created_at
=> "about 15 hours"
Very good. Let's put it in our view.
<ul>
<% @posts.each do |post| %>
<li>
<div class="title"><a href="<%= post.link %>"><%= post.title %></a></div>
<div class="tagline">submitted <%= time_ago_in_words post.created_at %> ago</div>
</li>
<% end %>
</ul>
People may want to see the more precise time, so we can throw that in a title
attribute of our .tagline
div. Most browsers will display that as a tooltip.
<ul>
<% @posts.each do |post| %>
<li>
<div class="title"><a href="<%= post.link %>"><%= post.title %></a></div>
<div class="tagline" title="<%= post.created_at %>">submitted <%= time_ago_in_words post.created_at %> ago</div>
</li>
<% end %>
</ul>
Now try hovering your mouse over the more readable version of the time, and you should see the full UTC date.
Let's also add a class
to the list containing the posts. We'll ultimately want to style them differently from other lists.
<ul class="posts">