In this chapter, we’re going to give users the ability to post to a common feed. We’ll only allow text for now because allowing users to upload images is a bit complicated, and we’ll do it in the next chapter.

First we need to generate a model for posts. This model will have to handle image uploads in the future, so we need to take that into account. Our post model is going to need an attachment field for images, and to add this we first need to install a new gem called paperclip. Add it to the end of your gemfile:

### Gemfile

...
gem 'paperclip

And run bundle install. Now generate a new migration called CreatePosts with the command rails generate migration CreatePosts and update its contents:

### db/migrations/20160424054404_create_posts.rb

class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.integer :user_id
      t.integer :receiver_id
      
      t.string :message
      t.attachment :image
      
      t.timestamps null: false
    end
  end
end

We’ve got a user_id field to keep track of who the post belongs to and a receiver_id which will be set if the post is being posted onto somebody else’s profile. The attachment property type is available because of the paperclip gem This will contain information about an image attached to the post. Run rake db:migrate to create this table in the database.

If you ever want to see what your whole database looks like, check db/schema.rb.

Now that we have our model, we have to create the same things we always do: a model, a controller, and some views. We’ll make the model next.

Model

Create the file app/models/post.rb:

### app/models/post.rb

class Post < ActiveRecord::Base
    belongs_to :user
end

The belongs_to method creates an ActiveRecord association. It gives us access to a convenient method: @post.user, which will return the user object with the id of @post.user_id. We’re also going to add the inverse relation to our User model:

### app/models/user.rb

class User < ActiveRecord::Base
  has_many :posts
...

has_many gives us access to the method @user.posts, which returns an array of all the posts with @post.user_id equal to @user.id. We also get access to a post constructor that’s used like this: @user.posts.create(post_params). This returns a new post with the attributes specified in post_params, and with a user_id attribute equal to @user.id. We’ll use these methods in our posts controller.

Controllers

Create the file app/controllers/posts_controller.rb with the following contents:

### app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :requires_logged_in, only: [:create]
  
  def index
    @post = Post.new
    @posts = Post.all
  end
  
  def create
    @post = current_user.posts.create(post_params)
    if params[:id]
      @post.receiver_id = params[:id].to_i
      @post.save
      redirect_to user_path(params[:id])
    else
      @post.save
      redirect_to newsfeed_url
    end
  end
  
  private
  
    def post_params
      params.require('post').permit('message', 'image')
    end
    
end

We’ve got a few things going on here.

  • We have an index action to list all the posts; this is going to be our news feed. We’ll also have a post form in our index view, so we need  @post = Post.new to pass into the rails form_for helper.
  • We have a create action to handle post form submissions. When we’re creating a post on a user’s wall, we’ll post to /users/:id/post. Otherwise we’ll post to /users. Both routes will point here, but params[:id] will only be defined if the post is going on somebody’s profile. In this case, we set params[:id] to the new post’s receiver_id, otherwise it’s left blank.
  • The post_params method; this will be a hash that contains message and image post attributes if a post form is submitted.

Add some corresponding routes:

### config/routes.rb

...
  get 'newsfeed', to: "posts#index", as: "posts"
  post 'newsfeed', to: "posts#create"
  post 'users/:id/post', to: "posts#create", as: "receive_post"
end

Views

Let’s create our news feed view first. Because the controller method is posts#index, we need to put the view at app/views/posts/index.html.erb. Create the folder app/views/posts and the file index.html.erb with the following contents:

### app/views/posts/index.html.erb

<div class="row">
    <div class="col-md-6 col-md-offset-3 col-sml-12">
        <h2 align="center">News Feed</h2>
        
        <% if current_user %>
        <div class="panel panel-default">
          <div class="panel-body">
            <%= form_for(@post) do |f| %>
                <div class="form-group">
                    <input type="text" name="post[message]" class="form-control" placeholder="What's on your mind?"/>
                </div>
                <input type="submit" value="Submit" class="btn btn-primary"/>
            <% end %>
          </div>
        </div>
        <% end %>
        
        <% @posts.each do |post| %>
            <div class="panel panel-default">
              <div class="panel-body">
                  <a href="<%=user_path(post.user)%>">
                    <h4 style="margin-top: 0px;"><%= post.user.name %></h4>
                  </a>
                  <%= post.message %>
              </div>
            </div>
        <% end %>
    </div>
