Rails Fitness Tracker App Part 3: Final Touches & Lessons Learned

Rails Fitness Tracker App Part 3: Final Touches & Lessons Learned

This is Part 3 of a series of posts about my experience building my first Rails application. Read Part 1 or Part 2 if you haven't, yet.

blur-1972569_640.jpg

The Story, Thus Far (TL;DR-style)

Up until this point, in the pursuit of building a Rails fitness tracker app, I have managed to accomplish the following:

I Incorporated 3rd-Party Auth via Devise/ OmniAuth

While going with Native user authentication would have been totally valid, I wanted to go with Devise for the following reasons:

  1. It's a litany of tools (such as Registerable, Recoverable and OmniAuthable --> more on that next)
  2. It comes with the OmniAuth gem out of the box, making 3rd-party auth setup smooth and easy.

I also had to configure my app in the developer pages of whichever providers I went with (like Facebook for Developers).

I Routed the Appropriate Controller Actions to the Appropriate Views

Understanding how I wanted a user's typical experience to go was tantamount to routing the right controller actions to the right views.

I don't need to worry about controllers for User or Session because that's taken care of by Devise. Awesome! However, I did need to include an omniauth_controller.rb file that contain callback actions for the various providers.

For Routine and Exercise, I chose to go with nested routing because I want my paths to have some sort of logical naming that implies "This is the exercise for this routine" --> /routines/1/exercises/4.

Since a user can create a routine, and then create/ add exercises within, and they should be able to edit/ delete that routine's exercise:

  resources :routines, only: [:show] do
    resources :exercises, only: [:show, :index, :edit, :update, :destroy]
  end

I Worked to DRY Out Code and Use Partials

By this point, I thought it was time to take a look over what was done so far and see if any concerns could be separated into their own methods or forms. This included providing a views/routines/_form.erb partial for routines#new and routines#edit along with a views/exercises/_form.erb partial for exercises#edit.

I also DRY-ed out the controllers by creating private methods that would set variables for a given routine, exercise and routine_exercise instance, set before any action is taken:

 # app/controllers/routines_controller.rb
  before_action :authenticate_user!, :set_routine, only: [:show, :edit, :update, :destroy]

  ...
   private
   def set_routine
    @routine = Routine.find_by(id: params[:id])
  end



# app/controllers/exercises_controller.rb
   before_action :set_exercise, only: [:show, :edit, :update, :destroy]
   before_action :set_routine, only: [:show, :edit]
   before_action :set_routine_exercise, only: [:show, :edit, :update, :destroy]
   ...

  private
  def exercise_params
    params.require(:exercise).permit(:id, :name, :exercise_type, :description, :routine_exercises_attributes => [:id, :routine_id, :sets, :reps])
  end

  def set_exercise
    @exercise = Exercise.find_by(id: params[:id])
  end

  def set_routine
    @routine = current_user.routines.find_by(id: params[:routine_id])
  end

  def set_routine_exercise
    @routine_exercise = RoutineExercise.find_by(routine_id: params[:routine_id], exercise_id: params[:id])
  end

Last Challenges

For this last leg of the project, there were a few more important project requirements to hash out.

Custom Attribute Setters & Persisting Data

Because I want to mass-assign nested-attributes for a routine's related exercise (that is being created anew within the form), I have some choices:

  • I could go with accepts_nested_attributes_for :exercises in Routine or...
  • Use custom attributes setters and manually populate the nested attributes for the newly created exercise (exercises_attributes=(exercise))

I don't necessarily want to risk having duplicate exercise data on the basis of its name (a validation in Exercise), so using accepts_nested_attributes_for :exercises won't work well in this case. Judging from my experience with them, custom attribute setters are harder to work with and get right. But they ended up doing the job a little better.

# app/models/routine.rb

class Routine < ApplicationRecord
    def exercises_attributes=(exercise)
      if exercise[:name] != "" && exercise[:exercise_type] != "" && exercise[:description] != ""
        new_exercise = self.exercises.build
        new_exercise.name = exercise[:name]
        new_exercise.exercise_type = exercise[:exercise_type]
        new_exercise.description = exercise[:description]
      end
    end
