impress/app/models/closet_page.rb

237 lines
6.8 KiB
Ruby
Raw Normal View History

2011-07-12 16:37:16 -07:00
require 'yaml'
class ClosetPage
2011-07-12 17:51:43 -07:00
include ActiveModel::Conversion
extend ActiveModel::Naming
2011-08-02 19:42:56 -07:00
@selectors = {
2011-07-12 16:37:16 -07:00
:items => "form[action=\"process_closet.phtml\"] tr[bgcolor!=silver][bgcolor!=\"#E4E4E4\"]",
:item_thumbnail => "img",
:item_name => "td:nth-child(2)",
:item_quantity => "td:nth-child(5)",
:item_remove => "input",
:page_select => "select[name=page]",
:selected => "option[selected]"
}
2011-07-12 21:25:14 -07:00
attr_accessor :index
2013-04-09 13:50:33 -07:00
attr_reader :hangers, :list_id, :source, :total_pages, :unknown_item_names, :user
2011-07-12 16:37:16 -07:00
def initialize(user)
raise ArgumentError, "Expected #{user.inspect} to be a User", caller unless user.is_a?(User)
@user = user
end
2011-07-12 17:51:43 -07:00
def last?
@index == @total_pages
end
2011-08-02 19:42:56 -07:00
def name
2012-12-30 14:07:29 -08:00
I18n.translate('neopets_pages.names.closet')
2011-08-02 19:42:56 -07:00
end
2011-07-12 17:51:43 -07:00
def persisted?
false
end
2011-07-12 16:37:16 -07:00
def save_hangers!
counts = {created: 0, updated: 0}
2011-07-12 17:51:43 -07:00
ClosetHanger.transaction do
@hangers.each do |hanger|
if hanger.new_record?
counts[:created] += 1
hanger.save!
elsif hanger.changed?
counts[:updated] += 1
hanger.save!
end
end
end
counts
2011-07-12 16:37:16 -07:00
end
2013-04-09 13:50:33 -07:00
def list_id=(list_id)
@list_id = list_id
if list_id == 'true'
@closet_list = nil
@hangers_owned = true
elsif list_id == 'false'
@closet_list = nil
@hangers_owned = false
elsif list_id.present?
@closet_list = @user.closet_lists.find(list_id)
@hangers_owned = @closet_list.hangers_owned?
end
end
2011-07-12 16:37:16 -07:00
def source=(source)
2011-07-12 17:51:43 -07:00
@source = source
2011-07-12 16:37:16 -07:00
parse_source!(source)
end
2011-07-12 21:25:14 -07:00
def url
"http://www.neopets.com/closet.phtml?per_page=50&page=#{@index}"
end
2011-07-12 16:37:16 -07:00
protected
def element(selector_name, parent)
2011-08-02 19:42:56 -07:00
parent.at_css(self.class.selectors[selector_name]) ||
raise(ParseError, "#{selector_name} element not found")
2011-07-12 16:37:16 -07:00
end
def elements(selector_name, parent)
2011-08-02 19:42:56 -07:00
parent.css(self.class.selectors[selector_name])
end
def find_id(row)
element(:item_remove, row)['name']
end
def find_index(page_selector)
element(:selected, page_selector)['value'].to_i
end
def find_items(doc)
elements(:items, doc)
end
def find_name(row)
# For normal items, the td contains essentially:
# <b>NAME<br/><span>OPTIONAL ADJECTIVE</span></b>
# For PB items, the td contains:
# NAME<br/><span>OPTIONAL ADJECTIVE</span>
# So, we want the first text node. If it's a PB item, that's the first
# child. If it's a normal item, it's the first child <b>'s child.
name_el = element(:item_name, row).children[0]
name_el = name_el.children[0] if name_el.name == 'b'
name_el.text
end
def find_page_selector(doc)
element(:page_select, doc)
end
def find_quantity(row)
element(:item_quantity, row).text.to_i
end
def find_thumbnail_url(row)
element(:item_thumbnail, row)['src']
end
def find_total_pages(page_selector)
page_selector.children.size
2011-07-12 16:37:16 -07:00
end
def parse_source!(source)
doc = Nokogiri::HTML(source)
2011-08-02 19:42:56 -07:00
page_selector = find_page_selector(doc)
@total_pages = find_total_pages(page_selector)
@index = find_index(page_selector)
2011-07-12 16:37:16 -07:00
items_data = {
:id => {},
:thumbnail_url => {}
}
2011-08-02 19:42:56 -07:00
# Go through the items, and find the ID/thumbnail for each and data with it
find_items(doc).each do |row|
2011-07-12 16:37:16 -07:00
data = {
2011-08-02 19:42:56 -07:00
:name => find_name(row),
:quantity => find_quantity(row)
2011-07-12 16:37:16 -07:00
}
2011-08-02 19:42:56 -07:00
if id = find_id(row)
2011-07-12 16:37:16 -07:00
id = id.to_i
items_data[:id][id] = data
else # if this is a pb item, which does not give ID, go by thumbnail
2011-08-02 19:42:56 -07:00
thumbnail_url = find_thumbnail_url(row)
2011-07-12 16:37:16 -07:00
items_data[:thumbnail_url][thumbnail_url] = data
end
end
# Find items with either a matching ID or matching thumbnail URL
# Check out that single-query beauty :)
i = Item.arel_table
items = Item.where(
i[:id].in(items_data[:id].keys).
or(
i[:thumbnail_url].in(items_data[:thumbnail_url].keys)
)
)
# And now for some more single-query beauty: check for existing hangers.
# We don't want to insert duplicate hangers of what a user owns if they
# already have it in another list (e.g. imports to Items You Own *after*
# curating their Up For Trade list), so we check for the hanger's presence
# in *all* items the user owns or wants (whichever is appropriate for this
# request).
hangers_scope = @user.closet_hangers.where(owned: @hangers_owned)
# Group existing hangers by item ID and whether they're from the current
# list or another list.
current_list_id = @closet_list.try(:id)
existing_hangers_by_item_id = hangers_scope.
where(item_id: items.map(&:id)).
group_by(&:item_id)
# Careful! We're just using a single default empty list for performance,
# but it must not be mutated! If mutation becomes necessary, change this
# to a default_proc assignment.
existing_hangers_by_item_id.default = []
# Create closet hanger from each item, and remove them from the reference
# lists.
2011-07-12 16:37:16 -07:00
@hangers = items.map do |item|
data = items_data[:id].delete(item.id) ||
items_data[:thumbnail_url].delete(item.thumbnail_url)
# If there's a hanger in the current list, we want it so we can update
# its quantity. If there's a hanger in another list, we want it so we
# know not to build a new one. Otherwise, build away!
existing_hangers = existing_hangers_by_item_id[item.id]
existing_hanger_in_current_list = existing_hangers.detect { |h|
h.list_id == current_list_id
}
hanger = existing_hanger_in_current_list || existing_hangers.first ||
hangers_scope.build
# We also don't want to move existing hangers from other lists, so only
# set the list if the hanger is new. (The item assignment is only
# necessary for new records, so may as well put it here, too.)
if hanger.new_record?
hanger.item = item
hanger.list = @closet_list
end
# Finally, we don't want to update the quantity of hangers in those other
# lists, either, so only update quantity if it's in this list. (This will
# be true for some existing hangers and all new hangers. This is also the
# only value that could change for existing hangers; if nothing changes,
# it was an existing hanger from another list.)
hanger.quantity = data[:quantity] if hanger.list_id == current_list_id
2011-07-12 16:37:16 -07:00
hanger
end
# Take the names of the items remaining in the reference lists, meaning
# that they weren't found
@unknown_item_names = []
items_data.each do |type, data_by_key|
data_by_key.each do |key, data|
@unknown_item_names << data[:name]
end
end
end
2011-08-02 19:42:56 -07:00
def self.selectors
@selectors
end
2011-07-12 16:37:16 -07:00
class ParseError < RuntimeError;end
end