recipe as ingredient work

This commit is contained in:
Dan Elbert 2018-09-11 10:38:07 -05:00
parent a6cf7c1b16
commit 56fe5aae35
33 changed files with 407 additions and 273 deletions

View File

@ -29,7 +29,7 @@ class CalculatorController < ApplicationController
end end
def ingredient_search def ingredient_search
@ingredients = Ingredient.has_density.search(params[:query]).order(:name) @foods = Food.has_density.search(params[:query]).order(:name)
end end
end end

View File

@ -0,0 +1,141 @@
class FoodsController < ApplicationController
before_action :set_food, only: [:show, :edit, :update, :destroy]
before_action :ensure_valid_user, except: [:index, :show]
# GET /foods
# GET /foods.json
def index
@foods = Food.all.order(:name).page(params[:page]).per(params[:per])
if params[:name].present?
@foods = @foods.matches_tokens(:name, params[:name].split.take(4))
end
end
def show
end
# GET /foods/new
def new
@food = Food.new
end
# GET /foods/1/edit
def edit
ensure_owner @food
end
# POST /foods
# POST /foods.json
def create
@food = Food.new(food_params)
@food.user = current_user
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
respond_to do |format|
if @food.save
format.html { redirect_to foods_path, notice: 'Ingredient was successfully created.' }
format.json { render json: { success: true }, status: :created, location: @food }
else
format.html { render :new }
format.json { render json: @food.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /foods/1
def update
ensure_owner @food do
@food.assign_attributes(food_params)
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
respond_to do |format|
if @food.save
format.html { redirect_to foods_path, notice: 'Ingredient was successfully updated.' }
format.json { render json: { success: true }, status: :ok, location: @food }
else
format.html { render :edit }
format.json { render json: @food.errors, status: :unprocessable_entity }
end
end
end
end
# DELETE /foods/1
# DELETE /foods/1.json
def destroy
ensure_owner @food do
@food.destroy
respond_to do |format|
format.html { redirect_to foods_url, notice: 'Ingredient was successfully destroyed.' }
format.json { head :no_content }
end
end
end
def select_ndbn
if params[:id].present?
@food = Food.find(params[:id])
else
@food = Food.new
end
@food.assign_attributes(food_params)
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
render :show
end
def prefetch
@foods = Food.all.order(:name)
render :search
end
def search
@foods = Food.search(params[:query]).order(:name)
end
def convert
@conversion = Conversion.new(conversion_params)
if @conversion.valid?
@output_quantity = @conversion.output_quantity
@conversion = Conversion.new
else
@output_quantity = ''
end
end
def usda_food_search
@foods = UsdaFood.search(params[:query]).limit(250).order(:long_description)
respond_to do |format|
format.html { render :layout => false }
format.json { }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_food
@food = Food.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def food_params
params.require(:food).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ash, :iron, :magnesium, :phosphorus, :potassium, :zinc, :copper, :manganese, :vit_c, :vit_b6, :vit_b12, :vit_a, :vit_e, :vit_d, :cholesterol, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy])
end
def conversion_params
params.require(:conversion).permit(:input_quantity, :input_units, :scale, :output_units, :ingredient_id)
end
end

View File

