Jan 3, 2019

Adding a budget feature to our expense tracker - Part 1

Recently I made some changes to my hobby Rails budget application. In reality, this “budget” application is really more of an expense tracker application as it doesn’t actually have any budgeting functionality. This has worked well for me in the past, but coming into the new year I want to watch my budget a little closer; so now is a great time to add a budget feature!

We see here a common trap when dealing with a legacy code base… really we should be upgrading and strengthing the existing code… but adding new features always tends to compete with the more mundane task of upgrading the existing code. In this case the new feature is winning out over upgrading… so let’s see what we’ll be doing.

What we’ll be adding

We’ll be adding a new “budget” page to our application. A rough proto-type of what we’ll be looking to add is:

So nothing too complicated!

Today’s objective

Today we’ll be taking things pretty easy, all we’re looking to do is add a new placeholder page for our budget functionality and we’ll update our navigation links to include the new page.

Getting started

Similar to the legacy code series of posts, I’m not sure this is really a “follow along” type of series, I’m more doing it for my own documentation. I will set things up so that following along is possible however.

The first step is to grab our code from GitHub. I’ve created a “budget feature branch” for the new functionality.

Clone the Repo:

Terminal
git clone -b bfb-starting-point https://github.com/riebeekn/wmcgy2 budget-app
cd budget-app

After grabbing the code we should set our local Ruby version and run bundle install.

Terminal
rbenv local 2.3.6
Terminal
bundle install

Next let’s create a new branch for today’s work.

Terminal
git checkout -b bfb-initial-UI-updates

With that out of the way, let’s make sure we can run everything locally prior to making any changes.

Getting our code running locally

Before we get going on making any changes, let’s make sure everything is currently working as expected.

Create the DB

The first step in getting our code running is to create our database user; create the database; and then run the migrations.

Terminal
createuser wmcgy2 -d
createdb -Owmcgy2 -Eutf8 wmcgy2_development
createdb -Owmcgy2 -Eutf8 wmcgy2_test

Now we’ll run our migrations:

Terminal
bundle exec rake db:drop db:create db:migrate db:test:prepare

Run the tests

And let’s make sure all our tests are passing.

Terminal
bundle exec rspec spec/

Setting up a user

The next step is to run the application locally and set-up a user.

Terminal
rails s

I won’t go thru creating a user with the UI… it is pretty straight-forward… if you run into any issues, step by step instructions are included in Part 2 of the legacy code series of posts.

With our code running, the tests passing and a user created we are now ready to move on to tackling our changes.

Update the UI

All of today’s changes are going to be dealing with the UI. We’ll be doing the following:

  • Add a placeholder Budget page.
  • Update our navigation links to include the Budget page.

Adding a blank budget page

We’ll start off by adding the placeholder page. We can use a generator to create the primary files we require.

Terminal
rails g controller Budget --no-test-framework

Next we need to add a view for the page.

Terminal
touch app/views/budget/index.html.erb

We’ll just throw a heading and a page title into the view page for now.

/app/views/budget/index.html.erb
<% provide(:title, 'Budget') %>
<h1>Budget</h1>

In the same vein we’ll add an empty index method in the controller.

/app/controllers/budget_controller.rb
class BudgetController < ApplicationController

  def index

  end
end

Next we need to update our routes file.

/config/routes.rb
...
...
resources :reports,           only: [:index]
resources :contacts,          only: [:new, :create]
resources :budget,            only: [:index]

match '/budget',  to: 'budget#index'
match '/about',   to: 'static_pages#about'

match '/contact', to: 'contacts#new', as: :new_contact
...
...

Pretty simple, we’ve just added two entries for the budget page to represent the index route.

Let’s have a quick look and validate that our new routes show up as expected.

Terminal
bundle exec rake routes

Looks good!

We should now be able to navigate to the Budget page.

Our placeholder page shows up, so we can now move onto the navigation.

Updating the navigation.

I think it makes sense to add the Budget navigation item directly after the existing Categories link.

We can do so by altering the _signed_in_header file.

/app/views/layouts/_signed_in_header.html.erb
...
<li><%= link_to "Categories", categories_path, class: "lead" %></li>
<li class="divider-vertical"></li>
<li><%= link_to "Budget", budget_path, class: "lead" %></li>
<li class="divider-vertical"></li>
<li><%= link_to "Reports", reports_path, class: "lead" %></li>
<li class="divider-vertical"></li>
...

This work alright, but I think this makes the navigation header too big, and this is going to cause some less than ideal UI experiences, for instance this does not look very good!

So I think what will work better is if we convert the right hand menu to a drop-down that contains the Account and Sign out links. We can then remove the Account link from the main navigation menu.

/app/views/layouts/_signed_in_header.html.erb
<ul class="nav">
  <li class="divider-vertical"></li>
  <li><%= link_to "Transactions", root_path, class: "lead" %></li>
  <li class="divider-vertical"></li>
  <li><%= link_to "Categories", categories_path, class: "lead" %></li>
  <li class="divider-vertical"></li>
  <li><%= link_to "Budget", budget_path, class: "lead" %></li>
  <li class="divider-vertical"></li>
  <li><%= link_to "Reports", reports_path, class: "lead" %></li>
  <li class="divider-vertical"></li>
</ul>
<ul class="nav pull-right">
  <li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
      <%= "Signed in as #{current_user.email}" %>
      <b class="caret"></b>
    </a>
    <ul class="dropdown-menu">
      <li><%= link_to "Account", account_path, :id => "accountLink" %></li>
      <li><%= link_to "Sign out", signout_path %></li>
    </ul>
  </li>
