We will start by copying our Twitter application from last week:

$ cd twitter
$ git pull
$ git tag -a lab10

Start up the server and make sure everything works. In particular, you should verify that a person can log in and submit new tweets.

AJAX Errors

If you remember the discussion during lab last week, there is bug with the tweet submission form. Really, there are two bugs. Both bugs deal with error handling. Since the form is submitted in the background via AJAX, we cannot use the same methods of handling errors that we previously used. redirect_to doesn’t work anymore for AJAX queries.

Redirecting the User

The first bug is that tweets can only be submitted if a user is logged in. There are two ways to address this problem. The first solution is to use an if statement in the view to only display the form if the user is logged in. ( unless @user.nil?; render partial: "newtweet", object: Tweet.new; end) However, we aren’t going to do this because we want to encourage users to write tweets. So we will instead do what we did before – redirect them to the log in page. When the user hits enter to submit their tweet, we run the create action in the tweets controller. Since this is an AJAX request, we can only send javascript code back. So what we can do is use the render function to generate some javascript code to redirect the user to the login page:

  render :js => "window.location.replace('#{login_url}');"

This code generates javascript code that will cause the web browser to load the auth/login page. Next, we can generate the appropriate response by checking request.xhr?. XHR stands for ‘‘XMLHTTPRequest’’ – the technical name for a background AJAX request. So we can use code like this:

  if request.xhr?
     # Code for an AJAX request 
  else
    # Code for a normal request
  end

Remember that because of our before_filter, we run the require_login function first. So, all of this code belongs in the require_login function. Putting it all together, our code should look like this:

  def require_login
    get_logged_in_user
    if @current_user.nil?
      flash[:notice] = "You must log in first"
      if request.xhr?
        render js: "window.location.replace('#{login_url}');"
      else
        redirect_to login_url
      end
    end
  end

Read through this code and make sure you understand what its doing. Also, test it! Does it work?

As you are testing it, you may notice that when you create a new user, you still have to log in. That seems redundant, right? So, remember, technically, what does it mean for a user to be logged in? It means that we have put their user id into the session. SO, let’s add that to our “create user” code. Look up the create action in the users controller. Let’s add the user id to the session (which effectively logs them in) after they are successfully saved:

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

Don’t forget to test this out. Try creating a new user, and then create a tweet and see if it works.

Adding Error Messages to the Existing Webpage

Using javascript to redirect the user is only one way of handling errors. Another strategy is to modify the existing webpage to add error messages. The second bug with the tweet submission form is that tweets are currently limited to 160 characters. If you submit a tweet that is too long, it just won’t work, but it also won’t display any error messages. Test this error and see what happens right now; I bet it just won’t work but won’t display an error message either. So let’s fix that.

The first thing to do is to make sure our newtweet partial contains code to display error messages. (The code from last week included the error display code):

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

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

Next, we can detect errors in our controller and save the results in a variable:

  def create
    tweet_params = params.require(:tweet).permit(:content)
    @tweet = Tweet.new tweet_params
    @tweet.posted = Time.now
    @tweet.user = @current_user
    @it_worked = @tweet.save
  end

Finally, we modify our create.js.erb file to redraw the newtweet partial with the (error filled) @tweet object when there is an error:

<% if @it_worked %>
  $('#tweet_list').prepend("<%=j render @tweet %>");
  $('#newtweet').replaceWith("<%=j render partial: "newtweet", object: Tweet.new %>");
  $('#tweet_<%= @tweet.id %>').effect("highlight");
<% else %>
  $('#newtweet').replaceWith("<%=j render partial: "newtweet", object: @tweet%>");
<% end %> 

This code stores the results of @tweet.save in a variable called it_worked. Then, in the javascript view, it checks that variable to see if the save worked; if it didn’t then it generates some javascript code that replaces the entire form with a new copy; but this time we pass the @tweet variable into the form. This variable contains the error messages that will then be rendered by the form. Test this and see if it works now.

Having this error message displayed is really annoying; the error is very long. So we can use another trick called .delay(n). This function generates javascript code that waits n micro-seconds (hundreths of a second) and then executes whatever follows it. So, let’s try adding this line after the code that draws the newtweet:

  $('#errorExplanation').effect('highlight').delay(5000).fadeOut(500);

This code displays a new version of the form with error messages. It then highlights the message briefly. And, after 5 seconds, it fades the error message away. Try it out and see how it works!

Once you’ve finished developing your application, you will want to deploy a ‘‘production’’ version of it. The production version will

For today, we are going to take our lab 11 twitter application and put it in production on our class webserver. To do this, we are going to use a framework called capistrano to create our production version.

Once you’ve done all this, go to http://webdev.cas.msu.edu/s14/labs, click on your application, and see if it works!

If you discover a bug in your application, go back to your development version and fix it. And TEST it so there aren’t any more bugs. Then, when you are ready, you can set up a new version with a single command:

$ bundle exec cap production deploy

For the group projects, you will have to create production versions of your final project once it is done and put them in the /tc359/s14/gallery directory (replace labs with gallery in the above instructions).

Optional Additions using AJAX

Here are another feature that we can add to our twitter application that is optional. You can implement it if you like.

Show More rather than Next Page

Rather than using pages, Twitter uses AJAX to dynamically add more tweets to the bottom of the list when the user clicks a “More” button. We can do that too!

We begin by creating a new partial – _more_button.html.erb – that creates this button. We use the function called button_to_remote to create a button that makes a background AJAX call. It takes the same parameters as link_to_remote. We will add one piece of information to our url: the date/time of the oldest tweet (which happens to be the last tweet in our list, since we know our list is sorted chronologically).

<% unless @tweet_list.empty? %>
<%= button_to "More Tweets", more_url(:oldest => @tweet_list.last.posted), :remote => true %>
<% end %>

Next, we can modify index.html.erb to display this partial. This code can replace the Next Page / Previous Page code we had their previously, since we aren’t using pages anymore.

<div id="more_button">
<%= render "more_button" %>
</div>

In our controller, we need to create a new “more” action. This action first grabs the date/time of the oldest tweet that is displayed on the screen. It then finds up to the 10 newest tweets that are older than this date. We use the :conditions parameter to specify this; we want a condition that the date the tweet was posted to be less than earliest_seen. controller

  def more
    earliest = params[:oldest]
    @tweet_list = Tweet.order('posted DESC').limit(10).where('posted < ?', earliest).all
  end

Whenever we create a new action (that isn’t one of the standard 7 for resources), we need to add a new route for it:

  match "more" => "tweets#more", :as => "more"

Finally, we need to create our ruby-javascript view – more.rjs – that adds these tweets to the web page using page.insert_html. We also will replace the more_button with a new more button that has a correct oldest_posted date/time.

<% unless @tweet_list.empty? %>
  $('#tweet_list').append('<%=j render @tweet_list %>');
  $('#more_button').replaceWith('<%=j render 'more_button' %>');
<% end %>