Merge remote-tracking branch 'origin/master' into scheduler_agent

Conflicts:
db/schema.rb

Akinori MUSHA 10 年之前
父節點
當前提交
8dc5244e6e

+ 2 - 1
Gemfile.lock

@@ -45,9 +45,10 @@ GEM
45 45
       extlib (>= 0.9.15)
46 46
       multi_json (>= 1.0.0)
47 47
     bcrypt (3.1.7)
48
-    better_errors (1.1.0)
48
+    better_errors (2.0.0)
49 49
       coderay (>= 1.0.0)
50 50
       erubis (>= 2.6.6)
51
+      rack (>= 0.9.0)
51 52
     binding_of_caller (0.7.2)
52 53
       debug_inspector (>= 0.0.1)
53 54
     bootstrap-kaminari-views (0.0.3)

+ 2 - 0
README.md

@@ -71,6 +71,8 @@ If you need more detailed instructions, see the [Novice setup guide][novice-setu
71 71
 
72 72
 ## Deployment
73 73
 
74
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
75
+
74 76
 Huginn can run on Heroku for free!  Please see [the Huginn Wiki](https://github.com/cantino/huginn/wiki#deploying-huginn) for detailed deployment strategies for different providers.
75 77
 
76 78
 ### Optional Setup

+ 23 - 0
app.json

@@ -0,0 +1,23 @@
1
+{
2
+    "name": "Huginn",
3
+    "description": "Build agents that monitor and act on your behalf.  Your agents are standing by!",
4
+    "website": "https://github.com/cantino/huginn",
5
+    "repository": "https://github.com/cantino/huginn",
6
+    "env": {
7
+        "BUILDPACK_URL": "https://github.com/ddollar/heroku-buildpack-multi.git",
8
+        "APP_SECRET_TOKEN": {
9
+            "generator": "secret"
10
+        },
11
+        "PROCFILE_PATH": "deployment/heroku/Procfile.heroku",
12
+        "ON_HEROKU": "true",
13
+        "FORCE_SSL": "true",
14
+        "INVITATION_CODE": {
15
+            "generator": "secret"
16
+        },
17
+        "USE_GRAPHVIZ_DOT": "dot"
18
+    },
19
+    "scripts": {
20
+      "postdeploy": "bundle exec rake db:migrate"
21
+    },
22
+    "success_url": "/users/sign_up"
23
+}

+ 31 - 0
app/assets/stylesheets/application.css.scss.erb

@@ -117,6 +117,37 @@ span.not-applicable:after {
117 117
   }
118 118
 }
119 119
 
120
+// Heroku
121
+
122
+.heroku-instructions {
123
+  color: rgba(255, 255, 255, 0.9);
124
+  background-color: rgba(132, 132, 196, 0.8);
125
+  border: 3px solid rgba(132, 132, 196, 0.5);
126
+  -webkit-border-radius: 4px;
127
+     -moz-border-radius: 4px;
128
+          border-radius: 4px;
129
+  padding: 12px;
130
+  margin-bottom: 10px;
131
+
132
+  a {
133
+    text-decoration: underline;
134
+  }
135
+
136
+  a:link {
137
+    color: inherit;
138
+  }
139
+
140
+  a:hover, a:visited, a:active {
141
+    color: #fff;
142
+  }
143
+
144
+  pre {
145
+    background-color: #ffffff;
146
+    color: #3F3F44;
147
+    margin: 4px;
148
+  }
149
+}
150
+
120 151
 // Logs
121 152
 
122 153
 #logs .action-icon {

+ 15 - 3
app/models/agents/human_task_agent.rb

@@ -62,6 +62,8 @@ module Agents
62 62
       which contain `key` and `text`.  For _free\\_text_, the special configuration options are all optional, and are
63 63
       `default`, `min_length`, and `max_length`.
64 64
 
65
+      By default, all answers are emitted in a single event.  If you'd like separate events for each answer, set `separate_answers` to `true`.
66
+
65 67
       # Combining answers
66 68
 
67 69
       There are a couple of ways to combine HITs that have multiple `assignments`, all of which involve setting `combination_mode` at the top level.
@@ -105,7 +107,7 @@ module Agents
105 107
             }
