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.
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:
- It's a litany of tools (such as Registerable, Recoverable and OmniAuthable --> more on that next)
- 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
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.
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" -->
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#edit along with a
views/exercises/_form.erb partial for
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
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
- Use custom attributes setters and manually populate the nested attributes for the newly created 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
Exercise to ensure a user can't submit an invalid form. I have added
field_with_errors to those validated fields:
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 %>
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!