app/controllers/homepage_controller.rb
class HomepageController < ApplicationController
before_action :save_current_path, :except => :sign_in
APP_DEFAULT_VIEW_TYPE = "grid".freeze
VIEW_TYPES_NO_LOCATION = ["grid".freeze, "list".freeze].freeze
VIEW_TYPES = (VIEW_TYPES_NO_LOCATION + ["map".freeze]).freeze
# rubocop:disable AbcSize
# rubocop:disable MethodLength
def index
params = unsafe_params_hash.select{|k, v| v.present? }
redirect_to landing_page_path and return if no_current_user_in_private_clp_enabled_marketplace?
all_shapes = @current_community.shapes
shape_name_map = all_shapes.map { |s| [s[:id], s[:name]]}.to_h
filter_params = {}
m_selected_category = Maybe(@current_community.categories.find_by_url_or_id(params[:category]))
filter_params[:categories] = m_selected_category.own_and_subcategory_ids.or_nil
selected_category = m_selected_category.or_nil
relevant_filters = select_relevant_filters(m_selected_category.own_and_subcategory_ids.or_nil)
@seo_service.category = selected_category
if FeatureFlagHelper.feature_enabled?(:searchpage_v1)
@view_type = "grid"
else
@view_type = SearchPageHelper.selected_view_type(params[:view], @current_community.default_browse_view, APP_DEFAULT_VIEW_TYPE, allowed_view_types)
@big_cover_photo = !(@current_user || CustomLandingPage::LandingPageStore.enabled?(@current_community.id)) || params[:big_cover_photo]
@categories = @current_community.categories.includes(:children)
@main_categories = @categories.select { |c| c.parent_id == nil }
# This assumes that we don't never ever have communities with only 1 main share type and
# only 1 sub share type, as that would make the listing type menu visible and it would look bit silly
listing_shape_menu_enabled = all_shapes.size > 1
@show_categories = @categories.size > 1
show_price_filter = @current_community.show_price_filter && all_shapes.any? { |s| s[:price_enabled] }
@show_custom_fields = relevant_filters.present? || show_price_filter
@category_menu_enabled = @show_categories || @show_custom_fields
if @show_categories
@category_display_names = category_display_names(@current_community, @categories)
end
end
listing_shape_param = params[:transaction_type]
selected_shape = all_shapes.find { |s| s[:name] == listing_shape_param }
filter_params[:listing_shape] = Maybe(selected_shape)[:id].or_else(nil)
compact_filter_params = HashUtils.compact(filter_params)
per_page = @view_type == "map" ? APP_CONFIG.map_listings_limit : APP_CONFIG.grid_listings_limit
includes =
case @view_type
when "grid"
[:author, :listing_images]
when "list"
[:author, :listing_images, :num_of_reviews]
when "map"
[:location]
else
raise ArgumentError.new("Unknown view_type #{@view_type}")
end
main_search = search_mode
enabled_search_modes = search_modes_in_use(params[:q], params[:lc], main_search)
keyword_in_use = enabled_search_modes[:keyword]
location_in_use = enabled_search_modes[:location]
current_page = Maybe(params)[:page].to_i.map { |n| n > 0 ? n : 1 }.or_else(1)
relevant_search_fields = parse_relevant_search_fields(params, relevant_filters)
search_result = find_listings(params: params,
current_page: current_page,
listings_per_page: per_page,
filter_params: compact_filter_params,
includes: includes.to_set,
location_search_in_use: location_in_use,
keyword_search_in_use: keyword_in_use,
relevant_search_fields: relevant_search_fields)
if @view_type == 'map'
viewport = viewport_geometry(params[:boundingbox], params[:lc], @current_community.location)
end
if FeatureFlagHelper.feature_enabled?(:searchpage_v1)
search_result.on_success { |listings|
render layout: "layouts/react_page.haml", template: "search_page/search_page", locals: { props: searchpage_props(listings, current_page, per_page) }
}.on_error {
flash[:error] = t("homepage.errors.search_engine_not_responding")
render layout: "layouts/react_page.haml", template: "search_page/search_page", locals: { props: searchpage_props(nil, current_page, per_page) }
}
elsif request.xhr? # checks if AJAX request
search_result.on_success { |listings|
@listings = listings # TODO Remove
if @view_type == "grid" then
render partial: "grid_item", collection: @listings, as: :listing, locals: { show_distance: location_in_use }
elsif location_in_use
render partial: "list_item_with_distance", collection: @listings, as: :listing, locals: { shape_name_map: shape_name_map, show_distance: location_in_use }
else
render partial: "list_item", collection: @listings, as: :listing, locals: { shape_name_map: shape_name_map }
end
}.on_error {
render body: nil, status: :internal_server_error
}
else
locals = {
shapes: all_shapes,
filters: relevant_filters,
show_price_filter: show_price_filter,
selected_category: selected_category,
selected_shape: selected_shape,
shape_name_map: shape_name_map,
listing_shape_menu_enabled: listing_shape_menu_enabled,
main_search: main_search,
location_search_in_use: location_in_use,
current_page: current_page,
current_search_path_without_page: search_path(params.except(:page)),
viewport: viewport,
search_params: CustomFieldSearchParams.remove_irrelevant_search_params(params, relevant_search_fields)
}
search_result.on_success { |listings|
@listings = listings
render locals: locals.merge(
seo_pagination_links: seo_pagination_links(params, @listings.current_page, @listings.total_pages))
}.on_error { |e|
flash[:error] = t("homepage.errors.search_engine_not_responding")
@listings = Listing.none.paginate(:per_page => 1, :page => 1)
render status: :internal_server_error,
locals: locals.merge(
seo_pagination_links: seo_pagination_links(params, @listings.current_page, @listings.total_pages))
}
end
end
# rubocop:enable AbcSize
# rubocop:enable MethodLength
helper_method :allowed_view_types
def allowed_view_types
show_location? ? VIEW_TYPES : VIEW_TYPES_NO_LOCATION
end
private
def parse_relevant_search_fields(params, relevant_filters)
search_filters = SearchPageHelper.parse_filters_from_params(params)
checkboxes = search_filters[:checkboxes]
dropdowns = search_filters[:dropdowns]
numbers = filter_unnecessary(search_filters[:numeric], @current_community.custom_numeric_fields)
search_fields = checkboxes.concat(dropdowns).concat(numbers)
SearchPageHelper.remove_irrelevant_search_fields(search_fields, relevant_filters)
end
def find_listings(params:, current_page:, listings_per_page:, filter_params:, includes:, location_search_in_use:, keyword_search_in_use:, relevant_search_fields:)
search = {
# Add listing_id
categories: filter_params[:categories],
listing_shape_ids: Array(filter_params[:listing_shape]),
price_cents: filter_range(params[:price_min], params[:price_max]),
keywords: keyword_search_in_use ? params[:q] : nil,
fields: relevant_search_fields,
per_page: listings_per_page,
page: current_page,
price_min: params[:price_min],
price_max: params[:price_max],
locale: I18n.locale,
include_closed: false,
sort: nil
}
if @view_type != 'map' && location_search_in_use
search.merge!(location_search_params(params, keyword_search_in_use))
end
raise_errors = Rails.env.development?
if FeatureFlagHelper.feature_enabled?(:searchpage_v1)
DiscoveryClient.get(:query_listings,
params: DiscoveryUtils.listing_query_params(search.merge(marketplace_id: @current_community.id)))
.rescue {
Result::Error.new(nil, code: :discovery_api_error)
}
.and_then{ |res|
Result::Success.new(res[:body])
}
else
ListingIndexService::API::Api.listings.search(
community_id: @current_community.id,
search: search,
includes: includes,
engine: FeatureFlagHelper.search_engine,
raise_errors: raise_errors
).and_then { |res|
Result::Success.new(
ListingIndexViewUtils.to_struct(
result: res,
includes: includes,
page: search[:page],
per_page: search[:per_page]
)
)
}
end
end
# Time to cache category translations per locale
CATEGORY_DISPLAY_NAME_CACHE_EXPIRE_TIME = 24.hours
def category_display_names(community, categories)
Rails.cache.fetch(["catnames",
community,
I18n.locale,
categories],
expires_in: CATEGORY_DISPLAY_NAME_CACHE_EXPIRE_TIME) do
cat_names = {}
categories.each do |cat|
cat_names[cat.id] = cat.display_name(I18n.locale)
end
cat_names
end
end
def location_search_params(params, keyword_search_in_use)
marketplace_configuration = @current_community.configuration
distance = params[:distance_max].to_f
distance_system = marketplace_configuration ? marketplace_configuration[:distance_unit].to_sym : nil
distance_unit = distance_system == :metric ? :km : :miles
limit_search_distance = marketplace_configuration ? marketplace_configuration[:limit_search_distance] : true
distance_limit = [distance, APP_CONFIG[:external_search_distance_limit_min].to_f].max if limit_search_distance
corners = params[:boundingbox].split(',') if params[:boundingbox].present?
center_point = if limit_search_distance && corners&.length == 4
LocationUtils.center(*corners.map { |n| LocationUtils.to_radians(n) })
else
search_coordinates(params[:lc])
end
scale_multiplier = APP_CONFIG[:external_search_scale_multiplier].to_f
offset_multiplier = APP_CONFIG[:external_search_offset_multiplier].to_f
combined_search_in_use = keyword_search_in_use && scale_multiplier && offset_multiplier
combined_search_params = if combined_search_in_use
{
scale: [distance * scale_multiplier, APP_CONFIG[:external_search_scale_min].to_f].max,
offset: [distance * offset_multiplier, APP_CONFIG[:external_search_offset_min].to_f].max
}
else
{}
end
sort = :distance unless combined_search_in_use
{
distance_unit: distance_unit,
distance_max: distance_limit,
sort: sort
}
.merge(center_point)
.merge(combined_search_params)
.compact
end
# Filter search params if their values equal min/max
def filter_unnecessary(search_params, numeric_fields)
search_params.reject do |search_param|
numeric_field = numeric_fields.find(search_param[:id])
search_param.slice(:id, :value) == { id: numeric_field.id, value: (numeric_field.min..numeric_field.max) }
end
end
def filter_range(price_min, price_max)
if (price_min && price_max)
min = MoneyUtil.parse_str_to_money(price_min, @current_community.currency).cents
max = MoneyUtil.parse_str_to_money(price_max, @current_community.currency).cents
if ((@current_community.price_filter_min..@current_community.price_filter_max) != (min..max))
(min..max)
else
nil
end
end
end
def search_coordinates(latlng)
lat, lng = latlng.split(',')
if(lat.present? && lng.present?)
return { latitude: lat, longitude: lng }
else
ArgumentError.new("Format of latlng coordinate pair \"#{latlng}\" wasn't \"lat,lng\" ")
end
end
def no_current_user_in_private_clp_enabled_marketplace?
CustomLandingPage::LandingPageStore.enabled?(@current_community.id) &&
@current_community.private &&
!@current_user
end
def search_modes_in_use(q, lc, main_search)
# lc should be two decimal coordinates separated with a comma
# e.g. 65.123,-10
coords_valid = /^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/.match(lc)
{
keyword: q && [:keyword, :keyword_and_location].include?(main_search),
location: coords_valid && [:location, :keyword_and_location].include?(main_search)
}
end
def viewport_geometry(boundingbox, lc, community_location)
coords = Maybe(boundingbox).split(',').or_else(nil)
if coords
sw_lat, sw_lng, ne_lat, ne_lng = coords
{ boundingbox: { sw: [sw_lat, sw_lng], ne: [ne_lat, ne_lng] } }
elsif lc.present?
{ center: lc.split(',') }
else
Maybe(community_location)
.map { |l| { center: [l.latitude, l.longitude] }}
.or_else(nil)
end
end
def seo_pagination_links(params, current_page, total_pages)
prev_page =
if current_page > 1
search_path(params.merge(page: current_page - 1))
end
next_page =
if current_page < total_pages
search_path(params.merge(page: current_page + 1))
end
{
prev: prev_page,
next: next_page
}
end
def searchpage_props(bootstrapped_data, page, per_page)
SearchPageHelper.searchpage_props(
page: page,
per_page: per_page,
bootstrapped_data: bootstrapped_data,
notifications_to_react: notifications_to_react,
display_branding_info: display_branding_info?,
community: @current_community,
path_after_locale_change: @return_to,
user: @current_user,
search_placeholder: @community_customization&.search_placeholder,
current_path: request.fullpath,
locale_param: params[:locale],
host_with_port: request.host_with_port)
end
# Database select for "relevant" filters based on the `category_ids`
#
# If `category_ids` is present, returns only filter that belong to
# one of the given categories. Otherwise returns all filters.
#
def select_relevant_filters(category_ids)
relevant_filters =
if category_ids.present?
@current_community
.custom_fields
.joins(:category_custom_fields)
.where("category_custom_fields.category_id": category_ids, search_filter: true)
.distinct
else
@current_community
.custom_fields.where(search_filter: true)
end
relevant_filters.sort
end
def unsafe_params_hash
params.to_unsafe_hash
end
end