106 108
           }
107 109
 
108
-      Resulting events will have the original `answers`, as well as the `poll` results, and a field called `best_answer` that contains the best answer as determined by the poll.
110
+      Resulting events will have the original `answers`, as well as the `poll` results, and a field called `best_answer` that contains the best answer as determined by the poll.  (Note that `separate_answers` won't work when doing a poll.)
109 111
 
110 112
       # Other settings
111 113
 
@@ -351,8 +353,18 @@ module Agents
351 353
 
352 354
               log "Poll HIT created with ID #{poll_hit.id} and URL #{poll_hit.url}.  Original HIT: #{hit_id}", :inbound_event => inbound_event
353 355
             else
354
-              event = create_event :payload => payload
355
-              log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event
356
+              if options[:separate_answers]
357
+                payload['answers'].each.with_index do |answer, index|
358
+                  sub_payload = payload.dup
359
+                  sub_payload.delete('answers')
360
+                  sub_payload['answer'] = answer
361
+                  event = create_event :payload => sub_payload
362
+                  log "Event emitted with answer ##{index}", :outbound_event => event, :inbound_event => inbound_event
363
+                end
364
+              else
365
+                event = create_event :payload => payload
366
+                log "Event emitted with answer(s)", :outbound_event => event, :inbound_event => inbound_event
367
+              end
356 368
             end
357 369
           end
358 370
 

+ 1 - 1
app/models/agents/java_script_agent.rb

@@ -13,7 +13,7 @@ module Agents
13 13
       You can implement `Agent.check` and `Agent.receive` as you see fit.  The following methods will be available on Agent in the JavaScript environment:
14 14
 
15 15
       * `this.createEvent(payload)`
16
-      * `this.incomingEvents()`
16
+      * `this.incomingEvents()` (the returned event objects will each have a `payload` property)
17 17
       * `this.memory()`
18 18
       * `this.memory(key)`
19 19
       * `this.memory(keyToSet, valueToSet)`

+ 4 - 0
app/models/event.rb

@@ -16,6 +16,9 @@ class Event < ActiveRecord::Base
16 16
   belongs_to :user
17 17
   belongs_to :agent, :counter_cache => true, :touch => :last_event_at
18 18
 
19
+  has_many :agent_logs_as_inbound_event, :class_name => "AgentLog", :foreign_key => :inbound_event_id, :dependent => :nullify
20
+  has_many :agent_logs_as_outbound_event, :class_name => "AgentLog", :foreign_key => :outbound_event_id, :dependent => :nullify
21
+
19 22
   scope :recent, lambda { |timespan = 12.hours.ago|
20 23
     where("events.created_at > ?", timespan)
21 24
   }
@@ -36,6 +39,7 @@ class Event < ActiveRecord::Base
36 39
   end
37 40
 
38 41
   protected
42
+
39 43
   def possibly_propagate
40 44
     #immediately schedule agents that want immediate updates
41 45
     propagate_ids = agent.receivers.where(:propagate_immediately => true).pluck(:id)

+ 22 - 0
app/views/devise/registrations/new.html.erb

@@ -7,6 +7,28 @@
7 7
 
8 8
         <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :class => 'form-horizontal' }) do |f| %>
9 9
           <%= devise_error_messages! %>
10
+          <% if ENV['ON_HEROKU'] && User.count.zero? %>
11
+          <div class="heroku-instructions">
12
+            <% app_name = request.host[/\A[^.]+/] %>
13
+            <p>If you are the owner of this application, take the following steps to complete the setup:</p>
14
+
15
+            <ul>
16
+              <li>Read <a href="https://github.com/cantino/huginn/wiki/Run-Huginn-for-free-on-Heroku" target="_target">this document</a> carefully if you are going to try out Huginn for free on <a href="https://id.heroku.com/" target="_target">Heroku</a>.</li>
17
+
18
+              <li>Install the <a href="https://toolbelt.heroku.com/" target="_target">Heroku Toolbelt</a> and run <kbd>heroku login</kbd> if you haven't already.</li>
19
+
20
+              <li>Run the following commands:<br />
21
+                <%= content_tag :pre do -%>
22
+heroku git:clone <%= content_tag :var, app_name %>
23
+cd <%= content_tag :var, app_name %>
24
+bundle
25
+bin/setup_heroku
26
+<%- end %>
27
+
28
+              <li>Get back to this page and sign up with the invitation code shown by the last command.</li>
29
+            </ul>
30
+          </div>
31
+          <% end %>
10 32
 
