2023-08-02 16:05:02 -07:00
|
|
|
class ClosetHanger < ApplicationRecord
|
2011-07-12 16:37:16 -07:00
|
|
|
belongs_to :item
|
2024-02-20 18:42:42 -08:00
|
|
|
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
|
2024-01-19 00:00:46 -08:00
|
|
|
delegate :log_trade_activity, to: :user
|
2013-06-22 15:45:59 -07:00
|
|
|
|
2011-10-10 19:43:46 -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
|
|
|
|
|
2011-07-29 10:47:01 -07:00
|
|
|
validate :list_belongs_to_user
|
|
|
|
|
2023-07-22 14:04:01 -07:00
|
|
|
scope :alphabetical_by_item_name, -> {
|
2024-02-20 15:36:20 -08:00
|
|
|
i = Item.arel_table
|
|
|
|
joins(:item).order(i[:name].asc)
|
2013-01-28 01:01:25 -08:00
|
|
|
}
|
2024-01-19 00:00:46 -08:00
|
|
|
scope :trading, -> {
|
|
|
|
ch = arel_table
|
2024-01-19 01:27:57 -08:00
|
|
|
# sigh… our default-lists continue to be a pain
|
2024-01-19 00:00:46 -08:00
|
|
|
cl = ClosetList.arel_table
|
|
|
|
u = User.arel_table
|
2024-01-19 01:27:57 -08:00
|
|
|
joins(:user).left_outer_joins(:list).where(
|
|
|
|
ch[:list_id].not_eq(nil).and(cl[:visibility].gteq(
|
2024-01-19 00:00:46 -08:00
|
|
|
ClosetVisibility[:trading].id))
|
2024-01-19 01:27:57 -08:00
|
|
|
).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)
|
2024-01-19 00:00:46 -08:00
|
|
|
)
|
2024-01-19 01:27:57 -08:00
|
|
|
))
|
2024-01-19 00:00:46 -08:00
|
|
|
}
|
2023-07-22 14:04:01 -07:00
|
|
|
scope :newest, -> { order(arel_table[:created_at].desc) }
|
|
|
|
scope :owned_before_wanted, -> { order(arel_table[:owned].desc) }
|
|
|
|
scope :unlisted, -> { where(:list_id => nil) }
|
2024-01-19 00:38:16 -08:00
|
|
|
scope :user_is_active, -> {
|
|
|
|
u = User.arel_table
|
|
|
|
joins(:user).where(u[:last_trade_activity_at].gteq(6.months.ago))
|
|
|
|
}
|
2011-07-22 12:31:23 -07:00
|
|
|
|
2011-10-10 19:43:46 -07:00
|
|
|
before_validation :merge_quantities, :set_owned_by_list
|
2011-07-26 17:27:23 -07:00
|
|
|
|
2024-01-19 00:00:46 -08:00
|
|
|
after_save :log_trade_activity, if: :trading?
|
|
|
|
after_destroy :log_trade_activity, if: :trading?
|
|
|
|
|
2013-12-27 11:49:46 -08:00
|
|
|
def possibly_null_closet_list
|
|
|
|
list || user.null_closet_list(owned)
|
|
|
|
end
|
|
|
|
|
|
|
|
def trading?
|
|
|
|
possibly_null_closet_list.trading?
|
|
|
|
end
|
|
|
|
|
2024-01-19 00:28:28 -08:00
|
|
|
def wanted?
|
|
|
|
!owned?
|
|
|
|
end
|
|
|
|
|
2015-09-26 19:55:09 -07:00
|
|
|
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
|
|
|
|
|
2011-07-22 12:31:23 -07:00
|
|
|
def verb(subject=:someone)
|
2011-07-22 14:55:05 -07:00
|
|
|
self.class.verb(subject, owned?)
|
|
|
|
end
|
|
|
|
|
2011-07-26 15:41:15 -07:00
|
|
|
def self.verb(subject, owned, positive=true)
|
2011-07-22 14:55:05 -07:00
|
|
|
base = (owned) ? 'own' : 'want'
|
2011-07-26 17:27:23 -07:00
|
|
|
base << 's' if positive && subject != :you && subject != :i
|
2011-07-22 12:31:23 -07:00
|
|
|
base
|
|
|
|
end
|
Do preloading manually on user list pages, to reduce memory usage
I used the new profiler tools on this page, and noticed a lot of
allocations in the Globalize library, which we use for translating
database records. I realized that we were loading all of the fields of
not just all of the items on the page, but all of their translation
records in all locales! We used to scrape data for lots of languages, so
that can be quite a lot!
Unfortunately, Rails's `includes` method to efficiently preload related
records always loads all fields, and simply can't be overridden.
So, in this change we write manual preloading code, to identify the
records we need, load them in big bulk queries, and assign them back to
the appropriate associations. Basically just what `includes` does, but
written out a bit more, to give us the chance to specify SELECT and
WHERE clauses!
2023-10-27 19:42:02 -07:00
|
|
|
|
2024-02-20 15:36:20 -08:00
|
|
|
# TODO: Is the performance improvement on this actually much better than just
|
|
|
|
# `includes`, now that `Item::Translation` records aren't part of it anymore?
|
Do preloading manually on user list pages, to reduce memory usage
I used the new profiler tools on this page, and noticed a lot of
allocations in the Globalize library, which we use for translating
database records. I realized that we were loading all of the fields of
not just all of the items on the page, but all of their translation
records in all locales! We used to scrape data for lots of languages, so
that can be quite a lot!
Unfortunately, Rails's `includes` method to efficiently preload related
records always loads all fields, and simply can't be overridden.
So, in this change we write manual preloading code, to identify the
records we need, load them in big bulk queries, and assign them back to
the appropriate associations. Basically just what `includes` does, but
written out a bit more, to give us the chance to specify SELECT and
WHERE clauses!
2023-10-27 19:42:02 -07:00
|
|
|
def self.preload_items(
|
|
|
|
hangers,
|
2024-02-20 15:36:20 -08:00
|
|
|
items_scope: Item.all
|
Do preloading manually on user list pages, to reduce memory usage
I used the new profiler tools on this page, and noticed a lot of
allocations in the Globalize library, which we use for translating
database records. I realized that we were loading all of the fields of
not just all of the items on the page, but all of their translation
records in all locales! We used to scrape data for lots of languages, so
that can be quite a lot!
Unfortunately, Rails's `includes` method to efficiently preload related
records always loads all fields, and simply can't be overridden.
So, in this change we write manual preloading code, to identify the
records we need, load them in big bulk queries, and assign them back to
the appropriate associations. Basically just what `includes` does, but
written out a bit more, to give us the chance to specify SELECT and
WHERE clauses!
2023-10-27 19:42:02 -07:00
|
|
|
)
|
|
|
|
# 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
|
2024-02-20 15:36:20 -08:00
|
|
|
# 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!)
|
Do preloading manually on user list pages, to reduce memory usage
I used the new profiler tools on this page, and noticed a lot of
allocations in the Globalize library, which we use for translating
database records. I realized that we were loading all of the fields of
not just all of the items on the page, but all of their translation
records in all locales! We used to scrape data for lots of languages, so
that can be quite a lot!
Unfortunately, Rails's `includes` method to efficiently preload related
records always loads all fields, and simply can't be overridden.
So, in this change we write manual preloading code, to identify the
records we need, load them in big bulk queries, and assign them back to
the appropriate associations. Basically just what `includes` does, but
written out a bit more, to give us the chance to specify SELECT and
WHERE clauses!
2023-10-27 19:42:02 -07:00
|
|
|
hangers.each do |hanger|
|
|
|
|
hanger.association(:item).target = items_by_id[hanger.item_id]
|
|
|
|
end
|
|
|
|
end
|
2011-10-10 19:43:46 -07:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2013-01-25 08:44:15 -08:00
|
|
|
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
|
|
|
|
|
2011-10-10 19:43:46 -07:00
|
|
|
hanger.quantity = quantity
|
|
|
|
hanger.save!
|
2013-01-25 08:44:15 -08:00
|
|
|
elsif hanger
|
|
|
|
# If quantity is zero and there's a hanger, destroy it.
|
|
|
|
hanger.destroy
|
2011-10-10 19:43:46 -07:00
|
|
|
end
|
2013-01-25 08:44:15 -08:00
|
|
|
|
|
|
|
# If quantity is zero and there's no hanger, good. Do nothing.
|
2011-10-10 19:43:46 -07:00
|
|
|
end
|
2011-07-29 10:47:01 -07:00
|
|
|
|
2024-01-21 00:39:20 -08:00
|
|
|
# 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
|
|
|
|
|
2024-01-21 03:10:06 -08:00
|
|
|
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
|
|
|
|
|
2011-07-29 10:47:01 -07:00
|
|
|
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
|
2011-10-10 19:43:46 -07:00
|
|
|
|
|
|
|
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
|
2013-07-28 23:30:29 -07:00
|
|
|
# hanger. Select enough for our logic and to update flex_source.
|
2023-07-22 12:18:53 -07:00
|
|
|
# TODO: We deleted flex, does this reduce what data we need here?
|
2013-07-28 23:30:29 -07:00
|
|
|
conflicting_hanger = self.class.select([:id, :quantity, :user_id, :item_id,
|
2024-02-28 13:30:55 -08:00
|
|
|
:owned, :list_id]).
|
2011-10-10 19:43:46 -07:00
|
|
|
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
|
2011-07-29 10:47:01 -07:00
|
|
|
|
|
|
|
def set_owned_by_list
|
|
|
|
self.owned = list.hangers_owned if list
|
|
|
|
true
|
|
|
|
end
|
2011-07-12 16:37:16 -07:00
|
|
|
end
|
|
|
|
|