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 inapplication_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
Post
s, and Post
belongs_to
User
. These database relations are pretty straightforward, if you think of it simply.
For example, Post
has_many
User
s 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.
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 Post
s 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 ourschema.rb
file,user
isn't a database field for thepost
table. Then how are we accessing theuser
?This is another example of Rails magic. Since
Post
belongs toUser
, Rails allows you to call theuser
that owns thePost
by doingpost.user
.In this case,
post
is the placeholder variable in theeach
loop, representing a singlePost
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 Post
s 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.