Sets the new user up with an example set of agents

- gives them a place to start from
- leverages existing code to import scenarios
- adds test coverage to seeds to ensure sees can run multiple times against the same db as is the case for Docker
- adds env variable to set if importing is turned on for new user
- adds env variable to point to a custom scenario for new users

Will Read 8 years ago
parent
commit
98f50de06b

+ 7 - 0
.env.example

@@ -81,6 +81,13 @@ UNLOCK_AFTER=1.hour
81 81
 # Duration for which the user will be remembered without asking for credentials again.
82 82
 REMEMBER_FOR=4.weeks
83 83
 
84
+# Set to 'true' if you would prefer new users to start with a default set of agents
85
+IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS=true
86
+
87
+# Users can be given a default set of agents to get them started
88
+# You can override this scenario with your own scenario via file path or URL
89
+# DEFAULT_SCENARIO_FILE=path-or-url-to-scenario.json
90
+
84 91
 #############################
85 92
 #    Email Configuration    #
86 93
 #############################

+ 1 - 0
app/controllers/admin/users_controller.rb

@@ -26,6 +26,7 @@ class Admin::UsersController < ApplicationController
26 26
 
27 27
     respond_to do |format|
28 28
       if @user.save
29
+        DefaultScenarioImporter.import(@user)    
29 30
         format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully created." }
30 31
         format.json { render json: @user, status: :ok, location: admin_users_path(@user) }
31 32
       else

+ 11 - 0
app/controllers/users/registrations_controller.rb

@@ -0,0 +1,11 @@
1
+module Users
2
+  class RegistrationsController < Devise::RegistrationsController
3
+    after_action :create_default_scenario, only: :create
4
+
5
+    private
6
+
7
+    def create_default_scenario
8
+      DefaultScenarioImporter.import(@user) if @user.persisted?
9
+    end
10
+  end
11
+end

+ 20 - 0
app/importers/default_scenario_importer.rb

@@ -0,0 +1,20 @@
1
+require 'open-uri'
2
+class DefaultScenarioImporter
3
+  def self.import(user)
4
+    return unless ENV['IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS'] == 'true'
5
+    seed(user)
6
+  end
7
+
8
+  def self.seed(user)
9
+    scenario_import = ScenarioImport.new()
10
+    scenario_import.set_user(user)
11
+    scenario_file = ENV['DEFAULT_SCENARIO_FILE'].presence || File.join(Rails.root, "data", "default_scenario.json")
12
+    begin
13
+      scenario_import.file = open(scenario_file)
14
+      raise "Import failed" unless scenario_import.valid? && scenario_import.import
15
+    ensure
16
+      scenario_import.file.close
17
+    end
18
+    return true
19
+  end
20
+end

app/models/scenario_import.rb → app/importers/scenario_import.rb


+ 4 - 1
config/routes.rb

@@ -82,7 +82,10 @@ Huginn::Application.routes.draw do
82 82
   post  "/users/:user_id/update_location/:secret" => "web_requests#update_location" # legacy
83 83
 
84 84
   devise_for :users,
85
-             controllers: { omniauth_callbacks: 'omniauth_callbacks' },
85
+             controllers: { 
86
+               omniauth_callbacks: 'omniauth_callbacks',
87
+               registrations: 'users/registrations'
88
+             },
86 89
              sign_out_via: [:post, :delete]
87 90
   
88 91
   if Rails.env.development?

+ 162 - 0
data/default_scenario.json

