class Recipe < Ingredient include DefaultValues include TokenizedLike has_many :recipe_ingredients, -> { order :sort_order }, inverse_of: :recipe, dependent: :destroy belongs_to :user has_and_belongs_to_many :tags, autosave: true scope :undeleted, -> { where('deleted <> ? OR deleted IS NULL', true) } scope :not_log, -> { where('is_log <> ? OR is_log IS NULL', true) } scope :active, -> { undeleted.not_log } scope :is_ingredient, -> { where(is_ingredient: true) } accepts_nested_attributes_for :recipe_ingredients, allow_destroy: true default_values is_ingredient: false validates :name, presence: true validates :total_time, numericality: true, allow_blank: true validates :active_time, numericality: true, allow_blank: true validate :validate_is_ingredient attr_accessor :converted_scale, :converted_system, :converted_unit def cache_key [ 'recipes', self.id.to_s, converted_scale || '-', converted_system || '-', converted_unit || '-' ].join('/') end def validate_is_ingredient if self.is_ingredient if self.recipe_ingredients.any? { |ri| !ri.recipe_as_ingredient_id.nil? } errors[:base] << 'A recipe marked as an ingredient cannot contain recipe ingredients' end end end def scale(factor, auto_unit = false) self.converted_scale = factor recipe_ingredients.each do |ri| ri.scale(factor, auto_unit) end self end def convert_to_metric self.converted_system = 'metric' recipe_ingredients.each do |ri| ri.to_metric end self end def convert_to_standard self.converted_system = 'standard' recipe_ingredients.each do |ri| ri.to_standard end self end def convert_to_mass self.converted_unit = 'mass' recipe_ingredients.each do |ri| ri.to_mass end self end def convert_to_volume self.converted_unit = 'volume' recipe_ingredients.each do |ri| ri.to_volume end self end def yields=(val) @yields_list = nil super end def yields_list @yields_list ||= self.yields.to_s.split(',').concat(['1 each']).map { |y| y.strip }.select { |y| y.present? }.map do |y| begin vu = UnitConversion::parse(y) if vu.unit.nil? vu = UnitConversion::ValueUnit.for(vu.value, 'servings') end vu rescue UnitConversion::UnparseableUnitError nil end end.compact end def tag_names self.tags.map { |t| t.name } end def tag_names=(names) names = Array.wrap(names).map { |n| n.to_s }.select { |n| n.length > 0 } existing_tags = Tag.by_name(names) new_tags = names.select { |n| existing_tags.none? { |t| t.is?(n) } } self.tags = existing_tags new_tags.each do |n| self.tags << Tag.new(name: n) end end def nutrition_data(recalculate = false) if recalculate || @nutrition_data.nil? @nutrition_data = calculate_nutrition_data end @nutrition_data end def nutrition_errors nutrition_data.errors end def update_rating! logs = Log.for_recipe(self).for_user(self.user_id).where('rating IS NOT NULL') if logs.count > 0 self.rating = logs.average(:rating) save(validate: false) else self.rating = nil end self.rating end # Creates a copy of this recipe suitable for associating to a log def log_copy(user) copy = Recipe.new copy.user = user copy.is_log = true copy.name = self.name copy.description = self.description copy.source = self.source copy.yields = self.yields copy.total_time = self.total_time copy.active_time = self.active_time copy.step_text = self.step_text self.recipe_ingredients.each do |ri| copy.recipe_ingredients << ri.log_copy end copy end def density mas = yields_list.detect { |y| y.mass? } vol = yields_list.detect { |y| y.volume? } if mas && vol "#{mas.value.value / vol.value.value} #{mas.unit.original_unit}/#{vol.unit.original_unit}" else nil end end def custom_units arbitrary = self.yields_list.select { |y| !y.mass? && !y.volume? } mass = self.yields_list.select { |y| y.mass? } volume = self.yields_list.select { |y| y.volume? } primary_unit = mass.first || volume.first if primary_unit cus = {} arbitrary.each do |y| ratio = 1 if y.value.value != 1 ratio = 1.0 / y.value.value end cus[y.unit.unit] = primary_unit.scale(ratio) end cus else {} end end def nutrition_unit UnitConversion.parse('1 each') end def self.for_criteria(criteria) query = self.active.order(criteria.sort_column => criteria.sort_direction) if criteria.name.present? query = query.matches_tokens(:name, criteria.name.split(' ')) end if criteria.tags.present? tags = Tag.by_name(criteria.tags.split) query = query.where(id: tags.joins(:recipes).pluck('recipes.id')) end query.page(criteria.page).per(criteria.per) end def self.search_by_name(query) tokens = query.to_s.split(' ') if tokens.empty? self.none else self.matches_tokens(:name, tokens) end end private def calculate_nutrition_data NutritionData.new(recipe_ingredients) end end