Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

January 14, 2019

Last updated November 5, 2023

Let’s Build: With Ruby On Rails - Project Management App

Welcome to my next installment to the ongoing series called "Let's Build: With Ruby on Rails". This 10-part series will cover building a project management type of application from the ground up.

Similar to the previous installments I'll dive a bit further into more technical aspects of Ruby on Rails. We'll discuss things such as nested attributes, model relations, and getting a little fancy with forms using Vue.js.

As a prerequisite, I recommend following at least one of the previous parts of the "Let's Build: With Ruby on Rails" series. These will help explain some foundational concepts of which I may skim over in this series. You can find those below:

What are we building? Projekt

Download the source code

For lack of a better name, I titled the app "Projekt". The goal of the app is to be a home for any amount of projects(think Basecamp but much more stripped down). A project lives within a team and can have as many users as necessary. A user can only belong to one team at a time (this is a small side-effect of the Devise gem. Ultimately, we'd want to extend this to allow a single user to belong to multiple teams.)

The app will have 3 overlying models/relationships to tie together each other as we press forward but I'll outline the "wants" below:

  1. A User can create a project if they belong to a team.
  2. Creating a team assigns both your own account plus those you invite to a team.
  3. Projects require a team in order to be created.

Part 2

Part 3

Part 4

Part 5

Part 6

Part 7

Part 8

Part 9

Part 10

The tech stack

I think it's pretty obvious we'll be using Ruby on Rails in this series. On top of Rails, I wanted to approach our app with a little more modern of a mindset set. Much of the interactivity I introduce has Vue.js to think. In our Teams model, for example, I implement a form with nested user attributes. The end goal is to create a team and invite new users to the team all at once. Nested attributes and Vue.js make this a very fluid process.

We also will be using some new gems in this series that I hadn't before. Find the full list and general description of each below.

The Gem List:

  • Bulma Rails - For general styles and layout. I introduce more custom CSS for small tweaks to UI as well.
  • SimpleForm - For simpler forms!
  • Devise - Authentication and allowing us to have a complete User system out of the gate. We'll extend this gem quite a bit in this series so stay tuned.
  • Gravatar Image Tag - a handy gem for displaying Gravatars based on a user's email address.
  • jQuery Rails - We need jQuery for the gem just below.
  • WYSIWYG-Rails - A handy and highly customizable WYSIWYG editor for Rails.
  • Public Activity - A great way to track the history of a model in Ruby on Rails. In our case, we'll track a given user's activity to display a history of CRUD actions they've performed throughout the app.
  • Better Errors - enough said
  • Guard - think Grunt or Gulp for Ruby on Rails.
  • Guard Livereload - Handy for updating changes when hitting save. The browser automatically refreshes.
  • Webpacker - Webpacker comes bundled with Rails 5.1+. We'll create our app with a flag of --webpacker=vue from the get-go to install Vue.js and all of its dependencies(you can always add it later if need be). In a new tab on our terminal, we can then run webpack-dev-server to run webpack on our project. This compiles and minifies our JavaScript files(more to come about this) and we are left with more optimized code.

  • MailCatcher - http://127.0.0.1:1080/ - Rails is a complete web app framework so much like Models, Views, and Controller, Emails can be sent programmatically using what are known as Mailers. I like using MailCatcher to preview such emails locally rather than trying to hook into an online service such as Gmail. MailCatcher is quick to install and highly dependable. You may also consider MailHog if MailCatcher isn't your thing.

  1. gem install mailcatcher
  2. mailcatcher
  3. Go to http://localhost:1080/
  # projekt/config/environments/development.rb

  config.action_mailer.default_url_options =  { :host  => 'localhost: 3000' }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }

Kicking things off

While I recommend following along in the videos, below is a summarized approach to the main functionality of our application. I'll cover primarily the models here and within the videos, you can gather the rest. As always, check the github repo for the entire project to reference as you go.

Creating a New Project

rails new projekt --webpack=vue - Requires Rails 5.1+ to run initially. If you're using an older version of rails head to the installation section of the repo for the necessary gem info to add to your Gemfile.

