This repository has been archived by the owner on Sep 18, 2021. It is now read-only.
/
evaluation.rb
359 lines (316 loc) · 13 KB
/
evaluation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# Copyright 2012 Twitter, Inc. and others.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Represents a HIT, comprised of a number of individual tasks and questions
# asked for each task.
#
# Attributes
#
# name: The name displayed in Clockwork Raven
# title: The name displayed on Mechanical Turk
# creator: The name of the user who created this Evaluation
# email: The email of the user who created this Evaluation
# desc: Description shown on MTurk
# payment: Payment given to workers for tasks, in cents
# keywords: Comma-separated list of keywords, shown on MTurk
# duration: Time, in seconds, workers have to complete this task
# lifetime: Amount of time, in seconds, to leave this task on MTurk
# auto_approve: Amount of time, in seconds, between when a worker submits
# a response and that response is automatically approved
# mturk_hit_type: The ID of the MTurk HIT Type assigned to this Evaluation
# by the API
# status: The status of this evaluation in its lifecycle (see
# Evaluation::STATUS_ID)
# mturk_qualfication: The name of the qualification required of workers. Should
# be one of "none", "trusted", or "master".
# note: Internal note displayed on Clockwork Raven
# prod: Boolean flag. If true, this evaluation is being run on
# the production MTurk system, rather than the sandbox.
# template: Serialized representation of the template used to
# format this evaluation's tasks. See below for format.
# metadata: Which fields of this evaluation's tasks' data are
# metadata. See below for more.
class Evaluation < ActiveRecord::Base
belongs_to :user
belongs_to :job
# we use these fields in the form to create/update Evaluations -- they're
# not persisted to the database, but giving them accessors/mutators means
# we can use form helpers
attr_accessor :data, :replace_data
# The template this evaluation's tasks are build from. This is an array of
# hashes where each hash represent an item to be displayed. These items
# can take 3 forms.
#
# Form 1: a block of text or a header
# {
# :type => :_text OR :_header,
# :content => <string>
# }
#
# Form 2: a MC or FR question
# {
# :type => :_mc OR :_fr,
# :order => <integer>
# }
#
# The order key is the "order" property of the MCQuestion or FRQuestion.
# The primary function of the key is the set the order in which questions
# are displayed, but we lookup by this key so we don't need to update the
# template when we copy an eval.
#
# Form 3: a component from app/views/components
# {
# :type => <name of partial in app/views/components>,
# :data => {
# <key 1> => <data item 1>,
# <key 2> => <data item 2>,
# ...etc...
# }
# }
#
# A data item represents a value that will be given to the partial, as
# requested in its entry in app/views/components/manifest.yml. It can be
# nil, a literal string value, or the name of a field (one of the columns
# in the uploaded data). The format:
#
# data item ::= {
# :value => :_literal OR :_nil OR <field name>,
# :literal => <string> // <- only if :value is :_literal
# }
serialize :template, Array
# Array of strings. These strings are the keys of this evaluation's tasks
# that will be treated as metadata (not shown to MTurk judges, but included
# as multiple-choice questions when reviewing the data)
serialize :metadata, Array
# status name => status id
# new: not yet submitted to MTurk
# submitted: on MTurk, open for judges
# closed: closed on MTurk
# approved: all tasks have been approved or rejected
# purged: removed from MTurk
STATUS_ID = {
:new => 0,
:submitted => 1,
:closed => 2,
:approved => 3,
:purged => 4
}
# status id => status name
STATUS_NAME = STATUS_ID.invert
# establish associations. establish a stable sort order for questions.
has_many :tasks, :dependent => :destroy
has_many :mc_questions, :dependent => :destroy, :order => '`metadata` ASC, `order` ASC'
has_many :mc_question_options, :through => :mc_questions
has_many :fr_questions, :dependent => :destroy, :order => '`order` ASC'
has_many :task_responses, :through => :tasks
# use nested attributes so we can easily add/modify questions from the
# evaluation form
accepts_nested_attributes_for :mc_questions,
:allow_destroy => true
accepts_nested_attributes_for :fr_questions,
:allow_destroy => true
# basic validations
validates :name, :presence => true, :uniqueness => {:case_sensitive => false}
validates :desc, :title, :presence => true
validates :payment, :numericality => { :only_integer => true, :greater_than_or_equal_to => 1}
validates :status, :numericality => { :only_integer => true,
:greater_than_or_equal_to => 0,
:less_than_or_equal_to => 4 }
validates :mturk_qualification, :inclusion => { :in => %w(none trusted master) }
# Given an array of objects, add a Task to this evaluation for each element
# of the array
#
# Each element of the array should be a Hash. That Hash will be stored in the
# Task's "data" property.
def add_data data
data.each do |item|
task = self.tasks.build :data => item
task.save!
end
end
# Returns a random task that belongs to this Evaluation
def random_task
# special-case this: rand(0) gives a floating point between 0 and 1
return nil if self.tasks.empty?
self.tasks.find(:first, :offset => rand(self.tasks.size))
end
# Returns the name of the current status as a symbol: :new, :submitted,
# :closed, :approved, or :purged.
def status_name
return STATUS_NAME[self.status]
end
# Fetches the number of available results from Mechanical Turk
def available_results_count
return MTurkUtils.num_results self
end
# Returns the url of the task group associated with this evaluation on
# mechanical turk
def mturk_url
return MTurkUtils.get_hit_url self
end
# returns the mturk qualifiaction id for this evaluation's mturk_qualification
# (which should be "trusted", "master", or "none".) Returns nil if
# mturk_qualification is "none" or an unrecognized value.
#
# note that the qualification type ids are different for production mturk
# vs sandbox mturk
def mturk_qualification_type
if self.mturk_qualification == 'trusted'
return MTurkUtils.get_trusted_qual_id self.prod?
elsif self.mturk_qualification == 'master'
return MTurkUtils.get_master_qual_id self.prod?
else
return nil
end
end
# fields to copy when basing an evaluation on another evaluation
BASED_ON_FIELDS = [:desc, :keywords, :payment, :duration,
:lifetime, :auto_approve, :mturk_qualification, :title,
:template, :metadata]
# Copies specific parts of this evaluation. instructions, desc, keywords,
# payment, duration, lifetime, auto_approve, mturk_qualification, template,
# metadata and title are copied. name, creator, email, note, and prod are not.
#
# Questions associated with this evaluation (and, for MC questions,
# their options) are copied to the new evaluation by value, not reference.
#
# If copy_template is set to true
def Evaluation.based_on base
e = Evaluation.new
# copy fields
BASED_ON_FIELDS.each do |field|
e[field] = base[field]
end
# copy FR questions
base.fr_questions.each do |q|
new_q = e.fr_questions.build(:label => q.label, :order => q.order, :required => q.required)
end
# copy MC questions
base.mc_questions.where(:metadata => false).each do |base_q|
new_q = e.mc_questions.build(:label => base_q.label, :order => base_q.order)
base_q.mc_question_options.each do |base_option|
new_q.mc_question_options.build(:label => base_option.label,
:value => base_option.value,
:order => base_option.order)
end
end
return e
end
# Registers this evaluation as a HIT Type on MTurk, then submits all
# of this evaluation's tasks as HITs. Tasks are submitted in the background.
# Returns the Job corresponding to submitting the tasks.
#
# Sets this Evaluation's status to :submitted on successful completion.
#
# If the job fails, it is safe to re-run this method within 24 hours.
# It will not create duplicate HITs within 24 hours.
def submit!
MTurkUtils.register_hit_type self
run_task_job SubmitProcessor
end
# Closes all of this Evluation's Tasks on MTurk and imports the results
# as TaskResponses. Removes all of this Evaluation's existing TaskResponses.
# Closing/importing is done in the background. Returns the Job corresponding
# to closing/importing HITs.
#
# Sets this Evaluation's status to :closed upon successful completion.
#
# Becuase existing TaskResponses are deleted, it is safe to re-run this
# method if it fails.
def close!
run_task_job CloseProcessor
end
# Approves all tasks that haven't already been approved or rejected.
# Approval is done in the background. Returns the Job corresponding
# to approving the tasks.
#
# Call Evaluation#close! before this method -- tasks cannot be
# approved until they are closed.
#
# Sets the Evaluation's status to :approved upon successful completion.
def approve_all!
run_task_job ApproveProcessor
end
# Removes the HITs corresponding to this Evaluation's Tasks from
# Mechanical Turk. Removal is done in the background. Returns the Job
# corresponding to removing the HITs.
#
# Call Evaluation#approve_all! before this method -- tasks cannot
# be purged until they have been closed and either approved or rejected.
#
# Sets this Evaluation's status to :purged upon successful completion.
def purge_from_mturk!
run_task_job PurgeProcessor
end
# The minimum commission taken by MTurk per HIT, in cents.
MTURK_COMMISSION_MINIMUM = 0.5
# Calculates cost: (payment + commmission) * (number of tasks)
# Commission is 10%, minimum 0.5 cents
# Returns: total cost, in cents
def cost
commission = [self.payment/10.0, MTURK_COMMISSION_MINIMUM].max
return (self.payment + commission) * tasks.size
end
# Mean average amount of time it took workers to complete tasks, in seconds.
def mean_time
return 0 if self.task_responses.size == 0
(self.task_responses.map{|r| r.work_duration}.inject(:+) / self.task_responses.size.to_f)
end
# Median amount of time it took workers to complete tasks, in seconds
def median_time
return 0 if self.task_responses.size == 0
durations = self.task_responses.map(&:work_duration).sort
middle = durations.length / 2
if (durations.size % 2) == 0
# even, take mean of middle 2
return (durations[middle] + durations[middle-1]) / 2.0
else
# odd, return middle
return durations[middle]
end
end
# Effective pay rate, in cents per hour, based on mean time
# tasks per second * seconds per hour * pay per task = pay per hour
def mean_pay_rate
mean = self.mean_time
return 0 if mean_time == 0
(1.0/mean) * (60.0*60.0) * self.payment
end
# Effective pay rate, in cents per hour, based on median time
# tasks per second * seconds per hour * pay per task = pay per hour
def median_pay_rate
median = self.median_time
return 0 if median == 0
(1.0/median) * (60.0*60.0) * self.payment
end
private
# Runs a Job whose complete_url and back_url point to this evaluation,
# and whose total number of items is the number of tasks this evaluation has.
# This Evaluation's status will be set to new_status upon completion, and the
# job will consist of passing each task in this evaluation to the block
# in a thread pool.
#
# if oncomplete is passed, it will be executed when the job is done, and gets
# the Job as an argument.
def run_task_job processor
# Create a job whose completion and back url point to this evaluation
url = Rails.application.routes.url_helpers.evaluation_url(self, :only_path => true)
job = Job.create(:complete_url => url,
:back_url => url,
:title => processor::NAME)
job.run processor, self.tasks.map{|t| t.id}, :evaluation_id => id
self.job = job
self.save!
return job
end
end