turkee 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,18 +5,6 @@ Seamlessly convert your Rails forms for use on Mechanical Turk. Then, easily im
5
5
  External forms are created using a simple form helper. HITs are created by issuing a rake command. Retrieving submitted response data and importing that data into your model(s) requires just one more rake command.
6
6
 
7
7
 
8
- == Rails 2
9
-
10
- I'm no longer supporting Rails 2.x. If you have Rails 2 changes, message me and we can look to maintaing a Rails 2.x branch.
11
-
12
-
13
- == Mechanical Turk API Changes
14
-
15
- Mechanical Turk is now requiring that the hitId, workerId, and the turkSubmitTo parameters be passed in along with the assignmentId and form parameters.
16
-
17
- What does this mean for you? Not much besides the fact that now when you construct your forms using turkee_form_for, you'll be passing in your entire params hash instead of just the assignment_id. The code snippet below reflect this change.
18
-
19
-
20
8
  == Install/Upgrade
21
9
 
22
10
  Add turkee to your Gemfile as a gem dependency, then do a 'bundle install':
@@ -131,6 +119,14 @@ As for Mechanical Turk approval, if the row is created and you haven't specified
131
119
  end
132
120
  end
133
121
 
122
+ 9) If all of specified assignments for a HIT have not been completed by the end of the HITs lifetime, Turkee will attempt to call the optional hit_expired class method for the model. This can be used for any cleanup logic. E.g. :
123
+ class Survey < ActiveRecord::Base
124
+ def self.hit_expired(turkee_task)
125
+ #do something
126
+ end
127
+ end
128
+
129
+
134
130
  == Advanced Usage
135
131
 