@ -1,141 +0,0 @@
class IngredientsController < ApplicationController
before_action :set_ingredient, only: [:show, :edit, :update, :destroy]
before_action :ensure_valid_user, except: [:index, :show]
# GET /ingredients
# GET /ingredients.json
def index
@ingredients = Ingredient.all.order(:name).page(params[:page]).per(params[:per])
if params[:name].present?
@ingredients = @ingredients.matches_tokens(:name, params[:name].split.take(4))
end
end
def show
end
# GET /ingredients/new
def new
@ingredient = Ingredient.new
end
# GET /ingredients/1/edit
def edit
ensure_owner @ingredient
end
# POST /ingredients
# POST /ingredients.json
def create
@ingredient = Ingredient.new(ingredient_params)
@ingredient.user = current_user
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
respond_to do |format|
if @ingredient.save
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully created.' }
format.json { render json: { success: true }, status: :created, location: @ingredient }
else
format.html { render :new }
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /ingredients/1
def update
ensure_owner @ingredient do
@ingredient.assign_attributes(ingredient_params)
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
respond_to do |format|
if @ingredient.save
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully updated.' }
format.json { render json: { success: true }, status: :ok, location: @ingredient }
else
format.html { render :edit }
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
end
end
end
end
# DELETE /ingredients/1
# DELETE /ingredients/1.json
def destroy
ensure_owner @ingredient do
@ingredient.destroy
respond_to do |format|
format.html { redirect_to ingredients_url, notice: 'Ingredient was successfully destroyed.' }
format.json { head :no_content }
end
end
end
def select_ndbn
if params[:id].present?
@ingredient = Ingredient.find(params[:id])
else
@ingredient = Ingredient.new
end
@ingredient.assign_attributes(ingredient_params)
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
render :show
end
def prefetch
@ingredients = Ingredient.all.order(:name)
render :search
end
def search
@ingredients = Ingredient.search(params[:query]).order(:name)
end
def convert
@conversion = Conversion.new(conversion_params)
if @conversion.valid?
@output_quantity = @conversion.output_quantity
@conversion = Conversion.new
else
@output_quantity = ''
end
end
def usda_food_search
@foods = UsdaFood.search(params[:query]).limit(250).order(:long_description)
respond_to do |format|
format.html { render :layout => false }
format.json { }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_ingredient
@ingredient = Ingredient.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def ingredient_params
params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ash, :iron, :magnesium, :phosphorus, :potassium, :zinc, :copper, :manganese, :vit_c, :vit_b6, :vit_b12, :vit_a, :vit_e, :vit_d, :cholesterol, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy])
end
def conversion_params
params.require(:conversion).permit(:input_quantity, :input_units, :scale, :output_units, :ingredient_id)
end
end

View File

@ -0,0 +1,4 @@
module FoodsHelper
end

View File

@ -1,4 +0,0 @@
module IngredientsHelper
end

View File

@ -0,0 +1,24 @@
# Mixin for Ingredient-like things.
# Consumer is expected to have the following methods:
# #name => name of ingredient
# #density => valid density string or nil
# #custom_units => map of unit names to a weight (eg { 'clove' => '25 g', 'bulb' => '175 g' })
# #nutrition_per_100g => Object that responds to all nutrition keys in NutritionData
# nutrition_per_100g_errors => list of errors
module Ingredient
extend ActiveSupport::Concern
included do
end
# Shared helper methods
def density?
!density.nil?
end
module ClassMethods
end
end

View File

@ -21,7 +21,7 @@ class Conversion
def check_conversion def check_conversion
begin begin
ingredient = ingredient_id.blank? ? nil : Ingredient.find(ingredient_id) ingredient = ingredient_id.blank? ? nil : Food.find(ingredient_id)
scale = self.scale.blank? ? '1' : self.scale scale = self.scale.blank? ? '1' : self.scale
density = ingredient.nil? ? nil : ingredient.density density = ingredient.nil? ? nil : ingredient.density
@output_quantity = UnitConversion.convert(input_quantity, scale, input_units, output_units, density) @output_quantity = UnitConversion.convert(input_quantity, scale, input_units, output_units, density)

View File

@ -1,10 +1,11 @@
class Ingredient < ApplicationRecord class Food < ApplicationRecord
include Ingredient
include TokenizedLike include TokenizedLike
belongs_to :user belongs_to :user
has_many :ingredient_units, inverse_of: :ingredient, dependent: :destroy has_many :food_units, inverse_of: :food, dependent: :destroy
accepts_nested_attributes_for :ingredient_units, allow_destroy: true accepts_nested_attributes_for :food_units, allow_destroy: true
validates :name, presence: true validates :name, presence: true
validates :density, density: true, allow_blank: true validates :density, density: true, allow_blank: true
@ -15,20 +16,26 @@ class Ingredient < ApplicationRecord
tokens = query.to_s.split(' ') tokens = query.to_s.split(' ')
if tokens.empty? if tokens.empty?
Ingredient.none Food.none
else else
Ingredient.matches_tokens(:name, tokens) Food.matches_tokens(:name, tokens)
end end
end end
def custom_unit_weight(unit) def custom_units
ingredient_units.each do |iu| units = {}
if iu.matches?(unit) food_units.each do |fu|
return UnitConversion::parse(iu.gram_weight, 'grams') units[fu.name] = UnitConversion::parse(fu.gram_weight, "grams")
end end
units
end end
nil def nutrition_per_100g
self
end
def nutrition_per_100g_errors
[]
end end
def ndbn=(value) def ndbn=(value)

