1
0
Fork 0
forked from OpenNeo/impress
impress/app/controllers/closet_hangers_controller.rb
Matchu 7948974949 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

281 lines
8.7 KiB
Ruby

class ClosetHangersController < ApplicationController
before_action :authorize_user!, :only => [:destroy, :create, :update, :update_quantities, :petpage]
before_action :find_item, :only => [:create, :update_quantities]
before_action :find_user, :only => [:index, :petpage, :update_quantities]
def destroy
if params[:list_id]
@closet_list = current_user.find_closet_list_by_id_or_null_owned params[:list_id]
@closet_list.hangers.destroy_all
respond_to do |format|
format.html {
flash[:notice] = t("closet_hangers.destroy_all.success")
redirect_back!(user_closet_hangers_path(current_user))
}
format.json { render :json => true }
end
elsif params[:ids]
ClosetHanger.transaction do
current_user.closet_hangers.where(id: params[:ids]).destroy_all
end
render json: true
else
@closet_hanger = current_user.closet_hangers.find params[:id]
@closet_hanger.destroy
@item = @closet_hanger.item
closet_hanger_destroyed
end
end
def index
is_user = user_signed_in? && current_user == @user
@public_perspective = params.has_key?(:public) || !is_user
@perspective_user = current_user unless @public_perspective
closet_lists = @user.closet_lists
unless @perspective_user == @user
# If we run this when the user matches, we'll end up with effectively:
# WHERE belongs_to_user AND (is_public OR belongs_to_user)
# and it's a bit silly to put the SQL server through a condition that's
# always true.
closet_lists = closet_lists.visible_to(@perspective_user)
end
@closet_lists_by_owned = find_closet_lists_by_owned(closet_lists)
visible_groups = @user.closet_hangers_groups_visible_to(@perspective_user)
@unlisted_closet_hangers_by_owned = find_unlisted_closet_hangers_by_owned(visible_groups)
items = []
@closet_lists_by_owned.each do |owned, lists|
lists.each do |list|
list.hangers.each do |hanger|
items << hanger.item
end
end
end
@unlisted_closet_hangers_by_owned.each do |owned, hangers|
hangers.each do |hanger|
items << hanger.item
end
end
if @public_perspective && user_signed_in?
current_user.assign_closeted_to_items!(items)
end
@campaign = Campaign.current
end
def petpage
# Find all closet lists, and also the hangers of the visible closet lists
closet_lists = @user.closet_lists.select([:id, :name, :hangers_owned]).alphabetical
if params[:filter]
# If user specified which lists should be visible, restrict to those
if params[:lists] && params[:lists].respond_to?(:keys)
visible_closet_lists = closet_lists.where(:id => params[:lists].keys)
else
visible_closet_lists = []
end
else
# Otherwise, default to public lists
visible_closet_lists = closet_lists.publicly_visible
end
@closet_lists_by_owned = closet_lists.group_by(&:hangers_owned)
@visible_closet_lists_by_owned = find_closet_lists_by_owned(visible_closet_lists)
# Find which groups (own/want) should be visible
if params[:filter]
# If user specified which groups should be visible, restrict to those
# (must be either true or false)
@visible_groups = []
if params[:groups] && params[:groups].respond_to?(:keys)
@visible_groups << true if params[:groups].keys.include?('true')
@visible_groups << false if params[:groups].keys.include?('false')
end
else
# Otherwise, default to public groups
@visible_groups = @user.public_closet_hangers_groups
end
@visible_unlisted_closet_hangers_by_owned =
find_unlisted_closet_hangers_by_owned(@visible_groups)
end
def create
@closet_hanger = current_user.closet_hangers.build(closet_hanger_params)
@closet_hanger.item = @item
if @closet_hanger.save
closet_hanger_saved
else
closet_hanger_invalid
end
end
def update
if params[:ids]
ClosetHanger.transaction do
@closet_hangers = current_user.closet_hangers.includes(:list).find params[:ids]
@closet_hangers.each do |h|
h.possibly_null_list_id = params[:list_id]
h.save!
end
end
redirect_back!(user_closet_hangers_path(current_user))
else
@closet_hanger = current_user.closet_hangers.find(params[:id])
@closet_hanger.attributes = closet_hanger_params
@item = @closet_hanger.item
unless @closet_hanger.quantity == 0 # save the hanger, new record or not
if @closet_hanger.save
closet_hanger_saved
else
closet_hanger_invalid
end
else # delete the hanger since the user doesn't want it
@closet_hanger.destroy
closet_hanger_destroyed
end
end
end
def update_quantities
begin
ClosetHanger.transaction do
params[:quantity].each do |key, quantity|
ClosetHanger.set_quantity!(quantity, :user_id => @user.id,
:item_id => @item.id, :key => key)
end
flash[:notice] = t('closet_hangers.update_quantities.success',
:item_name => @item.name)
end
rescue ActiveRecord::RecordInvalid => e
flash[:alert] = t('closet_hangers.update_quantities.invalid',
:errors => e.message)
end
redirect_to @item
end
private
def closet_hanger_params
params.require(:closet_hanger).permit(:list_id, :owned, :quantity)
end
def closet_hanger_destroyed
respond_to do |format|
format.html {
ownership_key = @closet_hanger.owned? ? 'owned' : 'wanted'
flash[:notice] = t("closet_hangers.destroy.success.#{ownership_key}",
:item_name => @item.name)
redirect_back!(@item)
}
format.json { render :json => true }
end
end
def closet_hanger_invalid
respond_to do |format|
format.html {
ownership_key = @closet_hanger.owned? ? 'owned' : 'wanted'
flash[:alert] = t("closet_hangers.create.invalid.#{ownership_key}",
:item_name => @item.name,
:errors => @closet_hanger.errors.full_messages.to_sentence)
redirect_back!(@item)
}
format.json { render :json => {:errors => @closet_hanger.errors.full_messages}, :status => :unprocessable_entity }
end
end
def closet_hanger_saved
respond_to do |format|
format.html {
ownership_key = @closet_hanger.owned? ? 'owned' : 'wanted'
if @closet_hanger.list
flash[:notice] = t("closet_hangers.create.success.#{ownership_key}.in_list",
:item_name => @item.name,
:list_name => @closet_hanger.list.name,
:count => @closet_hanger.quantity)
else
flash[:notice] = t("closet_hangers.create.success.#{ownership_key}.unlisted",
:item_name => @item.name,
:count => @closet_hanger.quantity)
end
redirect_back!(@item)
}
format.json { render :json => true }
end
end
def find_item
@item = Item.find params[:item_id]
end
def find_user
if params[:user_id]
@user = User.find params[:user_id]
elsif user_signed_in?
redirect_to user_closet_hangers_path(current_user)
else
redirect_to new_auth_user_session_path(:return_to => request.fullpath)
end
end
def find_closet_lists_by_owned(lists_scope)
return {} if lists_scope == []
lists = lists_scope.alphabetical
ClosetList.preload_items(
lists,
hangers_scope: hangers_scope,
items_scope: items_scope,
item_translations_scope: item_translations_scope,
)
lists.group_by(&:hangers_owned)
end
def find_unlisted_closet_hangers_by_owned(visible_groups)
unless visible_groups.empty?
hangers = @user.closet_hangers.unlisted.
owned_before_wanted.alphabetical_by_item_name.
where(:owned => [visible_groups])
ClosetHanger.preload_items(
hangers,
items_scope: items_scope,
item_translations_scope: item_translations_scope,
)
hangers.group_by(&:owned)
else
{}
end
end
def hangers_scope
ClosetHanger.select(:id, :item_id, :list_id, :quantity)
end
def items_scope
Item.select(:id, :thumbnail_url, :rarity_index)
end
def item_translations_scope
Item::Translation.select(:id, :item_id, :locale, :name, :description).
where(locale: I18n.locale)
end
def owned
owned = true
if closet_hanger_params
owned = case closet_hanger_params[:owned]
when 'true', '1' then true
when 'false', '0' then false
end
end
end
end