For today’s lab, we are going to create a basic version of Twitter.

Create ability to Submit tweets

The first thing we need to do is to create a new rails application using the new_repo twitter and rails new twitter -d mysql commands, and then configure the database correctly in config/database.yml. Don’t forget to create the database using the rake db:create command.

Next, we can create a resource to store Tweets – snippets of text submitted by the user. Tweets are very simple, they have two pieces of information: the content of the tweet (which we are going to limit to 160 characters), and the date/time the tweet was posted. One of the nice things about rails is that it automatically stores the time a record is created in the database in the created_at column, so we don’t necessarily have to write the code for that. But we are going to anyway so you can see how its done.

$ rails generate resource Tweet content:string posted:datetime

This will create the Tweet model, which will have two columns: the content of the tweet, and the time when that tweet was posted. Verify that the table looks correct in your db/migrate/..._create_tweets.rb file, and then run rake db:migrate to create the table.

We also want to limit tweets to 160 characters. We can do this with a validation in the app/models/tweet.rb file:

class Tweet < ActiveRecord::Base
  validates :content, length: {maximum: 160}
end

Next, we need to fill in our basic controller for submitting tweets. We really only need three actions: display the “new tweet” form, create a new tweet, and view the index of all the tweets in the system. So, let’s create this controller.

Submitting a new tweet

Now, we need to create a simple form interface to submit a new tweet. The new action will display the form, and the create action will actually save the new tweet. Let’s begin with the new action in the app/controllers/tweets_controller.rb controller:

def new
  @tweet = Tweet.new
end

Then we can display a form where the user can enter the tweet in new.html.erb:

<h2> New Tweet </h2>

<%= form_for @tweet do |f| %>
  <% if @tweet.errors.any? %>
    <div id="errorExplanation">
      <h2><%= pluralize(@tweet.errors.count, "error") %> prohibited this user from being saved:</h2>
      <ul>
      <% @tweet.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
    <% end %>

  <%= f.label :content, "What's happening?" %> <br />
  <%= f.text_area :content, rows: 3, cols: 70 %> <br />
  <%= f.submit "Tweet" %>
<% end %>
<%= link_to "Back", tweets_url %>

Note two things: First, this form submits to the create action, and we still need to write this action. Second, we don’t have a form field for the posted column. We will fill that in later automatically.

Now, we need to write our post action that actually saves the new tweet:

  def create
    tweet_params = params.require(:tweet).permit(:content)
    @tweet = Tweet.new tweet_params
    @tweet.posted = Time.now
    if @tweet.save
      redirect_to tweets_url
        else
      render action: 'new'
    end
  end

Finally, we need to display the list of all the tweets in the system for the index action. We begin by grabbing the list of all the tweets:

  def index
    @tweet_list = Tweet.all
  end

And then we can display them in index.html.erb:

<h2> Rick's Twitter </h2>

<%= link_to "Add a new tweet", new_tweet_url %>

<br /> <br />
<h4> Public Timeline </h4>
<% @tweet_list.each do |tweet| %>
  <div class="tweet">
    <div class="content">
     <%= tweet.content %>
    </div>
    <span style="font-size:small;">
     <%= tweet.posted %>
        </span>
  </div>
<hr />
<% end %>

Ok, now this should work. Go and test it. Try submitting a few tweets, and see if they appear in the public timeline.

Pages of tweets

OK, in testing, on of the first things you probably noticed is the order of the tweets. (You only noticed this is you submitted more than one tweet.) The tweets are in the order that you submit them; this is not the order we want. We want them in reverse chronological order! To do this, we can make one small change in the controller:

@tweet_list = Tweet.order(posted: :desc).all

When we retrieve the list of all the tweets, we tell the database that we want them ordered by the posted column (which contains the date/time when they were posted), and the order should be desc. desc means descending order, from last to first. It is the opposite of asc or ascending order, from first to last.