View File

@ -1,5 +1,5 @@
class IngredientUnit < ApplicationRecord class FoodUnit < ApplicationRecord
belongs_to :ingredient, inverse_of: :ingredient_units belongs_to :food, inverse_of: :food_units
validates :name, presence: true validates :name, presence: true
validates :gram_weight, presence: true validates :gram_weight, presence: true

View File

@ -2,24 +2,24 @@ class IngredientProxy
attr_reader :ingredient attr_reader :ingredient
def initialize(ingredient) def initialize(food)
@ingredient = ingredient @food = food
end end
def name def name
@ingredient.name @food.name
end end
def density def density
@ingredient.density @food.density
end end
def density? def density?
@ingredient.density.present? @food.density.present?
end end
def get_custom_unit_equivalent(custom_unit_name) def get_custom_unit_equivalent(custom_unit_name)
@ingredient.custom_unit_weight(custom_unit_name) @food.custom_unit_weight(custom_unit_name)
end end
end end

View File

@ -53,7 +53,7 @@ class NutritionData
missing = [] missing = []
NUTRIENTS.each do |k, n| NUTRIENTS.each do |k, n|
value = i.ingredient.send(k) value = i.food.send(k)
if value.present? if value.present?
value = value.to_f value = value.to_f
running_total = self.instance_variable_get("@#{k}".to_sym) running_total = self.instance_variable_get("@#{k}".to_sym)
@ -70,11 +70,6 @@ class NutritionData
end end
end end
NUTRIENTS.each do |k, n|
v = self.instance_variable_get("@#{k}".to_sym)
self.instance_variable_set("@#{k}".to_sym, v.round(2))
end
end end
end end

View File

@ -1,4 +1,5 @@
class Recipe < ApplicationRecord class Recipe < ApplicationRecord
include Ingredient
include TokenizedLike include TokenizedLike
has_many :recipe_ingredients, -> { order :sort_order }, inverse_of: :recipe, dependent: :destroy has_many :recipe_ingredients, -> { order :sort_order }, inverse_of: :recipe, dependent: :destroy
@ -71,7 +72,13 @@ class Recipe < ApplicationRecord
def yields_list def yields_list
if self.yields.present? if self.yields.present?
self.yields.split(',').map { |y| y.strip }.select { |y| y.present? } self.yields.split(',').map { |y| y.strip }.select { |y| y.present? }.map do |y|
begin
UnitConversion::parse(y)
rescue UntConversion::UnparseableUnitError
nil
end
end.compact
else else
[] []
end end
@ -123,6 +130,28 @@ class Recipe < ApplicationRecord
copy copy
end end
def density
mas = yields_list.detect { |y| y.mass? }
vol = yields_list.detect { |y| y.volume? }
if mas && vol
"#{mas.value.value / vol.value.value} #{mas.unit.original_unit}/#{vol.unit.original_unit}"
else
nil
end
end
def custom_units
Hash[yields_list.select { |y| !y.mass? && !y.volume? && y.unit }.map { [y.unit.unit, y] }]
end
def nutrition_per_100g
end
def nutrition_per_100g_errors
end
def self.for_criteria(criteria) def self.for_criteria(criteria)
query = active.order(criteria.sort_column => criteria.sort_direction) query = active.order(criteria.sort_column => criteria.sort_direction)

View File

