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