@@ -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 |