From 16bc8b562bc7db9614c180128312518c165554d2 Mon Sep 17 00:00:00 2001 From: Dan Elbert Date: Tue, 5 Jul 2016 16:31:36 -0500 Subject: [PATCH] Added ingredient functionality --- app/assets/stylesheets/application.scss | 22 ++++++++++ .../stylesheets/journal_custom_colors.scss | 1 + app/controllers/ingredients_controller.rb | 2 +- app/models/ingredient.rb | 13 ++++++ app/models/ingredient_unit.rb | 17 ++++++++ app/models/recipe_ingredient.rb | 43 +++++++++++++------ app/views/ingredients/_form.html.erb | 41 ++++++++++++++++++ .../_ingredient_unit_fields.html.erb | 22 ++++++++++ config/initializers/inflections.rb | 4 ++ .../20160705181132_add_ingredient_units.rb | 9 ++++ db/schema.rb | 10 ++++- spec/factories/ingredient_unit.rb | 8 ++++ spec/models/ingredient_spec.rb | 13 ++++++ spec/models/ingredient_unit_spec.rb | 31 +++++++++++++ spec/models/recipe_ingredient_spec.rb | 21 +++++++++ 15 files changed, 241 insertions(+), 16 deletions(-) create mode 100644 app/models/ingredient_unit.rb create mode 100644 app/views/ingredients/_ingredient_unit_fields.html.erb create mode 100644 db/migrate/20160705181132_add_ingredient_units.rb create mode 100644 spec/factories/ingredient_unit.rb create mode 100644 spec/models/ingredient_unit_spec.rb diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5fbeb62..abbdaba 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -24,6 +24,28 @@ @import "typeahead-bootstrap"; @import "recipes"; +// Skin overrides +.has-error { + .help-block, + .control-label, + .radio, + .checkbox, + .radio-inline, + .checkbox-inline, + &.radio label, + &.checkbox label, + &.radio-inline label, + &.checkbox-inline label, + .form-control-feedback { + color: $state-danger-text; + } + + .form-control, + .form-control:focus { + border-color: $state-danger-border; + } +} + $footer_height: 40px; html { diff --git a/app/assets/stylesheets/journal_custom_colors.scss b/app/assets/stylesheets/journal_custom_colors.scss index ed9e16d..4196c72 100644 --- a/app/assets/stylesheets/journal_custom_colors.scss +++ b/app/assets/stylesheets/journal_custom_colors.scss @@ -1,4 +1,5 @@ $brand-primary: darken(#93C54B, 10%); +$brand-success: $brand-primary; $text-color: #3E3F3A; diff --git a/app/controllers/ingredients_controller.rb b/app/controllers/ingredients_controller.rb index b646bd0..dde57e2 100644 --- a/app/controllers/ingredients_controller.rb +++ b/app/controllers/ingredients_controller.rb @@ -127,7 +127,7 @@ class IngredientsController < ApplicationController # 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) + params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy]) end def conversion_params diff --git a/app/models/ingredient.rb b/app/models/ingredient.rb index ffd5f85..217ec9c 100644 --- a/app/models/ingredient.rb +++ b/app/models/ingredient.rb @@ -2,6 +2,9 @@ class Ingredient < ActiveRecord::Base include TokenizedLike belongs_to :user + has_many :ingredient_units, inverse_of: :ingredient, dependent: :destroy + + accepts_nested_attributes_for :ingredient_units, allow_destroy: true validates :name, presence: true validates :density, density: true, allow_blank: true @@ -18,6 +21,16 @@ class Ingredient < ActiveRecord::Base end end + def custom_unit_weight(unit) + ingredient_units.each do |iu| + if iu.matches?(unit) + return UnitConversion::parse(iu.gram_weight, 'grams') + end + end + + nil + end + def ndbn=(value) @usda_food = nil super diff --git a/app/models/ingredient_unit.rb b/app/models/ingredient_unit.rb new file mode 100644 index 0000000..0a92278 --- /dev/null +++ b/app/models/ingredient_unit.rb @@ -0,0 +1,17 @@ +class IngredientUnit < ActiveRecord::Base + belongs_to :ingredient, inverse_of: :ingredient_units + + validates :name, presence: true + validates :gram_weight, presence: true + + def matches?(unit) + case + when unit.empty? && ['each', 'ech', 'item', 'per'].include?(self.name.downcase) + true + when unit.downcase.singularize == self.name.downcase + true + else + false + end + end +end \ No newline at end of file diff --git a/app/models/recipe_ingredient.rb b/app/models/recipe_ingredient.rb index 2c9b7dc..0be47b2 100644 --- a/app/models/recipe_ingredient.rb +++ b/app/models/recipe_ingredient.rb @@ -67,32 +67,47 @@ class RecipeIngredient < ActiveRecord::Base end def to_mass - return unless self.quantity.present? - if ingredient && ingredient.density - density = UnitConversion.parse(ingredient.density) - if density.density? - value_unit = UnitConversion.parse(self.quantity, self.units) - value_unit = value_unit.to_mass(density) + value_unit = as_value_unit + density = self.ingredient.density? ? UnitConversion.parse(ingredient.density) : nil + case + 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) self.quantity = value_unit.pretty_value self.units = value_unit.unit.to_s - end end end + def custom_unit? + self.ingredient && self.ingredient.custom_unit_weight(self.units).present? + end + def can_convert_to_grams? - if self.quantity.present? && self.units.present? - value_unit = UnitConversion.parse(self.quantity, self.units) - value_unit.mass? || (value_unit.volume? && self.ingredient && self.ingredient.density.present?) - else - false - end + vu = as_value_unit + vu.present? && (vu.mass? || (vu.volume? && self.ingredient && self.ingredient.density.present?)) end def to_grams - value_unit = UnitConversion.parse(self.quantity, self.units) + value_unit = as_value_unit gram_unit = value_unit.convert('g', self.ingredient ? self.ingredient.density : nil) gram_unit.raw_value end + def as_value_unit + case + when self.quantity.blank? + nil + when self.custom_unit? + self.ingredient.custom_unit_weight(self.units).scale(self.quantity) + when self.units.present? + UnitConversion.parse(self.quantity, self.units) + else + nil + end + end + end diff --git a/app/views/ingredients/_form.html.erb b/app/views/ingredients/_form.html.erb index cf84fca..af4f632 100644 --- a/app/views/ingredients/_form.html.erb +++ b/app/views/ingredients/_form.html.erb @@ -33,6 +33,47 @@ <%= f.text_field :density, class: 'form-control', disabled: has_ndbn %> +
+
+