11 33
           <div class="form-group">
12 34
             <%= f.label :invitation_code, :class => 'col-md-4 control-label' %>

+ 20 - 2
bin/setup_heroku

@@ -48,6 +48,7 @@ def set_value(key, value, options = {})
48 48
   if $config[key].nil? || $config[key] == '' || ($config[key] != value && options[:force] != false)
49 49
     puts "Setting #{key} to #{value}" unless options[:silent]
50 50
     puts capture("heroku config:set #{key}=#{value}")
51
+    $config[key] = value
51 52
   end
52 53
 end
53 54
 
@@ -94,11 +95,15 @@ unless $config['APP_SECRET_TOKEN']
94 95
   puts capture("heroku config:set APP_SECRET_TOKEN=`rake secret`")
95 96
 end
96 97
 
98
+unless $config['DOMAIN']
99
+  set_value 'DOMAIN', "#{app_name}.herokuapp.com", force: false
100
+  first_time = true
101
+end
102
+
97 103
 set_value 'BUILDPACK_URL', "https://github.com/ddollar/heroku-buildpack-multi.git"
98 104
 set_value 'PROCFILE_PATH', "deployment/heroku/Procfile.heroku", force: false
99 105
 set_value 'ON_HEROKU', "true"
100 106
 set_value 'FORCE_SSL', "true"
101
-set_value 'DOMAIN', "#{app_name}.herokuapp.com", force: false
102 107
 set_value 'USE_GRAPHVIZ_DOT', 'dot'
103 108
 
104 109
 unless $config['INVITATION_CODE']
