4. Writing Logic for Creating Post and Add Validations

Now that we've set up our form, let's write the backend for it to make sure that everything works correctly.

Let's start off by writing our create action in our posts_controller.rb. When we hit submit on our form, it will then trigger the create action. Right now we haven't set up the create action, so it will give us an error.

In our posts_controller.rb under our new action, let's add the following lines of code, and then we'll go over what it's doing.

def create
  @post = current_user.posts.create(post_params)
  if @post.valid?
    redirect_to root_path
  else
    render :new, status: :unprocessable_entity
  end
end

private

def post_params
  params.require(:post).permit(:photo, :caption)
end

Let's go over what we are doing here.

First, we're creating an instance variable @post and assigning it current_user.posts.create(post_params). There are several parts to this code that probably don't make sense to you right now. Let's try to clear this up.

current_user

current_user in a method that we setup in application_controller. Refer back to this lesson to view this method.

Next, we're having this current_user.posts.create(post_params) code. What this is saying is, create a post by current_user. This automatically hooks up and connects the current_user with the created post. In other words, the user_id of the new Post is set to the id of current_user, so that we know who posted it.

Lastly, we can see that we're passing in post_params as a parameter to create. We've done the same for the Ideator app, but the purpose of post_params is to make sure bad inputs aren't saved in the database. This is related to the topic of Strong Parameters.

Since it's a super important topic that you need to know, take 10 minutes to read more about it here.

Notice how post_params is under private. Putting this private keyword means that the code below it is a private method.

The code after that is pretty straightforward. If @posts is valid (meaning it passes all validations), then it redirects to the root_path, otherwise it renders the new template again with an error message.

Our controller is looking good. Now we need to configure our models. In the section above, I mentioned that current_user.posts.create() connects the current_user and the new post. However, before this code will work, we also need to specify the relationship between the User model and the Post model.

Specifically, we need to tell Rails that User has_many Posts, and Post belongs_to User. These database relations are pretty straightforward, if you think of it simply.

For example, Post has_many Users in this case doesn't really make sense. Instead, the User creates and owns the Post, so it should be the other way around.

So in our post.rb file, let's tell rails that it belongs to User. We can do this by adding the following line:

belongs_to :user

Awesome. Now let's go into our user.rb file and add in the following:

has_many :posts

Great. And that's it! Just like that, we have our database relations setup.

Let's go back to our browsers and go to http://localhost:3000/posts/new to try the form out. It works...or it might not have for you depending on whether you're logged in or not.

In other words, if you're logged in, you won't get an error. If you are logged out though, you'll get an error telling you that there's a problem with current_user.

Let's fix this. Devise provides us with yet another convenient helper method to achieve what we want to do: make sure the user is logged in. All it takes is one line of code!

Since we are not using devise, we will setup the method in the application controller.

In your application_controller.rb, add the method below the current_user method

class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user
    if session[:user_id]
      @current_user ||= User.find(session[:user_id])
    else
      @current_user = nil
    end
  end


  def authenticate_user
    if current_user.nil?
      flash[:error] = 'You must be signed in to view that page.'
      redirect_to login_path
    end
  end
end

In your posts_controller.rb, add the following line of code right under class PostsController < ApplicationController.

before_action :authenticate_user

This line of code will make sure that the user is logged in before it can access the pages related to the PostsController. However, that also means that users won't be able to access the index page unless they are logged in, which we don't want, since that's a page we want all users to be able to see. Let's change the code to be this:

before_action :authenticate_user, only: [:new, :create]

By adding the only: [:new, :create], we have told Rails to make sure the user is logged in, only for the new and create pages.

Let's test this out. Let's first log out of our application, and then try going to http://localhost:3000/posts/new. It should redirect you to the sign in page.

Setting Up Validations

We need to make sure that the data that is passed in to the database is valid. Let's make sure that user_id, photo, and description is present.

In your post.rb, add in the following lines:

validates :photo, :description, presence: true

Your post.rb file should now look like this:

class Post < ApplicationRecord
  belongs_to :user

  has_one_attached :photo

  validates :photo, :description, presence: true
end

Now our database won't save any Posts without a photo and description.

Now that we've hooked up our backend and we've got everything in place, let's create some posts!

