"
5 | exit
6 | fi
7 |
8 | APPNAME=$1
9 |
10 | cf create-app-manifest $APPNAME
11 | cf-ssh -f ${APPNAME}_manifest.yml --verbose
12 |
--------------------------------------------------------------------------------
/db/migrate/20160627172503_rename_message_time_frame_to_type.rb:
--------------------------------------------------------------------------------
1 | class RenameMessageTimeFrameToType < ActiveRecord::Migration
2 | def change
3 | rename_column :scheduled_messages, :message_time_frame, :type
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/db/migrate/20160617101033_add_type_to_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class AddTypeToScheduledMessages < ActiveRecord::Migration
2 | def change
3 | add_column :scheduled_messages, :message_time_frame, :integer, default: 0
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/app/controllers/sent_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class SentMessagesController < ApplicationController
2 | def index
3 | @sent_messages = SentMessage.filter(params).
4 | order(created_at: :desc).
5 | page(params[:page])
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/application/_flashes.html.erb:
--------------------------------------------------------------------------------
1 | <% if flash.any? %>
2 |
3 | <% user_facing_flashes.each do |key, value| -%>
4 |
<%= value %>
5 | <% end -%>
6 |
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/db/migrate/20151021183449_add_time_zone_to_employees.rb:
--------------------------------------------------------------------------------
1 | class AddTimeZoneToEmployees < ActiveRecord::Migration
2 | def change
3 | add_column :employees, :time_zone, :string, null: false, default: "Eastern Time (US & Canada)"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20160623213452_remove_null_constraint_on_days_after_start.rb:
--------------------------------------------------------------------------------
1 | class RemoveNullConstraintOnDaysAfterStart < ActiveRecord::Migration
2 | def change
3 | change_column_null(:scheduled_messages, :days_after_start, true)
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/models/broadcast_message_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe BroadcastMessage do
4 | describe "Validations" do
5 | it { should validate_presence_of(:title) }
6 | it { should validate_presence_of(:body) }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'header', :title => 'Update quarterly message' %>
2 |
3 |
4 | <%= render "flashes" -%>
5 | <%= render "disabled_alert" %>
6 | <%= render "form" %>
7 |
8 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'header', :title => 'Add a new quarterly message' %>
2 |
3 |
4 | <%= render "flashes" -%>
5 | <%= render "disabled_alert" %>
6 | <%= render "form" %>
7 |
8 |
--------------------------------------------------------------------------------
/db/migrate/20160430175239_add_slack_user_id_to_employee.rb:
--------------------------------------------------------------------------------
1 | class AddSlackUserIdToEmployee < ActiveRecord::Migration
2 | def change
3 | add_column :employees, :slack_user_id, :string
4 | add_index :employees, :slack_user_id
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/presets/_utils.scss:
--------------------------------------------------------------------------------
1 | .icon-18f {
2 | color: $bg-18f;
3 | }
4 |
5 | .logo-18f {
6 | width: 25px;
7 | float: left;
8 | margin-right: 10px;
9 | }
10 |
11 | *[aria-hidden='true'] {
12 | display: none !important;
13 | }
14 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
3 |
4 | development:
5 | <<: *default
6 |
7 | test:
8 | <<: *default
9 |
10 | staging:
11 | <<: *default
12 |
13 | production:
14 | <<: *default
15 |
--------------------------------------------------------------------------------
/db/migrate/20151021194020_add_time_of_day_to_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class AddTimeOfDayToScheduledMessages < ActiveRecord::Migration
2 | def change
3 | add_column :scheduled_messages, :time_of_day, :time, null: false, default: "12:00:00"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | engines:
2 | rubocop:
3 | enabled: true
4 | brakeman:
5 | enabled: true
6 | scss-lint:
7 | enabled: true
8 | ratings:
9 | paths:
10 | - app/**
11 | - lib/**
12 | - "**.rb"
13 | exclude_paths:
14 | - spec/**/*
15 |
--------------------------------------------------------------------------------
/config/locales/simple_form.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | simple_form:
3 | "yes": 'Yes'
4 | "no": 'No'
5 | required:
6 | text: 'required'
7 | mark: '*'
8 | error_notification:
9 | default_message: "Please review the problems below:"
10 |
--------------------------------------------------------------------------------
/db/migrate/20151021202217_add_sent_at_to_sent_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class AddSentAtToSentScheduledMessages < ActiveRecord::Migration
2 | def change
3 | add_column :sent_scheduled_messages, :sent_at, :time, null: false, default: "12:00:00"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/time_helper.rb:
--------------------------------------------------------------------------------
1 | module TimeHelper
2 |
3 | def time_zone_from_offset(offset)
4 | ActiveSupport::TimeZone.new(offset).name
5 | end
6 |
7 | def fast_forward_one_hour
8 | Timecop.freeze(Time.now + 1.hour)
9 | end
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/_disabled_alert.html.erb:
--------------------------------------------------------------------------------
1 |
2 | Quarterly messages are temporiarily disabled.
3 | You can still create, edit, delete and test querterly messages,
4 | but they will not be sent at the start of a new quarter.
5 |
6 |
--------------------------------------------------------------------------------
/spec/support/fixtures/message_posted.json:
--------------------------------------------------------------------------------
1 | {
2 | "ok": true,
3 | "channel": "123ABC",
4 | "ts": "1443053184.000012",
5 | "message": {
6 | "type": "message",
7 | "user": "123ABC",
8 | "text": "blah blah ",
9 | "ts": "1443053184.000012"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/views/application/_javascript.html.erb:
--------------------------------------------------------------------------------
1 | <%= javascript_include_tag :application %>
2 |
3 | <%= yield :javascript %>
4 |
5 | <% if Rails.env.test? %>
6 | <%= javascript_tag do %>
7 | $.fx.off = true;
8 | $.ajaxSetup({ async: false });
9 | <% end %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/app/services/slack_channel_id_finder.rb:
--------------------------------------------------------------------------------
1 | class SlackChannelIdFinder < SlackApiWrapper
2 | def run
3 | if slack_user_by_id
4 | slack_user_id = slack_user["id"]
5 | chat = client.im_open(user: slack_user_id)
6 | chat["channel"]["id"]
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20160122233938_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 | t.timestamps null: false
5 | t.string :email, null: false
6 | t.boolean :admin, default: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_alert.scss:
--------------------------------------------------------------------------------
1 | .alert {
2 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
3 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
4 | background-color: #d0e9c6;
5 | font-size: 0.8rem;
6 | padding: 0.5em;
7 | border-radius:0.5em;
8 | margin-bottom: 1em;
9 | }
10 |
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def new
5 | end
6 |
7 | def destroy
8 | signout_user!
9 | redirect_to new_session_url
10 | end
11 |
12 | def create
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/views/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <% unless current_user %>
9 | Sign in with GitHub
10 | <% end %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/db/migrate/20150917233323_create_employees.rb:
--------------------------------------------------------------------------------
1 | class CreateEmployees < ActiveRecord::Migration
2 | def change
3 | create_table :employees do |t|
4 | t.timestamps null: false
5 | t.string :slack_username, null: false
6 | t.date :started_on, null: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/.sample.env:
--------------------------------------------------------------------------------
1 | SLACK_API_TOKEN='CHANGE_ME'
2 | ASSET_HOST=localhost:3000
3 | APPLICATION_HOST=localhost:3000
4 | RACK_ENV=development
5 | SECRET_KEY_BASE=development_secret
6 | EXECJS_RUNTIME=Node
7 | GITHUB_CLIENT_ID=consumer_public_key
8 | GITHUB_CLIENT_SECRET=consumer_secret_key
9 | GITHUB_TEAM_ID=yourteamid
10 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_simple_form.scss:
--------------------------------------------------------------------------------
1 | .input .date {
2 | display: inline-block;
3 |
4 | select.date {
5 | padding: 1em;
6 | }
7 | }
8 |
9 | .input {
10 | margin-bottom: $base-padding-lite;
11 | }
12 |
13 | .input:last-of-type {
14 | margin-bottom: $base-padding;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/presets/_colors.scss:
--------------------------------------------------------------------------------
1 | $white: #fff;
2 | $gray-mid: #ccc;
3 | $bg-error: #FF6565;
4 | $bg-notice: #A4E7A0;
5 | $bg-18f: #1188ff;
6 |
7 | $light-18f: #B3EFFF;
8 | $bright-18f: #00CFFF;
9 | $medium-18f: #046B99;
10 | $dark-18f: #1C304A;
11 | $black-18f: #000000;
12 |
13 |
14 | $red: #c5403c;
15 |
--------------------------------------------------------------------------------
/config/initializers/kaminari_config.rb:
--------------------------------------------------------------------------------
1 | Kaminari.configure do |config|
2 | config.default_per_page = 10
3 | config.max_per_page = nil
4 | config.window = 4
5 | config.outer_window = 0
6 | config.left = 0
7 | config.right = 0
8 | config.page_method_name = :page
9 | config.param_name = :page
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/elements/_lists.scss:
--------------------------------------------------------------------------------
1 | ul,
2 | ol {
3 | list-style: none;
4 | margin: 0;
5 | padding: 0;
6 |
7 | li {
8 | display: list-item;
9 | margin: 0;
10 |
11 | &:before {
12 | display: none;
13 | }
14 |
15 | &:after {
16 | display: none;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require_relative "config/application"
2 |
3 | Rails.application.load_tasks
4 | task(:default).clear
5 | task default: [:spec]
6 |
7 | if defined? RSpec
8 | task(:spec).clear
9 | RSpec::Core::RakeTask.new(:spec) do |t|
10 | t.verbose = false
11 | end
12 | end
13 |
14 | # task default: "bundler:audit"
15 |
--------------------------------------------------------------------------------
/db/migrate/20160718213232_create_messages.rb:
--------------------------------------------------------------------------------
1 | class CreateMessages < ActiveRecord::Migration
2 | def change
3 | create_table :messages do |t|
4 | t.string :title, null: false
5 | t.string :body, null: false
6 | t.datetime :last_sent_at
7 | t.timestamps null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/presets/_variables.scss:
--------------------------------------------------------------------------------
1 | $base-padding: 1.25em;
2 | $base-padding-lite: ( $base-padding / 2 );
3 | $base-padding-large: ( $base-padding * 2 );
4 | $base-padding-extra: ( $base-padding * 2.5 );
5 |
6 |
7 | $base-font-family: $nimbus-regular;
8 |
9 | $weight-light: 200;
10 | $weight-book: 400;
11 | $weight-bold: 600;
12 |
--------------------------------------------------------------------------------
/db/chores/migration_helper_methods.rb:
--------------------------------------------------------------------------------
1 | module MigrationHelperMethods
2 | def execute(*args)
3 | ActiveRecord::Base.connection.execute(*args)
4 | end
5 |
6 | def insert(*args)
7 | ActiveRecord::Base.connection.insert(*args)
8 | end
9 |
10 | def sanitize(*args)
11 | ActiveRecord::Base.sanitize(*args)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_components.scss:
--------------------------------------------------------------------------------
1 | @import "components/buttons";
2 | @import "components/flashes";
3 | @import "components/alert";
4 | @import "components/simple_form";
5 | @import "components/tables";
6 | @import "components/fixedsticky";
7 | @import "components/banner";
8 | @import "components/filter";
9 | @import "components/sidenav-list";
10 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | workers Integer(ENV.fetch("WEB_CONCURRENCY", 2))
2 | threads_count = Integer(ENV.fetch("MAX_THREADS", 2))
3 | threads(threads_count, threads_count)
4 |
5 | preload_app!
6 |
7 | rackup DefaultRackup
8 | environment ENV.fetch("RACK_ENV", "development")
9 |
10 | on_worker_boot do
11 | ActiveRecord::Base.establish_connection
12 | end
13 |
--------------------------------------------------------------------------------
/lib/tasks/bundler_audit.rake:
--------------------------------------------------------------------------------
1 | if Rails.env.development? || Rails.env.test?
2 | require "bundler/audit/cli"
3 |
4 | namespace :bundler do
5 | desc "Updates the ruby-advisory-db and runs audit"
6 | task :audit do
7 | %w(update check).each do |command|
8 | Bundler::Audit::CLI.start [command]
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/users/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'header', :title => 'Update user' %>
2 |
3 |
4 | <%= render "flashes" -%>
5 |
6 | <%= simple_form_for @user do |form| %>
7 | <%= form.input :email, disabled: true %>
8 | <%= form.input :admin %>
9 | <%= form.button :submit %>
10 | <% end %>
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/db/migrate/20150918175357_create_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class CreateScheduledMessages < ActiveRecord::Migration
2 | def change
3 | create_table :scheduled_messages do |t|
4 | t.timestamps null: false
5 | t.string :title, null: false
6 | t.text :body, null: false
7 | t.integer :days_after_start, null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/config/business_time.yml:
--------------------------------------------------------------------------------
1 | business_time:
2 | beginning_of_workday: 9:00 am
3 | end_of_workday: 5:00 pm
4 | holidays:
5 | - September 28, 2015
6 | - September 29, 2015
7 | - September 30, 2015
8 | - December 24, 2015
9 | - May 4, 2016
10 | - May 5, 2016
11 | work_week:
12 | - mon
13 | - tue
14 | - wed
15 | - thu
16 | - fri
17 |
--------------------------------------------------------------------------------
/app/services/next_working_day_finder.rb:
--------------------------------------------------------------------------------
1 | require 'business_time'
2 |
3 | class NextWorkingDayFinder
4 | attr_reader :date
5 |
6 | def self.run(date = Time.today)
7 | new(date).run
8 | end
9 |
10 | def initialize(date = Time.today)
11 | @date = date
12 | end
13 |
14 | def run
15 | @date += 1.day until @date.workday?
16 | @date
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/tasks/dev.rake:
--------------------------------------------------------------------------------
1 | if Rails.env.development? || Rails.env.test?
2 | require "factory_girl"
3 |
4 | namespace :dev do
5 | desc "Sample data for local development environment"
6 | task prime: "db:setup" do
7 | include FactoryGirl::Syntax::Methods
8 |
9 | # create(:user, email: "user@example.com", password: "password")
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/assets/javascripts/flashes.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 |
3 | var flashCallback;
4 |
5 | flashCallback = function() {
6 | return $(".flashes").fadeOut();
7 | };
8 |
9 | $(".flashes").bind('click', (function(_this) {
10 | return function(event) {
11 | return $(".flashes").fadeOut();
12 | };
13 | })(this));
14 |
15 | setTimeout(flashCallback, 3000);
16 | });
17 |
--------------------------------------------------------------------------------
/config/initializers/omniauth.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.middleware.use OmniAuth::Builder do
2 | GITHUB_TEAM_ID = ENV.fetch("GITHUB_TEAM_ID")
3 | provider(
4 | :githubteammember,
5 | ENV["GITHUB_CLIENT_ID"],
6 | ENV["GITHUB_CLIENT_SECRET"],
7 | scope: "read:org user:email",
8 | teams: {
9 | "team_member?" => ENV["GITHUB_TEAM_ID"].to_i,
10 | },
11 | )
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/broadcast_messages/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "header", title: "Broadcast messages" %>
2 |
3 | <%= render "flashes" %>
4 | <% @broadcast_messages.each do |message| %>
5 | <%= render 'broadcast_message', message: message %>
6 | <% end %>
7 | <% if @broadcast_messages.empty? %>
8 | No matches found.
9 | <% end %>
10 |
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/layout/_containers.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | @include outer-container;
3 | }
4 |
5 | .container-banner {
6 | @extend .container;
7 |
8 | height: 100%;
9 | overflow-y: auto;
10 | padding: $base-padding;
11 | position: fixed;
12 | width: 25%;
13 | }
14 |
15 | .container-content {
16 | @extend .container;
17 |
18 | margin-left: 25%;
19 | padding: $base-padding;
20 | width: 75%;
21 | }
22 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_base.scss:
--------------------------------------------------------------------------------
1 | // Bitters 1.0.0
2 | // http://bitters.bourbon.io
3 | // Copyright 2013-2015 thoughtbot, inc.
4 | // MIT License
5 |
6 | @import "variables";
7 |
8 | // Neat Settings -- uncomment if using Neat -- must be imported before Neat
9 | // @import "grid-settings";
10 |
11 | @import "buttons";
12 | @import "forms";
13 | @import "lists";
14 | @import "tables";
15 | @import "typography";
16 |
--------------------------------------------------------------------------------
/app/services/slack_user_finder.rb:
--------------------------------------------------------------------------------
1 | class SlackUserFinder < SlackApiWrapper
2 | def existing_user?
3 | !slack_user.nil?
4 | end
5 |
6 | def user_id
7 | if slack_user.present?
8 | slack_user["id"]
9 | end
10 | end
11 |
12 | def username
13 | if slack_user_by_id.present?
14 | slack_user["name"]
15 | end
16 | end
17 |
18 | def users_list
19 | all_slack_users
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/db/migrate/20151006161913_add_missing_taggable_index.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 4)
2 | class AddMissingTaggableIndex < ActiveRecord::Migration
3 | def self.up
4 | add_index :taggings, [:taggable_id, :taggable_type, :context]
5 | end
6 |
7 | def self.down
8 | remove_index :taggings, [:taggable_id, :taggable_type, :context]
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/models/quarterly_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe QuarterlyMessage do
4 | describe "Associations" do
5 | it { should have_many(:sent_messages).dependent(:destroy) }
6 | end
7 |
8 | describe "Validations" do
9 | it { should validate_presence_of(:body) }
10 | it { should validate_presence_of(:tag_list) }
11 | it { should validate_presence_of(:title) }
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | // Bourban, Neat, Bitters
2 | @import 'core';
3 |
4 | // Presets – variables, colors, mixins
5 | @import 'presets';
6 |
7 | // Elements – overwrites element styles
8 | @import 'elements';
9 |
10 | // Layouts – grid
11 | @import 'layout/containers';
12 |
13 | // Components – bundles of styles applied to site components
14 | @import 'components';
15 |
16 | @import 'scheduled_messages';
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/elements/_headings.scss:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6 {
7 | @include font-nimbus('regular');
8 |
9 | line-height: normal;
10 | margin-top: 0;
11 | }
12 |
13 | h1 {
14 | @include font-nimbus('bold');
15 |
16 | font-size: 1.5em;
17 | letter-spacing: 1px;
18 | margin: 0 0 0.4em;
19 | }
20 |
21 | h2 {
22 | font-size: 1.2em;
23 | font-weight: 300;
24 | letter-spacing: 1px;
25 | }
26 |
--------------------------------------------------------------------------------
/config/initializers/business_time.rb:
--------------------------------------------------------------------------------
1 | require 'holidays'
2 |
3 | BusinessTime::Config.load("#{Rails.root}/config/business_time.yml")
4 |
5 | Holidays.between(Date.civil(2013, 1, 1), 2.years.from_now, :us, :observed).map do |holiday|
6 | BusinessTime::Config.holidays << holiday[:date]
7 | # Implement long weekends if they apply to the region, eg:
8 | # BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
9 | end
10 |
--------------------------------------------------------------------------------
/app/services/broadcast_message_sender.rb:
--------------------------------------------------------------------------------
1 | class BroadcastMessageSender
2 | def initialize(broadcast_message)
3 | @broadcast_message = broadcast_message
4 | end
5 |
6 | def run
7 | Employee.find_each do |employee|
8 | MessageSender.new(employee, broadcast_message).delay.run
9 | end
10 |
11 | broadcast_message.update(last_sent_at: Time.current)
12 | end
13 |
14 | private
15 |
16 | attr_reader :broadcast_message
17 | end
18 |
--------------------------------------------------------------------------------
/app/services/message_formatter.rb:
--------------------------------------------------------------------------------
1 | class MessageFormatter
2 | def initialize(message)
3 | @message = message
4 | end
5 |
6 | def escape_slack_characters
7 | escaped_body = message.body
8 | escaped_body = escaped_body.gsub('&', '&')
9 | escaped_body = escaped_body.gsub('<', '<')
10 | escaped_body = escaped_body.gsub('>', '>')
11 | escaped_body
12 | end
13 |
14 | private
15 |
16 | attr_reader :message
17 | end
18 |
--------------------------------------------------------------------------------
/db/migrate/20170310195553_remove_sent_message_employee_uniqueness_constraint.rb:
--------------------------------------------------------------------------------
1 | class RemoveSentMessageEmployeeUniquenessConstraint < ActiveRecord::Migration[5.0]
2 | def up
3 | remove_index(:sent_messages, name: "by_employee_message")
4 | end
5 |
6 | def down
7 | add_index(
8 | :sent_messages,
9 | [:employee_id, :message_id, :message_type],
10 | unique: true,
11 | name: "by_employee_message",
12 | )
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_grid-settings.scss:
--------------------------------------------------------------------------------
1 | @import "neat-helpers"; // or "../neat/neat-helpers" when not in Rails
2 |
3 | // Neat Overrides
4 | // $column: 90px;
5 | // $gutter: 30px;
6 | // $grid-columns: 12;
7 | // $max-width: em(1088);
8 |
9 | // Neat Breakpoints
10 | $medium-screen: em(640);
11 | $large-screen: em(860);
12 |
13 | $medium-screen-up: new-breakpoint(min-width $medium-screen 4);
14 | $large-screen-up: new-breakpoint(min-width $large-screen 8);
15 |
--------------------------------------------------------------------------------
/app/services/quarterly_message_sender.rb:
--------------------------------------------------------------------------------
1 | class QuarterlyMessageSender
2 | def run
3 | QuarterlyMessage.all.each do |message|
4 | employees_for_message = find_employees(message)
5 |
6 | employees_for_message.each do |employee|
7 | MessageSender.new(employee, message).run
8 | end
9 | end
10 | end
11 |
12 | private
13 |
14 | def find_employees(message)
15 | QuarterlyMessageEmployeeMatcher.new(message).run
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/services/onboarding_message_sender.rb:
--------------------------------------------------------------------------------
1 | class OnboardingMessageSender
2 | def run
3 | OnboardingMessage.active.each do |message|
4 | employees_for_message = find_employees(message)
5 |
6 | employees_for_message.each do |employee|
7 | MessageSender.new(employee, message).run
8 | end
9 | end
10 | end
11 |
12 | private
13 |
14 | def find_employees(message)
15 | OnboardingMessageEmployeeMatcher.new(message).run
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/employees/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= simple_form_for @employee do |form| %>
2 | <%= form.input :slack_username,
3 | required: true,
4 | pattern: '^[a-z0-9_.-]{1,21}$',
5 | input_html: { title: I18n.t('employees.errors.slack_username') } %>
6 | <%= form.input :started_on,
7 | input_html: { class: "date-time-select" },
8 | order: [:month, :day, :year] %>
9 | <%= form.input :time_zone, priority: /US/ %>
10 | <%= form.button :submit %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't
4 | # wish to see in your backtraces.
5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
6 |
7 | # You can also remove all the silencers if you're trying to debug a problem that
8 | # might stem from framework code.
9 | # Rails.backtrace_cleaner.remove_silencers!
10 |
--------------------------------------------------------------------------------
/lib/tasks/on_first_instance.rake:
--------------------------------------------------------------------------------
1 | # Creates a Rake task to limit an idempotent command to the first instance of a
2 | # deployed application
3 | # More here: https://docs.cloudfoundry.org/buildpacks/ruby/ruby-tips.html
4 |
5 | namespace :cf do
6 | desc "Only run on the first application instance"
7 | task :on_first_instance do
8 | instance_index = JSON.parse(ENV["VCAP_APPLICATION"])["instance_index"] rescue nil
9 | exit(0) unless instance_index == 0
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/features/visit_root_path_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Visit root path" do
4 | scenario "sees messages index" do
5 | message = create(:onboarding_message)
6 | login_with_oauth
7 |
8 | visit root_path
9 |
10 | expect(page).to have_content(message.title)
11 | end
12 |
13 | scenario "roots to sessions#new when not logged in" do
14 | visit root_path
15 |
16 | expect(page).to have_content("Sign in with GitHub")
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/send_broadcast_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class SendBroadcastMessagesController < ApplicationController
2 | before_action :current_user_admin
3 |
4 | def create
5 | broadcast_message = BroadcastMessage.find(params[:broadcast_message_id])
6 | BroadcastMessageSender.new(broadcast_message).run
7 |
8 | flash[:notice] = I18n.t(
9 | "controllers.send_broadcast_messages_controller.notices.create",
10 | )
11 | redirect_to broadcast_messages_path
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/database_cleaner.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.before(:suite) do
3 | DatabaseCleaner.clean_with(:deletion)
4 | end
5 |
6 | config.before(:each) do
7 | DatabaseCleaner.strategy = :transaction
8 | end
9 |
10 | config.before(:each, js: true) do
11 | DatabaseCleaner.strategy = :deletion
12 | end
13 |
14 | config.before(:each) do
15 | DatabaseCleaner.start
16 | end
17 |
18 | config.after(:each) do
19 | DatabaseCleaner.clean
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/db/migrate/20151006161914_change_collation_for_tag_names.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 5)
2 | # This migration is added to circumvent issue #623 and have special characters
3 | # work properly
4 | class ChangeCollationForTagNames < ActiveRecord::Migration
5 | def up
6 | if ActsAsTaggableOn::Utils.using_mysql?
7 | execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | !.keep
2 | *.DS_Store
3 | *.swo
4 | *.swp
5 | /.bundle
6 | /.env
7 | /.foreman
8 | /coverage/*
9 | /db/*.sqlite3
10 | /log/*
11 | /public/system
12 | /public/assets
13 | /tags
14 | /tmp/*
15 |
16 | # Ignore Cloud Foundry files
17 | *_manifest.yml
18 | cf-ssh.yml
19 |
20 | # Ignore files generated by Vagrant
21 | /.vagrant
22 |
23 | # Prevent gems bundled to vendor/bundle from causing problems deploying from CI
24 | # Ref: https://github.com/18F/dolores-landingham-slack-bot/pull/233
25 | /vendor/bundle
26 |
--------------------------------------------------------------------------------
/app/views/application/_header.html.erb:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | # production environment-specific configuration
2 | no-hostname: true
3 | applications:
4 | - name: dolores-app
5 | domains:
6 | - dolores-app.18f.gov
7 | env:
8 | DEFAULT_URL_HOST: dolores-app.18f.gov
9 | HOST: dolores-app.18f.gov
10 | APPLICATION_HOST: dolores-app.18f.gov
11 | services:
12 | - dolores-app-db
13 | command: script/start
14 | memory: 1GB
15 | buildpack: ruby_buildpack
16 | env:
17 | RESTRICT_ACCESS: true
18 | RACK_ENV: production
19 | RAILS_ENV: production
20 | stack: cflinuxfs3
21 |
--------------------------------------------------------------------------------
/app/helpers/time_zone_display_helper.rb:
--------------------------------------------------------------------------------
1 | module TimeZoneDisplayHelper
2 | def display_local_time_zone(sent_scheduled_message)
3 | zone = ActiveSupport::TimeZone.new(sent_scheduled_message.employee.time_zone)
4 | tz_offset = zone.now.strftime("%:z")
5 | sent_local_time = sent_scheduled_message.sent_at.localtime(tz_offset)
6 | sent_local_time_zone = sent_scheduled_message.sent_at.in_time_zone(sent_scheduled_message.employee.time_zone).zone
7 | sent_local_time.strftime("%l:%M %p (#{sent_local_time_zone})").strip
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/controllers/send_messages_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe SendBroadcastMessagesController do
4 | describe "POST :create" do
5 | it "redirects if the user is not an admin" do
6 | user = create(:user, admin: false)
7 | allow(controller).to receive(:current_user).and_return(user)
8 |
9 | process :create, method: :post, params: { broadcast_message_id: 1 }
10 |
11 | expect(response).to redirect_to root_path
12 | expect(flash[:error]).to be_present
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/support/slack_api_helper.rb:
--------------------------------------------------------------------------------
1 | module SlackApiHelper
2 | def mock_slack_channel_finder_for_employee(employee, options = {})
3 | slack_channel_id = options[:channel_id]
4 | client_double = Slack::Web::Client.new
5 | slack_channel_finder_double = double(run: slack_channel_id)
6 |
7 | allow(Slack::Web::Client).to receive(:new).and_return(client_double)
8 | allow(SlackChannelIdFinder).
9 | to receive(:new).with(employee.slack_user_id, client_double).
10 | and_return(slack_channel_finder_double)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_tables.scss:
--------------------------------------------------------------------------------
1 | table {
2 | @include font-feature-settings("kern", "liga", "tnum");
3 | border-collapse: collapse;
4 | margin: $small-spacing 0;
5 | table-layout: fixed;
6 | width: 100%;
7 | }
8 |
9 | th {
10 | border-bottom: 1px solid darken($base-border-color, 15%);
11 | font-weight: 600;
12 | padding: $small-spacing 0;
13 | text-align: left;
14 | }
15 |
16 | td {
17 | border-bottom: $base-border;
18 | padding: $small-spacing 0;
19 | }
20 |
21 | tr,
22 | td,
23 | th {
24 | vertical-align: middle;
25 | }
26 |
--------------------------------------------------------------------------------
/app/views/layouts/_admin_sidenav_items.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Users
4 |
5 |
6 |
7 |
8 | <% if signed_in_as_admin? %>
9 | <%= nav_link "View/Edit", users_path %>
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/test_messages/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'header', :title => 'Send test message' %>
2 |
3 |
4 | <%= render "flashes" -%>
5 | <%= simple_form_for :test_message, url: @url do |form| %>
6 | <%= form.input :body, input_html: { value: @message.body }, readonly: true, label: "Message body to send (to edit, update the scheduled message you are testing)" %>
7 | <%= form.input :slack_username, label: "Slack username to send test to", as: :string %>
8 | <%= form.submit "Send test" %>
9 | <% end %>
10 |
11 |
--------------------------------------------------------------------------------
/manifest_staging.yml:
--------------------------------------------------------------------------------
1 | # staging environment-specific configuration
2 | no-hostname: true
3 | applications:
4 | - name: dolores-staging
5 | domains:
6 | - dolores-staging.18f.gov
7 | env:
8 | APPLICATION_HOST: dolores-staging.18f.gov
9 | DEFAULT_URL_HOST: dolores-staging.18f.gov
10 | HOST: dolores-staging.18f.gov
11 | services:
12 | - dolores-staging-db
13 | command: script/start
14 | memory: 1GB
15 | buildpack: ruby_buildpack
16 | env:
17 | RESTRICT_ACCESS: true
18 | RACK_ENV: production
19 | RAILS_ENV: production
20 | stack: cflinuxfs3
21 |
--------------------------------------------------------------------------------
/db/migrate/20160121174450_add_deleted_at_to_employees_scheduled_messages_and_sent_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class AddDeletedAtToEmployeesScheduledMessagesAndSentScheduledMessages < ActiveRecord::Migration
2 | def change
3 | add_column :employees, :deleted_at, :datetime
4 | add_column :scheduled_messages, :deleted_at, :datetime
5 | add_column :sent_scheduled_messages, :deleted_at, :datetime
6 |
7 | add_index :employees, :deleted_at
8 | add_index :scheduled_messages, :deleted_at
9 | add_index :sent_scheduled_messages, :deleted_at
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/services/slack_username_updater.rb:
--------------------------------------------------------------------------------
1 | class SlackUsernameUpdater < SlackApiWrapper
2 | def initialize(employee)
3 | @employee = employee
4 | end
5 |
6 | def update
7 | slack_username = SlackUserFinder.new(
8 | employee.slack_user_id,
9 | client,
10 | ).username
11 |
12 | if employee.slack_username != slack_username
13 | employee.update(slack_username: slack_username)
14 | end
15 | end
16 |
17 | private
18 |
19 | attr_reader :employee
20 |
21 | def client
22 | @client ||= Slack::Web::Client.new
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/services/slack_username_updater_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe SlackUsernameUpdater do
4 | describe "#update" do
5 | it "updates the employee's username when it has changed" do
6 | slack_user_id_from_fixture = "123ABC_ID"
7 | employee = create(
8 | :employee,
9 | slack_username: "oldname",
10 | slack_user_id: slack_user_id_from_fixture
11 | )
12 |
13 | SlackUsernameUpdater.new(employee).update
14 |
15 | expect(employee.reload.slack_username).to eq "testusername"
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/features/create_broadcast_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Create broadcast message" do
4 | scenario "successfully" do
5 | login_with_oauth(create(:admin))
6 |
7 | visit new_broadcast_message_path
8 | fill_in "Title", with: "Message title"
9 | fill_in "Message body", with: "Message body"
10 | click_on "Create Broadcast message"
11 |
12 | expect(page).to have_content "Broadcast message created successfully"
13 | expect(page).to have_content "Message title"
14 | expect(page).to have_content "Message body"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/20151006161912_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 3)
2 | class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
3 | def self.up
4 | add_column :tags, :taggings_count, :integer, default: 0
5 |
6 | ActsAsTaggableOn::Tag.reset_column_information
7 | ActsAsTaggableOn::Tag.find_each do |tag|
8 | ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
9 | end
10 | end
11 |
12 | def self.down
13 | remove_column :tags, :taggings_count
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_lists.scss:
--------------------------------------------------------------------------------
1 | ul,
2 | ol {
3 | list-style-type: none;
4 | margin: 0;
5 | padding: 0;
6 |
7 | &%default-ul {
8 | list-style-type: disc;
9 | margin-bottom: $small-spacing;
10 | padding-left: $base-spacing;
11 | }
12 |
13 | &%default-ol {
14 | list-style-type: decimal;
15 | margin-bottom: $small-spacing;
16 | padding-left: $base-spacing;
17 | }
18 | }
19 |
20 | dl {
21 | margin-bottom: $small-spacing;
22 |
23 | dt {
24 | font-weight: bold;
25 | margin-top: $small-spacing;
26 | }
27 |
28 | dd {
29 | margin: 0;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/views/auth/forbidden.html.erb:
--------------------------------------------------------------------------------
1 |
2 | Forbidden
3 |
4 | We were unable to authenticate your user profile.
5 |
6 |
7 | If you are signing in via GitHub, please verify that you are a member of the
8 | appropriate GitHub organization and that your
9 | <%=
10 | link_to "membership is public",
11 | "https://help.github.com/articles/publicizing-or-hiding-organization-membership/"
12 | %>.
13 |
14 |
15 | <%= link_to "Try to sign in with GitHub again", "/auth/githubteammember" %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/config/clock.rb:
--------------------------------------------------------------------------------
1 | require "clockwork"
2 | require_relative "boot"
3 | require_relative "environment"
4 |
5 | module Clockwork
6 | every(1.minute, "onboarding_messages.send") do
7 | puts "Sending onboarding messages"
8 | OnboardingMessageSender.new.delay.run
9 | end
10 |
11 | # every(1.hour, "quarterly_messages.send") do
12 | # puts "Sending quarterly messages"
13 | # QuarterlyMessageSender.new.delay.run
14 | # end
15 |
16 | every(1.day, "employees.update", at: "03:00", tz: "UTC") do
17 | puts "Updating employee slack usernames"
18 | EmployeeUpdater.new.delay.run
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_flashes.scss:
--------------------------------------------------------------------------------
1 | .flash {
2 | &-error {
3 | background-color: #FF6565;
4 | }
5 |
6 | &-notice {
7 | background-color: #A4E7A0;
8 | }
9 |
10 | color: black;
11 | position: fixed;
12 | font-weight: bold;
13 | width: 96%;
14 | margin: 20px;
15 |
16 | p {
17 | font-size: 110%;
18 | margin: 15px 0;
19 | }
20 | }
21 |
22 | .flash-notice {
23 | @include font-nimbus('bold');
24 |
25 | display: block;
26 | padding-bottom: $base-padding-lite;
27 | padding-left: $base-padding;
28 | padding-right: $base-padding;
29 | padding-top: $base-padding-lite;
30 | }
31 |
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | set -e
2 |
3 | API="https://api.fr.cloud.gov"
4 | ORG="gsa-18f-dolores"
5 | SPACE=$1
6 |
7 | if [ $SPACE = 'prod' ]; then
8 | NAME="dolores-app"
9 | CF_USERNAME=$CF_USERNAME_PRODUCTION
10 | CF_PASSWORD=$CF_PASSWORD_PRODUCTION
11 | MANIFEST="manifest.yml"
12 | elif [ $SPACE = 'staging' ]; then
13 | NAME="dolores-staging"
14 | CF_USERNAME=$CF_USERNAME_STAGING
15 | CF_PASSWORD=$CF_PASSWORD_STAGING
16 | MANIFEST="manifest_staging.yml"
17 | else
18 | echo "Unknown space: $SPACE"
19 | exit
20 | fi
21 |
22 | cf login -a $API -u $CF_USERNAME -p $CF_PASSWORD -o $ORG -s $SPACE
23 | cf push -f $MANIFEST
24 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | before_action :current_user_admin, only: [:edit, :update]
3 |
4 | def edit
5 | @user = User.find(params[:id])
6 | end
7 |
8 | def update
9 | @user = User.find(params[:id])
10 |
11 | if @user.update(user_params)
12 | flash[:notice] = I18n.t('controllers.users_controller.notices.update')
13 | redirect_to employees_path
14 | end
15 | end
16 |
17 | def index
18 | @users = User.all
19 | end
20 |
21 | private
22 |
23 | def user_params
24 | params.require(:user).permit(:admin)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/services/slack_api_wrapper.rb:
--------------------------------------------------------------------------------
1 | class SlackApiWrapper
2 | def initialize(slack_username, client)
3 | @slack_username = slack_username
4 | @client = client
5 | end
6 |
7 | private
8 |
9 | attr_accessor :client, :slack_username
10 |
11 | def slack_user
12 | @slack_user ||= all_slack_users.find do |user_data|
13 | user_data["name"] == slack_username
14 | end
15 | end
16 |
17 | def slack_user_by_id
18 | @slack_user ||= all_slack_users.find do |user_data|
19 | user_data["id"] == slack_username
20 | end
21 | end
22 |
23 | def all_slack_users
24 | client.users_list["members"]
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | development: &default
2 | adapter: postgresql
3 | database: dolores-landingham-bot_development
4 | encoding: utf8
5 | host: localhost
6 | min_messages: warning
7 | pool: <%= ENV.fetch("DB_POOL", 5) %>
8 | reaping_frequency: <%= ENV.fetch("DB_REAPING_FREQUENCY", 10) %>
9 | timeout: 5000
10 |
11 | test:
12 | <<: *default
13 | database: dolores-landingham-bot_test
14 |
15 | production: &deploy
16 | encoding: utf8
17 | min_messages: warning
18 | pool: <%= [ENV.fetch("MAX_THREADS", 5), ENV.fetch("DB_POOL", 5)].max %>
19 | timeout: 5000
20 | url: <%= ENV.fetch("DATABASE_URL", "") %>
21 |
22 | staging: *deploy
23 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_fixedsticky.scss:
--------------------------------------------------------------------------------
1 | .fixedsticky {
2 | position: -webkit-sticky;
3 | position: -moz-sticky;
4 | position: -ms-sticky;
5 | position: -o-sticky;
6 | position: sticky;
7 | }
8 | /* When position: sticky is supported but native behavior is ignored */
9 | .fixedsticky-withoutfixedfixed .fixedsticky-off,
10 | .fixed-supported .fixedsticky-off {
11 | position: static;
12 | }
13 | .fixedsticky-withoutfixedfixed .fixedsticky-on,
14 | .fixed-supported .fixedsticky-on {
15 | position: fixed;
16 | }
17 | .fixedsticky-dummy {
18 | display: none;
19 | }
20 | .fixedsticky-on + .fixedsticky-dummy {
21 | display: block;
22 | }
23 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_banner.scss:
--------------------------------------------------------------------------------
1 | .banner {
2 | background-color: $bright-18f;
3 | color: $dark-18f;
4 | border-right: 1px solid $dark-18f;
5 |
6 | hr {
7 | border-bottom: 1px solid $dark-18f;
8 | }
9 | }
10 |
11 | .banner-image_small {
12 | // float: left;
13 | width: 100px;
14 | height: 100px;
15 | border-radius: 50%;
16 | margin-right: $base-padding;
17 | margin-bottom: $base-padding;
18 | border: 2px solid $dark-18f;
19 | }
20 |
21 | .banner-head {
22 | margin-bottom: $base-padding-large;
23 | }
24 |
25 | .banner-header {
26 | // margin-bottom: $base-padding;
27 | }
28 |
29 | .banner-text {
30 | font-size: 0.9rem;
31 | }
32 |
--------------------------------------------------------------------------------
/app/helpers/nav_link_helper.rb:
--------------------------------------------------------------------------------
1 | module NavLinkHelper
2 | def nav_link(link_text, link_path)
3 |
4 | class_name = current_page?(link_path) ? 'current' : nil
5 |
6 | content_tag(:li, :class => class_name) do
7 | link_to link_text, link_path
8 | end
9 | end
10 |
11 | def is_current(paths)
12 | is_array = paths.is_a?(Array)
13 |
14 | if is_array
15 | paths.each do |path|
16 | if request.fullpath.include? path
17 | return true
18 | end
19 | end
20 | else
21 | if request.fullpath.include? paths
22 | return true
23 | end
24 | end
25 |
26 | return false
27 | end
28 | end
29 |
30 |
--------------------------------------------------------------------------------
/app/views/broadcast_messages/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "header", title: "Create a new message to send to all users" %>
2 |
3 |
4 | <%= simple_form_for @broadcast_message do |form| %>
5 | <%= form.input :title,
6 | hint: "This is for identifying purposes only, employees will not see the message title" %>
7 | <%= form.input :body, label: "Message body",
8 | hint: "Don't forget, you can format your message using #{link_to "Slack's message formatting rules",
9 | "https://slack.zendesk.com/hc/en-us/articles/202288908-Formatting-your-messages", target: "_blank"}.".html_safe %>
10 | <%= form.button :submit %>
11 | <% end %>
12 |
13 |
--------------------------------------------------------------------------------
/db/migrate/20150923224816_create_sent_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | class CreateSentScheduledMessages < ActiveRecord::Migration
2 | def change
3 | create_table :sent_scheduled_messages do |t|
4 | t.timestamps null: false
5 | t.belongs_to :employee, null: false
6 | t.string :error_message, null: false, default: ''
7 | t.text :message_body, null: false
8 | t.belongs_to :scheduled_message, null: false
9 | t.date :sent_on, null: false
10 | end
11 |
12 | add_index(
13 | :sent_scheduled_messages,
14 | [:employee_id, :scheduled_message_id],
15 | unique: true,
16 | name: 'by_employee_scheduled_message',
17 | )
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/services/employee_finder.rb:
--------------------------------------------------------------------------------
1 | require "slack-ruby-client"
2 |
3 | class EmployeeFinder
4 | def initialize(slack_username = '')
5 | @slack_username = slack_username
6 | end
7 |
8 | def existing_employee?
9 | slack_user_finder.existing_user?
10 | end
11 |
12 | def slack_user_id
13 | slack_user_finder.user_id
14 | end
15 |
16 | def users_list
17 | @_users_list ||= slack_user_finder.users_list
18 | end
19 |
20 | private
21 |
22 | attr_reader :slack_username
23 |
24 | def slack_user_finder
25 | @_slack_user_finder ||= SlackUserFinder.new(slack_username, client)
26 | end
27 |
28 | def client
29 | @client ||= Slack::Web::Client.new
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/views/users/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'header', :title => 'Dolores Users' %>
2 |
3 |
4 | <%= render "flashes" -%>
5 |
6 |
7 | Email
8 | Admin
9 |
10 |
11 |
12 | <% @users.each do |user| %>
13 |
14 | <%= user.email %>
15 | <%= user.admin? %>
16 |
17 | <%= link_to edit_user_path(user), class: 'button button-edit' do %>
18 |
19 | Edit
20 | <% end %>
21 |
22 |
23 | <% end %>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= simple_form_for @quarterly_message do |form| %>
2 | <%= form.input :title,
3 | hint: "This is for identifying purposes only, the employee will not see the message title" %>
4 | <%= form.input :body, label: "Message body",
5 | hint: "Don't forget, you can format your message using #{link_to "Slack's message formatting rules",
6 | "https://slack.zendesk.com/hc/en-us/articles/202288908-Formatting-your-messages", target: "_blank"}.".html_safe %>
7 | <%= form.input :tag_list, label: "Tags", hint: "Separate tags with commas to enter multiple tags at once.", input_html: {value: @quarterly_message.tag_list.to_s} %>
8 | <%= form.button :submit %>
9 | <% end %>
10 |
11 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | if ENV.fetch("COVERAGE", false)
2 | require "simplecov"
3 | SimpleCov.start "rails"
4 | end
5 |
6 | require "webmock/rspec"
7 |
8 | RSpec.configure do |config|
9 | config.expect_with :rspec do |expectations|
10 | expectations.syntax = :expect
11 | end
12 |
13 | config.mock_with :rspec do |mocks|
14 | mocks.syntax = :expect
15 | mocks.verify_partial_doubles = true
16 | end
17 |
18 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
19 | config.order = :random
20 | end
21 |
22 | def click_accept_on_javascript_popup(&block)
23 | page.accept_confirm do
24 | yield block
25 | end
26 | end
27 |
28 | WebMock.disable_net_connect!(allow_localhost: true)
29 |
--------------------------------------------------------------------------------
/config/initializers/errors.rb:
--------------------------------------------------------------------------------
1 | require "net/http"
2 | require "net/smtp"
3 |
4 | # Example:
5 | # begin
6 | # some http call
7 | # rescue *HTTP_ERRORS => error
8 | # notify_hoptoad error
9 | # end
10 |
11 | HTTP_ERRORS = [
12 | EOFError,
13 | Errno::ECONNRESET,
14 | Errno::EINVAL,
15 | Net::HTTPBadResponse,
16 | Net::HTTPHeaderSyntaxError,
17 | Net::ProtocolError,
18 | ].freeze
19 |
20 | SMTP_SERVER_ERRORS = [
21 | IOError,
22 | Net::SMTPAuthenticationError,
23 | Net::SMTPServerBusy,
24 | Net::SMTPUnknownError,
25 | ].freeze
26 |
27 | SMTP_CLIENT_ERRORS = [
28 | Net::SMTPFatalError,
29 | Net::SMTPSyntaxError,
30 | ].freeze
31 |
32 | SMTP_ERRORS = SMTP_SERVER_ERRORS + SMTP_CLIENT_ERRORS
33 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/requests/auth_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 | require "support/oauth_helper"
3 | include OauthHelper
4 |
5 | describe "Auth request" do
6 | it "creates a new user if a user for the email address does not exist" do
7 | email = "test@example.com"
8 | setup_mock_auth(email)
9 |
10 | get "/auth/githubteammember/callback"
11 |
12 | expect(User.count).to eq 1
13 | expect(User.last.email).to eq email
14 | end
15 |
16 | it "does not create a user if a user for the email address does exist" do
17 | user = create(:user)
18 | setup_mock_auth(user.email)
19 |
20 | get "/auth/githubteammember/callback"
21 |
22 | expect(User.count).to eq 1
23 | expect(User.last).to eq user
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/support/fake_slack_api.rb:
--------------------------------------------------------------------------------
1 | require "sinatra/base"
2 |
3 | class FakeSlackApi < Sinatra::Base
4 | cattr_accessor :failure
5 |
6 | post "/api/chat.postMessage" do
7 | if failure
8 | json_response 200, "message_post_failure.json"
9 | else
10 | json_response 200, "message_posted.json"
11 | end
12 | end
13 |
14 | post "/api/users.list" do
15 | json_response 200, "users_list.json"
16 | end
17 |
18 | post "/api/im.open" do
19 | json_response 200, "im_open.json"
20 | end
21 |
22 | private
23 |
24 | def json_response(response_code, file_name)
25 | content_type :json
26 | status response_code
27 | File.open(File.dirname(__FILE__) + '/fixtures/' + file_name, 'rb').read
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/services/slack_channel_id_finder_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe SlackChannelIdFinder do
4 | describe "#run" do
5 | it "finds the slack user channel id if member of bot's org" do
6 | channel_id_from_fixture = "123ABC_IM_ID"
7 | slack_user_id_from_fixture = "123ABC_ID"
8 |
9 | channel_id = SlackChannelIdFinder.new(
10 | slack_user_id_from_fixture,
11 | Slack::Web::Client.new
12 | ).run
13 |
14 | expect(channel_id).to eq channel_id_from_fixture
15 | end
16 |
17 | it "does not error when user not found" do
18 | channel_id = SlackChannelIdFinder.new("not_found", Slack::Web::Client.new).run
19 |
20 | expect(channel_id).to be_nil
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/services/slack_user_finder_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe SlackUserFinder do
4 | describe "#existing_user?" do
5 | it "returns true if a user is found" do
6 | slack_username_from_fixture = "testusername"
7 |
8 | user = SlackUserFinder.new(
9 | slack_username_from_fixture,
10 | Slack::Web::Client.new
11 | )
12 |
13 | expect(user).to be_existing_user
14 | end
15 |
16 | it "returns false if a user was not found" do
17 | fake_slack_user_name = "testusernamethatdoesnotexist"
18 |
19 | user = SlackUserFinder.new(
20 | fake_slack_user_name,
21 | Slack::Web::Client.new
22 | )
23 |
24 | expect(user).not_to be_existing_user
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/auth_controller.rb:
--------------------------------------------------------------------------------
1 | class AuthController < ApplicationController
2 | skip_before_action :authenticate_user!, only: [:oauth_callback]
3 |
4 | def oauth_callback
5 | if team_member?
6 | user = User.find_or_create_by(email: auth_email)
7 | sign_in(user)
8 | flash[:success] = I18n.t('controllers.auth_controller.successes.oauth_callback')
9 | redirect_to root_path
10 | else
11 | render :forbidden
12 | end
13 | end
14 |
15 | private
16 |
17 | def team_member?
18 | auth_hash.credentials.team_member?
19 | end
20 |
21 | def auth_email
22 | info.email
23 | end
24 |
25 | def info
26 | auth_hash.info
27 | end
28 |
29 | def auth_hash
30 | request.env["omniauth.auth"]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/db/migrate/20150923224817_create_delayed_jobs.rb:
--------------------------------------------------------------------------------
1 | class CreateDelayedJobs < ActiveRecord::Migration
2 | def up
3 | create_table :delayed_jobs, force: true do |table|
4 | table.integer :priority, default: 0, null: false
5 | table.integer :attempts, default: 0, null: false
6 | table.text :handler, null: false
7 | table.text :last_error
8 | table.datetime :run_at
9 | table.datetime :locked_at
10 | table.datetime :failed_at
11 | table.string :locked_by
12 | table.string :queue
13 | table.timestamps null: true
14 | end
15 |
16 | add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority"
17 | end
18 |
19 | def down
20 | drop_table :delayed_jobs
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/services/message_formatter_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe MessageFormatter do
4 | describe "#escape_slack_characters" do
5 | it "escapes specific characters (&, <, and >) as defined by the Slack Message API" do
6 | message = create(
7 | :onboarding_message,
8 | body: "This is a \"message\" with &, <, and > characters that should be escaped and some 'others' that _should_ *not* be!"
9 | )
10 | formatted_message_body = MessageFormatter.new(message).escape_slack_characters
11 |
12 | expect(formatted_message_body).to eq(
13 | "This is a \"message\" with &, <, and > characters that should be escaped and some 'others' that _should_ *not* be!",
14 | )
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/services/quarterly_message_sender_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe QuarterlyMessageSender do
4 | describe "#run" do
5 | it "sends quarterly messages to employees" do
6 | quarterly_message = create(:quarterly_message)
7 | employee = create(:employee)
8 | message_sender_double = double(run: true)
9 |
10 | matcher_double = double(run: [employee])
11 | allow(QuarterlyMessageEmployeeMatcher).
12 | to receive(:new).with(quarterly_message).and_return(matcher_double)
13 | allow(MessageSender).to receive(:new).with(employee, quarterly_message).and_return(message_sender_double)
14 |
15 | QuarterlyMessageSender.new.run
16 |
17 | expect(message_sender_double).to have_received(:run)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 | require "rails/all"
3 |
4 | Bundler.require(*Rails.groups)
5 | module DoloresLandinghamBot
6 | class Application < Rails::Application
7 | config.time_zone = "UTC"
8 | config.assets.paths << Rails.root.join("app", "assets", "fonts")
9 | config.assets.quiet = true
10 | config.generators do |generate|
11 | generate.helper false
12 | generate.javascript_engine false
13 | generate.request_specs false
14 | generate.routing_specs false
15 | generate.stylesheets false
16 | generate.test_framework :rspec
17 | generate.view_specs false
18 | end
19 | config.action_controller.action_on_unpermitted_parameters = :raise
20 | config.active_job.queue_adapter = :delayed_job
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_buttons.scss:
--------------------------------------------------------------------------------
1 | #{$all-button-inputs},
2 | button {
3 | @include appearance(none);
4 | -webkit-font-smoothing: antialiased;
5 | background-color: $action-color;
6 | border-radius: $base-border-radius;
7 | border: none;
8 | color: #fff;
9 | cursor: pointer;
10 | display: inline-block;
11 | font-family: $base-font-family;
12 | font-size: $base-font-size;
13 | font-weight: 600;
14 | line-height: 1;
15 | padding: 0.75em 1em;
16 | text-decoration: none;
17 | user-select: none;
18 | vertical-align: middle;
19 | white-space: nowrap;
20 |
21 | &:hover,
22 | &:focus {
23 | background-color: darken($action-color, 15%);
24 | color: #fff;
25 | }
26 |
27 | &:disabled {
28 | cursor: not-allowed;
29 | opacity: 0.5;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/spec/features/edit_users_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Edit user" do
4 | context "current user is an admin" do
5 | scenario "can make other users admins" do
6 | admin = create(:admin)
7 | user = create(:user)
8 | login_with_oauth(admin)
9 |
10 | visit edit_user_path(user)
11 |
12 | check "Admin"
13 | click_on "Update User"
14 |
15 | expect(page).to have_content("User updated successfully")
16 | end
17 | end
18 |
19 | context "current user is not an admin" do
20 | scenario "cannot edit users" do
21 | user = create(:user)
22 | login_with_oauth(user)
23 |
24 | visit edit_user_path(user)
25 |
26 | expect(page).to have_content("You are not permitted to view that page")
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/broadcast_messages/_broadcast_message.html.erb:
--------------------------------------------------------------------------------
1 | <%= message.title %>
2 | <%= message.body %>
3 | <% if message.last_sent_at.present? %>
4 |
5 | Last sent at:
6 | <%= message.last_sent_at %>
7 |
8 | <% end %>
9 | <% if current_user.admin? %>
10 | <%= link_to broadcast_message_send_broadcast_messages_path(message), method: :post, class: 'button button-send', data: { confirm: "Are you sure you want to send this message to all Slack users right now?" } do %>
11 |
12 | Send
13 | <% end %>
14 | <%= link_to new_broadcast_message_test_message_path(message), class: 'button button-test' do %>
15 |
16 | Test
17 | <% end %>
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/db/migrate/20151006161911_add_missing_unique_indices.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 2)
2 | class AddMissingUniqueIndices < ActiveRecord::Migration
3 | def self.up
4 | add_index :tags, :name, unique: true
5 |
6 | remove_index :taggings, :tag_id
7 | remove_index :taggings, [:taggable_id, :taggable_type, :context]
8 | add_index :taggings,
9 | [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
10 | unique: true, name: 'taggings_idx'
11 | end
12 |
13 | def self.down
14 | remove_index :tags, :name
15 |
16 | remove_index :taggings, name: 'taggings_idx'
17 | add_index :taggings, :tag_id
18 | add_index :taggings, [:taggable_id, :taggable_type, :context]
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/config/initializers/simple_form.rb:
--------------------------------------------------------------------------------
1 | SimpleForm.setup do |config|
2 | config.wrappers :default, class: :input,
3 | hint_class: :field_with_hint, error_class: :field_with_errors do |b|
4 | b.use :html5
5 | b.use :placeholder
6 | b.optional :maxlength
7 | b.optional :pattern
8 | b.optional :min_max
9 | b.optional :readonly
10 | b.use :label_input
11 | b.use :hint, wrap_with: { tag: :span, class: :hint }
12 | b.use :error, wrap_with: { tag: :span, class: :error }
13 | end
14 |
15 | config.default_wrapper = :default
16 | config.boolean_style = :nested
17 | config.button_class = 'btn'
18 | config.error_notification_tag = :div
19 | config.error_notification_class = 'error_notification'
20 | config.browser_validations = true
21 | config.boolean_label_class = 'checkbox'
22 | end
23 |
--------------------------------------------------------------------------------
/spec/support/oauth_helper.rb:
--------------------------------------------------------------------------------
1 | module OauthHelper
2 | def login_with_oauth(user = create(:user))
3 | setup_mock_auth(user.email)
4 | visit "/auth/githubteammember"
5 | end
6 |
7 | def setup_mock_auth(email = "test@example.com", options = {})
8 | OmniAuth.config.mock_auth[:githubteammember] =
9 | OmniAuth::AuthHash.new(
10 | credentials: {
11 | "team_member?" => team_member?(options),
12 | },
13 | provider: "githubteammember",
14 | info: {
15 | name: "Doris Doogooder",
16 | email: email,
17 | nickname: "github_username",
18 | uid: "12345",
19 | },
20 | )
21 | end
22 |
23 | private
24 |
25 | def team_member?(options)
26 | if options[:team_member].nil?
27 | true
28 | else
29 | options[:team_member]
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_classes = true
3 | config.eager_load = false
4 | config.public_file_server.enabled = true
5 | config.public_file_server.headers = {
6 | "Cache-Control" => "public, max-age=3600",
7 | }
8 | config.consider_all_requests_local = true
9 | config.action_controller.perform_caching = false
10 | config.action_dispatch.show_exceptions = false
11 | config.action_controller.allow_forgery_protection = false
12 | config.action_mailer.perform_caching = false
13 | config.action_mailer.delivery_method = :test
14 | config.active_support.deprecation = :stderr
15 | config.action_view.raise_on_missing_translations = true
16 | config.assets.raise_runtime_errors = true
17 | config.action_mailer.default_url_options = { host: "www.example.com" }
18 | config.active_job.queue_adapter = :inline
19 | end
20 |
--------------------------------------------------------------------------------
/spec/features/send_broadcast_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Send broadcast message" do
4 | scenario "broadcast message sends successfully", js: true do
5 | create_broadcast_message
6 | create_employee
7 | login_with_oauth(create(:admin))
8 | visit broadcast_messages_path
9 |
10 | page.accept_confirm do
11 | page.find(".button-send").click
12 | end
13 |
14 | expect(page).to have_content("Broadcast message sent to all users")
15 | end
16 |
17 | def create_broadcast_message
18 | @message ||= create(:broadcast_message)
19 | end
20 |
21 | def create_scheduled_message
22 | @scheduled_message ||= create(:scheduled_message)
23 | end
24 |
25 | def create_employee
26 | @employee ||= create(:employee, slack_username: username_from_fixture)
27 | end
28 |
29 | def username_from_fixture
30 | "testusername"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/_quarterly_message.html.erb:
--------------------------------------------------------------------------------
1 | <%= message.title %>
2 | <%= message.body %>
3 |
4 | Tags:
5 | <% message.tag_list.each do |tag| %>
6 | <%= link_to tag, quarterly_messages_path(tag: tag, title: '', body: '') %>
7 | <% end %>
8 |
9 | <% if current_user.admin? %>
10 | <%= link_to edit_quarterly_message_path(message), class: 'button button-edit' do %>
11 |
12 | Edit
13 | <% end %>
14 | <%= link_to new_quarterly_message_test_message_path(message), class: 'button button-test' do %>
15 |
16 | Test
17 | <% end %>
18 | <%= link_to 'Delete', message,
19 | method: :delete,
20 | class: 'button button-delete icon-circle-x',
21 | data: { confirm: 'Are you sure you want to delete this message?' }
22 | %>
23 | <% end %>
24 |
25 |
--------------------------------------------------------------------------------
/spec/features/delete_quarterly_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Delete quarterly messages" do
4 | context "admin user" do
5 | scenario "from list of quarterly messages", :js do
6 | admin = create(:admin)
7 | message = create(:quarterly_message)
8 |
9 | login_with_oauth(admin)
10 | visit quarterly_messages_path
11 | click_accept_on_javascript_popup do
12 | page.find(".button-delete").click
13 | end
14 |
15 | expect(page).to have_content("You deleted #{message.title}")
16 | end
17 | end
18 |
19 | context "non-admin user" do
20 | scenario "does not see link to destroy quarterly messages", :js do
21 | user = create(:user)
22 | message = create(:quarterly_message)
23 |
24 | login_with_oauth(user)
25 | visit quarterly_messages_path
26 |
27 | expect(page).not_to have_content("Delete")
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/features/delete_onboarding_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Delete onboarding messages" do
4 | context "admin user" do
5 | scenario "from list of onboarding messages", :js do
6 | admin = create(:admin)
7 | message = create(:onboarding_message)
8 |
9 | login_with_oauth(admin)
10 | visit onboarding_messages_path
11 | click_accept_on_javascript_popup do
12 | page.find(".button-delete").click
13 | end
14 |
15 | expect(page).to have_content("You deleted #{message.title}")
16 | end
17 | end
18 |
19 | context "non-admin user" do
20 | scenario "does not see link to destroy onboarding messages", :js do
21 | user = create(:user)
22 | message = create(:onboarding_message)
23 |
24 | login_with_oauth(user)
25 | visit onboarding_messages_path
26 |
27 | expect(page).not_to have_content("Delete")
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/broadcast_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class BroadcastMessagesController < ApplicationController
2 | before_action :current_user_admin, only: [:new, :create]
3 |
4 | def new
5 | @broadcast_message = BroadcastMessage.new
6 | end
7 |
8 | def create
9 | @broadcast_message = BroadcastMessage.new(broadcast_message_params)
10 |
11 | if @broadcast_message.save
12 | flash[:notice] = I18n.t(
13 | "controllers.broadcast_messages_controller.notices.create",
14 | )
15 | redirect_to broadcast_messages_path
16 | else
17 | flash.now[:error] = @broadcast_message.errors.full_messages.join(", ")
18 | render action: :new
19 | end
20 | end
21 |
22 | def index
23 | @broadcast_messages = BroadcastMessage.order(created_at: :desc)
24 | end
25 |
26 | private
27 |
28 | def broadcast_message_params
29 | params.require(:broadcast_message).permit(:title, :body)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/controllers/broadcast_messages_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe BroadcastMessagesController do
4 | describe "GET :new" do
5 | it "redirects if the user is not an admin" do
6 | user = create(:user, admin: false)
7 | allow(controller).to receive(:current_user).and_return(user)
8 |
9 | get :new
10 |
11 | expect(response).to redirect_to root_path
12 | expect(flash[:error]).to be_present
13 | end
14 | end
15 |
16 | describe "POST :create" do
17 | it "redirects if the user is not an admin" do
18 | user = create(:user, admin: false)
19 | allow(controller).to receive(:current_user).and_return(user)
20 |
21 | process :create, method: :post, params: {
22 | broadcast_message: { title: "t", body: "b" }
23 | }
24 |
25 | expect(response).to redirect_to root_path
26 | expect(flash[:error]).to be_present
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_variables.scss:
--------------------------------------------------------------------------------
1 | // Typography
2 | $base-font-family: $helvetica;
3 | $heading-font-family: $base-font-family;
4 |
5 | // Font Sizes
6 | $base-font-size: 1em;
7 |
8 | // Line height
9 | $base-line-height: 1.5;
10 | $heading-line-height: 1.2;
11 |
12 | // Other Sizes
13 | $base-border-radius: 3px;
14 | $base-spacing: $base-line-height * 1em;
15 | $small-spacing: $base-spacing / 2;
16 | $base-z-index: 0;
17 |
18 | // Colors
19 | $blue: #477dca;
20 | $dark-gray: #333;
21 | $medium-gray: #999;
22 | $light-gray: #ddd;
23 |
24 | // Font Colors
25 | $base-background-color: #fff;
26 | $base-font-color: $dark-gray;
27 | $action-color: $blue;
28 |
29 | // Border
30 | $base-border-color: $light-gray;
31 | $base-border: 1px solid $base-border-color;
32 |
33 | // Forms
34 | $form-box-shadow: inset 0 1px 3px rgba(#000, 0.06);
35 | $form-box-shadow-focus: $form-box-shadow, 0 0 5px adjust-color($action-color, $lightness: -5%, $alpha: -0.3);
36 |
--------------------------------------------------------------------------------
/app/models/quarterly_message.rb:
--------------------------------------------------------------------------------
1 | class QuarterlyMessage < ActiveRecord::Base
2 | acts_as_paranoid
3 | acts_as_taggable
4 |
5 | has_many :sent_messages, as: :message, dependent: :destroy
6 |
7 | validates :body, presence: true
8 | validates :tag_list, presence: true
9 | validates :title, presence: true
10 |
11 | def self.filter(params)
12 | if params[:title].present? ||
13 | params[:body].present? ||
14 | params[:tag].present?
15 |
16 | results = where("lower(title) like ?", "%#{params[:title].downcase}%").
17 | where("lower(body) like ?", "%#{params[:body].downcase}%")
18 |
19 | if params[:tag].present?
20 | tags = params[:tag].split(",").each(&:strip)
21 | results = results.tagged_with(tags, any: true)
22 | end
23 | results
24 | else
25 | all
26 | end
27 | end
28 |
29 | def self.ordered_by_created_at
30 | order(created_at: :desc)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/features/view_employees_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "View employees" do
4 | scenario "sees all employee details" do
5 | login_with_oauth
6 | visit root_path
7 | create_employees
8 |
9 | visit employees_path
10 |
11 | expect(page).to have_content(first_employee.slack_username)
12 | expect(page).to have_content(first_employee.started_on)
13 | expect(page).to have_content(first_employee.time_zone)
14 | expect(page).to have_content(second_employee.slack_username)
15 | expect(page).to have_content(second_employee.started_on)
16 | expect(page).to have_content(second_employee.time_zone)
17 | end
18 |
19 | private
20 |
21 | def create_employees
22 | first_employee
23 | second_employee
24 | end
25 |
26 | def first_employee
27 | @first_employee ||= create(:employee)
28 | end
29 |
30 | def second_employee
31 | @second_employee ||= create(:employee)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/services/next_working_day_finder_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe NextWorkingDayFinder do
4 |
5 | it "returns the given day when it is a non-holiday weekday" do
6 | expect(NextWorkingDayFinder.run(non_holiday_monday)).to eq non_holiday_monday
7 | end
8 |
9 | it "returns the coming non holiday monday when it is a weekend day" do
10 | expect(NextWorkingDayFinder.run(saturday)).to eq saturday + 2.days
11 | end
12 |
13 | it "returns the nearest workday after a midweek holiday" do
14 | BusinessTime::Config.holidays << tues_nov_1_national_pet_your_cat_day
15 |
16 | expect(NextWorkingDayFinder.run(tues_nov_1_national_pet_your_cat_day)).
17 | to eq tues_nov_1_national_pet_your_cat_day + 1.days
18 | end
19 |
20 |
21 | def non_holiday_monday
22 | Date.parse('4-4-2016')
23 | end
24 |
25 | def saturday
26 | Date.parse('4-6-2016')
27 | end
28 |
29 | def tues_nov_1_national_pet_your_cat_day
30 | Date.parse('1-11-2016')
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/db/migrate/20151006161910_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 1)
2 | class ActsAsTaggableOnMigration < ActiveRecord::Migration
3 | def self.up
4 | create_table :tags do |t|
5 | t.string :name
6 | end
7 |
8 | create_table :taggings do |t|
9 | t.references :tag
10 |
11 | # You should make sure that the column created is
12 | # long enough to store the required class names.
13 | t.references :taggable, polymorphic: true
14 | t.references :tagger, polymorphic: true
15 |
16 | # Limit is created to prevent MySQL error on index
17 | # length for MyISAM table type: http://bit.ly/vgW2Ql
18 | t.string :context, limit: 128
19 |
20 | t.datetime :created_at
21 | end
22 |
23 | add_index :taggings, :tag_id
24 | add_index :taggings, [:taggable_id, :taggable_type, :context]
25 | end
26 |
27 | def self.down
28 | drop_table :taggings
29 | drop_table :tags
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/models/sent_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe SentMessage do
4 | describe "Associations" do
5 | it { should belong_to(:employee) }
6 | it { should belong_to(:message) }
7 | end
8 |
9 | describe "Validations" do
10 | it { should validate_presence_of(:employee) }
11 | it { should validate_presence_of(:message_body) }
12 | it { should validate_presence_of(:message) }
13 | it { should validate_presence_of(:sent_at) }
14 | it { should validate_presence_of(:sent_on) }
15 | end
16 |
17 | describe "Delegated methods" do
18 | it { should delegate_method(:slack_username).to(:employee) }
19 | end
20 |
21 | describe "Scopes" do
22 | describe ".by_year" do
23 | it "should select records created in the given year" do
24 | _older = create(:sent_message, created_at: Date.parse('1-4-2015'))
25 | newer = create(:sent_message, created_at: Date.parse('1-4-2016'))
26 |
27 | expect(SentMessage.by_year(2016)).to match_array([newer])
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/services/quarterly_message_day_verifier.rb:
--------------------------------------------------------------------------------
1 | require 'business_time'
2 |
3 | class QuarterlyMessageDayVerifier
4 | attr_reader :date, :current_year
5 |
6 | DEFAULT_MONTH_DAYS_TO_SEND_ON = [[1, 1], [4, 1], [10, 1], [7, 1]].freeze
7 |
8 | def initialize(date: Time.today)
9 | @date = date
10 | @current_year = @date.year
11 | end
12 |
13 | def run
14 | quarterly_message_day?
15 | end
16 |
17 | private
18 |
19 | def quarterly_message_day?
20 | this_years_quarterly_message_days_pushed_to_workdays.any? do |quarter_date|
21 | @date.month == quarter_date.month && @date.day == quarter_date.day
22 | end
23 | end
24 |
25 | def this_years_unadjusted_quarterly_message_days
26 | DEFAULT_MONTH_DAYS_TO_SEND_ON.map do |month_day|
27 | Date.new(@current_year, month_day.first, month_day.last)
28 | end
29 | end
30 |
31 | def this_years_quarterly_message_days_pushed_to_workdays
32 | this_years_unadjusted_quarterly_message_days.map do |date|
33 | NextWorkingDayFinder.run(date)
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/features/delete_employees_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Delete employees" do
4 | scenario "from list of employees", :js do
5 | employee = create(:employee)
6 |
7 | login_with_oauth
8 | visit employees_path
9 | click_accept_on_javascript_popup do
10 | page.find(".button-delete").click
11 | end
12 |
13 | expect(page).to have_content("You deleted #{employee.slack_username}")
14 | end
15 |
16 | scenario "and re-add" do
17 | username = "testusername2"
18 | _employee = create(:employee, slack_username: username)
19 |
20 | login_with_oauth
21 | visit employees_path
22 | page.find(".button-delete").click
23 |
24 | visit new_employee_path
25 | fill_in "Slack username", with: username
26 | select "2015", from: "employee_started_on_1i"
27 | select "June", from: "employee_started_on_2i"
28 | select "1", from: "employee_started_on_3i"
29 | select "Eastern Time (US & Canada)", from: "employee_time_zone"
30 | click_on "Create Employee"
31 |
32 | expect(page).to have_content("Thanks for adding #{username}")
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/views/onboarding_messages/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render :layout => '/application/header', :locals => {:title => 'Filter', :subtitle => '(Please separate tags with a comma)'} do %>
2 | <%= form_tag(onboarding_messages_path, method: "get", class: "navbar-form", slack_username: "search-form") do %>
3 |
4 | <%= text_field_tag :title, params[:title], class: "filter-input", placeholder: "title" %>
5 | <%= text_field_tag :body, params[:body], class: "filter-input", placeholder: "body" %>
6 | <%= text_field_tag :tag, params[:tag], class: "filter-input", placeholder: "tag, tag" %>
7 |
8 | Filter
9 |
10 | <% end %>
11 | <% end %>
12 |
13 |
14 | <%= render "flashes" -%>
15 | <% @onboarding_messages.each do |message| %>
16 | <%= render 'onboarding_message', message: message %>
17 | <% end %>
18 | <% if @onboarding_messages.empty? %>
19 | No matches found.
20 | <% end %>
21 |
22 | <%= paginate(@onboarding_messages) %>
23 |
24 |
--------------------------------------------------------------------------------
/spec/features/view_sent_messages_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "View message sent to employees" do
4 | scenario "see all sent scheduled messages" do
5 | login_with_oauth
6 | visit root_path
7 | create_message
8 | create_employee
9 | send_message_to_employee
10 | change_message_body
11 |
12 | visit sent_messages_path
13 |
14 | expect(page).to have_content(original_message_body)
15 | expect(page).to have_content(create_employee.slack_username)
16 | end
17 |
18 | private
19 |
20 | def create_message
21 | @message ||= create(:onboarding_message, body: original_message_body)
22 | end
23 |
24 | def original_message_body
25 | "original message body"
26 | end
27 |
28 | def create_employee
29 | @employee ||= create(:employee)
30 | end
31 |
32 | def send_message_to_employee
33 | create(
34 | :sent_message,
35 | message_body: original_message_body,
36 | message: create_message,
37 | employee: create_employee
38 | )
39 | end
40 |
41 | def change_message_body
42 | create_message.update(body: "new message body")
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/models/onboarding_message.rb:
--------------------------------------------------------------------------------
1 | class OnboardingMessage < ActiveRecord::Base
2 | acts_as_paranoid
3 | acts_as_taggable
4 |
5 | has_many :sent_messages, as: :message, dependent: :destroy
6 |
7 | validates :body, presence: true
8 | validates :days_after_start, presence: true
9 | validates :tag_list, presence: true
10 | validates :time_of_day, presence: true
11 | validates :title, presence: true
12 |
13 | def self.active
14 | where("end_date IS NULL OR end_date > ?", Date.today)
15 | end
16 |
17 | def self.date_time_ordering
18 | order(:days_after_start, :time_of_day)
19 | end
20 |
21 | def self.filter(params)
22 | if params[:title].present? ||
23 | params[:body].present? ||
24 | params[:tag].present?
25 |
26 | results = where("lower(title) like ?", "%#{params[:title].downcase}%").
27 | where("lower(body) like ?", "%#{params[:body].downcase}%")
28 |
29 | if params[:tag].present?
30 | tags = params[:tag].split(",").each(&:strip)
31 | results = results.tagged_with(tags, any: true)
32 | end
33 | results
34 | else
35 | all
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 5.0 upgrade.
4 | #
5 | # Once upgraded flip defaults one by one to migrate to the new default.
6 | #
7 | # Read the Rails 5.0 release notes for more info on each option.
8 |
9 | # Enable per-form CSRF tokens. Previous versions had false.
10 | Rails.application.config.action_controller.per_form_csrf_tokens = false
11 |
12 | # Enable origin-checking CSRF mitigation. Previous versions had false.
13 | Rails.application.config.action_controller.forgery_protection_origin_check =
14 | false
15 |
16 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
17 | # Previous versions had false.
18 | ActiveSupport.to_time_preserves_timezone = false
19 |
20 | # Require `belongs_to` associations by default. Previous versions had false.
21 | Rails.application.config.active_record.belongs_to_required_by_default = false
22 |
23 | # Do not halt callback chains when a callback returns false.
24 | # Previous versions had true.
25 | ActiveSupport.halt_callback_chains_on_return_false = true
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_typography.scss:
--------------------------------------------------------------------------------
1 | body {
2 | @include font-feature-settings("kern", "liga", "pnum");
3 | -webkit-font-smoothing: antialiased;
4 | color: $base-font-color;
5 | font-family: $base-font-family;
6 | font-size: $base-font-size;
7 | line-height: $base-line-height;
8 | }
9 |
10 | h1,
11 | h2,
12 | h3,
13 | h4,
14 | h5,
15 | h6 {
16 | font-family: $heading-font-family;
17 | font-size: $base-font-size;
18 | line-height: $heading-line-height;
19 | margin: 0 0 $small-spacing;
20 | }
21 |
22 | h2 {
23 | font-size: $base-font-size * 1.5;
24 | }
25 |
26 | p {
27 | margin: 0 0 $small-spacing;
28 | }
29 |
30 | a {
31 | color: $action-color;
32 | text-decoration: none;
33 | transition: color 0.1s linear;
34 |
35 | &:active,
36 | &:focus,
37 | &:hover {
38 | color: darken($action-color, 15%);
39 | }
40 |
41 | &:active,
42 | &:focus {
43 | outline: none;
44 | }
45 | }
46 |
47 | hr {
48 | border-bottom: $base-border;
49 | border-left: none;
50 | border-right: none;
51 | border-top: none;
52 | margin: $base-spacing 0;
53 | }
54 |
55 | img,
56 | picture {
57 | margin: 0;
58 | max-width: 100%;
59 | }
60 |
--------------------------------------------------------------------------------
/app/views/quarterly_messages/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render :layout => '/application/header', :locals => {:title => 'Filter', :subtitle => '(Please separate tags with a comma)'} do %>
2 | <%= form_tag(quarterly_messages_path, method: "get", class: "navbar-form", slack_username: "search-form") do %>
3 |
4 | <%= text_field_tag :title, params[:title], class: "filter-input", placeholder: "title" %>
5 | <%= text_field_tag :body, params[:body], class: "filter-input", placeholder: "body" %>
6 | <%= text_field_tag :tag, params[:tag], class: "filter-input", placeholder: "tag, tag" %>
7 |
8 | Filter
9 |
10 | <% end %>
11 | <% end %>
12 |
13 |
14 | <%= render "flashes" -%>
15 | <%= render "disabled_alert" %>
16 | <% @quarterly_messages.each do |message| %>
17 | <%= render 'quarterly_message', message: message %>
18 | <% end %>
19 | <% if @quarterly_messages.empty? %>
20 | No matches found.
21 | <% end %>
22 |
23 | <%= paginate(@quarterly_messages) %>
24 |
25 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_buttons.scss:
--------------------------------------------------------------------------------
1 | button,
2 | button a,
3 | input[type='button'],
4 | input[type='submit'],
5 | input[type='reset'] {
6 | background: $bright-18f;
7 | border: 1px solid $bright-18f;
8 | color: $dark-18f;
9 |
10 | &:hover,
11 | &:focus {
12 | background: $dark-18f;
13 | color: $white;
14 | border: 1px solid $bright-18f;
15 | text-decoration: none;
16 |
17 | a {
18 | background: $dark-18f;
19 | color: $white;
20 | border: 1px solid $bright-18f;
21 | text-decoration: none;
22 | }
23 | }
24 | }
25 |
26 | .button {
27 | @extend button;
28 | }
29 |
30 | .button-test {
31 | background: $medium-18f;
32 | color: $white;
33 | border: 1px solid $medium-18f;
34 | }
35 |
36 | .button-delete {
37 | background: $dark-18f;
38 | color: $white;
39 | border: 1px solid $bright-18f;
40 |
41 | &:hover,
42 | &:focus {
43 | background: $red;
44 | color: $dark-18f;
45 | border: 1px solid $dark-18f;
46 | }
47 | }
48 |
49 | .button-edit {
50 |
51 | object {
52 | width: 20px;
53 | height: 20px;
54 | }
55 |
56 | }
57 |
58 | .test-svg path {
59 | stroke: white;
60 | }
61 |
--------------------------------------------------------------------------------
/app/views/onboarding_messages/_onboarding_message.html.erb:
--------------------------------------------------------------------------------
1 | <%= message.title %>
2 |
3 | Day <%= message.days_after_start %> -
4 | <%= message.time_of_day.strftime('%l:%M %p') %>
5 |
6 | <%= message.body %>
7 | <% if message.end_date.present? %>
8 |
9 | End date:
10 | <%= message.end_date %>
11 |
12 | <% end %>
13 |
14 | Tags:
15 | <% message.tag_list.each do |tag| %>
16 | <%= link_to tag, onboarding_messages_path(tag: tag, title: '', body: '') %>
17 | <% end %>
18 |
19 | <% if current_user.admin? %>
20 | <%= link_to edit_onboarding_message_path(message), class: 'button button-edit' do %>
21 |
22 | Edit
23 | <% end %>
24 | <%= link_to new_onboarding_message_test_message_path(message), class: 'button button-test' do %>
25 |
26 | Test
27 | <% end %>
28 | <%= link_to 'Delete', message,
29 | method: :delete,
30 | class: 'button button-delete icon-circle-x',
31 | data: { confirm: 'Are you sure you want to delete this message?' }
32 | %>
33 | <% end %>
34 |
35 |
--------------------------------------------------------------------------------
/spec/services/broadcast_message_sender_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe BroadcastMessageSender do
4 | describe "#run" do
5 | it "sends the broadcast message to all employees and sents the last_sent_at" do
6 | broadcast_message = create(:broadcast_message)
7 | employees = usernames_from_fixture.map do |username|
8 | create(:employee, slack_username: username)
9 | end
10 |
11 | message_sender = double(:message_sender)
12 | allow(message_sender).to receive(:delay).and_return(double(run: true))
13 | allow(MessageSender).to receive(:new).and_return(message_sender)
14 |
15 | Timecop.freeze do
16 | BroadcastMessageSender.new(broadcast_message).run
17 |
18 | employees.each do |employee|
19 | expect(MessageSender).to have_received(:new).with(employee, broadcast_message)
20 | expect(message_sender).to have_received(:delay).exactly(4).times
21 | end
22 |
23 | expect(broadcast_message.reload.last_sent_at).
24 | to be_within(1.second).of Time.current
25 | end
26 | end
27 | end
28 |
29 | def usernames_from_fixture
30 | %w(testusername testusername2 testusername3 u2)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # Set up Rails app. Run this script immediately after cloning the codebase.
4 | # https://github.com/thoughtbot/guides/tree/master/protocol
5 |
6 | # Exit if any subcommand fails
7 | set -e
8 |
9 | # Set up Ruby dependencies via Bundler
10 | gem install bundler --conservative
11 | bundle check || bundle install
12 |
13 | # Set up configurable environment variables
14 | if [ ! -f .env ]; then
15 | cp .sample.env .env
16 | fi
17 |
18 | # Set up database and add any development seed data
19 | bundle exec rake db:setup dev:prime
20 |
21 | # Add binstubs to PATH via export PATH=".git/safe/../../bin:$PATH" in ~/.zshenv
22 | mkdir -p .git/safe
23 |
24 | # Pick a port for Foreman
25 | if ! grep --quiet --no-messages --fixed-strings 'port' .foreman; then
26 | printf 'port: 5000\n' >> .foreman
27 | fi
28 |
29 | if ! command -v foreman > /dev/null; then
30 | printf 'Foreman is not installed.\n'
31 | printf 'See https://github.com/ddollar/foreman for install instructions.\n'
32 | fi
33 |
34 | # Foreman uses the tmp/pids directory to create its processes, so we need to
35 | # make sure that dir is created
36 | mkdir -p tmp/pids
37 |
38 | # Only if this isn't CI
39 | # if [ -z "$CI" ]; then
40 | # fi
41 |
--------------------------------------------------------------------------------
/spec/features/edit_employees_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Edit employees" do
4 | scenario "from list of employees" do
5 | old_slack_username = "testusername"
6 | new_slack_username = "testusername3"
7 | create(:employee, slack_username: old_slack_username)
8 |
9 | login_with_oauth
10 | visit employees_path
11 | page.find(".button-edit").click
12 | fill_in "Slack username", with: new_slack_username
13 | click_on "Update Employee"
14 |
15 | expect(page).to have_content "Employee updated successfully"
16 | expect(page).to have_content new_slack_username
17 | end
18 |
19 | scenario "unsuccessfully due to an invalid username" do
20 | old_slack_username = "testusername"
21 | new_slack_username = "fakeusername3"
22 | create(:employee, slack_username: old_slack_username)
23 |
24 | login_with_oauth
25 | visit employees_path
26 | page.find(".button-edit").click
27 | fill_in "Slack username", with: new_slack_username
28 | click_on "Update Employee"
29 |
30 | expect(page).to have_content(
31 | I18n.t(
32 | 'employees.errors.slack_username_in_org',
33 | slack_username: new_slack_username
34 | )
35 | )
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.cache_classes = false
3 | config.eager_load = false
4 | config.consider_all_requests_local = true
5 | if Rails.root.join("tmp/caching-dev.txt").exist?
6 | config.action_controller.perform_caching = true
7 | config.cache_store = :memory_store
8 | config.public_file_server.headers = {
9 | "Cache-Control" => "public, max-age=172800",
10 | }
11 | else
12 | config.action_controller.perform_caching = false
13 | config.cache_store = :null_store
14 | end
15 | config.action_mailer.raise_delivery_errors = true
16 | config.after_initialize do
17 | Bullet.enable = true
18 | Bullet.bullet_logger = true
19 | Bullet.rails_logger = true
20 | end
21 | config.action_mailer.delivery_method = :file
22 | config.action_mailer.perform_caching = false
23 | config.active_support.deprecation = :log
24 | config.active_record.migration_error = :page_load
25 | config.assets.debug = true
26 | config.assets.quiet = true
27 | config.action_view.raise_on_missing_translations = true
28 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
29 | config.action_mailer.default_url_options = { host: "localhost:3000" }
30 | end
31 |
--------------------------------------------------------------------------------
/app/views/onboarding_messages/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= simple_form_for @onboarding_message do |form| %>
2 | <%= form.input :title,
3 | hint: "This is for identifying purposes only, the employee will not see the message title" %>
4 | <%= form.input :body, label: "Message body", as: 'text',
5 | hint: "Don't forget, you can format your message using #{link_to "Slack's message formatting rules",
6 | "https://slack.zendesk.com/hc/en-us/articles/202288908-Formatting-your-messages", target: "_blank"}.".html_safe %>
7 | <%= form.input :days_after_start, label: "Business days after employee starts to send message" %>
8 | <%= form.input :time_of_day,
9 | label: "Time of day to send message (uses employee's local time zone)",
10 | ampm: true,
11 | input_html: { class: "date-time-select" } %>
12 | <%= form.input :end_date,
13 | label: "End date",
14 | hint: "If this message is only valid for a limited time, enter its end date here",
15 | input_html: { class: "date-select" },
16 | include_blank: true %>
17 | <%= form.input :tag_list, label: "Tags", hint: "Separate tags with commas to enter multiple tags at once.", input_html: {value: @onboarding_message.tag_list.to_s} %>
18 | <%= form.button :submit %>
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= (yield(:title) || "Untitled") %>
8 | <%= stylesheet_link_tag :application, media: "all" %>
9 | <%= favicon_link_tag '18F_Logo/favicons/favicon-16x16.png' %>
10 | <%= csrf_meta_tags %>
11 |
12 |
13 |
14 |
15 |
16 | <%= link_to root_path do %>
17 | <%= image_tag "dolores.jpg", class: "banner-image_small" %>
18 | <% end %>
19 |
20 |
A Slack bot to welcome new 18F hires with the authority and compassion of Mrs. Landingham
21 |
22 |
23 |
24 | <% if signed_in? %>
25 | <% if signed_in_as_admin? %>
26 | <%= render 'layouts/admin_sidenav_items'%>
27 | <% end %>
28 | <%= render 'layouts/sidenav_items'%>
29 | <% end %>
30 |
31 |
32 |
33 |
36 | <%= render "javascript" %>
37 |
38 |
--------------------------------------------------------------------------------
/spec/factories.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | sequence(:slack_username) { |n| "testusername#{n}" }
3 | sequence(:slack_user_id) { |n| "ID123#{n}" }
4 |
5 |
6 | factory :user do
7 | email "test@example.com"
8 |
9 | factory :admin do
10 | admin true
11 | email "admin@example.com"
12 | end
13 | end
14 |
15 | factory :employee do
16 | slack_username
17 | slack_user_id
18 | started_on { Date.today }
19 | time_zone "Eastern Time (US & Canada)"
20 | end
21 |
22 | factory :sent_message do
23 | employee
24 | association :message, factory: :onboarding_message
25 | message_body "Message body!"
26 | sent_on { Date.today }
27 | sent_at { Time.parse("10:00:00 UTC") }
28 | end
29 |
30 | factory :onboarding_message do
31 | title "Onboarding message 1"
32 | body "This is an awesome onboarding message!"
33 | days_after_start 3
34 | time_of_day { Time.parse("10:00:00 UTC") }
35 | tag_list "test_tag, test_tag_two"
36 | end
37 |
38 | factory :quarterly_message do
39 | title "Quarterly message 1"
40 | body "This is an awesome quarterly message!"
41 | tag_list "test_tag, test_tag_two"
42 | end
43 |
44 | factory :broadcast_message do
45 | title "Message to everyone"
46 | body "Everyone needs to know this thing!"
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_filter.scss:
--------------------------------------------------------------------------------
1 | .filter-area {
2 | border-bottom: 1px solid $dark-18f;
3 | left: 25%;
4 | right: 0;
5 | padding: $base-padding;
6 | background: $dark-18f;
7 | color: $white;
8 | top: 0;
9 | width: 75%;
10 | position: fixed;
11 |
12 | &.fixedsticky-on {
13 | border-bottom: 1px solid $dark-18f;
14 | left: 25%;
15 | right: 0;
16 | padding: $base-padding;
17 | background: $dark-18f;
18 | color: $white;
19 | width: 75%;
20 | }
21 | }
22 |
23 | .filter-input {
24 | display: inline-block;
25 | float: left;
26 | margin-right: 5px;
27 | max-width: 150px;
28 | }
29 |
30 | .filter-button {
31 | float: left;
32 | padding: 9px;
33 | padding-left: 15px;
34 | padding-right: 15px;
35 | background: $dark-18f;
36 | color: $white;
37 | &:hover,
38 | &:focus {
39 | background: $bright-18f;
40 | color: $dark-18f;
41 | }
42 | }
43 |
44 | .filter-logo {
45 | float: right;
46 | width: 50px;
47 | }
48 |
49 | .filter-logo_text {
50 | float: left;
51 | margin-right: $base-padding;
52 | }
53 |
54 | .filter-logo_container {
55 | float: right;
56 | }
57 |
58 | .filter-inputs {
59 | float: left;
60 | }
61 |
62 | .filter-header {
63 | @include font-nimbus('bold');
64 | }
65 |
66 | .filter-subheader {
67 | font-style: italic;
68 | font-size: 0.8em;
69 | }
70 |
--------------------------------------------------------------------------------
/app/services/quarterly_message_employee_matcher.rb:
--------------------------------------------------------------------------------
1 | require "business_time"
2 |
3 | class QuarterlyMessageEmployeeMatcher
4 | TIME_OF_DAY_TO_SEND = 900
5 |
6 | def initialize(message)
7 | @message = message
8 | end
9 |
10 | def run
11 | if right_day_to_send_message?
12 | employees_needing_a_quarterly_message
13 | else
14 | []
15 | end
16 | end
17 |
18 | private
19 |
20 | def employees_needing_a_quarterly_message
21 | Employee.all.select do |employee|
22 | quarterly_message_not_already_sent?(employee) &&
23 | time_to_send_message?(employee.time_zone)
24 | end
25 | end
26 |
27 | def right_day_to_send_message?
28 | QuarterlyMessageDayVerifier.new(date: Time.now).run
29 | end
30 |
31 | def quarterly_message_not_already_sent?(employee)
32 | SentMessage.by_year(current_year).
33 | where("sent_on > ?", 1.week.ago).
34 | where(employee: employee, message: @message).count == 0
35 | end
36 |
37 | def time_to_send_message?(time_zone)
38 | employee_current_time = Time.current.in_time_zone(time_zone)
39 | employee_current_time_value = employee_current_time.strftime("%H%M").to_i
40 | message_time_value = TIME_OF_DAY_TO_SEND
41 | employee_current_time_value >= message_time_value
42 | end
43 |
44 | def current_year
45 | Time.now.year
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | class AuthConstraint
2 | def matches?(request)
3 | email = request.session[:user] ? request.session[:user]["email"] : nil
4 | email.present? && User.exists?(email: email)
5 | end
6 | end
7 |
8 | Rails.application.routes.draw do
9 | constraints(AuthConstraint.new) do
10 | root to: "onboarding_messages#index"
11 | end
12 |
13 | root to: "sessions#new"
14 |
15 | match "/auth/:provider/callback" => "auth#oauth_callback", via: [:get]
16 | resource :session, only: [:new, :create, :destroy]
17 | resources :employees, only: [:new, :create, :index, :edit, :update, :destroy]
18 | resources :users, only: [:edit, :update, :index]
19 | resources :sent_messages, only: [:index]
20 | resources :onboarding_messages, only: [
21 | :new,
22 | :create,
23 | :index,
24 | :edit,
25 | :update,
26 | :destroy,
27 | ] do
28 | resources :test_messages, only: [:new, :create]
29 | end
30 | resources :quarterly_messages, only: [
31 | :new,
32 | :create,
33 | :index,
34 | :edit,
35 | :update,
36 | :destroy,
37 | ] do
38 | resources :test_messages, only: [:new, :create]
39 | end
40 | resources :broadcast_messages, only: [:new, :create, :index] do
41 | resources :send_broadcast_messages, only: [:create]
42 | resources :test_messages, only: [:new, :create]
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | Vagrant.configure(2) do |config|
5 | config.vm.box = "ubuntu/trusty64"
6 |
7 | config.vm.network "forwarded_port", guest: 5000, host: 5000
8 |
9 | config.vm.provider "virtualbox" do |vb|
10 | vb.memory = "1024"
11 | end
12 |
13 | config.vm.provision "shell", privileged: false, inline: <<-SHELL
14 | # print command to stdout before executing it:
15 | set -x
16 | set -e
17 |
18 | curl -sSL https://rvm.io/mpapis.asc | gpg --import -
19 | curl -L https://get.rvm.io | bash -s stable --autolibs=enabled --ruby
20 |
21 | source "$HOME/.rvm/scripts/rvm"
22 | rvm install 2.3.1
23 | rvm use 2.3.1
24 |
25 | echo 'source "$HOME/.rvm/scripts/rvm"' >> .bashrc
26 | echo "rvm use 2.3.1" >> .bashrc
27 |
28 | # install postgres
29 | sudo apt-get -y install postgresql postgresql-contrib libpq-dev node npm git
30 | sudo -u postgres psql -c "CREATE USER vagrant WITH PASSWORD 'vagrant';"
31 | sudo -u postgres psql -c "ALTER USER vagrant CREATEDB;"
32 |
33 | echo "localhost:5432:*:vagrant:vagrant" > .pgpass
34 | chmod 0600 .pgpass
35 |
36 | sudo mv /usr/sbin/node /usr/sbin/node-bak
37 | sudo ln -s /usr/bin/nodejs /usr/bin/node
38 | sudo npm install -g phantomjs
39 |
40 | cd /vagrant
41 |
42 | gem install bundle
43 | cp .sample.env .env
44 |
45 | ./bin/setup
46 | SHELL
47 | end
48 |
--------------------------------------------------------------------------------
/app/services/onboarding_message_employee_matcher.rb:
--------------------------------------------------------------------------------
1 | require "business_time"
2 |
3 | class OnboardingMessageEmployeeMatcher
4 | def initialize(message)
5 | @message = message
6 | end
7 |
8 | def run
9 | retrieve_employees_needing_onboarding_message
10 | end
11 |
12 | private
13 |
14 | attr_reader :message
15 |
16 | def retrieve_employees_needing_onboarding_message
17 | Employee.where(
18 | started_on: Range.new(
19 | day_count.business_days.ago - 1.day,
20 | day_count.business_days.ago + 1.day,
21 | ),
22 | ).select do |employee|
23 | time_to_send_message?(employee) &&
24 | onboarding_message_not_already_sent?(employee)
25 | end
26 | end
27 |
28 | def day_count
29 | message.days_after_start
30 | end
31 |
32 | def time_to_send_message?(employee)
33 | send_date_without_timezone = day_count.business_days.after(
34 | employee.started_on,
35 | )
36 | send_date_with_timezone = send_date_without_timezone.in_time_zone(
37 | employee.time_zone,
38 | )
39 | send_time = send_date_with_timezone.change(
40 | hour: message.time_of_day.hour,
41 | min: message.time_of_day.min,
42 | )
43 | Time.current >= send_time
44 | end
45 |
46 | def onboarding_message_not_already_sent?(employee)
47 | SentMessage.where(employee: employee, message: message).count == 0
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/features/sign_in_user_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Sign in user" do
4 | scenario "signing in a new user that is a member of the correct Github team" do
5 | setup_mock_auth("user@example.com")
6 |
7 | visit root_path
8 | click_link "Sign in with GitHub"
9 |
10 | expect(page).to have_content "You successfully signed in"
11 | end
12 |
13 | scenario "attempting to sign in a new user that is not a member of the Github team and encountering an error" do
14 | setup_mock_auth("user@example.com", team_member: false)
15 |
16 | visit root_path
17 | click_link "Sign in with GitHub"
18 |
19 | expect(page).to have_content "We were unable to authenticate your user profile"
20 | end
21 |
22 | scenario "signing in an existing user that is a member of the correct Github team" do
23 | user = create(:user)
24 | setup_mock_auth(user.email)
25 |
26 | visit root_path
27 | click_link "Sign in with GitHub"
28 |
29 | expect(page).to have_content "You successfully signed in"
30 | end
31 |
32 | scenario "signing in an existing user that has been removed from the correct Github team" do
33 | user = create(:user)
34 | setup_mock_auth(user.email, team_member: false)
35 |
36 | visit root_path
37 | click_link "Sign in with GitHub"
38 |
39 | expect(page).to have_content "We were unable to authenticate your user profile"
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/models/sent_message.rb:
--------------------------------------------------------------------------------
1 | class SentMessage < ActiveRecord::Base
2 | acts_as_paranoid
3 |
4 | belongs_to :employee
5 | belongs_to :message, polymorphic: true
6 |
7 | validates :employee, presence: true
8 | validates :message_body, presence: true
9 | validates :message, presence: true
10 | validates :sent_at, presence: true
11 | validates :sent_on, presence: true
12 |
13 | delegate :slack_username, to: :employee
14 |
15 | def self.by_year(year)
16 | where("extract(year from created_at) = ?", year)
17 | end
18 |
19 | def self.filter(params)
20 | if params[:slack_username].present? ||
21 | params[:message_body].present? ||
22 | params[:sent_on].present?
23 |
24 | @employees = Employee.where(
25 | "slack_username like ?",
26 | "%#{params[:slack_username].downcase}%",
27 | )
28 |
29 | results = none
30 |
31 | if @employees
32 | @employees.each do |e|
33 | new_results = where(employee_id: e.id)
34 | results = results.union(new_results)
35 | end
36 | end
37 |
38 | results = results.where(
39 | "lower(message_body) like ?",
40 | "%#{params[:message_body].downcase}%",
41 | )
42 |
43 | if !params[:sent_on].blank?
44 | results = results.where(sent_on: params[:sent_on])
45 | end
46 |
47 | results
48 | else
49 | all
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/app/models/employee.rb:
--------------------------------------------------------------------------------
1 | class Employee < ActiveRecord::Base
2 | acts_as_paranoid
3 |
4 | has_many :sent_messages, dependent: :destroy
5 |
6 | validates :slack_username, presence: true
7 | validates_uniqueness_of :slack_username, conditions: -> { where(deleted_at: nil) }
8 | validates(
9 | :slack_username,
10 | format: {
11 | with: /\A[a-z_0-9.-]+\z/,
12 | message: I18n.t('employees.errors.slack_username_format'),
13 | },
14 | )
15 |
16 | validates :started_on, presence: true
17 | validates :time_zone, presence: true
18 |
19 | def self.filter(params)
20 | results = self.all.where("slack_username like ?", "%#{params[:slack_username]}%")
21 | if params[:started_on].present?
22 | results.where(started_on: params[:started_on])
23 | end
24 | results
25 | end
26 |
27 | def validate_slack_username_in_org
28 | if !employee_finder.existing_employee?
29 | errors.add(
30 | :slack_username,
31 | I18n.t(
32 | 'employees.errors.slack_username_in_org',
33 | slack_username: slack_username,
34 | ),
35 | )
36 | end
37 | end
38 |
39 | def add_slack_user_id_to_employee
40 | user_id = employee_finder.slack_user_id
41 | if user_id.present?
42 | self.slack_user_id = user_id
43 | end
44 | end
45 |
46 | def employee_finder
47 | @_employee_finder ||= EmployeeFinder.new(slack_username)
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | As a work of the United States Government, this project is in the
2 | public domain within the United States.
3 |
4 | Additionally, we waive copyright and related rights in the work
5 | worldwide through the CC0 1.0 Universal public domain dedication.
6 |
7 | ## CC0 1.0 Universal Summary
8 |
9 | This is a human-readable summary of the
10 | [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
11 |
12 | ### No Copyright
13 |
14 | The person who associated a work with this deed has dedicated the work to
15 | the public domain by waiving all of his or her rights to the work worldwide
16 | under copyright law, including all related and neighboring rights, to the
17 | extent allowed by law.
18 |
19 | You can copy, modify, distribute and perform the work, even for commercial
20 | purposes, all without asking permission.
21 |
22 | ### Other Information
23 |
24 | In no way are the patent or trademark rights of any person affected by CC0,
25 | nor are the rights that other persons may have in the work or in how the
26 | work is used, such as publicity or privacy rights.
27 |
28 | Unless expressly stated otherwise, the person who associated a work with
29 | this deed makes no warranties about the work, and disclaims liability for
30 | all uses of the work, to the fullest extent permitted by applicable law.
31 | When using or citing the work, you should not imply endorsement by the
32 | author or the affirmer.
33 |
--------------------------------------------------------------------------------
/spec/services/employee_finder_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe EmployeeFinder do
4 | describe "#employee_exists?" do
5 | it "returns true if an employee exists in the Slack organization associated with the auth token" do
6 | employee = create(:employee)
7 | client_double = Slack::Web::Client.new
8 | existing_user_double = double(existing_user?: true)
9 |
10 | allow(Slack::Web::Client).to receive(:new).and_return(client_double)
11 | allow(SlackUserFinder).
12 | to receive(:new).with(employee.slack_username, client_double).
13 | and_return(existing_user_double)
14 |
15 | employee_finder = EmployeeFinder.new(employee.slack_username)
16 |
17 | expect(employee_finder).to be_existing_employee
18 | end
19 |
20 | it "returns false if an employee does not exist in the Slack organization associated with the auth token" do
21 | employee = create(:employee)
22 | client_double = Slack::Web::Client.new
23 | existing_user_double = double(existing_user?: false)
24 |
25 | allow(Slack::Web::Client).to receive(:new).and_return(client_double)
26 | allow(SlackUserFinder).
27 | to receive(:new).with(employee.slack_username, client_double).
28 | and_return(existing_user_double)
29 |
30 | employee_finder = EmployeeFinder.new(employee.slack_username)
31 |
32 | expect(employee_finder).not_to be_existing_employee
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 | helper_method :current_user, :signed_in?, :signed_in_as_admin?
4 | before_action :authenticate_user!
5 |
6 | protected
7 |
8 | def current_user_admin
9 | if !current_user.admin?
10 | flash[:error] = I18n.t('controllers.application_controller.errors.current_user_admin')
11 | redirect_to root_path
12 | end
13 | end
14 |
15 | def current_user
16 | @current_user ||= find_current_user
17 | end
18 |
19 | def find_current_user
20 | if session[:user] && session[:user]["email"]
21 | User.find_by(email: session[:user]["email"])
22 | end
23 | end
24 |
25 | def sign_in(user)
26 | session[:user] ||= {}
27 | session[:user]["email"] = user.email
28 | @current_user = user
29 | end
30 |
31 | def signout_user!
32 | session[:user]["email"] = nil
33 | @current_user = nil
34 | end
35 |
36 | def signed_in?
37 | current_user.present?
38 | end
39 |
40 | def signed_in_as_admin?
41 | @current_user && @current_user.admin?
42 | end
43 |
44 | def authenticate_user!
45 | unless signed_in?
46 | flash[:error] = I18n.t('application_controller.errors.authenticate_user')
47 | redirect_to "/session/new"
48 | end
49 | end
50 |
51 | def unauthorized
52 | raise ActionController::RoutingError.new("Unauthorized")
53 | end
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.middleware.use Rack::CanonicalHost, ENV.fetch("APPLICATION_HOST")
3 | config.cache_classes = true
4 | config.eager_load = true
5 | config.consider_all_requests_local = false
6 | config.action_controller.perform_caching = true
7 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
8 | config.assets.js_compressor = :uglifier
9 | config.assets.compile = false
10 | config.action_controller.asset_host =
11 | ENV.fetch("ASSET_HOST", ENV.fetch("APPLICATION_HOST"))
12 | config.log_level = :debug
13 | config.log_tags = [:request_id]
14 | config.action_mailer.perform_caching = false
15 | config.action_mailer.delivery_method = :smtp
16 | config.i18n.fallbacks = true
17 | config.active_support.deprecation = :notify
18 | config.log_formatter = ::Logger::Formatter.new
19 |
20 | if ENV["RAILS_LOG_TO_STDOUT"].present?
21 | logger = ActiveSupport::Logger.new(STDOUT)
22 | logger.formatter = config.log_formatter
23 | config.logger = ActiveSupport::TaggedLogging.new(logger)
24 | end
25 |
26 | config.active_record.dump_schema_after_migration = false
27 | config.middleware.use Rack::Deflater
28 | config.static_cache_control = "public, max-age=31557600"
29 | config.action_mailer.default_url_options = {
30 | host: ENV.fetch("APPLICATION_HOST"),
31 | }
32 | end
33 |
34 | Rack::Timeout.timeout = (ENV["RACK_TIMEOUT"] || 10).to_i
35 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | ENV["TZ"] = "UTC"
3 |
4 | require File.expand_path("../../config/environment", __FILE__)
5 | abort("DATABASE_URL environment variable is set") if ENV["DATABASE_URL"]
6 |
7 | require "rspec/rails"
8 | require "shoulda/matchers"
9 | require "capybara/poltergeist"
10 |
11 | Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }
12 |
13 | module Features
14 | include OauthHelper
15 | end
16 |
17 | module Services
18 | include SlackApiHelper
19 | end
20 |
21 | include TimeHelper
22 |
23 | RSpec.configure do |config|
24 | config.before(:each) do
25 | FakeSlackApi.failure = false
26 | stub_request(:any, /slack.com/).to_rack(FakeSlackApi)
27 | end
28 |
29 | config.include Rails.application.routes.url_helpers
30 | config.include Features, type: :feature
31 | config.include Services
32 | config.infer_base_class_for_anonymous_controllers = false
33 | config.infer_spec_type_from_file_location!
34 | config.use_transactional_fixtures = false
35 | end
36 |
37 | ActiveRecord::Migration.maintain_test_schema!
38 | Capybara.javascript_driver = :poltergeist
39 | OmniAuth.config.test_mode = true
40 |
41 | Capybara.register_driver :poltergeist do |app|
42 | Capybara::Poltergeist::Driver.new(app, js_errors: false)
43 | end
44 |
45 | Shoulda::Matchers.configure do |config|
46 | config.integrate do |with|
47 | with.test_framework :rspec
48 | with.library :rails
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/assets/images/18F_Logo/SVG/18F-Logo-2016-Black.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/app/assets/images/18F_Logo/SVG/18F-Logo-2016-Blue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
9 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/features/send_onboarding_message_after_test_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Send an oboarding message after a test message" do
4 | scenario "Receiving a test message should not prevent a user from receiving the same scheduled onboarding message" do
5 | days_after_start = 3
6 | start_time = Time.parse("11:00:00 UTC")
7 | employee_time_zone = "Eastern Time (US & Canada)"
8 | current_time_for_employee = start_time.in_time_zone(employee_time_zone)
9 | current_time_et_as_utc = Time.zone.utc_to_local(current_time_for_employee)
10 | message_send_time = days_after_start.business_days.after start_time
11 |
12 | onboarding_message = create(
13 | :onboarding_message,
14 | days_after_start: days_after_start,
15 | time_of_day: current_time_et_as_utc
16 | )
17 | employee = create(:employee, started_on: start_time)
18 |
19 | Timecop.freeze(start_time) do
20 | send_test_onboarding_message(employee, onboarding_message)
21 |
22 | Timecop.travel(days_after_start.business_days.after start_time)
23 |
24 | expect { OnboardingMessageSender.new.run }.to change{ employee.sent_messages.count }.from(0).to(1)
25 | end
26 | end
27 |
28 | def send_test_onboarding_message(employee, onboarding_message)
29 | admin = create(:admin)
30 | login_with_oauth(admin)
31 | visit root_path
32 | visit new_onboarding_message_test_message_path(onboarding_message)
33 | fill_in :test_message_slack_username, with: employee.slack_username
34 | click_on "Send test"
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | ruby "2.3.8"
4 | gem "rails", "~> 5.0.0"
5 |
6 | gem "active_record_union"
7 | gem "acts-as-taggable-on", "4.0.0.pre"
8 | gem "autoprefixer-rails"
9 | gem "business_time"
10 | gem "bourbon"
11 | gem "clockwork"
12 | gem "daemons" # for delayed_job
13 | gem "delayed_job_active_record"
14 | gem "flutie"
15 | gem "foreman"
16 | gem "holidays"
17 | gem "jquery-rails"
18 | gem "kaminari"
19 | gem "neat"
20 | gem "omniauth-github-team-member"
21 | gem "paranoia", "2.2.0.pre"
22 | gem "pg"
23 | gem "puma"
24 | gem "rack-canonical-host"
25 | gem "recipient_interceptor"
26 | gem "sass-rails", "~> 5.0"
27 | gem "simple_form"
28 | gem "slack-ruby-client"
29 | gem "sprockets", ">= 3.0.0"
30 | gem "sprockets-es6"
31 | gem "title"
32 | gem "uglifier"
33 |
34 | group :development, :test do
35 | gem "awesome_print"
36 | gem "bullet"
37 | gem "bundler-audit", ">= 0.5.0", require: false
38 | gem "byebug"
39 | gem "dotenv-rails"
40 | gem "factory_girl_rails"
41 | gem "pry-rails"
42 | gem "rspec-rails", "~> 3.5.0.beta4"
43 | end
44 |
45 | group :development do
46 | gem "listen"
47 | gem "spring"
48 | gem "spring-commands-rspec"
49 | gem "rubocop", require: false
50 | end
51 |
52 | group :test do
53 | gem "poltergeist"
54 | gem "capybara", ">= 2.6.2"
55 | gem "database_cleaner"
56 | gem "shoulda-matchers"
57 | gem "simplecov", require: false
58 | gem "sinatra"
59 | gem "timecop"
60 | gem "webmock"
61 | end
62 |
63 | group :staging, :production do
64 | gem "rack-timeout"
65 | gem "rails_stdout_logging"
66 | end
67 |
--------------------------------------------------------------------------------
/spec/models/employee_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe Employee do
4 | describe "Associations" do
5 | it { should have_many(:sent_messages).dependent(:destroy) }
6 | end
7 |
8 | describe "Validations" do
9 | subject { build(:employee) }
10 | it { should validate_presence_of(:slack_username) }
11 | it { should validate_uniqueness_of(:slack_username) }
12 | it { should allow_value("test_user_1").for(:slack_username) }
13 | it { should allow_value("x").for(:slack_username) }
14 | it { should allow_value("test-user-1").for(:slack_username) }
15 | it { should allow_value("test.user.1").for(:slack_username) }
16 | it { should_not allow_value("TEST USER 1").for(:slack_username) }
17 | it { should_not allow_value("@test_user_1").for(:slack_username) }
18 | it { should validate_presence_of(:started_on) }
19 | it { should validate_presence_of(:time_zone) }
20 | end
21 |
22 | describe "#validate_slack_username_in_org" do
23 | it "returns true if slack username is in org" do
24 | username_from_fixture = "testusername"
25 | employee = build(:employee, slack_username: username_from_fixture)
26 |
27 |
28 | employee.validate_slack_username_in_org
29 |
30 | expect(employee.errors.size).to eq 0
31 | end
32 |
33 | it "adds error if slack username is not in org" do
34 | employee = build(:employee, slack_username: "not_in_org")
35 |
36 |
37 | employee.validate_slack_username_in_org
38 |
39 | expect(employee.errors.size).to eq 1
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/views/sent_messages/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render :layout => '/application/header', :locals => {:title => 'Filter'} do %>
2 | <%= form_tag(sent_messages_path, method: "get", class: "navbar-form", slack_username: "search-form") do %>
3 |
4 | <%= text_field_tag :slack_username, params[:slack_username], class: "filter-input", placeholder: "slack username" %>
5 | <%= text_field_tag :sent_on, params[:sent_on], class: "filter-input", placeholder: "sent on" %>
6 | <%= text_field_tag :message_body, params[:message_body], class: "filter-input", placeholder: "body" %>
7 |
8 |
9 | Filter
10 |
11 | <% end %>
12 | <% end %>
13 |
14 |
15 | <%= render "flashes" -%>
16 |
17 |
18 | Slack username
19 | Sent on
20 | Sent at
21 | Message body
22 | Error message
23 |
24 |
25 | <% @sent_messages.each do |sent_message| %>
26 |
27 | <%= sent_message.slack_username %>
28 | <%= sent_message.sent_on %>
29 | <%= display_local_time_zone(sent_message) %>
30 | <%= truncate(sent_message.message_body, length: 50, separator: ' ') %>
31 | <%= sent_message.error_message %>
32 |
33 | <% end %>
34 |
35 | <% if @sent_messages.empty? %>
36 | No matches found.
37 | <% end %>
38 |
39 | <%= paginate(@sent_messages) %>
40 |
41 |
--------------------------------------------------------------------------------
/spec/models/onboarding_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe OnboardingMessage do
4 | describe "Associations" do
5 | it { should have_many(:sent_messages).dependent(:destroy) }
6 | end
7 |
8 | describe "Validations" do
9 | it { should validate_presence_of(:body) }
10 | it { should validate_presence_of(:days_after_start) }
11 | it { should validate_presence_of(:tag_list) }
12 | it { should validate_presence_of(:time_of_day) }
13 | it { should validate_presence_of(:title) }
14 | end
15 |
16 | describe '.date_time_ordering' do
17 | it 'should display the messages in chronological order' do
18 | m5 = create(:onboarding_message, days_after_start: 4, time_of_day: '2000-01-01 11:00:00 UTC')
19 | m2 = create(:onboarding_message, time_of_day: '2000-01-01 16:00:00 UTC')
20 | m1 = create(:onboarding_message, days_after_start: 1, time_of_day: '2000-01-01 11:00:00 UTC')
21 | m4 = create(:onboarding_message, time_of_day: '2000-01-01 11:00:00 UTC')
22 | m3 = create(:onboarding_message, time_of_day: '2000-01-01 14:00:00 UTC')
23 | expect(OnboardingMessage.date_time_ordering).to match_array([m1, m2, m3, m4, m5])
24 | end
25 | end
26 |
27 | describe '.active' do
28 | it 'should display messages which have no end date or have future end dates' do
29 | nil_end_date = create(:onboarding_message)
30 | _expired = create(:onboarding_message, end_date: Date.yesterday)
31 | future_end_date = create(:onboarding_message, end_date: Date.tomorrow)
32 | expect(OnboardingMessage.active).to match_array([nil_end_date, future_end_date])
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/base/_forms.scss:
--------------------------------------------------------------------------------
1 | fieldset {
2 | background-color: lighten($base-border-color, 10%);
3 | border: $base-border;
4 | margin: 0 0 $small-spacing;
5 | padding: $base-spacing;
6 | }
7 |
8 | input,
9 | label,
10 | select {
11 | font-family: $base-font-family;
12 | font-size: $base-font-size;
13 | }
14 |
15 | label {
16 | font-weight: 600;
17 | margin-bottom: $small-spacing / 2;
18 |
19 | &.required::after {
20 | content: "*";
21 | }
22 |
23 | abbr {
24 | display: none;
25 | }
26 | }
27 |
28 | #{$all-text-inputs},
29 | select[multiple=multiple],
30 | textarea {
31 | background-color: $base-background-color;
32 | border: $base-border;
33 | border-radius: $base-border-radius;
34 | box-shadow: $form-box-shadow;
35 | box-sizing: border-box;
36 | font-family: $base-font-family;
37 | font-size: $base-font-size;
38 | margin-bottom: $base-spacing / 2;
39 | padding: $base-spacing / 3;
40 | transition: border-color;
41 | width: 100%;
42 |
43 | &:hover {
44 | border-color: darken($base-border-color, 10%);
45 | }
46 |
47 | &:focus {
48 | border-color: $action-color;
49 | box-shadow: $form-box-shadow-focus;
50 | outline: none;
51 | }
52 | }
53 |
54 | textarea {
55 | resize: vertical;
56 | }
57 |
58 | input[type="search"] {
59 | @include appearance(none);
60 | }
61 |
62 | input[type="checkbox"],
63 | input[type="radio"] {
64 | display: inline;
65 | margin-right: $small-spacing / 2;
66 | }
67 |
68 | input[type="file"] {
69 | padding-bottom: $small-spacing;
70 | width: 100%;
71 | }
72 |
73 | select {
74 | margin-bottom: $base-spacing;
75 | max-width: 100%;
76 | width: auto;
77 | }
78 |
--------------------------------------------------------------------------------
/spec/features/create_employees_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Create employees" do
4 | scenario "successfully" do
5 | username = "testusername2"
6 | login_with_oauth
7 |
8 | visit new_employee_path
9 | fill_in "Slack username", with: username
10 | select "2015", from: "employee_started_on_1i"
11 | select "June", from: "employee_started_on_2i"
12 | select "1", from: "employee_started_on_3i"
13 | select "Eastern Time (US & Canada)", from: "employee_time_zone"
14 | click_on "Create Employee"
15 |
16 | expect(page).to have_content("Thanks for adding #{username}")
17 | end
18 |
19 | scenario "with short username" do
20 | username = "u2"
21 | login_with_oauth
22 |
23 | visit new_employee_path
24 | fill_in "Slack username", with: username
25 | select "2015", from: "employee_started_on_1i"
26 | select "June", from: "employee_started_on_2i"
27 | select "1", from: "employee_started_on_3i"
28 | select "Eastern Time (US & Canada)", from: "employee_time_zone"
29 | click_on "Create Employee"
30 |
31 | expect(page).to have_content("Thanks for adding #{username}")
32 | end
33 |
34 | scenario "unsuccessfully with invalid username" do
35 | username = "fakeusername2"
36 | login_with_oauth
37 |
38 | visit new_employee_path
39 | fill_in "Slack username", with: username
40 | select "2015", from: "employee_started_on_1i"
41 | select "June", from: "employee_started_on_2i"
42 | select "1", from: "employee_started_on_3i"
43 | select "Eastern Time (US & Canada)", from: "employee_time_zone"
44 | click_on "Create Employee"
45 |
46 | expect(page).to have_content(
47 | I18n.t('employees.errors.slack_username_in_org', slack_username: username)
48 | )
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/features/send_quarterly_messages_across_all_time_zones_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Create and send quarterly messages to users in every time zone" do
4 | context "admin user" do
5 | scenario "can create a quarterly message", :js do
6 | Timecop.freeze(day_before_quarterly_messages)
7 | create_employees_from_all_time_zones
8 |
9 | create_a_quarterly_message_via_form
10 | quarterly_message = QuarterlyMessage.first
11 | simulate_64_hours_of_daily_message_sending
12 |
13 | verify_that_all_employees_got_the_message(quarterly_message)
14 | end
15 | end
16 |
17 | #helpers
18 |
19 | def verify_that_all_employees_got_the_message(quarterly_message)
20 | Employee.all.each do |employee|
21 | expect(SentMessage.
22 | where(employee: employee, message: quarterly_message)).not_to be_empty
23 | end
24 | end
25 |
26 | def day_before_quarterly_messages
27 | Time.parse("2015-3-31 00:00:00 UTC")
28 | end
29 |
30 | def simulate_64_hours_of_daily_message_sending
31 | 64.times do
32 | fast_forward_one_hour
33 | QuarterlyMessageSender.new.run
34 | end
35 | end
36 |
37 | def create_a_quarterly_message_via_form
38 | admin = create(:admin)
39 | login_with_oauth(admin)
40 | visit root_path
41 | visit new_quarterly_message_path
42 | fill_in "Title", with: "Message title"
43 | fill_in "Message body", with: message_body
44 | fill_in "Tags", with: "tag_one, tag_two, tag_three"
45 | click_on "Create Quarterly message"
46 | end
47 |
48 | def message_body
49 | "Tax time is coming up!"
50 | end
51 |
52 | def create_employees_from_all_time_zones
53 | (-11..13).each do |zone|
54 | create(:employee, time_zone: time_zone_from_offset(zone))
55 | end
56 | end
57 |
58 | end
59 |
60 |
--------------------------------------------------------------------------------
/app/views/employees/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render :layout => '/application/header', :locals => {:title => 'Employees'} do %>
2 | <%= form_tag(employees_path, method: "get", class: "navbar-form") do %>
3 |
4 | <%= text_field_tag :slack_username, params[:slack_username], class: "filter-input", placeholder: "slack username" %>
5 | <%= text_field_tag :started_on, params[:started_on], class: "filter-input", placeholder: "date started" %>
6 | Filter
7 |
8 | <% end %>
9 | <% end %>
10 |
11 |
12 |
13 | <%= render "flashes" -%>
14 |
15 |
16 |
17 | Slack username
18 | Started on
19 | Time Zone
20 |
21 |
22 |
23 |
24 | <% @employees.each do |employee| %>
25 |
26 | <%= employee.slack_username %>
27 | <%= employee.started_on %>
28 | <%= employee.time_zone %>
29 |
30 | <%= link_to edit_employee_path(employee), class: 'button button-edit' do %>
31 |
32 | Edit
33 | <% end %>
34 |
35 |
36 | <%= link_to 'Delete', employee,
37 | method: :delete,
38 | class: 'button button-delete icon-circle-x',
39 | data: { confirm: "Are you sure you want to delete @#{employee.slack_username}?" }
40 | %>
41 |
42 |
43 | <% end %>
44 |
45 | <% if @employees.empty? %>
46 | No matches found.
47 | <% end %>
48 |
49 | <%= paginate(@employees) %>
50 |
51 |
--------------------------------------------------------------------------------
/spec/services/onboarding_message_sender_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe OnboardingMessageSender do
4 | describe "#run" do
5 | it "sends onboarding messages to employees" do
6 | onboarding_message = create(:onboarding_message)
7 | employee = create(:employee)
8 | message_sender_double = double(run: true)
9 |
10 | matcher_double = double(run: [employee])
11 | allow(OnboardingMessageEmployeeMatcher).
12 | to receive(:new).with(onboarding_message).and_return(matcher_double)
13 | allow(MessageSender).to receive(:new).with(employee, onboarding_message).and_return(message_sender_double)
14 |
15 | OnboardingMessageSender.new.run
16 |
17 | expect(message_sender_double).to have_received(:run)
18 | end
19 |
20 | it "sends only active onboarding messages" do
21 | onboarding_message = create(:onboarding_message)
22 | onboarding_message2 = create(:onboarding_message, end_date: Date.tomorrow)
23 | expired_message = create(:onboarding_message, end_date: Date.yesterday)
24 | employee = create(:employee)
25 | message_sender_double = double(run: true)
26 |
27 | matcher_double = double(run: [employee])
28 | allow(OnboardingMessageEmployeeMatcher).
29 | to receive(:new).with(onboarding_message).and_return(matcher_double)
30 | allow(OnboardingMessageEmployeeMatcher).
31 | to receive(:new).with(onboarding_message2).and_return(matcher_double)
32 | allow(MessageSender).to receive(:new).with(employee, onboarding_message).and_return(message_sender_double)
33 | allow(MessageSender).to receive(:new).with(employee, onboarding_message2).and_return(message_sender_double)
34 |
35 | OnboardingMessageSender.new.run
36 |
37 | expect(message_sender_double).to have_received(:run).twice
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | We're sorry, but something went wrong (500)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
We're sorry, but something went wrong.
64 |
65 |
If you are the application owner check the logs for more information.
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The change you wanted was rejected (422)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
The change you wanted was rejected.
64 |
Maybe you tried to change something you didn't have access to.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The page you were looking for doesn't exist (404)
8 |
9 |
58 |
59 |
60 |
61 |
62 |
63 |
The page you were looking for doesn't exist.
64 |
You may have mistyped the address or the page may have moved.
65 |
66 |
If you are the application owner check the logs for more information.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/spec/services/quarterly_message_day_verifier_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe QuarterlyMessageDayVerifier do
4 | context "not on weekends or holidays" do
5 | it "returns true if the current date is a Quarterly Message day" do
6 | expect(QuarterlyMessageDayVerifier.new(date: april_1_non_holiday_wednesday).run).to be_truthy
7 | end
8 | it "returns false if the current date is not a Quarterly Message day" do
9 | expect(QuarterlyMessageDayVerifier.new(date: april_3_non_holiday_friday).run).to be_falsey
10 | end
11 | end
12 |
13 | context "on a weekend" do
14 | it "returns false if the current date is a Quarterly Message day on a weekend" do
15 | expect(QuarterlyMessageDayVerifier.new(date: october_1_saturday).run).to be_falsey
16 | end
17 | it "returns true for the first workday following the weekend quarterly message day" do
18 | expect(QuarterlyMessageDayVerifier.new(date: october_1_saturday + 2.days).run).to be_truthy
19 | end
20 | end
21 |
22 | context "on a holiday" do
23 | it "returns false if the current date is a holiday" do
24 | BusinessTime::Config.holidays << fri_feb_6_national_eat_pickles_day
25 |
26 | expect(QuarterlyMessageDayVerifier.new(date: fri_feb_6_national_eat_pickles_day).run).to be_falsey
27 | end
28 | it "returns true for the first workday following the holiday quarterly message day" do
29 | BusinessTime::Config.holidays << fri_feb_6_national_eat_pickles_day
30 |
31 | expect(QuarterlyMessageDayVerifier.new(date: fri_feb_6_national_eat_pickles_day + 2.days).run).to be_falsey
32 | end
33 | end
34 |
35 | #helpers
36 |
37 | def april_1_non_holiday_wednesday
38 | Date.parse('1-4-2015')
39 | end
40 |
41 | def april_3_non_holiday_friday
42 | Date.parse('3-4-2015')
43 | end
44 |
45 | def october_1_saturday
46 | Date.parse('1-10-2016')
47 | end
48 |
49 | def fri_feb_6_national_eat_pickles_day
50 | Date.parse('6-1-2015')
51 | end
52 |
53 |
54 | end
55 |
--------------------------------------------------------------------------------
/app/controllers/test_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class TestMessagesController < ApplicationController
2 | def new
3 | @message = message
4 | @url = if message.is_a? OnboardingMessage
5 | onboarding_message_test_messages_path(@message)
6 | elsif message.is_a? QuarterlyMessage
7 | quarterly_message_test_messages_path(@message)
8 | else
9 | broadcast_message_test_messages_path(@message)
10 | end
11 | end
12 |
13 | def create
14 | employee = Employee.find_by(slack_username: slack_username)
15 |
16 | if employee && (employee.slack_channel_id || employee.slack_user_id)
17 | MessageSender.new(employee, message, test_message: true).delay.run
18 | flash[:notice] = I18n.t('controllers.test_messages_controller.notices.create')
19 | elsif employee.nil?
20 | flash[:error] = I18n.t('controllers.test_messages_controller.errors.create.employee_nil', slack_username: slack_username)
21 | else
22 | flash[:error] = I18n.t('controllers.test_messages_controller.errors.create.not_up_to_date')
23 | end
24 |
25 | redirect_to redirect_path
26 | end
27 |
28 | private
29 |
30 | def message
31 | @message ||= if params[:onboarding_message_id]
32 | OnboardingMessage.find(params[:onboarding_message_id])
33 | elsif params[:quarterly_message_id]
34 | QuarterlyMessage.find(params[:quarterly_message_id])
35 | else
36 | BroadcastMessage.find(params[:broadcast_message_id])
37 | end
38 | end
39 |
40 | def slack_username
41 | params[:test_message][:slack_username]
42 | end
43 |
44 | def slack_user_id
45 | params[:test_message][:slack_user_id]
46 | end
47 |
48 | def redirect_path
49 | if params[:onboarding_message_id]
50 | onboarding_messages_path
51 | elsif params[:quarterly_message_id]
52 | quarterly_messages_path
53 | else
54 | broadcast_messages_path
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/spec/features/create_quarterly_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Create quarterly message" do
4 | context "admin user" do
5 | scenario "can create a quarterly message", :js do
6 | create_a_quarterly_message_via_form
7 |
8 | expect(page).to have_content("Quarterly message created successfully")
9 | expect(QuarterlyMessage.count).to eq 1
10 | end
11 |
12 | scenario "can create a quarterly message that goes out to employees on the right day", :js do
13 | create_a_quarterly_message_via_form
14 | employee = create(:employee, time_zone: should_get_message_zone)
15 | Timecop.freeze(wed_april_1_nine_am_utc)
16 |
17 | QuarterlyMessageSender.new.run
18 |
19 | latest_sent = SentMessage.last
20 | expect(latest_sent.employee).to eq employee
21 | expect(latest_sent.message_body).to eq message_body
22 | end
23 |
24 | scenario "can create a quarterly message that doesn't go out to employees on the wrong day", :js do
25 | create_a_quarterly_message_via_form
26 | create(:employee, time_zone: should_get_message_zone)
27 | Timecop.freeze(wed_april_1_nine_am_utc - 7.days)
28 |
29 | QuarterlyMessageSender.new.run
30 |
31 | expect(SentMessage.count).to eq 0
32 | end
33 | end
34 |
35 |
36 | def create_a_quarterly_message_via_form
37 | admin = create(:admin)
38 | login_with_oauth(admin)
39 | visit root_path
40 | visit new_quarterly_message_path
41 | fill_in "Title", with: "Message title"
42 | fill_in "Message body", with: message_body
43 | fill_in "Tags", with: "tag_one, tag_two, tag_three"
44 | click_on "Create Quarterly message"
45 | end
46 |
47 | def message_body
48 | "Tax time is coming up!"
49 | end
50 |
51 | def wed_april_1_nine_am_utc
52 | Time.parse("2015-4-1 09:00:00 UTC")
53 | end
54 |
55 | def time_zone_from_offset(offset)
56 | ActiveSupport::TimeZone.new(offset).name
57 | end
58 |
59 | def should_get_message_zone
60 | time_zone_from_offset(+2)
61 | end
62 |
63 | end
64 |
65 |
--------------------------------------------------------------------------------
/spec/features/view_quarterly_messages_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "View quarterly messages" do
4 | scenario "sees all message details" do
5 | login_with_oauth
6 | visit root_path
7 | create_quarterly_messages
8 |
9 | visit quarterly_messages_path
10 |
11 | expect(page).to have_content(first_quarterly_message.title)
12 | expect(page).to have_content(first_quarterly_message.body)
13 | expect(page).to have_content(second_quarterly_message.title)
14 | expect(page).to have_content(second_quarterly_message.body)
15 | end
16 |
17 | scenario "sees pagination controls" do
18 | allow(Kaminari.config).to receive(:default_per_page).and_return(1)
19 |
20 | login_with_oauth
21 | visit root_path
22 | create_quarterly_messages
23 |
24 | visit quarterly_messages_path
25 |
26 | expect(page).to have_content(first_quarterly_message.title)
27 | expect(page).to have_content(first_quarterly_message.body)
28 |
29 | expect(page).not_to have_content(second_quarterly_message.title)
30 |
31 | expect(page).to have_content("Next")
32 | expect(page).to have_content("Last")
33 |
34 | click_on "Last"
35 |
36 | expect(page).not_to have_content(first_quarterly_message.title)
37 |
38 | expect(page).to have_content(second_quarterly_message.title)
39 | expect(page).to have_content(second_quarterly_message.body)
40 |
41 | expect(page).to have_content("Prev")
42 | expect(page).to have_content("First")
43 | end
44 |
45 | private
46 |
47 | def create_quarterly_messages
48 | first_quarterly_message
49 | second_quarterly_message
50 | end
51 |
52 | def first_quarterly_message
53 | @first_quarterly_message ||= create(:quarterly_message,
54 | created_at: Time.now)
55 | end
56 |
57 | def second_quarterly_message
58 | @second_quarterly_message ||= create(:quarterly_message,
59 | title: 'Quarterly message 2',
60 | created_at: 1.day.ago)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/presets/_fonts.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'dolores';
3 | src: font-url('dolores/dolores.eot');
4 | src: font-url('dolores/dolores.eot?#iefix') format('embedded-opentype'),
5 | font-url('dolores/dolores.woff') format('woff'),
6 | font-url('dolores/dolores.ttf') format('truetype'),
7 | font-url('dolores/dolores.svg#dolores') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | [class*='icon-']:before{
13 | display: inline-block;
14 | font-family: 'dolores';
15 | font-size: 1em;
16 | font-style: normal;
17 | font-weight: normal;
18 | line-height: 1;
19 | margin-right: 5px;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale
22 | }
23 |
24 | .icon-x:before {
25 | content: "\e901";
26 | }
27 | .icon-contacts:before {
28 | content: "\e902";
29 | }
30 | .icon-send:before {
31 | content: "\e903";
32 | }
33 | .icon-circle-x:before {
34 | content: "\e904";
35 | }
36 | .icon-circle-minus:before {
37 | content: "\e905";
38 | }
39 | .icon-circle-plus:before {
40 | content: "\e906";
41 | }
42 | .icon-edit:before {
43 | content: "\e900";
44 | }
45 |
46 | @font-face {
47 | font-family: 'Nimbus Sans-regular';
48 | src: font-url('18F_Nimbus/18FNimbusSansL-regular.ttf') format('truetype');
49 | }
50 |
51 | @font-face {
52 | font-family: 'Nimbus Sans-bold';
53 | src: font-url('18F_Nimbus/18FNimbusSansL-bold.ttf') format('truetype');
54 | }
55 |
56 | @font-face {
57 | font-family: 'Nimbus Sans-italic';
58 | src: font-url('18F_Nimbus/18FNimbusSansL-italic.ttf') format('truetype');
59 | }
60 |
61 | $nimbus-bold: 'Nimbus Sans-bold', $helvetica;
62 | $nimbus-italic: 'Nimbus Sans-italic', $helvetica;
63 | $nimbus-regular: 'Nimbus Sans-regular', $helvetica;
64 |
65 | @mixin font-nimbus($style) {
66 | @if $style == bold {
67 | font-family: $nimbus-bold;
68 | letter-spacing: 1px;
69 | } @else if $style == italic {
70 | font-family: $nimbus-italic;
71 | } @else {
72 | font-family: $nimbus-regular;
73 | font-weight: 200;
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/app/controllers/employees_controller.rb:
--------------------------------------------------------------------------------
1 | class EmployeesController < ApplicationController
2 | def new
3 | @employee = Employee.new
4 | @employee.time_zone = "Eastern Time (US & Canada)"
5 | end
6 |
7 | def create
8 | @employee = Employee.new(employee_params)
9 | @employee.validate_slack_username_in_org
10 | @employee.add_slack_user_id_to_employee
11 |
12 | if @employee.errors.full_messages.empty? && @employee.save
13 | flash[:notice] = I18n.t('controllers.employees_controller.notices.create', slack_username: @employee.slack_username)
14 | redirect_to employees_path
15 | else
16 | flash.now[:error] = @employee.errors.full_messages.to_sentence
17 | render action: :new
18 | end
19 | end
20 |
21 | def index
22 | if params[:slack_username].present? || params[:started_on].present?
23 | @employees = Employee.filter(params).order(slack_username: :asc).page(params[:page])
24 | else
25 | @employees = Employee.order(created_at: :desc).page(params[:page])
26 | end
27 | end
28 |
29 | def edit
30 | @employee = Employee.find(params[:id])
31 | end
32 |
33 | def update
34 | @employee = Employee.find(params[:id])
35 | @employee.slack_username = params[:employee][:slack_username]
36 | @employee.validate_slack_username_in_org
37 | @employee.add_slack_user_id_to_employee
38 |
39 |
40 | if @employee.errors.full_messages.empty? && @employee.update(employee_params)
41 | flash[:notice] = I18n.t('controllers.employees_controller.notices.update')
42 | redirect_to employees_path
43 | else
44 | flash.now[:error] = @employee.errors.full_messages.to_sentence
45 | render action: :edit
46 | end
47 | end
48 |
49 | def destroy
50 | @employee = Employee.find(params[:id])
51 | @employee.destroy
52 |
53 | flash[:notice] = I18n.t('controllers.employees_controller.notices.destroy', slack_username: @employee.slack_username)
54 | redirect_to employees_path
55 | end
56 |
57 | private
58 |
59 | def employee_params
60 | params.require(:employee).permit(:slack_username, :started_on, :time_zone)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/app/controllers/quarterly_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class QuarterlyMessagesController < ApplicationController
2 | before_action :current_user_admin, only: [:new, :create, :edit, :update]
3 |
4 | def new
5 | @quarterly_message = QuarterlyMessage.new
6 | end
7 |
8 | def create
9 | @quarterly_message = QuarterlyMessage.new(quarterly_message_params)
10 |
11 | if @quarterly_message.save
12 | flash[:notice] = I18n.t(
13 | "controllers.quarterly_messages_controller.notices.create",
14 | )
15 | redirect_to quarterly_messages_path
16 | else
17 | flash.now[:error] = I18n.t(
18 | "controllers.quarterly_messages_controller.errors.create",
19 | )
20 | render action: :new
21 | end
22 | end
23 |
24 | def index
25 | @quarterly_messages = QuarterlyMessage.
26 | filter(params).
27 | ordered_by_created_at.
28 | page(params[:page])
29 | end
30 |
31 | def edit
32 | @quarterly_message = QuarterlyMessage.find(params[:id])
33 | end
34 |
35 | def update
36 | @quarterly_message = QuarterlyMessage.find(params[:id])
37 |
38 | if @quarterly_message.update(quarterly_message_params)
39 | flash[:notice] = I18n.t(
40 | "controllers.quarterly_messages_controller.notices.update",
41 | )
42 | redirect_to quarterly_messages_path
43 | else
44 | flash.now[:error] = I18n.t(
45 | "controllers.quarterly_messages_controller.errors.update",
46 | )
47 | render action: :edit
48 | end
49 | end
50 |
51 | def destroy
52 | quarterly_message = QuarterlyMessage.find(params[:id])
53 | quarterly_message.destroy
54 |
55 | flash[:notice] = I18n.t(
56 | "controllers.quarterly_messages_controller.notices.destroy",
57 | quarterly_message_title: quarterly_message.title,
58 | )
59 | redirect_to quarterly_messages_path
60 | end
61 |
62 | private
63 |
64 | def quarterly_message_params
65 | params.
66 | require(:quarterly_message).
67 | permit(
68 | :body,
69 | :days_after_start,
70 | :end_date,
71 | :tag_list,
72 | :time_of_day,
73 | :title,
74 | :type,
75 | )
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/app/controllers/onboarding_messages_controller.rb:
--------------------------------------------------------------------------------
1 | class OnboardingMessagesController < ApplicationController
2 | before_action :current_user_admin, only: [:new, :create, :edit, :update]
3 |
4 | def new
5 | @onboarding_message = OnboardingMessage.new
6 | end
7 |
8 | def create
9 | @onboarding_message = OnboardingMessage.new(onboarding_message_params)
10 |
11 | if @onboarding_message.save
12 | flash[:notice] = I18n.t(
13 | "controllers.onboarding_messages_controller.notices.create",
14 | )
15 | redirect_to onboarding_messages_path
16 | else
17 | flash.now[:error] = I18n.t(
18 | "controllers.onboarding_messages_controller.errors.create",
19 | )
20 | render action: :new
21 | end
22 | end
23 |
24 | def index
25 | @onboarding_messages = OnboardingMessage.
26 | filter(params).
27 | date_time_ordering.
28 | page(params[:page])
29 | end
30 |
31 | def edit
32 | @onboarding_message = OnboardingMessage.find(params[:id])
33 | end
34 |
35 | def update
36 | @onboarding_message = OnboardingMessage.find(params[:id])
37 |
38 | if @onboarding_message.update(onboarding_message_params)
39 | flash[:notice] = I18n.t(
40 | "controllers.onboarding_messages_controller.notices.update",
41 | )
42 | redirect_to onboarding_messages_path
43 | else
44 | flash.now[:error] = I18n.t(
45 | "controllers.onboarding_messages_controller.errors.update",
46 | )
47 | render action: :edit
48 | end
49 | end
50 |
51 | def destroy
52 | onboarding_message = OnboardingMessage.find(params[:id])
53 | onboarding_message.destroy
54 |
55 | flash[:notice] = I18n.t(
56 | "controllers.onboarding_messages_controller.notices.destroy",
57 | onboarding_message_title: onboarding_message.title,
58 | )
59 | redirect_to onboarding_messages_path
60 | end
61 |
62 | private
63 |
64 | def onboarding_message_params
65 | params.
66 | require(:onboarding_message).
67 | permit(
68 | :body,
69 | :days_after_start,
70 | :end_date,
71 | :tag_list,
72 | :time_of_day,
73 | :title,
74 | )
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/helpers/format_time_zone_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | describe TimeZoneDisplayHelper do
4 | describe "#display_local_time_zone" do
5 | it "formats for Eastern Time (US & Canada)" do
6 | sent_message = create(:sent_message)
7 |
8 | time_display = display_local_time_zone(sent_message)
9 |
10 | sent_message.sent_at.in_time_zone(sent_message.employee.time_zone).dst? ?
11 | expected_time_display = "6:00 AM (EDT)" :
12 | expected_time_display = "5:00 AM (EST)"
13 |
14 | expect(time_display).to eq(expected_time_display)
15 | end
16 |
17 | it "formats for Central Time (US & Canada)" do
18 | employee = create(:employee, time_zone: "Central Time (US & Canada)")
19 | sent_message = create(:sent_message, employee: employee)
20 |
21 | time_display = display_local_time_zone(sent_message)
22 |
23 | sent_message.sent_at.in_time_zone(sent_message.employee.time_zone).dst? ?
24 | expected_time_display = "5:00 AM (CDT)" :
25 | expected_time_display = "4:00 AM (CST)"
26 |
27 | expect(time_display).to eq(expected_time_display)
28 | end
29 |
30 | it "formats for Mountain Time (US & Canada)" do
31 | employee = create(:employee, time_zone: "Mountain Time (US & Canada)")
32 | sent_message = create(:sent_message, employee: employee)
33 |
34 | time_display = display_local_time_zone(sent_message)
35 |
36 | sent_message.sent_at.in_time_zone(sent_message.employee.time_zone).dst? ?
37 | expected_time_display = "4:00 AM (MDT)" :
38 | expected_time_display = "3:00 AM (MST)"
39 |
40 | expect(time_display).to eq(expected_time_display)
41 | end
42 |
43 | it "formats for Pacific Time (US & Canada)" do
44 | employee = create(:employee, time_zone: "Pacific Time (US & Canada)")
45 | sent_message = create(:sent_message, employee: employee)
46 |
47 | time_display = display_local_time_zone(sent_message)
48 |
49 | sent_message.sent_at.in_time_zone(sent_message.employee.time_zone).dst? ?
50 | expected_time_display = "3:00 AM (PDT)" :
51 | expected_time_display = "2:00 AM (PST)"
52 |
53 | expect(time_display).to eq(expected_time_display)
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_script:
2 | - cp .sample.env .env
3 | - bin/rake db:setup --trace
4 | bundler_args: "--jobs=3 --retry=3 --without development"
5 | cache: bundler
6 | before_deploy:
7 | - export PATH=$HOME:$PATH
8 | - travis_retry curl -L -o $HOME/cf.tgz "https://cli.run.pivotal.io/stable?release=linux64-binary&version=6.24.0"
9 | - tar xzvf $HOME/cf.tgz -C $HOME
10 | env:
11 | global:
12 | - DB=postgresql
13 | - CF_USERNAME_PRODUCTION=95a52758-4a02-460b-9942-54d552b298b8
14 | - secure: "ceP4d1q5Kac7wpnLu9Oee1TmJEVFoICJdgsSbzA3R4y5neVO3djn1UwH7N3lScaOYgXJTocyrUpLjxx0ILWdyIwFrMo9ClgzNynd9hyOBWxKpkFD9ZW7+L7G71IPg3wHZJSxVwLix1bXoRbhoJhHcJTkg4jr5nCHVYyw1mLUL3ah/ltsHGm64ehtRzjhFIKow5iPRuvO/7+u5g8mjL+3+BzSH4VDyP1hZnpY0cWE79zRwHL6+Fgxsfp11iqy/yGDFLUCdg0yn8mx7B+ULWOrEs3CyCJXr0oZn4GCca7Ywj5V6w+yy2FuLGlDZBElp5FXFHMxCc2tzEFQ+qFDYOiAfNksb+2vk86RSrdiiypWvh/EE4sIEZs91BMgGrTNVRifQNM0HU2Is2jKqcyM1zoh+1F/9pOPx2bbyx+ro9wi+aj+EbUns80KHEzmzQ4yP4j8ZczC1iZdVl6HH82t1uZ2as8ezeG5figw+H1ojEM2mNd8WicoeGkH8/HabPDrskkksg6Q3nNxmrl97COLCrOQ6FpskF+4F6ABCxIvw5yMzgAlz7RwwAliqxw1I8/6+wa19QQ/TXotnB15lB+Dy//Yt3RWmxv67t89aTkfO7wwRs/PAgZg4q/r/trkOj5r6fW9cnZg1V7c90VWXmCdJ65yfZz5RKbcx0ccniw+a8oqCD0="
15 | - CF_USERNAME_STAGING=a31c1a7d-6929-48d4-9edf-be2a805a5c7e
16 | - secure: "dM+XWHIKL/lkm3edTGCY81hyFlTMJUNZqanGCfFLuONBdeMAwql6shRBL4DsQeDGRA9wL4BByUAObqCTtnmfFQaAu99ONC4V8j0UdvIfirNjdTX1OK7Q2ovzjxNs1EY/ui4TNzTmnGSeO4QPKFPWI+7YVgjAPkYZsY3tK6dMSk7il+GeXTN3u2D7rhT5VtYym/4AJFZmPjtGDotiuSfoA/FpFdaK2dNcZDxAmtA1g9LirWMExwd0VDfI4pGLROUX45KtugU/3ExOxQ9Esdsv1nzoMRmaXrqWZ4gWufoVErUMs5UI6g18h6bb6KNnRN36o59GttevkK8y56nshbeuTcHSMnSjSbjrX6vvfWXiDEiBcsuJAYWV1yMTbGpBZ+KkYfZm5PXSKmZwOQ1bWd7FHURA3gL4EtWBxo9qq4gBMYmbZmtTJF4dsCD9+htFXg8j11cQUE7S1X8oJJgty31uQuDCk403zIeN6TE6ByDJUyiaZ4m/DFvdcIlrHSZgPOEqY0nXdoxby8DN7r8sAgiv3WrFd9unMcU86uvVzogOZhheMv/FPGqVM5WJe3aS0o/4z47AVLOrLLcFRZCKq3/wc8evXWk6/Tv5kbssOiS0bKwV/ClKBC/eXHaGcvd/h1at1tH5g1z3a9QSjeAUpqgCJ4ChcSOpoDOvC704x45SmD8="
17 | language: ruby
18 | rvm:
19 | - ruby-2.3.3
20 | sudo: false
21 | script:
22 | - bundle exec rake
23 | deploy:
24 | - provider: script
25 | script: "./bin/deploy.sh staging"
26 | skip_cleanup: true
27 | on:
28 | branch: develop
29 | - provider: script
30 | script: "./bin/deploy.sh prod"
31 | skip_cleanup: true
32 | on:
33 | branch: master
34 |
--------------------------------------------------------------------------------
/spec/features/edit_quarterly_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Edit quarterly messages" do
4 | context "admin user" do
5 | scenario "successfully" do
6 | old_title = "Old title"
7 | new_title = "New title"
8 | tags = "tag_one, tag_two, tag_three"
9 | create(:quarterly_message, title: old_title)
10 | admin = create(:admin)
11 |
12 | login_with_oauth(admin)
13 | visit quarterly_messages_path
14 | page.find(".button-edit").click
15 | fill_in "Title", with: new_title
16 | fill_in "Tags", with: tags
17 | click_on "Update Quarterly message"
18 |
19 | expect(page).to have_content "Quarterly message updated successfully"
20 | expect(page).to have_content new_title
21 | expect(page).to have_content "tag_one"
22 | expect(page).to have_content "tag_two"
23 | expect(page).to have_content "tag_three"
24 | end
25 |
26 | scenario "unsuccessfully due to missing required fields" do
27 | old_title = "Old title"
28 | new_title = "New title"
29 | tags = ""
30 | create(:quarterly_message, title: old_title)
31 | admin = create(:admin)
32 |
33 | login_with_oauth(admin)
34 | visit quarterly_messages_path
35 | page.find(".button-edit").click
36 | fill_in "Title", with: new_title
37 | fill_in "Tags", with: tags
38 | click_on "Update Quarterly message"
39 |
40 | expect(page).to have_content "Could not update quarterly message"
41 | expect(page).to have_content "can't be blank"
42 | end
43 | end
44 |
45 | context "non admin user" do
46 | scenario "does not see link to edit an quarterly message" do
47 | user = create(:user)
48 | create(:quarterly_message)
49 | login_with_oauth(user)
50 |
51 | visit quarterly_messages_path
52 |
53 | expect(page).not_to have_selector(".button-edit")
54 | end
55 |
56 | scenario "cannot visit edit path for a quarterly message" do
57 | quarterly_message = create(:quarterly_message)
58 | user = create(:user)
59 | login_with_oauth(user)
60 |
61 | visit edit_quarterly_message_path(quarterly_message)
62 |
63 | expect(page).to have_content("You are not permitted to view that page")
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/features/edit_onboarding_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Edit onboarding messages" do
4 | context "admin user" do
5 | scenario "successfully" do
6 | old_title = "Old title"
7 | new_title = "New title"
8 | tags = "tag_one, tag_two, tag_three"
9 | create(:onboarding_message, title: old_title)
10 | admin = create(:admin)
11 |
12 | login_with_oauth(admin)
13 | visit onboarding_messages_path
14 | page.find(".button-edit").click
15 | fill_in "Title", with: new_title
16 | fill_in "Tags", with: tags
17 | click_on "Update Onboarding message"
18 |
19 | expect(page).to have_content "Onboarding message updated successfully"
20 | expect(page).to have_content new_title
21 | expect(page).to have_content "tag_one"
22 | expect(page).to have_content "tag_two"
23 | expect(page).to have_content "tag_three"
24 | end
25 |
26 | scenario "unsuccessfully due to missing required fields" do
27 | old_title = "Old title"
28 | new_title = "New title"
29 | tags = ""
30 | create(:onboarding_message, title: old_title)
31 | admin = create(:admin)
32 |
33 | login_with_oauth(admin)
34 | visit onboarding_messages_path
35 | page.find(".button-edit").click
36 | fill_in "Title", with: new_title
37 | fill_in "Tags", with: tags
38 | click_on "Update Onboarding message"
39 |
40 | expect(page).to have_content "Could not update onboarding message"
41 | expect(page).to have_content "can't be blank"
42 | end
43 | end
44 |
45 | context "non admin user" do
46 | scenario "does not see link to edit an onboarding message" do
47 | user = create(:user)
48 | create(:onboarding_message)
49 | login_with_oauth(user)
50 |
51 | visit onboarding_messages_path
52 |
53 | expect(page).not_to have_selector(".button-edit")
54 | end
55 |
56 | scenario "cannot visit edit path for a onboarding message" do
57 | onboarding_message = create(:onboarding_message)
58 | user = create(:user)
59 | login_with_oauth(user)
60 |
61 | visit edit_onboarding_message_path(onboarding_message)
62 |
63 | expect(page).to have_content("You are not permitted to view that page")
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/services/quarterly_message_employee_matcher_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 | require "business_time"
3 |
4 | describe QuarterlyMessageEmployeeMatcher do
5 | it "sends a quarterly message at/after 9am in the employees time zone to the employee" do
6 | Timecop.freeze(wed_april_1_nine_am_utc) do
7 | quarterly_message = create(:quarterly_message)
8 | _employee_before_9_am_in_their_zone = create(:employee, time_zone: shouldnt_get_message_zone)
9 | employee_after_9_am_in_their_zone = create(:employee, time_zone: should_get_message_zone)
10 |
11 | matched_employees_and_messages = QuarterlyMessageEmployeeMatcher.new(quarterly_message).run
12 |
13 | expect(matched_employees_and_messages).to match_array [employee_after_9_am_in_their_zone]
14 | end
15 | end
16 |
17 | it "matches an employee who already was sent a message last quarter" do
18 | Timecop.freeze(wed_april_1_nine_am_utc) do
19 | employee = create(:employee, time_zone: should_get_message_zone)
20 | quarterly_message = create(:quarterly_message)
21 | _last_quarters_message = create(
22 | :sent_message,
23 | message: quarterly_message,
24 | employee: employee,
25 | sent_on: 3.months.ago)
26 |
27 | matched_employees = QuarterlyMessageEmployeeMatcher.new(quarterly_message).run
28 |
29 | expect(matched_employees).to match_array [employee]
30 | end
31 | end
32 |
33 | it "does not match an employee who already was sent a message this quarter" do
34 | Timecop.freeze(wed_april_1_nine_am_utc) do
35 | employee = create(:employee, time_zone: should_get_message_zone)
36 | quarterly_message = create(:quarterly_message)
37 | _this_quarters_message = create(
38 | :sent_message,
39 | message: quarterly_message,
40 | employee: employee,
41 | sent_on: 3.days.ago
42 | )
43 |
44 | matched_employees = QuarterlyMessageEmployeeMatcher.new(quarterly_message).run
45 |
46 | expect(matched_employees).to be_empty
47 | end
48 | end
49 |
50 | private
51 |
52 | def wed_april_1_nine_am_utc
53 | Time.parse("2015-4-1 09:00:00 UTC")
54 | end
55 |
56 | def should_get_message_zone
57 | time_zone_from_offset(+2)
58 | end
59 |
60 | def shouldnt_get_message_zone
61 | time_zone_from_offset(-2)
62 | end
63 |
64 | def time_zone_from_offset(offset)
65 | ActiveSupport::TimeZone.new(offset).name
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/spec/support/fixtures/users_list.json:
--------------------------------------------------------------------------------
1 | {
2 | "ok": true,
3 | "members": [
4 | {
5 | "id": "123ABC_ID",
6 | "name": "testusername",
7 | "deleted": false,
8 | "color": "9f69e7",
9 | "profile": {
10 | "first_name": "Bobby",
11 | "last_name": "Tables",
12 | "real_name": "Bobby Tables",
13 | "email": "bobby@example.com"
14 | },
15 | "is_admin": true,
16 | "is_owner": true,
17 | "is_bot": false,
18 | "has_2fa": false,
19 | "has_files": true
20 | },
21 | {
22 | "id": "456DEF_ID",
23 | "name": "testusername2",
24 | "deleted": false,
25 | "color": "9f69e7",
26 | "profile": {
27 | "first_name": "Joe",
28 | "last_name": "Smith",
29 | "real_name": "Joe Smith",
30 | "email": "joe@example.com"
31 | },
32 | "is_admin": true,
33 | "is_owner": true,
34 | "is_bot": false,
35 | "has_2fa": false,
36 | "has_files": true
37 | },
38 | {
39 | "id": "789GHI_ID",
40 | "name": "testusername3",
41 | "deleted": false,
42 | "color": "9f69e7",
43 | "profile": {
44 | "first_name": "Jane",
45 | "last_name": "Smith",
46 | "real_name": "Jane Smith",
47 | "email": "jane@example.com"
48 | },
49 | "is_admin": true,
50 | "is_owner": true,
51 | "is_bot": false,
52 | "has_2fa": false,
53 | "has_files": true
54 | },
55 | {
56 | "id": "987FED_ID",
57 | "name": "u2",
58 | "deleted": false,
59 | "color": "9f69e7",
60 | "profile": {
61 | "first_name": "Lily",
62 | "last_name": "Jones",
63 | "real_name": "Lily Jones",
64 | "email": "lily@example.com"
65 | },
66 | "is_admin": true,
67 | "is_owner": true,
68 | "is_bot": false,
69 | "has_2fa": false,
70 | "has_files": true
71 | },
72 | {
73 | "id": "789XYZ_ID",
74 | "name": "bot",
75 | "deleted": false,
76 | "color": "9f69e7",
77 | "profile": {
78 | "bot_id": "B1SSC50SD",
79 | "api_app_id": ""
80 | },
81 | "is_admin": true,
82 | "is_owner": true,
83 | "is_bot": true,
84 | "has_2fa": false,
85 | "has_files": true
86 | }
87 | ]
88 | }
89 |
--------------------------------------------------------------------------------
/app/services/message_sender.rb:
--------------------------------------------------------------------------------
1 | require "slack-ruby-client"
2 |
3 | class MessageSender
4 | def initialize(employee, message, test_message: false)
5 | @employee = employee
6 | @message = message
7 | @test_message = test_message
8 | end
9 |
10 | def run
11 | slack_user_id = EmployeeFinder.new(employee.slack_username).slack_user_id
12 |
13 | if employee.slack_user_id.nil? && slack_user_id.present?
14 | employee.slack_user_id = slack_user_id
15 | employee.save
16 | end
17 |
18 | if employee.slack_channel_id.nil?
19 | channel_id = SlackChannelIdFinder.new(employee.slack_user_id, client).run
20 | employee.slack_channel_id = channel_id
21 | employee.save
22 | end
23 |
24 | if employee.slack_channel_id.present?
25 | begin
26 | post_message(channel_id: employee.slack_channel_id, message: message)
27 | create_sent_message(
28 | employee: employee,
29 | message: message,
30 | error: nil,
31 | )
32 | rescue Slack::Web::Api::Error => error
33 | create_sent_message(
34 | employee: employee,
35 | message: message,
36 | error: error,
37 | )
38 | end
39 | else
40 | create_sent_message(
41 | employee: employee,
42 | message: message,
43 | error: StandardError.new("Was unable to find a slack channel for user with name #{employee.slack_username} and slack user id #{employee.slack_user_id}"),
44 | )
45 | end
46 |
47 | end
48 |
49 | private
50 |
51 | attr_reader :employee, :message, :test_message
52 |
53 | def client
54 | @client ||= Slack::Web::Client.new
55 | end
56 |
57 | def post_message(options)
58 | client.chat_postMessage(
59 | channel: options[:channel_id],
60 | as_user: true,
61 | text: options[:message].body,
62 | )
63 | end
64 |
65 | def create_sent_message(options)
66 | if !test_message
67 | SentMessage.create(
68 | employee: options[:employee],
69 | message: options[:message],
70 | sent_on: Date.current,
71 | sent_at: Time.current,
72 | error_message: error_message(options[:error]),
73 | message_body: formatted_message(options),
74 | )
75 | end
76 | end
77 |
78 | def error_message(error)
79 | if error
80 | error.message
81 | else
82 | ""
83 | end
84 | end
85 |
86 | def formatted_message(options)
87 | MessageFormatter.new(options[:message]).escape_slack_characters
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/components/_sidenav-list.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Adapted from US Web Design Standards
3 | https://playbook.cio.gov/designstandards
4 | */
5 |
6 | $sidenav-padding: 1rem 1rem 1rem 1.8rem;
7 |
8 | .sidenav-list {
9 | margin-bottom: $base-padding-extra;
10 | }
11 |
12 | .sidenav-list a {
13 | @include font-nimbus('regular');
14 |
15 | border: none;
16 | color: $dark-18f;
17 | display: block;
18 | line-height: 1;
19 | padding: $sidenav-padding;
20 |
21 | }
22 |
23 | .sidenav-list-item {
24 | border-top: 1px solid $dark-18f;
25 |
26 | &:first-child {
27 | border-top: none;
28 | }
29 |
30 | }
31 |
32 | .sidenav-sub_list {
33 | margin: 0;
34 | width: 100%;
35 |
36 | li {
37 | border: none;
38 | margin-left: 1.8rem;
39 | }
40 |
41 | a:hover,
42 | a.current {
43 | border: none;
44 | }
45 | }
46 |
47 | .sidenav-sub_list {
48 | margin-bottom: $base-padding;
49 | }
50 |
51 | .sidenav-sub_list li {
52 | &:focus,
53 | &:hover {
54 | @include font-nimbus('bold');
55 |
56 | background-color: transparent;
57 | border-left: 4px solid $dark-18f;
58 | color: $dark-18f;
59 | outline: 1px;
60 | }
61 | }
62 |
63 | .sidenav-sub_list li a {
64 | padding-left: 1.8rem;
65 | }
66 |
67 | .sidenav-sub_list li.current a {
68 | @include font-nimbus('bold');
69 | }
70 |
71 | .sidenav-list_title {
72 | @include font-nimbus('regular');
73 |
74 | border-radius: 0;
75 | letter-spacing: 1px;
76 | padding: $sidenav-padding;
77 | text-align: left;
78 | width: 100%;
79 |
80 | &[aria-expanded='true'],
81 | &:focus,
82 | &:hover {
83 | @include font-nimbus('bold');
84 |
85 | background-color: transparent;
86 | border-left: 4px solid $dark-18f;
87 | color: $dark-18f;
88 | outline: 1px;
89 | }
90 | }
91 |
92 | .sidenav-list_title .icon-circle-plus,
93 | .sidenav-list_title .icon-circle-minus {
94 | float: right;
95 | color: transparent;
96 | outline: 1px;
97 | }
98 |
99 | .sidenav-list_title {
100 | &:hover,
101 | &:focus {
102 | .icon-circle-plus,
103 | .icon-circle-minus {
104 | color: $dark-18f;
105 | }
106 | }
107 | }
108 |
109 | .sidenav-list_title[aria-expanded=true] .icon-circle-plus {
110 | display: none;
111 | }
112 |
113 | .sidenav-list_title[aria-expanded=true] .icon-circle-minus {
114 | display: block;
115 | color: $dark-18f;
116 | }
117 |
118 | .sidenav-list_title[aria-expanded=false] .icon-circle-plus {
119 | display: block;
120 | }
121 |
122 | .sidenav-list_title[aria-expanded=false] .icon-circle-minus {
123 | display: none;
124 | }
125 |
--------------------------------------------------------------------------------
/db/chores/migrate_messages_to_scheduled_messages.rb:
--------------------------------------------------------------------------------
1 | require_relative "migration_helper_methods"
2 |
3 | class MigrateMessagesToScheduledMessages
4 | include MigrationHelperMethods
5 |
6 | def perform
7 | execute("SELECT * FROM onboarding_messages").each do |row|
8 | migrate_onboarding_message_to_scheduled_message(row)
9 | end
10 | execute("SELECT * FROM quarterly_messages").each do |row|
11 | migrate_quarterly_message_to_scheduled_message(row)
12 | end
13 | end
14 |
15 | private
16 |
17 | SCHEDULED_MESSAGE_COLUMNS = [
18 | "created_at",
19 | "updated_at",
20 | "title",
21 | "body",
22 | "days_after_start",
23 | "time_of_day",
24 | "type",
25 | "end_date",
26 | "deleted_at",
27 | ].freeze
28 |
29 | def migrate_onboarding_message_to_scheduled_message(row)
30 | # Migrate to new table
31 | row["type"] = 0
32 | values = SCHEDULED_MESSAGE_COLUMNS.map do |column|
33 | sanitize(row[column])
34 | end
35 | scheduled_message_id = insert(
36 | <<-SQL
37 | INSERT INTO scheduled_messages (#{SCHEDULED_MESSAGE_COLUMNS.join(',')})
38 | VALUES (#{values.join(',')})
39 | SQL
40 | )
41 |
42 | # Update other tables
43 | migrate_sent_messages(row["id"], "OnboardingMessage", scheduled_message_id)
44 | migrate_taggings(row["id"], "OnboardingMessage", scheduled_message_id)
45 | end
46 |
47 | def migrate_quarterly_message_to_scheduled_message(row)
48 | # Migrate to new table
49 | row["type"] = 1
50 | row["time_of_day"] = "2000-01-01 12:00:00"
51 | values = SCHEDULED_MESSAGE_COLUMNS.map do |column|
52 | sanitize(row[column])
53 | end
54 | quarterly_message_id = insert(
55 | <<-SQL
56 | INSERT INTO scheduled_messages (#{SCHEDULED_MESSAGE_COLUMNS.join(',')})
57 | VALUES (#{values.join(',')})
58 | SQL
59 | )
60 |
61 | # Update other tables
62 | migrate_sent_messages(row["id"], "QuarterlyMessage", quarterly_message_id)
63 | migrate_taggings(row["id"], "QuarterlyMessage", quarterly_message_id)
64 | end
65 |
66 | def migrate_sent_messages(old_id, old_type, new_id)
67 | execute(
68 | <<-SQL
69 | UPDATE sent_messages
70 | SET message_id = #{new_id}, message_type = NULL
71 | WHERE message_id = #{old_id} AND message_type = '#{old_type}'
72 | SQL
73 | )
74 | end
75 |
76 | def migrate_taggings(old_id, old_type, new_id)
77 | execute(
78 | <<-SQL
79 | UPDATE taggings
80 | SET taggable_id = #{new_id}, taggable_type = 'ScheduledMessage'
81 | WHERE taggable_id = #{old_id} AND taggable_type = '#{old_type}'
82 | SQL
83 | )
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/db/chores/migrate_scheduled_messages_to_messages.rb:
--------------------------------------------------------------------------------
1 | require_relative "migration_helper_methods"
2 |
3 | class MigrateScheduledMessagesToMessages
4 | include MigrationHelperMethods
5 |
6 | def perform
7 | execute("SELECT * FROM scheduled_messages").each do |row|
8 | if row["type"] == 0
9 | migrate_scheduled_message_to_onboarding_message(row)
10 | elsif row["type"] == 1
11 | migrate_scheduled_message_to_quarterly_message(row)
12 | end
13 | end
14 | end
15 |
16 | private
17 |
18 | ONBOARDING_MESSAGE_COLUMNS = [
19 | "created_at",
20 | "updated_at",
21 | "title",
22 | "body",
23 | "days_after_start",
24 | "time_of_day",
25 | "end_date",
26 | "deleted_at",
27 | ].freeze
28 |
29 | QUARTERLY_MESSAGE_COLUMNS = [
30 | "created_at",
31 | "updated_at",
32 | "title",
33 | "body",
34 | "deleted_at",
35 | ].freeze
36 |
37 | def migrate_scheduled_message_to_onboarding_message(row)
38 | # Migrate to new table
39 | values = ONBOARDING_MESSAGE_COLUMNS.map do |column|
40 | sanitize(row[column])
41 | end
42 | onboarding_message_id = insert(
43 | <<-SQL
44 | INSERT INTO onboarding_messages (
45 | #{ONBOARDING_MESSAGE_COLUMNS.join(',')}
46 | )
47 | VALUES (#{values.join(',')})
48 | SQL
49 | )
50 |
51 | # Update other tables
52 | migrate_sent_messages(row["id"], onboarding_message_id, "OnboardingMessage")
53 | migrate_taggings(row["id"], onboarding_message_id, "OnboardingMessage")
54 | end
55 |
56 | def migrate_scheduled_message_to_quarterly_message(row)
57 | # Migrate to new table
58 | values = QUARTERLY_MESSAGE_COLUMNS.map do |column|
59 | sanitize(row[column])
60 | end
61 | quarterly_message_id = insert(
62 | <<-SQL
63 | INSERT INTO quarterly_messages (#{QUARTERLY_MESSAGE_COLUMNS.join(',')})
64 | VALUES (#{values.join(',')})
65 | SQL
66 | )
67 |
68 | # Update other tables
69 | migrate_sent_messages(row["id"], quarterly_message_id, "QuarterlyMessage")
70 | migrate_taggings(row["id"], quarterly_message_id, "QuarterlyMessage")
71 | end
72 |
73 | def migrate_sent_messages(old_id, new_id, new_type)
74 | execute(
75 | <<-SQL
76 | UPDATE sent_messages
77 | SET message_id = #{new_id}, message_type = '#{new_type}'
78 | WHERE message_id = #{old_id} AND message_type IS NULL
79 | SQL
80 | )
81 | end
82 |
83 | def migrate_taggings(old_id, new_id, new_type)
84 | execute(
85 | <<-SQL
86 | UPDATE taggings
87 | SET taggable_id = #{new_id}, taggable_type = '#{new_type}'
88 | WHERE taggable_id = #{old_id} AND taggable_type = 'ScheduledMessage'
89 | SQL
90 | )
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/spec/features/send_test_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Send test message" do
4 | scenario "onboarding message sends successfully" do
5 | create_onboarding_message
6 | create_employee
7 | login_with_oauth create(:admin)
8 | visit onboarding_messages_path
9 |
10 | page.find(".button-test").click
11 | fill_in "Slack username", with: username_from_fixture
12 | click_on "Send test"
13 |
14 | expect(page).to have_content("Test message sent")
15 | end
16 |
17 | scenario "quarterly message sends successfully" do
18 | create_quarterly_message
19 | create_employee
20 | login_with_oauth create(:admin)
21 | visit quarterly_messages_path
22 |
23 | page.find(".button-test").click
24 | fill_in "Slack username", with: username_from_fixture
25 | click_on "Send test"
26 |
27 | expect(page).to have_content("Test message sent")
28 | end
29 |
30 | scenario "broadcast message sends successfully" do
31 | create_broadcast_message
32 | create_employee
33 | login_with_oauth create(:admin)
34 | visit broadcast_messages_path
35 |
36 | page.find(".button-test").click
37 | fill_in "Slack username", with: username_from_fixture
38 | click_on "Send test"
39 |
40 | expect(page).to have_content("Test message sent")
41 | end
42 |
43 | scenario "attempt to send test to Slack username that does not exist" do
44 | create_broadcast_message
45 | login_with_oauth create(:admin)
46 | visit broadcast_messages_path
47 |
48 | page.find(".button-test").click
49 | fill_in "Slack username", with: "notreal"
50 | click_on "Send test"
51 |
52 | expect(page).to have_content("isn't in the Dolores system")
53 | end
54 |
55 | scenario "attempt to send test to employee missing Slack info" do
56 | create_broadcast_message
57 | create(
58 | :employee,
59 | slack_username: username_from_fixture,
60 | slack_channel_id: nil,
61 | slack_user_id: nil,
62 | )
63 |
64 | login_with_oauth create(:admin)
65 | visit broadcast_messages_path
66 |
67 | page.find(".button-test").click
68 | fill_in "Slack username", with: username_from_fixture
69 | click_on "Send test"
70 |
71 | expect(page).to have_content("isn't up to date")
72 | end
73 |
74 | private
75 |
76 | def create_broadcast_message
77 | @broadcast_message ||= create(:broadcast_message)
78 | end
79 |
80 | def create_employee
81 | @employee ||= create(:employee, slack_username: username_from_fixture)
82 | end
83 |
84 | def create_quarterly_message
85 | @quarterly_message ||= create(:quarterly_message)
86 | end
87 |
88 | def create_onboarding_message
89 | @onboarding_message ||= create(:onboarding_message)
90 | end
91 |
92 | def username_from_fixture
93 | "testusername"
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | date:
3 | formats:
4 | default:
5 | "%m/%d/%Y"
6 | with_weekday:
7 | "%a %m/%d/%y"
8 |
9 | time:
10 | formats:
11 | default:
12 | "%a, %b %-d, %Y at %r"
13 | date:
14 | "%b %-d, %Y"
15 | short:
16 | "%B %d"
17 |
18 | titles:
19 | application: Dolores-landingham-bot
20 | employees:
21 | errors:
22 | slack_username_format:
23 | 'Slack usernames can only contain lowercase letters, numbers,
24 | underscores, hyphens, and periods.'
25 | slack_username_in_org:
26 | "There is not a Slack user with the username \"%{slack_username}\" in
27 | your organization."
28 |
29 | controllers:
30 | application_controller:
31 | errors:
32 | current_user_admin:
33 | "You are not permitted to view that page"
34 | authenticate_user:
35 | "You need to sign in for access to this page"
36 | auth_controller:
37 | successes:
38 | oauth_callback: "You successfully signed in"
39 | broadcast_messages_controller:
40 | notices:
41 | create: "Broadcast message created successfully"
42 | employees_controller:
43 | notices:
44 | create: "Thanks for adding %{slack_username}"
45 | update: "Employee updated successfully"
46 | destroy: "You deleted %{slack_username}"
47 | quarterly_messages_controller:
48 | notices:
49 | create: "Quarterly message created successfully"
50 | update: "Quarterly message updated successfully"
51 | destroy: "You deleted %{quarterly_message_title}"
52 | errors:
53 | create: "Could not create quarterly message"
54 | update: "Could not update quarterly message"
55 | onboarding_messages_controller:
56 | notices:
57 | create: "Onboarding message created successfully"
58 | update: "Onboarding message updated successfully"
59 | destroy: "You deleted %{onboarding_message_title}"
60 | errors:
61 | create: "Could not create onboarding message"
62 | update: "Could not update onboarding message"
63 | send_broadcast_messages_controller:
64 | notices:
65 | create: "Broadcast message sent to all users"
66 | test_messages_controller:
67 | notices:
68 | create: "Test message sent successfully"
69 | errors:
70 | create:
71 | employee_nil:
72 | "Oops! Looks like that employee isn't in the Dolores system yet! Make sure you've entered the Slack handle (%{slack_username})
73 | for the employee before sending him/her/them a test message."
74 | not_up_to_date:
75 | "Oops! Looks like this employees information isn't up to date. Check to make sure they haven't changed their slackname!"
76 | users_controller:
77 | notices:
78 | update: "User updated successfully"
79 |
--------------------------------------------------------------------------------
/spec/features/create_onboarding_message_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | feature "Create onboarding message" do
4 | context "admin user" do
5 | scenario "can create a onboarding message" do
6 | admin = create(:admin)
7 | login_with_oauth(admin)
8 | visit root_path
9 | visit new_onboarding_message_path
10 |
11 | fill_in "Title", with: "Message title"
12 | fill_in "Message body", with: "Message body"
13 | fill_in "Business days after employee starts to send message", with: 1
14 | select "11 AM", from: "onboarding_message_time_of_day_4i"
15 | select "45", from: "onboarding_message_time_of_day_5i"
16 | fill_in "Tags", with: "tag_one, tag_two, tag_three"
17 | click_on "Create Onboarding message"
18 |
19 | expect(page).to have_content("Onboarding message created successfully")
20 | end
21 |
22 | scenario "can create a onboarding message with optional end date" do
23 | admin = create(:admin)
24 | login_with_oauth(admin)
25 | visit root_path
26 | visit new_onboarding_message_path
27 |
28 | fill_in "Title", with: "Message title"
29 | fill_in "Message body", with: "Message body"
30 | fill_in "Business days after employee starts to send message", with: 1
31 | select Date.today.year, from: "onboarding_message_end_date_1i"
32 | select Date::MONTHNAMES[Date.today.month], from: "onboarding_message_end_date_2i"
33 | select Date.today.day, from: "onboarding_message_end_date_3i"
34 | select "11 AM", from: "onboarding_message_time_of_day_4i"
35 | select "45", from: "onboarding_message_time_of_day_5i"
36 | fill_in "Tags", with: "tag_one, tag_two, tag_three"
37 | click_on "Create Onboarding message"
38 |
39 | expect(page).to have_content("Onboarding message created successfully")
40 | end
41 |
42 | scenario "unsuccessfully due to missing required fields" do
43 | admin = create(:admin)
44 | login_with_oauth(admin)
45 | visit root_path
46 | visit new_onboarding_message_path
47 |
48 | fill_in "Title", with: "Message title"
49 | fill_in "Business days after employee starts to send message", with: 1
50 | click_on "Create Onboarding message"
51 |
52 | expect(page).to have_content("Could not create onboarding message")
53 | expect(page).to have_content("can't be blank")
54 | end
55 | end
56 |
57 | context "non admin user" do
58 | scenario "does not see link to create a onboarding message" do
59 | user = create(:user)
60 | login_with_oauth(user)
61 |
62 | visit root_path
63 |
64 | expect(page).not_to have_link("Create", href: new_onboarding_message_path)
65 | end
66 |
67 | scenario "cannot view onboarding message form" do
68 | user = create(:user)
69 | login_with_oauth(user)
70 |
71 | visit new_onboarding_message_path
72 |
73 | expect(page).to have_content("You are not permitted to view that page")
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------