@@ -0,0 +1,162 @@
1
+{
2
+  "schema_version": 1,
3
+  "name": "default-scenario",
4
+  "description": "This scenario has a few agents to get you started. Feel free to change them or delete them as you see fit!",
5
+  "source_url": false,
6
+  "guid": "ee4299225e6531c401a8bbbce0771ce4",
7
+  "tag_fg_color": "#ffffff",
8
+  "tag_bg_color": "#5bc0de",
9
+  "exported_at": "2016-04-03T18:24:42Z",
10
+  "agents": [
11
+    {
12
+      "type": "Agents::TriggerAgent",
13
+      "name": "Rain Notifier",
14
+      "disabled": false,
15
+      "guid": "361ee2e955d4726b52c8b044d4f75e25",
16
+      "options": {
17
+        "expected_receive_period_in_days": "2",
18
+        "rules": [
19
+          {
20
+            "type": "regex",
21
+            "value": "rain|storm",
22
+            "path": "conditions"
23
+          }
24
+        ],
25
+        "message": "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}"
26
+      },
27
+      "keep_events_for": 0,
28
+      "propagate_immediately": false
29
+    },
30
+    {
31
+      "type": "Agents::WebsiteAgent",
32
+      "name": "XKCD Source",
33
+      "disabled": false,
34
+      "guid": "505c9bba65507c40e5786afff36f688c",
35
+      "options": {
36
+        "url": "http://xkcd.com",
37
+        "mode": "on_change",
38
+        "expected_update_period_in_days": 5,
39
+        "extract": {
40
+          "url": {
41
+            "css": "#comic img",
42
+            "value": "@src"
43
+          },
44
+          "title": {
45
+            "css": "#comic img",
46
+            "value": "@alt"
47
+          },
48
+          "hovertext": {
49
+            "css": "#comic img",
50
+            "value": "@title"
51
+          }
52
+        }
53
+      },
54
+      "schedule": "every_1d",
55
+      "keep_events_for": 0,
56
+      "propagate_immediately": false
57
+    },
58
+    {
59
+      "type": "Agents::EmailDigestAgent",
60
+      "name": "Afternoon Digest",
61
+      "disabled": false,
62
+      "guid": "65e8ae4533881537de3c346b5178b75d",
63
+      "options": {
64
+        "subject": "Your Afternoon Digest",
65
+        "expected_receive_period_in_days": "7"
66
+      },
67
+      "schedule": "5pm",
68
+      "propagate_immediately": false
69
+    },
70
+    {
71
+      "type": "Agents::EmailDigestAgent",
72
+      "name": "Morning Digest",
73
+      "disabled": false,
74
+      "guid": "b34eaee75d8dc67843c3bd257c213852",
75
+      "options": {
76
+        "subject": "Your Morning Digest",
77
+        "expected_receive_period_in_days": "30"
78
+      },
79
+      "schedule": "6am",
80
+      "propagate_immediately": false
81
+    },
82
+    {
83
+      "type": "Agents::WeatherAgent",
84
+      "name": "SF Weather Agent",
85
+      "disabled": false,
86
+      "guid": "bdae6dfdf9d01a123ddd513e695fd466",
87
+      "options": {
88
+        "location": "94103",
89
+        "api_key": "put-your-key-here"
90
+      },
91
+      "schedule": "10pm",
92
+      "keep_events_for": 0
93
+    },
94
+    {
95
+      "type": "Agents::WebsiteAgent",
96
+      "name": "iTunes Trailer Source",
97
+      "disabled": false,
98
+      "guid": "e9afa65457d0a736b9ec20a8dd452fc8",
99
+      "options": {
100
+        "url": "http://trailers.apple.com/trailers/home/rss/newtrailers.rss",
101
+        "mode": "on_change",
102
+        "type": "xml",
103
+        "expected_update_period_in_days": 5,
104
+        "extract": {
105
+          "title": {
106
+            "css": "item title",
107
+            "value": ".//text()"
108
+          },
109
+          "url": {
110
+            "css": "item link",
111
+            "value": ".//text()"
112
+          }
113
+        }
114
+      },
115
+      "schedule": "every_1d",
116
+      "keep_events_for": 0,
117
+      "propagate_immediately": false
118
+    },
119
+    {
120
+      "type": "Agents::EventFormattingAgent",
121
+      "name": "Comic Formatter",
122
+      "disabled": false,
123
+      "guid": "d86b069650edadfc61db9df767c8b65c",
124
+      "options": {
125
+        "instructions": {
126
+          "message": "<h2>{{title}}</h2><img src=\"{{url}}\"/> <p>{{hovertext}}</p>"
127
+        },
128
+        "matchers": [
129
+
130
+        ],
131
+        "mode": "clean"
132
+      },
133
+      "keep_events_for": 2592000,
134
+      "propagate_immediately": false
135
+    }
136
+  ],
137
+  "links": [
138
+    {
139
+      "source": 0,
140
+      "receiver": 3
141
+    },
142
+    {
143
+      "source": 1,
144
+      "receiver": 6
145
+    },
146
+    {
147
+      "source": 4,
148
+      "receiver": 0
149
+    },
150
+    {
151
+      "source": 5,
152
+      "receiver": 2
153
+    },
154
+    {
155
+      "source": 6,
156
+      "receiver": 2
157
+    }
158
+  ],
159
+  "control_links": [
160
+
161
+  ]
162
+}

