diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index 3704f63c..4aff95a7 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -13,13 +13,14 @@ class ItemsController < ApplicationController end # Note that we sort by name by hand, since we might have to use # fallbacks after the fact - # TODO: use proxies for everything! - output_format = params[:format] == :html ? :records : :proxies @items = Item::Search::Query.from_text(@query, current_user). - paginate(page: params[:page], per_page: per_page, as: output_format) + paginate(page: params[:page], per_page: per_page, as: :proxies) assign_closeted! respond_to do |format| - format.html { render } + format.html { + @items.prepare_partial(:item_link_partial) + render + } format.json { @items.prepare_method(:as_json) render json: {items: @items, total_pages: @items.total_pages} diff --git a/app/models/item.rb b/app/models/item.rb index 4ee78eba..1698a8bb 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -288,6 +288,12 @@ class Item < ActiveRecord::Base translatable_locales - translated_locales end + def method_cached?(method_name) + # No methods are cached on a full item. This is for duck typing with item + # proxies. + false + end + def self.all_by_ids_or_children(ids, swf_assets) swf_asset_ids = [] swf_assets_by_id = {} diff --git a/app/models/item/proxy.rb b/app/models/item/proxy.rb index f90e3464..0ac4e30f 100644 --- a/app/models/item/proxy.rb +++ b/app/models/item/proxy.rb @@ -3,33 +3,47 @@ class Item include FragmentLocalization attr_reader :id - attr_writer :item + attr_writer :item, :owned, :wanted + + delegate :description, :name, :nc?, :thumbnail_url, to: :item def initialize(id) @id = id - @known_method_outputs = {} - end - - def method_cached?(method_name) - # TODO: is there a way to cache nil? Right now we treat is as a miss. - # We eagerly read the cache rather than just check if the value exists, - # which will usually cut down on cache requests. - @known_method_outputs[method_name] ||= Rails.cache.read( - method_fragment_key(method_name)) - !@known_method_outputs[method_name].nil? + @known_outputs = {method: {}, partial: {}} end def as_json(options={}) cache_method(:as_json) end + def cached?(type, name) + # TODO: is there a way to cache nil? Right now we treat is as a miss. + # We eagerly read the cache rather than just check if the value exists, + # which will usually cut down on cache requests. + @known_outputs[type][name] ||= Rails.cache.read(fragment_key(type, name)) + !@known_outputs[type][name].nil? + end + + def owned? + @owned + end + + def to_partial_path + # HACK: could break without warning! + Item._to_partial_path + end + + def wanted? + @wanted + end + private def cache_method(method_name, &block) # Two layers of cache: a local copy, in case the method is called again, # and then the Rails cache, before we hit the actual method call. - @known_method_outputs[method_name] ||= begin - key = method_fragment_key(method_name) + @known_outputs[method_name] ||= begin + key = fragment_key(:method, method_name) Rails.cache.fetch(key) { item.send(method_name) } end end @@ -38,8 +52,10 @@ class Item @item ||= Item.find(@id) end - def method_fragment_key(method_name) - localize_fragment_key("item/#{@id}##{method_name}", I18n.locale) + def fragment_key(type, name) + prefix = type == :partial ? 'views/' : '' + base = localize_fragment_key("items/#{@id}##{name}", I18n.locale) + prefix + base end end end \ No newline at end of file diff --git a/app/models/item/proxy_array.rb b/app/models/item/proxy_array.rb index 03ce5a66..2f707587 100644 --- a/app/models/item/proxy_array.rb +++ b/app/models/item/proxy_array.rb @@ -1,16 +1,37 @@ class Item class ProxyArray < Array - METHOD_SCOPES = {as_json: Item.includes(:translations)} + # TODO: do we really need to include translations? The search documents + # know the proper name for each locale, so proxies can tell their + # parent items what their names are and save the query entirely. + SCOPES = HashWithIndifferentAccess.new({ + method: { + as_json: Item.includes(:translations), + }, + partial: { + item_link_partial: Item.includes(:translations) + } + }) def initialize(ids) self.replace(ids.map { |id| Proxy.new(id.to_i) }) end - def prepare_method(method_name) + def prepare_method(name) + prepare(:method, name) + end + + def prepare_partial(name) + prepare(:partial, name) + end + + private + + def prepare(type, name) missed_proxies_by_id = self. - reject { |p| p.method_cached?(method_name) }. + reject { |p| p.cached?(type, name) }. index_by(&:id) - item_scope = METHOD_SCOPES[method_name.to_sym] || Item.scoped + item_scope = SCOPES[type][name] + raise "unexpected #{type} #{name.inspect}" unless item_scope item_scope.find(missed_proxies_by_id.keys).each do |item| missed_proxies_by_id[item.id].item = item end diff --git a/app/models/user.rb b/app/models/user.rb index 730d8619..c1fda667 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -57,11 +57,7 @@ class User < ActiveRecord::Base # Assigning these items to a hash by ID means that we don't have to go # N^2 searching the items list for items that match the given IDs or vice # versa, and everything stays a lovely O(n) - items_by_id = {} - items.each do |item| - items_by_id[item.id] ||= [] - items_by_id[item.id] << item - end + items_by_id = items.group_by(&:id) closet_hangers.where(:item_id => items_by_id.keys).each do |hanger| items = items_by_id[hanger.item_id] items.each do |item|