Jan 24, 2019

Adding a budget feature to our expense tracker - Part 4

In part 3 we finished off our budget functionality, but currently there is no way to exclude certain categories from the budget. Today we’ll close off the budget enhancement by adding the ability to scope which categories are included in the budget.

Today’s objective

Our budget feature is complete but as mentionned last time out, we would like to have more control over what categories appear in our budget, i.e. we’d like to be able to exclude certain categories:

The approach we’ll take with this is to add a new field to the categories table, that indicates whether or not a particular category should be included in the 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 3, or you can grab the code from GitHub.

Clone the Repo:

If cloning:

Terminal
git clone -b bfb-finishing-the-UI 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-scoping-categories-on-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 category page.

Terminal
rails s

So basically we need a new column that allows the user to include / exclude a category from displaying in the budget page.

Updating the Category page

The first step will be to create a migration for a new database column. We’ll create an include_in_budget column on categories.

Terminal
rails g migration add_include_in_budget_to_categories include_in_budget:boolean

Let’s update the migration to set the default value of the column to true. We’ll assume that the majority of categories will be included on the budget page so defaulting to true seems reasonable for existing categories and any new categories the user creates in the future.

/db/migrate/datestamp_add_include_in_budget_to_categories.rb
class AddIncludeInBudgetToCategories < ActiveRecord::Migration
  def change
    add_column :categories, :include_in_budget, :boolean, :default => true
  end
end

Now let’s run the migration.

Terminal
bundle exec rake db:migrate

We now need to make a small change to the Category model so that we can access the include_in_budget field. We do so by including it in the attr_accessible items.

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

Next we need to update the category view to include the new field:

/app/views/categories/_category.html.erb
<tr>
  <td>
    <%= best_in_place category, :name, activator: "##{category.id}" %>
  </td>
  <td>
    <a id="<%= "#{category.id}" %>" href="#">Edit</a>
  </td>
  <td>
    <%= link_to "Delete", category, method: :delete, data: { confirm: "Are you sure you want to delete this category?" } %>
  </td>
  <td>
    <%= check_box_tag 'include_in_budget', category.id, category.include_in_budget,
      data: {
        remote: true,
        url: url_for(action: :toggle_budget_inclusion_flag, id: category.id),
        method: "POST"
      } %>
  </td>
</tr>

We’ve added a checkbox column which can be used to toggle whether the category should be included on the budget page. We’ve set remote: true in order to make this an Ajax style checkbox. We need to add the new controller method and also a new route for the toggle_budget_inclusion_flag action.

Before updating the controller and routes let’s update the main category view to include a table header.

/app/views/categories/index.html.erb …line 13
<% if @categories.any? %>
  <table>
    <thead>
      <th>Name</th>
      <th colspan="2"></th>
      <th>Include category in budget?</th>
    </thead>
    <tbody>
      ...

Now for the route.

/config/routes.rb …line 15
match '/about',   to: 'static_pages#about'
match '/toggle',  to: 'categories#toggle_budget_inclusion_flag'
...

And finally the update to the controller.

/app/controllers/categories_controller.rb …line 26
def toggle_budget_inclusion_flag
  @category = current_user.categories.find(params[:id])
  @category.update_attribute(:include_in_budget, !@category.include_in_budget)
  redirect_to categories_path
end

def destroy
  ...

Nothing complicated… the method just grabs the category associated with the passed in id and toggles the include_in_budget value.

The above works but doesn’t looks so good, the table is a little squished!

Let’s update the category styling. For some reason we had the width of the table set to 10px. We can just remove that from our style.

/assets/stylesheets/categories.css.scss
.categories {
  table {
    // width: 10px; remove this!
    min-width: 25%;
    @extend .table, .table-striped;
  }
  .field_with_errors { display: inline-block; }
  #category_name {
    margin-top: 10px;
  }
}

// hide any errors from the edit in place control
.purr {
  display: none !important;
}

That looks better!

The final step is to update the budget controller to only load categories the user has specified they want in their budget.

/app/controllers/budget_controller.rb …line 10
expenses = current_user.expenses_by_category_and_date_range(range)

@categories = current_user.categories.where(include_in_budget: true)
@categories.entries.each do |category|
...
...

And with that we can now exclude purely income categories such as Pay from our budget:

Updating the budget status widget

The other issue we discussed in part 3 was that the budget status doesn’t update without a page refresh. This is pretty easy to fix, we just need make a few updates to budget.js.

/assets/javascripts/budget.js …line 9
function bindBudgetValueUpdateSuccessEvent() {
  $('.best_in_place').bind("ajax:success", function (){
    $(this).formatCurrency();
    setPlusMinus();
    setBudgetedTotal();
    setPlusMinusTotal();
    updateBudgetStatus();
  });
}

function updateBudgetStatus() {
  if ($('#plus-minus-total').asNumber() < 0) {
    $('section.budget-status header').html('OVER BUDGET')
    $('section.budget-status header').addClass('debit');
    $('section.budget-status header').removeClass('credit');
  } else {
    $('section.budget-status header').html('ON BUDGET');
    $('section.budget-status header').addClass('credit');
    $('section.budget-status header').removeClass('debit');
  }
}
...
...

All we’ve done is add a new function updateBudgetStatus that sets the budget status text and style based on whether the plus-minus-total we already calculate on the page is positive or negative. We add a call to this new function where we bind to the ajax:success event so that the status is updated anytime a change is made to a budget item.

The status now updates without requiring a page refresh!

Tests

Before finishing up for the day let’s update a few tests. We should update the category model test and the budget page test.

Let’s start with the category_spec.

/spec/models/category_spec.rb …line 24
it { should respond_to :budgeted }
it { should respond_to :include_in_budget }
it { should be_valid}
...

We’ve just added a check for the new field.

And now we’ll want to update the budget_spec to ensure it tests for categories that should not be loaded.

/spec/requests/budget_spec.rb …line 32
describe "when categories have been created" do
  before do
    user.categories.build(name: "Pay", include_in_budget: false).save!
    user.categories.build(name: "Rent", budgeted: 800).save!
    ...

All we’ve done is created a new category which we expect not to appear on the page (since include_in_budget is false). The rest of the test can stay as is.

Let’s make sure everything is still passing. Remember since we’ve updated our database we need to apply the migration to our test database before running the tests.

Terminal
bundle exec rake db:test:prepare
Terminal
bundle exec rspec spec/

Fantastic, done and dusted! We can merge our code from today into our budget feature branch.

Terminal
git add .
git commit -am "added ability to scope which categories appear in budget"
git checkout budget-feature-branch
git merge bfb-scoping-categories-on-the-budget-page
git branch -d bfb-scoping-categories-on-the-budget-page

Now we can merge the budget-feature-branch to master and deploy our enhancement! These steps are similar to what we did in the final post of the original series of posts around our budget app so I won’t go over them here.

The only deviation from the steps above is that since we’ve added new javascript and css files we need to precompile the assets prior to deploying to heroku, i.e. something along the lines of:

Terminal
RAILS_ENV=production bundle exec rake assets:precompile
git add .
git commit -am "compiled assets"

Summary

That’s it for the new budget feature and the end of this series of posts. Thanks for reading and hope you enjoyed!



Comment on this post!