Ingredient Units

+
+
+
+ <%= f.fields_for :ingredient_units do |iu_form| %> + <%= render partial: 'ingredients/ingredient_unit_fields', locals: {f: iu_form } %> + <% end %> + + <%= link_to_add_association 'add unit', f, :ingredient_units, class: 'btn btn-primary' %> +
+
+
+ + <% if has_ndbn %> + +
+
+

NDBN Unit Weights

+
+
+ + + + + + + + <% @ingredient.usda_food.usda_food_weights.each do |w| %> + + + + + <% end %> +
NameGrams
<%= "#{w.amount} #{w.description}" %><%= w.gram_weight %>
+
+
+ + <% end %> +
<%= f.label :notes, class: 'control-label' %> <%= f.text_area :notes, class: 'form-control' %> diff --git a/app/views/ingredients/_ingredient_unit_fields.html.erb b/app/views/ingredients/_ingredient_unit_fields.html.erb new file mode 100644 index 0000000..804bbf8 --- /dev/null +++ b/app/views/ingredients/_ingredient_unit_fields.html.erb @@ -0,0 +1,22 @@ +
+ +
+ <%= f.label :name, class: 'control-label' %> + <%= f.text_field :name, class: 'form-control' %> +
+ +
+ <%= f.label :gram_weight, 'Grams', class: 'control-label' %> + <%= f.text_field :gram_weight, class: 'form-control' %> +
+ +
+ +

+ <%= link_to_remove_association f, class: 'btn btn-danger btn-sm' do %> + + <% end %> +