</div>

Now let’s add the new link to our navbar:

### app/views/layouts/_navbar.html.erb

<nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" ref="<%= root_url %>">Memespace</a>
      </div>
      <div id="navbar" class="navbar-collapse collapse">
        <ul class="nav navbar-nav">
          <li id="newsfeed-link"><a href="<%= newsfeed_path %>">News Feed</a></li>
          <li id="users-link"><a href="<%= users_path %>">Users</a></li>
...

Restart your server (because we migrated our database and installed a new gem). Now at /newsfeed we can create and view posts!

Create Posts

We need to be able to delete posts. So add a new route:

### config/routes.rb

...
  delete 'posts/:id', to: "posts#destroy", as: "delete_post"
end

Now add a new action to app/controllers/posts_controller.rb, and add it to our before_action :requires_logged_in method.

### app/controllers/posts_controller.rb

class PostsController < ApplicationController
 before_action :requires_logged_in, only: [:create, :destroy]
...
  def destroy
    @post = Post.find(params[:id])
    if current_user == @post.user
      @post.destroy
    end
    redirect_to newsfeed_url
  end
...

And add a delete button to the posts on our newsfeed page by changing a few lines:

### app/views/posts/index.html.erb

...
    <% @posts.each do |post| %>
        <div class="panel panel-default">
          <div class="panel-body">
              <h4 style="margin-top: 0px;">
                  <a href="<%=user_path(post.user)%>"><%= post.user.name %></a>
                  <% if current_user == post.user %>
                      | <%= link_to 'Delete', delete_post_path(post.id), method: :delete, data: { confirm: 'Are you sure?' } %>
                  <% end %>
              </h4>
              <%= post.message %>
          </div>
        </div>
    <% end %>
...

Also, our posts are in the wrong order. We want the new ones at the top. We can fix this with a single line added to our post model:

### app/models/post.rb

class Post < ActiveRecord::Base
    belongs_to :user
    default_scope { order(created_at: 'DESC') }
end

We better add this to our user model as well, for the user index:

### app/models/user.rb

class User < ActiveRecord::Base
    has_many :posts
    default_scope { order(created_at: 'DESC') }
...

Great. Now we can delete posts, and they’re in the right order.

Delete Posts

Finally, we just need to make it possible to post on each other’s walls. We’re going to add a version of the news feed code to our user profiles in app/views/users/show.html.erb. To avoid repetitive code, we’ll move the relevant code into a seperate file and reuse it.

In app/views/posts/index.html.erb, cut everything from inside the second div tag (<div class="col-md-6 col-md-offset-3 col-sml-12">) except the header (<h2 align="center">News Feed</h2>) and paste it into a new file in a new folder: app/views/shared/_feed.html.erb. app/views/shared/_feed.html.erb will look like this:

### app/views/shared/_feed.html.erb

<% if current_user %>
<div class="panel panel-default">
  <div class="panel-body">
    <%= form_for(@post) do |f| %>
        <div class="form-group">
            <input type="text" name="post[message]" class="form-control" placeholder="What's on your mind?"/>
        </div>
        <input type="submit" value="Submit" class="btn btn-primary"/>
    <% end %>
  </div>
</div>
<% end %>

<% @posts.each do |post| %>
    <div class="panel panel-default">
      <div class="panel-body">
          <h4 style="margin-top: 0px;">
              <a href="<%=user_path(post.user)%>"><%= post.user.name %></a>
              <% if current_user == post.user %>
                  | <%= link_to 'Delete', delete_post_path(post.id), method: :delete, data: { confirm: 'Are you sure?' } %>
              <% end %>
          </h4>
          <%= post.message %>
      </div>
    </div>
<% end %>

I’m going to make a pretty major change to this code now. We need to render a different form depending on whether we’re on a user’s profile, or if we’re looking at the actual news feed. Copy and paste the following code changes:

### app/views/shared/_feed.html.erb

