The Rambling Programmer

Random musings on programming, mostly.

Rails

Flavor Flaves: A Rails Recipe Managment App

If you’d like to download the repo and play along you can do so here: Flavor-Flaves GitHub Repo

Building a rails app from scratch is hard, especially when you’ve never done it before. Enter Flavor Flaves, the illest recipe managment app on the web. Well not yet, but it’s a constant work in progress. It began simply enough. A database of users, recipes, and ingredients. Soon the relationships amongst the three got complex though.

Users

Building the users model was fairly simple, thanks mostly to Devise. It created the user database migration you see below and we were off and running.

class DeviseCreateUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
  end
end

The only other thing to do was add OAuth to the user to fulfill the requirements of the project. Again pretty straightforward thanks to the thorough documentation found online. I chose to use github as my OAuth provider and did discover an odd quirk. In your GitHub User Profile settings, you MUST ensure that you have a public email. Otherwise github will return this field as blank and authorization will fail if you are using email only to authorize. This unfortunately took way longer to figure out than it should have. However, figure it out I did, one hurdle out of the way. Hopefully that will be the last, but let’s be realistic, it never is.

Recipes and Ingredients

These models were fairly straightforward as well, as you can see below.

create_table "ingredients", force: :cascade do |t|
    t.string   "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
end

create_table "recipes", force: :cascade do |t|
    t.string   "title"
    t.string   "directions"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
end

I should note, I originally built the ingredients table with a quantity field. This turned out to be a mistake. I eventually moved the quantity field to the recipe_ingredients join table I built later. This allowed for unique ingredients while still allowing each recipe to have different quantitites of those unique ingredients.

Associations

This is where it gets fun! The plan was to allow each individual User to save Recipes to his or her favorites. Thus we have a many-to-many relationship, each User may have many saved Recipes and each Recipe may be saved by many different Users. Fairly straightforward, but I would have to build a join table to allow for this.

create_table “user_recipes”, force: :cascade do |t|
  t.integer “user_id” t.integer “recipe_id” 
end

Simple enough, below are the entries in the two models to allow this to work.

class User < ApplicationRecord
  has_many :user_recipes 
  has_many :recipes, through: :user_recipes, dependent: :destroy 
end

class Recipe < ApplicationRecord 
  has_many :user_recipes 
  has_many :users, through: :user_recipes, dependent: :destroy 
end

Again pretty simple, perhaps the only thing to point out here is the dependent: :destroy. This allows ActiveRecord to destroy any user_recipes entries when either the associated User or Recipe is destroyed since those join table entries are no longer valid.

The next association to build out was the association between Recipes and Ingredients. Again a many-to-many relationship since a Recipe obviously would have many Ingredients and any particular Ingredient could belong to many Recipes. I set that up exactly the same way as I did the previous relationship between Users and Recipes with one exception. As mentioned above I added a quantity field to the join table to allow for multiple quantities of a particular Ingredient depending on the Recipe. See below.

create_table “recipe_ingredients”, force: :cascade do |t|
  t.integer “recipe_id” 
  t.integer “ingredient_id” 
  t.string “quantity” 
end

This turned out to be both a blessing and a curse as you will see in the next section.

Hurdles

The addition of the quantity field in the Recipe-Ingredient join table made it easy to allow the user to use existing Ingredients in his or her Recipe while still giving them the freedom to change the quantity of those Ingredients based on their needs since each Recipe-Ingredient association in the join table had it's own quantity. Implementing this in practice turned out to be rather difficult.

Initially, I allowed Recipe to accept nested attributes for both Ingredients and IngredientRecipes. This resulted in some odd behavior from Rails. It seems that Rails was trying to create the join table entries prior to actually saving the new Recipe. This obviously resulted in an error since there was no recipe_id to supply to the join table. Ultimately I allowed for Ingredient to accept the nested attribute and had to create a helper method for my RecipesController to insert the appropriate quantities. Below is that helper method, which is called after the new Recipe is saved in the RecipeController

def build_recipe_ingredients(recipe_params) 
  recipe_params[:ingredients_attributes].each do |ingredient_index|
    if recipe_params[:ingredients_attributes][ingredient_index][:name] != “” 
      ingredient = Ingredient.find_by(name: recipe_params[:ingredients_attributes][ingredient_index][:name]) 
      recipe_ingredient = RecipeIngredient.find_by(recipe_id: @recipe.id, ingredient_id: ingredient.id) 
      recipe_ingredient.quantity = recipe_params[:ingredients_attributes][ingredient_index][:recipe_ingredients_attributes][“0”][:quantity] 
      recipe_ingredient.save 
    end 
  end 
end

Yeah, its’s ugly. I’m sure there is a better way to do this and I’m continually working on refactoring it, but it works. Considering this problem took several days to get to the bottom of, for now, I’ll take it.