Rails Fitness Tracker App Part 2: Routes and Views

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!

Screen Shot 2020-06-05 at 8.21.34 AM.png

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:

  1. A nested form for if the user wants to create a new exercise while creating a new routine
  2. A nested form within the previous nested form that records the sets and reps 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 via routines#new *
  • edit via routines#edit *
  • show via routines#show
  • index via routines#show

Exercises

  • edit via exercises#edit *
  • show via exercises#show
  • index via exercises#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!

Comments (1)

Francisco Quintero's photo

Awesome here. Let me suggest you that instead of

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

You do something like:

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

.find_by returns nil when it doesn't found nothing and then you could face NoMethodError for NilClass.

.find raises an exception and you can better handle that with a notification or message or something.