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 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
@@ -2,3 +2,7 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in hci.gemspec
4
4
  gemspec
5
+
6
+ platforms :jruby do
7
+ gem "jruby-openssl"
8
+ end
data/README.markdown CHANGED
@@ -1,6 +1,8 @@
1
1
  Shared Workforce Client
2
2
  =======================
3
3
 
4
+ [![Build Status](https://secure.travis-ci.org/samoli/shared-workforce.png)](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 have a photo tagged on upload, create your first task in a file called tasks/tag_photo.rb
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
- SharedWorkforce::Task.define "Tag photo" do |t|
49
-
50
- t.directions = "Look at this photo. Please tick all that apply."
51
- t.answer_options = ['Offensive', 'Contains Nudity', 'Blurry or low quality']
52
- t.answer_type = :tags
53
- t.responses_required = 1
54
- t.replace = true
55
-
56
- t.on_completion do |result|
57
- photo = Photo.find(result.callback_params['photo_id'])
58
- if result.answers.include?('Offensive')
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 hidden because it was offensive")
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 SharedWorkforce::Task.request(name, options). If you are using Rails, this could be done in an after save callback on a model:
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 :request_tags
81
+ after_create :approve_photo
78
82
 
79
- def request_tags
80
- SharedWorkforce::Task.request "Tag photo", {:image_url => self.url, :callback_params => { :photo_id => self.id} }
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
- SharedWorkforce::Task.define "Tag photo" do |t|
103
-
104
- t.directions = "Look at this photo. Please tick all that apply."
105
- t.answer_options = ['Offensive', 'Contains Nudity', 'Blurry or low quality']
106
- t.answer_type = :tags
107
- t.responses_required = 3
108
-
109
- t.on_completion do |result|
110
- photo = Photo.find(result.callback_params['photo_id'])
111
- photo.hide! if result.answers.all? { |a| a == 'Contains Nudity' }
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
- SharedWorkforce::Task.define "Tag photo" do |t|
128
+ class ApprovePhotoTask
129
+ include SharedWorkforce::Task
121
130
  ...
122
- t.replace=true
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 < ActiveRecord::Base
139
+ class Photo
131
140
  after_destroy :cancel_tagging_request
132
141
 
133
142
  def cancel_tagging_request
134
- SharedWorkforce::Task.cancel "Classify photo", :callback_params=>{:photo_id=>self.id})
143
+ ApprovePhotoTask.cancel(self)
135
144
  end
136
145
  end
137
146
 
@@ -1,17 +1,9 @@
1
1
  module SharedWorkforce
2
2
  class Client
3
3
  class << self
4
- attr_accessor :load_path
5
-
6
4
  def version
7
5
  SharedWorkforce::VERSION
8
6
  end
9
-
10
- def load!
11
- Dir[File.join(load_path, "*.rb")].each do |file|
12
- load file
13
- end
14
- end
15
7
  end
16
8
  end
17
9
  end
@@ -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
 
@@ -1,4 +1,5 @@
1
1
  module SharedWorkforce
2
2
  class Error < RuntimeError; end
3
3
  class ConfigurationError < Error; end
4
+ class TaskNotFound < Error; end
4
5
  end
@@ -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
- class Task
3
-
4
- class << self
5
-
6
- attr_accessor :tasks
7
-
8
- def tasks
9
- @tasks ||= {}
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 define(name, &block)
13
- task = self.new
14
- task.name = name
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 request(name, options)
20
- @tasks[name].request(options)
45
+
46
+ def get_default_attribute(name)
47
+ @default_attributes ||= {}
48
+ @default_attributes[name]
21
49
  end
22
-
23
- def cancel(name, options)
24
- @tasks[name].cancel(options)
50
+
51
+ def create(*args)
52
+ task = new(*args)
53
+ task.request
54
+ task
25
55
  end
26
-
27
- def find(name)
28
- self.tasks[name]
56
+
57
+ def cancel(*args)
58
+ task = new(*args)
59
+ task.cancel
60
+ task
29
61
  end
30
-
31
- def clear!
32
- @tasks = {}
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
- attr_accessor :name
38
- attr_accessor :directions
39
- attr_accessor :image_url
40
- attr_accessor :answer_options
41
- attr_accessor :answer_type
42
- attr_accessor :responses_required
43
- attr_accessor :replace # whether tasks with the same resource id and name should be overwritten
44
-
45
- def replace
46
- @replace ||= false
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 request(options)
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
- :name=>name,
62
- :directions => directions,
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
- end
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
- def fail!(results)
87
- @on_failure_proc.call(results) if @on_failure_proc
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
@@ -24,6 +24,10 @@ module SharedWorkforce
24
24
  def api_key
25
25
  SharedWorkforce.configuration.api_key
26
26
  end
27
+
28
+ def task
29
+ @task
30
+ end
27
31
 
28
32
  end
29
33
  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
- self.callback_params = params['callback_params']
11
- self.responses = TaskResponse.create_collection_from_array(params['responses'])
12
- self.name = params['name']
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 answers
16
- responses.map(&:answer).flatten
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 = Task.find(name)
25
- task.complete!(self)
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
@@ -1,4 +1,4 @@
1
1
  module SharedWorkforce
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
 
4
4
  end
@@ -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
 
@@ -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/pigment/shared_workforce"
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 = "shared_workforce_client"
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(SharedWorkforce::Task.find("Approve photo"), :callback_params=>{:resource_id=>'1234'})
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(SharedWorkforce::Task.find("Approve photo"), :callback_params=>{:resource_id=>'1234'})
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