@@ -133,6 +138,19 @@ unless $config['SMTP_DOMAIN'] && $config['SMTP_USER_NAME'] && $config['SMTP_PASS
133 138
   end
134 139
 end
135 140
 
141
+if first_time
142
+  puts "Restarting..."
143
+  puts capture("heroku restart")
144
+
145
+  puts "Done!"
146
+  puts
147
+  puts "Visit https://#{app_name}.herokuapp.com/users/sign_up and use the invitation code shown below:"
148
+  puts
149
+  puts "\t#{$config['INVITATION_CODE']}"
150
+
151
+  exit
152
+end
153
+
136 154
 branch = capture("git rev-parse --abbrev-ref HEAD")
137 155
 if yes?("Should I push your current branch (#{branch}) to heroku?")
138 156
   puts "This may take a moment..."
@@ -158,4 +176,4 @@ if yes?("Should I push your current branch (#{branch}) to heroku?")
158 176
 end
159 177
 
160 178
 puts
161
-puts "Done!"
179
+puts "Done!"

+ 3 - 0
config/initializers/ar_mysql_column_charset.rb

@@ -0,0 +1,3 @@
1
+ActiveSupport.on_load :active_record do
2
+  require 'ar_mysql_column_charset'
3
+end

+ 74 - 0
db/migrate/20140813110107_set_charset_for_mysql.rb

@@ -0,0 +1,74 @@
1
+class SetCharsetForMysql < ActiveRecord::Migration
2
+  def all_models
3
+    @all_models ||= [
4
+      Agent,
5
+      AgentLog,
6
+      Contact,
7
+      Event,
8
+      Link,
9
+      Scenario,
10
+      ScenarioMembership,
11
+      User,
12
+      UserCredential,
13
+      Delayed::Job,
14
+    ]
15
+  end
16
+
17
+  def change
18
+    conn = ActiveRecord::Base.connection
19
+
20
+    # This is migration is for MySQL only.
21
+    return unless conn.is_a?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
22
+
23
+    reversible do |dir|
24
+      dir.up do
25
+        all_models.each { |model|
26
+          table_name = model.table_name
27
+
28
+          # `contacts` may not exist
29
+          next unless connection.table_exists? table_name
30
+
31
+          model.columns.each { |column|
32
+            name = column.name
33
+            type = column.type
34
+            limit = column.limit
35
+            options = {
36
+              limit: limit,
37
+              null: column.null,
38
+              default: column.default,
39
+            }
40
+
41
+            case type
42
+            when :string, :text
43
+              options.update(charset: 'utf8', collation: 'utf8_unicode_ci')
44
+              case name
45
+              when 'username'
46
+                options.update(limit: 767 / 4, charset: 'utf8mb4', collation: 'utf8mb4_unicode_ci')
47
+              when 'message', 'options', 'name', 'memory',
48
+                   'handler', 'last_error', 'payload', 'description'
49
+                options.update(charset: 'utf8mb4', collation: 'utf8mb4_bin')
50
+              when 'type', 'schedule', 'mode', 'email',
51
+                   'invitation_code', 'reset_password_token'
52
+                options.update(collation: 'utf8_bin')
53
+              when 'guid', 'encrypted_password'
54
+                options.update(charset: 'ascii', collation: 'ascii_bin')
55
+              end
56
+            else
57
+              next
58
+            end
59
+
60
+            change_column table_name, name, type, options
61
+          }
62
+
63
+          execute 'ALTER TABLE %s CHARACTER SET utf8 COLLATE utf8_unicode_ci' % table_name
64
+        }
65
+
66
+        execute 'ALTER DATABASE %s CHARACTER SET utf8 COLLATE utf8_unicode_ci' % conn.current_database
67
+      end
68
+
69
+      dir.down do
70
+        # Do nada; no use to go back
71
+      end
72
+    end
73
+  end
74
+end

+ 37 - 37
db/schema.rb

@@ -15,7 +15,7 @@ ActiveRecord::Schema.define(version: 20140901143732) do
15 15
 
16 16
   create_table "agent_logs", force: true do |t|
17 17
     t.integer  "agent_id",                      null: false
18
-    t.text     "message",                       null: false
18
+    t.text     "message",           limit: 16777215,             null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
19 19
     t.integer  "level",             default: 3, null: false
20 20
     t.integer  "inbound_event_id"
21 21
     t.integer  "outbound_event_id"
@@ -25,24 +25,24 @@ ActiveRecord::Schema.define(version: 20140901143732) do
25 25
 
26 26
   create_table "agents", force: true do |t|
27 27
     t.integer  "user_id"
28
-    t.text     "options"
29
-    t.string   "type"
30
-    t.string   "name"
31
-    t.string   "schedule"
28
+    t.text     "options",               limit: 16777215,                                charset: "utf8mb4", collation: "utf8mb4_bin"
29
+    t.string   "type",                                                                                      collation: "utf8_bin"
30
+    t.string   "name",                                                                  charset: "utf8mb4", collation: "utf8mb4_bin"
31
+    t.string   "schedule",                                                                                  collation: "utf8_bin"
32 32
     t.integer  "events_count"
33 33
     t.datetime "last_check_at"
34 34
     t.datetime "last_receive_at"
35 35
     t.integer  "last_checked_event_id"
36
-    t.datetime "created_at"
37
-    t.datetime "updated_at"
38
-    t.text     "memory",                limit: 2147483647
36
+    t.datetime "created_at",                                               null: false
37
+    t.datetime "updated_at",                                               null: false
38
+    t.text     "memory",                limit: 2147483647,                              charset: "utf8mb4", collation: "utf8mb4_bin"
39 39
     t.datetime "last_web_request_at"
40 40
     t.integer  "keep_events_for",                          default: 0,     null: false
41 41
     t.datetime "last_event_at"
42 42
     t.datetime "last_error_log_at"
43
-    t.boolean  "propagate_immediately",                    default: false, null: false
44
-    t.boolean  "disabled",                                 default: false, null: false
45
-    t.string   "guid",                                                     null: false
43
+    t.boolean  "propagate_immediately", default: false, null: false
44
+    t.boolean  "disabled",              default: false, null: false
45
+    t.string   "guid",                                                     null: false, charset: "ascii",   collation: "ascii_bin"
46 46
     t.integer  "service_id"
47 47
   end
48 48
 
@@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 20140901143732) do
64 64
   create_table "delayed_jobs", force: true do |t|
65 65
     t.integer  "priority",                    default: 0
