Welcome to the next installment in our Twitter clone series! In this tutorial, we'll build Ribbit from scratch, not using PHP, but with Ruby on Rails. Let's get started!
One quick service announcement before we get started: we won't be styling the UI for the application in this tutorial; that was done in Build a Twitter Clone From Scratch: The Design. I'll let you know if we have to tweak anything from that article.
Step 0: Setting up the Environment
First things first: I'm using Ruby 1.9.3 (p194) and Rails version 3.2.8 for this tutorial. Make sure that you're running the same versions. It doesn't matter how you install Ruby; you can use RVM ( tutorial ), rbenv, or just a regular Ruby installation. No matter the approach, each installer gives you the gem
binary, which you can then use to install Rails. Just use this command:
gem install rails
This installs the latest version of Rails, and we can start building our app now that it is installed. I hope you realize that Rails is a command line tool; you'll need to be comfortable in the terminal to be comfortable in this tutorial.
Step 1: Creating the Rails App
We begin by generating the project. In the command line, navigate to whatever directory you want the new project to reside in. Then, run this:
rails new ribbitApp
This single command generates multiple files inside a folder, called ribbitApp
. This is what Rails gives us to start with; it even installed the gems required for the project.
Let's cd
into that directory and initialize a git repo.
cd ribbitApp git init
One of the Rail-generated files is .gitignore
. If you're on a Mac, you'll probably want to add the following line to this file - just to keep things clean:
**.DS_Store
Now, we're ready to make our first commit!
git add . git commit -m 'initial rails app'
Step 2: Prepping the UI
The interface tutorial introduced you to Ribbit's images and stylesheet. Download those assets and copy the gfx
folder and less.js
and style.less
files into your app's public
directory.
Let's add a rule to style.less
: the style for our flash messages. Paste this at the bottom of the file:
.flash { padding: 10px; margin: 20px 0; &.error { background: #ffefef; color: #4c1717; border: 1px solid #4c1717; } &.warning { background: #ffe4c1; color: #79420d; border: 1px solid #79420d; } &.notice { background: #efffd7; color: #8ba015; border: 1px solid #8ba015; } }
That's all! Let's make another commit:
git add . git commit -m 'Add flash styling'
Now, let's create the layout. This is the HTML that wraps the main content of every page - essentially, the header and footer. A Rails app stores this in app/views/layouts/application.html.erb
. Get rid of everything in this file, and add the following code:
<!DOCTYPE html> <html> <head> <link rel="stylesheet/less" href="/style.less"> <script src="/less.js"></script> </head> <body> <header> <div class="wrapper"> <img src="/gfx/logo.png"> <span>Twitter Clone</span> </div> </header> <div id="content"> <div class="wrapper"> <% flash.each do |name, msg| %> <%= content_tag :div, msg, class: "flash #{name}" %> <% end %> <%= yield %> </div> </div> <footer> <div class="wrapper"> Ribbit - A Twitter Clone Tutorial<img src="/gfx/logo-nettuts.png"> </div> </footer> </body> </html>
There are three things you should notice about this. First, every URL to a public asset (images, stylesheet, JavaScript) begins with a forward slash (/
). This is so that we can still load the assets when we're in "deeper" routes. Second, notice the markup for displaying the flash messages. This displays flash messages when they exist. And third, note the <%= yield %>
; this is where we insert other "sub"-templates.
Okay, let's commit this:
git add . git commit -m 'Edit application.html.erb'
Step 3: Creating Users
Of course, we can't have a Twitter clone without users, so let's add that functionality. This can be a complex feature, but Rails makes it a little easier for us.
We naturally don't want to store our users' passwords as plain text; that's a huge security risk. Instead, we'll rely on Rails to automate an encryption process for our passwords. We begin by opening our Gemfile
and searching for these lines:
# To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0'
Using has_secure_password
is exactly what we want to do, so un-comment that second line. Save the file and head back to the command line. Now we have to run bundle install
to install the bcrypt gem.
We can create our user resource, once the gem has been installed.
A Rails resource is basically a model, its associated controller, and a few other files.
We create a resource by using the rails generate
(or rails g
) command:
rails generate resource user username name email password_digest avatar_url
We pass several parameters to this command, resulting in a resource
, called user
; its model has the following five fields:
-
username
: a unique username, the equivalent of a Twitter handle. -
name
: the user's actual name. -
email
: their email address. -
password_digest
: the encrypted version of their password. -
avatar_url
: the path to their avatar image.
We now need to migrate our database so that it's set up to store users. We accomplish this by running the following command:
rake db:migrate
This creates our users
table, but we don't directly interact with the database with Rails. Instead, we use the ActiveRecord ORM. We need to add some code to app/models/user.rb
. Open that file.
When Rails generated this file, it added a call to the attr_accessible
method. This method determines which properties are readable and writable on this class' instances. By default, all the aforementioned properties are accessible, but we want to change that:
attr_accessible :avatar_url, :email, :name, :password, :password_confirmation, :username
Most of these should make sense, but what's with password
and password_confirmation
? After all, we have only a password_digest
field in our database. This is part of the Rails magic that I mentioned earlier. We can set a password
and password_confirmation
field on a User
instance. If they match, the password will be encrypted and stored in the database. But to enable this functionality, we need to add another line to our User
model class:
has_secure_password
Next, we want to incorporate validation into our model. Calling has_secure_password
takes care of the password fields, so we'll deal with the email
, username
, and name
fields.
The name
field is simple: we just want to ensure that it is present.
validates :name, presence: true
For the username
field, we want to ensure that it exists and is unique; no two users can have the same username:
validates :username, uniqueness: true, presence: true
Finally, the email
field not only needs to exist and be unique, but it also needs to match a regular expression:
validates :email, uniqueness: true, presence: true, format: { with: /^[\w.+-]+@([\w]+.)+\w+$/ }
This is a very simplistic email regex, but it should do for our purposes.
Now, what about that avatar_url
field? We want to use the user's email address and pull their associated Gravatar, so we have to generate this URL. We could do this on the fly, but it will be more efficient to store it in the database. First, we need to make sure that we have a clean email address. Let's add a method to our User
class:
private def prep_email self.email = self.email.strip.downcase if self.email end
That private
keyword means that all the methods defined after the keyword are defined as private methods; they cannot be accessed from outside the class (on instances).
This
prep_email
method trims the whitespace at the beginning and end of the string, and then converts all characters to lowercase.
This is necessary because we're going to generate a hash for this value.
We want this method to run just before the validation process; add the following line of code near the top of the class.
before_validation :prep_email
Next, let's generate the URL for the avatar by writing another method (put it under the one above):
def create_avatar_url self.avatar_url = "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(self.email)}?s=50" end
We need to call this method before we save the user to the database. Add this call to the top of the file:
before_save :create_avatar_url
And that's it! Now we have the mechanics of our user functionality in place. Before writing the UI for creating users, let's commit our work so far:
git add . git commit -m 'Create user resource'
Step 5: Writing The User UI
Building a UI in Rails means that we need to be aware of the routes that render our views. If you open config/routes.rb
, you'll find a line like this:
resources :users
This was added to the routes.rb
file when we generated the user resource, and it sets up the default REST routes. Right now, the route we're interested in is the route that displays the form for creating new users: /users/new
. When someone goes to this route, the new
method on the users controller will execute, so that's where we'll start.
The users controller is found in app/controllers/users_controller.rb
. It has no methods by default, so let's add the new
method within the UsersController
class.
def new @user = User.new end
As you know, Ruby instance variables begin with @
- making @user
available from inside our view. Let's head over to the view by creating a file, named new.html.erb
in the app/views/users
folder. Here's what goes in that view:
<img src="/gfx/frog.jpg"> <div class="panel right"> <h1>New to Ribbit?</h1> <%= form_for @user do |f| %> <% if @user.errors.any? %> <ul> <% @user.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> <%= f.text_field :email, placeholder: "email" %> <%= f.text_field :username, placeholder: "username" %> <%= f.text_field :name, placeholder: "name" %> <%= f.password_field :password, placeholder: "password" %> <%= f.password_field :password_confirmation, placeholder: "password" %> <%= f.submit "Create Account" %> <% end %> </div>
This is actually the view that serves as the home page view, when a user is not logged in. We use the form_for
helper method to create a form and pass it the @user
variable as a parameter. Then, inside the form (which is inside the Ruby block), we first print out errors. Of course, there won't be any errors on the page the first time around.
However, any input that fails our validation rules results in an error message that is displayed in a list item.
Then, we have the fields for our user properties. Since this design doesn't have any labels for the text boxes, I've put what would be label text as the fields' placeholder (using the placeholder
attribute). These won't display in older browsers, but that's not relevant to our main goal here.
Now, what happens when the user clicks the "Create Account" button? This form will POST
to the /users
route, resulting in the execution of the create
method in the users controller. Back to that controller, then:
def create @user = User.new(params[:user]) if @user.save redirect_to @user, notice: "Thank you for signing up for Ribbit!" else render 'new' end end
We start by creating a new user, passing the new
method the values from our form. Then, we call the save
method. This method first validates the input; if the data is in the correct format, the method inserts the record into the database and returns true
. Otherwise, it returns false
.
If
@user.save
returnstrue
, we redirect the viewer to... the@user
object itself?
This actually redirects to the path for that user, which will be /users/
. If @user.save
returns false, we re-render the /users/new
path and display any validation errors. We also pre-populate the form fields with the the user's previously provided information. Clever, eh?
Well, if we direct the viewer to their new user profile, we need to create that page next. This triggers the show
method in the users controller, so we'll add that first:
def show @user = User.find(params[:id]) end
This method looks at the id number in the route (for example, /users/4
) and finds the associated user in the database. Just as before, we can now use this @user
variable from the view.
Create the app/views/users/show.html.erb
file, and add the following code:
<div id="createRibbit" class="panel right"> <h1>Create a Ribbit</h1> <p> <form> <textarea name="text" class="ribbitText"></textarea> <input type="submit" value="Ribbit!"> </form> </p> </div> <div id="ribbits" class="panel left"> <h1>Your Ribbit Profile</h1> <div class="ribbitWrapper"> <img class="avatar" src="<%= @user.avatar_url %>"> <span class="name"><%= @user.name %></span> @<%= @user.username %> <p> XX Ribbits <span class="spacing">XX Followers</span> <span class="spacing">XX Following</span> </p> </div> </div> <div class="panel left"> <h1>Your Ribbits</h1> <div class="ribbitWrapper"> Ribbits coming . . . </div> </div>
You'll notice that we have a few placeholders in this view. First, there's the form for creating a new ribbit. Then there's the follower, following numbers, and the list of your ribbits. We'll come to all this soon.
There's one more thing to do in this step: we want the root route (/
) to show the new user form for the time being. Open the config/routes.rb
file again, and add this line:
root to: 'users#new'
This simply makes the root route call the new
method in the users controller. Now, we just need to delete the public/index.html
file which overrides this configuration. After you delete that, run the following in the command line:
rails server
You could also run rails s
to achieve the same results. As you would expect, this starts the rails server. You can now point your browser to localhost:3000/
, and you should see the following:
Now, fill in the form and click "Create Account." You should be sent to the user profile, like this:
Great! Now we have our user accounts working. Let's commit this:
git add . git commit -m 'User form and profile pages'
Step 6: Adding Session Support
Even though we implemented the user feature, a user cannot log in just yet. So, let's add session support next.
You may have recognized the way we've created user accounts.
I've taken this general method from Railscast episode 250. That episode also demonstrates how to create session support, and I'll use that approach for Ribbit.
We start by creating a controller to manage our sessions. We won't actually store sessions in the database, but we do need to be able to set and unset session variables. A controller is the correct way to do that.
rails generate controller sessions new create destroy
Here, we create a new controller, called sessions
. We also tell it to generate the new
, create
, and destroy
methods. Of course, it won't fill in these methods, but it will create their "shell" for us.
Now, let's open the app/controllers/sessions_controller.rb
file. The new
method is fine as is, but the create
method needs some attention. This method executes after the user enters their credentials and clicks "Log In." Add the following code:
def create user = User.find_by_username(params[:username]) if user && user.authenticate(params[:password]) session[:userid] = user.id redirect_to rooturl, notice: "Logged in!" else flash[:error] = "Wrong Username or Password." redirect_to root_url end end
We use the find_by_username
method on the User
class to retrieve the user with the provided username. Then, we call the authenticate
method, passing it the password. This method was added as part of the use_secure_password
feature. If the user's credentials pass muster, we can set the user_id
session variable to the user's ID. Finally, we redirect to the root route with the message "Logged in!".
If the user's credentials fail to authenticate, we simply redirect to the root route and set the flash error message to "Username or password was wrong."
Logging out fires the destroy
method. It's a really simple method:
def destroy session[:userid] = nil redirect_to root_url, notice: "Logged out." end
This code is fairly self-explanatory; just get rid of that session variable and redirect to the root.
Rails helped us out once again and added three routes for these methods, found in config/routes.rb
:
get "sessions/new" get "sessions/create" get "sessions/destroy"
We want to change the sessions/create
route to POST, like this:
post "sessions/create"
We're almost ready to add the login form to our views. But first, let's create a helper method that allows us to quickly retrieve the currently logged-in user. We'll put this in the application controller so that we can access it from any view file. The path to the application controller is app/controllers/application_controller.rb
.
private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user
We pass User.find
the session[:user_id]
variable that we set in the sessions controller. The call to helper_method
is what makes this a helper method that we can call from the view.
Now, we can open our app/views/layouts/application.html.erb
file and add the login form. See the <span>Twitter Clone</span>
in the <header>
element? The following code goes right after that:
<% if current_user %> <%= link_to "Log Out", sessions_destroy_path %> <% else %> <%= form_tag sessions_create_path do %> <%= text_field_tag :username, nil, placeholder: "username" %> <%= password_field_tag :password, nil, placeholder: "password" %> <%= submit_tag "Log In" %> <% end %> <% end %>
If the user is logged in (or, if a non-nil
value is returned from current_user
), we'll display a logout link, but we'll add more links here later. You might not have seen the link_to
method before; it takes the provided text and URL and generates a hyperlink.
If no user is logged in, we use the
form_tag
method to create a form that posts to theseesions_create_path
.
Note that we can't use the form_for
method because we don't have an instance object for this form (like with our user object). The text_field_tag
and password_field_tag
methods accept the same parameters: the name for the field as a symbol, the value for the field (nil
in this case), and then an options object. We're just setting a placeholder value here.
There's a bit of a glitch in our session support: a user is not automatically logged in, after they create a new user account. We can fix this by adding a single line to the create
method in the UserController
class. Right after the if @user.save
line, add:
session[:user_id] = @user.id
Believe it or not, the above line of code finishes the session feature. You should now be able to re-start the Rails server and log in. Try to log out and back in again. The only difference between the two functions is the lack of the login form when you're logged in. But we'll add more later!
Let's commit this:
git add . git commit -m 'users are now logged in upon creation'
Step 7: Creating Ribbits
Now we're finally ready to get to the point of our application: creating Ribbits (our version of tweets). We begin by creating the ribbit resource:
rails g resource ribbit content:text user_id:integer rake db:migrate
This resource only needs two fields: the actual content of the ribbit, and the id of the user who created it. We'll migrate the database to create the new table. Then, we'll make a few modifications to the new Ribbit model. Open app/models/ribbit.rb
and add the following:
class Ribbit < ActiveRecord::Base default_scope order: 'createdat DESC' attr_accessible :content, :userid belongs_to :user validates :content, length: { maximum: 140 } end
The default_scope
call is important; it orders a list of ribbits in from the most recent to least recent. The belongs_to
method creates an association between this Ribbit class and the User class, making our user objects have a tweets
array as a property.
Finally, we have a validates
call, which ensures that our ribbits don't exceed 140 characters.
Oh, yeah: the flip side of the belongs_to
statement. In the User class (app/models/user.rb
), we want to add this line:
has_many :ribbits
This completes the association; now each user can have many ribbits.
We want users to have the ability to create ribbits from their profile page. As you'll recall, we have a form in that template. So let's replace that form in app/view/users/show.html.erb
, as well as make a few other changes. Here's what you should end up with:
<% if current_user %> <div id="createRibbit" class="panel right"> <h1>Create a Ribbit</h1> <p> <%= form_for @ribbit do |f| %> <%= f.textarea :content, class: 'ribbitText' %> <%= f.submit "Ribbit!" %> <% end %> </p> </div> <% end %> <div id="ribbits" class="panel left"> <h1>Your Ribbit Profile</h1> <div class="ribbitWrapper"> <img class="avatar" src="<%= @user.avatar_url %>"> <span class="name"><%= @user.name %></span> @<%= @user.username %> <p> <%= @user.ribbits.size %> Ribbits <span class="spacing">XX Followers</span> <span class="spacing">XX Following</span> </p> </div> </div> <div class="panel left"> <h1>Your Ribbits</h1> <% @user.ribbits.each do |ribbit| %> <div class="ribbitWrapper"> <img class="avatar" src="<%= @user.avatar_url %>"> <span class="name"><%= @user.name %></span> @<%= @user.username %> <span class="time"><%= time_ago_in_words(ribbit.created_at) %></span> <p> <%= ribbit.content %> </p> </div> <% end %> </div>
There are three areas that we change with this code. First, we remove the HTML form at the top and replace it with a call to the form_for
helper function. Of course, it makes sense that we only need a text area for the content (we already know the current user's ID). Note that form_for
accepts a @ribbit
as the parameter, and we need to add that object to the users_controller#show
method (app/controllers/users_controller.rb
):
def show @user = User.find(params[:id]) @ribbit = Ribbit.new end
Notice that we wrap the whole form section (the <div id="createRibbit">
) with an if
statement. If there's no current user (meaning no one is logged in), we won't show the ribbit form.
Next, we want to display the number of the user's ribbits. That number appears just above their follower count. Remember that our user instance has a ribbits
property. So, we can replace our filler text with this:
<%= @user.ribbits.size %> Ribbits
We need to show the user's ribbits. We can loop over that same ribbits
array, and display each ribbit in turn. That's the final part of the code above.
Lastly, (at least as far as ribbit creation is concerned), we need to modify the create
method in the ribbits controller (app/controllers/ribbits_controller.rb
. The method executes when the user clicks the "Ribbit!" button.
def create @ribbit = Ribbit.new(params[:ribbit]) @ribbit.userid = current_user.id</p> if @ribbit.save redirect_to current_user else flash[:error] = "Problem!" redirect_to current_user end end
I know the "Problem!" error message isn't very descriptive, but it will do for our simple application. Really, the only error that could occur is a ribbit longer than 140 characters.
So, give it a try: start the server (rails server
), log in, go to your profile page (http://localhost:3000/users/
, but of course, any user profile page will do), write a ribbit, and click "Ribbit!". The new ribbit should display in the ribbit list on your profile page.
Okay, let's commit these changes:
git add . git commit -m 'ribbit functionality created'
Step 8: Creating the Public Tweets Page
Next up, we want to create a public page that includes all the ribbits made by all users. Logically, that should be the ribbits index view, found at /ribbits
. The controller method for this is ribbits_controler#index
. It's actually a very simple method:
def index @ribbits = Ribbit.all include: :user @ribbit = Ribbit.new end
The first line fetches all the ribbits and their associated users, and the second line creates the new ribbit instance (this page will have a ribbit form).
The other step, of course, is the template (app/view/ribbits/index.html.erb
). It's similar to the user profile template:
<% if current_user %> <div id="createRibbit" class="panel right"> <h1>Create a Ribbit</h1> <p> <%= form_for @ribbit do |f| %> <%= f.textarea :content, class: 'ribbitText' %> <%= f.submit "Ribbit!" %> <% end %> </p> </div> <% end %> <div class="panel left"> <h1>Public Ribbits</h1> <% @ribbits.each do |ribbit| %> <div class="ribbitWrapper"> <a href="<%= user_path ribbit.user %>"> <img class="avatar" src="<%= ribbit.user.avatar_url %>"> <span class="name"><%= ribbit.user.name %></span> </a> @<%= ribbit.user.username %> <span class="time"><%= time_ago_in_words(ribbit.created_at) %></span> <p> <%= ribbit.content %> </p> </div> <% end %> </div>
In this template, the avatar image and the user's name are surrounded by a link that points to the user's profile page. Let's also add a link to the public tweets page. Just before the logout link in app/view/layouts/application.html.erb
, add this:
<%= link_to "Public Ribbits", ribbits_path %>
Finally:
git add . git commit -m 'added the public ribbits page'
Step 9: Following Other Users
It wouldn't be a Twitter clone if we couldn't follow other users, so let's work on that feature next.
This is a little tricky at first. Think about it: our User
records need to to follow other User
records and be followed by other User
records. It's a many-to-many, self-joining association. This means we'll need an association class, and we'll call it "Relationship". Start by creating this resource:
rails g resource relationship follower_id:integer followed_id:integer rake db:migrate
This model only needs two fields: the follower's ID, and the followed's ID (note: the terminology here can get a little confusing. In our case, I'm using the term "followed" to mean the user being, well, followed).
Next up, we want to relate the User
model with this Relationship
model, which we do from both sides. First, in the Relationship
class (app/models/relationship.rb
), we want to add these two lines:
belongs_to :follower, classname: "User" belongs_to :followed, classname: "User"
The first line relates a User
record to the follower_id
field, and the second line relates a User
record to the followed_id
field. It's important to include the class name, because Rails can't infer the class from the property names ('follower' and 'followed'). It can, however, infer the correct database fields (follower_id
and followed_id
) from those names.
Now, in the User
class (app/model/user.rb
), we have to first connect each user model to its associated relationships:
has_many :follower_relationships, classname: "Relationship", foreign_key: "followed_id" has_many :followed_relationships, classname: "Relationship", foreign_key: "follower_id"
We need to create two associations, because we have two sets of relationships per user: all the people following them and all the people they follow. And no, those foreign keys shouldn't be switched. The follower_relationship
association is responsible for all of your followers. Hence, it needs the followed_id
foreign key.
Then, we can use those relationships to get to the followers on the other side of them:
has_many :followers, through: :follower_relationships has_many :followeds, through: :followed_relationships
These give our user records the followers
and followeds
methods. They're both methods that return the arrays of our followers or the people we follow, respectively.
Finally, let's add two methods to our user model that helps us with the UI:
def following? user self.followeds.include? user end def follow user Relationship.create follower_id: self.id, followed_id: user.id end
Let's commit these changes before tweaking the UI.
git add . git commit -m 'created user relationships infrastructure'
Now, the UI is all on the user profile pages, which is app/views/users/show.html.erb
. We'll start with something simple: the follower and following count. See where we have this?
<span class="spacing">XX Followers</span> <span class="spacing">XX Following</span>
We'll replace these placeholder values, like so:
<span class="spacing"><%= @user.followers.count %> Followers</span> <span class="spacing"><%= @user.followeds.count %> Following</span>
We have the follow/unfollow button under these counts, but there are a few states we need to consider. First, we don't want to show any button if the user either is viewing their own profile or if they're not logged in. Second, we want to display an "Unfollow" button if the user already follows this profile's owner.
<% if current_user and @user != current_user %> <% if current_user.following? @user %> <%= form_tag relationship_path, method: :delete do %> <%= submit_tag "Unfollow" %> <% end %> <% else %> <%= form_for @relationship do %> <%= hidden_field_tag :followed_id, @user.id %> <%= submit_tag "Follow" %> <% end %> <% end %> <% end %>
A Rails resource is a basically a model, its associated controller, and a few other files.
Put this code just under the paragraph that holds the above spans.
The forms are the more complex parts here. First, if the current user already follows the viewed user, we'll use form_tag
to create a form that goes to the relationship_path
. Of course, we can't forget to set the method
as delete
because we're deleting a relationship.
If the current user doesn't follow the viewed user, we'll create a form_for
the current relationship. We'll simply use a hidden field to determine which user to follow.
If you're paying attention, you'll know that something's missing: the ability to manipulate a relationship instance from this view. We need a Relationship
instance. If the current user doesn't already follow this user, we need to create a blank relationship. Otherwise, we need to have a relationship on hand to delete! Back to app/controllers/users_controller.rb
, and add the following to the show
method:
@relationship = Relationship.where( follower_id: current_user.id, followed_id: @user.id ).first_or_initialize if current_user
This is a bit different from the usual way of finding or creating a record. This initializes a blank Relationship
instance if no records are found that match the where
parameters. Of course, we only want to do this if there is a current_user
.
The routes for this model are enabled by the resources :relationship
line in the config/routes.rb
, so we don't have to worry about that.
Now, in app/controllers/relationships_controller.rb
, we'll start with the new
method:
def create @relationship = Relationship.new @relationship.followed_id = params[:followed_id] @relationship.follower_id = current_user.id</p> if @relationship.save redirect_to User.find params[:followed_id] else flash[:error] = "Couldn't Follow" redirect_to root_url end end
Pretty standard stuff by now, right? We'll create the relationship, save it, and redirect back to the user's profile.
The destroy
method is also simple:
def destroy @relationship = Relationship.find(params[:id]) @relationship.destroy redirect_to user_path params[:id] end
Now, create another user (or four) and have a few users follow other users. You should see the text of the follow buttons change, as well as the follower / following count.
Great! Now we can commit this feature:
git add . git commit -m 'Following other users is now working'
Step 10: Creating a Few Other Pages
There are a few other simple pages that we want to add. First, let's create a page to list all the registered users. This would be a great place to find new friends, see their pages, and eventually follow them. Logically, this should be the /users
route, so we'll use the UsersController#index
method:
def index @users = User.all end
Now, for app/views/users/index.html.erb
:
<div id="ribbits" class="panel left"> <h1>Public Profile</h1> <% @users.each do |user| %> <div class="ribbitWrapper"> <a href="<%= user_path user %>"> <img class="avatar" src="<%= user.avatar_url %>"> <span class="name"><%= user.name %></span> </a> @<%= user.username %> <p> <%= user.ribbits.size %> Ribbits <span class="spacing"><%= user.followers.count %> Followers</span> <span class="spacing"><%= user.followeds.count %> Following</span> </p> <% if user.ribbits.first %> <p><%= user.ribbits.first.content %></p> <% end %> </div> <% end %> </div>
Finally, let's add a link to this page to the top of our template. Let's also add a link to the logged-in user's profile. Right beside the "Public Ribbits" link, add:
<%= link_to "Public Profiles", users_path %> <%= link_to "My Profile", current_user %>
Next is the buddies page. This is where a user goes to view the ribbits of the people they follow; we'll also redirect users to this page when they're logged in and view the home page.
Strangely, finding the correct place in the code for this page is a bit tricky. After all, each page in our Rails app must be based on a method in one of our controllers. Best practice dictates that each controller has six REST methods that control a resource. In this case, we want to view the ribbits of a subset of users, which, at least to me, seems to be a bit of an edge case. Here's how we'll handle it: let's create a buddies
method in the UsersController
:
def buddies if current_user @ribbit = Ribbit.new buddies_ids = current_user.followeds.map(&:id).push(current_user.id) @ribbits = Ribbit.find_all_by_user_id buddies_ids else redirect_to root_url end end
Obviously, there's nothing to show if a user isn't logged in, so we'll check the current_user
. If we're not logged in, we redirect to the root URL (/
). Otherwise, we create a new ribbit (for our new ribbit form).
We then need to find all ribbits from the current user and the people they follow.
We can use the followeds
array property, and map it to only retrieve the user ids. Then, we push in the current user's ids as well and finally retrieve the ribbits from those users.
Let's store the template in app/views/users/buddies.html.erb
; it's very similar to our public ribbits template:
<% if current_user %> <div id="createRibbit" class="panel right"> <h1>Create a Ribbit</h1> <p> <%= form_for @ribbit do |f| %> <%= f.textarea :content, class: 'ribbitText' %> <%= f.submit "Ribbit!" %> <% end %> </p> </div> <% end %> <div class="panel left"> <h1>Buddies' Ribbits</h1> <% @ribbits.each do |ribbit| %> <div class="ribbitWrapper"> <a href="<%= user_path ribbit.user %>"> <img class="avatar" src="<%= ribbit.user.avatar_url %>"> <span class="name"><%= ribbit.user.name %></span> </a> @<%= ribbit.user.username %> <span class="time"><%= time_ago_in_words(ribbit.created_at) %></span> <p> <%= ribbit.content %> </p> </div> <% end %> </div>
We need to make a route for this method, in order to use it. Open config/routes.rb
and add the following:
get 'buddies', to: 'users#buddies', as: 'buddies'
Now, we can go to /buddies
and see the page!
There's something else we want to do with this, however. If a logged-in user goes to the root URL, we need to redirect them to /buddies
. Remember, this route is currently:
def new @user = User.new end
Let's change it to this:
def new if current_user redirect_to buddies_path else @user = User.new end end
We should also add a link to the buddies page in app/views/layouts/application.html.erb
:
<%= link_to "Buddies' Ribbits", buddies_path %>
And now, we'll commit these changes:
git add . git commit -m 'added buddies page'
Step 11 Deploying to Heroku
The last step is to deploy the application. We'll use Heroku.
The last step is to deploy the application. We'll use Heroku. I'm going to assume that you have a Heroku account, and that you've installed the Heroku toolbelt (the command line tools).
We run into a problem before we even begin! We've been using a SQLite database, because Rails uses SQLite by default. However, Heroku doesn't use SQLite; it uses PostgreSQL for the database. We have to make a change to our Gemfile, a change that actually breaks our local copy of the app (unless you install and configure a PostgreSQL server). Here's my compromise: I'll show you how to do it here, and you can play with my deployed version. But you don't have make the change on your local project.
Thankfully, switching Rails to PostgreSQL is very simple. In our Gemfile
, there's a line that looks like this:
gem "sqlite"
Change that line to this:
gem "pg"
We now must install this gem locally, in order to update Gemfile.lock
. We do this by running:
bundle install
And we commit:
git add . git commit -m 'updated Gemfile with Postgres'
We can now create our Heroku application. In our project directory, run:
heroku create git push heroku master
And finally:
heroku open
That opens your browser with your deployed Heroku application. You can play with my deployed copy.
That's It!
And there you go! We just built a really simple Twitter clone. Sure, there are dozens of features we could add to this, but we nailed the most important pieces: users, ribbits, and followings.
Remember, if Rails isn't your racket, check out the other tutorials in this series! We have a whole line-up of Ribbit tutorials using different languages and frameworks in the pipes. Stay tuned!
Comments