diff --git a/app/assets/javascripts/ingredients.js b/app/assets/javascripts/ingredients.js index aa49ca3..14afb46 100644 --- a/app/assets/javascripts/ingredients.js +++ b/app/assets/javascripts/ingredients.js @@ -1,9 +1,63 @@ (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({ initialize: false, datumTokenizer: function(datum) { - return Bloodhound.tokenizers.whitespace(datum.name); + var str = datum ? datum.name : null; + return str ? str.split(/[\s,]+/) : []; }, queryTokenizer: Bloodhound.tokenizers.whitespace, identify: function(datum) { return datum.ndbn; }, @@ -26,18 +80,8 @@ var $ingredientForm = $("#ingredient_form"); 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); \ No newline at end of file diff --git a/app/assets/javascripts/typeahead_search.js b/app/assets/javascripts/typeahead_search.js new file mode 100644 index 0000000..c52b822 --- /dev/null +++ b/app/assets/javascripts/typeahead_search.js @@ -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 = $('
'); + var $btnSpan = $(""); + var $btn = $("").append($("").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); \ No newline at end of file diff --git a/app/controllers/ingredients_controller.rb b/app/controllers/ingredients_controller.rb index f42198f..db3bd5c 100644 --- a/app/controllers/ingredients_controller.rb +++ b/app/controllers/ingredients_controller.rb @@ -1,6 +1,6 @@ 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] @@ -36,11 +36,15 @@ class IngredientsController < ApplicationController end # PATCH/PUT /ingredients/1 - # PATCH/PUT /ingredients/1.json 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| - if @ingredient.update(ingredient_params) - format.html { redirect_to @ingredient, notice: 'Ingredient was successfully updated.' } + if @ingredient.save + format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully updated.' } format.json { render :show, status: :ok, location: @ingredient } else format.html { render :edit } @@ -59,14 +63,24 @@ class IngredientsController < ApplicationController 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 @ingredients = Ingredient.all.order(:name) render :search end def search - query = params[:query] + '%' - @ingredients = Ingredient.where("name LIKE ?", query).order(:name) + @ingredients = Ingredient.search(params[:query]).order(:name) end def convert @@ -81,7 +95,12 @@ class IngredientsController < ApplicationController end 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 private @@ -92,7 +111,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, :density, :notes) + params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :kcal, :fiber, :sugar) end def conversion_params diff --git a/app/models/concerns/tokenized_like.rb b/app/models/concerns/tokenized_like.rb new file mode 100644 index 0000000..b3a7c95 --- /dev/null +++ b/app/models/concerns/tokenized_like.rb @@ -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 \ No newline at end of file diff --git a/app/models/ingredient.rb b/app/models/ingredient.rb index 00121a6..bf15369 100644 --- a/app/models/ingredient.rb +++ b/app/models/ingredient.rb @@ -1,9 +1,22 @@ class Ingredient < ActiveRecord::Base + include TokenizedLike validates :name, presence: 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) + return unless food + self.ndbn = food.ndbn self.water = food.water self.protein = food.protein diff --git a/app/models/usda_food.rb b/app/models/usda_food.rb index 7357f98..14b4935 100644 --- a/app/models/usda_food.rb +++ b/app/models/usda_food.rb @@ -1,3 +1,14 @@ 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 \ No newline at end of file diff --git a/app/views/ingredients/_form.html.erb b/app/views/ingredients/_form.html.erb index 4632122..b63ffe0 100644 --- a/app/views/ingredients/_form.html.erb +++ b/app/views/ingredients/_form.html.erb @@ -1,19 +1,38 @@ +<% has_ndbn = @ingredient.ndbn.present? %> <%= form_for(@ingredient, html: {id: 'ingredient_form'}) do |f| %> <%= render partial: 'shared/error_list', locals: {model: @ingredient} %> + <%= f.hidden_field :ndbn, class: 'ndbn' %> + <%= f.hidden_field :id, class: 'id', disabled: true %> +<%= @ingredient.ndbn ? UsdaFood.find_by_ndbn(@ingredient.ndbn).short_description : '' %>
+