Initial setup

Refer to my Gemfile for all the gems associated with this project. If you're using the exact version of Rails as me you should be fine to just copy and paste the contents of my Gemfile into your own.

Run bundle from within your project folder and you should install the same gems needed for this series.

Home Controller

To set a root path both for our app and for the sake of the Devise gem we need a controller and action set. Run the following migration to create a new home controller.

$ rails g controller home index

This creates a file called home_controller.rb within app/controllers/. From there we need to set a root path. Head to config/routes.rb and add the following:

Rails.application.routes.draw do
  root 'home#index'
end

This ultimately lets us visit localhost:3000 when after typing rails server in the command line to fire up the server. Our "root" path now defaults to the home controller index action.

Generating models

USER Model

$ rails generate devise:install
$ rails generate devise User

The commands above will install devise and create a migration in which will create a new table for users. You can verify the migration by heading to db/migrate/within your app folder. This folder will be the home to all of your apps migrations. It's essentially responsible for modifying tables and columns in your database through very convenient Rails commands.

By running the second line above, our User model will get generated with Devise. The Devise gem does the hard work for us and revolves around any type of model you pass. It doesn't have to be called User every time. You could for example run rails generate devise Member and achieve the same effect.

Next, we need to migrate the changes Devise made during the install. Simply run:

bash$ rails db:migrate

Now our schema.rb file within db/ should contain a new table called users with a bunch of columns associated with the Devise gem.

Associating Users to teams

I know already I need to associate a User with a Team. This requires me to add a team_id column to the users table. We can do this easily with a migration. The code below does the heavy lifting for us.

$ rails g migration addTeamIdToUsers team_id:integer

After that generates a migration you can ultimately run the migration by typing:

$ rails db:migrate

Now, our users table has a team_id column. This column will be responsible for allowing us to find a user or set of users who belong to a given team.

Devise confirmable

Devise comes loaded with a :confirmable adapter. If you head to app/models/user.rb you should see all the available adapters possible with Devise. By default, a new user needs to sign up with an email, password, and password confirmation. I wanted our app to allow users to sign up with just a name and email field. Then they would be sent a link to confirm their account via email and by clicking that link is prompted to provide a password and password confirmation at a later time.

Implementing this is possible and you'll see in the videos that I achieved the goal I was after. We need this type of functionality so that I can invite users to a team without needing to first provide a password for them. I want new invites to create their own password upon confirming their account. Still with me?

To do this requires a bit of hacking and creating two new custom controllers that extend devise. I'll create both a registrations_controller.rb file and a confirmations_controller.rb file that allow us to add a name field to a user account as well as bypass the initial requirement of a password and password confirmation that comes out of the box with Devise.

Since this part of the setup is a little tedious I invite you to watch me code it in the videos as well as check out the code on github for the end result.

For more information on adding the :confirmable feature check out this article and for adding users with only an email/name check out this article.

Projects

Our project model will be relatively simple. I simply want a name, description and team_id field on each project. The team_id will be how we associate any given project with a team.

To first create our model run the following:

$ rails g scaffold Project name:string description:text

Then following that run:

$ rails db:migrate

I already know to associate a project with both a User and a Team I need to add a column for each type.

That looks like the following:

$ rails g migration addUserIdToProjects user_id:integer
$ rails g migration addTeamIdToProjects team_id:integer
$ rails db:migrate

Since a Project will belong to both a User and Team we need an associative id column for each model.

Teams

Teams are similar to projects but we only need to add the user_id field. To properly associate a User to a Team. We already added a team_id column to the users table in a previous step.

$ rails g scaffold Team name:string
$ rails g migration addUserIdToTeams user_id:integer
$ rails db:migrate

While we are at it, our Team model is going to be what we use Vue.js for. To do so requires a few dependencies. Go ahead and add the following via yarn.

yarn add vue-turbolinks
yarn add vue-resource

Models

With everything scaffolded we now have three new models within app/models/. Each model will require a different relationship with one another. The other code within our models at this point is omitted for brevity.

