solidus_searchkick 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +5 -0
  3. data/.gitignore +14 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +9 -0
  6. data/Gemfile.lock +404 -0
  7. data/LICENSE +26 -0
  8. data/README.md +78 -0
  9. data/Rakefile +21 -0
  10. data/app/assets/javascripts/spree/backend/spree_searchkick.js +2 -0
  11. data/app/assets/javascripts/spree/frontend/spree_searchkick.js +28 -0
  12. data/app/assets/javascripts/spree/frontend/typeahead.bundle.js +2451 -0
  13. data/app/assets/stylesheets/spree/backend/spree_searchkick.css +4 -0
  14. data/app/assets/stylesheets/spree/frontend/spree_searchkick.css +47 -0
  15. data/app/controllers/spree/products_controller_decorator.rb +7 -0
  16. data/app/helpers/spree/products_helper_decorator.rb +9 -0
  17. data/app/models/spree/order_decorator.rb +8 -0
  18. data/app/models/spree/product_decorator.rb +57 -0
  19. data/app/models/spree/property_decorator.rb +7 -0
  20. data/app/models/spree/taxonomy_decorator.rb +7 -0
  21. data/app/overrides/spree/admin/properties/_form/add_filterable_to_property_form.html.erb.deface +8 -0
  22. data/app/overrides/spree/admin/taxonomies/_form/add_filterable_to_taxonomy_form.html.erb.deface +6 -0
  23. data/app/views/spree/shared/_es_filter.html.erb +15 -0
  24. data/app/views/spree/shared/_filters.html.erb +12 -0
  25. data/bin/rails +7 -0
  26. data/config/locales/en.yml +5 -0
  27. data/config/routes.rb +6 -0
  28. data/db/migrate/20150819222417_add_filtrable_to_spree_taxonomies.rb +5 -0
  29. data/db/migrate/20151009155442_add_filterable_to_spree_property.rb +5 -0
  30. data/lib/generators/spree_searchkick/install/install_generator.rb +31 -0
  31. data/lib/solidus_searchkick.rb +2 -0
  32. data/lib/solidus_searchkick/engine.rb +23 -0
  33. data/lib/solidus_searchkick/factories.rb +6 -0
  34. data/lib/solidus_searchkick/version.rb +3 -0
  35. data/lib/spree/core/searchkick_filters.rb +50 -0
  36. data/lib/spree/search/searchkick.rb +58 -0
  37. data/solidus_searchkick.gemspec +38 -0
  38. data/spec/controllers/spree/products_controller_spec.rb +5 -0
  39. data/spec/lib/spree/search/searchkick_spec.rb +43 -0
  40. data/spec/models/spree/order_spec.rb +20 -0
  41. data/spec/models/spree/product_spec.rb +37 -0
  42. data/spec/models/spree/property_spec.rb +11 -0
  43. data/spec/models/spree/taxonomy_spec.rb +11 -0
  44. data/spec/routing/spree/products_routes_spec.rb +20 -0
  45. data/spec/spec_helper.rb +92 -0
  46. metadata +313 -0
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
@@ -0,0 +1,47 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
5
+ .tt-query {
6
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
7
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
8
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
9
+ }
10
+
11
+ .tt-hint {
12
+ color: #999
13
+ }
14
+
15
+ .tt-menu {
16
+ width: 100%;
17
+ margin-top: 4px;
18
+ padding: 4px 0;
19
+ background-color: #fff;
20
+ border: 1px solid #ccc;
21
+ border: 1px solid rgba(0, 0, 0, 0.2);
22
+ -webkit-border-radius: 4px;
23
+ -moz-border-radius: 4px;
24
+ border-radius: 4px;
25
+ -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
26
+ -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
27
+ box-shadow: 0 5px 10px rgba(0,0,0,.2);
28
+ }
29
+
30
+ .tt-suggestion {
31
+ padding: 3px 20px;
32
+ line-height: 24px;
33
+ }
34
+
35
+ .tt-suggestion.tt-cursor {
36
+ color: #fff;
37
+ background-color: #0097cf;
38
+
39
+ }
40
+
41
+ .tt-suggestion p {
42
+ margin: 0;
43
+ }
44
+
45
+ .twitter-typeahead {
46
+ width: 100%;
47
+ }
@@ -0,0 +1,7 @@
1
+ Spree::ProductsController.class_eval do
2
+ def autocomplete
3
+ keywords = params[:keywords] ||= nil
4
+ json = Spree::Product.autocomplete(keywords)
5
+ render json: json
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ Spree::ProductsHelper.module_eval do
2
+ # TODO, will need to handle this
3
+ def cache_key_for_products
4
+ count = @products.count
5
+ hash = Digest::SHA1.hexdigest(params.to_json)
6
+ max_updated_at = (@products.maximum(:updated_at) || Date.today).to_s(:number)
7
+ "#{I18n.locale}/#{current_pricing_options.cache_key}/spree/products/all-#{params[:page]}-#{hash}-#{max_updated_at}-#{count}"
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ Spree::Order.class_eval do
2
+ Spree::Order.state_machine.after_transition to: :complete, do: :reindex_order_products
3
+
4
+ def reindex_order_products
5
+ return unless complete?
6
+ products.map(&:reindex)
7
+ end
8
+ end
@@ -0,0 +1,57 @@
1
+ Spree::Product.class_eval do
2
+ searchkick autocomplete: [:name]
3
+
4
+ def search_data
5
+ json = {
6
+ name: name,
7
+ description: description,
8
+ active: available?,
9
+ price: price,
10
+ currency: cost_currency,
11
+ sku: sku,
12
+ conversions: orders.complete.count,
13
+ taxon_ids: taxon_and_ancestors.map(&:id),
14
+ taxon_names: taxon_and_ancestors.map(&:name)
15
+ }
16
+
17
+ Spree::Property.all.each do |prop|
18
+ json.merge!(Hash[prop.name.downcase, property(prop.name)])
19
+ end
20
+
21
+ Spree::Taxonomy.all.each do |taxonomy|
22
+ json.merge!(Hash["#{ taxonomy.name.downcase }_ids", taxon_by_taxonomy(taxonomy.id).map(&:id)])
23
+ end
24
+
25
+ json
26
+ end
27
+
28
+ def taxon_by_taxonomy(taxonomy_id)
29
+ taxons.joins(:taxonomy).where(spree_taxonomies: { id: taxonomy_id })
30
+ end
31
+
32
+ def taxon_and_ancestors
33
+ taxons.map(&:self_and_ancestors).flatten.uniq
34
+ end
35
+
36
+ def self.autocomplete(keywords)
37
+ if keywords
38
+ Spree::Product.search(
39
+ keywords,
40
+ autocomplete: true,
41
+ limit: 10, where: search_where
42
+ ).map(&:name).map(&:strip).uniq
43
+ else
44
+ Spree::Product.search(
45
+ '*',
46
+ where: search_where
47
+ ).map(&:name).map(&:strip)
48
+ end
49
+ end
50
+
51
+ def self.search_where
52
+ {
53
+ active: true,
54
+ price: { not: nil }
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,7 @@
1
+ Spree::Property.class_eval do
2
+ scope :filterable, -> { where(filterable: true) }
3
+
4
+ def filter_name
5
+ "#{name.downcase}"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ Spree::Taxonomy.class_eval do
2
+ scope :filterable, -> { where(filterable: true) }
3
+
4
+ def filter_name
5
+ "#{name.downcase}_ids"
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ <!-- insert_bottom 'div[data-hook=admin_property_form]' -->
2
+ <div class="col-md-6">
3
+ <%= f.field_container :filterable, class: ['form-group'] do %>
4
+ <%= f.label :filterable, Spree.t(:filterable) %> <span class="required">*</span>
5
+ <%= error_message_on :property, :filterable, :class => 'error-message' %>
6
+ <%= check_box :property, :filterable, :class => 'form-control' %>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- insert_bottom 'div.form-group' -->
2
+ <%= f.field_container :filterable, class: ['form-group'] do %>
3
+ <%= f.label :filterable, Spree.t(:filterable) %> <span class="required">*</span>
4
+ <%= error_message_on :taxonomy, :filterable, :class => 'error-message' %>
5
+ <%= check_box :taxonomy, :filterable, :class => 'form-control' %>
6
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <div class="navigation" data-hook="navigation">
2
+ <h4 class="filter-title"><%= Spree.t("esfilters.#{filter[:name]}") %></h4>
3
+ <ul class="list-group">
4
+ <% filter[:options].each do |f| %>
5
+ <li class="list-group-item">
6
+ <input type="checkbox"
7
+ id="<%= f[:label] %>"
8
+ name="search[<%= filter[:name] %>][]"
9
+ value="<%= f[:value] %>"
10
+ />
11
+ <label class="nowrap" for="<%= f[:label] %>"> <%= f[:label] %> </label>
12
+ </li>
13
+ <% end %>
14
+ </ul>
15
+ </div>
@@ -0,0 +1,12 @@
1
+ <%
2
+ aggregations = @products.aggs
3
+ filters = Spree::Core::SearchkickFilters.applicable_filters(aggregations)
4
+ %>
5
+ <%= form_tag '', :method => :get, :id => 'sidebar_products_search' do %>
6
+ <%= hidden_field_tag 'per_page', params[:per_page] %>
7
+ <% filters.each do |filter| %>
8
+ <% next if filter[:options].empty? %>
9
+ <%= render partial: "spree/shared/es_filter", locals: { filter: filter } %>
10
+ <% end %>
11
+ <%= submit_tag Spree.t(:search), :name => nil, :class => 'btn btn-primary' %>
12
+ <% end %>
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/solidus_searchkick/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,5 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ hello: "Hello world"
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ # Add your extension routes here
3
+ get '/best', to: 'products#best_selling'
4
+ get '/best/t/*id/', to: 'products#best_selling', as: :best_selling_taxon
5
+ get '/autocomplete/products', to: 'products#autocomplete', as: :autocomplete
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddFiltrableToSpreeTaxonomies < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_taxonomies, :filterable, :boolean
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddFilterableToSpreeProperty < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_properties, :filterable, :boolean
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ module SolidusSearchkick
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ class_option :auto_run_migrations, :type => :boolean, :default => false
6
+
7
+ def add_javascripts
8
+ append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_searchkick\n"
9
+ append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_searchkick\n"
10
+ end
11
+
12
+ def add_stylesheets
13
+ inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/solidus_searchkick\n", :before => /\*\//, :verbose => true
14
+ inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/solidus_searchkick\n", :before => /\*\//, :verbose => true
15
+ end
16
+
17
+ def add_migrations
18
+ run 'bundle exec rake railties:install:migrations FROM=solidus_searchkick'
19
+ end
20
+
21
+ def run_migrations
22
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
23
+ if run_migrations
24
+ run 'bundle exec rake db:migrate'
25
+ else
26
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ require 'spree_core'
2
+ require 'solidus_searchkick/engine'
@@ -0,0 +1,23 @@
1
+ module SolidusSearchkick
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'solidus_searchkick'
6
+
7
+ config.autoload_paths += %W(#{config.root}/lib)
8
+
9
+ # use rspec for tests
10
+ config.generators do |g|
11
+ g.test_framework :rspec
12
+ end
13
+
14
+ def self.activate
15
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
16
+ Rails.configuration.cache_classes ? require(c) : load(c)
17
+ end
18
+ Spree::Config.searcher_class = Spree::Search::Searchkick
19
+ end
20
+
21
+ config.to_prepare &method(:activate).to_proc
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ # Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them.
3
+ #
4
+ # Example adding this to your spec_helper will load these Factories for use:
5
+ # require 'solidus_searchkick/factories'
6
+ end
@@ -0,0 +1,3 @@
1
+ module SolidusSearchkick
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,50 @@
1
+ module Spree
2
+ module Core
3
+ module SearchkickFilters
4
+ def self.applicable_filters(aggregations)
5
+ es_filters = []
6
+ Spree::Taxonomy.filterable.each do |taxonomy|
7
+ es_filters << self.process_filter(taxonomy.filter_name, :taxon, aggregations[taxonomy.filter_name])
8
+ end
9
+
10
+ Spree::Property.filterable.each do |property|
11
+ es_filters << self.process_filter(property.filter_name, :property, aggregations[property.filter_name])
12
+ end
13
+
14
+ es_filters.uniq
15
+ end
16
+
17
+ def self.process_filter(name, type, filter)
18
+ options = []
19
+ case type
20
+ when :price
21
+ min = filter["buckets"].min {|a,b| a["key"] <=> b["key"] }
22
+ max = filter["buckets"].max {|a,b| a["key"] <=> b["key"] }
23
+ if min && max
24
+ options = {min: min["key"].to_i, max: max["key"].to_i, step: 100}
25
+ else
26
+ options = {}
27
+ end
28
+ when :taxon
29
+ ids = filter["buckets"].map{|h| h["key"]}
30
+ taxons = Spree::Taxon.where(id: ids).order(name: :asc)
31
+ taxons.each {|t| options << {label: t.name, value: t.id }}
32
+ when :property
33
+ values = filter["buckets"].map{|h| h["key"]}
34
+ values.each {|t| options << {label: t, value: t }}
35
+ end
36
+
37
+ {
38
+ name: name,
39
+ type: type,
40
+ options: options
41
+ }
42
+
43
+ end
44
+
45
+ def self.aggregation_term(aggregation)
46
+ aggregation["buckets"].sort_by { |hsh| hsh["key"] }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ module Spree::Search
2
+ class Searchkick < Spree::Core::Search::Base
3
+ def retrieve_products
4
+ @products = get_base_elasticsearch
5
+ end
6
+
7
+ protected
8
+
9
+ def get_base_elasticsearch
10
+ curr_page = page || 1
11
+ Spree::Product.search(keyword_query, where: where_query, aggs: aggregations, smart_aggs: true, order: sort, page: curr_page, per_page: per_page)
12
+ end
13
+
14
+ def where_query
15
+ where_query = {
16
+ active: true,
17
+ currency: pricing_options.currency,
18
+ price: { not: nil }
19
+ }
20
+ where_query.merge!({taxon_ids: taxon.id}) if taxon
21
+ add_search_filters(where_query)
22
+ end
23
+
24
+ def keyword_query
25
+ (keywords.nil? || keywords.empty?) ? "*" : keywords
26
+ end
27
+
28
+ def sort
29
+ order ? order : nil
30
+ end
31
+
32
+ def aggregations
33
+ fs = []
34
+ Spree::Taxonomy.filterable.each do |taxonomy|
35
+ fs << taxonomy.filter_name.to_sym
36
+ end
37
+ Spree::Property.filterable.each do |property|
38
+ fs << property.filter_name.to_sym
39
+ end
40
+ fs
41
+ end
42
+
43
+ def add_search_filters(query)
44
+ return query unless search
45
+ search.each do |name, scope_attribute|
46
+ query.merge!(Hash[name, scope_attribute])
47
+ end
48
+
49
+ query
50
+ end
51
+
52
+ def prepare(params)
53
+ @properties[:order] = params[:order].blank? ? nil : params[:order]
54
+ params = params.deep_symbolize_keys
55
+ super
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path("../lib/solidus_searchkick/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.platform = Gem::Platform::RUBY
6
+ s.name = 'solidus_searchkick'
7
+ s.version = SolidusSearchkick::VERSION
8
+ s.licenses = ['MIT']
9
+ s.summary = 'Add searchkick to Solidus'
10
+ s.description = 'Filters, suggests, autocompletes, sortings, searches'
11
+ s.required_ruby_version = '>= 2.0.0'
12
+
13
+ s.author = ['Jim Smith']
14
+ s.email = ['jim@jimsmithdesign.com']
15
+ s.homepage = 'https://github.com/elevatorup/solidus_searchkick'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.require_path = 'lib'
20
+ s.requirements << 'none'
21
+
22
+ s.add_runtime_dependency 'solidus', '~> 1.4', '>= 1.4.0'
23
+ s.add_runtime_dependency 'searchkick', '~> 1.2'
24
+
25
+ s.add_development_dependency 'capybara', '~> 2.4'
26
+ s.add_development_dependency 'coffee-rails'
27
+ s.add_development_dependency 'database_cleaner'
28
+ s.add_development_dependency 'factory_girl', '~> 4.5'
29
+ s.add_development_dependency 'ffaker'
30
+ s.add_development_dependency 'rspec-rails', '~> 3.1'
31
+ s.add_development_dependency 'sass-rails', '~> 5.0'
32
+ s.add_development_dependency 'selenium-webdriver'
33
+ s.add_development_dependency 'simplecov'
34
+ s.add_development_dependency 'sqlite3'
35
+ s.add_development_dependency 'pry'
36
+ s.add_development_dependency 'better_errors'
37
+ s.add_development_dependency 'binding_of_caller'
38
+ end