+
+ +
\ No newline at end of file diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf..1950a5b 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -14,3 +14,7 @@ # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end + +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.irregular 'clove', 'cloves' +end \ No newline at end of file diff --git a/db/migrate/20160705181132_add_ingredient_units.rb b/db/migrate/20160705181132_add_ingredient_units.rb new file mode 100644 index 0000000..43a66b6 --- /dev/null +++ b/db/migrate/20160705181132_add_ingredient_units.rb @@ -0,0 +1,9 @@ +class AddIngredientUnits < ActiveRecord::Migration + def change + create_table :ingredient_units do |t| + t.integer :ingredient_id, null: false, index: true + t.string :name, null: false + t.decimal :gram_weight, precision: 10, scale: 2, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d309b08..45691c5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,15 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160622180838) do +ActiveRecord::Schema.define(version: 20160705181132) do + + create_table "ingredient_units", force: :cascade do |t| + t.integer "ingredient_id", null: false + t.string "name", null: false + t.decimal "gram_weight", precision: 10, scale: 2, null: false + end + + add_index "ingredient_units", ["ingredient_id"], name: "index_ingredient_units_on_ingredient_id" create_table "ingredients", force: :cascade do |t| t.string "name" diff --git a/spec/factories/ingredient_unit.rb b/spec/factories/ingredient_unit.rb new file mode 100644 index 0000000..74fc05b --- /dev/null +++ b/spec/factories/ingredient_unit.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :ingredient_unit do + ingredient + name 'Each' + gram_weight 10.5 + end + +end diff --git a/spec/models/ingredient_spec.rb b/spec/models/ingredient_spec.rb index e79a252..682a401 100644 --- a/spec/models/ingredient_spec.rb +++ b/spec/models/ingredient_spec.rb @@ -27,6 +27,7 @@ RSpec.describe Ingredient, type: :model do it 'sets the density' do i = build(:ingredient) f = create(:salted_butter) + create(:usda_food_weight, usda_food: f) i.set_usda_food(f) @@ -34,4 +35,16 @@ RSpec.describe Ingredient, type: :model do end end + describe '#custom_unit_weight' do + it 'returns a ValueUnit for valid custom weights' do + i = build(:ingredient) + i.ingredient_units << IngredientUnit.new(name: 'clove', gram_weight: 20.0) + + vu = i.custom_unit_weight('clove') + expect(vu).not_to be_nil + expect(vu.raw_value).to eq 20.0 + expect(vu.unit.to_s).to eq 'gram' + end + end + end diff --git a/spec/models/ingredient_unit_spec.rb b/spec/models/ingredient_unit_spec.rb new file mode 100644 index 0000000..c2b189c --- /dev/null +++ b/spec/models/ingredient_unit_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe IngredientUnit, type: :model do + + describe 'matches?' do + it 'matches empty units to each-like terms' do + each_terms = [ + 'Each', + 'each', + 'ech', + 'item', + 'per', + 'PER', + 'Per' + ] + + each_terms.each do |t| + expect(create(:ingredient_unit, name: t).matches?('')).to eq(true), "expected #{t} to match" + end + end + + it 'matches unit names' do + expect(create(:ingredient_unit, name: 'Clove').matches?('clove')).to eq true + end + + it 'matches pluralized unit names' do + expect(create(:ingredient_unit, name: 'Clove').matches?('cloves')).to eq true + end + end + +end \ No newline at end of file diff --git a/spec/models/recipe_ingredient_spec.rb b/spec/models/recipe_ingredient_spec.rb index a0c61cf..091d8d8 100644 --- a/spec/models/recipe_ingredient_spec.rb +++ b/spec/models/recipe_ingredient_spec.rb @@ -2,6 +2,27 @@ require 'rails_helper' RSpec.describe RecipeIngredient, type: :model do + describe 'to_mass' do + + it 'converts volume ingredients with density' do + ri = RecipeIngredient.new(quantity: 2, units: 'tbsp', ingredient: create(:ingredient_with_density)) + expect(ri.as_value_unit.mass?).to be_falsey + ri.to_mass + expect(ri.as_value_unit.mass?).to be_truthy + end + + it 'converts ingredients with custom units' do + i = create(:ingredient_with_density) + i.ingredient_units << IngredientUnit.new(name: 'pat', gram_weight: 25) + ri = RecipeIngredient.new(quantity: 2, units: 'pat', ingredient: i) + ri.to_mass + vu = ri.as_value_unit + expect(vu.raw_value).to eq 50 + expect(vu.unit.to_s).to eq 'gram' + end + + end + describe '#can_convert_to_grams' do it 'returns false if no quantity or unit' do ri = RecipeIngredient.new