turkee-mongoid 2.0.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.
Files changed (61) hide show
  1. data/.document +5 -0
  2. data/.gitignore +27 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +136 -0
  6. data/Guardfile +12 -0
  7. data/LICENSE +188 -0
  8. data/README.rdoc +219 -0
  9. data/Rakefile +50 -0
  10. data/VERSION +1 -0
  11. data/lib/generators/turkee/templates/turkee.rb +8 -0
  12. data/lib/generators/turkee/turkee_generator.rb +13 -0
  13. data/lib/helpers/turkee_forms_helper.rb +69 -0
  14. data/lib/models/turkee_imported_assignment.rb +19 -0
  15. data/lib/models/turkee_study.rb +17 -0
  16. data/lib/models/turkee_task.rb +273 -0
  17. data/lib/tasks/turkee.rb +29 -0
  18. data/lib/turkee.rb +9 -0
  19. data/spec/dummy/Rakefile +7 -0
  20. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  21. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  22. data/spec/dummy/app/models/survey.rb +6 -0
  23. data/spec/dummy/app/views/layouts/application.html.erb +20 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +21 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/environment.rb +5 -0
  28. data/spec/dummy/config/environments/development.rb +24 -0
  29. data/spec/dummy/config/environments/production.rb +51 -0
  30. data/spec/dummy/config/environments/test.rb +37 -0
  31. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  32. data/spec/dummy/config/initializers/inflections.rb +10 -0
  33. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  34. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  35. data/spec/dummy/config/initializers/session_store.rb +8 -0
  36. data/spec/dummy/config/locales/en.yml +5 -0
  37. data/spec/dummy/config/mongoid.yml +21 -0
  38. data/spec/dummy/config/routes.rb +2 -0
  39. data/spec/dummy/db/seeds.rb +0 -0
  40. data/spec/dummy/public/404.html +26 -0
  41. data/spec/dummy/public/422.html +26 -0
  42. data/spec/dummy/public/500.html +26 -0
  43. data/spec/dummy/public/favicon.ico +0 -0
  44. data/spec/dummy/public/javascripts/application.js +2 -0
  45. data/spec/dummy/public/javascripts/controls.js +965 -0
  46. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  47. data/spec/dummy/public/javascripts/effects.js +1123 -0
  48. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  49. data/spec/dummy/public/javascripts/rails.js +191 -0
  50. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  51. data/spec/dummy/public/stylesheets/scaffold.css +56 -0
  52. data/spec/dummy/script/rails +6 -0
  53. data/spec/factories/survey_factory.rb +7 -0
  54. data/spec/factories/turkee_task_factory.rb +21 -0
  55. data/spec/helpers/turkee_forms_helper_spec.rb +75 -0
  56. data/spec/models/turkee_study_spec.rb +19 -0
  57. data/spec/models/turkee_task_spec.rb +139 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +49 -0
  60. data/turkee.gemspec +60 -0
  61. metadata +243 -0
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'bundler'
4
+ require 'rspec/core/rake_task'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ $:.push File.expand_path("../lib", __FILE__)
8
+
9
+ desc "Run specs"
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ desc 'Default: run specs.'
13
+ task :default => :spec
14
+
15
+ begin
16
+ INSTALL_MESSAGE = %q{
17
+ ========================================================================
18
+ Turkee Installation Complete.
19
+ ------------------------------------------------------------------------
20
+ If you're upgrading Turkee (1.1.1 and prior) or installing for the first time, run:
21
+
22
+ rails g turkee --skip
23
+
24
+ For full instructions on gem usage, visit:
25
+ http://github.com/aantix/turkee#readme
26
+
27
+ ** If you like the Turkee gem, please click the "watch" button on the
28
+ Github project page. You'll make me smile and feel appreciated. :)
29
+ http://github.com/aantix/turkee
30
+
31
+ ========================================================================
32
+ }
33
+
34
+ Gem::Specification.new do |gem|
35
+ gem.name = "turkee"
36
+ gem.summary = "Turkee makes dealing with Amazon's Mechnical Turk a breeze."
37
+ gem.description = "Turkee will help you to create your Rails forms, post the HITs, and retrieve the user entered values from Mechanical Turk."
38
+ gem.email = "jjones@aantix.com"
39
+ gem.homepage = "http://github.com/aantix/turkee"
40
+ gem.authors = ["Jim Jones"]
41
+ gem.add_dependency(%q<rails>, [">= 3.1.1"])
42
+ gem.add_dependency(%q<rturk>, [">= 2.4.0"])
43
+ gem.add_dependency(%q<lockfile>, [">= 1.4.3"])
44
+
45
+ gem.post_install_message = INSTALL_MESSAGE
46
+ gem.require_path = 'lib'
47
+ gem.files = %w(MIT-LICENSE README.textile Gemfile Rakefile init.rb) + Dir.glob("{lib,spec}/**/*")
48
+
49
+ end
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,8 @@
1
+ # Go to this page https://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key
2
+ # to retrieve your AWS/Mechanical Turk access keys.
3
+
4
+ AWSACCESSKEYID = 'XXXXXXXXXXXXXXXXXX'
5
+ AWSSECRETACCESSKEY = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYY'
6
+
7
+ RTurk::logger.level = Logger::DEBUG
8
+ RTurk.setup(AWSACCESSKEYID, AWSSECRETACCESSKEY, :sandbox => (Rails.env == 'production' ? false : true))
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ class TurkeeGenerator < Rails::Generators::Base
4
+
5
+ source_root File.expand_path("../templates", __FILE__)
6
+
7
+ desc "Creates initializer and migrations."
8
+
9
+ def create_initializer
10
+ template "turkee.rb", "config/initializers/turkee.rb"
11
+ end
12
+
13
+ end
@@ -0,0 +1,69 @@
1
+ module Turkee
2
+
3
+ module TurkeeFormHelper
4
+ def turkee_form_for(record, params, options = {}, &proc)
5
+ 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)
6
+
7
+ options.merge!({:url => mturk_url})
8
+
9
+ capture do
10
+ form_for record, options do |f|
11
+ params.each do |k,v|
12
+ unless ['action','controller'].include?(k) || !v.is_a?(String)
13
+ concat hidden_field_tag(k, v)
14
+ cookies[k] = v
15
+ end
16
+ end
17
+
18
+ ['assignmentId', 'workerId', 'hitId'].each do |k|
19
+ concat hidden_field_tag(k, cookies[k]) if !params.has_key?(k) && cookies.has_key?(k)
20
+ end
21
+
22
+ concat(capture(f, &proc))
23
+ end
24
+ end
25
+ end
26
+
27
+ def turkee_study(id = nil)
28
+ task = id.nil? ? Turkee::TurkeeTask.last : Turkee::TurkeeTask.find(id)
29
+ study = Turkee::TurkeeStudy.new
30
+ disabled = Turkee::TurkeeFormHelper::disable_form_fields?(params[:assignmentId])
31
+
32
+ if task.present?
33
+ style = "position: fixed; top: 120px; right: 30px; color: #FFF;"
34
+ style << "width: 400px; height: 375px; z-index: 100; padding: 10px;"
35
+ style << "background-color: rgba(0,0,0, 0.5); border: 1px solid #000;"
36
+
37
+ div_for(task, :style => style) do
38
+ capture do
39
+ concat content_tag(:h3, "DIRECTIONS", :style => 'text-align: right; color:#FF0000;')
40
+ concat task.hit_description.html_safe
41
+ concat '<hr/>'.html_safe
42
+ concat(turkee_form_for(study, params) do |f|
43
+ concat f.label(:feedback, "Feedback?:")
44
+ concat f.text_area(:feedback, :rows => 3, :disabled => disabled)
45
+ concat f.label(:gold_response, "Enter the fourth word from your above feedback :")
46
+ concat f.text_field(:gold_response, :disabled => disabled)
47
+ concat f.hidden_field(:turkee_task_id, :value => task.id)
48
+ concat '<br/>'.html_safe
49
+ concat f.submit('Submit', :disabled => disabled)
50
+ end)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Returns the external Mechanical Turk url used to post form data based on whether RTurk is cofigured
57
+ # for sandbox use or not.
58
+ def mturk_url
59
+ RTurk.sandbox? ? "https://workersandbox.mturk.com/mturk/externalSubmit" : "https://www.mturk.com/mturk/externalSubmit"
60
+ end
61
+
62
+ # Returns whether the form fields should be disabled or not (based on the assignment_id)
63
+ def self.disable_form_fields?(assignment)
64
+ assignment_id = assignment.is_a?(Hash) ? assignment[:assignmentId] : assignment
65
+ (assignment_id.nil? || assignment_id == 'ASSIGNMENT_ID_NOT_AVAILABLE')
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,19 @@
1
+ module Turkee
2
+ class TurkeeImportedAssignment
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ field :assignment_id, type: String # TODO validates assignment_id, uniqueness: true ?
7
+ field :turkee_task_id, type: Integer
8
+ field :worker_id, type: String
9
+ field :result_id, type: Integer
10
+
11
+ def self.record_imported_assignment(assignment, result, turk)
12
+ TurkeeImportedAssignment.create!(:assignment_id => assignment.id,
13
+ :turkee_task_id => turk.id,
14
+ :worker_id => assignment.worker_id,
15
+ :result_id => result.id)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Turkee
2
+ class TurkeeStudy
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ field :turkee_task_id, type: Integer
7
+ field :feedback, type: String
8
+ field :gold_response, type: String
9
+
10
+ GOLD_RESPONSE_INDEX = 3
11
+
12
+ def approve?
13
+ words = feedback.split(/\W+/)
14
+ gold_response.present? ? (gold_response == words[GOLD_RESPONSE_INDEX]) : true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,273 @@
1
+ require 'rubygems'
2
+ require 'socket'
3
+ require 'rturk'
4
+ require 'lockfile'
5
+ require "active_support/core_ext/object/to_query"
6
+ require 'action_controller'
7
+
8
+ module Turkee
9
+
10
+ class TurkeeTask
11
+ include Mongoid::Document
12
+ include Mongoid::Timestamps
13
+
14
+ field :sandbox, type: Boolean
15
+ field :hit_title, type: String #TODO text
16
+ field :hit_description, type: String #TODO text
17
+ field :hit_reward, type: BigDecimal # :precision => 10, :scale => 2
18
+ field :hit_num_assignments, type: Integer
19
+ field :hit_lifetime, type: Integer
20
+ field :hit_duration, type: Integer
21
+ field :form_url, type: String
22
+ field :hit_url, type: String
23
+ field :hit_id, type: String
24
+ field :task_type, type: String
25
+ field :complete, type: Boolean
26
+ field :completed_assignments, type: Integer, default: 0
27
+ field :expired, type: Integer
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, turk.task_type)
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
+ key = model.to_s.underscore.gsub('/','_') # Namespaced model will come across as turkee/turkee_study,
77
+ # we must translate to turkee_turkee_study"
78
+ model.create(param_hash[key])
79
+ end
80
+
81
+ # Creates a new Mechanical Turk task on AMZN with the given title, desc, etc
82
+ def self.create_hit(host, hit_title, hit_description, typ, num_assignments, reward, lifetime,
83
+ duration = nil, qualifications = {}, params = {}, opts = {})
84
+ model = typ.to_s.constantize
85
+ f_url = build_url(host, model, params, opts)
86
+
87
+ h = RTurk::Hit.create(:title => hit_title) do |hit|
88
+ hit.max_assignments = num_assignments if hit.respond_to?(:max_assignments)
89
+ hit.assignments = num_assignments if hit.respond_to?(:assignments)
90
+
91
+ hit.description = hit_description
92
+ hit.reward = reward
93
+ hit.lifetime = lifetime.to_i.days.seconds.to_i
94
+ hit.duration = duration.to_i.hours.seconds.to_i if duration
95
+ hit.question(f_url, :frame_height => HIT_FRAMEHEIGHT)
96
+ unless qualifications.empty?
97
+ qualifications.each do |key, value|
98
+ hit.qualifications.add key, value
99
+ end
100
+ end
101
+ end
102
+
103
+ TurkeeTask.create(:sandbox => RTurk.sandbox?,
104
+ :hit_title => hit_title, :hit_description => hit_description,
105
+ :hit_reward => reward.to_f, :hit_num_assignments => num_assignments.to_i,
106
+ :hit_lifetime => lifetime, :hit_duration => duration,
107
+ :form_url => f_url, :hit_url => h.url,
108
+ :hit_id => h.id, :task_type => typ,
109
+ :complete => false)
110
+
111
+ end
112
+
113
+ ##########################################################################################################
114
+ # DON'T PUSH THIS BUTTON UNLESS YOU MEAN IT. :)
115
+ def self.clear_all_turks(force = false)
116
+ # Do NOT execute this function if we're in production mode
117
+ raise "You can only clear turks in the sandbox/development environment unless you pass 'true' for the force flag." if Rails.env == 'production' && !force
118
+
119
+ hits = RTurk::Hit.all
120
+
121
+ logger.info "#{hits.size} reviewable hits. \n"
122
+
123
+ unless hits.empty?
124
+ logger.info "Approving all assignments and disposing of each hit."
125
+
126
+ hits.each do |hit|
127
+ begin
128
+ hit.expire!
129
+ hit.assignments.each do |assignment|
130
+ logger.info "Assignment status : #{assignment.status}"
131
+ assignment.approve!('__clear_all_turks__approved__') if assignment.status == 'Submitted'
132
+ end
133
+
134
+ turkee_task = TurkeeTask.where(hit_id: hit.id).first
135
+ turkee_task.complete_task if turkee_task.present?
136
+
137
+ hit.dispose!
138
+ rescue Exception => e
139
+ # Probably a service unavailable
140
+ logger.error "Exception : #{e.to_s}"
141
+ end
142
+ end
143
+ end
144
+
145
+ end
146
+
147
+ def complete_task
148
+ self.complete = true
149
+ save!
150
+ end
151
+
152
+ def set_complete?(hit, models)
153
+ if completed_assignments?
154
+ hit.dispose!
155
+ complete_task
156
+ initiate_callback(:hit_complete, models)
157
+ return true
158
+ end
159
+
160
+ false
161
+ end
162
+
163
+ def set_expired?(models)
164
+ if expired?
165
+ self.expired = true
166
+ save!
167
+ initiate_callback(:hit_expired, models)
168
+ end
169
+ end
170
+
171
+ def initiate_callback(method, models)
172
+ models.each { |model| model.send(method, self) if model.respond_to?(method) }
173
+ end
174
+
175
+ def process_result(assignment, result)
176
+ if result.errors.size > 0
177
+ logger.info "Errors : #{result.inspect}"
178
+ assignment.reject!('Failed to enter proper data.')
179
+ elsif result.respond_to?(:approve?)
180
+ logger.debug "Approving : #{result.inspect}"
181
+ self.increment_complete_assignments
182
+ result.approve? ? assignment.approve!('') : assignment.reject!('Rejected criteria.')
183
+ else
184
+ self.increment_complete_assignments
185
+ assignment.approve!('')
186
+ end
187
+ end
188
+
189
+ def increment_complete_assignments
190
+ raise "Missing :completed_assignments attribute. Please upgrade Turkee to the most recent version." unless respond_to?(:completed_assignments)
191
+
192
+ self.completed_assignments += 1
193
+ save
194
+ end
195
+
196
+ private
197
+
198
+ def logger
199
+ @logger ||= Logger.new($stderr)
200
+ end
201
+
202
+ def self.map_imported_values(assignment, default_type)
203
+ params = assignment_params(assignment.answers)
204
+ param_hash = Rack::Utils.parse_nested_query(params)
205
+
206
+ model = find_model(param_hash)
207
+ model = default_type.constantize if model.nil?
208
+
209
+ return model, param_hash
210
+ end
211
+
212
+ def self.assignment_exists?(assignment)
213
+ TurkeeImportedAssignment.find_by_assignment_id(assignment.id).present?
214
+ end
215
+
216
+ def completed_assignments?
217
+ completed_assignments == hit_num_assignments
218
+ end
219
+
220
+ def expired?
221
+ Time.now >= (created_at + hit_lifetime.days)
222
+ end
223
+
224
+ def self.task_items(turkee_task)
225
+ turkee_task.nil? ? TurkeeTask.unprocessed_hits : Array.new << turkee_task
226
+ end
227
+
228
+ def self.submitted?(status)
229
+ (status == 'Submitted')
230
+ end
231
+
232
+ def self.assignment_params(answers)
233
+ answers.to_query
234
+ end
235
+
236
+ # Method looks at the parameter and attempts to find an ActiveRecord model
237
+ # in the current app that would match the properties of one of the nested hashes
238
+ # x = {:submit = 'Create', :iteration_vote => {:iteration_id => 1}}
239
+ # The above _should_ return an IterationVote model
240
+ def self.find_model(param_hash)
241
+ param_hash.each do |k, v|
242
+ if v.is_a?(Hash)
243
+ model = k.to_s.camelize.constantize rescue next
244
+ return model if model.ancestors.include?(Mongoid::Document) rescue next
245
+ end
246
+ end
247
+ nil
248
+ end
249
+
250
+ # Returns custom URL if opts[:form_url] is specified. Otherwise, builds the default url from the model's :new route
251
+ def self.build_url(host, model, params, opts)
252
+ if opts[:form_url]
253
+ full_url(opts[:form_url], params)
254
+ else
255
+ form_url(host, model, params)
256
+ end
257
+ end
258
+
259
+ # Returns the default url of the model's :new route
260
+ def self.form_url(host, typ, params = {})
261
+ @app ||= ActionController::Integration::Session.new(Rails.application)
262
+ url = (host + @app.send("new_#{typ.to_s.underscore}_path"))
263
+ full_url(url, params)
264
+ end
265
+
266
+ # Appends params to the url as a query string
267
+ def self.full_url(u, params)
268
+ url = u
269
+ url = "#{u}?#{params.to_query}" unless params.empty?
270
+ url
271
+ end
272
+ end
273
+ end