shared_workforce 0.1.2 → 0.2.0
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.
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/README.markdown +48 -39
- data/lib/shared_workforce/client.rb +0 -8
- data/lib/shared_workforce/configuration.rb +0 -2
- data/lib/shared_workforce/exceptions.rb +1 -0
- data/lib/shared_workforce/frameworks/rails.rb +0 -7
- data/lib/shared_workforce/task.rb +127 -63
- data/lib/shared_workforce/task_request/task_request.rb +4 -0
- data/lib/shared_workforce/task_response.rb +5 -1
- data/lib/shared_workforce/task_result.rb +14 -9
- data/lib/shared_workforce/version.rb +1 -1
- data/lib/shared_workforce.rb +2 -0
- data/shared_workforce.gemspec +3 -2
- data/spec/client_spec.rb +0 -7
- data/spec/spec_helper.rb +1 -3
- data/spec/task_request/black_hole_spec.rb +2 -2
- data/spec/task_request/http_spec.rb +37 -80
- data/spec/task_result_spec.rb +22 -27
- data/spec/task_spec.rb +296 -81
- data/spec/tasks/approve_photo.rb +18 -9
- metadata +28 -28
data/.travis.yml
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.8.7
|
4
|
+
- 1.9.2
|
5
|
+
- 1.9.3
|
6
|
+
- jruby-18mode # JRuby in 1.8 mode
|
7
|
+
- jruby-19mode # JRuby in 1.9 mode
|
8
|
+
- rbx-18mode
|
9
|
+
# - rbx-19mode # currently in active development, may or may not work for your project
|
10
|
+
# uncomment this line if your project needs to run something other than `rake`:
|
11
|
+
script: bundle exec rspec spec
|
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
Shared Workforce Client
|
2
2
|
=======================
|
3
3
|
|
4
|
+
[](http://travis-ci.org/#!/samoli/shared-workforce)
|
5
|
+
|
4
6
|
Shared Workforce is a platform for managing and completing repetitive tasks that require human intelligence. For example, tagging photos, approving text and answering simple questions.
|
5
7
|
|
6
8
|
It differs from other similar services in the following ways:
|
@@ -43,47 +45,47 @@ If you're not using Rails, simply require the gem or include it in your Gemfile,
|
|
43
45
|
|
44
46
|
Create a directory called 'tasks' in the root of your app. This is where you define your tasks - all files in this directory will be loaded.
|
45
47
|
|
46
|
-
If, for example, you would like to
|
48
|
+
If, for example, you would like to approve a photo on upload, create your first task in a file called tasks/approve_photo.rb
|
49
|
+
|
50
|
+
class ApprovePhotoTask
|
51
|
+
include SharedWorkforce::Task
|
47
52
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
title 'Approve Photo'
|
54
|
+
|
55
|
+
instruction 'Look at this photo. Please tick all that apply.'
|
56
|
+
responses_required 1
|
57
|
+
|
58
|
+
answer_options ['Offensive', 'Contains Nudity', 'Blurry or low quality', 'Upside down or sideways']
|
59
|
+
answer_type :tags
|
60
|
+
image_url "http://www.google.com/logo.png"
|
61
|
+
|
62
|
+
on_success :moderate_photo
|
63
|
+
|
64
|
+
def moderate_photo(photo, responses)
|
65
|
+
if responses.map { |r| r[:answer] }.include?('Offensive')
|
59
66
|
photo.hide!
|
60
|
-
photo.add_comment("Photo
|
61
|
-
elsif result.answers.include?('Contains Nudity')
|
62
|
-
photo.refer!
|
63
|
-
photo.add_comment("Photo referred because it contains nudity")
|
64
|
-
else
|
65
|
-
photo.approve!
|
67
|
+
photo.add_comment("Photo is offensive")
|
66
68
|
end
|
69
|
+
puts "Photo Moderated"
|
67
70
|
end
|
68
|
-
|
71
|
+
|
69
72
|
end
|
73
|
+
|
70
74
|
|
71
75
|
### Step 4 - request tasks
|
72
76
|
|
73
|
-
Publishing tasks is simply a case of calling
|
77
|
+
Publishing tasks is simply a case of calling `TaskClass.create()`. If you are using Rails, this could be done in an after save callback on a model:
|
74
78
|
|
75
79
|
class Photo < ActiveRecord::Base
|
76
80
|
|
77
|
-
after_create :
|
81
|
+
after_create :approve_photo
|
78
82
|
|
79
|
-
def
|
80
|
-
|
83
|
+
def approve_photo
|
84
|
+
ApprovePhotoTask.create(self)
|
81
85
|
end
|
82
86
|
end
|
83
87
|
|
84
88
|
|
85
|
-
That's it - once your task is completed the callback you have defined in the task definition will be called. Everything you define in the :callback_params option will be sent back to your callback as shown in the example.
|
86
|
-
|
87
89
|
Advanced definition options
|
88
90
|
----------------------------------------
|
89
91
|
|
@@ -99,27 +101,34 @@ SharedWorkforce currently supports 2 types of task. :tags (multiple select) and
|
|
99
101
|
|
100
102
|
SharedWorkforce supports multiple responses for each task. The callback method provides you with an array of responses from multiple workers. You can create your own logic to decide what to do. This is useful if you want to prevent destructive action unless a number of workers agree.
|
101
103
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
104
|
+
class ApprovePhotoTask
|
105
|
+
include SharedWorkforce::Task
|
106
|
+
|
107
|
+
title 'Approve Photo'
|
108
|
+
|
109
|
+
instruction 'Look at this photo. Please tick all that apply.'
|
110
|
+
responses_required 3
|
111
|
+
|
112
|
+
answer_options ['Offensive', 'Contains Nudity', 'Blurry or low quality', 'Upside down or sideways']
|
113
|
+
answer_type :tags
|
114
|
+
|
115
|
+
on_complete :moderate_photo
|
116
|
+
|
117
|
+
def moderate_photo(photo, responses)
|
118
|
+
photo.hide! if responses.map { |r| r[:answer] }.all? { |a| a.include?('Contains Nudity') }
|
112
119
|
end
|
113
120
|
|
114
121
|
end
|
122
|
+
|
115
123
|
|
116
124
|
###Replacing tasks
|
117
125
|
|
118
126
|
The "replace" option allows you to overwrite or update any existing tasks with the same name and callback params. This could be useful in the example to handle the situation where a user re-uploads their photo - you may only care about the latest one.
|
119
127
|
|
120
|
-
|
128
|
+
class ApprovePhotoTask
|
129
|
+
include SharedWorkforce::Task
|
121
130
|
...
|
122
|
-
|
131
|
+
replace true
|
123
132
|
...
|
124
133
|
end
|
125
134
|
|
@@ -127,11 +136,11 @@ The "replace" option allows you to overwrite or update any existing tasks with t
|
|
127
136
|
|
128
137
|
You can cancel tasks when they are no longer relevant.
|
129
138
|
|
130
|
-
class Photo
|
139
|
+
class Photo
|
131
140
|
after_destroy :cancel_tagging_request
|
132
141
|
|
133
142
|
def cancel_tagging_request
|
134
|
-
|
143
|
+
ApprovePhotoTask.cancel(self)
|
135
144
|
end
|
136
145
|
end
|
137
146
|
|
@@ -2,7 +2,6 @@ module SharedWorkforce
|
|
2
2
|
class Configuration
|
3
3
|
|
4
4
|
attr_accessor :api_key
|
5
|
-
attr_accessor :load_path
|
6
5
|
attr_accessor :http_end_point
|
7
6
|
attr_accessor :callback_host
|
8
7
|
attr_accessor :request_class
|
@@ -10,7 +9,6 @@ module SharedWorkforce
|
|
10
9
|
|
11
10
|
def initialize
|
12
11
|
@http_end_point = "http://api.sharedworkforce.com"
|
13
|
-
@load_path = "tasks"
|
14
12
|
@request_class = TaskRequest::Http
|
15
13
|
end
|
16
14
|
|
@@ -2,13 +2,6 @@ if defined?(ActionController::Metal)
|
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
initializer 'shared_workforce' do |app|
|
4
4
|
app.config.middleware.use SharedWorkforce::EndPoint
|
5
|
-
SharedWorkforce::Client.load_path = Rails.root + SharedWorkforce.configuration.load_path
|
6
|
-
SharedWorkforce::Client.load!
|
7
5
|
end
|
8
6
|
end
|
9
|
-
else
|
10
|
-
Rails.configuration.after_initialize do
|
11
|
-
SharedWorkforce::Client.load_path = Rails.root + SharedWorkforce.configuration.load_path
|
12
|
-
SharedWorkforce::Client.load!
|
13
|
-
end
|
14
7
|
end
|
@@ -1,94 +1,146 @@
|
|
1
1
|
module SharedWorkforce
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
2
|
+
module Task
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.default_attributes(
|
7
|
+
:title,
|
8
|
+
:answer_type,
|
9
|
+
:instruction,
|
10
|
+
:responses_required,
|
11
|
+
:replace,
|
12
|
+
:answer_options,
|
13
|
+
:image_url,
|
14
|
+
:text,
|
15
|
+
:on_success,
|
16
|
+
:on_failure,
|
17
|
+
:on_complete
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
def default_attributes(*args)
|
24
|
+
if args.count > 0
|
25
|
+
args.each do |name|
|
26
|
+
class_eval %(
|
27
|
+
class << self
|
28
|
+
def #{name}(value)
|
29
|
+
set_default_attribute(:#{name}, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :#{name}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
@default_attributes || {}
|
38
|
+
end
|
10
39
|
end
|
11
40
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
yield task
|
16
|
-
self.tasks[name] = task
|
41
|
+
def set_default_attribute(name, value)
|
42
|
+
@default_attributes ||= {}
|
43
|
+
@default_attributes[name] = value
|
17
44
|
end
|
18
|
-
|
19
|
-
def
|
20
|
-
@
|
45
|
+
|
46
|
+
def get_default_attribute(name)
|
47
|
+
@default_attributes ||= {}
|
48
|
+
@default_attributes[name]
|
21
49
|
end
|
22
|
-
|
23
|
-
def
|
24
|
-
|
50
|
+
|
51
|
+
def create(*args)
|
52
|
+
task = new(*args)
|
53
|
+
task.request
|
54
|
+
task
|
25
55
|
end
|
26
|
-
|
27
|
-
def
|
28
|
-
|
56
|
+
|
57
|
+
def cancel(*args)
|
58
|
+
task = new(*args)
|
59
|
+
task.cancel
|
60
|
+
task
|
29
61
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
62
|
+
|
63
|
+
end # ends ClassMethods
|
64
|
+
|
65
|
+
attr_reader :attributes
|
66
|
+
|
67
|
+
def initialize_default_attributes
|
68
|
+
self.class.default_attributes.each do |name, value|
|
69
|
+
instance_variable_set("@#{name}", value)
|
33
70
|
end
|
34
|
-
|
35
71
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
72
|
+
|
73
|
+
def initialize(resource_or_result=nil, attributes=nil)
|
74
|
+
initialize_default_attributes
|
75
|
+
if resource_or_result.is_a?(TaskResult)
|
76
|
+
@result = resource_or_result
|
77
|
+
process_result(@result)
|
78
|
+
elsif resource_or_result
|
79
|
+
unless resource_or_result.respond_to?(:id) && resource_or_result.class.respond_to?(:find)
|
80
|
+
raise ArgumentError, "The resource you pass to new should respond to #id and it's class should respond to .find (or be an instance of ActiveRecord::Base) so it can be reloaded."
|
81
|
+
end
|
82
|
+
@resource = resource_or_result
|
83
|
+
initialize_attributes(attributes)
|
84
|
+
end
|
85
|
+
|
86
|
+
setup(resource) if respond_to?(:setup)
|
47
87
|
end
|
48
|
-
|
49
|
-
def
|
88
|
+
|
89
|
+
def process_result(result)
|
90
|
+
initialize_attributes(result.callback_params)
|
91
|
+
success!(result)
|
92
|
+
complete!(result)
|
93
|
+
end
|
94
|
+
|
95
|
+
def success!(result)
|
96
|
+
send(@on_success.to_sym, resource, result.responses) if @on_success
|
97
|
+
end
|
98
|
+
|
99
|
+
def complete!(result)
|
100
|
+
send(@on_complete.to_sym, resource, result.responses) if @on_complete
|
101
|
+
end
|
102
|
+
|
103
|
+
def fail!(result)
|
104
|
+
send(@on_failure.to_sym, resource, result.responses) if @on_failure
|
105
|
+
end
|
106
|
+
|
107
|
+
def resource
|
108
|
+
@resource ||= find_resource
|
109
|
+
end
|
110
|
+
|
111
|
+
def request(options = {})
|
50
112
|
task_request = remote_request(self, options)
|
51
113
|
task_request.create
|
52
114
|
end
|
53
115
|
|
54
|
-
def cancel(options)
|
116
|
+
def cancel(options = {})
|
55
117
|
task_request = remote_request(self, options)
|
56
118
|
task_request.cancel
|
57
119
|
end
|
58
120
|
|
59
121
|
def to_hash
|
60
122
|
{
|
61
|
-
:
|
62
|
-
:
|
123
|
+
:title => title,
|
124
|
+
:instruction => instruction,
|
63
125
|
:image_url => image_url,
|
64
126
|
:answer_options => answer_options,
|
65
127
|
:responses_required => responses_required,
|
66
128
|
:answer_type => answer_type.to_s,
|
67
129
|
:callback_url => callback_url,
|
68
|
-
:replace => replace
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# Callbacks
|
73
|
-
|
74
|
-
def on_completion(&block)
|
75
|
-
@on_complete_proc = block
|
76
|
-
end
|
77
|
-
|
78
|
-
def complete!(results)
|
79
|
-
@on_complete_proc.call(results) if @on_complete_proc
|
80
|
-
end
|
81
|
-
|
82
|
-
def on_failure(&block)
|
83
|
-
@on_failure_proc = block
|
130
|
+
:replace => replace,
|
131
|
+
:text => text,
|
132
|
+
:callback_params => attributes
|
133
|
+
}.reject {|k,v| v.nil? }
|
84
134
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def find_resource
|
139
|
+
if @result && @result.callback_params[:_task] && resource_params = @result.callback_params[:_task][:resource]
|
140
|
+
resource_params[:class_name].constantize.find(resource_params[:id])
|
141
|
+
end
|
88
142
|
end
|
89
143
|
|
90
|
-
private
|
91
|
-
|
92
144
|
def callback_url
|
93
145
|
SharedWorkforce.configuration.callback_url
|
94
146
|
end
|
@@ -96,5 +148,17 @@ module SharedWorkforce
|
|
96
148
|
def remote_request(*args)
|
97
149
|
SharedWorkforce.configuration.request_class.new(*args)
|
98
150
|
end
|
151
|
+
|
152
|
+
def initialize_attributes(attributes)
|
153
|
+
@attributes = if attributes
|
154
|
+
attributes.with_indifferent_access
|
155
|
+
else
|
156
|
+
{}
|
157
|
+
end
|
158
|
+
|
159
|
+
@attributes[:_task] = {:class_name => self.class.name}
|
160
|
+
@attributes[:_task][:resource] = {:class_name => @resource.class.name, :id => @resource.id} if @resource
|
161
|
+
end
|
162
|
+
|
99
163
|
end
|
100
164
|
end
|
@@ -4,7 +4,11 @@ module SharedWorkforce
|
|
4
4
|
def self.create_collection_from_array(ary)
|
5
5
|
ary.collect {|r| TaskResponse.new(r) }
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
|
+
def to_hash
|
9
|
+
{:answer=>answer, :answered_by=>username}
|
10
|
+
end
|
11
|
+
|
8
12
|
attr_accessor :answer, :callback_params, :username
|
9
13
|
|
10
14
|
def initialize(params)
|
@@ -7,26 +7,31 @@ module SharedWorkforce
|
|
7
7
|
attr_accessor :status
|
8
8
|
|
9
9
|
def initialize(params)
|
10
|
-
|
11
|
-
self.
|
12
|
-
|
10
|
+
params = params.with_indifferent_access
|
11
|
+
self.callback_params = params[:callback_params]
|
12
|
+
@responses = TaskResponse.create_collection_from_array(params[:responses]) if params[:responses]
|
13
|
+
self.name = callback_params[:_task][:class_name] if callback_params && callback_params[:_task] && callback_params[:_task][:class_name]
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
responses.map(&:
|
16
|
+
def responses
|
17
|
+
@responses.map(&:to_hash)
|
17
18
|
end
|
18
19
|
|
19
20
|
def usernames
|
20
|
-
responses.map(&:username).flatten
|
21
|
+
@responses.map(&:username).flatten
|
21
22
|
end
|
22
23
|
|
23
24
|
def process!
|
24
|
-
if task =
|
25
|
-
task.
|
25
|
+
if name && task = find_task(name)
|
26
|
+
task.new(self)
|
26
27
|
else
|
27
|
-
raise "The task #{name} could not be found"
|
28
|
+
raise TaskNotFound, "The task #{name} could not be found"
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
32
|
+
private
|
33
|
+
def find_task(name)
|
34
|
+
name.constantize
|
35
|
+
end
|
31
36
|
end
|
32
37
|
end
|
data/lib/shared_workforce.rb
CHANGED
@@ -10,6 +10,8 @@ require 'shared_workforce/task_result'
|
|
10
10
|
require 'shared_workforce/task_response'
|
11
11
|
require 'shared_workforce/end_point'
|
12
12
|
require 'shared_workforce/frameworks/rails' if defined?(Rails)
|
13
|
+
require 'active_support/inflector'
|
14
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
13
15
|
|
14
16
|
module SharedWorkforce
|
15
17
|
|
data/shared_workforce.gemspec
CHANGED
@@ -8,14 +8,15 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Sam Oliver"]
|
10
10
|
s.email = ["sam@samoliver.com"]
|
11
|
-
s.homepage = "http://github.com/
|
11
|
+
s.homepage = "http://github.com/sharedworkforce/sharedworkforce"
|
12
12
|
s.summary = %q{Shared Workforce Client}
|
13
13
|
s.description = %q{Shared Workforce is a service and simple API for human intelligence tasks}
|
14
14
|
|
15
|
-
s.rubyforge_project = "
|
15
|
+
s.rubyforge_project = "shared_workforce"
|
16
16
|
|
17
17
|
s.add_dependency "rest-client"
|
18
18
|
s.add_dependency "json"
|
19
|
+
s.add_dependency "activesupport"
|
19
20
|
s.add_development_dependency "rspec", ">= 1.2.9"
|
20
21
|
s.add_development_dependency "webmock"
|
21
22
|
|
data/spec/client_spec.rb
CHANGED
@@ -1,13 +1,6 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
3
|
describe "Client" do
|
4
|
-
it "should load tasks" do
|
5
|
-
SharedWorkforce::Client.load_path = File.dirname(__FILE__) + '/tasks'
|
6
|
-
SharedWorkforce::Client.load!
|
7
|
-
|
8
|
-
SharedWorkforce::Task.find("Approve photo").should_not == nil
|
9
|
-
end
|
10
|
-
|
11
4
|
it "should return the current version number" do
|
12
5
|
SharedWorkforce::Client.version.should == SharedWorkforce::VERSION
|
13
6
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -8,12 +8,10 @@ require 'webmock/rspec'
|
|
8
8
|
# Requires supporting ruby files with custom matchers and macros, etc,
|
9
9
|
# in spec/support/ and its subdirectories.
|
10
10
|
Dir[File.join(File.expand_path('../support', __FILE__), '**/*.rb')].each {|f| require f}
|
11
|
+
Dir[File.join(File.expand_path('../tasks', __FILE__), '**/*.rb')].each {|f| require f}
|
11
12
|
|
12
13
|
RSpec.configure do |config|
|
13
14
|
config.color_enabled = true
|
14
|
-
config.after :each do
|
15
|
-
SharedWorkforce::Task.clear!
|
16
|
-
end
|
17
15
|
|
18
16
|
config.before :each do
|
19
17
|
SharedWorkforce.configure do |config|
|
@@ -3,7 +3,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
|
3
3
|
describe "TaskRequest::BlackHole" do
|
4
4
|
describe "#create" do
|
5
5
|
it "should not make an HTTP request" do
|
6
|
-
task_request = SharedWorkforce::TaskRequest::BlackHole.new(
|
6
|
+
task_request = SharedWorkforce::TaskRequest::BlackHole.new(ApprovePhotoTask.new, :callback_params=>{:resource_id=>'1234'})
|
7
7
|
task_request.create
|
8
8
|
a_request(:any, "api.sharedworkforce.com/tasks").should_not have_been_made
|
9
9
|
end
|
@@ -11,7 +11,7 @@ describe "TaskRequest::BlackHole" do
|
|
11
11
|
|
12
12
|
describe "#cancel" do
|
13
13
|
it "should not make an HTTP request" do
|
14
|
-
task_request = SharedWorkforce::TaskRequest::BlackHole.new(
|
14
|
+
task_request = SharedWorkforce::TaskRequest::BlackHole.new(ApprovePhotoTask.new, :callback_params=>{:resource_id=>'1234'})
|
15
15
|
task_request.create
|
16
16
|
a_request(:any, "api.sharedworkforce.com/tasks/cancel").should_not have_been_made
|
17
17
|
end
|