#app/models/user.rb
class User < ApplicationRecord
    # users can have many projects
  has_many :projects
    # users can have many teams...sort of. Devise limits us here.      
  has_many :teams        
end

View the whole file

#app/models/team.rb
class Team < ApplicationRecord
  # we want projects within a team to be destroyed if a team is 
   has_many :projects, dependent: :destroy 

    # teams can have many users
  has_many :users

  # To create a team I need nested forms in which I can invite users. This is what allows us to do such a thing.
  accepts_nested_attributes_for :users, allow_destroy: true

end

View the whole file

# app/models/project.rb
class Project < ApplicationRecord
  # a project will always belong to a team.
  belongs_to :team
  # A project belongs to a user when created.
  belongs_to :user
    # To associated all projects to a team requires us to add a nested form for the team in mention.
    accepts_nested_attributes_for :team

end

View the whole file

Controllers

The functionality of our controllers between each model is fairly consistent. We also make use of our home_controller.rb file to be the landing area for all users who are logged in. You can view each controller below. I do a better job explaining what's happening inside each controller in the videos so be sure to watch them!

Registrations Controller - User
# app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController 

  private

  def sign_up_params 
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
  end

  def account_update_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation, :current_password)
  end
end

View it on Github

Confirmations Controller - User
# app/controllers/confirmations_controller.rb
# app/controllers/confirmations_controller.rb
class ConfirmationsController < Devise::ConfirmationsController
  # Remove the first skip_before_filter (:require_no_authentication) if you
  # don't want to enable logged users to access the confirmation page.
  # If you are using rails 5.1+ use: skip_before_action
  # skip_before_filter :require_no_authentication
  # skip_before_filter :authenticate_user!

  # PUT /resource/confirmation
  def update
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        @confirmable.attempt_set_password(params[:user])
        if @confirmable.valid? and @confirmable.password_match?
          do_confirm
        else
          do_show
          @confirmable.errors.clear #so that we wont render :new
        end
      else
        @confirmable.errors.add(:email, :password_already_set)
      end
    end

    if [email protected]?
      self.resource = @confirmable
      render 'devise/confirmations/new' #Change this if you don't have the views on default path
    end
  end

  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    with_unconfirmed_confirmable do
      if @confirmable.has_no_password?
        do_show
      else
        do_confirm
      end
    end
    unless @confirmable.errors.empty?
      self.resource = @confirmable
      render 'devise/confirmations/new' #Change this if you don't have the views on default path
    end
  end

  protected

  def with_unconfirmed_confirmable
    @confirmable = User.find_or_initialize_with_error_by(:confirmation_token, params[:confirmation_token])
    if [email protected]_record?
      @confirmable.only_if_unconfirmed {yield}
    end
  end

  def do_show
    @confirmation_token = params[:confirmation_token]
    @requires_password = true
    self.resource = @confirmable
    render 'devise/confirmations/show' #Change this if you don't have the views on default path
  end

  def do_confirm
    @confirmable.confirm
    set_flash_message :notice, :confirmed
    sign_in_and_redirect(resource_name, @confirmable)
  end
end

View it on Github

Home Controller
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    if user_signed_in?
      @teams = Team.where('id = ?', current_user.team_id)
      @projects = Project.where('team_id = ?', current_user.team_id)
    end

    @activities = PublicActivity::Activity.order("created_at DESC").where(owner_id: current_user, owner_type: "User")
  end
end

View it on Github