@ -1,14 +1,14 @@
class RecipeIngredient < ApplicationRecord class RecipeIngredient < ApplicationRecord
belongs_to :ingredient, optional: true belongs_to :food, optional: true
belongs_to :recipe_as_ingredient, class_name: 'Recipe', optional: true belongs_to :recipe_as_ingredient, class_name: 'Recipe', optional: true
belongs_to :recipe, inverse_of: :recipe_ingredients, touch: true belongs_to :recipe, inverse_of: :recipe_ingredients, touch: true
validates :sort_order, presence: true validates :sort_order, presence: true
def name def name
if self.ingredient_detail.present? if self.ingredient.present?
self.ingredient_detail.name self.ingredient.name
else else
super super
end end
@ -20,40 +20,42 @@ class RecipeIngredient < ApplicationRecord
str str
end end
def ingredient_detail_id def ingredient_id
case case
when recipe_as_ingredient_id when recipe_as_ingredient_id
"R#{recipe_as_ingredient_id}" "R#{recipe_as_ingredient_id}"
when ingredient_id when food_id
"I#{ingredient_id}" "F#{food_id}"
else else
nil nil
end end
end end
def ingredient_detail_id=(val) def ingredient_id=(val)
@recipe_detail = nil return val if self.ingredient_id == val
@ingredient = nil
case val case val
when -> (v) { v.blank? } when -> (v) { v.blank? }
self.recipe_as_ingredient_id = nil self.recipe_as_ingredient_id = nil
self.ingredient_id = nil self.food_id = nil
when /^R(\d+)$/ when /^R(\d+)$/
self.ingredient_id = nil self.food_id = nil
self.recipe_as_ingredient_id = $1.to_i self.recipe_as_ingredient_id = $1.to_i
when /^I(\d+)$/ when /^F(\d+)$/
self.recipe_as_ingredient_id = nil self.recipe_as_ingredient_id = nil
self.ingredient_id = $1.to_i self.food_id = $1.to_i
else else
raise "Invalid ingredient_detail_id: #{val}" raise "Invalid ingredient_id: #{val}"
end end
end end
def ingredient_detail def ingredient
@recipe_detail ||= case @ingredient ||= case
when self.recipe_as_ingredient_id when self.recipe_as_ingredient_id
RecipeProxy.new(self.recipe_as_ingredient) self.recipe_as_ingredient
when self.ingredient_id when self.food_id
IngredientProxy.new(self.ingredient) self.food
else else
nil nil
end end
@ -94,7 +96,7 @@ class RecipeIngredient < ApplicationRecord
def to_volume def to_volume
return unless self.quantity.present? return unless self.quantity.present?
if ingredient_detail && ingredient_detail.density? if ingredient && ingredient.density?
density = UnitConversion.parse(ingredient.density) density = UnitConversion.parse(ingredient.density)
if density.density? if density.density?
value_unit = UnitConversion.parse(self.quantity, self.units) value_unit = UnitConversion.parse(self.quantity, self.units)
@ -107,44 +109,56 @@ class RecipeIngredient < ApplicationRecord
end end
def to_mass def to_mass
if ingredient_detail return unless self.quantity.present?
value_unit = as_value_unit if ingredient && ingredient.density?
density = self.ingredient_detail.density? ? UnitConversion.parse(ingredient_detail.density) : nil density = UnitConversion.parse(ingredient.density)
if density.density?
case value_unit = UnitConversion.parse(self.quantity, self.units)
when value_unit.nil?
when value_unit.mass?
self.quantity = value_unit.pretty_value
self.units = value_unit.unit.to_s
when value_unit.volume? && density && density.density?
value_unit = value_unit.to_mass(density) value_unit = value_unit.to_mass(density)
self.quantity = value_unit.pretty_value self.quantity = value_unit.pretty_value
self.units = value_unit.unit.to_s self.units = value_unit.unit.to_s
end end
end end
end end
def custom_unit? def get_custom_unit_equivalent
self.ingredient_detail && self.ingredient_detail.get_custom_unit_equivalent(self.units).present? if self.ingredient
unit = self.units.present? ? self.units.downcase : ''
pair = self.ingredient.custom_units.detect do |u, e|
if unit.empty?
['each', 'ech', 'item', 'per'].include?(u.downcase)
else
[u.downcase, u.downcase.singularize, u.downcase.pluralize].any? { |uv| [unit, unit.singularize, unit.pluralize].include?(uv) }
end
end
pair ? pair[1] : nil
else
nil
end
end end
def can_convert_to_grams? def can_convert_to_grams?
vu = as_value_unit vu = as_value_unit
vu.present? && (vu.mass? || (vu.volume? && self.ingredient_detail && self.ingredient_detail.density.present?)) vu.present? && (vu.mass? || (vu.volume? && self.ingredient && self.ingredient.density?))
end end
def to_grams def to_grams
value_unit = as_value_unit value_unit = as_value_unit
gram_unit = value_unit.convert('g', self.ingredient_detail ? self.ingredient_detail.density : nil) gram_unit = value_unit.convert('g', self.ingredient ? self.ingredient.density : nil)
gram_unit.raw_value gram_unit.raw_value
end end
def as_value_unit def as_value_unit
custom_unit = self.get_custom_unit_equivalent
case case
when self.quantity.blank? when self.quantity.blank?
nil nil
when self.custom_unit? when custom_unit.present?
self.ingredient_detail.get_custom_unit_equivalent(self.units).scale(self.quantity) vu = UnitConversion.parse(custom_unit)
vu.scale(self.quantity)
when self.units.present? when self.units.present?
UnitConversion.parse(self.quantity, self.units) UnitConversion.parse(self.quantity, self.units)
else else
@ -154,7 +168,7 @@ class RecipeIngredient < ApplicationRecord
def log_copy def log_copy
copy = RecipeIngredient.new copy = RecipeIngredient.new
copy.ingredient = self.ingredient copy.food = self.food
copy.recipe_as_ingredient = self.recipe_as_ingredient copy.recipe_as_ingredient = self.recipe_as_ingredient
copy.name = self.name copy.name = self.name
copy.sort_order = self.sort_order copy.sort_order = self.sort_order

