diff --git a/.travis.yml b/.travis.yml index 5b3795ac5..dd6853a02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ before_script: script: - bundle exec rake db:migrate --trace - bundle exec rspec spec/ +services: + - elasticsearch before_deploy: - bundle exec script/heroku_maintenance.rb on deploy: @@ -32,3 +34,4 @@ deploy: - restart after_deploy: - bundle exec script/heroku_maintenance.rb off + diff --git a/Gemfile b/Gemfile index fcf7b39d4..d15f48067 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,7 @@ gem 'figaro' # for handling config via ENV variables gem 'cancancan', '~> 1.9' # for checking member privileges gem 'gibbon' # for Mailchimp newsletter subscriptions gem 'csv_shaper' # CSV export +gem 'ruby-units' # for unit conversion # vendored activemerchant for testing- needed for bogus paypal # gateway monkeypatch @@ -76,6 +77,12 @@ gem 'omniauth' gem 'omniauth-twitter' gem 'omniauth-flickr', '>= 0.0.15' +# client for Elasticsearch. Elasticsearch is a flexible +# and powerful, distributed, real-time search and analytics engine. +# An example of the use in the project is fuzzy crop search. +gem "elasticsearch-model" +gem "elasticsearch-rails" + gem 'rake', '>= 10.0.0' group :production, :staging do @@ -83,6 +90,7 @@ group :production, :staging do gem 'dalli' gem 'memcachier' gem 'rails_12factor' # supresses heroku plugin injection + gem 'bonsai-elasticsearch-rails' # Integration with Bonsa-Elasticsearch on heroku end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index d54873d81..09f8547c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,6 +56,7 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) bluecloth (2.2.0) + bonsai-elasticsearch-rails (0.0.4) bootstrap-datepicker-rails (1.3.0.2) railties (>= 3.0) builder (3.2.2) @@ -109,6 +110,19 @@ GEM json thread thread_safe + elasticsearch (1.0.6) + elasticsearch-api (= 1.0.6) + elasticsearch-transport (= 1.0.6) + elasticsearch-api (1.0.6) + multi_json + elasticsearch-model (0.1.6) + activesupport (> 3) + elasticsearch (> 0.4) + hashie + elasticsearch-rails (0.1.6) + elasticsearch-transport (1.0.6) + faraday + multi_json erubis (2.7.0) excon (0.43.0) execjs (2.2.2) @@ -117,6 +131,8 @@ GEM factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) figaro (1.0.0) thor (~> 0.14) flickraw (0.9.8) @@ -195,6 +211,7 @@ GEM minitest (5.5.1) multi_json (1.10.1) multi_xml (0.5.5) + multipart-post (2.0.0) netrc (0.10.0) newrelic_rpm (3.9.8.273) nokogiri (1.6.5) @@ -275,6 +292,7 @@ GEM rspec-mocks (~> 3.1.0) rspec-support (~> 3.1.0) rspec-support (3.1.2) + ruby-units (1.4.5) ruby_parser (3.1.3) sexp_processor (~> 4.1) sass (3.2.19) @@ -341,6 +359,7 @@ DEPENDENCIES better_errors binding_of_caller bluecloth + bonsai-elasticsearch-rails bootstrap-datepicker-rails bundler (>= 1.1.5) byebug @@ -353,6 +372,8 @@ DEPENDENCIES dalli database_cleaner (~> 1.3.0) devise (~> 3.4.1) + elasticsearch-model + elasticsearch-rails factory_girl_rails (~> 4.5.0) figaro flickraw @@ -388,6 +409,7 @@ DEPENDENCIES rake (>= 10.0.0) rspec-activemodel-mocks rspec-rails (~> 3.1.0) + ruby-units sass-rails (~> 4.0.4) therubyracer (~> 0.12) uglifier (~> 2.5.3) diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 30a570fc0..19f55d7ef 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -60,18 +60,16 @@ class CropsController < ApplicationController # GET /crops/search def search - @search = params[:search] - @exact_match = Crop.find_by_name(params[:search]) - - @partial_matches = Crop.search(params[:search]) - # exclude exact match from partial match list - @partial_matches = @partial_matches.reject{ |r| @exact_match && r.eql?(@exact_match) } - - @fuzzy = Crop.search(params[:term]) + @all_matches = Crop.search(params[:search]) + exact_match = Crop.find_by_name(params[:search]) + if exact_match + @all_matches.delete(exact_match) + @all_matches.unshift(exact_match) + end respond_to do |format| format.html - format.json { render :json => @fuzzy } + format.json { render :json => Crop.search(params[:term]) } end end diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 5927f3df1..4bc3a5d90 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -107,6 +107,6 @@ class HarvestsController < ApplicationController def harvest_params params.require(:harvest).permit(:crop_id, :harvested_at, :description, :owner_id, - :quantity, :unit, :weight_quantity, :weight_unit, :plant_part_id, :slug) + :quantity, :unit, :weight_quantity, :weight_unit, :plant_part_id, :slug, :si_weight) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index ead008065..ff5fe4994 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -21,13 +21,14 @@ class Ability cannot :read, Account cannot :read, AccountType - # and nobody should be able to view these expect admins and crop wranglers - cannot :read, Crop, :approval_status => ["rejected", "pending"] - # # ... unless the current member is also the requester - # can :read, Crop, :requester_id => member.id - # can :read, Crop, :approval_status => "approved" + # nobody should be able to view unapproved crops unless they + # are wranglers or admins + cannot :read, Crop + can :read, Crop, :approval_status => "approved" if member + # members can see even rejected or pending crops if they requested it + can :read, Crop, :requester_id => member.id # managing your own user settings can :update, Member, :id => member.id diff --git a/app/models/alternate_name.rb b/app/models/alternate_name.rb index 408740680..cb6f79e9d 100644 --- a/app/models/alternate_name.rb +++ b/app/models/alternate_name.rb @@ -1,4 +1,5 @@ class AlternateName < ActiveRecord::Base + after_commit { |an| an.crop.__elasticsearch__.index_document if an.crop } belongs_to :crop belongs_to :creator, :class_name => 'Member' end diff --git a/app/models/crop.rb b/app/models/crop.rb index c3fe377b8..905af8562 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -2,12 +2,12 @@ class Crop < ActiveRecord::Base extend FriendlyId friendly_id :name, use: [:slugged, :finders] - has_many :scientific_names + has_many :scientific_names, after_add: :update_index, after_remove: :update_index accepts_nested_attributes_for :scientific_names, :allow_destroy => true, :reject_if => :all_blank - has_many :alternate_names + has_many :alternate_names, after_add: :update_index, after_remove: :update_index has_many :plantings has_many :photos, :through => :plantings has_many :seeds @@ -43,6 +43,68 @@ class Crop < ActiveRecord::Base ## This validation addresses a race condition validate :approval_status_cannot_be_changed_again + #################################### + # Elastic search configuration + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + # In order to avoid clashing between different environments, + # use Rails.env as a part of index name (eg. development_growstuff) + index_name [Rails.env, "growstuff"].join('_') + settings index: { number_of_shards: 1 }, + analysis: { + tokenizer: { + gs_edgeNGram_tokenizer: { + type: "edgeNGram", # edgeNGram: NGram match from the start of a token + min_gram: 3, + max_gram: 10, + # token_chars: Elasticsearch will split on characters + # that don’t belong to any of these classes + token_chars: [ "letter", "digit" ] + } + }, + analyzer: { + gs_edgeNGram_analyzer: { + tokenizer: "gs_edgeNGram_tokenizer", + filter: ["lowercase"] + } + }, + } do + mappings dynamic: 'false' do + indexes :id, type: 'long' + indexes :name, type: 'string', analyzer: 'gs_edgeNGram_analyzer' + indexes :scientific_names do + indexes :scientific_name, + type: 'string', + analyzer: 'gs_edgeNGram_analyzer', + # Disabling field-length norm (norm). If the norm option is turned on(by default), + # higher weigh would be given for shorter fields, which in our case is irrelevant. + norms: { enabled: false } + end + indexes :alternate_names do + indexes :name, type: 'string', analyzer: 'gs_edgeNGram_analyzer' + end + end + end + + def as_indexed_json(options={}) + self.as_json( + only: [:id, :name], + include: { + scientific_names: { only: :scientific_name }, + alternate_names: { only: :name } + }) + end + + # update the Elasticsearch index (only if we're using it in this + # environment) + def update_index(name_obj) + if ENV["GROWSTUFF_ELASTICSEARCH"] == "true" + __elasticsearch__.index_document + end + end + + # End Elasticsearch section + def to_s return name end @@ -232,10 +294,25 @@ class Crop < ActiveRecord::Base end # Crop.search(string) - # searches for crops whose names match the string given - # just uses SQL LIKE for now, but can be made fancier later def self.search(query) - where("name ILIKE ?", "%#{query}%") + if ENV['GROWSTUFF_ELASTICSEARCH'] == "true" + search_str = query.nil? ? "" : query.downcase + response = __elasticsearch__.search( { + # Finds documents which match any field, but uses the _score from + # the best field insead of adding up _score from each field. + query: { + multi_match: { + query: "#{search_str}", + fields: ["name", "scientific_names.scientific_name", "alternate_names.name"] + } + }, + size: 50 + } + ) + return response.records.to_a + else + where("name ILIKE ?", "%#{query}%") + end end # Custom validations diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 4e43c6ee7..c565b2c91 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -59,6 +59,17 @@ class Harvest < ActiveRecord::Base after_validation :cleanup_quantities + before_save :set_si_weight + + # we're storing the harvest weight in kilograms in the db too + # to make data manipulation easier + def set_si_weight + if self.weight_unit != nil + weight_string = "#{self.weight_quantity} #{self.weight_unit}" + self.si_weight = Unit(weight_string).convert_to("kg").to_s("%0.3f").delete(" kg").to_f + end + end + def cleanup_quantities if quantity == 0 self.quantity = nil diff --git a/app/models/scientific_name.rb b/app/models/scientific_name.rb index 1a585e713..7469b3946 100644 --- a/app/models/scientific_name.rb +++ b/app/models/scientific_name.rb @@ -1,4 +1,5 @@ class ScientificName < ActiveRecord::Base + after_commit { |sn| sn.crop.__elasticsearch__.index_document if sn.crop } belongs_to :crop belongs_to :creator, :class_name => 'Member' end diff --git a/app/views/crops/search.html.haml b/app/views/crops/search.html.haml index e10ae6f76..2ed095402 100644 --- a/app/views/crops/search.html.haml +++ b/app/views/crops/search.html.haml @@ -1,6 +1,6 @@ - content_for :title, "Crops matching #{@search}" -- unless (@exact_match || @partial_matches.length > 0) +- if @all_matches.empty? %h2 No results found %p Sorry, we couldn't find any crops that matched your search for "#{@search}". @@ -9,18 +9,10 @@ = link_to "browsing our crop database", crops_path instead. -- if @exact_match - %div#exact_match - %h2 Exact match - +- else + %div#all_matches .row - .col-md-2.six-across - = render :partial => "thumbnail", :locals => { :crop => @exact_match } - -- if ! @partial_matches.empty? - %div#partial_matches - %h2 Partial matches - .row - - @partial_matches.each do |c| + - @all_matches.each do |c| .col-md-2.six-across = render :partial => "thumbnail", :locals => { :crop => c } + diff --git a/app/views/harvests/index.csv.shaper b/app/views/harvests/index.csv.shaper index 4560dde80..b75b7a879 100644 --- a/app/views/harvests/index.csv.shaper +++ b/app/views/harvests/index.csv.shaper @@ -10,6 +10,7 @@ csv.headers :id, :unit, :weight_quantity, :weight_unit, + :si_weight, :date_harvested, :description, :date_added, @@ -31,7 +32,7 @@ csv.headers :id, csv.cell :plant_part_id, h.plant_part ? h.plant_part.id : '' csv.cell :plant_part_name, h.plant_part ? h.plant_part.to_s : '' - csv.cells :quantity, :unit, :weight_quantity, :weight_unit + csv.cells :quantity, :unit, :weight_quantity, :weight_unit, :si_weight csv.cell :date_harvested, h.created_at.to_s(:db) diff --git a/config/application.yml.example b/config/application.yml.example index f00978be0..03e8afe2c 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -62,6 +62,13 @@ GROWSTUFF_PAYPAL_USERNAME: "dummy" GROWSTUFF_PAYPAL_PASSWORD: "dummy" GROWSTUFF_PAYPAL_SIGNATURE: "dummy" +# Elasticsearch is used for flexible search and it requires another component +# to be installed. To make it easy for people who don't need to test this feature +# it's been turned off for test and development environment as a default. +# If you want to test this functionality, install elasticsearch and +# set this flag to "true". +GROWSTUFF_ELASTICSEARCH: "false" + ############################################################################## # Other environments # You can override the above for staging, production, etc. @@ -78,6 +85,8 @@ test: staging: GROWSTUFF_SITE_NAME: Growstuff (staging) + GROWSTUFF_ELASTICSEARCH: "true" production: GROWSTUFF_SITE_NAME: Growstuff + GROWSTUFF_ELASTICSEARCH: "true" diff --git a/db/migrate/20150129034206_add_si_weight_to_harvest.rb b/db/migrate/20150129034206_add_si_weight_to_harvest.rb new file mode 100644 index 000000000..b1e532d8d --- /dev/null +++ b/db/migrate/20150129034206_add_si_weight_to_harvest.rb @@ -0,0 +1,5 @@ +class AddSiWeightToHarvest < ActiveRecord::Migration + def change + add_column :harvests, :si_weight, :float + end +end diff --git a/db/schema.rb b/db/schema.rb index ea9fe790e..2707a681e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -20,16 +20,16 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.string "name", null: false t.boolean "is_paid" t.boolean "is_permanent_paid" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "accounts", force: true do |t| t.integer "member_id", null: false t.integer "account_type_id" t.datetime "paid_until" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "alternate_names", force: true do |t| @@ -46,8 +46,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.string "uid" t.string "token" t.string "secret" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "name" end @@ -57,15 +57,15 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.integer "post_id", null: false t.integer "author_id", null: false t.text "body", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "crops", force: true do |t| t.string "name", null: false t.string "en_wikipedia_url" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.integer "parent_id" t.integer "plantings_count", default: 0 @@ -99,8 +99,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.string "name", null: false t.text "description", null: false t.integer "owner_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -110,8 +110,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.string "name", null: false t.integer "owner_id" t.string "slug", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.text "description" t.boolean "active", default: true t.string "location" @@ -121,7 +121,7 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.string "area_unit" end - add_index "gardens", ["owner_id"], name: "index_gardens_on_user_id", using: :btree + add_index "gardens", ["owner_id"], name: "index_gardens_on_owner_id", using: :btree add_index "gardens", ["slug"], name: "index_gardens_on_slug", unique: true, using: :btree create_table "gardens_photos", id: false, force: true do |t| @@ -138,12 +138,13 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.decimal "quantity" t.string "unit" t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.decimal "weight_quantity" t.string "weight_unit" t.integer "plant_part_id" + t.float "si_weight" end create_table "harvests_photos", id: false, force: true do |t| @@ -171,8 +172,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.integer "failed_attempts", default: 0 t.string "unlock_token" t.datetime "locked_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "login_name" t.string "slug" t.boolean "tos_agreement" @@ -187,11 +188,11 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.boolean "send_planting_reminder", default: true end - add_index "members", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree - add_index "members", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "members", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - add_index "members", ["slug"], name: "index_users_on_slug", unique: true, using: :btree - add_index "members", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree + add_index "members", ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true, using: :btree + add_index "members", ["email"], name: "index_members_on_email", unique: true, using: :btree + add_index "members", ["reset_password_token"], name: "index_members_on_reset_password_token", unique: true, using: :btree + add_index "members", ["slug"], name: "index_members_on_slug", unique: true, using: :btree + add_index "members", ["unlock_token"], name: "index_members_on_unlock_token", unique: true, using: :btree create_table "members_roles", id: false, force: true do |t| t.integer "member_id" @@ -205,8 +206,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.text "body" t.boolean "read", default: false t.integer "post_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "order_items", force: true do |t| @@ -214,13 +215,13 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.integer "product_id" t.integer "price" t.integer "quantity" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" end create_table "orders", force: true do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.datetime "completed_at" t.integer "member_id" t.string "paypal_express_token" @@ -237,8 +238,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.integer "owner_id", null: false t.string "thumbnail_url", null: false t.string "fullsize_url", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "title", null: false t.string "license_name", null: false t.string "license_url" @@ -253,8 +254,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do create_table "plant_parts", force: true do |t| t.string "name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -264,8 +265,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.date "planted_at" t.integer "quantity" t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.string "sunniness" t.string "planted_from" @@ -280,21 +281,22 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.integer "author_id", null: false t.string "subject", null: false t.text "body", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" t.integer "forum_id" + t.integer "parent_id" end - add_index "posts", ["created_at", "author_id"], name: "index_updates_on_created_at_and_user_id", using: :btree - add_index "posts", ["slug"], name: "index_updates_on_slug", unique: true, using: :btree + add_index "posts", ["created_at", "author_id"], name: "index_posts_on_created_at_and_author_id", using: :btree + add_index "posts", ["slug"], name: "index_posts_on_slug", unique: true, using: :btree create_table "products", force: true do |t| t.string "name", null: false t.text "description", null: false t.integer "min_price", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.integer "account_type_id" t.integer "paid_months" t.integer "recommended_price" @@ -303,8 +305,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do create_table "roles", force: true do |t| t.string "name", null: false t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "slug" end @@ -313,8 +315,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do create_table "scientific_names", force: true do |t| t.string "scientific_name", null: false t.integer "crop_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.integer "creator_id" end @@ -324,8 +326,8 @@ ActiveRecord::Schema.define(version: 20150201064502) do t.text "description" t.integer "quantity" t.date "plant_before" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at" + t.datetime "updated_at" t.string "tradable_to", default: "nowhere" t.string "slug" end diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 656961fd3..bfb7b8642 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -314,6 +314,20 @@ namespace :growstuff do end end end + + desc "January 2015: fill in si_weight column" + task :populate_si_weight => :environment do + Harvest.find_each do |h| + h.set_si_weight + h.save + end + end + + desc "January 2015: build Elasticsearch index" + task :elasticsearch_create_index => :environment do + Crop.__elasticsearch__.create_index! force: true + Crop.import + end end # end oneoff section end diff --git a/script/deploy-tasks.sh b/script/deploy-tasks.sh index 3d427ed85..e8461294c 100755 --- a/script/deploy-tasks.sh +++ b/script/deploy-tasks.sh @@ -18,3 +18,9 @@ rake growstuff:import_crops file=db/seeds/crops-15-squashes.csv echo "2014-12-01 - load alternate names for crops" rake growstuff:oneoff:add_alternate_names + +echo "2015-01-28 - populate the harvest si_weight field" +rake growstuff:oneoff:populate_si_weight + +echo "2015-01-30 - build Elasticsearch index" +rake growstuff:oneoff:elasticsearch_create_index diff --git a/spec/features/crops/request_new_crop_spec.rb b/spec/features/crops/request_new_crop_spec.rb index 3ec8f502f..66e996d66 100644 --- a/spec/features/crops/request_new_crop_spec.rb +++ b/spec/features/crops/request_new_crop_spec.rb @@ -11,7 +11,7 @@ feature "Requesting a new crop" do scenario "Submit request" do visit new_crop_path fill_in "Name", with: "Couch potato" - fill_in "Comments", with: "Couch are real for real." + fill_in "Comments", with: "Couch potatoes are real for real." click_button "Save" expect(page).to have_content "Crop was successfully requested." end diff --git a/spec/features/harvests/harvesting_a_crop_spec.rb b/spec/features/harvests/harvesting_a_crop_spec.rb index 40c0eb8f8..c1148cf4b 100644 --- a/spec/features/harvests/harvesting_a_crop_spec.rb +++ b/spec/features/harvests/harvesting_a_crop_spec.rb @@ -7,12 +7,13 @@ feature "Harvesting a crop", :js => true do background do login_as(member) visit new_harvest_path + sync_elasticsearch([maize]) end it_behaves_like "crop suggest", "harvest", "crop" scenario "Creating a new harvest", :js => true do - fill_autocomplete "crop", :with => "m" + fill_autocomplete "crop", :with => "mai" select_from_autocomplete "maize" within "form#new_harvest" do fill_in "When?", :with => "2014-06-15" diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index 16b47973f..bda85fadc 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -9,12 +9,13 @@ feature "Planting a crop", :js => true do background do login_as(member) visit new_planting_path + sync_elasticsearch([maize]) end it_behaves_like "crop suggest", "planting" scenario "Creating a new planting" do - fill_autocomplete "crop", :with => "m" + fill_autocomplete "crop", :with => "mai" select_from_autocomplete "maize" within "form#new_planting" do fill_in "When", :with => "2014-06-15" @@ -41,7 +42,7 @@ feature "Planting a crop", :js => true do end scenario "Marking a planting as finished" do - fill_autocomplete "crop", :with => "m" + fill_autocomplete "crop", :with => "mai" select_from_autocomplete "maize" within "form#new_planting" do fill_in "When?", :with => "2014-07-01" @@ -77,7 +78,7 @@ feature "Planting a crop", :js => true do end scenario "Marking a planting as finished without a date" do - fill_autocomplete "crop", :with => "m" + fill_autocomplete "crop", :with => "mai" select_from_autocomplete "maize" within "form#new_planting" do check "Mark as finished" diff --git a/spec/features/seeds/adding_seeds_spec.rb b/spec/features/seeds/adding_seeds_spec.rb index b58b475da..8cdd9c847 100644 --- a/spec/features/seeds/adding_seeds_spec.rb +++ b/spec/features/seeds/adding_seeds_spec.rb @@ -7,12 +7,13 @@ feature "Seeds", :js => true do background do login_as(member) visit new_seed_path + sync_elasticsearch([maize]) end it_behaves_like "crop suggest", "seed", "crop" scenario "Adding a new seed", :js => true do - fill_autocomplete "crop", :with => "m" + fill_autocomplete "crop", :with => "mai" select_from_autocomplete "maize" within "form#new_seed" do fill_in "Quantity:", :with => 42 diff --git a/spec/features/shared_examples/crop_suggest.rb b/spec/features/shared_examples/crop_suggest.rb index 141dfbabf..9856282ca 100644 --- a/spec/features/shared_examples/crop_suggest.rb +++ b/spec/features/shared_examples/crop_suggest.rb @@ -1,29 +1,39 @@ require 'rails_helper' shared_examples "crop suggest" do |resource| - let!(:popcorn) { FactoryGirl.create(:popcorn) } + let!(:pea) { FactoryGirl.create(:crop, :name => 'pea') } let!(:pear) { FactoryGirl.create(:pear) } let!(:tomato) { FactoryGirl.create(:tomato) } let!(:roma) { FactoryGirl.create(:roma) } + background do + sync_elasticsearch([pea, pear, maize, tomato]) + end + scenario "See text in crop auto suggest field" do expect(page).to have_selector("input[placeholder='e.g. lettuce']") end scenario "Typing in the crop name displays suggestions" do within "form#new_#{resource}" do - fill_autocomplete "crop", :with => "p" + fill_autocomplete "crop", :with => "pe" + end + + expect(page).to_not have_content("pear") + expect(page).to_not have_content("pea") + + within "form#new_#{resource}" do + fill_autocomplete "crop", :with => "pea" end expect(page).to have_content("pear") - expect(page).to have_content("popcorn") + expect(page).to have_content("pea") within "form#new_#{resource}" do fill_autocomplete "crop", :with => "pear" end expect(page).to have_content("pear") - expect(page).to_not have_content("popcorn") select_from_autocomplete("pear") @@ -32,16 +42,16 @@ shared_examples "crop suggest" do |resource| scenario "Typing and pausing does not affect input" do within "form#new_#{resource}" do - fill_autocomplete "crop", :with => "p" + fill_autocomplete "crop", :with => "pea" end expect(page).to have_content("pear") - expect(find_field("crop").value).to eq("p") + expect(find_field("crop").value).to eq("pea") end scenario "Searching for a crop casts a wide net on results" do within "form#new_#{resource}" do - fill_autocomplete "crop", :with => "to" + fill_autocomplete "crop", :with => "tom" end expect(page).to have_content("tomato") diff --git a/spec/mailers/notifier_spec.rb b/spec/mailers/notifier_spec.rb index 667ded7a2..4ad7bd53c 100644 --- a/spec/mailers/notifier_spec.rb +++ b/spec/mailers/notifier_spec.rb @@ -90,9 +90,9 @@ describe Notifier do end it 'includes links to plant, harvest and stash seeds for the new crop' do - expect(mail.body.encoded).to match new_planting_url - expect(mail.body.encoded).to match new_harvest_url - expect(mail.body.encoded).to match new_seed_url + expect(mail.body.encoded).to match "#{new_planting_url}\\?crop_id=#{crop.id}" + expect(mail.body.encoded).to match "#{new_harvest_url}\\?crop_id=#{crop.id}" + expect(mail.body.encoded).to match "#{new_seed_url}\\?crop_id=#{crop.id}" end end diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index 1850db459..9cda818e8 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -328,6 +328,7 @@ describe Crop do context "search" do before :each do @mushroom = FactoryGirl.create(:crop, :name => 'mushroom') + sync_elasticsearch([@mushroom]) end it "finds exact matches" do Crop.search('mushroom').should eq [@mushroom] diff --git a/spec/models/harvest_spec.rb b/spec/models/harvest_spec.rb index 504c8c414..cd6d5f1ef 100644 --- a/spec/models/harvest_spec.rb +++ b/spec/models/harvest_spec.rb @@ -118,6 +118,26 @@ describe Harvest do end end + context "standardized weights" do + it 'converts from pounds' do + @harvest = FactoryGirl.create(:harvest, :weight_quantity => 2, :weight_unit => "lb") + @harvest.should be_valid + @harvest.reload.si_weight.should eq 0.907 + end + + it 'converts from ounces' do + @harvest = FactoryGirl.create(:harvest, :weight_quantity => 16, :weight_unit => "oz") + @harvest.should be_valid + @harvest.reload.si_weight.should eq 0.454 + end + + it 'leaves kg alone' do + @harvest = FactoryGirl.create(:harvest, :weight_quantity => 2, :weight_unit => "kg") + @harvest.should be_valid + @harvest.reload.si_weight.should eq 2.0 + end + end + context 'ordering' do it 'lists most recent harvests first' do @h1 = FactoryGirl.create(:harvest, :created_at => 1.day.ago) diff --git a/spec/support/elasticsearch_helpers.rb b/spec/support/elasticsearch_helpers.rb new file mode 100644 index 000000000..0d89eb33b --- /dev/null +++ b/spec/support/elasticsearch_helpers.rb @@ -0,0 +1,18 @@ +module ElasticsearchHelpers + def sync_elasticsearch(crops) + if ENV['GROWSTUFF_ELASTICSEARCH'] == "true" + crops.each {|crop| crop.__elasticsearch__.index_document} + Crop.__elasticsearch__.refresh_index! + end + end +end + +RSpec.configure do |config| + config.include ElasticsearchHelpers + + config.before(:each) do + if ENV['GROWSTUFF_ELASTICSEARCH'] == "true" + Crop.__elasticsearch__.create_index! force: true + end + end +end diff --git a/spec/views/crops/search.html.haml_spec.rb b/spec/views/crops/search.html.haml_spec.rb index 82b6ea87c..8458f254e 100644 --- a/spec/views/crops/search.html.haml_spec.rb +++ b/spec/views/crops/search.html.haml_spec.rb @@ -11,19 +11,18 @@ describe "crops/search" do @tomato = FactoryGirl.create(:tomato) @roma = FactoryGirl.create(:crop, :name => 'Roma tomato', :parent => @tomato) assign(:search, 'tomato') - assign(:exact_match, @tomato) - assign(:partial_matches, [@roma]) + assign(:all_matches, [@tomato, @roma]) render end it "shows exact matches" do - assert_select "div#exact_match" do + assert_select "div#all_matches" do assert_select "a[href=#{crop_path(@tomato)}]" end end it "shows partial matches" do - assert_select "div#partial_matches" do + assert_select "div#all_matches" do assert_select "a[href=#{crop_path(@roma)}]" end end @@ -31,8 +30,7 @@ describe "crops/search" do context "no results" do before :each do - assign(:exact_match, nil) - assign(:partial_matches, []) + assign(:all_matches, []) assign(:search, 'tomato') render end