Jan 10, 2019

Adding a budget feature to our expense tracker - Part 2

In part 1 we added a placeholder page for our new budget feature and updated our navigation. This time out we are going to look at adding some actual content to the budget page.

Today’s objective

Today we’ll be looking to replace our placeholder budget page with some actual content. We’ll create the basic content we want to display and update our code so the user can set their budget.

Getting started

As previously mentionned I’m not sure this is really a “follow along” type of series, but it is possible to follow along if you wish.

If you’ve been following along you can continue with the code from part 1, or you can grab the code from GitHub.

Clone the Repo:

Terminal
git clone -b bfb-initial-UI-updates 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-adding-content-to-the-budget-page

With that out of the way, let’s get to it!

Where we’re starting from

Let’s fire up the server and have a quick look at our current budget page.

Terminal
rails s

Not too impressive! The first thing I notice is we are missing the current income versus expenses widget that shows up on the other pages, i.e.

So let’s work on that first.

Adding content to the budget page

First off, let’s get that widget displayed, this is an easy change.

Adding the widget

/app/views/budget/index.html.erb
<% provide(:title, 'Budget') %>
<div class="budget">
  <%= render 'shared/mtd_ytd' %>
  <div class="page-header">
    <h1>Budget</h1>
  </div>
</div>

A pretty simple change, we’ve added a few divs and included the widget via <%= render 'shared/mtd_ytd' %>.

We now see the widget showing up.

Next let’s figure out how to display each of our categories along with the current amount spent on that category for the current month. For now we won’t worry about the budgeted field or any functionality related to the budgeted field.

Displaying our categories on the Budget page

I’ve manually added in some test data in order to have a few categories and transactions in the database. One thing to note is we should already have a backend query that we can adapt for use on our Budget page. If we view the reports page, we’ll see our expense report is displaying the exact data we are after.

Looking at how the data is being loaded on the reports page, we’re using a method called expenses_by_category_and_date_range. So we just need to call into that method from the BudgetController. The method takes in a date range of the format dd mmm YYYY:TO:dd mmm YYYY.

Loading the data

So let’s update the controller code.

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

  def index
    now = DateTime.current
    eom = now.end_of_month
    year = now.strftime('%Y')
    month = now.strftime('%b')
    endDay = eom.strftime('%d')
    range = "01 #{month} #{year}:TO:#{endDay} #{month} #{year}"
    expenses = current_user.expenses_by_category_and_date_range(range)

    @categories = current_user.categories
    @categories.entries.each do |category|
      expenses.each do |expense|
        if expense.id == category.id
          category.spent = expense.sum
        end
      end
    end
  end
end

Pretty simple, we build up our range parameter to pass into expenses_by_category_and_date_range. Then we use the sum value returned by the method to populate a new spent attribute on the category model. In this way we end up with a list of categories along with how much has been spent on those categories for the current month.

The new spent attribute isn’t something we’ll be adding to the database so we can just use an attr_accessor. Let’s update the model:

/app/models/category.rb
class Category < ActiveRecord::Base
  attr_accessible :name
  attr_accessor :spent
  belongs_to :user

  validates :name, presence: true, length: { maximum: 255 }
  validates :user_id, presence: true
  validates :name, :uniqueness => { scope: :user_id, case_sensitive: false }

  default_scope order: 'LOWER(categories.name)'
end

One other thing to make note of is we’re populating the spent attribute by comparing the values of the category.id field. The expenses_by_category_and_date_range method does not currently return the category id, so we need to update the method to do so.

/app/models/user.rb …line 87
def expenses_by_category_and_date_range(range)
  transactions.
    select("name, SUM(amount), categories.id").
    joins("LEFT JOIN categories on categories.id = transactions.category_id").
    where(where_clause_for_transactions_by_date_and_category(true, range)).
    group("name, categories.id").
    order("name")
end

Simple, we just added the id in the select and group statements.

Now it’s time to update the UI!

Displaying the data

Let’s update the view.

/app/views/budget/index.html.erb
<% provide(:title, 'Budget') %>
<div class="budget">
  <%= render 'shared/mtd_ytd' %>
  <div class="page-header">
    <h1>Budget</h1>
  </div>
  <% if @categories.any? %>
    <table class="table">
      <thead>
        <th>Category</th>
        <th>Budgeted</th>
        <th>Spent</th>
        <th>+ / -</th>
      </thead>
      <tbody>
        <% @categories.each do |category| %>
          <%= render 'budget_item', category: category %>
        <% end %>
      </tbody>
      <tfoot>
        <tr>
          <th>Total</th>
          <th id="budget-total"></th>
          <th id="spent-total"></th>
          <th id="plus-minus-total"></th>
        </tr>
      </tfoot>
    </table>
  <% else %>
    <div class="well">
      You need to create some categories on the Category page prior to setting up your budget.
    </div>
  <% end %>
</div>

So a bit of a code dump, but all we’ve really got here is some HTML with a bit of logic mixed in. We’ve got an if statement to check if we have any categories to display. If not we output a message indicating the user needs to create some categories. If we do have category records to display we display them in a table. We’re rendering the individual category items in a seperate file via <%= render 'budget_item', item: item %> so we’ll need to create that.

Terminal
touch app/views/budget/_budget_item.html.erb
/app/views/budget/_budget_item.html.erb
<tr>
  <td><%= category.name %></td>
  <td></td>
  <td><%= category.spent %></td>
  <td></td>
</tr>

And with that we now have a pretty decent initial outline of what our page will display.

Adding the budget field