View File

@ -1,7 +1,7 @@
class User < ApplicationRecord class User < ApplicationRecord
has_many :recipes, dependent: :nullify has_many :recipes, dependent: :nullify
has_many :ingredients, dependent: :nullify has_many :foods, dependent: :nullify
has_many :task_lists, dependent: :destroy has_many :task_lists, dependent: :destroy
has_secure_password has_secure_password

View File

@ -1,5 +1,5 @@
json.array! @ingredients do |i| json.array! @foods do |i|
json.extract! i, :id, :name, :density json.extract! i, :id, :name, :density

View File

@ -1,10 +1,10 @@
json.cache_root! [Ingredient.all, @ingredients] do json.cache_root! [Food.all, @foods] do
json.extract! @ingredients, :total_count, :total_pages, :current_page json.extract! @foods, :total_count, :total_pages, :current_page
json.page_size @ingredients.limit_value json.page_size @foods.limit_value
json.ingredients @ingredients do |i| json.foods @foods do |i|
json.extract! i, :id, :name, :ndbn, :kcal json.extract! i, :id, :name, :ndbn, :kcal
json.usda i.ndbn.present? json.usda i.ndbn.present?

View File

@ -1,5 +1,5 @@
json.array! @ingredients do |i| json.array! @foods do |i|
json.extract! i, :id, :name, :density, :notes json.extract! i, :id, :name, :density, :notes

View File

@ -1,5 +1,5 @@
json.extract! @ingredient, json.extract! @food,
:id, :id,
:name, :name,
:ndbn, :ndbn,
@ -32,15 +32,15 @@ json.extract! @ingredient,
:cholesterol, :cholesterol,
:lipids :lipids
if @ingredient.ndbn.present? if @food.ndbn.present?
json.ndbn_units @ingredient.usda_food.usda_food_weights do |fw| json.ndbn_units @food.usda_food.usda_food_weights do |fw|
json.extract! fw, :amount, :description, :gram_weight json.extract! fw, :amount, :description, :gram_weight
end end
else else
json.ndbn_units [] json.ndbn_units []
end end
json.ingredient_units @ingredient.ingredient_units do |iu| json.ingredient_units @food.ingredient_units do |iu|
json.extract! iu, :id, :name, :gram_weight json.extract! iu, :id, :name, :gram_weight
json._destroy false json._destroy false
end end

View File

@ -10,10 +10,10 @@ json.ingredients recipe.recipe_ingredients do |ri|
json.extract! ri, :id, :ingredient_detail_id, :display_name, :name, :quantity, :units, :preparation, :sort_order json.extract! ri, :id, :ingredient_detail_id, :display_name, :name, :quantity, :units, :preparation, :sort_order
json.ingredient_detail do json.ingredient_detail do
if ri.ingredient.nil? && ri.ingredient_as_recipe.nil? if ri.food.nil? && ri.ingredient_as_recipe.nil?
json.null! json.null!
elsif ri.ingredient elsif ri.food
json.extract! ri.ingredient, :name, :density, :notes json.extract! ri.food, :name, :density, :notes
else else
json.extract! ri.recipe_as_ingredient, :name json.extract! ri.recipe_as_ingredient, :name
end end

View File

@ -10,7 +10,7 @@ Rails.application.routes.draw do
resources :logs, except: [:new, :create] resources :logs, except: [:new, :create]
resources :ingredients, except: [] do resources :foods, except: [] do
collection do collection do
get :usda_food_search get :usda_food_search

View File