First, while we're at it, let's add a link to the navbar so that users can navigate to the New Posts page easily. In our application.html.erb within our <nav> section, let's add a link to the New Posts page. You should already know how to do this by now :)

Just to make sure, your link should look something like this:

<%= link_to 'New Post', new_post_path %>

Check to see that the link is placed within the block that is only displayed when a user is signed in.

For setting up and styling navbar refer Styling the navbar and footer from ideator app.

Modifying and changing the styles of navbar is upto yours practice. Below is the basic design of the navbar

<nav class="bg-white border-gray-200 font-dosis">
  <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-1 md:p-4">
    <a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
      <span class="self-center text-2xl font-semibold whitespace-nowrap font-pacifico text-[30px] tracking-[2px] text-[#337ab7]">Instapost</span>
    </a>
    <button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200" aria-controls="navbar-default" aria-expanded="false">
        <span class="sr-only">Open main menu</span>
        <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
            <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
        </svg>
    </button>
    <div class="hidden w-full md:block md:w-auto" id="navbar-default">
      <ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white">
        <% if current_user %>
          <li>
            <%= link_to 'New Post', new_post_path, class: "block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0" %>
          </li>
          <li>
            <%= link_to 'Log Out', logout_path, method: "delete", class: "block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0" %>
          </li>
        <% else %>
          <li>
            <%= link_to 'Sign Up', signup_path, class: "block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0" %>
          </li>
          <li>
            <%= link_to 'Log In', login_path, class:"block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0" %>
          </li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

Let's now navigate ourselves to the New Posts page (http://localhost:3000/posts/new) and create some mock posts.

Right now, even if we create mock posts, they aren't displayed anywhere in our app. Let's make it so that all of our posts are posted in the homepage (the root page), which is pointed to the index action of the PostsController in routes.rb.

In our posts_controller.rb, let's add the following piece of code:

def index
  @posts = Post.all
end

Post.all will give us all of the posts in the database. In Rails, you can attach .all to a model to get all instances of the object.

Now, we can use @posts in our index.html.erb, which will give us all instances of the Post object.

Let's go into our index.html.erb file and try to display all of our posts. In order to do this, since we can use @posts which gives us all instances of Post, we can create a loop to iterate through the Posts to display the information of each Post.

In your index.html.erb, enter the following lines of code:

<% @posts.each do |post| %>
  <%= image_tag post.photo %>
  <%= post.description %>
<% end %>

Save the file and refresh the page. It's really ugly, but you should now see all of your posts!

Let's also add information about who posted the picture. We can display the user's profile picture as well as the user's username.

<% @posts.each do |post| %>
  <%= image_tag post.user.photo if post.user.photo.attached? %>
  <%= post.user.email.split('@').first %>
  <%= image_tag post.photo %>
  <%= post.description %>
<% end %>

Save the file and refresh the page. We should now see our user's name! Currently, the user does not have any photo so we have added a check if post.user.photo.attached?. This condition is very readable and describes itself. It renders the image_tag only if the user of the post has any attached photo in their model.

post.user - I'm confused

You might be wondering - where did post.user come from? If we look in our schema.rb file, user isn't a database field for the post table. Then how are we accessing the user?

This is another example of Rails magic. Since Post belongs to User, Rails allows you to call the user that owns the Post by doing post.user.

In this case, post is the placeholder variable in the each loop, representing a single Post from the @posts collection.

Similarly, we have not set up name for the user, so we are displaying the first part of their image. Let's refer to it as their display name. post.user.email.split('@') divides email into two separate parts. For example if the email was john.doe@gmail.com, then after splitting, it would be two different parts john.doe and gmail.com since we are splitting it from @. Then, we render the first part i.e. john.doe

Awesome! If you notice though, the oldest post is shown first. In the original Instagram app, newest posts come first. Let's change our app so it behaves like this.

In our posts_controller.rb, let's change our index method to be like this:

def index
  @posts = Post.all.order('created_at DESC')
end

It turns out that we can change the order of our collection of Posts by attaching the order method and giving it the parameter 'created_at DESC'. .order('created_at DESC') is saying, order it by the created_at attribute in descending order.

We can also do the opposite by adding .order('created_at ASC') instead as well.

If we refresh the page, we see that our posts are ordered from newest to oldest.

Awesome! Let's keep on going.

Lesson list