app/models/guide.rb
class Guide
include Mongoid::Document
include Mongoid::Paperclip
include Mongoid::Slug
include Mongoid::Timestamps
attr_accessor :current_user_compatibility_score
searchkick(callbacks: :async)
# The below seems to have made no difference, but it's based on:
# https://github.com/ankane/searchkick#stay-synced
# and the recommendations here:
# https://github.com/ankane/searchkick/issues/373#issuecomment-71967887
# Though it can probably be tweaked further.
scope :search_import, -> { includes(:user) }
# We need to override `impresionsist`s default behavior of
# `dependent: :delete_all`.
# If we don't, tests will fail with the following error:
# NameError uninitialized constant Mongoid::Relations::Cascading::DeleteAll
# has_many :impressions, as: :impressionable, dependent: :delete
# NOTE TO FUTURE SELF: Try removing the line above and see if tests pass.
# that would indicate that the bug was fixed upstream and we can remove this
# hotfix.
field :impressions_field, default: 0, type: Integer
belongs_to :crop, counter_cache: true
belongs_to :user
has_many :stages
field :draft, type: Boolean, default: true
embeds_one :time_span, cascade_callbacks: true, as: :timed
field :name
field :location
field :overview
field :practices, type: Array
field :completeness_score, default: 0
field :popularity_score, default: 0, type: Integer
field :times_favorited, type: Integer, default: 0
validates_presence_of :crop, :name
field :processing_pictures, type: Integer, default: 0
embeds_many :pictures, cascade_callbacks: true, as: :photographic
accepts_nested_attributes_for :pictures
# Points to the index of images which should
# be the featured image
field :featured_image, type: Integer
slug :name, history: true
after_save :calculate_completeness_score
# Maybe Popularity Score should be updated more frequently?
after_save :calculate_popularity_score
accepts_nested_attributes_for :time_span
def self.sorted_for_user(guides, user)
# PRODUCTION IS DOWN RIGHT NOW.
# I am going to plug this runtime error until
# we figure out what went wrong during the
# Elastic upgrade - RC 2 MAR 2019
if user
guides = guides.sort_by do |guide|
guide.compatibility_score(user)
guide.current_user_compatibility_score = guide.compatibility_score(user)
guide.current_user_compatibility_score
end
guides.reverse
else
guides
end
end
def owned_by?(current_user)
!!(current_user && user == current_user)
end
def search_data
as_json only: [:name, :overview, :crop_id, :draft, :compatibilities]
# We changed this to as_json ^ because it was causing weird nesting.
# Not sure that this should be a problem though, it's been filed:
# https://github.com/ankane/searchkick/issues/595
# {
# name: name,
# overview: overview,
# crop_id: crop_id,
# draft: draft,
# compatibilities: compatibilities
# }
end
def compatibilities
return @compatibilities if defined?(@compatibilities)
@compatibilities = []
User.includes(:gardens).each do |user|
@compatibilities << {
user_id: user.id.to_s, score: compatibility_score(user).to_i,
}
end
@compatibilities
end
def basic_needs(current_user)
return nil unless current_user
return nil if current_user.gardens.blank?
first_garden = current_user.gardens.first
# We should probably store these in the DB
basic_needs = [{ name: "Sun / Shade",
slug: "sun-shade",
overlap: [],
total: [],
percent: 0,
user: first_garden.average_sun,
garden: first_garden.name }, {
name: "Location",
slug: "location",
overlap: [],
total: [],
percent: 0,
user: first_garden.type,
garden: first_garden.name,
}, {
name: "Soil Type",
slug: "soil",
overlap: [],
total: [],
percent: 0,
user: first_garden.soil_type,
garden: first_garden.name,
}, {
name: "Practices",
slug: "practices",
overlap: [],
total: [],
percent: 0,
user: first_garden.growing_practices,
garden: first_garden.name,
}]
# Still have to implement:
# pH Range, Temperature, Water Use, Practices,
# Time Commitment, Physical Ability, Time of Year
find_overlap_in basic_needs
end
def compatibility_score(current_user)
return current_user_compatibility_score if current_user_compatibility_score
return nil unless current_user
return nil if current_user.gardens.blank?
count = 0
sum = basic_needs(current_user).inject(0) do |memo, n|
count += 1
n[:percent] ? memo + n[:percent] : memo
end
(sum.to_f / count * 100).round
end
def compatibility_label(current_user)
if current_user_compatibility_score
score = current_user_compatibility_score
else
score = compatibility_score(current_user)
end
if score.nil?
return ""
elsif score > 75
return "high"
elsif score > 50
return "medium"
else
return "low"
end
end
protected
# TODO: Fairly simplistic. This should be expanded to somehow take into
# consideration stages and selected practices
def calculate_completeness_score
total = 0.0
counted = 0.0
fields.keys.each do |key|
total += 1
if self[key]
counted += 1
end
end
write_attributes(completeness_score: counted / total)
end
# TODO: Fairly simplistic. Should probably be normalized properly.
# Right now normalization is based on the highest impressions, and how
# this one stacks up. It should probably also take into consideration
# How many gardens this thing is in.
def calculate_popularity_score
top_guide = (Guide.order_by("impressions_field" => :asc).last)
at_most = (top_guide.impressions_field || 0).to_i
normalized = impressions_field.to_f / at_most
write_attributes(popularity_score: normalized)
end
private
# For each stage, find the overlapping needs,
# and then calculate the percentage overlap of each need
# ToDO this can be cleaned up, probably significantly
# by externalizing the basic_need structure.
def find_overlap_in(basic_needs)
stages.each do |stage|
basic_needs.each do |need|
# This is bad structure
if need[:name] == "Sun / Shade"
build_overlap_and_total need, stage.light
end
if need[:name] == "Location"
build_overlap_and_total need, stage.environment
end
if need[:name] == "Soil Type"
build_overlap_and_total need, stage.soil
end
end
end
calculate_percents basic_needs
end
def build_overlap_and_total(need_hash, stage_req)
if stage_req
new_array = stage_req.select { |req| req == need_hash[:user] }
need_hash[:overlap] += new_array
need_hash[:total] = need_hash[:total] + stage_req
end
end
def calculate_percents(basic_needs)
basic_needs.each do |need|
if need[:total] && !need[:total].blank?
need[:percent] = need[:overlap].size.to_f / need[:total].size
else
need[:percent] = 0
end
# Compress the total array now that we don't need it's length
need[:total].uniq!
end
end
end