66 66
     t.integer  "attempts",                    default: 0
67
-    t.text     "handler",    limit: 16777215
68
-    t.text     "last_error"
67
+    t.text     "handler",    limit: 16777215,                          charset: "utf8mb4", collation: "utf8mb4_bin"
68
+    t.text     "last_error", limit: 16777215,                          charset: "utf8mb4", collation: "utf8mb4_bin"
69 69
     t.datetime "run_at"
70 70
     t.datetime "locked_at"
71 71
     t.datetime "failed_at"
@@ -80,11 +80,11 @@ ActiveRecord::Schema.define(version: 20140901143732) do
80 80
   create_table "events", force: true do |t|
81 81
     t.integer  "user_id"
82 82
     t.integer  "agent_id"
83
-    t.decimal  "lat",                         precision: 15, scale: 10
84
-    t.decimal  "lng",                         precision: 15, scale: 10
85
-    t.text     "payload",    limit: 16777215
86
-    t.datetime "created_at"
87
-    t.datetime "updated_at"
83
+    t.decimal  "lat",                           precision: 15, scale: 10
84
+    t.decimal  "lng",                           precision: 15, scale: 10
85
+    t.text     "payload",    limit: 2147483647,                                        charset: "utf8mb4", collation: "utf8mb4_bin"
86
+    t.datetime "created_at",                                              null: false
87
+    t.datetime "updated_at",                                              null: false
88 88
     t.datetime "expires_at"
89 89
   end
90 90
 
@@ -114,13 +114,13 @@ ActiveRecord::Schema.define(version: 20140901143732) do
114 114
   add_index "scenario_memberships", ["scenario_id"], name: "index_scenario_memberships_on_scenario_id", using: :btree
115 115
 
116 116
   create_table "scenarios", force: true do |t|
117
-    t.string   "name",                         null: false
118
-    t.integer  "user_id",                      null: false
117
+    t.string   "name",                        null: false, charset: "utf8mb4", collation: "utf8mb4_bin"
118
+    t.integer  "user_id",                     null: false
119 119
     t.datetime "created_at"
120 120
     t.datetime "updated_at"
121
-    t.text     "description"
122
-    t.boolean  "public",       default: false, null: false
123
-    t.string   "guid",                         null: false
121
+    t.text     "description",                              charset: "utf8mb4", collation: "utf8mb4_bin"
122
+    t.boolean  "public",      default: false, null: false
123
+    t.string   "guid",                        null: false, charset: "ascii",   collation: "ascii_bin"
124 124
     t.string   "source_url"
125 125
     t.string   "tag_bg_color"
126 126
     t.string   "tag_fg_color"
@@ -151,33 +151,33 @@ ActiveRecord::Schema.define(version: 20140901143732) do
151 151
     t.integer  "user_id",                           null: false
152 152
     t.string   "credential_name",                   null: false
153 153
     t.text     "credential_value",                  null: false
154
-    t.datetime "created_at"
155
-    t.datetime "updated_at"
156
-    t.string   "mode",             default: "text", null: false
154
+    t.datetime "created_at",                        null: false
155
+    t.datetime "updated_at",                        null: false
156
+    t.string   "mode",             default: "text", null: false, collation: "utf8_bin"
157 157
   end
158 158
 
159 159
   add_index "user_credentials", ["user_id", "credential_name"], name: "index_user_credentials_on_user_id_and_credential_name", unique: true, using: :btree
160 160
 
161 161
   create_table "users", force: true do |t|
162
-    t.string   "email",                  default: "",    null: false
163
-    t.string   "encrypted_password",     default: "",    null: false
164
-    t.string   "reset_password_token"
162
+    t.string   "email",                              default: "",    null: false,                     collation: "utf8_bin"
163
+    t.string   "encrypted_password",                 default: "",    null: false, charset: "ascii",   collation: "ascii_bin"
164
+    t.string   "reset_password_token",                                                                collation: "utf8_bin"
165 165
     t.datetime "reset_password_sent_at"
166 166
     t.datetime "remember_created_at"
167
-    t.integer  "sign_in_count",          default: 0
167
+    t.integer  "sign_in_count",                      default: 0
168 168
     t.datetime "current_sign_in_at"
