turkee 1.2.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +27 -0
- data/Gemfile.lock +124 -0
- data/Guardfile +12 -0
- data/README.rdoc +53 -21
- data/VERSION +1 -0
- data/lib/generators/turkee/templates/create_turkee_study.rb.erb +18 -0
- data/lib/generators/turkee/turkee_generator.rb +4 -0
- data/lib/helpers/turkee_forms_helper.rb +68 -0
- data/lib/models/turkee_imported_assignment.rb +15 -0
- data/lib/models/turkee_study.rb +11 -0
- data/lib/models/turkee_task.rb +257 -0
- data/lib/tasks/turkee.rb +13 -1
- data/lib/turkee.rb +4 -317
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +5 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +19 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +41 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +27 -0
- data/spec/dummy/config/environments/production.rb +54 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +12 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +62 -0
- data/spec/dummy/db/migrate/20110710143903_initial_tables.rb +23 -0
- data/spec/dummy/db/migrate/20120819164528_add_association_with_class_name.rb +12 -0
- data/spec/dummy/db/migrate/20130203095901_create_company.rb +14 -0
- data/spec/dummy/db/schema.rb +45 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/tmp/cache/.gitkeep +0 -0
- data/spec/factories/turkee_task_factory.rb +21 -0
- data/spec/helpers/turkee_forms_helper_spec.rb +44 -0
- data/spec/models/turkee_study_spec.rb +19 -0
- data/spec/{turkee_spec.rb → models/turkee_task_spec.rb} +0 -0
- data/spec/spec_helper.rb +21 -6
- data/turkee.gemspec +59 -0
- metadata +69 -21
@@ -0,0 +1,11 @@
|
|
1
|
+
module Turkee
|
2
|
+
class TurkeeStudy < ActiveRecord::Base
|
3
|
+
GOLD_RESPONSE_INDEX = 3
|
4
|
+
attr_accessible :turkee_task_id, :feedback, :gold_response
|
5
|
+
|
6
|
+
def approve?
|
7
|
+
words = feedback.split(/\W+/)
|
8
|
+
gold_response.present? ? (gold_response == words[GOLD_RESPONSE_INDEX]) : true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'socket'
|
3
|
+
require 'rturk'
|
4
|
+
require 'lockfile'
|
5
|
+
require 'active_record'
|
6
|
+
require "active_support/core_ext/object/to_query"
|
7
|
+
require 'action_controller'
|
8
|
+
|
9
|
+
module Turkee
|
10
|
+
|
11
|
+
class TurkeeTask < ActiveRecord::Base
|
12
|
+
attr_accessible :sandbox, :hit_title, :hit_description, :hit_reward, :hit_num_assignments, :hit_lifetime, :hit_duration,
|
13
|
+
:form_url, :hit_url, :hit_id, :task_type, :complete
|
14
|
+
|
15
|
+
HIT_FRAMEHEIGHT = 1000
|
16
|
+
|
17
|
+
scope :unprocessed_hits, lambda { where('complete = ? AND sandbox = ?', false, RTurk.sandbox?) }
|
18
|
+
|
19
|
+
# Use this method to go out and retrieve the data for all of the posted Turk Tasks.
|
20
|
+
# Each specific TurkeeTask object (determined by task_type field) is in charge of
|
21
|
+
# accepting/rejecting the assignment and importing the data into their respective tables.
|
22
|
+
def self.process_hits(turkee_task = nil)
|
23
|
+
|
24
|
+
begin
|
25
|
+
# Using a lockfile to prevent multiple calls to Amazon.
|
26
|
+
Lockfile.new('/tmp/turk_processor.lock', :max_age => 3600, :retries => 10) do
|
27
|
+
|
28
|
+
turks = task_items(turkee_task)
|
29
|
+
|
30
|
+
turks.each do |turk|
|
31
|
+
hit = RTurk::Hit.new(turk.hit_id)
|
32
|
+
|
33
|
+
callback_models = Set.new
|
34
|
+
hit.assignments.each do |assignment|
|
35
|
+
next unless submitted?(assignment.status)
|
36
|
+
next if assignment_exists?(assignment)
|
37
|
+
|
38
|
+
model, param_hash = map_imported_values(assignment, turk.task_type)
|
39
|
+
next if model.nil?
|
40
|
+
|
41
|
+
callback_models << model
|
42
|
+
|
43
|
+
result = save_imported_values(model, param_hash)
|
44
|
+
|
45
|
+
# If there's a custom approve? method, see if we should approve the submitted assignment
|
46
|
+
# otherwise just approve it by default
|
47
|
+
turk.process_result(assignment, result)
|
48
|
+
|
49
|
+
TurkeeImportedAssignment.record_imported_assignment(assignment, result, turk)
|
50
|
+
end
|
51
|
+
|
52
|
+
turk.set_expired?(callback_models) if !turk.set_complete?(hit, callback_models)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue Lockfile::MaxTriesLockError => e
|
56
|
+
logger.info "TurkTask.process_hits is already running or the lockfile /tmp/turk_processor.lock exists from an improperly shutdown previous process. Exiting method call."
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.save_imported_values(model, param_hash)
|
62
|
+
key = model.to_s.underscore.gsub('/','_') # Namespaced model will come across as turkee/turkee_study,
|
63
|
+
# we must translate to turkee_turkee_study"
|
64
|
+
model.create(param_hash[key])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Creates a new Mechanical Turk task on AMZN with the given title, desc, etc
|
68
|
+
def self.create_hit(host, hit_title, hit_description, typ, num_assignments, reward, lifetime,
|
69
|
+
duration = nil, qualifications = {}, params = {}, opts = {})
|
70
|
+
model = typ.to_s.constantize
|
71
|
+
f_url = build_url(host, model, params, opts)
|
72
|
+
|
73
|
+
h = RTurk::Hit.create(:title => hit_title) do |hit|
|
74
|
+
hit.assignments = num_assignments
|
75
|
+
hit.description = hit_description
|
76
|
+
hit.reward = reward
|
77
|
+
hit.lifetime = lifetime.to_i.days.seconds.to_i
|
78
|
+
hit.duration = duration.to_i.hours.seconds.to_i if duration
|
79
|
+
hit.question(f_url, :frame_height => HIT_FRAMEHEIGHT)
|
80
|
+
unless qualifications.empty?
|
81
|
+
qualifications.each do |key, value|
|
82
|
+
hit.qualifications.add key, value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
TurkeeTask.create(:sandbox => RTurk.sandbox?,
|
88
|
+
:hit_title => hit_title, :hit_description => hit_description,
|
89
|
+
:hit_reward => reward.to_f, :hit_num_assignments => num_assignments.to_i,
|
90
|
+
:hit_lifetime => lifetime, :hit_duration => duration,
|
91
|
+
:form_url => f_url, :hit_url => h.url,
|
92
|
+
:hit_id => h.id, :task_type => typ,
|
93
|
+
:complete => false)
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
##########################################################################################################
|
98
|
+
# DON'T PUSH THIS BUTTON UNLESS YOU MEAN IT. :)
|
99
|
+
def self.clear_all_turks(force = false)
|
100
|
+
# Do NOT execute this function if we're in production mode
|
101
|
+
raise "You can only clear turks in the sandbox/development environment unless you pass 'true' for the force flag." if Rails.env == 'production' && !force
|
102
|
+
|
103
|
+
hits = RTurk::Hit.all
|
104
|
+
|
105
|
+
logger.info "#{hits.size} reviewable hits. \n"
|
106
|
+
|
107
|
+
unless hits.empty?
|
108
|
+
logger.info "Approving all assignments and disposing of each hit."
|
109
|
+
|
110
|
+
hits.each do |hit|
|
111
|
+
begin
|
112
|
+
hit.expire!
|
113
|
+
hit.assignments.each do |assignment|
|
114
|
+
logger.info "Assignment status : #{assignment.status}"
|
115
|
+
assignment.approve!('__clear_all_turks__approved__') if assignment.status == 'Submitted'
|
116
|
+
end
|
117
|
+
|
118
|
+
turkee_task = TurkeeTask.find_by_hit_id(hit.id)
|
119
|
+
turkee_task.complete_task
|
120
|
+
|
121
|
+
hit.dispose!
|
122
|
+
rescue Exception => e
|
123
|
+
# Probably a service unavailable
|
124
|
+
logger.error "Exception : #{e.to_s}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
def complete_task
|
132
|
+
self.complete = true
|
133
|
+
save!
|
134
|
+
end
|
135
|
+
|
136
|
+
def set_complete?(hit, models)
|
137
|
+
if completed_assignments?
|
138
|
+
hit.dispose!
|
139
|
+
complete_task
|
140
|
+
initiate_callback(:hit_complete, models)
|
141
|
+
return true
|
142
|
+
end
|
143
|
+
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def set_expired?(models)
|
148
|
+
if expired?
|
149
|
+
self.expired = true
|
150
|
+
save!
|
151
|
+
initiate_callback(:hit_expired, models)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def initiate_callback(method, models)
|
156
|
+
models.each { |model| model.send(method, self) if model.respond_to?(method) }
|
157
|
+
end
|
158
|
+
|
159
|
+
def process_result(assignment, result)
|
160
|
+
if result.errors.size > 0
|
161
|
+
logger.info "Errors : #{result.inspect}"
|
162
|
+
assignment.reject!('Failed to enter proper data.')
|
163
|
+
elsif result.respond_to?(:approve?)
|
164
|
+
logger.debug "Approving : #{result.inspect}"
|
165
|
+
self.increment_complete_assignments
|
166
|
+
result.approve? ? assignment.approve!('') : assignment.reject!('Rejected criteria.')
|
167
|
+
else
|
168
|
+
self.increment_complete_assignments
|
169
|
+
assignment.approve!('')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def increment_complete_assignments
|
174
|
+
raise "Missing :completed_assignments attribute. Please upgrade Turkee to the most recent version." unless respond_to?(:completed_assignments)
|
175
|
+
|
176
|
+
self.completed_assignments += 1
|
177
|
+
save
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def logger
|
183
|
+
@logger ||= Logger.new($stderr)
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.map_imported_values(assignment, default_type)
|
187
|
+
params = assignment_params(assignment.answers)
|
188
|
+
param_hash = Rack::Utils.parse_nested_query(params)
|
189
|
+
|
190
|
+
model = find_model(param_hash)
|
191
|
+
model = default_type.constantize if model.nil?
|
192
|
+
|
193
|
+
return model, param_hash
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.assignment_exists?(assignment)
|
197
|
+
TurkeeImportedAssignment.find_by_assignment_id(assignment.id).present?
|
198
|
+
end
|
199
|
+
|
200
|
+
def completed_assignments?
|
201
|
+
completed_assignments == hit_num_assignments
|
202
|
+
end
|
203
|
+
|
204
|
+
def expired?
|
205
|
+
Time.now >= (created_at + hit_lifetime.days)
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.task_items(turkee_task)
|
209
|
+
turkee_task.nil? ? TurkeeTask.unprocessed_hits : Array.new << turkee_task
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.submitted?(status)
|
213
|
+
(status == 'Submitted')
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.assignment_params(answers)
|
217
|
+
answers.to_query
|
218
|
+
end
|
219
|
+
|
220
|
+
# Method looks at the parameter and attempts to find an ActiveRecord model
|
221
|
+
# in the current app that would match the properties of one of the nested hashes
|
222
|
+
# x = {:submit = 'Create', :iteration_vote => {:iteration_id => 1}}
|
223
|
+
# The above _should_ return an IterationVote model
|
224
|
+
def self.find_model(param_hash)
|
225
|
+
param_hash.each do |k, v|
|
226
|
+
if v.is_a?(Hash)
|
227
|
+
model = k.to_s.camelize.constantize rescue next
|
228
|
+
return model if model.descends_from_active_record? rescue next
|
229
|
+
end
|
230
|
+
end
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns custom URL if opts[:form_url] is specified. Otherwise, builds the default url from the model's :new route
|
235
|
+
def self.build_url(host, model, params, opts)
|
236
|
+
if opts[:form_url]
|
237
|
+
full_url(opts[:form_url], params)
|
238
|
+
else
|
239
|
+
form_url(host, model, params)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns the default url of the model's :new route
|
244
|
+
def self.form_url(host, typ, params = {})
|
245
|
+
@app ||= ActionController::Integration::Session.new(Rails.application)
|
246
|
+
url = (host + @app.send("new_#{typ.to_s.underscore}_path"))
|
247
|
+
full_url(url, params)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Appends params to the url as a query string
|
251
|
+
def self.full_url(u, params)
|
252
|
+
url = u
|
253
|
+
url = "#{u}?#{params.to_query}" unless params.empty?
|
254
|
+
url
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
data/lib/tasks/turkee.rb
CHANGED
@@ -6,7 +6,7 @@ namespace :turkee do
|
|
6
6
|
task :post_hit, [:host, :title, :description, :model, :num_assignments, :reward, :lifetime] => :environment do |t, args|
|
7
7
|
hit = Turkee::TurkeeTask.create_hit(args[:host],args[:title], args[:description], args[:model],
|
8
8
|
args[:num_assignments], args[:reward], args[:lifetime])
|
9
|
-
puts "Hit created
|
9
|
+
puts "Hit created : #{hit.hit_url}"
|
10
10
|
end
|
11
11
|
|
12
12
|
desc "Retrieve all results from Mechanical Turk for all open HITs."
|
@@ -14,4 +14,16 @@ namespace :turkee do
|
|
14
14
|
Turkee::TurkeeTask.process_hits
|
15
15
|
end
|
16
16
|
|
17
|
+
desc "Create a usability study. Task takes the application's full url, HIT title, HIT description, model name, number of responses, reward for each response, number of days the HIT should be valid, and number of hours a worker has to complete the HIT."
|
18
|
+
task :create_study, [:url, :title, :description, :num_assignments, :reward, :lifetime] => :environment do |t, args|
|
19
|
+
qualifications = {:approval_rate => {:gt => 70}, :country => {:eql => 'US'}}
|
20
|
+
|
21
|
+
hit = Turkee::TurkeeTask.create_hit(args[:url], args[:title], args[:description], "Turkee::TurkeeStudy",
|
22
|
+
args[:num_assignments], args[:reward], args[:lifetime],
|
23
|
+
nil, qualifications, {}, {:form_url => args[:url]})
|
24
|
+
puts
|
25
|
+
puts "Study created : #{hit.hit_url}"
|
26
|
+
end
|
27
|
+
|
28
|
+
|
17
29
|
end
|
data/lib/turkee.rb
CHANGED
@@ -1,321 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require 'socket'
|
3
|
-
require 'rturk'
|
4
|
-
require 'lockfile'
|
5
|
-
require 'active_record'
|
6
2
|
require 'action_view'
|
7
|
-
require
|
8
|
-
require '
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# Model simply tracks what assignments have been imported
|
13
|
-
class TurkeeImportedAssignment < ActiveRecord::Base
|
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
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
class TurkeeTask < ActiveRecord::Base
|
26
|
-
attr_accessible :sandbox, :hit_title, :hit_description, :hit_reward, :hit_num_assignments, :hit_lifetime, :hit_duration,
|
27
|
-
:form_url, :hit_url, :hit_id, :task_type, :complete
|
28
|
-
|
29
|
-
HIT_FRAMEHEIGHT = 1000
|
30
|
-
|
31
|
-
scope :unprocessed_hits, lambda{ where('complete = ? AND sandbox = ?', false, RTurk.sandbox?) }
|
32
|
-
|
33
|
-
# Use this method to go out and retrieve the data for all of the posted Turk Tasks.
|
34
|
-
# Each specific TurkeeTask object (determined by task_type field) is in charge of
|
35
|
-
# accepting/rejecting the assignment and importing the data into their respective tables.
|
36
|
-
def self.process_hits(turkee_task = nil)
|
37
|
-
|
38
|
-
begin
|
39
|
-
# Using a lockfile to prevent multiple calls to Amazon.
|
40
|
-
Lockfile.new('/tmp/turk_processor.lock', :max_age => 3600, :retries => 10) do
|
41
|
-
|
42
|
-
turks = task_items(turkee_task)
|
43
|
-
|
44
|
-
turks.each do |turk|
|
45
|
-
hit = RTurk::Hit.new(turk.hit_id)
|
46
|
-
|
47
|
-
callback_models = Set.new
|
48
|
-
hit.assignments.each do |assignment|
|
49
|
-
next unless submitted?(assignment.status)
|
50
|
-
next if assignment_exists?(assignment)
|
51
|
-
|
52
|
-
model, param_hash = map_imported_values(assignment)
|
53
|
-
next if model.nil?
|
54
|
-
|
55
|
-
callback_models << model
|
56
|
-
|
57
|
-
result = save_imported_values(model, param_hash)
|
58
|
-
|
59
|
-
# If there's a custom approve? method, see if we should approve the submitted assignment
|
60
|
-
# otherwise just approve it by default
|
61
|
-
turk.process_result(assignment, result)
|
62
|
-
|
63
|
-
TurkeeImportedAssignment.record_imported_assignment(assignment, result, turk)
|
64
|
-
end
|
65
|
-
|
66
|
-
turk.set_expired?(callback_models) if !turk.set_complete?(hit, callback_models)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
rescue Lockfile::MaxTriesLockError => e
|
70
|
-
logger.info "TurkTask.process_hits is already running or the lockfile /tmp/turk_processor.lock exists from an improperly shutdown previous process. Exiting method call."
|
71
|
-
end
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.save_imported_values(model, param_hash)
|
76
|
-
model.create(param_hash[model.to_s.underscore])
|
77
|
-
end
|
78
|
-
|
79
|
-
# Creates a new Mechanical Turk task on AMZN with the given title, desc, etc
|
80
|
-
def self.create_hit(host, hit_title, hit_description, typ, num_assignments, reward, lifetime, duration = nil, qualifications = {}, params = {}, opts = {})
|
81
|
-
model = typ.to_s.constantize
|
82
|
-
f_url = build_url(host, model, params, opts)
|
83
|
-
|
84
|
-
h = RTurk::Hit.create(:title => hit_title) do |hit|
|
85
|
-
hit.assignments = num_assignments
|
86
|
-
hit.description = hit_description
|
87
|
-
hit.reward = reward
|
88
|
-
hit.lifetime = lifetime.to_i.days.seconds.to_i
|
89
|
-
hit.duration = duration.to_i.hours.seconds.to_i if duration
|
90
|
-
hit.question(f_url, :frame_height => HIT_FRAMEHEIGHT)
|
91
|
-
unless qualifications.empty?
|
92
|
-
qualifications.each do |key, value|
|
93
|
-
hit.qualifications.add key, value
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
TurkeeTask.create(:sandbox => RTurk.sandbox?,
|
99
|
-
:hit_title => hit_title, :hit_description => hit_description,
|
100
|
-
:hit_reward => reward.to_f, :hit_num_assignments => num_assignments.to_i,
|
101
|
-
:hit_lifetime => lifetime, :hit_duration => duration,
|
102
|
-
:form_url => f_url, :hit_url => h.url,
|
103
|
-
:hit_id => h.id, :task_type => typ,
|
104
|
-
:complete => false)
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
##########################################################################################################
|
109
|
-
# DON'T PUSH THIS BUTTON UNLESS YOU MEAN IT. :)
|
110
|
-
def self.clear_all_turks(force = false)
|
111
|
-
# Do NOT execute this function if we're in production mode
|
112
|
-
raise "You can only clear turks in the sandbox/development environment unless you pass 'true' for the force flag." if Rails.env == 'production' && !force
|
113
|
-
|
114
|
-
hits = RTurk::Hit.all
|
115
|
-
|
116
|
-
logger.info "#{hits.size} reviewable hits. \n"
|
117
|
-
|
118
|
-
unless hits.empty?
|
119
|
-
logger.info "Approving all assignments and disposing of each hit."
|
120
|
-
|
121
|
-
hits.each do |hit|
|
122
|
-
begin
|
123
|
-
hit.expire!
|
124
|
-
hit.assignments.each do |assignment|
|
125
|
-
logger.info "Assignment status : #{assignment.status}"
|
126
|
-
assignment.approve!('__clear_all_turks__approved__') if assignment.status == 'Submitted'
|
127
|
-
end
|
128
|
-
|
129
|
-
turkee_task = TurkeeTask.find_by_hit_id(hit.id)
|
130
|
-
turkee_task.complete_task
|
131
|
-
|
132
|
-
hit.dispose!
|
133
|
-
rescue Exception => e
|
134
|
-
# Probably a service unavailable
|
135
|
-
logger.error "Exception : #{e.to_s}"
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
end
|
141
|
-
|
142
|
-
def complete_task
|
143
|
-
self.complete = true
|
144
|
-
save!
|
145
|
-
end
|
146
|
-
|
147
|
-
def set_complete?(hit, models)
|
148
|
-
if completed_assignments?
|
149
|
-
hit.dispose!
|
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)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
def initiate_callback(method, models)
|
167
|
-
models.each { |model| model.send(method, self) if model.respond_to?(method) }
|
168
|
-
end
|
169
|
-
|
170
|
-
def process_result(assignment, result)
|
171
|
-
if result.errors.size > 0
|
172
|
-
logger.info "Errors : #{result.inspect}"
|
173
|
-
assignment.reject!('Failed to enter proper data.')
|
174
|
-
elsif result.respond_to?(:approve?)
|
175
|
-
logger.debug "Approving : #{result.inspect}"
|
176
|
-
self.increment_complete_assignments
|
177
|
-
result.approve? ? assignment.approve!('') : assignment.reject!('Rejected criteria.')
|
178
|
-
else
|
179
|
-
self.increment_complete_assignments
|
180
|
-
assignment.approve!('')
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
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)
|
213
|
-
end
|
214
|
-
|
215
|
-
def self.task_items(turkee_task)
|
216
|
-
turkee_task.nil? ? TurkeeTask.unprocessed_hits : Array.new << turkee_task
|
217
|
-
end
|
218
|
-
|
219
|
-
def self.submitted?(status)
|
220
|
-
(status == 'Submitted')
|
221
|
-
end
|
222
|
-
|
223
|
-
def self.assignment_params(answers)
|
224
|
-
answers.to_query
|
225
|
-
end
|
226
|
-
|
227
|
-
# Method looks at the parameter and attempts to find an ActiveRecord model
|
228
|
-
# in the current app that would match the properties of one of the nested hashes
|
229
|
-
# x = {:submit = 'Create', :iteration_vote => {:iteration_id => 1}}
|
230
|
-
# The above _should_ return an IterationVote model
|
231
|
-
def self.find_model(param_hash)
|
232
|
-
param_hash.each do |k, v|
|
233
|
-
if v.is_a?(Hash)
|
234
|
-
model = k.to_s.camelize.constantize rescue next
|
235
|
-
return model if model.descends_from_active_record? rescue next
|
236
|
-
end
|
237
|
-
end
|
238
|
-
nil
|
239
|
-
end
|
240
|
-
|
241
|
-
# Returns custom URL if opts[:form_url] is specified. Otherwise, builds the default url from the model's :new route
|
242
|
-
def self.build_url(host, model, params, opts)
|
243
|
-
if opts[:form_url]
|
244
|
-
full_url(opts[:form_url], params)
|
245
|
-
else
|
246
|
-
form_url(host, model, params)
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
# Returns the default url of the model's :new route
|
251
|
-
def self.form_url(host, typ, params = {})
|
252
|
-
@app ||= ActionController::Integration::Session.new(Rails.application)
|
253
|
-
url = (host + @app.send("new_#{typ.to_s.underscore}_path"))
|
254
|
-
full_url(url, params)
|
255
|
-
end
|
256
|
-
|
257
|
-
# Appends params to the url as a query string
|
258
|
-
def self.full_url(u, params)
|
259
|
-
url = u
|
260
|
-
url = "#{u}?#{params.to_query}" unless params.empty?
|
261
|
-
url
|
262
|
-
end
|
263
|
-
|
264
|
-
end
|
265
|
-
|
266
|
-
|
267
|
-
module TurkeeFormHelper
|
268
|
-
|
269
|
-
# Rails 3.1.1 form_for implementation with the exception of the form action url
|
270
|
-
# will always point to the Amazon externalSubmit interface and you must pass in the
|
271
|
-
# assignment_id parameter.
|
272
|
-
def turkee_form_for(record, params, options = {}, &proc)
|
273
|
-
raise ArgumentError, "Missing block" unless block_given?
|
274
|
-
raise ArgumentError, "turkee_form_for now requires that you pass in the entire params hash, instead of just the assignmentId value. " unless params.is_a?(Hash)
|
275
|
-
options[:html] ||= {}
|
276
|
-
|
277
|
-
case record
|
278
|
-
when String, Symbol
|
279
|
-
object_name = record
|
280
|
-
object = nil
|
281
|
-
else
|
282
|
-
object = record.is_a?(Array) ? record.last : record
|
283
|
-
object_name = options[:as] || ActiveModel::Naming.param_key(object)
|
284
|
-
apply_form_for_options!(record, options)
|
285
|
-
end
|
286
|
-
|
287
|
-
options[:html][:remote] = options.delete(:remote) if options.has_key?(:remote)
|
288
|
-
options[:html][:method] = options.delete(:method) if options.has_key?(:method)
|
289
|
-
options[:html][:authenticity_token] = options.delete(:authenticity_token)
|
290
|
-
|
291
|
-
builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc)
|
292
|
-
fields_for = fields_for(object_name, object, options, &proc)
|
293
|
-
default_options = builder.multipart? ? { :multipart => true } : {}
|
294
|
-
|
295
|
-
output = form_tag(mturk_url, default_options.merge!(options.delete(:html)))
|
296
|
-
params.each do |k,v|
|
297
|
-
unless k =~ /^action$/i || k =~ /^controller$/i || v.class != String
|
298
|
-
output.safe_concat("<input type=\"hidden\" id=\"#{k}\" name=\"#{CGI.escape(k)}\" value=\"#{CGI.escape(v)}\"/>")
|
299
|
-
end
|
300
|
-
end
|
301
|
-
options[:disabled] = true if params[:assignmentId].nil? || Turkee::TurkeeFormHelper::disable_form_fields?(params[:assignmentId])
|
302
|
-
output << fields_for
|
303
|
-
output.safe_concat('</form>')
|
304
|
-
end
|
305
|
-
|
306
|
-
# Returns the external Mechanical Turk url used to post form data based on whether RTurk is cofigured
|
307
|
-
# for sandbox use or not.
|
308
|
-
def mturk_url
|
309
|
-
RTurk.sandbox? ? "https://workersandbox.mturk.com/mturk/externalSubmit" : "https://www.mturk.com/mturk/externalSubmit"
|
310
|
-
end
|
311
|
-
|
312
|
-
# Returns whether the form fields should be disabled or not (based on the assignment_id)
|
313
|
-
def self.disable_form_fields?(assignment)
|
314
|
-
assignment_id = assignment.is_a?(Hash) ? assignment[:assignmentId] : assignment
|
315
|
-
(assignment_id.nil? || assignment_id == 'ASSIGNMENT_ID_NOT_AVAILABLE')
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
end
|
3
|
+
require 'helpers/turkee_forms_helper'
|
4
|
+
require 'models/turkee_imported_assignment'
|
5
|
+
require 'models/turkee_task'
|
6
|
+
require 'models/turkee_study'
|
320
7
|
|
321
8
|
ActionView::Base.send :include, Turkee::TurkeeFormHelper
|