For this project, we'll get practice creating a real-world web application — our own Twitter clone. It will showcase a variety of features like posting and deleting tweets. We will revisit and dive deeper into Rails concepts such as generating models, running migrations, associations, handling routes, creating views and more. In addition, we'll learn to use Authentication using Devise that we mentioned in earlier tutorials.
Note: If you get stuck somewhere, you can download the complete source code for this project here from Github.
Project Goals
- Creating Models and Running Migrations
- Set up associations between models
- Create and define Controllers for each model
- Create Views
- Authentication using Devise
Initializing the Rails App
The first thing we'll want to do is create a new Rails application by running the rails new command after installing Rails. As we already know, Rails provides us with a number of scripts called generators that are designed to make our development work easier by creating everything that's necessary to start working on a particular task. The rails new command sets up a blank Rails App to run our application right out of the box.
$ rails new twitter-clonecd twitter-clone
You can see that the directory has a number of auto-generated files and folders that make up the structure of a Rails application. We’ll be mostly working in the /app folder for managing our Views, Controllers and Models.
Next, start a web server on your development machine. You can do this by running the following
$ rails s
To see your application in action, open a browser window and navigate to http://localhost:3000.
You should see something like
Recap: Rails Architecture
To create a Twitter clone, we’ll have to create models and build the schema for our application. Rails follows the MVC Architecture. MVC stands for Model (M), View (V), and Controller (C). Each part has its own responsibility.
Model
Models maintain a direct relation with the Database. Each model represents a database table. This model object gains capabilities to retrieve, save, edit, and delete data from database table. We will use model objects as a layer between our application and the database. Also, we can create validations and associations between models. We will look into these later.
View
Views generate the user interface which presents data to the user. It is passive, i.e. it doesn’t do any processing. Many views can access the same model for different purposes.
Controller
The controller is the bridge between the model and the view. It takes care of the flow: controllers use models to do queries, parse data and decide how to present the data to the views.
Creating our User model with Devise
Devise is the most popular authentication gem for Rails. It comes with a handful of modules, allowing you to choose only the required ones. There are modules to support password recovery, e-mail confirmation, account lock-out, and many other account management features. We’ll use it to add user registration, log-in, log-out and other features to our Twitter Clone.
Devise generates the auth model, controller and views for us. To install Devise, include the following line in your Gemfile.
gem 'devise'
Now, install it by running:
$ bundle$ rails g devise:install
Now that we’ve installed Devise, we can create a model for authenticating users into out Twitter clone. We will call it User. Create a model named User using Devise by using the following command:
$ rails g devise User
In the terminal you’ll see a set of instructions from the devise gem. We only have to add the following line of code to the config/enviroments/development.rb path:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
This will create a User model and configure it with the default Devise modules. The generator also configures your ‘config/routes.rb’ file to point to the Devise controller. Devise also creates the default login/registration views for you.
Next, we’ll migrate our database. Migrations are a convenient way to alter our database schema. Consider each migration as being a new 'version' of the database. A schema starts off with nothing in it, and each migration modifies it to add or remove tables, columns, or entries.
Run the following command in your terminal:
$ rails db:migrate
At this point you actually have a devise user profile and log in. If you’d like to try it out, restart the rails server with :
$ rails s
Then go to localhost:3000/users/sign_in in your browser to see the sign in. It should appear something like this:
Designing Our Tweet Model
Now that we’ve created our User model, our second model will be the Tweet model. Let’s create the Tweet model by running the following command:
$ rails g model Tweet
A standard tweet has content, the user identity and timestamp. Let’s model our Tweet accordingly.
Rails has the following datatypes - primary_key, string, text, integer, bigint, float, decimal, numeric, datetime, time, date, binary and boolean. For our tweet body, we’ll use the string datatype.
Change the following migration file.
File: db/migrate/(date)_create_tweets.rb
class CreateTweets < ActiveRecord::Migrationdef changecreate_table :tweets do |t|t.belongs_to :usert.string :bodyt.timestampsendendend
Model Associations
In Rails, an association is a connection between two Active Record models. Since a tweet can be posted by only one User and one User can have many Tweets. To make sure no two Users have control over the same Tweet, we use associations.
Rails supports six types of associations:
- belongs_to
- has_one
- has_many
- has_many :through
- has_one :through
- Has_and_belongs_to_many
We’ll use the belongs_to associations for our Tweet model and has_many for our User model.
Change our User model in app/models/user.rb to:
class User < ActiveRecord::Basehas_many :tweets# Include default devise modules. Others available are:# :confirmable, :lockable, :timeoutable and :omniauthabledevise :database_authenticatable, :registerable,:recoverable, :rememberable, :trackable, :validatableend
Change our Tweet model in app/models/tweet.rb to:
class Tweet < ActiveRecord::Basebelongs_to :userend
Lets migrate :
$ rails db:migrate
Designing our Tweet Controller
The User controller is handled by Devise, but we’ll have to design the Tweet Controller ourselves.
Generate the tweet controller by the following command:
$ rails g controller tweets
Add the following methods in tweets_controller.rb:
def index@tweets = Tweet.allenddef new@tweet = Tweet.newenddef create@tweet = Tweet.new(tweet_params)@tweet.user_id = current_user.idif @tweet.saveredirect_to '/tweets#index'elserender 'new'endendprivatedef tweet_paramsparams.require(:tweet).permit(:body)end
Our controller will provide us the data for our views to display. Our landing (index) page should have all the tweets (as shown on the Twitter home page). Tweet.all is used to return all instances of the model. This is a bit simplistic, since Twitter just doesn't display all the tweets from the database on its homepage, but we'll move ahead for now.
The new method creates an object instance and the create method additionally tries to save it to the database if it's possible. We use the tweet_params method while creating instances to verify if our Tweet has any content because we wouldn’t want to store an empty tweet.
Generating Views
Next, we’ll generate a few views for our Tweet model. Scaffolding in Ruby on Rails refers to the auto-generation of a set of a model, views, and a controller. It's way easier to do this, instead of coding everything yourself. Since we already have generated the model and controller, we need to scaffold just the views.
Enter the following command in the terminal
rails g erb:scaffold Tweet
The command scaffolds views for our model. It will return
create app/views/tweetscreate app/views/tweets/index.html.erbcreate app/views/tweets/edit.html.erbcreate app/views/tweets/show.html.erbcreate app/views/tweets/new.html.erbcreate app/views/tweets/_form.html.erb
Scaffolding views gives us a standard index page and pages for creating, editing and showing instances. Since we’ll be showing tweets on our index page, we don’t need show.html.erb and since editing Tweets is not a feature, we won’t need edit.html.erb either. Feel free to delete these two.
Now, let’s design the form where we’ll post tweets. _form.html.erb is the template used for both creating and editing tweets.
Replace the contents of _form.html.erb with
<%= form_with(model: tweet, local: true) do |form| %><% if tweet.errors.any? %><div id="error_explanation"><h2><%= pluralize(tweet.errors.count, "error") %> prohibited this tweet from being saved:</h2><ul><% tweet.errors.full_messages.each do |message| %><li><%= message %></li><% end %></ul></div><% end %><p><%= form.label :body%><br><%= form.text_field :body, :id => 'body' %></p><div class="actions"><%= form.submit %></div><% end %>
Our form only has one field like it should. A text field for the tweet content. Tweet.errors.any makes sure that only valid data is being stored.
Creating an Index page
Let’s replace the default Ruby on Rails page with a view of our own. We can change the default ‘Yay, You’re on Rails!’ page by setting the root route in config/routes.rb to our Twitter landing page. Let’s create the landing page which will be our root route.
Replace the contents of index.html.erb with
<div><% @tweets.each do |tweet| %><tr><h1><td><%= tweet.user.email %></td></h1><h2><td><%= tweet.body %></td></h2><h3><td><%= tweet.created_at %></td></h3></tr><% end %></div><%= link_to 'New Tweet', new_tweet_path(@user) %>
Adding Authentication
Every tweet needs to have an author, which is the User model in this case. So, we need to make sure that the user can only view the landing page after he is logged in. Our User model is managed by Devise, which provides us with a number of helpers. One of them is authenticate_user. Adding ‘before_action :authenticate_user!’ ensures that the controller won’t be accessed until a user has logged in. Add the line in tweets_controller.rb before any of the methods.
class TweetsController < ApplicationController# before any blog action happens, it will authenticate the userbefore_action :authenticate_user!def index@tweets = Tweet.allend…end
Setting up Routes
The Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views. The config/routes.rb file defines the actions available in the applications and the type of action such as get, post, and patch.
We can check our routes by running
$ rails routes
We have to set routes in our configs folder :
Rails.application.routes.draw doroot 'tweets#index'devise_for :usersresources :users doresources :tweetsendend
The rails routes will show us the following:
The above routes are nested. The user and tweet routes are nested meaning the tweet routes cannot be managed independently. Let’s move tweets outside users in routes.rb.
Rails.application.routes.draw doroot 'tweets#index'devise_for :usersresources :tweetsresources :users doendend
This will generate the following routes:
For this project, we will not nest our routes and leave the routes.rb in this state.
Creating a navbar
Rails provides us with default application layout which is included on every page of our application. The layout defined in the file app/views/layouts/application.html.erb is used for rendering any page. It has the title of the page and the stylesheets and script tags. We will need our navbar to be present on every page of our app. So let’s add the following above <%= yield %> in views/layouts/application.html.erb:
<p class="navbar-text"><span>Rails Twitter Clone</span><% if user_signed_in? %>Logged in as <strong><%= current_user.email %></strong>.<%= link_to 'Edit profile', edit_user_registration_path, :class => 'navbar-link' %> |<%= link_to "Logout", destroy_user_session_path, method: :delete, :class => 'navbar-link' %><% else %><%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |<%= link_to "Login", new_user_session_path, :class => 'navbar-link' %><% end %></p>
We have included 4 links in our navbar. Using Devise’s user_signed_in helper, we’ll show the Edit Profile and Logout links to logged in users and Login and Register links to others.
Deleting Tweets
Twitter may not allow us to edit tweets, but we can surely delete tweets. Add the following methods in tweets_controller.rb.
def destroy@tweet = Tweet.find(params[:id])@tweet.destroyredirect_to '/', :notice => "Your tweet has been deleted"endInside tweet body<% if current_user == tweet.user %><%= link_to "Delete", tweet_path(tweet.id), :confirm => "Are you sure?", :method => :delete %><% end %>
Styling our views
Next, we can style our views as required.
Add the following in application.cs
@import url('https://fonts.googleapis.com/css?family=Montserrat');body{font-family: 'Montserrat', sans-serif;}.twitter-button{background-color: #62A9E0;border-color: transparent;border: 1px solid #1da1f2;color: #fff !important;border-radius: 100px;box-shadow: none;cursor: pointer;font-size: 14px;font-weight: bold;line-height: 20px;padding: 8px 18px;position: relative;text-align: center;white-space: nowrap;text-decoration: none !important;margin: 5px;}a {text-decoration: none !important;color: #fff !important;}input[type=submit]{background-color: #62A9E0;border-color: transparent;border: 1px solid #1da1f2;color: #fff !important;border-radius: 100px;box-shadow: none;cursor: pointer;font-size: 14px;font-weight: bold;line-height: 20px;padding: 8px 18px;position: relative;text-align: center;white-space: nowrap;text-decoration: none !important;margin: 5px;}input[type=text]{padding: 12px 20px;margin: 8px 0;/* box-sizing: border-box; */width: 50%;border-radius: 5px;border: 2px solid #d3d3d3;}.navbar-text{color: #fff;background-color: #62A9E0;padding: 15px;margin: -10px;font-size: 14px;}.tweet{background: #fff;border-radius: 8px;border: 1px solid #d3d3d3;box-shadow: 3px 3px #888888;color: #14171a;font-size: 14px;line-height: 20px;width: 80%;padding: 20px;margin-left: 10%;margin-right: 10%;/* margin: 10px; */}.right {float: right !important;}
Displaying Tweets Chronologically
Currently our tweets are displayed based on the time they’re posted. Older tweets first and the latest tweets at the end. To have tweets chronologically arranged, we’ll have to make the following change in our index.html.erb:
<% @tweets.reverse.each do |tweet| %>
This will display tweets in the reverse order.