169 169
     t.datetime "last_sign_in_at"
170 170
     t.string   "current_sign_in_ip"
171 171
     t.string   "last_sign_in_ip"
172
-    t.datetime "created_at"
173
-    t.datetime "updated_at"
174
-    t.boolean  "admin",                  default: false, null: false
175
-    t.integer  "failed_attempts",        default: 0
172
+    t.datetime "created_at",                                         null: false
173
+    t.datetime "updated_at",                                         null: false
174
+    t.boolean  "admin",                              default: false, null: false
175
+    t.integer  "failed_attempts",                    default: 0
176 176
     t.string   "unlock_token"
177 177
     t.datetime "locked_at"
178
-    t.string   "username",                               null: false
179
-    t.string   "invitation_code",                        null: false
180
-    t.integer  "scenario_count",         default: 0,     null: false
178
+    t.string   "username",               limit: 191,                 null: false, charset: "utf8mb4", collation: "utf8mb4_unicode_ci"
179
+    t.string   "invitation_code",                                    null: false,                     collation: "utf8_bin"
180
+    t.integer  "scenario_count",                     default: 0,     null: false
181 181
   end
182 182
 
183 183
   add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree

+ 110 - 0
lib/ar_mysql_column_charset.rb

@@ -0,0 +1,110 @@
1
+require 'active_record'
2
+
3
+# Module#prepend support for Ruby 1.9
4
+require 'prepend' unless Module.method_defined?(:prepend)
5
+
6
+module ActiveRecord::ConnectionAdapters
7
+  class ColumnDefinition
8
+    module CharsetSupport
9
+      attr_accessor :charset, :collation
10
+    end
11
+
12
+    prepend CharsetSupport
13
+  end
14
+
15
+  class TableDefinition
16
+    module CharsetSupport
17
+      def new_column_definition(name, type, options)
18
+        column = super
19
+        column.charset   = options[:charset]
20
+        column.collation = options[:collation]
21
+        column
22
+      end
23
+    end
24
+
25
+    prepend CharsetSupport
26
+  end
27
+
28
+  class AbstractMysqlAdapter
29
+    module CharsetSupport
30
+      def prepare_column_options(column, types)
31
+        spec = super
32
+        conn = ActiveRecord::Base.connection
33
+        spec[:charset]   = column.charset.inspect if column.charset && column.charset != conn.charset
34
+        spec[:collation] = column.collation.inspect if column.collation && column.collation != conn.collation
35
+        spec
36
+      end
37
+
38
+      def migration_keys
39
+        super + [:charset, :collation]
40
+      end
41
+
42
+      def utf8mb4_supported?
43
+        if @utf8mb4_supported.nil?
44
+          @utf8mb4_supported = !select("show character set like 'utf8mb4'").empty?
45
+        else
46
+          @utf8mb4_supported
47
+        end
48
+      end
49
+
50
+      def charset_collation(charset, collation)
51
+        [charset, collation].map { |name|
52
+          case name
53
+          when nil
54
+            nil
55
+          when /\A(utf8mb4(_\w*)?)\z/
56
+            if utf8mb4_supported?
57
+              $1
58
+            else
59
+              "utf8#{$2}"
60
+            end
61
+          else
62
+            name.to_s
63
+          end
64
+        }
65
+      end
66
+    end
67
+
68
+    prepend CharsetSupport
69
+
70
+    class SchemaCreation
71
+      module CharsetSupport
72
+        def column_options(o)
73
+          column_options = super
74
+          column_options[:charset]   = o.charset unless o.charset.nil?
75
+          column_options[:collation] = o.collation unless o.collation.nil?
76
+          column_options
77
+        end
78
+
79
+        def add_column_options!(sql, options)
80
+          charset, collation = @conn.charset_collation(options[:charset], options[:collation])
81
+
82
+          if charset
83
+            sql << " CHARACTER SET #{charset}"
84
+          end
85
+
86
+          if collation
87
+            sql << " COLLATE #{collation}"
88
+          end
89
+
90
+          super
91
+        end
92
+      end
93
+
94
+      prepend CharsetSupport
95
+    end
96
+
97
+    class Column
98
+      module CharsetSupport
99
+        attr_reader :charset
100
+
101
+        def initialize(*args)
102
+          super
103
+          @charset = @collation[/\A[^_]+/] unless @collation.nil?
104
+        end
105
+      end
106
+
107
+      prepend CharsetSupport
108
+    end
109
+  end
110
+end

