diff --git a/.env.example b/.env.example index 43b21f71..37588f4f 100644 --- a/.env.example +++ b/.env.example @@ -81,6 +81,13 @@ UNLOCK_AFTER=1.hour # Duration for which the user will be remembered without asking for credentials again. REMEMBER_FOR=4.weeks +# Set to 'true' if you would prefer new users to start with a default set of agents +IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS=true + +# Users can be given a default set of agents to get them started +# You can override this scenario with your own scenario via file path or URL +# DEFAULT_SCENARIO_FILE=path-or-url-to-scenario.json + ############################# # Email Configuration # ############################# diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 891499e7..dc2ae19a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -26,6 +26,7 @@ class Admin::UsersController < ApplicationController respond_to do |format| if @user.save + DefaultScenarioImporter.import(@user) format.html { redirect_to admin_users_path, notice: "User '#{@user.username}' was successfully created." } format.json { render json: @user, status: :ok, location: admin_users_path(@user) } else diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 00000000..d3814905 --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,11 @@ +module Users + class RegistrationsController < Devise::RegistrationsController + after_action :create_default_scenario, only: :create + + private + + def create_default_scenario + DefaultScenarioImporter.import(@user) if @user.persisted? + end + end +end diff --git a/app/importers/default_scenario_importer.rb b/app/importers/default_scenario_importer.rb new file mode 100644 index 00000000..eb86e846 --- /dev/null +++ b/app/importers/default_scenario_importer.rb @@ -0,0 +1,20 @@ +require 'open-uri' +class DefaultScenarioImporter + def self.import(user) + return unless ENV['IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS'] == 'true' + seed(user) + end + + def self.seed(user) + scenario_import = ScenarioImport.new() + scenario_import.set_user(user) + scenario_file = ENV['DEFAULT_SCENARIO_FILE'].presence || File.join(Rails.root, "data", "default_scenario.json") + begin + scenario_import.file = open(scenario_file) + raise "Import failed" unless scenario_import.valid? && scenario_import.import + ensure + scenario_import.file.close + end + return true + end +end diff --git a/app/models/scenario_import.rb b/app/importers/scenario_import.rb similarity index 100% rename from app/models/scenario_import.rb rename to app/importers/scenario_import.rb diff --git a/config/routes.rb b/config/routes.rb index 0bd99a25..5d8fab4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -82,7 +82,10 @@ Huginn::Application.routes.draw do post "/users/:user_id/update_location/:secret" => "web_requests#update_location" # legacy devise_for :users, - controllers: { omniauth_callbacks: 'omniauth_callbacks' }, + controllers: { + omniauth_callbacks: 'omniauth_callbacks', + registrations: 'users/registrations' + }, sign_out_via: [:post, :delete] if Rails.env.development? diff --git a/data/default_scenario.json b/data/default_scenario.json new file mode 100644 index 00000000..2bee9e62 --- /dev/null +++ b/data/default_scenario.json @@ -0,0 +1,162 @@ +{ + "schema_version": 1, + "name": "default-scenario", + "description": "This scenario has a few agents to get you started. Feel free to change them or delete them as you see fit!", + "source_url": false, + "guid": "ee4299225e6531c401a8bbbce0771ce4", + "tag_fg_color": "#ffffff", + "tag_bg_color": "#5bc0de", + "exported_at": "2016-04-03T18:24:42Z", + "agents": [ + { + "type": "Agents::TriggerAgent", + "name": "Rain Notifier", + "disabled": false, + "guid": "361ee2e955d4726b52c8b044d4f75e25", + "options": { + "expected_receive_period_in_days": "2", + "rules": [ + { + "type": "regex", + "value": "rain|storm", + "path": "conditions" + } + ], + "message": "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" + }, + "keep_events_for": 0, + "propagate_immediately": false + }, + { + "type": "Agents::WebsiteAgent", + "name": "XKCD Source", + "disabled": false, + "guid": "505c9bba65507c40e5786afff36f688c", + "options": { + "url": "http://xkcd.com", + "mode": "on_change", + "expected_update_period_in_days": 5, + "extract": { + "url": { + "css": "#comic img", + "value": "@src" + }, + "title": { + "css": "#comic img", + "value": "@alt" + }, + "hovertext": { + "css": "#comic img", + "value": "@title" + } + } + }, + "schedule": "every_1d", + "keep_events_for": 0, + "propagate_immediately": false + }, + { + "type": "Agents::EmailDigestAgent", + "name": "Afternoon Digest", + "disabled": false, + "guid": "65e8ae4533881537de3c346b5178b75d", + "options": { + "subject": "Your Afternoon Digest", + "expected_receive_period_in_days": "7" + }, + "schedule": "5pm", + "propagate_immediately": false + }, + { + "type": "Agents::EmailDigestAgent", + "name": "Morning Digest", + "disabled": false, + "guid": "b34eaee75d8dc67843c3bd257c213852", + "options": { + "subject": "Your Morning Digest", + "expected_receive_period_in_days": "30" + }, + "schedule": "6am", + "propagate_immediately": false + }, + { + "type": "Agents::WeatherAgent", + "name": "SF Weather Agent", + "disabled": false, + "guid": "bdae6dfdf9d01a123ddd513e695fd466", + "options": { + "location": "94103", + "api_key": "put-your-key-here" + }, + "schedule": "10pm", + "keep_events_for": 0 + }, + { + "type": "Agents::WebsiteAgent", + "name": "iTunes Trailer Source", + "disabled": false, + "guid": "e9afa65457d0a736b9ec20a8dd452fc8", + "options": { + "url": "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", + "mode": "on_change", + "type": "xml", + "expected_update_period_in_days": 5, + "extract": { + "title": { + "css": "item title", + "value": ".//text()" + }, + "url": { + "css": "item link", + "value": ".//text()" + } + } + }, + "schedule": "every_1d", + "keep_events_for": 0, + "propagate_immediately": false + }, + { + "type": "Agents::EventFormattingAgent", + "name": "Comic Formatter", + "disabled": false, + "guid": "d86b069650edadfc61db9df767c8b65c", + "options": { + "instructions": { + "message": "