<% if current_user %>
    <div class="panel panel-default">
      <div class="panel-body">
        <% if @user %>
          <%= form_for(@post, url: receive_post_path(@user.id)) do |f| %>
              <div class="form-group">
                  <input type="text" name="post[message]" class="form-control" placeholder="What's on your mind?"/>
              </div>
              <input type="submit" value="Submit" class="btn btn-primary"/>
          <% end %>
        <% else %>
          <%= form_for(@post) do |f| %>
              <div class="form-group">
                  <input type="text" name="post[message]" class="form-control" placeholder="What's on your mind?"/>
              </div>
              <input type="submit" value="Submit" class="btn btn-primary"/>
          <% end %>
        <% end %>
      </div>
    </div>
<% end %>
...

The if-else statement for choosing between the two forms works here because @user will only be defined when we’re on a user’s profile.

Now add a line in app/views/posts/index.html.erb to render our news feed code partial:

### app/views/posts/index.html.erb

<div class="row">
  <div class="col-md-6 col-md-offset-3 col-sml-12">
    <h2 align="center">News Feed</h2>

    <%= render 'shared/feed' %>
  </div>
</div>

And also render the news feed in our user profile view:

### app/views/users/show.html.erb

<div class="row">
    <div class="col-md-3 col-sm-12">
        <img src="<%[email protected]_url%>" alt="Avatar" style="width: 300px; max-width: 100%;">
        <h1><%[email protected]%></h1>
        <p><%[email protected]%></p>
        <% if current_user.id == @user.id %>
            <p><a href="<%=edit_user_path(@user.id)%>" class="btn btn-primary" role="button">Edit</a></p>
        <% end %>
    </div>
    <div class="col-md-9 col-sm-12">
        <%= render 'shared/feed' %>
    </div>
</div>

In the app/views/shared/_feed.html.erb partial we refer to the instance variables @post and @posts. We render that partial here in users#show so we need to go into our controller and define those instance variables:

### app/controllers/users_controller.rb

...
  def show
      @user = User.find(params[:id])
      @post = Post.new
      @posts = Post.where("user_id = ? or receiver_id = ?", @user.id, @user.id)
  end
...

The string being passed to Post.where is actually SQL, a language for interacting with databases. Generally rails will help you avoid writing SQL (and you should avoid it), but in this situation it’s handy. Any posts made or received by @user will be returned by the Post.where method here.

Before we test our app, I just want to make one last change to the app/views/shared/_feed.html.erb partial. Posts that were from one user to another need to show who the receiver is. I’m going to make a small change to our Post model first, to make this a bit cleaner:

### app/models/post.rb

class Post < ActiveRecord::Base
    belongs_to :user
    belongs_to :receiver, class_name: 'User'
    default_scope { order(created_at: 'DESC') }
end

We can add the association belongs_to :receiver because our post model has the receiver_id attribute; we just need to clarify that receiver_id is referring to the id of a 'User'. This lets us refer to the user object @post.receiver. Now add the following lines to app/views/shared/_feed.html.erb:

### app/views/shared/_feed.html.erb

...
<% @posts.each do |post| %>
    <div class="panel panel-default">
      <div class="panel-body">
          <h4 style="margin-top: 0px;">
              <a href="<%=user_path(post.user)%>"><%= post.user.name %></a>
              <% if post.receiver %>
                > <a href="<%=user_path(post.receiver)%>"><%= post.receiver.name %></a>
              <% end %>
              <% if current_user == post.user %>
                  | <%= link_to 'Delete', delete_post_path(post.id), method: :delete, data: { confirm: 'Are you sure?' } %>
              <% end %>
          </h4>
          <%= post.message %>
      </div>
    </div>
<% end %>

Adding Some Test Users

Our app is all ready to go, but to experiment with the new features we need other users. Boot up rails console in Cloud9 and run the following commands (just copy and paste one line at a time):

@u = User.create(name: "Test User", avatar_url: "https://placeholdit.imgix.net/~text?txtsize=28&bg=0099ff&txtclr=ffffff&txt=300%C3%97300&w=300&h=300&fm=png")
@u.save
@u = User.create(name: "Another User", avatar_url: "https://placeholdit.imgix.net/~text?txtsize=28&bg=0099ff&txtclr=ffffff&txt=300%C3%97300&w=300&h=300&fm=png")
@u.save
exit

And you should see we now have multiple users listed:

Multiple Users

And you should be able to post to their profiles:

Post To Profile

As always, we finish off the chapter by pushing our new code into production.

git add -A
git commit -am "Added post functionality"
git push heroku master
heroku run rake db:migrate

In the next chapter, we’ll start attaching images to posts.