+ 85 - 0
lib/prepend.rb

@@ -0,0 +1,85 @@
1
+# Fake implementation of prepend(), which does not support overriding
2
+# inherited methods nor methods that are formerly overridden by
3
+# another invocation of prepend().
4
+#
5
+# Here's what <Original>.prepend(<Wrapper>) does:
6
+#
7
+# - Create an anonymous stub module (hereinafter <Stub>) and define
8
+#   <Stub>#<method> that calls #<method>_without_<Wrapper> for each
9
+#   instance method of <Wrapper>.
10
+#
11
+# - Rename <Original>#<method> to #<method>_without_<Wrapper> for each
12
+#   instance method of <Wrapper>.
13
+#
14
+# - Include <Stub> and <Wrapper> into <Original> in that order.
15
+#
16
+# This way, a call of <Original>#<method> is dispatched to
17
+# <Wrapper><method>, which may call super which is dispatched to
18
+# <Stub>#<method>, which finally calls
19
+# <Original>#<method>_without_<Wrapper> which is used to be called
20
+# <Original>#<method>.
21
+#
22
+# Usage:
23
+#
24
+#     class Mechanize
25
+#       # module with methods that overrides those of X
26
+#       module Y
27
+#       end
28
+#
29
+#       unless X.respond_to?(:prepend, true)
30
+#         require 'mechanize/prependable'
31
+#         X.extend(Prependable)
32
+#       end
33
+#
34
+#       class X
35
+#         prepend Y
36
+#       end
37
+#     end
38
+class Module
39
+  def prepend(mod)
40
+    stub = Module.new
41
+
42
+    mod_id = (mod.name || 'Module__%d' % mod.object_id).gsub(/::/, '__')
43
+
44
+    mod.instance_methods.each { |name|
45
+      method_defined?(name) or next
46
+
47
+      original = instance_method(name)
48
+      next if original.owner != self
49
+
50
+      name = name.to_s
51
+      name_without = name.sub(/(?=[?!=]?\z)/) { '_without_%s' % mod_id }
52
+
53
+      arity = original.arity
54
+      arglist = (
55
+        if arity >= 0
56
+          (1..arity).map { |i| 'x%d' % i }
57
+        else
58
+          (1..(-arity - 1)).map { |i| 'x%d' % i } << '*a'
59
+        end << '&b'
60
+      ).join(', ')
61
+
62
+      if name.end_with?('=')
63
+        stub.module_eval %{
64
+          def #{name}(#{arglist})
65
+            __send__(:#{name_without}, #{arglist})
66
+          end
67
+        }
68
+      else
69
+        stub.module_eval %{
70
+          def #{name}(#{arglist})
71
+            #{name_without}(#{arglist})
72
+          end
73
+        }
74
+      end
75
+      module_eval {
76
+        alias_method name_without, name
77
+        remove_method name
78
+      }
79
+    }
80
+
81
+    include stub
82
+    include mod
83
+  end
84
+  private :prepend
85
+end unless Module.method_defined?(:prepend)

+ 2 - 0
spec/fixtures/agent_logs.yml

@@ -1,9 +1,11 @@
1 1
 log_for_jane_website_agent:
2 2
   agent: jane_website_agent
3
+  outbound_event: jane_website_agent_event
3 4
   message: "fetching some website data"
4 5
 
5 6
 log_for_bob_website_agent:
6 7
   agent: bob_website_agent
8
+  outbound_event: bob_website_agent_event
7 9
   message: "fetching some other website data"
8 10
 
9 11
 first_log_for_bob_weather_agent:

+ 43 - 20
spec/models/agents/human_task_agent_spec.rb

@@ -345,30 +345,53 @@ describe Agents::HumanTaskAgent do
345 345
       @checker.memory['hits'].should == { "JH3132836336DHG" => { 'event_id' => @event.id } }
346 346
     end
347 347
 
348
-    it "should create events when all assignments are ready" do
349
-      @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } }
350
-      mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } }
351
-      assignments = [
352
-        FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"neutral", "feedback"=>""}),
353
-        FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"happy", "feedback"=>"Take 2"})
354
-      ]
355
-      hit = FakeHit.new(:max_assignments => 2, :assignments => assignments)
356
-      hit.should_not be_disposed
357
-      mock(RTurk::Hit).new("JH3132836336DHG") { hit }
348
+    context "emitting events" do
349
+      before do
350
+        @checker.memory['hits'] = { "JH3132836336DHG" => { 'event_id' => @event.id } }
351
+        mock(RTurk::GetReviewableHITs).create { mock!.hit_ids { %w[JH3132836336DHG JH39AA63836DHG JH39AA63836DH12345] } }
352
+        @assignments = [
353
+          FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"neutral", "feedback"=>""}),
354
+          FakeAssignment.new(:status => "Submitted", :answers => {"sentiment"=>"happy", "feedback"=>"Take 2"})
355
+        ]
356
+        @hit = FakeHit.new(:max_assignments => 2, :assignments => @assignments)
357
+        @hit.should_not be_disposed
358
+        mock(RTurk::Hit).new("JH3132836336DHG") { @hit }
359
+      end
358 360
 
