diff --git a/Gemfile b/Gemfile index d2f2e627..ab837d3b 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,8 @@ gem 'right_aws', '~> 2.1.0' gem "character-encodings", "~> 0.4.1", :platforms => :ruby_18 +gem "nokogiri", "~> 1.5.0" + group :development_async do # async wrappers gem 'eventmachine', :git => 'git://github.com/eventmachine/eventmachine.git' diff --git a/Gemfile.lock b/Gemfile.lock index 4fd0dbbf..1554d6b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,7 @@ GEM mime-types (1.16) msgpack (0.4.4) mysql2 (0.2.6) + nokogiri (1.5.0) openneo-auth-signatory (0.1.0) ruby-hmac polyglot (0.3.1) @@ -204,6 +205,7 @@ DEPENDENCIES msgpack (~> 0.4.3) mysql2 mysqlplus! + nokogiri (~> 1.5.0) openneo-auth-signatory (~> 0.1.0) rack-fiber_pool rails (= 3.0.4) diff --git a/app/models/closet_hanger.rb b/app/models/closet_hanger.rb new file mode 100644 index 00000000..1129cf63 --- /dev/null +++ b/app/models/closet_hanger.rb @@ -0,0 +1,7 @@ +class ClosetHanger < ActiveRecord::Base + belongs_to :item + belongs_to :user + + scope :alphabetical_by_item_name, joins(:item).order(Item.arel_table[:name]) +end + diff --git a/app/models/closet_page.rb b/app/models/closet_page.rb new file mode 100644 index 00000000..d9de7430 --- /dev/null +++ b/app/models/closet_page.rb @@ -0,0 +1,110 @@ +require 'yaml' + +class ClosetPage + SELECTORS = { + :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]" + } + + attr_reader :hangers, :index, :total_pages, :unknown_item_names + + def initialize(user) + raise ArgumentError, "Expected #{user.inspect} to be a User", caller unless user.is_a?(User) + @user = user + end + + def save_hangers! + @hangers.each(&:save!) + end + + def source=(source) + parse_source!(source) + end + + protected + + def element(selector_name, parent) + parent.at_css(SELECTORS[selector_name]) || + raise(ParseError, "Closet #{selector_name} element not found in #{parent.inspect}") + end + + def elements(selector_name, parent) + parent.css(SELECTORS[selector_name]) + end + + def parse_source!(source) + doc = Nokogiri::HTML(source) + + page_selector = element(:page_select, doc) + @total_pages = page_selector.children.size + @index = element(:selected, page_selector)['value'] + + items_data = { + :id => {}, + :thumbnail_url => {} + } + + # Go through the items, and find the ID/thumbnail for each and data with it + elements(:items, doc).each do |row| + # For normal items, the td contains essentially: + # NAME
OPTIONAL ADJECTIVE
+ # For PB items, the td contains: + # NAME
OPTIONAL ADJECTIVE + # 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 's child. + name_el = element(:item_name, row).children[0] + name_el = name_el.children[0] if name_el.name == 'b' + + data = { + :name => name_el.text, + :quantity => element(:item_quantity, row).text.to_i + } + + if id = element(:item_remove, row)['name'] + id = id.to_i + items_data[:id][id] = data + else # if this is a pb item, which does not give ID, go by thumbnail + thumbnail_url = element(:item_thumbnail, row)['src'] + 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) + ) + ) + + # Create closet hanger from each item, and remove them from the reference + # lists + @hangers = items.map do |item| + data = items_data[:id].delete(item.id) || + items_data[:thumbnail_url].delete(item.thumbnail_url) + hanger = @user.closet_hangers.build + hanger.item = item + hanger.quantity = data[:quantity] + 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 + + class ParseError < RuntimeError;end +end + diff --git a/app/models/item.rb b/app/models/item.rb index 2d4e4f37..91386315 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -3,6 +3,7 @@ class Item < ActiveRecord::Base SwfAssetType = 'object' + has_many :closet_hangers has_one :contribution, :as => :contributed has_many :parent_swf_asset_relationships, :foreign_key => 'parent_id', :conditions => {:swf_asset_type => SwfAssetType} diff --git a/app/models/user.rb b/app/models/user.rb index a13ade3a..fe8f6820 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,14 +1,15 @@ class User < ActiveRecord::Base DefaultAuthServerId = 1 PreviewTopContributorsCount = 3 - + + has_many :closet_hangers has_many :contributions has_many :outfits - + scope :top_contributors, order('points DESC').where(arel_table[:points].gt(0)) - + devise :rememberable - + def contribute!(pet) new_contributions = [] new_points = 0 @@ -38,7 +39,7 @@ class User < ActiveRecord::Base end new_points end - + def self.find_or_create_from_remote_auth_data(user_data) user = find_or_initialize_by_remote_id_and_auth_server_id( user_data['id'], @@ -50,9 +51,10 @@ class User < ActiveRecord::Base end user end - + def self.points_required_to_pass_top_contributor(offset) user = User.top_contributors.select(:points).limit(1).offset(offset).first user ? user.points : 0 end end + diff --git a/db/migrate/20110712232259_create_closet_hangers.rb b/db/migrate/20110712232259_create_closet_hangers.rb new file mode 100644 index 00000000..80fc9f07 --- /dev/null +++ b/db/migrate/20110712232259_create_closet_hangers.rb @@ -0,0 +1,15 @@ +class CreateClosetHangers < ActiveRecord::Migration + def self.up + create_table :closet_hangers do |t| + t.integer :item_id + t.integer :user_id + t.integer :quantity + + t.timestamps + end + end + + def self.down + drop_table :closet_hangers + end +end diff --git a/db/schema.rb b/db/schema.rb index 69c5dca6..931e3818 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20110626202605) do +ActiveRecord::Schema.define(:version => 20110712232259) do create_table "auth_servers", :force => true do |t| t.string "short_name", :limit => 10, :null => false @@ -20,6 +20,14 @@ ActiveRecord::Schema.define(:version => 20110626202605) do t.string "secret", :limit => 64, :null => false end + create_table "closet_hangers", :force => true do |t| + t.integer "item_id" + t.integer "user_id" + t.integer "quantity" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "contributions", :force => true do |t| t.string "contributed_type", :limit => 8, :null => false t.integer "contributed_id", :null => false diff --git a/spec/models/closet_hanger_spec.rb b/spec/models/closet_hanger_spec.rb new file mode 100644 index 00000000..d8d2f0bb --- /dev/null +++ b/spec/models/closet_hanger_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe ClosetHanger do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/vendor/cache/nokogiri-1.5.0.gem b/vendor/cache/nokogiri-1.5.0.gem new file mode 100644 index 00000000..47c37a67 Binary files /dev/null and b/vendor/cache/nokogiri-1.5.0.gem differ