This is Part 2 of a series of posts about my experience building my first Rails application. Read Part 1 here
At this point of the project, I have successfully pulled off 3rd-party authentication using Devise and OmniAuth!
So now, along with signing up/ logging in the "old-fashioned way", a user can do the same thing using an account they already have with another provider (like Facebook or Google).
Routes and the Controllers
With my user login/signup out of the way, I can now focus on routing the controllers to my views (more on the views later).
One of my pain points in this project was routing and working with controllers. By the end of this, I understood that routes are supposed to set up the endpoints of their related controller action. Those controller actions, then, perform a bunch of tasks with whatever data is available in the strong params. While I still have much to learn about controllers and Rails' MVC architecture, I feel I am coming out of this with a bit more understanding of them.
I also wanted to use custom attribute setters for a newly-created routine's nested exercises, to guard against duplicate exercises being created. While I could have gone with something like accepts_nested_attributes_for :exercises
in my Routine
model, I wanted to ensure that both the nested attributes for exercises (1-level deep) and the related routine exercise (2-levels deep) were mass-assigned upon creation of the routine. So I went ahead and created custom attribute setters for the following:
# app/models/routine.rb
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
If a user decides they don't want to create a new exercise while creating a new routine, the outer-if statement allows for that.
# app/models/exercise.rb
def routine_exercises_attributes=(routine_exercise)
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
When editing/ updating an exercise inside a routine, the Exercise
model needs its own custom attribute setter for routine_exercises_attributes
since it is stored inside of exercises_attributes
(which is ultimately stored as strong params in the routine's routine_params
).
Inside the controllers for Routine
and Exercise
, I added some private-level set methods to DRY out some of the controller actions:
# app/controllers/routines_controller.rb
class RoutinesController < ApplicationController
before_action :authenticate_user!, :set_routine
# other code here
def set_routine
@routine = Routine.find_by(id: params[:id])
end
end
# app/controllers/exercises_controller.rb
class ExercisesController < ApplicationController
before_action :authenticate_user!, :set_exercise
# other code here
def set_exercise
@exercise = Exercise.find_by(id: params[:id])
end
end
This is going to be covered in more detail later, but it should be mentioned that, in conjunction with these class-level setter methods, there is a form with two levels of data entry:
- A nested form for if the user wants to create a new exercise while creating a new routine
- A nested form within the previous nested form that records the
sets
andreps
for the exercise's (and, by extension, also the routine's)RoutineExercise
.
Speaking of nested, I also decided to go with nested resources for when I user is dealing with their exercises in a particular routine. In other words, in config/routes.rb
, I added nested resources for :routines
and :exercises
, scoped to certain views, that allow for the
The Views
At this point in the project, it's time to start thinking about how I want the user experience to play out while they are creating, editing, updating and deleting routines and exercises.
In config/routes.rb
, I have set nested routes that describe the relationship between the Routine
and Exercise
models. This provides for some nifty paths including routine_exercises_path
.
resources :routines, only: [:show] do
resources :exercises, only: [:show, :index, :edit, :update, :destroy]
end
My application should be nimble enough to "know" which routine and which exercise we're dealing with at a given time. (e.g. routines/1/exercises/3
) As such, we should have the following views:
Routines
new
viaroutines#new
*edit
viaroutines#edit
*show
viaroutines#show
index
viaroutines#show
Exercises
edit
viaexercises#edit
*show
viaexercises#show
index
viaexercises#show
*These views will be supplied with a _form.erb
partial to DRY out the forms a bit.
For the next post, I'm going to write about the struggles that awaited me after this point in the project. This included things like keeping track of nested attributes in multiple models, preventing changed data from overwriting other data and a lot more.
Until then,
Happy Coding!
SIDENOTE: As always, if you want to check out the project for yourself, play around with it and maybe give some feedback, that'd be awesome!