+ 2 - 89
db/seeds.rb

@@ -1,93 +1,6 @@
1 1
 # This file should contain all the record creation needed to seed the database with its default values.
2 2
 # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 3
 
4
-user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com")
4
+require_relative 'seeds/seeder'
5 5
 
6
-if user.persisted?
7
-  puts "User with email '#{user.email}' already exists, not seeding."
8
-  exit
9
-end
10
-
11
-user.username = ENV['SEED_USERNAME'] || "admin"
12
-user.password = ENV['SEED_PASSWORD'] || "password"
13
-user.password_confirmation = ENV['SEED_PASSWORD'] || "password"
14
-user.invitation_code = User::INVITATION_CODES.first
15
-user.admin = true
16
-user.save!
17
-
18
-puts
19
-puts
20
-
21
-unless user.agents.where(:name => "SF Weather Agent").exists?
22
-  Agent.build_for_type("Agents::WeatherAgent", user,
23
-                       :name => "SF Weather Agent",
24
-                       :schedule => "10pm",
25
-                       :options => { 'location' => "94103", 'api_key' => "put-your-key-here" }).save!
26
-
27
-  puts "NOTE: The example 'SF Weather Agent' will not work until you edit it and put in a free API key from http://www.wunderground.com/weather/api/"
28
-end
29
-
30
-unless user.agents.where(:name => "XKCD Source").exists?
31
-  Agent.build_for_type("Agents::WebsiteAgent", user,
32
-                       :name => "XKCD Source",
33
-                       :schedule => "every_1d",
34
-                       :type => "html",
35
-                       :options => {
36
-                           'url' => "http://xkcd.com",
37
-                           'mode' => "on_change",
38
-                           'expected_update_period_in_days' => 5,
39
-                           'extract' => {
40
-                               'url' => { 'css' => "#comic img", 'value' => "@src" },
41
-                               'title' => { 'css' => "#comic img", 'value' => "@alt" },
42
-                               'hovertext' => { 'css' => "#comic img", 'value' => "@title" }
43
-                           }
44
-                       }).save!
45
-end
46
-
47
-unless user.agents.where(:name => "iTunes Trailer Source").exists?
48
-  Agent.build_for_type("Agents::WebsiteAgent", user, :name => "iTunes Trailer Source",
49
-                       :schedule => "every_1d",
50
-                       :options => {
51
-                           'url' => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss",
52
-                           'mode' => "on_change",
53
-                           'type' => "xml",
54
-                           'expected_update_period_in_days' => 5,
55
-                           'extract' => {
56
-                               'title' => { 'css' => "item title", 'value' => ".//text()"},
57
-                               'url' => { 'css' => "item link", 'value' => ".//text()"}
58
-                           }
59
-                       }).save!
60
-end
61
-
62
-unless user.agents.where(:name => "Rain Notifier").exists?
63
-  Agent.build_for_type("Agents::TriggerAgent", user,
64
-                       :name => "Rain Notifier",
65
-                       :source_ids => user.agents.where(:name => "SF Weather Agent").pluck(:id),
66
-                       :options => {
67
-                           'expected_receive_period_in_days' => "2",
68
-                           'rules' => [{
69
-                                          'type' => "regex",
70
-                                          'value' => "rain|storm",
71
-                                          'path' => "conditions"
72
-                                      }],
73
-                           'message' => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}"
74
-                       }).save!
75
-end
76
-
77
-unless user.agents.where(:name => "Morning Digest").exists?
78
-  Agent.build_for_type("Agents::EmailDigestAgent", user,
79
-                       :name => "Morning Digest",
80
-                       :schedule => "6am",
81
-                       :options => { 'subject' => "Your Morning Digest", 'expected_receive_period_in_days' => "30" },
82
-                       :source_ids => user.agents.where(:name => "Rain Notifier").pluck(:id)).save!
83
-end
84
-
85
-unless user.agents.where(:name => "Afternoon Digest").exists?
86
-  Agent.build_for_type("Agents::EmailDigestAgent", user,
87
-                       :name => "Afternoon Digest",
88
-                       :schedule => "5pm",
89
-                       :options => { 'subject' => "Your Afternoon Digest", 'expected_receive_period_in_days' => "7" },
90
-                       :source_ids => user.agents.where(:name => ["iTunes Trailer Source", "XKCD Source"]).pluck(:id)).save!
91
-end
92
-
93
-puts "See the Huginn Wiki for more Agent examples!  https://github.com/cantino/huginn/wiki"
6
+Seeder.seed