end
# app/models/exercise.rb

class Exercise < ApplicationRecord 
    def exercises_attributes=(exercise)
      if exercise[:name] != "" && exercise[:exercise_type] != "" && exercise[:description] != ""
        new_exercise = self.exercises.build
        new_exercise.name = exercise[:name]
        new_exercise.exercise_type = exercise[:exercise_type]
        new_exercise.description = exercise[:description]
      end
    end


    def routine_exercises_attributes=(routine_exercise)
      if !routine_exercise[:sets].blank? && !routine_exercise[:reps].blank?
        new_routine_exercise = self.routine_exercises.find_by(exercise_id: self.id)
        new_routine_exercise.update(sets: routine_exercise[:sets], reps: routine_exercise[:reps])
      end
    end
end

Validations and Error Messages

I wanted to have reasonable validations for my models to ensure that invalid data wasn't being persisted to the database. I added validations for a name for both Routine and Exercise to ensure a user can't submit an invalid form. I have added field_with_errors to those validated fields:

Screen Shot 2020-06-11 at 3.40.47 PM.png

Screen Shot 2020-06-11 at 3.41.26 PM.png

Scope Methods

In the routines#index page, I provided a filter that lets a user select from one of three options. Each of the options triggers a Routine-level scope method that filters @routines by how "intense" a routine is (using times_per_week as a metric).

# app/models/routine.rb
class Routine < ApplicationRecord
    def self.beginner_level_routines
      where('times_per_week <= 3')
    end

  def self.mid_level_routines
    where('times_per_week <= 4')
  end

  def self.intense_level_routines
    where('times_per_week > 4')
  end
end

They are then provided in a drop-down list of options on the index page for the routines:

<!-- app/views/routines/index.erb -->

<h2>List of Routines</h2>
<h4>Filter by: </h4>
<%= form_tag routines_path, method: "get" do %>
  <%= select_tag "routine", options_for_select(["Beginner-Level", "Mid-Level", "Intense-Level"]), include_blank: true %>
  <%= submit_tag "Filter" %>
<% end %>

<% if !params[:routine].blank? %>
  <% if params[:routine] == "Beginner-Level" %>
    <% @routines = Routine.beginner_level_routines %>
    <h3>Beginner-Level Routines</h3>
  <% elsif params[:routine] == "Mid-Level" %>
    <% @routines = Routine.mid_level_routines %>
    <h3>Mid-Level Routines</h3>
  <% elsif params[:routine] == "Intense-Level" %>
    <% @routines = Routine.intense_level_routines %>
    <h3>Intense-Level Routines</h3>
  <% end %>
<% end %>

Lessons Learned

Always Keep the Minimum-Viable-Product in Mind

When I'm working on any project, especially when I've just started it, it is important to always keep the minimum-viable-product in mind. It doesn't matter how humble or how modest the final product might be. What matters is that it is functionally on a basic, minimum level and ripe for adding new features in the future. This is how all the largest, most impactful websites were when they were born -- MVPs.

Don't Wait Too Long To Ask Questions

As I progress through this bootcamp, I am becoming more certain that collaboration is key in the world of tech. It makes sense, after all. Every thing we know and love about the Internet was built not be a single person, but by a team of people. And they asked questions and sought help from the peers when they knew they needed it. Waiting too long to ask a question could prove detrimental to the progress of any project. It halts everything and there's a standstill. Eventually, nothing gets done.

Always be willing to take a leap of faith and share your troubles with the tech community. They are more than willing to help and share their experiences and wisdom.

Don't Be Discouraged

There were a lot of times where I wanted to give up on this project. And by no means, this project is far from finished in my opinion. There are a lot of bells and whistles that I want to test and implement in the future. But for now, I'm proud of what I've built for my first Rails Project. And you should be, as well, if you're also new to Rails and are working on your first project.

Until then, Happy Coding!

Resources

My Rails Fitness Tracker App (GitHub repo)