@ -0,0 +1,9 @@
class RenameIngredient < ActiveRecord::Migration[5.2]
def change
rename_table :ingredients, :foods
rename_table :ingredient_units, :food_units
rename_column :food_units, :ingredient_id, :food_id
rename_column :recipe_ingredients, :ingredient_id, :food_id
end
end

View File

@ -10,16 +10,16 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_09_06_191333) do ActiveRecord::Schema.define(version: 2018_09_10_235229) do
create_table "ingredient_units", force: :cascade do |t| create_table "food_units", force: :cascade do |t|
t.integer "ingredient_id", null: false t.integer "food_id", null: false
t.string "name", null: false t.string "name", null: false
t.decimal "gram_weight", precision: 10, scale: 2, null: false t.decimal "gram_weight", precision: 10, scale: 2, null: false
t.index ["ingredient_id"], name: "index_ingredient_units_on_ingredient_id" t.index ["food_id"], name: "index_food_units_on_food_id"
end end
create_table "ingredients", force: :cascade do |t| create_table "foods", force: :cascade do |t|
t.string "name" t.string "name"
t.string "density" t.string "density"
t.text "notes" t.text "notes"
@ -74,7 +74,7 @@ ActiveRecord::Schema.define(version: 2018_09_06_191333) do
end end
create_table "recipe_ingredients", force: :cascade do |t| create_table "recipe_ingredients", force: :cascade do |t|
t.integer "ingredient_id" t.integer "food_id"
t.integer "recipe_id" t.integer "recipe_id"
t.string "name" t.string "name"
t.integer "sort_order" t.integer "sort_order"

View File