Projects Controller
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
  before_action :set_project, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, only: [:edit, :update, :destroy]

  # GET /projects
  # GET /projects.json
  def index
    @projects = Project.all.order("created_at DESC")
  end

  # GET /projects/1
  # GET /projects/1.json
  def show
  end

  # GET /projects/new
  def new
    @project = current_user.projects.build
    @teams = Team.where('id = ?', current_user.team_id)
  end

  # GET /projects/1/edit
  def edit
    @teams = current_user.teams
  end

  # POST /projects
  # POST /projects.json
  def create
    @project = current_user.projects.build(project_params)

    respond_to do |format|
      if @project.save
        format.html { redirect_to @project, notice: 'Project was successfully created.' }
        format.json { render :show, status: :created, location: @project }
      else
        format.html { render :new }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /projects/1
  # PATCH/PUT /projects/1.json
  def update
    respond_to do |format|
      if @project.update(project_params)
        format.html { redirect_to @project, notice: 'Project was successfully updated.' }
        format.json { render :show, status: :ok, location: @project }
      else
        format.html { render :edit }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /projects/1
  # DELETE /projects/1.json
  def destroy
    @project.destroy
    respond_to do |format|
      format.html { redirect_to root_path, notice: 'Project was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_project
      @project = Project.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the whitelist through.
    def project_params
      params.require(:project).permit(:name, :description, :team_id)
    end
end

View it on Github

Teams Controller
# app/controllers/teams_controller.rb
class TeamsController < ApplicationController
  before_action :set_team, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, only: [:edit, :update, :destroy]

  # GET /teams
  # GET /teams.json
  def index
    @teams = Team.all.order("created_at DESC")
  end

  # GET /teams/1
  # GET /teams/1.json
  def show
  end

  # GET /teams/new
  def new
    @team = current_user.teams.build
    @user = current_user
  end

  # GET /teams/1/edit
  def edit
  end

  # POST /teams
  # POST /teams.json
  def create
    @team = current_user.teams.build(team_params)
    @team.users << current_user

    respond_to do |format|
      if @team.save
        format.html { redirect_to root_path, notice: 'Team was successfully created.' }
        format.json { render :show, status: :created, location: @team }
      else
        format.html { render :new }
        format.json { render json: @team.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /teams/1
  # PATCH/PUT /teams/1.json
  def update
    respond_to do |format|
      if @team.update(team_params)
        format.html { redirect_to @team, notice: 'Team was successfully updated.' }
        format.json { render :show, status: :ok, location: @team }
      else
        format.html { render :edit }
        format.json { render json: @team.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /teams/1
  # DELETE /teams/1.json
  def destroy
    @team.destroy
    respond_to do |format|
      format.html { redirect_to teams_url, notice: 'Team was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_team
      @team = Team.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the whitelist through.
    def team_params
      params.require(:team).permit(:name, users_attributes: [:id, :name, :email, :_destroy])
    end
end

View it on Github

Routing

Our routes file is fairly straightforward minus a put method that occurs when a user signs up and confirms their account initially. The file ends up looking like the code below.

# config/routes.rb

Rails.application.routes.draw do

  resources :projects
  resources :teams

   as :user do
    put '/user/confirmation' => 'confirmations#update', :via => :patch, :as => :update_user_confirmation
  end

   devise_for :users, controllers: {
    registrations: 'registrations',
    confirmations: 'confirmations'
  }

  root 'home#index'
end

Views

For the views, I invite you to take a peek at the GitHub view directory. You can copy and paste most of what's inside these files but I recommend typing out the main parts of the app which include the views for projects, teams and home.

What about the Vue.Js?

We use Vue.Json our nested teams form located in app/views/teams/new.html.erb. Within both this file and the edit.html.erb file in the same directory we render a partial called _form.html.erb.

Typically from this point forward, I would opt to use SimpleForm to generate and html form that does the heavy lifting. The method works really well but it would be neat to add some more interactivity into the mix.

Enter Vue.js. Within both the edit.html.erb and new.html.erb files I render a content_tag in rails that spits out some data we need to make it all work.

<!-- app/views/teams/new.html.erb -->

<section class="section">
<%= content_tag :div,
      id: "team-form",
      data: {
        id: @team.id,
        team: @team.to_json(except: [:id, :created_at, :update_at]),
        users_attributes: @team.users.to_json(except: [:created_at, :update_at])
      } do %>

  <div class="column is-5" :class="{ formstatic: scrollPosition < 100, formfixed: scrollPosition > 100 }">
    <h1 class="subtitle is-2">Create a new team</h1>
    <%= render 'form', team: @team %>
  </div>

  <div class="column is-5" :class="{ datastatic: scrollPosition < 100, datafixed: scrollPosition > 100 }">
    <h2 class="subtitle is-3">{{ team.name }}</h2>
    <table class="table is-fullwidth" v-if="team.users_attributes.length">
      <thead>
        <th>Name</th>
        <th>Email</th>
      </thead>
      <tr class="user" v-for="(user, index) in team.users_attributes">
        <td>{{ user.name}}</td><td>{{ user.email}}</td>
      </tr>
      </table>
    </div>
  </div>
<% end %>
</section>
<!--app/views/teams/edit.html.erb-->

<section class="section">
<%= content_tag :div,
      id: "team-form",
      data: {
        id: @team.id,
        team: @team.to_json(except: [:id, :created_at, :update_at]),
        users_attributes: @team.users.to_json(except: [:created_at, :update_at])
      } do %>
<div class="columns is-centered">
  <div class="column is-4">
    <h1 class="title is-2">Editing Team</h1>
    <%= render 'form', team: @team %>
  </div>
</div>  

<div class="utility-buttons">
  <%= link_to 'Cancel', @team, class:'button is-gray' %>
</div>
<% end %>
</section>

Inside both those templates is a _form.html.erb partial that gets rendered. That file contains the following:

<div class="field">
  <div class="control">
    <label class="label">Team Name</label>
    <input type="text" class="input" v-model="team.name" />
  </div>
</div>

<div class="content">
  <h2 class="title is-4">Invite users</h2>
  <p>Invite users to create an account and join this team. Each user will be sent a confirmation email in which they can supply their own unique password.</p>
</div>

<div v-for="(user, index) in team.users_attributes">
  <div v-if="user._destroy =='1'">
    {{ user.name }} will be removed.
    <button v-on:click="undoRemove(index)" class="button is-small">Undo</button>
  </div>
  <div v-else>
    <div class="field">
      <div class="control">
        <label class="label">Name</label>
        <input type="text" class="input" v-model="user.name" />
      </div>
    </div>

    <div class="field">
      <div class="control">
        <label class="label">Email</label>
        <input type="text" class="input" v-model="user.email" />
        <div id="errors" class="has-text-danger" v-if="errors.length">{{ errors }}</div>
      </div>
    </div>
    <input type="hidden" value="<%= current_user.id %>" />
    <div class="field">
      <div class="control">
        <button v-on:click="removeUser(index)" class="button is-small">Remove</button>
      </div>
    </div>
  </div>
  <hr />
</div>

<div class="field">
  <div class="control">
    <button v-on:click="addUser" class="button is-link">Add User</button>
  </div>
</div>

<hr />

<div class="field">
  <div class="control">
    <% if current_page?(new_team_path) %>
      <button v-on:click="saveTeam" class="button is-success is-large">Create Team</button>
    <% else %>
      <button v-on:click="saveTeam" class="button is-success is-large">Save Team</button>
    <% end %>
  </div>
</div>

Without our Vue app initialized this leaves little to the imagination. To go ahead and set this up I add the following code to our app/javascript directory that got generated in the very beginning when we ran rails new projekt --webpacker=vue.

/* app/javascript/packs/projekt.js */

import Vue from 'vue/dist/vue.esm'
import VueResource from 'vue-resource'


Vue.use(VueResource)

document.addEventListener('turbolinks:load', () => {
  Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

  var element = document.getElementById("team-form")
  if (element != null) {

    var id = element.dataset.id
    var team = JSON.parse(element.dataset.team)
    var users_attributes = JSON.parse(element.dataset.usersAttributes)
    users_attributes.forEach(function(user) { user._destroy = null })
    team.users_attributes = users_attributes

    var app = new Vue({
      el: element,
      data: function() {
        return {
          id: id,
          team: team,
          errors: [],
          scrollPosition: null
        }
      },
      mounted() {
        window.addEventListener('scroll', this.updateScroll);
      },
      methods: {
        updateScroll() {
          this.scrollPosition = window.scrollY
        },
        addUser: function() {
          this.team.users_attributes.push({
            id: null,
            name: "",
            email: "",
            _destroy: null
          })
        },

        removeUser: function(index) {
          var user = this.team.users_attributes[index]

          if (user.id == null) {
            this.team.users_attributes.splice(index, 1)
          } else {
            this.team.users_attributes[index]._destroy = "1"
          }
        },

        undoRemove: function(index) {
          this.team.users_attributes[index]._destroy = null
        },

        saveTeam: function() {
          // Create a new team

          if (this.id == null) {
            this.$http.post('/teams', { team: this.team }).then(response => {
              Turbolinks.visit(`/teams/${response.body.id}`)
            }, response => {
              console.log(response)

              if (response.status = 422) {
                var json = JSON.parse(response.bodyText);
                this.errors = json["users.email"][0];
              }
            })


          // Edit an existing team
          } else {
            this.$http.put(`/teams/${this.id}`, { team: this.team }).then(response => {
              Turbolinks.visit(`/teams/${response.body.id}`)
            }, response => {
              console.log(response)
            })
          }
        },

        existingTeam: function() {
          return this.team.id != null
        }
      }
    })

  }
})

This code is responsible for the same things rails can do but in a more interactive way. Adding a new user happens what appears instantly with the help of Vue.js and its interactive DOM.

To get this to render correctly remember to update the application.html.erb layout file to the following:

<!-- views/layouts/application.html.erb -->

<!DOCTYPE html>
<html>
  <head>
    <title>Projekt</title>
    <%= csrf_meta_tags %>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <%= stylesheet_link_tag "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag  'projekt' %>
  </head>

  <body>
    <% if flash[:notice] %>
      <div class="notification is-success global-notification">
        <p class="notice"><%= notice %></p>
      </div>
     <% end %>
    <% if flash[:alert] %>
    <div class="notification is-danger global-notification">
      <p class="alert"><%= alert %></p>
    </div>
    <% end %>
     <nav class="navbar is-link" role="navigation" aria-label="main navigation">
      <div class="navbar-brand">
        <%= link_to root_path, class:"navbar-item" do %>
          <h1 class="title is-5 has-text-white">Projekt</h1>
        <% end  %>
      <div class="navbar-burger burger" data-target="navbar">
        <span></span>
        <span></span>
        <span></span>
      </div>
    </div>

      <div id="navbar" class="navbar-menu">
        <div class="navbar-end">
          <% if user_signed_in? %>
          <div class="navbar-item has-dropdown is-hoverable">
            <%= link_to 'New', '#', class: "navbar-link" %>
            <div class="navbar-dropdown">
              <%= link_to 'Project', new_project_path, class:"navbar-item" %>
              <%= link_to 'Team', new_team_path, class:"navbar-item" %>
            </div>
          </div>
          <div class="navbar-item has-dropdown is-hoverable">
            <%= link_to 'Account', edit_user_registration_path, class: "navbar-link" %>
            <div class="navbar-dropdown is-right">
              <%= link_to current_user.name, edit_user_registration_path, class:"navbar-item" %>
              <%= link_to "Log Out", destroy_user_session_path, method: :delete, class:"navbar-item" %>
            </div>
          </div>
         <% else %>
          <%= link_to "Sign In", new_user_session_path, class:"navbar-item" %>
          <%= link_to "Sign up", new_user_registration_path, class:"navbar-item"%>
          <% end %>

        </div>
    </div>
  </nav>

    <%= yield %>

  </body>
</html>

Activity

To add a new activity timeline there is a series of steps in which you can follow by watching me in the videos. The goal with the activity feed is to show a timeline of events for your own account as you're logged in. Feel free to extend this to include other users, models, and more. Look to the public_activity gem for more information on how to do such a thing as well as my own code to see how I came about with my own solution.

Finishing Up

I think that about wraps it up! As I stated before, the videos are more of a step-by-step approach whereas this blog post is more of a quick reference. Use both to follow along and build something awesome. I invite you to extend this app even further!

If you have any question or are having trouble anywhere along the way feel free to reach out with a comment on my blog below or on YouTube. I'm also on twitter at @webcrunchblog or @justalever.

Link this article
Est. reading time: 24 minutes
Stats: 9,993 views

Categories

Collection

Part of the Let's Build: With Ruby on Rails collection

Products and courses