Before stopping for today, let’s add the new budgeted field. We have a few options for this. We could create a new database table containing a category_id, user_id and a budgeted value. However I think it is reasonable to simply add a new field to the existing categories table. This avoids the complexity (albeit minor) of adding a new database table / model and logically it is reasonable to think that for each category record a field exists that indicates the amount a user has budgeted for that category.

So let’s create a migration for the new field.

Terminal
rails g migration add_budgeted_to_categories budgeted

We’ll update the migration as follows:

/db/migrate/datestamp_add_budgeted_to_categories.rb
class AddBudgetedToCategories < ActiveRecord::Migration
  def change
    add_column :categories, :budgeted, :decimal, :precision => 10, :scale => 2, :default => 0.0
  end
end

And now we can run the migration.

Terminal
bundle exec rake db:migrate

We need to make a small change to the Category model so that we can access the budgeted field.

/app/models/category.rb
class Category < ActiveRecord::Base
  attr_accessible :name, :budgeted
  ...
  ...

Now we just need to update _budget_item.html.erb… since we want the user to be able to set the value of the budgeted field we’ll use the functionality of the best_in_place gem to allow for this.

/app/views/budget/_budget_item.html.erb
<tr>
  <td><%= category.name %></td>
  <td>
    <%= best_in_place category, :budgeted,
      activator: "##{category.id}",
      :display_with => :number_to_currency %>
    <a id="<%= "#{category.id}" %>" href="#">Edit</a>
  </td>
  <td><%= category.spent %></td>
  <td></td>
</tr>

So we’ve added the field and made it editable via the best_in_place gem. Since we already have an update method on the Category controller we don’t need to make any backend changes. When an edit occurs it will be sent to the update method of categories_controller.rb, i.e.

def update
  @category = current_user.categories.find(params[:id])
  @category.update_attributes(params[:category])
  respond_with @category
end

With the _budget_item.html.erb changes in place we now have an editable budgeted field.

Sweet!

What about tests?

Actually before wrapping up for the day, let’s take care of one more thing. As we’ve been building out this new functionality we’ve been neglecting our tests… let’s remedy that!

Since we’ve updated our database, the first step is to update our test database.

Terminal
bundle exec rake db:test:prepare

Model tests

Now let’s update the tests for our category model. We’ll add respond_to tests for the two new fields we added.

/spec/models/category_spec.rb
require 'spec_helper'

describe Category do
  let(:user) { FactoryGirl.create(:user) }
  before { @category = user.categories.build(name: "some category") }

  subject { @category }

  it { should respond_to :name }
  it { should respond_to :user_id }
  it { should respond_to :user }
  it { should respond_to :spent }
  it { should respond_to :budgeted }
  it { should be_valid}
  ...
  ...

Straightforward, looking at the rest of the category_spec, I think it would also make sense to add a validation for the new budget field. We’ll add this at the end of the spec.

/spec/models/category_spec.rb
    ...
    ...
    describe "budgeted should be numeric" do
      before { @category.budgeted = "abc" }
      it { should_not be_valid }
    end
  end
end

We also need to add the actual validation.

/app/models/category.rb
...
validates :name, :uniqueness => { scope: :user_id, case_sensitive: false }
validates_numericality_of :budgeted
...

And with that, let’s ensure the category model tests pass:

Terminal
bundle exec rspec spec/models/category_spec.rb

Great!

Budget page tests

Now we should create some tests for the actual budget page. We’ll need a new file for this.

Terminal
touch spec/requests/budget_spec.rb

As far as the content of the test goes… I am going to vomit out some code and won’t provide much of an explanation… I feel the code is pretty self explanatory however. So our test looks like:

/spec/requests/budget_spec.rb
require 'spec_helper'

describe "Budget" do
  let(:user) { FactoryGirl.create(:user, active: true) }
  before { sign_in user }

  subject { page }

  describe "index" do

    describe "items that should be present on the page" do
      before { visit budget_path }
      it { should have_selector("title", text: full_title("Budget")) }
      it { should have_selector("h1", text: "Budget") }
    end

    context "mtd / ytd widget" do
      before { visit budget_path }
      it_behaves_like 'mtd / ytd widget'
    end

    describe "when no categories have been created" do
      before { visit budget_path }
      it { should have_content("You need to create some categories") }
    end

    describe "when categories have been created" do
      before do
        user.categories.build(name: "Rent", budgeted: 800).save!
        user.categories.build(name: "Groceries", budgeted: 500).save!
        user.categories.build(name: "Entertainment", budgeted: 200).save!
        visit budget_path
      end

      it { should_not have_content("You need to create some categories") }
      it "should order the categories in alphabetical order ignoring case" do
        document = Nokogiri::HTML(page.body)
        rows = document.xpath('//table//tr').collect { |row| row.xpath('.//th|td') }

        rows[0][0].should have_content("Entertainment")
        rows[0][1].should have_content("$200.00")

        rows[1][0].should have_content("Groceries")
        rows[1][1].should have_content("$500.00")

        rows[2][0].should have_content("Rent")
        rows[2][1].should have_content("$800.00")
      end
    end
  end
end

Let’s ensure the budget tests all pass.

Terminal
bundle exec rspec spec/requests/budget_spec.rb

And then just for good measure, let’s run the entire suite.

Terminal
bundle exec rspec spec/

Sweet all looks good!

That’s it for today, we can merge our changes and we can remove our current working branch.

Terminal
git add .
git commit -am "added content for the budget page"
git checkout budget-feature-branch
git merge bfb-adding-content-to-the-budget-page
git branch -d bfb-adding-content-to-the-budget-page

Summary

We are pretty close to finishing up the implentation of our new feature. Next time out we just need to add in a bit of javascript to calculate the sums and recalculate in the case of an edit. 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!