@@ -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) |
@@ -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 |
+[](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 |
@@ -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 |
+} |
@@ -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 { |
@@ -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 |
|
@@ -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)` |
@@ -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) |
@@ -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' %> |
@@ -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!" |
@@ -0,0 +1,3 @@ |
||
1 |
+ActiveSupport.on_load :active_record do |
|
2 |
+ require 'ar_mysql_column_charset' |
|
3 |
+end |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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) |
@@ -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: |
@@ -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 |
@@ -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 |