Now, keep testing this really simple twitter. Submit 10 or 12 new tweets. I’m sure you can come up with something interesting to say. Notice that our list of tweets is getting long. Now imagine being successful like twitter – it has 10 billion tweets so far. We don’t always want to display ALL the tweets. We should just display the most recent ones, and allow the user to go back to previous pages.

The first thing we can do is limit the size of the tweet list. We will only display the 10 newest tweets:

@tweet_list = Tweet.order(posted: :desc).limit(10).all

We can limit our database query to only return the first 10 results, which because of our ordering is the 10 newest tweets. Try this out and see if it works. Notice that to really test this, you need to submit at least 10 tweets to the system, and see if the oldest ones disappear. Also, try adding new tweets and make sure that the newest tweets appear, and that the older ones go away.

We can also use the offset() function to start getting tweets after the first one. For example, we can get the second page of tweets by saying Tweet.order(posted: :desc).offset(10).limit(10).all.

However, there’s an easier way. We can use a 3rd party library / gem: kaminari. This gem will make it really easy to separate our tweets into pages.

First, we need to add the gem to our Gemfile:

gem 'kaminari'

We need to run the command bundle install to actually install the gem, and restart our server so it knows to load it.

Next, we can add the ability to view older pages of tweets. Do do this, we will add an optional parameter called page to the index action. Basically, when we link to the index action using tweets_url, we can add an additional parameter: :page => 5 to get a specified page number. Here is the new code for the index action:

  # Optional parameter: :page, a number that specifies which page of tweets to view
  def index
    @tweet_list = Tweet.order(posted: :desc).page(params[:page]).per(10)
  end

Try it out: see if your tweets list only displays the most recent 10 tweets.

Next, we need to do one more thing. We need to add a “next page” and “previous page” links to our index view. Our kaminari gem makes this easy to do:

<%= paginate @tweet_list %>

OK, now go and test. See if you can view multiple pages of tweets. Note that you’ll have to add at least 10-20 tweets in order to properly test this, so you have more than one page to view.

Swap Drivers

Now, it is time to swap drivers, so the person that was navigating will now drive, and the person was was driving will become navigator and have a chance to think more about the lab. Don’t forget to git add ., git commit, and git push the code.

Create Users

Next, we are going to add users to the system. To start with, we first need to add a model to store the user information in the database:

$ rails generate resource user username:string password:string real_name:string

Check that the db/migrate file that was created contains the correct columns, and then run rake db:migrate. Now we have a model that stores user information.

Next, we need to edit the controller that will contain the code for managing our users: creating users, logging in, and viewing a user. Note that we want 5 new actions that users should be able to do. We will do the first 3 in the users controller:

We will begin by creating the first two actions: new and create. These actions look pretty familiar based on our previous work:

  def new
    @user = User.new
  end

  def create
    user_params = params.require(:user).permit(:username, :password, :real_name, :password_confirmation)
    @user = User.new user_params
    if (@user.save)
      redirect_to tweets_url
    else
      render action: 'new'
    end
  end

This is the controller code for our new and create actions. It looks exactly like our previous new and create actions. Our new view will also look very familiar:

<h2> New user </h2>

<%= form_for @user do |f| %>
 <% if @user.errors.any? %>
  <div id="errorExplanation">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>
  <%= f.label :username, "Username" %> <%= f.text_field :username %><br />
  <%= f.label :real_name, "Real Name" %> <%= f.text_field :real_name %><br />
  <%= f.label :password, "Password" %> <%= f.password_field :password %> <br />
  Confirm: <%= f.password_field :password_confirmation %>  <br />
  <%= f.submit %>
<% end %>
<hr />
<%= link_to "Cancel", tweets_url %>

There is one thing that’s slightly odd about this form. We ask for both a password and a password_confirmation. This is normal when asking for passwords, to make sure the user typed in their password correctly. But we need to verify that both passwords that are entered are the same! To do that, we can use a validator in the model:

class User < ActiveRecord::Base
  validates :username, :presence => true, :uniqueness => true
  validates :real_name, :presence => true
  validates :password, :presence => true, :confirmation => true
end

Once you’ve written this code, don’t forget to test it. Try adding a user and see if it works. Make sure the password confirmation works correctly – by typing in two different passwords and see if you get an error.

Provide the Ability to Log In

Next, we can add the ability to log in. To do this, we need to create a special controller (NOT a resource, but just a controller) for handling logins and logouts:

$ rails generate controller auth login logout

This creates a new controller called the “auth” controller, with two actions: “login” and “logout”. It also created corresponding routes in config/routes.rb. Delete those two automatically added routes, and add them as follows, along with a third route:

get "login" => "auth#login", :as => "login"
get "logout" => "auth#logout", :as => "logout"
post "do_login" => "auth#do_login", :as => "do_login"

The first two – login_url and logout_url – are exactly what they sound like. Login displays the login form, and logout logs the user out. The third, do_login, is where the login form submits to. Now, let’s begin by creating the login form.

We need to provide the form where the user can enter their username and password to login. The form is pretty simple and straightforward. We are going to use form_tag rather than form_for (like we did at the beginning of the semester), so that means we need to use text_field_tag rather than f.text_field. Also, we are going to display any string that is in flash[:notice]; if there is an error message, we can put the error message there and it will be displayed. So, in app/views/auth/login.html.erb, add:

 <h1> Log In </h1>
 
 <div class="notice"><%= flash[:notice] %></div>
 <%= form_tag(do_login_url, method: "post") do %>
   Username: <%= text_field_tag(:username) %><br />
   Password: <%= password_field_tag(:password) %><br />
   <%= submit_tag("Log In") %>
 <% end %>
 <%= link_to "Create New User", new_user_url %>

Next, we can process the log in. Notice that the form submits to a different action – do_login in the auth_controller.rb – so most of the login code needs to go there. However, that action will redirect the user back to login if there is an error.

  def login
  end

  def do_login
    username_from_form = params[:username]
    password_from_form = params[:password]
    @user = User.find_by_username(username_from_form)
    if (@user.nil?)
      flash[:notice] = "Unknown User"
      redirect_to login_url
    else
      if (@user.password == password_from_form)
        # User successfully logged in
        session[:user_id] = @user.id
        redirect_to tweets_url
      else
        flash[:notice] = "Incorrect Password"
        redirect_to login_url
      end
    end
  end

The basic process of this action is straightforward: get the information from the form and compare it to the information in the database. If they match, then the user has successfully logged in. If not, then return an error. Notice that we are using the flash variable to send error messages to our view. Anything stored in the flash will stay there for the next action (after a single redirect) but then be deleted from the flash. This is ideal for sending error messages to a view; the error message automatically goes away if the page is reloaded or returned to later.

One thing to notice is that once the user is logged in, we store his or her ID number in the session hash under :user_id. The session has is a small place where we can store information that sticks around. We can’t store the actual user variable in the session, but we can store an ID number. We will use this ID number in a little bit below:

Submitting Tweets

Next, we want logged-in users (and only logged-in users) to be able to submit new tweets to our system. We want to get rid of this publicly-available tweet interface we currently have. The first step in doing that is to create a many-to-one relationship between tweets and users. Each user can have multiple tweets. This means we need to add a user_id column to the tweets table. To do that, we can use a migration:

$ rails generate migration AddUserIDToTweets

And that migration can add the required column:

add_column :tweets, :user_id, :integer

Once you’ve created that migration and run rake db:migrate, then we need to edit the model files (app/model/tweet.rb and app/model/user.rb) to add the appropriate relationship:

belongs_to :user

and

has_many :tweets

Now we can associate tweets with users. The next thing we can do is modify our tweets controller to automatically associate each new tweet with the currently logged-in user. But to do that, we need to verify that the user is logged in – that they aren’t visiting the site anonymously. To do that, we are going to add a ‘filter’ function to out controller. At the bottom of the controller, we can add the following private function:

private
  def get_logged_in_user
    id = session[:user_id]
    if id.nil?
      flash[:notice] = "You must log in first"
      redirect_to login_url
    else
      @user = User.find id
    end
  end

(Note, this has to be at the bottom of the controller. Anything after the word private won’t work as an action anymore. But it has to be before the end for the class.)

This function checks for the user id in the session hash. If it isn’t there, it redirects the user to the log in page. If it does exist, then it creates an @user variable and fills it in with the information about the user who is logged in.

Then, near the top of the controller, we can add the following line. This line will cause this ‘‘filter’’ function to be executed before running the new and create actions!

before_filter :get_logged_in_user, :only => [:new, :create]

If we want to verify logged-in status before any other action, we can just add that action to the list.

Another nice thing about this is that we can use the @user variable inside any of the actions which the filter has been applied to. We can use this in the post action to associate a new tweet with the user who created it by adding one line to our post action:

 def create
    tweet_params = params.require(:tweet).permit(:content)
    @tweet = Tweet.new tweet_params
    @tweet.posted = Time.now
    @tweet.user = @user
    if @tweet.save
      redirect_to tweets_url
    else
      render :action => :new
    end
  end

Next, we need to modify the views to take this relationship into account. We want to make two changes. First, we want to add the user/show action that views all the tweets by a given user. And second, we want to add the user name to the main list of all tweets. We begin with the show action in the user controller

  def show
    @user = User.find(params[:id])
  end

And we need to create the associated show.html.erb file that displays the list of tweets for that user:

<h2> Tweets from <%= @user.username %> </h2>

<% @user.tweets.each do |tweet| %>
  <div class="tweet">
    <div class="content">
      <%= tweet.content %>
    </div>
    <span style="font-size:small;"> <%= tweet.posted %> </span>
  </div>
<hr />
<% end %>

<%= link_to "All Tweets", tweets_url %>

Finally, let’s go back to our “Public Timeline” of all tweets, and add the user who posted the tweet. We can make the username a link to that person’s tweets. We will also use a condition to only display that information if the tweet was posted by a user because we should have 10 or more tweets that we created before there were users.

<h4> Public Timeline </h4>
<% @tweet_list.each do |tweet| %>
  <div class="tweet">
    <div class="content">
     <%= tweet.content %>
    </div>
    <span style="font-size:small;">
     <%= tweet.posted %>
     <% unless tweet.user.nil? %>
       by <%= link_to(tweet.user.username, user_url(tweet.user)) %>
     <% end %>
    </span>
  </div>
<hr />
<% end %>

Logging Out

Finally, we want to write the code to allow a person to log out. This is really important for us to be able to test our code. We will put this in the login controller, which is where all of our user management code lives:

  def logout
    session[:user_id] = nil
  end

And we can display a message when a user logs out indicating that they were successfully logged out in logout.html.erb:

You were successfully logged out. <br />

<%= link_to "Return to the list of tweets", tweets_url %>

Finally, we should add a link to the index.html.erb file to allow users to log out:

<hr />
<%= link_to "Log In", login_url %> |
<%= link_to "Log Out", logout_url %>

Exercise: Testing our New Application

OK, now that we are finished, we to test it to make sure everything works correctly. With your pair, spend some time thinking about everything the application is supposed to do, and how to test it to see if it all works. Test it by trying out lots of things: create multiple users, log in and log out, submit tweets from different users, try too long or blank tweets, etc.

Optional Exercise: Add Pages to a User’s Tweets

Right now, we display all of a user’s tweets on the same page. This works fine for people like me who have about 10-20 tweets total. But some people on Twitter have hundreds or thousands of tweets. As an optional exercise, add pages to a user’s tweets, much like we did on the main list of tweets. Don’t forget to test it!