diff --git a/app/controllers/public/search_controller.rb b/app/controllers/public/search_controller.rb index 689bf65..84774f8 100644 --- a/app/controllers/public/search_controller.rb +++ b/app/controllers/public/search_controller.rb @@ -8,6 +8,7 @@ class SearchController < PublicController before_filter :load_category before_filter :load_search_assets before_filter :load_query + before_filter :load_search_engine # Backwards compatibility with old URLs def redirect_asset_param @@ -18,7 +19,7 @@ class SearchController < PublicController no_design_blocks def facets_browse - @asset = params[:asset] + @asset = params[:asset].to_sym @asset_class = asset_class(@asset) @facets_only = true @@ -31,11 +32,12 @@ class SearchController < PublicController end def articles - if !@empty_query - full_text_search ['public:true'] + if @search_engine && !@empty_query + full_text_search else @results[@asset] = @environment.articles.public.send(@filter).paginate(paginate_options) end + render :template => 'search/search_page' end def contents @@ -43,49 +45,51 @@ class SearchController < PublicController end def people - if !@empty_query - full_text_search ['public:true'] + if @search_engine && !@empty_query + full_text_search else @results[@asset] = visible_profiles(Person).send(@filter).paginate(paginate_options) end + render :template => 'search/search_page' end def products - public_filters = ['public:true', 'enabled:true'] - if !@empty_query - full_text_search public_filters + if @search_engine && !@empty_query + full_text_search else - @one_page = true @geosearch = logged_in? && current_user.person.lat && current_user.person.lng extra_limit = LIST_SEARCH_LIMIT*5 sql_options = {:limit => LIST_SEARCH_LIMIT, :order => 'random()'} if @geosearch - full_text_search public_filters, :sql_options => sql_options, :extra_limit => extra_limit, + full_text_search :sql_options => sql_options, :extra_limit => extra_limit, :alternate_query => "{!boost b=recip(geodist(),#{"%e" % (1.to_f/DistBoost)},1,1)}", :radius => DistFilt, :latitude => current_user.person.lat, :longitude => current_user.person.lng else - full_text_search public_filters, :sql_options => sql_options, :extra_limit => extra_limit, + full_text_search :sql_options => sql_options, :extra_limit => extra_limit, :boost_functions => ['recip(ms(NOW/HOUR,updated_at),1.3e-10,1,1)'] end end + render :template => 'search/search_page' end def enterprises - if !@empty_query - full_text_search ['public:true'] + if @search_engine && !@empty_query + full_text_search else @filter_title = _('Enterprises from network') @results[@asset] = visible_profiles(Enterprise, [{:products => :product_category}]).paginate(paginate_options) end + render :template => 'search/search_page' end def communities - if !@empty_query - full_text_search ['public:true'] + if @search_engine && !@empty_query + full_text_search else @results[@asset] = visible_profiles(Community).send(@filter).paginate(paginate_options) end + render :template => 'search/search_page' end def events @@ -104,7 +108,7 @@ class SearchController < PublicController environment.events.by_day(@selected_day) end - if !@empty_query + if @search_engine && !@empty_query full_text_search else @results[@asset] = date_range ? environment.events.by_range(date_range) : environment.events @@ -189,6 +193,7 @@ class SearchController < PublicController def load_query @asset = params[:action].to_sym @order ||= [@asset] + params[:display] ||= 'list' @results ||= {} @filter = filter @filter_title = filter_description(@asset, @filter) @@ -211,6 +216,10 @@ class SearchController < PublicController end end + def load_search_engine + @search_engine = @plugins.first_plugin(:search_engine?) + end + FILTERS = %w( more_recent more_active @@ -260,9 +269,7 @@ class SearchController < PublicController if map_search? MAP_SEARCH_LIMIT elsif !multiple_search? - if [:people, :communities].include? @asset - BLOCKS_SEARCH_LIMIT - elsif @asset == :enterprises and @empty_query + if [:people, :communities, :enterprises].include? @asset BLOCKS_SEARCH_LIMIT else LIST_SEARCH_LIMIT @@ -273,41 +280,12 @@ class SearchController < PublicController end def paginate_options(page = params[:page]) - page = 1 if multiple_search? or params[:display] == 'map' + page = 1 if multiple_search? or @display == 'map' { :per_page => limit, :page => page } end - def full_text_search(filters = [], options = {}) - paginate_options = paginate_options(params[:page]) - asset_class = asset_class(@asset) - solr_options = options - pg_options = paginate_options(params[:page]) - - if !multiple_search? - if !@results_only and asset_class.respond_to? :facets - solr_options.merge! asset_class.facets_find_options(params[:facet]) - solr_options[:all_facets] = true - end - solr_options[:filter_queries] ||= [] - solr_options[:filter_queries] += filters - solr_options[:filter_queries] << "environment_id:#{environment.id}" - solr_options[:filter_queries] << asset_class.facet_category_query.call(@category) if @category - - solr_options[:boost_functions] ||= [] - params[:order_by] = nil if params[:order_by] == 'none' - if params[:order_by] - order = SortOptions[@asset][params[:order_by].to_sym] - raise "Unknown order by" if order.nil? - order[:solr_opts].each do |opt, value| - solr_options[opt] = value.is_a?(Proc) ? instance_eval(&value) : value - end - end - end - - ret = asset_class.find_by_contents(@query, paginate_options, solr_options) - @results[@asset] = ret[:results] - @facets = ret[:facets] - @all_facets = ret[:all_facets] + def full_text_search(options = {}) + @results[@asset] = @plugins.first(:full_text_search, @asset, @query, @category, paginate_options(params[:page])) end private diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a0c5996..aac7d45 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -2,7 +2,7 @@ module SearchHelper MAP_SEARCH_LIMIT = 2000 LIST_SEARCH_LIMIT = 20 - BLOCKS_SEARCH_LIMIT = 18 + BLOCKS_SEARCH_LIMIT = 24 MULTIPLE_SEARCH_LIMIT = 8 DistFilt = 200 DistBoost = 50 @@ -50,7 +50,7 @@ module SearchHelper end def map_search? - !@empty_query and !multiple_search? and params[:display] == 'map' + !multiple_search? and params[:display] == 'map' end def search_page_title(title, category = nil) @@ -66,8 +66,12 @@ module SearchHelper :align => 'center', :class => 'search-category-context') if category end - def display_results(map_capable = false) - if map_capable and map_search? + def map_capable?(asset) + [:enterprises, :products].include?(asset) + end + + def display_results(asset) + if map_capable?(asset) and map_search? partial = 'google_maps' klass = 'map' else @@ -78,12 +82,6 @@ module SearchHelper content_tag('div', render(:partial => partial), :class => "map-or-list-search-results #{klass}") end - def display_map_list_button - button(:search, params[:display] == 'map' ? _('Display in list') : _('Display in map'), - params.merge(:display => (params[:display] == 'map' ? 'list' : 'map')), - :class => "map-toggle-button" ) - end - def city_with_state(city) if city and city.kind_of?(City) s = city.parent @@ -97,17 +95,6 @@ module SearchHelper end end - def facets_menu(asset, _facets) - @asset_class = asset_class(asset) - @facets = _facets - render(:partial => 'facets_menu') - end - - def facets_unselect_menu(asset) - @asset_class = asset_class(asset) - render(:partial => 'facets_unselect_menu') - end - def facet_javascript(input_id, facet, array) array = [] if array.nil? hintText = _('Type in an option') @@ -117,94 +104,6 @@ module SearchHelper #{jquery_token_input_messages_json(hintText)}});") end - def facet_link_html(facet, params, value, label, count) - params = params ? params.dup : {} - has_extra = label.kind_of?(Array) - link_label = has_extra ? label[0] : label - id = facet[:solr_field].to_s - params[:facet] ||= {} - params[:facet][id] ||= {} - params[:page] = {} if params[:page] - - selected = facet[:label_id].nil? ? params[:facet][id] == value : params[:facet][id][facet[:label_id]].to_a.include?(value) - - if count > 0 - url = params.merge(:facet => params[:facet].merge( - id => facet[:label_id].nil? ? value : params[:facet][id].merge( facet[:label_id] => params[:facet][id][facet[:label_id]].to_a | [value] ) - )) - else - # preserve others filters and change this filter - url = params.merge(:facet => params[:facet].merge( - id => facet[:label_id].nil? ? value : { facet[:label_id] => value } - )) - end - - content_tag 'div', link_to(link_label, url, :class => 'facet-result-link-label') + - content_tag('span', (has_extra ? label[1] : ''), :class => 'facet-result-extra-label') + - (count > 0 ? content_tag('span', " (#{count})", :class => 'facet-result-count') : ''), - :class => 'facet-menu-item' + (selected ? ' facet-result-link-selected' : '') - end - - def facet_selecteds_html_for(environment, klass, params) - def name_with_extra(klass, facet, value) - name = klass.facet_result_name(facet, value) - name = name[0] + name[1] if name.kind_of?(Array) - name - end - - ret = [] - params = params.dup - params[:facet].each do |id, value| - facet = klass.facet_by_id(id.to_sym) - next unless facet - if value.kind_of?(Hash) - label_hash = facet[:label].call(environment) - value.each do |label_id, value| - facet[:label_id] = label_id - facet[:label] = label_hash[label_id] - value.to_a.each do |value| - ret << [facet[:label], name_with_extra(klass, facet, value), - params.merge(:facet => params[:facet].merge(id => params[:facet][id].merge(label_id => params[:facet][id][label_id].to_a.reject{ |v| v == value })))] - end - end - else - ret << [klass.facet_label(facet), name_with_extra(klass, facet, value), - params.merge(:facet => params[:facet].reject{ |k,v| k == id })] - end - end - - ret.map do |label, name, url| - content_tag('div', content_tag('span', label, :class => 'facet-selected-label') + - content_tag('span', name, :class => 'facet-selected-name') + - link_to('', url, :class => 'facet-selected-remove', :title => 'remove facet'), :class => 'facet-selected') - end.join - end - - def order_by(asset) - options = SortOptions[asset].map do |name, options| - next if options[:if] and ! instance_eval(&options[:if]) - [_(options[:label]), name.to_s] - end.compact - - content_tag('div', _('Sort results by ') + - select_tag(asset.to_s + '[order]', options_for_select(options, params[:order_by] || 'none'), - {:onchange => "window.location = jQuery.param.querystring(window.location.href, { 'order_by' : this.options[this.selectedIndex].value})"}), - :class => "search-ordering") - end - - def label_total_found(asset, total_found) - labels = { - :products => _("%s products offers found"), - :articles => _("%s articles found"), - :events => _("%s events found"), - :people => _("%s people found"), - :enterprises => _("%s enterprises found"), - :communities => _("%s communities found"), - } - content_tag('span', labels[asset] % total_found, - :class => "total-pages-found") if labels[asset] - end - def asset_class(asset) asset.to_s.singularize.camelize.constantize end @@ -213,4 +112,19 @@ module SearchHelper asset_class(asset).table_name end + def display_filter(asset, display, float = 'right') + if map_capable?(asset) + list_link = display == 'list' ? _('List') : link_to(_('List'), params.merge(:display => 'list')) + map_link = display == 'map' ? _('Map') : link_to(_('Map'), params.merge(:display => 'map')) + content_tag('div', + content_tag('strong', _('Display')) + ': ' + + list_link + + ' | ' + + map_link, + :id => 'search-display-filter', + :style => "float: #{float}" + ) + end + end + end diff --git a/app/views/search/_display_results.rhtml b/app/views/search/_display_results.rhtml index 47c930d..d18ba70 100644 --- a/app/views/search/_display_results.rhtml +++ b/app/views/search/_display_results.rhtml @@ -1,10 +1,9 @@
<% @order.each do |name| %> <% results = @results[name] %> - <% empty = results.nil? || results.empty? %> -
"> - <% if not empty %> +
"> + <% if !results.blank? %> <% partial = partial_for_class(results.first.class.class_name.constantize) %> <% if multiple_search? %> diff --git a/app/views/search/_facets_menu.rhtml b/app/views/search/_facets_menu.rhtml deleted file mode 100644 index 1893876..0000000 --- a/app/views/search/_facets_menu.rhtml +++ /dev/null @@ -1,36 +0,0 @@ -<% less_options_limit = 8 %> - -
- <% @asset_class.map_facets_for(environment).each do |facet| %> - -
-
- <%= @asset_class.facet_label(facet) %> -
- - <% results = @asset_class.map_facet_results(facet, params[:facet], @facets, @all_facets, :limit => less_options_limit) %> - <% facet_count = results.total_entries %> - - <% if facet_count > 0 %> - - -
- <% results.each do |id, label, count| %> - <%= facet_link_html(facet, params, id, label, count) %>
- <% end %> -