</ul>

Note we’ve added an id property to the account link… this is to make selecting the link easier when it comes to updating our tests.

Anyway, the only problem with the above is the links in the dropdown are currently white… so don’t show up on the white dropdown background!

A quick style update should solve things.

/app/assets/stylesheets/layout.css.scss
header {
  li { padding-top: 15px; }
  .divider-vertical { height: 70px !important; }
  .current-user { color: #FEFFF7; }
  .pull-right {
    a {
      color: #FEFFF7 !important;
      &:hover { color: #999 !important; }
		}
    .dropdown-menu {
      a {
        color: #000 !important;
      }
    }
  }
  ...
  ...

We’ve added a new style for the links within our dropdown menu by setting the color for the dropdown-menu class.

The navigation now looks much better.

That’s pretty much it for our changes, so now would be a good time to re-run and update our tests.

Updating the tests

Let’s see if our changes have caused any issues with our tests… I would imagine so, considering we’ve moved some things around on our navigation header.

Terminal
bundle exec rspec spec/

One failing test, let’s have a look and see what is going on.

/spec/requests/static_pages_spec.rb …line 29
describe "should have the right links on the header" do
  describe "when not signed in" do
    before { visit root_path }
    it { should_not have_link("Transactions")}
    it { should_not have_link("Categories") }
    it { should_not have_link("Sign out") }
    it { should_not have_content("Signed in as") }
  end

  describe "when signed in" do
    let(:user) { FactoryGirl.create(:user, active: true) }
    before do
      sign_in user
    end
    it { should have_link("Transactions") }
    it { should have_link("Categories") }
    it { should have_link("Reports") }
    it { should have_link("Account") }
    it { should have_link("Sign out") }
    it { should have_link("Signed in as #{user.email}") }
    it { should_not have_link("Sign in") }
    it { should_not have_link("Sign up") }

    ...
    ...  line 53
    ...

    it "should navigate to the correct page when header links are clicked" do
        visit root_path
        click_link "Transactions"
        page.should have_selector("title", text: full_title("Transactions"))
        click_link "Categories"
        page.should have_selector("title", text: full_title("Categories"))
        click_link "Reports"
        page.should have_selector("title", text: full_title("Reports"))
        click_link "Account"
        page.should have_selector("title", text: full_title("Account Settings"))
        visit root_path
        click_link "Signed in as #{user.email}"
        page.should have_selector("title", text: full_title("Account Settings"))
      end

So we need to make some changes to the tests that check which links are displayed, and also to the tests that check the functionality of the navigation links.

/spec/requests/static_pages_spec.rb …line 29
describe "should have the right links on the header" do
    describe "when not signed in" do
      before { visit root_path }
      it { should_not have_link("Transactions")}
      it { should_not have_link("Categories") }
      it { should_not have_link("Budget")}
      it { should_not have_link("Sign out") }
      it { should_not have_content("Signed in as") }
    end

    describe "when signed in" do
      let(:user) { FactoryGirl.create(:user, active: true) }
      before do
        sign_in user
      end
      it { should have_link("Transactions") }
      it { should have_link("Categories") }
      it { should have_link("Reports") }
      it { should have_link("Budget") }
      it { should have_link("Sign out") }
      it { should have_link("Signed in as #{user.email}") }
      it { should_not have_link("Sign in") }
      it { should_not have_link("Sign up") }

      it "should navigate to the correct page when header links are clicked" do
        visit root_path
        click_link "Transactions"
        page.should have_selector("title", text: full_title("Transactions"))
        click_link "Categories"
        page.should have_selector("title", text: full_title("Categories"))
        click_link "Reports"
        page.should have_selector("title", text: full_title("Reports"))
        click_link "Budget"
        page.should have_selector("title", text: full_title("Budget"))
        visit root_path
        find("#accountLink", visible: false).click
        page.should have_selector("title", text: full_title("Account Settings"))
      end
    end
  end
end

We’ve made a few changes to the test. We’ve updated the when not signed in section to check to ensure that the Budget page doesn’t show up when a user is not signed in. Conversely in the when signed in section we’re ensuring the Budget link does show up. Finally we’ve added a test in the should navigate to the correct page when header links are clicked section for the Budget page and we’ve also updated the Account link test to make use of our accountLink id mentionned earlier, as we can no longer simply click on the Account link as it is now contained within the dropdown menu.

We also need to update secure_pages_spec.rb.

/spec/requests/secure_pages_spec.rb …line 15
describe "categories" do
    it "should prevent access when not signed in" do
      visit categories_path
      check_path_and_message(current_path)
    end
  end

  describe "budget" do
    it "should prevent access when not signed in" do
      visit budget_path
      check_path_and_message(current_path)
    end
  end

We’ve added a test for our new page under the existing categories test.

Let’s re-run the tests.

Terminal
bundle exec rspec spec/

Fantastic! We can merge our changes to our feature branch and remove the current working branch.

Terminal
git add .
git commit -am "added placeholder budget page and updated navigation"
git checkout budget-feature-branch
git merge bfb-initial-UI-updates
git branch -d bfb-initial-UI-updates

Summary

So that’s it for today, a pretty gentle start to implementing our new functionality!

Next time, we’ll be looking to add some actual content to the Budget page.

Thanks for reading and I hope you enjoyed the post!



Comment on this post!