solidus_searchkick 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +5 -0
- data/.gitignore +14 -0
- data/.travis.yml +18 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +404 -0
- data/LICENSE +26 -0
- data/README.md +78 -0
- data/Rakefile +21 -0
- data/app/assets/javascripts/spree/backend/spree_searchkick.js +2 -0
- data/app/assets/javascripts/spree/frontend/spree_searchkick.js +28 -0
- data/app/assets/javascripts/spree/frontend/typeahead.bundle.js +2451 -0
- data/app/assets/stylesheets/spree/backend/spree_searchkick.css +4 -0
- data/app/assets/stylesheets/spree/frontend/spree_searchkick.css +47 -0
- data/app/controllers/spree/products_controller_decorator.rb +7 -0
- data/app/helpers/spree/products_helper_decorator.rb +9 -0
- data/app/models/spree/order_decorator.rb +8 -0
- data/app/models/spree/product_decorator.rb +57 -0
- data/app/models/spree/property_decorator.rb +7 -0
- data/app/models/spree/taxonomy_decorator.rb +7 -0
- data/app/overrides/spree/admin/properties/_form/add_filterable_to_property_form.html.erb.deface +8 -0
- data/app/overrides/spree/admin/taxonomies/_form/add_filterable_to_taxonomy_form.html.erb.deface +6 -0
- data/app/views/spree/shared/_es_filter.html.erb +15 -0
- data/app/views/spree/shared/_filters.html.erb +12 -0
- data/bin/rails +7 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20150819222417_add_filtrable_to_spree_taxonomies.rb +5 -0
- data/db/migrate/20151009155442_add_filterable_to_spree_property.rb +5 -0
- data/lib/generators/spree_searchkick/install/install_generator.rb +31 -0
- data/lib/solidus_searchkick.rb +2 -0
- data/lib/solidus_searchkick/engine.rb +23 -0
- data/lib/solidus_searchkick/factories.rb +6 -0
- data/lib/solidus_searchkick/version.rb +3 -0
- data/lib/spree/core/searchkick_filters.rb +50 -0
- data/lib/spree/search/searchkick.rb +58 -0
- data/solidus_searchkick.gemspec +38 -0
- data/spec/controllers/spree/products_controller_spec.rb +5 -0
- data/spec/lib/spree/search/searchkick_spec.rb +43 -0
- data/spec/models/spree/order_spec.rb +20 -0
- data/spec/models/spree/product_spec.rb +37 -0
- data/spec/models/spree/property_spec.rb +11 -0
- data/spec/models/spree/taxonomy_spec.rb +11 -0
- data/spec/routing/spree/products_routes_spec.rb +20 -0
- data/spec/spec_helper.rb +92 -0
- metadata +313 -0
@@ -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,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,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
|
data/app/overrides/spree/admin/properties/_form/add_filterable_to_property_form.html.erb.deface
ADDED
@@ -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>
|
data/app/overrides/spree/admin/taxonomies/_form/add_filterable_to_taxonomy_form.html.erb.deface
ADDED
@@ -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'
|
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,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,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,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
|