359
-      lambda {
360
-        @checker.send :review_hits
361
-      }.should change { Event.count }.by(1)
361
+      it "should create events when all assignments are ready" do
362
+        lambda {
363
+          @checker.send :review_hits
364
+        }.should change { Event.count }.by(1)
362 365
 
363
-      assignments.all? {|a| a.approved == true }.should be_truthy
364
-      hit.should be_disposed
366
+        @assignments.all? {|a| a.approved == true }.should be_truthy
367
+        @hit.should be_disposed
365 368
 
366
-      @checker.events.last.payload['answers'].should == [
367
-        {'sentiment' => "neutral", 'feedback' => ""},
368
-        {'sentiment' => "happy", 'feedback' => "Take 2"}
369
-      ]
369
+        @checker.events.last.payload['answers'].should == [
370
+          {'sentiment' => "neutral", 'feedback' => ""},
371
+          {'sentiment' => "happy", 'feedback' => "Take 2"}
372
+        ]
373
+
374
+        @checker.memory['hits'].should == {}
375
+      end
376
+
377
+      it "should emit separate answers when options[:separate_answers] is true" do
378
+        @checker.options[:separate_answers] = true
370 379
 
371
-      @checker.memory['hits'].should == {}
380
+        lambda {
381
+          @checker.send :review_hits
382
+        }.should change { Event.count }.by(2)
383
+
384
+        @assignments.all? {|a| a.approved == true }.should be_truthy
385
+        @hit.should be_disposed
386
+
387
+        event1, event2 = @checker.events.last(2)
388
+        event1.payload.should_not have_key('answers')
389
+        event2.payload.should_not have_key('answers')
390
+        event1.payload['answer'].should == { 'sentiment' => "happy", 'feedback' => "Take 2" }
391
+        event2.payload['answer'].should == { 'sentiment' => "neutral", 'feedback' => "" }
392
+
393
+        @checker.memory['hits'].should == {}
394
+      end
372 395
     end
373 396
 
374 397
     describe "taking majority votes" do

+ 12 - 0
spec/models/event_spec.rb

@@ -75,6 +75,18 @@ describe Event do
75 75
       Event.find_by_id(event.id).should_not be_nil
76 76
     end
77 77
   end
78
+
79
+  describe "after destroy" do
80
+    it "nullifies any dependent AgentLogs" do
81
+      agent_logs(:log_for_jane_website_agent).outbound_event_id.should be_present
82
+      agent_logs(:log_for_bob_website_agent).outbound_event_id.should be_present
83
+
84
+      agent_logs(:log_for_bob_website_agent).outbound_event.destroy
85
+
86
+      agent_logs(:log_for_jane_website_agent).reload.outbound_event_id.should be_present
87
+      agent_logs(:log_for_bob_website_agent).reload.outbound_event_id.should be_nil
88
+    end
89
+  end
78 90
 end
79 91
 
80 92
 describe EventDrop do