- - <% if facet_count > less_options_limit %> - <%= link_to_function _("Options"), - "facet_options_toggle('#{facet[:id].to_s}', '#{url_for(params.merge(:action => 'facets_browse', :facet_id => facet[:id], :asset => @asset, :escape => false))}'); " + - "jQuery(this).toggleClass('facet-less-options')", :class => "facet-options-toggle" %> -
- <% end %> - - <% else %> - <%= _("No filter available") %> - <% end %> -
- <% end %> -
diff --git a/app/views/search/_profile.rhtml b/app/views/search/_profile.rhtml index c0daf94..b2f665f 100644 --- a/app/views/search/_profile.rhtml +++ b/app/views/search/_profile.rhtml @@ -1,5 +1,5 @@
  • -<% if @empty_query or multiple_search? or !profile.enterprise? %> +<% if @empty_query || multiple_search? || !profile.enterprise? || !@search_engine %> <%= profile_image_link profile, :portrait, 'div', @filter == 'more_recent' ? profile.send(@filter + '_label') + show_date(profile.created_at) : profile.send(@filter + '_label') %> <% else %> diff --git a/app/views/search/_results_header.rhtml b/app/views/search/_results_header.rhtml index 3449eba..635d231 100644 --- a/app/views/search/_results_header.rhtml +++ b/app/views/search/_results_header.rhtml @@ -1,21 +1,5 @@
    "> - <% if !@empty_query %> -
    - <% if @results[@asset].total_entries > 0 %> - <%= label_total_found(@asset, @results[@asset].total_entries) %> - <% if params[:display] != 'map' %> - <%= _("Showing page %s of %s") % [@results[@asset].current_page, @results[@asset].total_pages] %> - <% end %> - <% end %> -
    - -
    - <%= facets_unselect_menu(@asset) %> - <%= order_by(@asset) if params[:display] != 'map' %> -
    - <% else %> -
    <%= @filter_title if @filter_title %>
    - <% end %> - +
    <%= @filter_title if @filter_title %>
    + <%= display_filter(@asset, params[:display]) if map_capable?(@asset) %>
    diff --git a/app/views/search/articles.rhtml b/app/views/search/articles.rhtml deleted file mode 100644 index ee20056..0000000 --- a/app/views/search/articles.rhtml +++ /dev/null @@ -1,17 +0,0 @@ -<%= search_page_title( @titles[:articles], @category ) %> - -
    - <% if !@empty_query %> - <%= facets_menu(:articles, @facets) %> - <% end %> -
    - -
    - <%= render :partial => 'search_form', :locals => { :hint => _('Type the title, author or content desired') } %> - <%= render :partial => 'results_header' %> - - <%= display_results %> - <%= pagination_links @results[:articles] %> -
    - -
    diff --git a/app/views/search/communities.rhtml b/app/views/search/communities.rhtml deleted file mode 100644 index 4566cf5..0000000 --- a/app/views/search/communities.rhtml +++ /dev/null @@ -1,25 +0,0 @@ -<%= search_page_title( @titles[:communities], @category ) %> - -
    - <% if logged_in? %> - <% button_bar do %> - <%# FIXME shouldn't the user create the community in the current environment instead of going to its home environment? %> - <%= button(:add, __('New community'), user.url.merge(:controller => 'memberships', :action => 'new_community', :profile => user.identifier)) %> - <% end %> - <% end %> - - <% if !@empty_query %> - <%= facets_menu(:communities, @facets) %> - <% end %> -
    - -
    - <%= render :partial => 'search_form', :locals => { :hint => _("Type words about the community you're looking for") } %> - <%= render :partial => 'results_header' %> - - <%= display_results %> - <%= pagination_links @results.values.first %> -
    - -
    - diff --git a/app/views/search/contents.rhtml b/app/views/search/contents.rhtml deleted file mode 120000 index 3015e62..0000000 --- a/app/views/search/contents.rhtml +++ /dev/null @@ -1 +0,0 @@ -articles.rhtml \ No newline at end of file diff --git a/app/views/search/enterprises.rhtml b/app/views/search/enterprises.rhtml deleted file mode 100644 index e8bfedf..0000000 --- a/app/views/search/enterprises.rhtml +++ /dev/null @@ -1,28 +0,0 @@ -<%= search_page_title( @titles[:enterprises], @category ) %> - -
    - <% if logged_in? && environment.enabled?('enterprise_registration') %> - <% button_bar do %> - <%= button(:add, __('New enterprise'), {:controller => 'enterprise_registration'}) %> - <% end %> - <% end %> - - <% if !@empty_query %> - <% button_bar do %> - <%= display_map_list_button %> - <% end %> - <%= facets_menu(:enterprises, @facets) %> - <% end %> -
    - -
    - <%= render :partial => 'search_form', :locals => { :hint => _("Type words about the enterprise you're looking for") } %> - <%= render :partial => 'results_header' %> - - <%= display_results(true) %> - <% if params[:display] != 'map' %> - <%= pagination_links @results[:enterprises] %> - <% end %> -
    - -
    diff --git a/app/views/search/events.html.erb b/app/views/search/events.html.erb new file mode 100644 index 0000000..0785346 --- /dev/null +++ b/app/views/search/events.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'events/agenda' %> diff --git a/app/views/search/events.rhtml b/app/views/search/events.rhtml deleted file mode 100644 index 133ae77..0000000 --- a/app/views/search/events.rhtml +++ /dev/null @@ -1,3 +0,0 @@ -<%= search_page_title( Environment.default.name + "'s " + @titles[:events], @category ) %> - -<%= render :partial => 'events/agenda' %> diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 7b2ee19..ec950d0 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -7,7 +7,7 @@ <%= search_page_title(_('Search Results'), @category) %> <%= render :partial => 'search_form', :locals => { :hint => '' } %> <%= category_context(@category, params) %> - <%= display_results %> + <%= display_results(@asset) %>
    <% if @category %> diff --git a/app/views/search/people.rhtml b/app/views/search/people.rhtml deleted file mode 100644 index adb29fd..0000000 --- a/app/views/search/people.rhtml +++ /dev/null @@ -1,19 +0,0 @@ -<%= search_page_title( @titles[:people], @category ) %> - -
    - <% if !@empty_query %> - <%= facets_menu(:people, @facets) %> - <% end %> -
    - -
    - <%= render :partial => 'search_form', :locals => { :hint => _("Type words about the person you're looking for") } %> - <%= render :partial => 'results_header' %> - - <%= display_results %> - <% if params[:display] != 'map' %> - <%= pagination_links @results.values.first %> - <% end %> -
    - -
    diff --git a/app/views/search/products.rhtml b/app/views/search/products.rhtml deleted file mode 100644 index 74e8f02..0000000 --- a/app/views/search/products.rhtml +++ /dev/null @@ -1,26 +0,0 @@ -<%= search_page_title( @titles[:products], @category ) %> - -
    - <% if !@empty_query %> - <% button_bar do %> - <%= display_map_list_button %> -<% end %> - <%= facets_menu(:products, @facets) %> -<% end %> -
    - -
    - <%= render :partial => 'search_form', :locals => { :hint => _('Type the product, service, city or qualifier desired') } %> - <%= render :partial => 'results_header' %> - - <%= display_results(true) %> - <% if !@one_page and params[:display] != 'map' %> - <%= pagination_links @results[:products] %> -<% end %> -
    - -<% javascript_tag do %> - jQuery('.search-product-price-details').altBeautify(); -<% end %> - -
    diff --git a/app/views/search/search_page.html.erb b/app/views/search/search_page.html.erb new file mode 100644 index 0000000..463ad7d --- /dev/null +++ b/app/views/search/search_page.html.erb @@ -0,0 +1,16 @@ +<%= search_page_title( @titles[@asset], @category ) %> + +<%= render :partial => 'results_header' %> + +<%= display_results(@asset) %> +<% if params[:display] != 'map' %> + <%= pagination_links @results[@asset] %> +<% end %> + +
    + +<% if @asset == :product %> + <% javascript_tag do %> + jQuery('.search-product-price-details').altBeautify(); + <% end %> +<% end %> diff --git a/lib/noosfero/plugin.rb b/lib/noosfero/plugin.rb index e374a5d..666686b 100644 --- a/lib/noosfero/plugin.rb +++ b/lib/noosfero/plugin.rb @@ -351,6 +351,17 @@ class Noosfero::Plugin nil end + # -> Specifies plugin that works as a search engine + # returns = true/false + def search_engine? + false + end + + # -> Realizes a full text search + # returns = whatever the plugin needs to render the view + def full_text_search(asset, query, category, paginate_options) + end + def method_missing(method, *args, &block) # This is a generic hotspot for all controllers on Noosfero. # If any plugin wants to define filters to run on any controller, the name of diff --git a/plugins/solr/dependencies.rb b/plugins/solr/dependencies.rb new file mode 100644 index 0000000..e52c70f --- /dev/null +++ b/plugins/solr/dependencies.rb @@ -0,0 +1,3 @@ +require 'active_record' +require 'acts_as_searchable' +require 'acts_as_faceted' diff --git a/plugins/solr/lib/solr_plugin.rb b/plugins/solr/lib/solr_plugin.rb index c32fc1f..d760d27 100644 --- a/plugins/solr/lib/solr_plugin.rb +++ b/plugins/solr/lib/solr_plugin.rb @@ -1,5 +1,9 @@ +require_dependency 'solr_plugin/search_helper' + class SolrPlugin < Noosfero::Plugin + include SolrPlugin::SearchHelper + def self.plugin_name "Solr" end @@ -8,6 +12,16 @@ class SolrPlugin < Noosfero::Plugin _("Uses Solr as search engine.") end + def search_engine? + true + end + + def full_text_search(asset, query, category, paginate_options) + asset_class = asset_class(asset) + solr_options = solr_options(asset, category) + asset_class.find_by_contents(query, paginate_options, solr_options) + end + end Dir[File.join(SolrPlugin.root_path, 'lib', 'ext', '*.rb')].each {|file| require_dependency file } diff --git a/plugins/solr/lib/solr_plugin/results_helper.rb b/plugins/solr/lib/solr_plugin/results_helper.rb new file mode 100644 index 0000000..2311394 --- /dev/null +++ b/plugins/solr/lib/solr_plugin/results_helper.rb @@ -0,0 +1,113 @@ +class SolrPlugin < Noosfero::Plugin + module ResultsHelper + def set_results_variables + if @results[@asset].kind_of?(Hash) + ret = @results[@asset] + @results[@asset] = ret[:results] + @facets = ret[:facets] + @all_facets = ret[:all_facets] + end + end + + def order_by(asset) + options = SolrPlugin::SortOptions[asset].map do |name, options| + next if options[:if] && !instance_eval(&options[:if]) + [_(options[:label]), name.to_s] + end.compact + + content_tag('div', _('Sort results by ') + + select_tag(asset.to_s + '[order]', options_for_select(options, params[:order_by] || 'none'), + {:onchange => "window.location = jQuery.param.querystring(window.location.href, { 'order_by' : this.options[this.selectedIndex].value})"} + ), + :class => "search-ordering" + ) + end + + def label_total_found(asset, total_found) + labels = { + :products => _("%s products offers found"), + :articles => _("%s articles found"), + :events => _("%s events found"), + :people => _("%s people found"), + :enterprises => _("%s enterprises found"), + :communities => _("%s communities found"), + } + content_tag('span', labels[asset] % total_found, + :class => "total-pages-found") if labels[asset] + end + + def facets_menu(asset, _facets) + @asset_class = asset_class(asset) + @facets = _facets + render(:partial => 'facets_menu') + end + + def facets_unselect_menu(asset) + @asset_class = asset_class(asset) + render(:partial => 'facets_unselect_menu') + end + + def facet_selecteds_html_for(environment, klass, params) + def name_with_extra(klass, facet, value) + name = klass.facet_result_name(facet, value) + name = name[0] + name[1] if name.kind_of?(Array) + name + end + + ret = [] + params = params.dup + params[:facet].each do |id, value| + facet = klass.facet_by_id(id.to_sym) + next unless facet + if value.kind_of?(Hash) + label_hash = facet[:label].call(environment) + value.each do |label_id, value| + facet[:label_id] = label_id + facet[:label] = label_hash[label_id] + value.to_a.each do |value| + ret << [facet[:label], name_with_extra(klass, facet, value), + params.merge(:facet => params[:facet].merge(id => params[:facet][id].merge(label_id => params[:facet][id][label_id].to_a.reject{ |v| v == value })))] + end + end + else + ret << [klass.facet_label(facet), name_with_extra(klass, facet, value), + params.merge(:facet => params[:facet].reject{ |k,v| k == id })] + end + end + + ret.map do |label, name, url| + content_tag('div', content_tag('span', label, :class => 'facet-selected-label') + + content_tag('span', name, :class => 'facet-selected-name') + + link_to('', url, :class => 'facet-selected-remove', :title => 'remove facet'), :class => 'facet-selected') + end.join + end + + def facet_link_html(facet, params, value, label, count) + params = params ? params.dup : {} + has_extra = label.kind_of?(Array) + link_label = has_extra ? label[0] : label + id = facet[:solr_field].to_s + params[:facet] ||= {} + params[:facet][id] ||= {} + params[:page] = {} if params[:page] + + selected = facet[:label_id].nil? ? params[:facet][id] == value : params[:facet][id][facet[:label_id]].to_a.include?(value) + + if count > 0 + url = params.merge(:facet => params[:facet].merge( + id => facet[:label_id].nil? ? value : params[:facet][id].merge( facet[:label_id] => params[:facet][id][facet[:label_id]].to_a | [value] ) + )) + else + # preserve others filters and change this filter + url = params.merge(:facet => params[:facet].merge( + id => facet[:label_id].nil? ? value : { facet[:label_id] => value } + )) + end + + content_tag 'div', link_to(link_label, url, :class => 'facet-result-link-label') + + content_tag('span', (has_extra ? label[1] : ''), :class => 'facet-result-extra-label') + + (count > 0 ? content_tag('span', " (#{count})", :class => 'facet-result-count') : ''), + :class => 'facet-menu-item' + (selected ? ' facet-result-link-selected' : '') + end + end +end diff --git a/plugins/solr/lib/solr_plugin/search_helper.rb b/plugins/solr/lib/solr_plugin/search_helper.rb new file mode 100644 index 0000000..0d0dac9 --- /dev/null +++ b/plugins/solr/lib/solr_plugin/search_helper.rb @@ -0,0 +1,83 @@ +class SolrPlugin < Noosfero::Plugin + + SortOptions = { + :products => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :more_recent, {:label => _('More recent'), :solr_opts => {:sort => 'updated_at desc, score desc'}}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + :closest, {:label => _('Closest to me'), :if => proc{ logged_in? && (profile=current_user.person).lat && profile.lng }, + :solr_opts => {:sort => "geodist() asc", + :latitude => proc{ current_user.person.lat }, :longitude => proc{ current_user.person.lng }}}, + ], + :events => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + ], + :articles => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + :more_recent, {:label => _('More recent'), :solr_opts => {:sort => 'updated_at desc, score desc'}}, + ], + :enterprises => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + ], + :people => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + ], + :communities => ActiveSupport::OrderedHash[ :none, {:label => _('Relevance')}, + :name, {:label => _('Name'), :solr_opts => {:sort => 'solr_plugin_name_sortable asc'}}, + ], + } + + module SearchHelper + def asset_class(asset) + asset.to_s.singularize.camelize.constantize + end + + def asset_table(asset) + asset_class(asset).table_name + end + + def multiple_search? + ['index', 'category_index'].include?(context.params[:action]) + end + + def filters(asset) + case asset + when :products + ['solr_plugin_public:true'] + when :events + [] + else + ['solr_plugin_public:true'] + end + end + + def results_only? + context.params[:action] == 'index' + end + + def solr_options(asset, category) + asset_class = asset_class(asset) + solr_options = {} + if !multiple_search? + if !results_only? and asset_class.respond_to? :facets + solr_options.merge! asset_class.facets_find_options(context.params[:facet]) + solr_options[:all_facets] = true + end + solr_options[:filter_queries] ||= [] + solr_options[:filter_queries] += filters(asset) + solr_options[:filter_queries] << "environment_id:#{context.environment.id}" + solr_options[:filter_queries] << asset_class.facet_category_query.call(category) if category + + solr_options[:boost_functions] ||= [] + context.params[:order_by] = nil if context.params[:order_by] == 'none' + if context.params[:order_by] + order = SolrPlugin::SortOptions[asset][context.params[:order_by].to_sym] + raise "Unknown order by" if order.nil? + order[:solr_opts].each do |opt, value| + solr_options[opt] = value.is_a?(Proc) ? instance_eval(&value) : value + end + end + end + solr_options + end + end +end diff --git a/plugins/solr/test/unit/article_test.rb b/plugins/solr/test/unit/article_test.rb new file mode 100644 index 0000000..a749977 --- /dev/null +++ b/plugins/solr/test/unit/article_test.rb @@ -0,0 +1,115 @@ +require 'test_helper' + +class ArticleTest < ActiveSupport::TestCase + def setup + @environment = Environment.default + @environment.enable_plugin(SolrPlugin) + @profile = create_user('testing').person + end + + attr_accessor :environment, :profile + + should 'act as faceted' do + person = fast_create(Person) + cat = Category.create!(:name => 'hardcore', :environment_id => Environment.default.id) + a = Article.create!(:name => 'black flag review', :profile_id => person.id) + a.add_category(cat, true) + a.save! + assert_equal Article.type_name, Article.facet_by_id(:solr_plugin_f_type)[:proc].call(a.send(:solr_plugin_f_type)) + assert_equal Person.type_name, Article.facet_by_id(:solr_plugin_f_profile_type)[:proc].call(a.send(:solr_plugin_f_profile_type)) + assert_equal a.published_at, a.send(:solr_plugin_f_published_at) + assert_equal ['hardcore'], a.send(:solr_plugin_f_category) + assert_equal "solr_plugin_category_filter:\"#{cat.id}\"", Article.facet_category_query.call(cat) + end + + should 'act as searchable' do + TestSolr.enable + person = fast_create(Person, :name => "Hiro", :address => 'U-Stor-It @ Inglewood, California', + :nickname => 'Protagonist') + person2 = fast_create(Person, :name => "Raven") + category = fast_create(Category, :name => "science fiction", :acronym => "sf", :abbreviation => "sci-fi") + a = Article.create!(:name => 'a searchable article about bananas', :profile_id => person.id, + :body => 'the body talks about mosquitos', :abstract => 'and the abstract is about beer', + :filename => 'not_a_virus.exe') + a.add_category(category) + c = a.comments.build(:title => 'snow crash', :author => person2, :body => 'wanna try some?') + c.save! + + # fields + assert_includes Article.find_by_contents('bananas')[:results].docs, a + assert_includes Article.find_by_contents('mosquitos')[:results].docs, a + assert_includes Article.find_by_contents('beer')[:results].docs, a + assert_includes Article.find_by_contents('not_a_virus.exe')[:results].docs, a + # filters + assert_includes Article.find_by_contents('bananas', {}, {:filter_queries => ["solr_plugin_public:true"]})[:results].docs, a + assert_not_includes Article.find_by_contents('bananas', {}, {:filter_queries => ["solr_plugin_public:false"]})[:results].docs, a + assert_includes Article.find_by_contents('bananas', {}, {:filter_queries => ["environment_id:\"#{Environment.default.id}\""]})[:results].docs, a + assert_includes Article.find_by_contents('bananas', {}, {:filter_queries => ["profile_id:\"#{person.id}\""]})[:results].docs, a + # includes + assert_includes Article.find_by_contents('Hiro')[:results].docs, a + assert_includes Article.find_by_contents("person-#{person.id}")[:results].docs, a + assert_includes Article.find_by_contents("California")[:results].docs, a + assert_includes Article.find_by_contents("Protagonist")[:results].docs, a +# FIXME: After merging with AI1826, searching on comments is not working +# assert_includes Article.find_by_contents("snow")[:results].docs, a +# assert_includes Article.find_by_contents("try some")[:results].docs, a +# assert_includes Article.find_by_contents("Raven")[:results].docs, a +# +# FIXME: After merging with AI1826, searching on categories is not working +# assert_includes Article.find_by_contents("science")[:results].docs, a +# assert_includes Article.find_by_contents(category.slug)[:results].docs, a +# assert_includes Article.find_by_contents("sf")[:results].docs, a +# assert_includes Article.find_by_contents("sci-fi")[:results].docs, a + end + + should 'boost name matches' do + TestSolr.enable + person = fast_create(Person) + in_body = Article.create!(:name => 'something', :profile_id => person.id, :body => 'bananas in the body!') + in_name = Article.create!(:name => 'bananas in the name!', :profile_id => person.id) + assert_equal [in_name, in_body], Article.find_by_contents('bananas')[:results].docs + end + + should 'boost if profile is enabled' do + TestSolr.enable + person2 = fast_create(Person, :enabled => false) + art_profile_disabled = Article.create!(:name => 'profile disabled', :profile_id => person2.id) + person1 = fast_create(Person, :enabled => true) + art_profile_enabled = Article.create!(:name => 'profile enabled', :profile_id => person1.id) + assert_equal [art_profile_enabled, art_profile_disabled], Article.find_by_contents('profile')[:results].docs + end + + should 'index comments body together with article' do + TestSolr.enable + owner = create_user('testuser').person + art = fast_create(TinyMceArticle, :profile_id => owner.id, :name => 'ytest') + c1 = Comment.create(:title => 'test comment', :body => 'anything', :author => owner, :source => art); c1.save! + + assert_includes Article.find_by_contents('anything')[:results], art + end + + should 'index by schema name when database is postgresql' do + TestSolr.enable + uses_postgresql 'schema_one' + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id) + assert_equal [art1], Article.find_by_contents('thing')[:results].docs + uses_postgresql 'schema_two' + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id) + assert_not_includes Article.find_by_contents('thing')[:results], art1 + assert_includes Article.find_by_contents('thing')[:results], art2 + uses_postgresql 'schema_one' + assert_includes Article.find_by_contents('thing')[:results], art1 + assert_not_includes Article.find_by_contents('thing')[:results], art2 + uses_sqlite + end + + should 'not index by schema name when database is not postgresql' do + TestSolr.enable + uses_sqlite + art1 = Article.create!(:name => 'some thing', :profile_id => @profile.id) + assert_equal [art1], Article.find_by_contents('thing')[:results].docs + art2 = Article.create!(:name => 'another thing', :profile_id => @profile.id) + assert_includes Article.find_by_contents('thing')[:results], art1 + assert_includes Article.find_by_contents('thing')[:results], art2 + end +end diff --git a/plugins/solr/test/unit/environment_test.rb b/plugins/solr/test/unit/environment_test.rb new file mode 100644 index 0000000..2fd3a22 --- /dev/null +++ b/plugins/solr/test/unit/environment_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' + +class EnvironmentTest < ActiveSupport::TestCase + def setup + @environment = Environment.default + @environment.enable_plugin(SolrPlugin) + end + + attr_accessor :environment + + should 'find by contents from articles' do + TestSolr.enable + env = fast_create(Environment) + env.enable_plugin(SolrPlugin) + assert_nothing_raised do + env.articles.find_by_contents('')[:results] + end + end + + should 'return more than 10 enterprises by contents' do + TestSolr.enable + Enterprise.destroy_all + ('1'..'20').each do |n| + Enterprise.create!(:name => 'test ' + n, :identifier => 'test_' + n) + end + + assert_equal 20, environment.enterprises.find_by_contents('test')[:results].total_entries + end +end diff --git a/plugins/solr/test/unit/event_test.rb b/plugins/solr/test/unit/event_test.rb new file mode 100644 index 0000000..1d401d1 --- /dev/null +++ b/plugins/solr/test/unit/event_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class EventTest < ActiveSupport::TestCase + def setup + @environment = Environment.default + @environment.enable_plugin(SolrPlugin) + @profile = create_user('testing').person + end + + attr_accessor :environment, :profile + + should 'be indexed by title' do + TestSolr.enable + e = Event.create!(:name => 'my surprisingly nice event', :start_date => Date.new(2008, 06, 06), :profile => profile) + assert_includes Event.find_by_contents('surprisingly')[:results], e + end + + should 'be indexed by body' do + TestSolr.enable + e = Event.create!(:name => 'bli', :start_date => Date.new(2008, 06, 06), :profile => profile, :body => 'my surprisingly long description about my freaking nice event') + assert_includes Event.find_by_contents('surprisingly')[:results], e + end +end diff --git a/plugins/solr/test/unit/text_article_test.rb b/plugins/solr/test/unit/text_article_test.rb new file mode 100644 index 0000000..7f570ae --- /dev/null +++ b/plugins/solr/test/unit/text_article_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class TextArticleTest < ActiveSupport::TestCase + def setup + @environment = Environment.default + @environment.enable_plugin(SolrPlugin) + end + + attr_accessor :environment + + should 'found TextileArticle by TextArticle indexes' do + TestSolr.enable + person = create_user('testuser').person + article = TextileArticle.create!(:name => 'found article test', :profile => person) + assert_equal TextileArticle.find_by_contents('found')[:results].docs, TextArticle.find_by_contents('found')[:results].docs + end +end diff --git a/plugins/solr/test/unit/tiny_mce_article_test.rb b/plugins/solr/test/unit/tiny_mce_article_test.rb new file mode 100644 index 0000000..b2d720b --- /dev/null +++ b/plugins/solr/test/unit/tiny_mce_article_test.rb @@ -0,0 +1,18 @@ +require 'test_helper' + +class TinyMceArticleTest < ActiveSupport::TestCase + def setup + @environment = Environment.default + @environment.enable_plugin(SolrPlugin) + @profile = create_user('testing').person + end + + attr_accessor :environment, :profile + + should 'be found when searching for articles by query' do + TestSolr.enable + tma = TinyMceArticle.create!(:name => 'test tinymce article', :body => '---', :profile => profile) + assert_includes TinyMceArticle.find_by_contents('article')[:results], tma + assert_includes Article.find_by_contents('article')[:results], tma + end +end diff --git a/plugins/solr/views/search/.communities.rhtml.swp b/plugins/solr/views/search/.communities.rhtml.swp new file mode 100644 index 0000000..ff23962 Binary files /dev/null and b/plugins/solr/views/search/.communities.rhtml.swp differ diff --git a/plugins/solr/views/search/.people.rhtml.swp b/plugins/solr/views/search/.people.rhtml.swp new file mode 100644 index 0000000..9add0ea Binary files /dev/null and b/plugins/solr/views/search/.people.rhtml.swp differ diff --git a/plugins/solr/views/search/_facets.html.erb b/plugins/solr/views/search/_facets.html.erb new file mode 100644 index 0000000..de029e9 --- /dev/null +++ b/plugins/solr/views/search/_facets.html.erb @@ -0,0 +1,10 @@ +<% if logged_in? %> + <% button_bar do %> + <%# FIXME shouldn't the user create the community in the current environment instead of going to its home environment? %> + <%= button(:add, __('New community'), user.url.merge(:controller => 'memberships', :action => 'new_community', :profile => user.identifier)) if @asset == :communities %> + <%= button(:add, __('New enterprise'), {:controller => 'enterprise_registration'}) if @asset == :enterprises && environment.enabled?('enterprise_registration') %> + <% end %> +<% end %> +<% if !@empty_query %> + <%= facets_menu(@asset, @facets) %> +<% end %> diff --git a/plugins/solr/views/search/_facets_menu.html.erb b/plugins/solr/views/search/_facets_menu.html.erb new file mode 100644 index 0000000..1893876 --- /dev/null +++ b/plugins/solr/views/search/_facets_menu.html.erb @@ -0,0 +1,36 @@ +<% less_options_limit = 8 %> + +
    + <% @asset_class.map_facets_for(environment).each do |facet| %> + +
    +
    + <%= @asset_class.facet_label(facet) %> +
    + + <% results = @asset_class.map_facet_results(facet, params[:facet], @facets, @all_facets, :limit => less_options_limit) %> + <% facet_count = results.total_entries %> + + <% if facet_count > 0 %> + + +
    + <% results.each do |id, label, count| %> + <%= facet_link_html(facet, params, id, label, count) %>
    + <% end %> +

    + + <% if facet_count > less_options_limit %> + <%= link_to_function _("Options"), + "facet_options_toggle('#{facet[:id].to_s}', '#{url_for(params.merge(:action => 'facets_browse', :facet_id => facet[:id], :asset => @asset, :escape => false))}'); " + + "jQuery(this).toggleClass('facet-less-options')", :class => "facet-options-toggle" %> +
    + <% end %> + + <% else %> + <%= _("No filter available") %> + <% end %> +
    + <% end %> +
    diff --git a/plugins/solr/views/search/_results.html.erb b/plugins/solr/views/search/_results.html.erb new file mode 100644 index 0000000..cd97d3f --- /dev/null +++ b/plugins/solr/views/search/_results.html.erb @@ -0,0 +1,7 @@ +<%= render :partial => 'search_form', :locals => { :hint => _("Type words about the %s you're looking for") % @asset.to_s.singularize } %> +<%= render :partial => 'results_header' %> + +<%= display_results(@asset) %> +<% if params[:display] != 'map' %> + <%= pagination_links @results[@asset] %> +<% end %> diff --git a/plugins/solr/views/search/_results_header.html.erb b/plugins/solr/views/search/_results_header.html.erb new file mode 100644 index 0000000..9f1785b --- /dev/null +++ b/plugins/solr/views/search/_results_header.html.erb @@ -0,0 +1,22 @@ +
    "> + <% if !@empty_query %> +
    + <% if @results[@asset].total_entries > 0 %> + <%= label_total_found(@asset, @results[@asset].total_entries) %> + <% if params[:display] != 'map' %> + <%= _("Showing page %s of %s") % [@results[@asset].current_page, @results[@asset].total_pages] %> + <% end %> + <% end %> +
    + +
    + <%= facets_unselect_menu(@asset) %> + <%= order_by(@asset) if params[:display] != 'map' %> +
    + <% else %> +
    <%= @filter_title if @filter_title %>
    + <% end %> + <% float = !@empty_query && params[:display] == 'list' ? 'left' : 'right' %> + <%= display_filter(@asset, params[:display], float) if map_capable?(@asset) %> +
    +
    diff --git a/plugins/solr/views/search/search_page.html.erb b/plugins/solr/views/search/search_page.html.erb new file mode 100644 index 0000000..8ee945d --- /dev/null +++ b/plugins/solr/views/search/search_page.html.erb @@ -0,0 +1,24 @@ +<% extend SolrPlugin::ResultsHelper %> +<% set_results_variables %> + +<%= search_page_title( @titles[@asset], @category ) %> + +<% if !@empty_query %> +
    + <%= render :partial => 'facets' %> +
    + +
    + <%= render :partial => 'results' %> +
    +<% else %> + <%= render :partial => 'results' %> +<% end %> + +
    + +<% if @asset == :products %> + <% javascript_tag do %> + jQuery('.search-product-price-details').altBeautify(); + <% end %> +<% end %> diff --git a/public/stylesheets/search.css b/public/stylesheets/search.css index b218b92..1c3a4f5 100644 --- a/public/stylesheets/search.css +++ b/public/stylesheets/search.css @@ -647,6 +647,7 @@ li.search-product-item hr { font-weight: bold; font-size: 130%; line-height: 35px; + float: left; } .search-results-header.search-no-results { border-bottom: 0px; @@ -665,6 +666,9 @@ li.search-product-item hr { color: #ff0000 } +#search-display-filter { + margin-top: 20px; +} /* Search field and button */ -- libgit2 0.21.2