+ 23 - 0
db/seeds/seeder.rb

@@ -0,0 +1,23 @@
1
+class Seeder
2
+  def self.seed
3
+    user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com")
4
+    if user.persisted?
5
+      puts "User with email '#{user.email}' already exists, not seeding."
6
+      exit
7
+    end
8
+
9
+    user.username = ENV['SEED_USERNAME'] || "admin"
10
+    user.password = ENV['SEED_PASSWORD'] || "password"
11
+    user.password_confirmation = ENV['SEED_PASSWORD'] || "password"
12
+    user.invitation_code = User::INVITATION_CODES.first
13
+    user.admin = true
14
+    user.save!
15
+
16
+    if DefaultScenarioImporter.seed(user)
17
+      puts "NOTE: The example 'SF Weather Agent' will not work until you edit it and put in a free API key from http://www.wunderground.com/weather/api/"
18
+      puts "See the Huginn Wiki for more Agent examples!  https://github.com/cantino/huginn/wiki"
19
+    else
20
+      raise('Unable to import the default scenario')
21
+    end
22
+  end
23
+end

+ 22 - 0
spec/controllers/admin/users_controller_spec.rb

@@ -0,0 +1,22 @@
1
+require 'rails_helper'
2
+
3
+describe Admin::UsersController do
4
+  describe 'POST #create' do
5
+    context 'with valid user params' do
6
+      it 'imports the default scenario for the new user' do
7
+        mock(DefaultScenarioImporter).import(is_a(User))
8
+        sign_in users(:jane)
9
+        post :create, :user => {username: 'jdoe', email: 'jdoe@example.com',
10
+                             password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false }
11
+      end
12
+    end
13
+    
14
+    context 'with invalid user params' do
15
+      it 'does not import the default scenario' do
16
+        stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" }
17
+        sign_in users(:jane)
18
+        post :create, :user => {}
19
+      end
20
+    end
21
+  end
22
+end

+ 29 - 0
spec/controllers/users/registrations_controller_spec.rb

@@ -0,0 +1,29 @@
1
+require 'rails_helper'
2
+
3
+module Users
4
+  describe RegistrationsController do
5
+    include Devise::TestHelpers
6
+
7
+    describe "POST create" do
8
+      context 'with valid params' do
9
+        it "imports the default scenario for the new user" do
10
+          mock(DefaultScenarioImporter).import(is_a(User))
11
+
12
+          @request.env["devise.mapping"] = Devise.mappings[:user]
13
+          post :create, :user => {username: 'jdoe', email: 'jdoe@example.com',
14
+            password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false, invitation_code: 'try-huginn'}
15
+        end
16
+      end
17
+
18
+      context 'with invalid params' do
19
+        it "does not import the default scenario" do
20
+          stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" }
21
+
22
+          @request.env["devise.mapping"] = Devise.mappings[:user]
23
+          setup_controller_for_warden
24
+          post :create, :user => {}
25
+        end
26
+      end
27
+    end
28
+  end
29
+end

+ 29 - 0
spec/db/seeds/admin_and_default_scenario_spec.rb

@@ -0,0 +1,29 @@
1
+require 'rails_helper'
2
+require_relative '../../../db/seeds/seeder'
3
+
4
+describe Seeder do
5
+  before do
6
+    stub_puts_to_prevent_spew_in_spec_output
7
+  end
8
+
9
+  describe '.seed' do
10
+    it 'imports a default scenario' do
11
+      expect { Seeder.seed }.to change(Agent, :count).by(7)
12
+    end
13
+
14
+    it 'creates an admin' do
15
+      expect { Seeder.seed }.to change(User, :count).by(1)
16
+      expect(User.last).to be_admin
17
+    end
18
+
19
+    it 'can be run multiple times and exit normally' do
20
+      Seeder.seed
21
+      expect { Seeder.seed }.to raise_error(SystemExit)
22
+    end
23
+  end
24
+
25
+  def stub_puts_to_prevent_spew_in_spec_output
26
+    stub(Seeder).puts(anything)
27
+    stub(Seeder).puts
28
+  end
29
+end

