impress/app/models/closet_hanger.rb

201 lines
6.1 KiB
Ruby

class ClosetHanger < ApplicationRecord
belongs_to :item
belongs_to :list, class_name: 'ClosetList', optional: true
belongs_to :user
delegate :name, to: :item, prefix: true
delegate :log_trade_activity, to: :user
validates :item_id, :uniqueness => {:scope => [:user_id, :owned, :list_id]}
validates :quantity, :numericality => {:greater_than => 0}
validates_presence_of :item, :user
validate :list_belongs_to_user
scope :alphabetical_by_item_name, -> {
it = Item::Translation.arel_table
joins(:item => :translations).where(it[:locale].eq(I18n.locale)).
order(it[:name].asc)
}
scope :trading, -> {
ch = arel_table
cl = ClosetList.arel_table
u = User.arel_table
joins(:user, :list).where(
# sigh… our default-lists continue to be a pain
(
ch[:list_id].not_eq(nil).and(cl[:visibility].gteq(
ClosetVisibility[:trading].id))
).or(
(
ch[:list_id].eq(nil).and(ch[:owned].eq(true))
).and(
u[:owned_closet_hangers_visibility].gteq(
ClosetVisibility[:trading].id)
)
).or(
(
ch[:list_id].eq(nil).and(ch[:owned].eq(false))
).and(
u[:wanted_closet_hangers_visibility].gteq(
ClosetVisibility[:trading].id)
)
)
)
}
scope :newest, -> { order(arel_table[:created_at].desc) }
scope :owned_before_wanted, -> { order(arel_table[:owned].desc) }
scope :unlisted, -> { where(:list_id => nil) }
{:owned => true, :wanted => false}.each do |name, owned|
scope "#{name}_trading", -> {
joins(:user).includes(:list).
where(:owned => owned).
where((
arel_table[:list_id].eq(nil).and(
User.arel_table["#{name}_closet_hangers_visibility"].gteq(ClosetVisibility[:trading].id)
)
).or(
ClosetList.arel_table[:visibility].gteq(ClosetVisibility[:trading].id)
))
}
end
before_validation :merge_quantities, :set_owned_by_list
after_save :log_trade_activity, if: :trading?
after_destroy :log_trade_activity, if: :trading?
def possibly_null_closet_list
list || user.null_closet_list(owned)
end
def trading?
possibly_null_closet_list.trading?
end
def possibly_null_list_id=(list_id_or_owned)
if list_id_or_owned.to_s == 'true' || list_id_or_owned.to_s == 'false'
self.list_id = nil
self.owned = list_id_or_owned
else
self.list_id = list_id_or_owned
# owned is set in the set_owned_by_list hook
end
end
def verb(subject=:someone)
self.class.verb(subject, owned?)
end
def self.verb(subject, owned, positive=true)
base = (owned) ? 'own' : 'want'
base << 's' if positive && subject != :you && subject != :i
base
end
def self.preload_items(
hangers,
items_scope: Item.all,
item_translations_scope: Item::Translation.all
)
# Preload the records we need. (This is like `includes`, but `includes`
# always selects all fields for all records, and we give the caller the
# opportunity to specify which fields it actually wants via scope!)
items = items_scope.where(id: hangers.map(&:item_id))
translations = item_translations_scope.where(item_id: items.map(&:id))
# Group the records by relevant IDs.
translations_by_item_id = translations.group_by(&:item_id)
items_by_id = items.to_h { |i| [i.id, i] }
# Assign the preloaded records to the records they belong to. (This is like
# doing e.g. i.translations = ..., but that's a database write - we
# actually just want to set the `translations` field itself directly!
# Hacky, ripped from how `ActiveRecord::Associations::Preloader` does it!)
items.each do |item|
item.association(:translations).target = translations_by_item_id[item.id]
end
hangers.each do |hanger|
hanger.association(:item).target = items_by_id[hanger.item_id]
end
end
def self.set_quantity!(quantity, options)
quantity = quantity.to_i
conditions = {:user_id => options[:user_id].to_i,
:item_id => options[:item_id].to_i}
if options[:key] == "true"
conditions[:owned] = true
conditions[:list_id] = nil
elsif options[:key] == "false"
conditions[:owned] = false
conditions[:list_id] = nil
else
conditions[:list_id] = options[:key].to_i
end
hanger = self.where(conditions).first
if quantity > 0
# If quantity is non-zero, create/update the corresponding hanger.
unless hanger
hanger = self.new
hanger.user_id = conditions[:user_id]
hanger.item_id = conditions[:item_id]
# One of the following will be nil, and that's okay. If owned is nil,
# we'll cover for it before validation, as always.
hanger.owned = conditions[:owned]
hanger.list_id = conditions[:list_id]
end
hanger.quantity = quantity
hanger.save!
elsif hanger
# If quantity is zero and there's a hanger, destroy it.
hanger.destroy
end
# If quantity is zero and there's no hanger, good. Do nothing.
end
protected
def list_belongs_to_user
if list_id?
if list
errors.add(:list_id, "must belong to you") unless list.user_id == user_id
else
errors.add(:list, "must exist")
end
end
end
def merge_quantities
# Find a hanger that conflicts: for the same item, in the same user's
# closet, same owned status, same list. It also must not be the current
# hanger. Select enough for our logic and to update flex_source.
# TODO: We deleted flex, does this reduce what data we need here?
conflicting_hanger = self.class.select([:id, :quantity, :user_id, :item_id,
:owned]).
where(:user_id => user_id, :item_id => item_id, :owned => owned,
:list_id => list_id).where(['id != ?', self.id]).first
# If there is such a hanger, remove it and merge its quantity into this one.
if conflicting_hanger
self.quantity += conflicting_hanger.quantity
conflicting_hanger.destroy
end
true
end
def set_owned_by_list
self.owned = list.hangers_owned if list
true
end
end