impress/app/models/closet_hanger.rb

225 lines
6.9 KiB
Ruby
Raw Permalink Normal View History

class ClosetHanger < ApplicationRecord
2011-07-12 16:37:16 -07:00
belongs_to :item
belongs_to :list, class_name: 'ClosetList', optional: true, touch: true
2011-07-12 16:37:16 -07:00
belongs_to :user
2013-06-22 15:45:59 -07:00
delegate :name, to: :item, prefix: true
delegate :log_trade_activity, to: :user
2013-06-22 15:45:59 -07:00
validates :item_id, :uniqueness => {:scope => [:user_id, :owned, :list_id]}
2011-07-14 09:50:24 -07:00
validates :quantity, :numericality => {:greater_than => 0}
validates_presence_of :item, :user
validate :list_belongs_to_user
scope :alphabetical_by_item_name, -> {
i = Item.arel_table
joins(:item).order(i[:name].asc)
}
scope :trading, -> {
ch = arel_table
# sigh… our default-lists continue to be a pain
cl = ClosetList.arel_table
u = User.arel_table
joins(:user).left_outer_joins(:list).where(
ch[:list_id].not_eq(nil).and(cl[:visibility].gteq(
ClosetVisibility[:trading].id))
).or(where(
(
ch[:list_id].eq(nil).and(ch[:owned].eq(true))
).and(
u[:owned_closet_hangers_visibility].gteq(
ClosetVisibility[:trading].id)
)
)).or(where(
(
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) }
scope :user_is_active, -> {
u = User.arel_table
joins(:user).where(u[:last_trade_activity_at].gteq(6.months.ago))
}
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 wanted?
!owned?
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)
2011-07-22 14:55:05 -07:00
self.class.verb(subject, owned?)
end
def self.verb(subject, owned, positive=true)
2011-07-22 14:55:05 -07:00
base = (owned) ? 'own' : 'want'
base << 's' if positive && subject != :you && subject != :i
base
end
# TODO: Is the performance improvement on this actually much better than just
# `includes`, now that `Item::Translation` records aren't part of it anymore?
def self.preload_items(
hangers,
items_scope: Item.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))
# Group the records by relevant IDs.
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. h.item = ..., but that's a database write - we actually just
# want to set the `item` field itself directly! Hacky, ripped from how
# `ActiveRecord::Associations::Preloader` does it!)
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
# Use this with a scoped relation to convert it into a list of trades, e.g.
# `item.hangers.trading.to_trades`.
#
# A trade includes the user who's trading, and the available closet hangers
# (which you can use to get e.g. the list name).
#
# We don't preload anything here - if you want user names or list names, you
# should `includes` them in the hanger scope first, to avoid extra queries!
def self.to_trades
# Let's ensure that the `trading` filter is applied, to avoid data leaks.
# (I still recommend doing it at the call site too for clarity, though!)
all_trading_hangers = trading.to_a
owned_hangers = all_trading_hangers.filter(&:owned?)
wanted_hangers = all_trading_hangers.filter(&:wanted?)
# Group first into offering vs seeking, then by user.
offering, seeking = [owned_hangers, wanted_hangers].map do |hangers|
hangers.group_by(&:user_id).map do |user_id, user_hangers|
Trade.new(user_id, user_hangers)
end
end
{offering: offering, seeking: seeking}
end
Trade = Struct.new('Trade', :user_id, :hangers) do
def user
# Take advantage of `includes(:user)` on the hangers, if applied.
hangers.first.user
end
def lists
hangers.map(&:list).filter(&:present?)
end
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, :list_id]).
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
2011-07-12 16:37:16 -07:00
end