{{title}}

{{hovertext}}

" + }, + "matchers": [ + + ], + "mode": "clean" + }, + "keep_events_for": 2592000, + "propagate_immediately": false + } + ], + "links": [ + { + "source": 0, + "receiver": 3 + }, + { + "source": 1, + "receiver": 6 + }, + { + "source": 4, + "receiver": 0 + }, + { + "source": 5, + "receiver": 2 + }, + { + "source": 6, + "receiver": 2 + } + ], + "control_links": [ + + ] +} diff --git a/db/seeds.rb b/db/seeds.rb index 5baf1d37..a56a97ce 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,93 +1,6 @@ # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). -user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com") +require_relative 'seeds/seeder' -if user.persisted? - puts "User with email '#{user.email}' already exists, not seeding." - exit -end - -user.username = ENV['SEED_USERNAME'] || "admin" -user.password = ENV['SEED_PASSWORD'] || "password" -user.password_confirmation = ENV['SEED_PASSWORD'] || "password" -user.invitation_code = User::INVITATION_CODES.first -user.admin = true -user.save! - -puts -puts - -unless user.agents.where(:name => "SF Weather Agent").exists? - Agent.build_for_type("Agents::WeatherAgent", user, - :name => "SF Weather Agent", - :schedule => "10pm", - :options => { 'location' => "94103", 'api_key' => "put-your-key-here" }).save! - - 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/" -end - -unless user.agents.where(:name => "XKCD Source").exists? - Agent.build_for_type("Agents::WebsiteAgent", user, - :name => "XKCD Source", - :schedule => "every_1d", - :type => "html", - :options => { - 'url' => "http://xkcd.com", - 'mode' => "on_change", - 'expected_update_period_in_days' => 5, - 'extract' => { - 'url' => { 'css' => "#comic img", 'value' => "@src" }, - 'title' => { 'css' => "#comic img", 'value' => "@alt" }, - 'hovertext' => { 'css' => "#comic img", 'value' => "@title" } - } - }).save! -end - -unless user.agents.where(:name => "iTunes Trailer Source").exists? - Agent.build_for_type("Agents::WebsiteAgent", user, :name => "iTunes Trailer Source", - :schedule => "every_1d", - :options => { - 'url' => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", - 'mode' => "on_change", - 'type' => "xml", - 'expected_update_period_in_days' => 5, - 'extract' => { - 'title' => { 'css' => "item title", 'value' => ".//text()"}, - 'url' => { 'css' => "item link", 'value' => ".//text()"} - } - }).save! -end - -unless user.agents.where(:name => "Rain Notifier").exists? - Agent.build_for_type("Agents::TriggerAgent", user, - :name => "Rain Notifier", - :source_ids => user.agents.where(:name => "SF Weather Agent").pluck(:id), - :options => { - 'expected_receive_period_in_days' => "2", - 'rules' => [{ - 'type' => "regex", - 'value' => "rain|storm", - 'path' => "conditions" - }], - 'message' => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" - }).save! -end - -unless user.agents.where(:name => "Morning Digest").exists? - Agent.build_for_type("Agents::EmailDigestAgent", user, - :name => "Morning Digest", - :schedule => "6am", - :options => { 'subject' => "Your Morning Digest", 'expected_receive_period_in_days' => "30" }, - :source_ids => user.agents.where(:name => "Rain Notifier").pluck(:id)).save! -end - -unless user.agents.where(:name => "Afternoon Digest").exists? - Agent.build_for_type("Agents::EmailDigestAgent", user, - :name => "Afternoon Digest", - :schedule => "5pm", - :options => { 'subject' => "Your Afternoon Digest", 'expected_receive_period_in_days' => "7" }, - :source_ids => user.agents.where(:name => ["iTunes Trailer Source", "XKCD Source"]).pluck(:id)).save! -end - -puts "See the Huginn Wiki for more Agent examples! https://github.com/cantino/huginn/wiki" +Seeder.seed diff --git a/db/seeds/seeder.rb b/db/seeds/seeder.rb new file mode 100644 index 00000000..a841e046 --- /dev/null +++ b/db/seeds/seeder.rb @@ -0,0 +1,23 @@ +class Seeder + def self.seed + user = User.find_or_initialize_by(:email => ENV['SEED_EMAIL'] || "admin@example.com") + if user.persisted? + puts "User with email '#{user.email}' already exists, not seeding." + exit + end + + user.username = ENV['SEED_USERNAME'] || "admin" + user.password = ENV['SEED_PASSWORD'] || "password" + user.password_confirmation = ENV['SEED_PASSWORD'] || "password" + user.invitation_code = User::INVITATION_CODES.first + user.admin = true + user.save! + + if DefaultScenarioImporter.seed(user) + 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/" + puts "See the Huginn Wiki for more Agent examples! https://github.com/cantino/huginn/wiki" + else + raise('Unable to import the default scenario') + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb new file mode 100644 index 00000000..ea6580a3 --- /dev/null +++ b/spec/controllers/admin/users_controller_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe Admin::UsersController do + describe 'POST #create' do + context 'with valid user params' do + it 'imports the default scenario for the new user' do + mock(DefaultScenarioImporter).import(is_a(User)) + sign_in users(:jane) + post :create, :user => {username: 'jdoe', email: 'jdoe@example.com', + password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false } + end + end + + context 'with invalid user params' do + it 'does not import the default scenario' do + stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" } + sign_in users(:jane) + post :create, :user => {} + end + end + end +end diff --git a/spec/controllers/users/registrations_controller_spec.rb b/spec/controllers/users/registrations_controller_spec.rb new file mode 100644 index 00000000..02f44f80 --- /dev/null +++ b/spec/controllers/users/registrations_controller_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +module Users + describe RegistrationsController do + include Devise::TestHelpers + + describe "POST create" do + context 'with valid params' do + it "imports the default scenario for the new user" do + mock(DefaultScenarioImporter).import(is_a(User)) + + @request.env["devise.mapping"] = Devise.mappings[:user] + post :create, :user => {username: 'jdoe', email: 'jdoe@example.com', + password: 's3cr3t55', password_confirmation: 's3cr3t55', admin: false, invitation_code: 'try-huginn'} + end + end + + context 'with invalid params' do + it "does not import the default scenario" do + stub(DefaultScenarioImporter).import(is_a(User)) { fail "Should not attempt import" } + + @request.env["devise.mapping"] = Devise.mappings[:user] + setup_controller_for_warden + post :create, :user => {} + end + end + end + end +end diff --git a/spec/db/seeds/admin_and_default_scenario_spec.rb b/spec/db/seeds/admin_and_default_scenario_spec.rb new file mode 100644 index 00000000..684252d6 --- /dev/null +++ b/spec/db/seeds/admin_and_default_scenario_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' +require_relative '../../../db/seeds/seeder' + +describe Seeder do + before do + stub_puts_to_prevent_spew_in_spec_output + end + + describe '.seed' do + it 'imports a default scenario' do + expect { Seeder.seed }.to change(Agent, :count).by(7) + end + + it 'creates an admin' do + expect { Seeder.seed }.to change(User, :count).by(1) + expect(User.last).to be_admin + end + + it 'can be run multiple times and exit normally' do + Seeder.seed + expect { Seeder.seed }.to raise_error(SystemExit) + end + end + + def stub_puts_to_prevent_spew_in_spec_output + stub(Seeder).puts(anything) + stub(Seeder).puts + end +end diff --git a/spec/fixtures/test_default_scenario.json b/spec/fixtures/test_default_scenario.json new file mode 100644 index 00000000..05422297 --- /dev/null +++ b/spec/fixtures/test_default_scenario.json @@ -0,0 +1,68 @@ +{ + "schema_version": 1, + "name": "default-scenario", + "description": "This scenario has a few agents to get you started. Feel free to change them or delete them as you see fit!", + "source_url": false, + "guid": "ee4299225e6531c401a8bbbce0771ce4", + "tag_fg_color": "#ffffff", + "tag_bg_color": "#5bc0de", + "exported_at": "2016-04-03T18:24:42Z", + "agents": [ + { + "type": "Agents::TriggerAgent", + "name": "Rain Notifier", + "disabled": false, + "guid": "361ee2e955d4726b52c8b044d4f75e25", + "options": { + "expected_receive_period_in_days": "2", + "rules": [ + { + "type": "regex", + "value": "rain|storm", + "path": "conditions" + } + ], + "message": "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" + }, + "keep_events_for": 0, + "propagate_immediately": false + }, + { + "type": "Agents::EmailDigestAgent", + "name": "Morning Digest", + "disabled": false, + "guid": "b34eaee75d8dc67843c3bd257c213852", + "options": { + "subject": "Your Morning Digest", + "expected_receive_period_in_days": "30" + }, + "schedule": "6am", + "propagate_immediately": false + }, + { + "type": "Agents::WeatherAgent", + "name": "SF Weather Agent", + "disabled": false, + "guid": "bdae6dfdf9d01a123ddd513e695fd466", + "options": { + "location": "94103", + "api_key": "put-your-key-here" + }, + "schedule": "10pm", + "keep_events_for": 0 + } + ], + "links": [ + { + "source": 2, + "receiver": 0 + }, + { + "source": 0, + "receiver": 1 + } + ], + "control_links": [ + + ] +} diff --git a/spec/importers/default_scenario_importer_spec.rb b/spec/importers/default_scenario_importer_spec.rb new file mode 100644 index 00000000..48673f3e --- /dev/null +++ b/spec/importers/default_scenario_importer_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe DefaultScenarioImporter do + let(:user) { users(:bob) } + describe '.import' do + it 'imports a set of agents to get the user going when they are first created' do + mock(DefaultScenarioImporter).seed(is_a(User)) + stub.proxy(ENV).[](anything) + stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'true' } + DefaultScenarioImporter.import(user) + end + + it 'can be turned off' do + stub(DefaultScenarioImporter).seed { fail "seed should not have been called"} + stub.proxy(ENV).[](anything) + stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'false' } + DefaultScenarioImporter.import(user) + end + + it 'is turned off for existing instances of Huginn' do + stub(DefaultScenarioImporter).seed { fail "seed should not have been called"} + stub.proxy(ENV).[](anything) + stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { nil } + DefaultScenarioImporter.import(user) + end + + end + + describe '.seed' do + it 'imports a set of agents to get the user going when they are first created' do + expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(7) + end + + it 'respects an environment variable that specifies a path or URL to a different scenario' do + stub.proxy(ENV).[](anything) + stub(ENV).[]('DEFAULT_SCENARIO_FILE') { File.join(Rails.root, "spec", "fixtures", "test_default_scenario.json") } + expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(3) + end + + it 'can not be turned off' do + stub.proxy(ENV).[](anything) + stub(ENV).[]('IMPORT_DEFAULT_SCENARIO_FOR_ALL_USERS') { 'true' } + expect { DefaultScenarioImporter.seed(user) }.to change(user.agents, :count).by(7) + end + end +end diff --git a/spec/models/scenario_import_spec.rb b/spec/importers/scenario_import_spec.rb similarity index 100% rename from spec/models/scenario_import_spec.rb rename to spec/importers/scenario_import_spec.rb