Ingredient editor
This commit is contained in:
parent
7f7a81d49a
commit
70e7a8b415
@ -1,9 +1,63 @@
|
|||||||
(function($) {
|
(function($) {
|
||||||
|
|
||||||
|
function initializeEditor($ingredientForm) {
|
||||||
|
usdaFoodSearchEngine.initialize(false);
|
||||||
|
|
||||||
|
var $typeahead = $ingredientForm.find(".ndbn_typeahead");
|
||||||
|
var $usdaModal = $("#link_ndbn_modal");
|
||||||
|
var $name = $ingredientForm.find(".name");
|
||||||
|
var $ndbn = $ingredientForm.find("input.ndbn");
|
||||||
|
var $ndbn_group = $ingredientForm.find(".ndbn_group");
|
||||||
|
|
||||||
|
if ($ndbn.val()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$typeahead.typeahead_search({
|
||||||
|
searchUrl: '/ingredients/usda_food_search.html',
|
||||||
|
resultsContainer: '#link_ndbn_modal .results'
|
||||||
|
},{
|
||||||
|
name: 'usdaFoods',
|
||||||
|
source: usdaFoodSearchEngine,
|
||||||
|
display: function(datum) {
|
||||||
|
return datum.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$typeahead.on("typeahead_search:selected", function(evt, item) {
|
||||||
|
selectNdbn(item.ndbn);
|
||||||
|
});
|
||||||
|
|
||||||
|
$usdaModal.on("shown.bs.modal", function() {
|
||||||
|
var $this = $(this);
|
||||||
|
$typeahead.typeahead("val", $name.val());
|
||||||
|
$typeahead.focus();
|
||||||
|
$typeahead.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
$ingredientForm.on("click", "#link_ndbn_modal .results .food_result", function(evt) {
|
||||||
|
var $item = $(evt.target);
|
||||||
|
var ndbn = $item.data("ndbn");
|
||||||
|
selectNdbn(ndbn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNdbn(ndbn) {
|
||||||
|
var $ingredientForm = $("#ingredient_form");
|
||||||
|
var id = $ingredientForm.find("input.id").val();
|
||||||
|
|
||||||
|
$ingredientForm.find("input.ndbn").val(ndbn);
|
||||||
|
$ingredientForm.attr("action", "/ingredients/" + id + "/select_ndbn").attr("data-remote", "true");
|
||||||
|
|
||||||
|
$("#link_ndbn_modal").modal('hide').on('hidden.bs.modal', function() {
|
||||||
|
$ingredientForm.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var usdaFoodSearchEngine = new Bloodhound({
|
var usdaFoodSearchEngine = new Bloodhound({
|
||||||
initialize: false,
|
initialize: false,
|
||||||
datumTokenizer: function(datum) {
|
datumTokenizer: function(datum) {
|
||||||
return Bloodhound.tokenizers.whitespace(datum.name);
|
var str = datum ? datum.name : null;
|
||||||
|
return str ? str.split(/[\s,]+/) : [];
|
||||||
},
|
},
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
identify: function(datum) { return datum.ndbn; },
|
identify: function(datum) { return datum.ndbn; },
|
||||||
@ -26,18 +80,8 @@
|
|||||||
var $ingredientForm = $("#ingredient_form");
|
var $ingredientForm = $("#ingredient_form");
|
||||||
|
|
||||||
if ($ingredientForm.length) {
|
if ($ingredientForm.length) {
|
||||||
usdaFoodSearchEngine.initialize(false);
|
initializeEditor($ingredientForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ingredientForm.find(".ndbn_typeahead").typeahead_selector({
|
|
||||||
|
|
||||||
},{
|
|
||||||
name: 'usdaFoods',
|
|
||||||
source: usdaFoodSearchEngine,
|
|
||||||
display: function(datum) {
|
|
||||||
return datum.name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
107
app/assets/javascripts/typeahead_search.js
Normal file
107
app/assets/javascripts/typeahead_search.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
(function($) {
|
||||||
|
|
||||||
|
var pluginName = "typeahead_search";
|
||||||
|
|
||||||
|
var defaultOptions = {
|
||||||
|
};
|
||||||
|
|
||||||
|
var methods = {
|
||||||
|
initialize: function (opts, sources) {
|
||||||
|
|
||||||
|
return this.each(function() {
|
||||||
|
var options = $.extend({}, defaultOptions, opts);
|
||||||
|
var $this = $(this);
|
||||||
|
$this.data(pluginName, {options: options});
|
||||||
|
|
||||||
|
var $inputGroup = $('<div class="input-group" />');
|
||||||
|
var $btnSpan = $("<span class='input-group-btn' />");
|
||||||
|
var $btn = $("<button class='btn btn-default' type='button' />").append($("<span />").addClass("glyphicon glyphicon-search"));
|
||||||
|
|
||||||
|
$btnSpan.append($btn);
|
||||||
|
|
||||||
|
$this.after($inputGroup);
|
||||||
|
$this.detach();
|
||||||
|
$inputGroup.append($this);
|
||||||
|
$inputGroup.append($btnSpan);
|
||||||
|
|
||||||
|
$this.typeahead(opts, sources);
|
||||||
|
|
||||||
|
$btn.on("click", function(evt) {
|
||||||
|
privateMethods.search($this);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this
|
||||||
|
.on("typeahead:change", function(evt, value) {
|
||||||
|
privateMethods.change($this, value);
|
||||||
|
})
|
||||||
|
.on("typeahead:select", function(evt, value) {
|
||||||
|
privateMethods.select($this, value);
|
||||||
|
})
|
||||||
|
.on("typeahead:autocomplete", function(evt, value) {
|
||||||
|
privateMethods.autocomplete($this, value);
|
||||||
|
})
|
||||||
|
.on("keydown", function(evt) {
|
||||||
|
if (evt.keyCode == 13) {
|
||||||
|
evt.preventDefault();
|
||||||
|
privateMethods.search($this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
val: function() {
|
||||||
|
if (this.length) {
|
||||||
|
return $(this[0]).typeahead("val");
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var privateMethods = {
|
||||||
|
change: function($this, value) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
select: function($this, item) {
|
||||||
|
$this.trigger("typeahead_search:selected", item);
|
||||||
|
},
|
||||||
|
|
||||||
|
autocomplete: function($this, item) {
|
||||||
|
$this.trigger("typeahead_search:selected", item);
|
||||||
|
},
|
||||||
|
|
||||||
|
search: function($this) {
|
||||||
|
var options = privateMethods.options($this);
|
||||||
|
var input = $this.typeahead("val");
|
||||||
|
|
||||||
|
if (input.length && options.searchUrl && options.searchUrl.length) {
|
||||||
|
$.get({
|
||||||
|
url: options.searchUrl,
|
||||||
|
data: {query: input},
|
||||||
|
dataType: 'html',
|
||||||
|
success: function(data) {
|
||||||
|
$(options.resultsContainer).empty().append(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$this.typeahead("close");
|
||||||
|
},
|
||||||
|
|
||||||
|
options: function($this) {
|
||||||
|
return $this.data(pluginName).options;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn[pluginName] = function (method) {
|
||||||
|
if (methods[method]) {
|
||||||
|
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||||
|
} else if (typeof method === 'object' || ! method) {
|
||||||
|
return methods.initialize.apply(this, arguments);
|
||||||
|
} else {
|
||||||
|
$.error('Method ' + method + ' does not exist on jQuery.' + pluginName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
@ -1,6 +1,6 @@
|
|||||||
class IngredientsController < ApplicationController
|
class IngredientsController < ApplicationController
|
||||||
|
|
||||||
before_action :set_ingredient, only: [:edit, :update, :destroy]
|
before_action :set_ingredient, only: [:edit, :update, :destroy, :select_ndbn]
|
||||||
|
|
||||||
before_filter :ensure_valid_user, only: [:new, :edit, :create, :update, :destroy]
|
before_filter :ensure_valid_user, only: [:new, :edit, :create, :update, :destroy]
|
||||||
|
|
||||||
@ -36,11 +36,15 @@ class IngredientsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
# PATCH/PUT /ingredients/1
|
# PATCH/PUT /ingredients/1
|
||||||
# PATCH/PUT /ingredients/1.json
|
|
||||||
def update
|
def update
|
||||||
|
@ingredient.assign_attributes(ingredient_params)
|
||||||
|
if @ingredient.ndbn.present?
|
||||||
|
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
|
||||||
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @ingredient.update(ingredient_params)
|
if @ingredient.save
|
||||||
format.html { redirect_to @ingredient, notice: 'Ingredient was successfully updated.' }
|
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully updated.' }
|
||||||
format.json { render :show, status: :ok, location: @ingredient }
|
format.json { render :show, status: :ok, location: @ingredient }
|
||||||
else
|
else
|
||||||
format.html { render :edit }
|
format.html { render :edit }
|
||||||
@ -59,14 +63,24 @@ class IngredientsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def select_ndbn
|
||||||
|
@ingredient.assign_attributes(ingredient_params)
|
||||||
|
if @ingredient.ndbn.present?
|
||||||
|
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.js {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def prefetch
|
def prefetch
|
||||||
@ingredients = Ingredient.all.order(:name)
|
@ingredients = Ingredient.all.order(:name)
|
||||||
render :search
|
render :search
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
query = params[:query] + '%'
|
@ingredients = Ingredient.search(params[:query]).order(:name)
|
||||||
@ingredients = Ingredient.where("name LIKE ?", query).order(:name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert
|
def convert
|
||||||
@ -81,7 +95,12 @@ class IngredientsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def usda_food_search
|
def usda_food_search
|
||||||
@foods = UsdaFood.where("short_description LIKE ?", "%#{params[:query]}%").limit(50)
|
@foods = UsdaFood.search(params[:query]).limit(50)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render :layout => false }
|
||||||
|
format.json { }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -92,7 +111,7 @@ class IngredientsController < ApplicationController
|
|||||||
|
|
||||||
# Never trust parameters from the scary internet, only allow the white list through.
|
# Never trust parameters from the scary internet, only allow the white list through.
|
||||||
def ingredient_params
|
def ingredient_params
|
||||||
params.require(:ingredient).permit(:name, :density, :notes)
|
params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :kcal, :fiber, :sugar)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversion_params
|
def conversion_params
|
||||||
|
26
app/models/concerns/tokenized_like.rb
Normal file
26
app/models/concerns/tokenized_like.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module TokenizedLike
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
def matches_tokens(attribute, tokens)
|
||||||
|
table = self.arel_table
|
||||||
|
query = self.all
|
||||||
|
|
||||||
|
tokens.each do |t|
|
||||||
|
match1 = "#{t}%"
|
||||||
|
match2 = "% #{t}%"
|
||||||
|
match3 = "%,#{t}%"
|
||||||
|
|
||||||
|
matcher = ->(m) { table[attribute.to_sym].matches(m) }
|
||||||
|
|
||||||
|
cond = matcher.call(match1).or(matcher.call(match2)).or(matcher.call(match3))
|
||||||
|
|
||||||
|
query = query.where(cond)
|
||||||
|
end
|
||||||
|
|
||||||
|
query
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
@ -1,9 +1,22 @@
|
|||||||
class Ingredient < ActiveRecord::Base
|
class Ingredient < ActiveRecord::Base
|
||||||
|
include TokenizedLike
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :density, density: true, allow_blank: true
|
validates :density, density: true, allow_blank: true
|
||||||
|
|
||||||
|
def self.search(query)
|
||||||
|
tokens = query.to_s.split(' ')
|
||||||
|
|
||||||
|
if tokens.empty?
|
||||||
|
Ingredient.none
|
||||||
|
else
|
||||||
|
Ingredient.matches_tokens(:name, tokens)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_usda_food(food)
|
def set_usda_food(food)
|
||||||
|
return unless food
|
||||||
|
|
||||||
self.ndbn = food.ndbn
|
self.ndbn = food.ndbn
|
||||||
self.water = food.water
|
self.water = food.water
|
||||||
self.protein = food.protein
|
self.protein = food.protein
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
class UsdaFood < ActiveRecord::Base
|
class UsdaFood < ActiveRecord::Base
|
||||||
|
include TokenizedLike
|
||||||
|
|
||||||
|
def self.search(query)
|
||||||
|
tokens = query.to_s.split(' ')
|
||||||
|
|
||||||
|
if tokens.empty?
|
||||||
|
UsdaFood.none
|
||||||
|
else
|
||||||
|
UsdaFood.matches_tokens(:short_description, tokens)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
@ -1,65 +1,74 @@
|
|||||||
|
|
||||||
|
<% has_ndbn = @ingredient.ndbn.present? %>
|
||||||
|
|
||||||
<%= form_for(@ingredient, html: {id: 'ingredient_form'}) do |f| %>
|
<%= form_for(@ingredient, html: {id: 'ingredient_form'}) do |f| %>
|
||||||
|
|
||||||
<%= render partial: 'shared/error_list', locals: {model: @ingredient} %>
|
<%= render partial: 'shared/error_list', locals: {model: @ingredient} %>
|
||||||
|
|
||||||
<div class="form-group">
|
<%= f.hidden_field :ndbn, class: 'ndbn' %>
|
||||||
<%= f.label :name, class: 'control-label' %>
|
<%= f.hidden_field :id, class: 'id', disabled: true %>
|
||||||
<%= f.text_field :name, class: 'form-control', autofocus: true %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<%= f.label :name, class: 'control-label' %>
|
||||||
|
<%= f.text_field :name, class: 'form-control name', autofocus: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group ndbn_group">
|
||||||
<%= f.label :ndbn, "Nutrient Databank Number", class: 'control-label' %>
|
<%= f.label :ndbn, "Nutrient Databank Number", class: 'control-label' %>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-btn">
|
||||||
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#link_ndbn_modal">
|
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#link_ndbn_modal">
|
||||||
<span class="glyphicon glyphicon-link"></span><span class="ndbn"><%= @ingredient.ndbn %></span>
|
<span class="glyphicon glyphicon-link"></span><span class="ndbn"><%= @ingredient.ndbn %></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="form-control-static" style="padding-left: 7px;"><%= @ingredient.ndbn ? UsdaFood.find_by_ndbn(@ingredient.ndbn).short_description : '' %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<%= f.label :density, class: 'control-label' %>
|
||||||
|
<%= f.text_field :density, class: 'form-control', disabled: has_ndbn %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<%= f.label :notes, class: 'control-label' %>
|
||||||
|
<%= f.text_area :notes, class: 'form-control' %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Per 100 Grams</legend>
|
<legend>Per 100 Grams</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :water, "Grams of Water", class: 'control-label' %>
|
<%= f.label :water, "Grams of Water", class: 'control-label' %>
|
||||||
<%= f.text_field :water, class: 'form-control' %>
|
<%= f.text_field :water, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :protein, "Grams of Protein", class: 'control-label' %>
|
<%= f.label :protein, "Grams of Protein", class: 'control-label' %>
|
||||||
<%= f.text_field :protein, class: 'form-control' %>
|
<%= f.text_field :protein, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :lipids, "Grams of Fat", class: 'control-label' %>
|
<%= f.label :lipids, "Grams of Fat", class: 'control-label' %>
|
||||||
<%= f.text_field :lipids, class: 'form-control' %>
|
<%= f.text_field :lipids, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :kcal, "Calories", class: 'control-label' %>
|
<%= f.label :kcal, "Calories", class: 'control-label' %>
|
||||||
<%= f.text_field :kcal, class: 'form-control' %>
|
<%= f.text_field :kcal, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :fiber, "Grams of Fiber", class: 'control-label' %>
|
<%= f.label :fiber, "Grams of Fiber", class: 'control-label' %>
|
||||||
<%= f.text_field :fiber, class: 'form-control' %>
|
<%= f.text_field :fiber, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :sugar, "Grams of Sugar", class: 'control-label' %>
|
<%= f.label :sugar, "Grams of Sugar", class: 'control-label' %>
|
||||||
<%= f.text_field :sugar, class: 'form-control' %>
|
<%= f.text_field :sugar, class: 'form-control', disabled: has_ndbn %>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<%= f.label :density, class: 'control-label' %>
|
|
||||||
<%= f.text_field :density, class: 'form-control' %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<%= f.label :notes, class: 'control-label' %>
|
|
||||||
<%= f.text_area :notes, class: 'form-control' %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<%= f.submit class: 'btn btn-primary' %>
|
<%= f.submit class: 'btn btn-primary' %>
|
||||||
</div>
|
</div>
|
||||||
@ -75,10 +84,13 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :ndbn, "", class: 'control-label' %>
|
<%= f.label :ndbn, "", class: 'control-label' %>
|
||||||
<input type="text" class="ndbn_typeahead form-control" />
|
<input type="text" class="ndbn_typeahead form-control" />
|
||||||
<%= f.hidden_field :ndbn %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
2
app/views/ingredients/select_ndbn.js.erb
Normal file
2
app/views/ingredients/select_ndbn.js.erb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
$("#ingredient_form").replaceWith($("<%= escape_javascript(render(partial: 'ingredients/form')) %>"));
|
20
app/views/ingredients/usda_food_search.html.erb
Normal file
20
app/views/ingredients/usda_food_search.html.erb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<table class="table">
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>NDBN</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<% @foods.each do |f| %>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><%= f.ndbn %></th>
|
||||||
|
<th><%= link_to f.short_description, '#', class: 'food_result', data: {ndbn: f.ndbn} %></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</table>
|
@ -8,13 +8,18 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :ingredients, except: [:show] do
|
resources :ingredients, except: [:show] do
|
||||||
collection do
|
collection do
|
||||||
|
get :usda_food_search
|
||||||
|
|
||||||
constraints format: 'json' do
|
constraints format: 'json' do
|
||||||
get :search
|
get :search
|
||||||
get :prefetch
|
get :prefetch
|
||||||
get :convert
|
get :convert
|
||||||
get :usda_food_search
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
member do
|
||||||
|
match :select_ndbn, via: [:post, :patch, :put]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resource :user, only: [:new, :create, :edit, :update]
|
resource :user, only: [:new, :create, :edit, :update]
|
||||||
|
@ -2,9 +2,9 @@ class CreateUsdaFood < ActiveRecord::Migration
|
|||||||
def change
|
def change
|
||||||
create_table :usda_foods do |t|
|
create_table :usda_foods do |t|
|
||||||
|
|
||||||
t.string :ndbn, limit: 5, index: true, null: false
|
t.string :ndbn, limit: 5, index: :unique, null: false
|
||||||
|
|
||||||
t.string :short_description
|
t.string :short_description, index: true
|
||||||
t.decimal :water, precision: 10, scale: 2
|
t.decimal :water, precision: 10, scale: 2
|
||||||
t.integer :kcal
|
t.integer :kcal
|
||||||
t.decimal :protein, precision: 10, scale: 2
|
t.decimal :protein, precision: 10, scale: 2
|
||||||
|
@ -88,6 +88,7 @@ ActiveRecord::Schema.define(version: 20160124231837) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
add_index "usda_foods", ["ndbn"], name: "index_usda_foods_on_ndbn"
|
add_index "usda_foods", ["ndbn"], name: "index_usda_foods_on_ndbn"
|
||||||
|
add_index "usda_foods", ["short_description"], name: "index_usda_foods_on_short_description"
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "username"
|
t.string "username"
|
||||||
|
@ -31,6 +31,8 @@ Ingredient.create!([
|
|||||||
{name: 'Milk, 2%', density: '1.03 gram/ml'}
|
{name: 'Milk, 2%', density: '1.03 gram/ml'}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
User.create!({username: 'dan', full_name: 'Dan', email: 'dan.elbert@gmail.com', password: 'qwerty', password_confirmation: 'qwerty'})
|
||||||
|
|
||||||
importer = UsdaImporter.new(Rails.root.join('vendor', 'data', 'usda', 'ABBREV.txt'))
|
importer = UsdaImporter.new(Rails.root.join('vendor', 'data', 'usda', 'ABBREV.txt'))
|
||||||
importer.import
|
importer.import
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace :usda do
|
||||||
|
|
||||||
|
desc 'Empties usda_foods table, imports all data, and then updates any linked ingredients'
|
||||||
|
task import: :environment do
|
||||||
|
importer = UsdaImporter.new(Rails.root.join('vendor', 'data', 'usda', 'ABBREV.txt'))
|
||||||
|
importer.import
|
||||||
|
end
|
||||||
|
end
|
25
spec/factories/usda_foods.rb
Normal file
25
spec/factories/usda_foods.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FactoryGirl.define do
|
||||||
|
|
||||||
|
sequence :unique_ndbn do |n|
|
||||||
|
n.to_s.rjust(5, '0')
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :usda_food do
|
||||||
|
short_description 'Food'
|
||||||
|
ndbn '01234'
|
||||||
|
water 1.0
|
||||||
|
kcal 101
|
||||||
|
protein 1.2
|
||||||
|
lipid 3.4
|
||||||
|
ash 0
|
||||||
|
carbohydrates 3.3
|
||||||
|
fiber 1.2
|
||||||
|
sugar 10
|
||||||
|
gram_weight_1 200
|
||||||
|
gram_weight_2 100
|
||||||
|
gram_weight_desc_1 '1 cup'
|
||||||
|
gram_weight_desc_2 '1/2 cup'
|
||||||
|
refuse_percent 3
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
41
spec/models/usda_food_spec.rb
Normal file
41
spec/models/usda_food_spec.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe UsdaFood do
|
||||||
|
|
||||||
|
let!(:data) do
|
||||||
|
{
|
||||||
|
salted_butter: create(:usda_food, short_description: 'Salted Butter'),
|
||||||
|
unsalted_butter: create(:usda_food, short_description: 'Unsalted Butter'),
|
||||||
|
flour: create(:usda_food, short_description: 'Flour'),
|
||||||
|
bread_flour: create(:usda_food, short_description: 'Bread Flour'),
|
||||||
|
sugar: create(:usda_food, short_description: 'Sugar,Granulated')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def items(*syms)
|
||||||
|
Array.wrap(syms).map { |s| data[s] }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be found by single tokens' do
|
||||||
|
r = UsdaFood.matches_tokens(:short_description, ['sal'])
|
||||||
|
expect(r.length).to eq 1
|
||||||
|
expect(r).to contain_exactly *items(:salted_butter)
|
||||||
|
|
||||||
|
r = UsdaFood.matches_tokens(:short_description, ['flour'])
|
||||||
|
expect(r.length).to eq 2
|
||||||
|
expect(r).to contain_exactly *items(:flour, :bread_flour)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can be found by multiple tokens' do
|
||||||
|
r = UsdaFood.matches_tokens(:short_description, ['sal', 'butter'])
|
||||||
|
expect(r.length).to eq 1
|
||||||
|
expect(r).to contain_exactly *items(:salted_butter)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'treats commas like spaces' do
|
||||||
|
r = UsdaFood.matches_tokens(:short_description, ['gran', 'sugar'])
|
||||||
|
expect(r.length).to eq 1
|
||||||
|
expect(r).to contain_exactly *items(:sugar)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user