@ -44,7 +44,7 @@ ingredients = {
} }
ingredients.each do |k, v| ingredients.each do |k, v|
ingredients[k] = Ingredient.create!({user_id: dan.id}.merge(v)) ingredients[k] = Food.create!({user_id: dan.id}.merge(v))
end end
g = Recipe.create!({ g = Recipe.create!({
@ -69,19 +69,19 @@ bb = Recipe.create!({
bb.tag_names = ['beef', 'dinner', 'stirfry'] bb.tag_names = ['beef', 'dinner', 'stirfry']
[ [
{quantity: '1', units: 'pound', preparation: 'flank steak, skirt steak, hanger steak, or flap meat, cut into 1/4-inch thick strips', ingredient: ingredients[:flank]}, {quantity: '1', units: 'pound', preparation: 'flank steak, skirt steak, hanger steak, or flap meat, cut into 1/4-inch thick strips', food: ingredients[:flank]},
{quantity: '1/4', units: 'cup', preparation: 'divided', ingredient: ingredients[:soy_sauce]}, {quantity: '1/4', units: 'cup', preparation: 'divided', food: ingredients[:soy_sauce]},
{quantity: '1/4', units: 'cup', preparation: 'divided', ingredient: ingredients[:shaoxing]}, {quantity: '1/4', units: 'cup', preparation: 'divided', food: ingredients[:shaoxing]},
{quantity: '2', units: 'teaspoons', preparation: '', ingredient: ingredients[:cornstarch]}, {quantity: '2', units: 'teaspoons', preparation: '', food: ingredients[:cornstarch]},
{quantity: '1/3', units: 'cup', preparation: '', ingredient: ingredients[:stock]}, {quantity: '1/3', units: 'cup', preparation: '', food: ingredients[:stock]},
{quantity: '1/4', units: 'cup', preparation: '', ingredient: ingredients[:oyster_sauce]}, {quantity: '1/4', units: 'cup', preparation: '', food: ingredients[:oyster_sauce]},
{quantity: '1', units: 'tablespoon', preparation: '', ingredient: ingredients[:sugar]}, {quantity: '1', units: 'tablespoon', preparation: '', food: ingredients[:sugar]},
{quantity: '1', units: 'teaspoon', preparation: '', ingredient: ingredients[:seasame_oil]}, {quantity: '1', units: 'teaspoon', preparation: '', food: ingredients[:seasame_oil]},
{quantity: '2', units: 'medium cloves', preparation: 'finely minced', ingredient: ingredients[:garlic]}, {quantity: '2', units: 'medium cloves', preparation: 'finely minced', food: ingredients[:garlic]},
{quantity: '2', units: 'teaspoons', preparation: 'finely minced', name: 'ginger root'}, {quantity: '2', units: 'teaspoons', preparation: 'finely minced', name: 'ginger root'},
{quantity: '3', units: '', preparation: 'whites finely sliced, greens cut into 1/2-inch segments, reserved separately', name: 'Scallions'}, {quantity: '3', units: '', preparation: 'whites finely sliced, greens cut into 1/2-inch segments, reserved separately', name: 'Scallions'},
{quantity: '4', units: 'tablespoons', preparation: '', ingredient: ingredients[:peanut_oil]}, {quantity: '4', units: 'tablespoons', preparation: '', food: ingredients[:peanut_oil]},
{quantity: '1', units: 'pound', preparation: '', ingredient: ingredients[:broccoli]}, {quantity: '1', units: 'pound', preparation: '', food: ingredients[:broccoli]},
].each_with_index do |ri, i| ].each_with_index do |ri, i|
RecipeIngredient.create!({recipe: bb, sort_order: i}.merge(ri)) RecipeIngredient.create!({recipe: bb, sort_order: i}.merge(ri))
end end

View File

@ -207,7 +207,7 @@ class UsdaImporter
sorted_files.each { |k, v| `rm #{v}` } sorted_files.each { |k, v| `rm #{v}` }
end end
Ingredient.where('ndbn != ?', '').where('ndbn IS NOT NULL').each do |i| Food.where('ndbn != ?', '').where('ndbn IS NOT NULL').each do |i|
i.set_usda_food(i.usda_food) i.set_usda_food(i.usda_food)
i.save! i.save!
end end

View File

@ -1,6 +1,6 @@
FactoryBot.define do FactoryBot.define do
factory :ingredient_unit do factory :food_unit do
ingredient food
name 'Each' name 'Each'
gram_weight 10.5 gram_weight 10.5
end end

View File

@ -1,12 +1,12 @@
FactoryBot.define do FactoryBot.define do
factory :ingredient do factory :food do
name 'Ingredient' name 'Food'
density nil density nil
notes 'note note note' notes 'note note note'
user user
end end
factory :ingredient_with_density, parent: :ingredient do factory :food_with_density, parent: :food do
name 'Butter' name 'Butter'
density '8 oz/cup' density '8 oz/cup'
end end

View File

@ -2,7 +2,7 @@ FactoryBot.define do
factory :recipe_ingredient do factory :recipe_ingredient do
sort_order 1 sort_order 1
recipe recipe
ingredient food
end end
end end

View File

@ -1,11 +1,11 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Ingredient, type: :model do RSpec.describe Food, type: :model do
describe 'validation' do describe 'validation' do
it 'validates density' do it 'validates density' do
i = build(:ingredient) i = build(:food)
expect(i).to be_valid expect(i).to be_valid
i.density = '5' i.density = '5'
@ -25,7 +25,7 @@ RSpec.describe Ingredient, type: :model do
describe 'set_usda_food' do describe 'set_usda_food' do
it 'sets the density' do it 'sets the density' do
i = build(:ingredient) i = build(:food)
f = create(:salted_butter) f = create(:salted_butter)
create(:usda_food_weight, usda_food: f) create(:usda_food_weight, usda_food: f)
@ -35,15 +35,15 @@ RSpec.describe Ingredient, type: :model do
end end
end end
describe '#custom_unit_weight' do describe '#custom_units' do
it 'returns a ValueUnit for valid custom weights' do it 'returns a hash based on food_units' do
i = build(:ingredient) i = build(:food)
i.ingredient_units << IngredientUnit.new(name: 'clove', gram_weight: 20.0) i.food_units << FoodUnit.new(name: 'clove', gram_weight: 20.0)
vu = i.custom_unit_weight('clove') units = i.custom_units
expect(vu).not_to be_nil expect(units['clove']).to be_a UnitConversion::ValueUnit
expect(vu.raw_value).to eq 20.0 expect(units['clove'].value.value).to eq 20.0
expect(vu.unit.to_s).to eq 'gram' expect(units['clove'].unit.unit).to eq 'g'
end end
end end

View File

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe IngredientUnit, type: :model do RSpec.describe FoodUnit, type: :model do
describe 'matches?' do describe 'matches?' do
it 'matches empty units to each-like terms' do it 'matches empty units to each-like terms' do
@ -15,16 +15,16 @@ RSpec.describe IngredientUnit, type: :model do
] ]
each_terms.each do |t| each_terms.each do |t|
expect(create(:ingredient_unit, name: t).matches?('')).to eq(true), "expected #{t} to match" expect(create(:food_unit, name: t).matches?('')).to eq(true), "expected #{t} to match"
end end
end end
it 'matches unit names' do it 'matches unit names' do
expect(create(:ingredient_unit, name: 'Clove').matches?('clove')).to eq true expect(create(:food_unit, name: 'Clove').matches?('clove')).to eq true
end end
it 'matches pluralized unit names' do it 'matches pluralized unit names' do
expect(create(:ingredient_unit, name: 'Clove').matches?('cloves')).to eq true expect(create(:food_unit, name: 'Clove').matches?('cloves')).to eq true
end end
end end

View File

@ -0,0 +1,56 @@
require 'rails_helper'
RSpec.describe NutritionData, type: :model do
let(:rec1_ingredients) do
[
RecipeIngredient.new({
quantity: '100',
units: 'g',
sort_order: 1,
food: create(:food, kcal: 10, protein: 2)
}),
RecipeIngredient.new({
quantity: '100',
units: 'g',
sort_order: 2,
food: create(:food, kcal: 10, lipids: 2)
})
]
end
let(:recipe1) do
create(:recipe, yields: '500 g').tap do |r|
r.recipe_ingredients = rec1_ingredients
end
end
let(:rec2_ingredients) do
[
RecipeIngredient.new({
quantity: '100',
units: 'g',
sort_order: 1,
recipe_as_ingredient: recipe1
}),
RecipeIngredient.new({
quantity: '100',
units: 'g',
sort_order: 2,
food: create(:food, kcal: 10, lipids: 2)
})
]
end
let(:recipe2) do
create(:recipe, yields: '500 g').tap do |r|
r.recipe_ingredients = rec2_ingredients
end
end
it 'runs' do
n = recipe2.nutrition_data
end
end

View File

@ -5,16 +5,16 @@ RSpec.describe RecipeIngredient, type: :model do
describe 'to_mass' do describe 'to_mass' do
it 'converts volume ingredients with density' do it 'converts volume ingredients with density' do
ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', ingredient: create(:ingredient_with_density)) ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', food: create(:food_with_density))
expect(ri.as_value_unit.mass?).to be_falsey expect(ri.as_value_unit.mass?).to be_falsey
ri.to_mass ri.to_mass
expect(ri.as_value_unit.mass?).to be_truthy expect(ri.as_value_unit.mass?).to be_truthy
end end
it 'converts ingredients with custom units' do it 'converts ingredients with custom units' do
i = create(:ingredient_with_density) i = create(:food_with_density)
i.ingredient_units << IngredientUnit.new(name: 'pat', gram_weight: 25) i.food_units << FoodUnit.new(name: 'pat', gram_weight: 25)
ri = RecipeIngredient.new(quantity: 2, units: 'pat', ingredient: i) ri = RecipeIngredient.new(quantity: 2, units: 'pat', food: i)
ri.to_mass ri.to_mass
vu = ri.as_value_unit vu = ri.as_value_unit
expect(vu.raw_value).to eq 50 expect(vu.raw_value).to eq 50
@ -58,19 +58,19 @@ RSpec.describe RecipeIngredient, type: :model do
describe 'with ingredient' do describe 'with ingredient' do
it 'returns false if unit is volume and ingredient has no density' do it 'returns false if unit is volume and ingredient has no density' do
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', ingredient: create(:ingredient)) ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', food: create(:food))
expect(ri.can_convert_to_grams?).to be_falsey expect(ri.can_convert_to_grams?).to be_falsey
end end
it 'returns true if unit is volume and ingredient has density' do it 'returns true if unit is volume and ingredient has density' do
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', ingredient: create(:ingredient_with_density)) ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', food: create(:food_with_density))
expect(ri.can_convert_to_grams?).to be_truthy expect(ri.can_convert_to_grams?).to be_truthy
end end
end end
describe 'with recipe_as_ingredient' do describe 'with recipe_as_ingredient' do
it 'return true if unit is volume and recipe has density' do it 'return true if unit is volume and recipe has density' do
ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', recipe_as_ingredient: create(:recipe)) ri = RecipeIngredient.new(quantity: '5 1/2', units: 'cups', recipe_as_ingredient: create(:recipe, yields: '4 cups, 13 oz'))
expect(ri.can_convert_to_grams?).to be_truthy expect(ri.can_convert_to_grams?).to be_truthy
end end
end end