solidus_searchkick 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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