136
132
  1) You can use the params hash to pass object IDs to your forms. E.g. if you wanted to setup a form of questions about a given URL (let's call the model UrlSurvey), your code would look something like :
data/Rakefile CHANGED
@@ -5,6 +5,10 @@ Bundler::GemHelper.install_tasks
5
5
 
6
6
  $:.push File.expand_path("../lib", __FILE__)
7
7
 
8
+ task :default do
9
+ sh "rspec spec/"
10
+ end
11
+
8
12
  begin
9
13
  INSTALL_MESSAGE = %q{
10
14
  ========================================================================
@@ -0,0 +1,15 @@
1
+ class AddExpired < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ unless column_exists? :turkee_tasks, :expired
5
+ add_column :turkee_tasks, :expired, :integer
6
+ end
7
+ end
8
+
9
+ def self.down
10
+ if column_exists? :turkee_tasks, :expired
11
+ remove_column :turkee_tasks, :expired
12
+ end
13
+ end
14
+ end
15
+
@@ -34,6 +34,10 @@ class TurkeeGenerator < Rails::Generators::Base
34
34
 
35
35
  migration_template "add_hit_duration.rb.erb", "db/migrate/add_hit_duration.rb"
36
36
 
37
+ sleep 1
38
+
39
+ migration_template "add_expired.rb.erb", "db/migrate/add_expired.rb"
40
+
37
41
  end
38
42
 
39
43
  def create_initializer
@@ -12,6 +12,14 @@ module Turkee
12
12
  # Model simply tracks what assignments have been imported
13
13
  class TurkeeImportedAssignment < ActiveRecord::Base
14
14
  attr_accessible :assignment_id, :turkee_task_id, :worker_id, :result_id
15
+
16
+ def self.record_imported_assignment(assignment, result, turk)
17
+ TurkeeImportedAssignment.create!(:assignment_id => assignment.id,
18
+ :turkee_task_id => turk.id,
19
+ :worker_id => assignment.worker_id,
20
+ :result_id => result.id)
21
+ end
22
+
15
23
  end
16
24
 
17
25
  class TurkeeTask < ActiveRecord::Base
@@ -36,29 +44,26 @@ module Turkee
36
44
  turks.each do |turk|
37
45
  hit = RTurk::Hit.new(turk.hit_id)
38
46
 
39
- models = Set.new
47
+ callback_models = Set.new
40
48
  hit.assignments.each do |assignment|
41
49
  next unless submitted?(assignment.status)
42
- next unless TurkeeImportedAssignment.find_by_assignment_id(assignment.id).nil?
43
-
44
- params = assignment_params(assignment.answers)
45
- param_hash = Rack::Utils.parse_nested_query(params)
46
- model = find_model(param_hash)
50
+ next if assignment_exists?(assignment)
47
51
 
52
+ model, param_hash = map_imported_values(assignment)
48
53
  next if model.nil?
49
- models << model
54
+
55
+ callback_models << model
50
56
 
51
- logger.debug "param_hash = #{param_hash}"
52
- result = model.create(param_hash[model.to_s.underscore])
57
+ result = save_imported_values(model, param_hash)
53
58
 
54
59
  # If there's a custom approve? method, see if we should approve the submitted assignment
55
60
  # otherwise just approve it by default
56
- process_result(assignment, result, turk)
61
+ turk.process_result(assignment, result)
57
62
 
58
- TurkeeImportedAssignment.create!(:assignment_id => assignment.id, :turkee_task_id => turk.id, :worker_id => assignment.worker_id, :result_id => result.id)
63
+ TurkeeImportedAssignment.record_imported_assignment(assignment, result, turk)
59
64
  end
60
65
 
61
- check_hit_completeness(hit, turk, models)
66
+ turk.set_expired?(callback_models) if !turk.set_complete?(hit, callback_models)
62
67
  end
63
68
  end
64
69
  rescue Lockfile::MaxTriesLockError => e
@@ -67,9 +72,13 @@ module Turkee
67
72
 
68
73
  end
69
74
 
75
+ def self.save_imported_values(model, param_hash)
76
+ model.create(param_hash[model.to_s.underscore])
77
+ end
78
+
70
79
  # Creates a new Mechanical Turk task on AMZN with the given title, desc, etc
71
80
  def self.create_hit(host, hit_title, hit_description, typ, num_assignments, reward, lifetime, duration = nil, qualifications = {}, params = {}, opts = {})
72
- model = Object::const_get(typ)
81
+ model = typ.to_s.constantize
73
82
  f_url = build_url(host, model, params, opts)
74
83
 
75
84
  h = RTurk::Hit.create(:title => hit_title) do |hit|
@@ -112,19 +121,13 @@ module Turkee
112
121
  hits.each do |hit|
113
122
  begin
114
123
  hit.expire!
115
-
116
124
  hit.assignments.each do |assignment|
117
-
118
125
  logger.info "Assignment status : #{assignment.status}"
119
-
120
126
  assignment.approve!('__clear_all_turks__approved__') if assignment.status == 'Submitted'
121
127
  end
122
128
 
123
129
  turkee_task = TurkeeTask.find_by_hit_id(hit.id)
124
- if turkee_task
125
- turkee_task.complete = true
126
- turkee_task.save
127
- end
130
+ turkee_task.complete_task
128
131
 
129
132
  hit.dispose!
130
133
  rescue Exception => e
@@ -136,43 +139,77 @@ module Turkee
136
139
 
137
140
  end
138
141
 
139
- private
140
-
141
- def logger
142
- @logger ||= Logger.new($stderr)
142
+ def complete_task
143
+ self.complete = true
144
+ save!
143
145
  end
144
146
 
145
- def self.check_hit_completeness(hit, turk, models)
146
- logger.debug "#### turk.completed_assignments == turk.hit_num_assignments :: #{turk.completed_assignments} == #{turk.hit_num_assignments}"
147
- if turk.completed_assignments == turk.hit_num_assignments
147
+ def set_complete?(hit, models)
148
+ if completed_assignments?
148
149
  hit.dispose!
149
- turk.complete = true
150
- turk.save
151
- models.each { |model| model.hit_complete(turk) if model.respond_to?(:hit_complete) }
150
+ complete_task
151
+ initiate_callback(:hit_complete, models)
152
+ return true
153
+ end
154
+
155
+ false
156
+ end
157
+
158
+ def set_expired?(models)
159
+ if expired?
160
+ self.expired = true
161
+ save!
162
+ initiate_callback(:hit_expired, models)
152
163
  end
153
164
  end
154
165
 
166
+ def initiate_callback(method, models)
167
+ models.each { |model| model.send(method, self) if model.respond_to?(method) }
168
+ end
155
169
 
156
- def self.process_result(assignment, result, turk)
170
+ def process_result(assignment, result)
157
171
  if result.errors.size > 0
158
172
  logger.info "Errors : #{result.inspect}"
159
173
  assignment.reject!('Failed to enter proper data.')
160
174
  elsif result.respond_to?(:approve?)
161
175
  logger.debug "Approving : #{result.inspect}"
162
- increment_complete_assignments(turk)
176
+ self.increment_complete_assignments
163
177
  result.approve? ? assignment.approve!('') : assignment.reject!('Rejected criteria.')
164
178
  else
165
- increment_complete_assignments(turk)
179
+ self.increment_complete_assignments
166
180
  assignment.approve!('')
167
181
  end
168
182
  end
169
183
 
170
- def self.increment_complete_assignments(turk)
171
- # Backward compatibility; completed_assignments may not exist in the table
172
- if turk.respond_to?(:completed_assignments)
173
- turk.completed_assignments += 1
174
- turk.save
175
- end
184
+ def increment_complete_assignments
185
+ raise "Missing :completed_assignments attribute. Please upgrade Turkee to the most recent version." unless respond_to?(:completed_assignments)
186
+
187
+ self.completed_assignments += 1
188
+ save
189
+ end
190
+
191
+ private
192
+
193
+ def logger
194
+ @logger ||= Logger.new($stderr)
195
+ end
196
+
197
+ def self.map_imported_values(assignment)
198
+ params = assignment_params(assignment.answers)
199
+ param_hash = Rack::Utils.parse_nested_query(params)
200
+ return find_model(param_hash), param_hash
201
+ end
202
+
203
+ def self.assignment_exists?(assignment)
204
+ TurkeeImportedAssignment.find_by_assignment_id(assignment.id).present?
205
+ end
206
+
207
+ def completed_assignments?
208
+ completed_assignments == hit_num_assignments
209
+ end
210
+
211
+ def expired?
212
+ Time.now >= (created_at + hit_lifetime.days)
176
213
  end
177
214
 
178
215
  def self.task_items(turkee_task)
@@ -194,7 +231,7 @@ module Turkee
194
231
  def self.find_model(param_hash)
195
232
  param_hash.each do |k, v|
196
233
  if v.is_a?(Hash)
197
- model = Object::const_get(k.to_s.camelize) rescue next
234
+ model = k.to_s.camelize.constantize rescue next
198
235
  return model if model.descends_from_active_record? rescue next
199
236
  end
200
237
  end
@@ -4,7 +4,6 @@ require "bundler/setup"
4
4
  require 'factory_girl'
5
5
  require 'rspec'
6
6
  require 'spork'
7
- require 'growl'
8
7
  require 'rails'
9
8
  require 'rturk'
10
9
  require 'lockfile'
@@ -22,9 +21,18 @@ ActiveRecord::Schema.define(:version => 1) do
22
21
  t.decimal "hit_reward", :precision => 10, :scale => 2
23
22
  t.integer "hit_num_assignments"
24
23
  t.integer "hit_lifetime"
25
- t.integer "hit_duration"
26
24
  t.string "form_url"
25
+ t.integer "completed_assignments", :default => 0
27
26
  t.boolean "complete"
27
+ t.boolean "expired"
28
+ t.datetime "created_at", :null => false
29
+ t.datetime "updated_at", :null => false
30
+ t.integer "turkee_flow_id"
31
+ t.integer "hit_duration"
32
+ end
33
+
34
+ create_table :surveys do |t|
35
+ t.string :answer
28
36
  end
29
37
  end
30
38
 
@@ -1,23 +1,125 @@
1
1
  require 'spec_helper'
2
2
 
3
-
4
3
  describe Turkee::TurkeeTask do
5
- #include ActionController::Routing::Routes
6
4
  class TestTask < ActiveRecord::Base
7
- def self.abstract_class; true; end
5
+ def self.abstract_class
6
+ true
7
+ end
8
+
8
9
  attr_accessor :description
9
10
  end
10
11
 
11
- describe "#process_hits" do
12
- before(:each) do
12
+ class Survey < ActiveRecord::Base
13
+ def self.abstract_class
14
+ true
15
+ end
16
+
17
+ def self.hit_complete(hit)
18
+ end
19
+ end
20
+
21
+ describe ".completed_assignments?" do
22
+ it "is not complete" do
23
+ turkee_task = FactoryGirl.create(:turkee_task)
24
+ turkee_task.send("completed_assignments?").should be_false
25
+ end
26
+
27
+ it "is complete" do
28
+ turkee_task = FactoryGirl.create(:turkee_task, :completed_assignments => 100)
29
+ turkee_task.send("completed_assignments?").should be_true
30
+ end
31
+ end
13
32
 
33
+ describe ".expired?" do
34
+ it "is not expired" do
35
+ turkee_task = FactoryGirl.create(:turkee_task)
36
+ turkee_task.send("expired?").should be_false
14
37
  end
15
38
 
39
+ it "is expired" do
40
+ turkee_task = FactoryGirl.create(:turkee_task, :created_at => Time.now - 2.days, :hit_lifetime => 1)
41
+ turkee_task.send("expired?").should be_true
42
+
43
+ turkee_task = FactoryGirl.create(:turkee_task, :created_at => Time.now - 1.day, :hit_lifetime => 1)
44
+ turkee_task.send("expired?").should be_true
45
+ end
16
46
  end
17
47
 
48
+ describe ".set_complete?" do
49
+ before do
50
+ @hit = RTurk::Hit.new(123)
51
+ end
52
+ context "completed hits" do
53
+ before do
54
+ @turkee_task = FactoryGirl.create(:turkee_task,
55
+ :hit_num_assignments => 100,
56
+ :completed_assignments => 100)
57
+ end
18
58
 
19
- describe "#find_model" do
59
+ it "marks the turkee task as complete" do
60
+ @hit.should_receive(:dispose!).once
61
+ Survey.should_receive(:hit_complete).once
62
+ @turkee_task.set_complete?(@hit, [Survey])
63
+ @turkee_task.complete.should be_true
64
+ end
65
+ end
66
+
67
+ context "incomplete hits" do
68
+ before do
69
+ @turkee_task = FactoryGirl.create(:turkee_task,
70
+ :hit_num_assignments => 99,
71
+ :completed_assignments => 100)
72
+ end
20
73
 
74
+ it "keeps the turkee task as incomplete" do
75
+ @hit.should_not_receive(:dispose!)
76
+ Survey.should_not_receive(:hit_complete)
77
+ @turkee_task.set_complete?(@hit, [Survey]).should be_false
78
+ @turkee_task.complete.should be_false
79
+ end
80
+ end
81
+ end
82
+
83
+ describe ".set_expired?" do
84
+ context "unexpired hits" do
85
+ before do
86
+ @turkee_task = FactoryGirl.create(:turkee_task)
87
+ end
88
+
89
+ it "keeps the turkee task as unexpired" do
90
+ Survey.should_not_receive(:hit_expired)
91
+ @turkee_task.set_expired?([Survey])
92
+ @turkee_task.expired.should be_false
93
+ end
94
+ end
95
+
96
+ context "expired hits" do
97
+ before do
98
+ @turkee_task = FactoryGirl.create(:turkee_task,
99
+ :created_at => 2.days.ago,
100
+ :hit_lifetime => 1)
101
+ end
102
+
103
+ it "marks the turkee task as expired" do
104
+ Survey.should_receive(:hit_expired)
105
+ @turkee_task.set_expired?([Survey]).should be_true
106
+ @turkee_task.expired.should be_true
107
+ end
108
+ end
109
+ end
110
+
111
+ describe ".initiate_callback" do
112
+ before do
113
+ @turkee_task = FactoryGirl.create(:turkee_task)
114
+ end
115
+ it "calls hit_complete for the given callback model" do
116
+ Survey.should_receive(:hit_complete).once
117
+ @turkee_task.initiate_callback(:hit_complete, [Survey])
118
+ end
119
+
120
+ end
121
+
122
+ describe "#find_model" do
21
123
  it "should return a turkee_task mode " do
22
124
  returned_data = {:submit => 'Create', "test_task" => {:description => "desc"}}
23
125
  Turkee::TurkeeTask.find_model(returned_data).should == TestTask
@@ -27,9 +129,8 @@ describe Turkee::TurkeeTask do
27
129
  returned_data = {:submit => 'Create', "another_task_class" => {:description => "desc"}}
28
130
  Turkee::TurkeeTask.find_model(returned_data).should be_nil
29
131
  end
30
-
31
132
  end
32
-
133
+
33
134
  describe "#assignment_params" do
34
135
  it "should encode the params properly" do
35
136
  answers = {:test => "abc", :test2 => "this is a test"}
@@ -42,5 +143,4 @@ describe Turkee::TurkeeTask do
42
143
  Turkee::TurkeeTask.submitted?("Submitted").should == true
43
144
  end
44
145
  end
45
-
46
146
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turkee
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-09 00:00:00.000000000 Z
12
+ date: 2013-01-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: lockfile
16
- requirement: &70217997125780 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70217997125780
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rails
27
- requirement: &70217997123680 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,10 +37,15 @@ dependencies:
32
37
  version: 3.1.1
33
38
  type: :runtime
34
39
  prerelease: false
35
- version_requirements: *70217997123680
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.1.1
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: rturk
38
- requirement: &70217997120760 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
39
49
  none: false
40
50
  requirements:
41
51
  - - ! '>='
@@ -43,10 +53,15 @@ dependencies:
43
53
  version: 2.4.0
44
54
  type: :runtime
45
55
  prerelease: false
46
- version_requirements: *70217997120760
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.4.0
47
62
  - !ruby/object:Gem::Dependency
48
63
  name: mocha
49
- requirement: &70217997119040 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
50
65
  none: false
51
66
  requirements:
52
67
  - - ! '>='
@@ -54,10 +69,31 @@ dependencies:
54
69
  version: '0'
55
70
  type: :development
56
71
  prerelease: false
57
- version_requirements: *70217997119040
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
58
78
  - !ruby/object:Gem::Dependency
59
79
  name: sqlite3
60
- requirement: &70217997114560 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: spork
96
+ requirement: !ruby/object:Gem::Requirement
61
97
  none: false
62
98
  requirements:
63
99
  - - ! '>='
@@ -65,10 +101,15 @@ dependencies:
65
101
  version: '0'
66
102
  type: :development
67
103
  prerelease: false
68
- version_requirements: *70217997114560
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
69
110
  - !ruby/object:Gem::Dependency
70
111
  name: factory_girl
71
- requirement: &70217997110980 !ruby/object:Gem::Requirement
112
+ requirement: !ruby/object:Gem::Requirement
72
113
  none: false
73
114
  requirements:
74
115
  - - ! '>='
@@ -76,10 +117,15 @@ dependencies:
76
117
  version: 1.3.2
77
118
  type: :development
78
119
  prerelease: false
79
- version_requirements: *70217997110980
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 1.3.2
80
126
  - !ruby/object:Gem::Dependency
81
127
  name: rspec
82
- requirement: &70217997110120 !ruby/object:Gem::Requirement
128
+ requirement: !ruby/object:Gem::Requirement
83
129
  none: false
84
130
  requirements:
85
131
  - - ! '>='
@@ -87,7 +133,12 @@ dependencies:
87
133
  version: 2.5.0
88
134
  type: :development
89
135
  prerelease: false
90
- version_requirements: *70217997110120
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: 2.5.0
91
142
  description: Turkee will help you to create your Rails forms, post the HITs, and retrieve
92
143
  the user entered values from Mechanical Turk.
93
144
  email: jjones@aantix.com
@@ -105,6 +156,7 @@ files:
105
156
  - lib/generators/turkee/templates/turkee_migration.rb.erb
106
157
  - lib/generators/turkee/templates/add_imported_assignment_details.rb.erb
107
158
  - lib/generators/turkee/templates/add_hit_duration.rb.erb
159
+ - lib/generators/turkee/templates/add_expired.rb.erb
108
160
  - lib/generators/turkee/turkee_generator.rb
109
161
  - lib/tasks/turkee.rb
110
162
  - lib/turkee.rb
@@ -142,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
194
  version: '0'
143
195
  requirements: []
144
196
  rubyforge_project:
145
- rubygems_version: 1.8.15
197
+ rubygems_version: 1.8.24
146
198
  signing_key:
147
199
  specification_version: 3
148
200
  summary: Turkee makes dealing with Amazon's Mechnical Turk a breeze.