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 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
inRoutine
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:
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!