+ 68 - 0
spec/fixtures/test_default_scenario.json

@@ -0,0 +1,68 @@
1
+{
2
+  "schema_version": 1,
3
+  "name": "default-scenario",
4
+  "description": "This scenario has a few agents to get you started. Feel free to change them or delete them as you see fit!",
5
+  "source_url": false,
6
+  "guid": "ee4299225e6531c401a8bbbce0771ce4",
7
+  "tag_fg_color": "#ffffff",
8
+  "tag_bg_color": "#5bc0de",
9
+  "exported_at": "2016-04-03T18:24:42Z",
10
+  "agents": [
11
+    {
12
+      "type": "Agents::TriggerAgent",
13
+      "name": "Rain Notifier",
14
+      "disabled": false,
15
+      "guid": "361ee2e955d4726b52c8b044d4f75e25",
16
+      "options": {
17
+        "expected_receive_period_in_days": "2",
18
+        "rules": [
19
+          {
20
+            "type": "regex",
21
+            "value": "rain|storm",
22
+            "path": "conditions"
23
+          }
24
+        ],
25
+        "message": "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}"
26
+      },
27
+      "keep_events_for": 0,
28
+      "propagate_immediately": false
29
+    },
30
+    {
31
+      "type": "Agents::EmailDigestAgent",
32
+      "name": "Morning Digest",
33
+      "disabled": false,
34
+      "guid": "b34eaee75d8dc67843c3bd257c213852",
35
+      "options": {
36
+        "subject": "Your Morning Digest",
37
+        "expected_receive_period_in_days": "30"
38
+      },
39
+      "schedule": "6am",
40
+      "propagate_immediately": false
41
+    },
42
+    {
43
+      "type": "Agents::WeatherAgent",
44
+      "name": "SF Weather Agent",
45
+      "disabled": false,
46
+      "guid": "bdae6dfdf9d01a123ddd513e695fd466",
47
+      "options": {
48
+        "location": "94103",
49
+        "api_key": "put-your-key-here"
50
+      },
51
+      "schedule": "10pm",
52
+      "keep_events_for": 0
53
+    }
54
+  ],
55
+  "links": [
56
+    {
57
+      "source": 2,
58
+      "receiver": 0
59
+    },
60
+    {
61
+      "source": 0,
62
+      "receiver": 1
63
+    }
64
+  ],
65
+  "control_links": [
66
+
67
+  ]
68
+}

+ 46 - 0
spec/importers/default_scenario_importer_spec.rb

@@ -0,0 +1,46 @@
1
+require 'rails_helper'
2
+
3
+describe DefaultScenarioImporter do
4
+  let(:user) { users(:bob) }
5
+  describe '.import' do
6
+    it 'imports a set of agents to get the user going when they are first created' do
7
+      mock(DefaultScenarioImporter).seed(is_a(User))
8
+      stub.proxy(ENV).[](anything)
9
+      stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'true' }
10
+      DefaultScenarioImporter.import(user)
11
+    end
12
+
13
+    it 'can be turned off' do
14
+      stub(DefaultScenarioImporter).seed { fail "seed should not have been called"}
15
+      stub.proxy(ENV).[](anything)
16
+      stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'false' }
17
+      DefaultScenarioImporter.import(user)
18
+    end
19
+
20
+    it 'is turned off for existing instances of Huginn' do
21
+      stub(DefaultScenarioImporter).seed { fail "seed should not have been called"}
22
+      stub.proxy(ENV).[](anything)
23
+      stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { nil }
24
+      DefaultScenarioImporter.import(user)
25
+    end
26
+
27
+  end
28
+
29
+  describe '.seed' do
30
+    it 'imports a set of agents to get the user going when they are first created' do
31
+      expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(7)
32
+    end
33
+
34
+    it 'respects an environment variable that specifies a path or URL to a different scenario' do
35
+      stub.proxy(ENV).[](anything)
36
+      stub(ENV).[]('DEFAULT_SCENARIO_FILE') { File.join(Rails.root, "spec", "fixtures", "test_default_scenario.json") }
37
+      expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(3)
38
+    end
39
+
40
+    it 'can not be turned off' do
41
+      stub.proxy(ENV).[](anything)
42
+      stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'true' }
43
+      expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(7)
44
+    end
45
+  end
46
+end

spec/models/scenario_import_spec.rb → spec/importers/scenario_import_spec.rb