├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .cfignore ├── .ruby-version ├── app ├── assets │ ├── stylesheets │ │ ├── elements │ │ │ ├── _forms.scss │ │ │ ├── _body.scss │ │ │ ├── _tables.scss │ │ │ ├── _links.scss │ │ │ ├── _lists.scss │ │ │ └── _headings.scss │ │ ├── _layouts.scss │ │ ├── _scheduled_messages.scss │ │ ├── _core.scss │ │ ├── _presets.scss │ │ ├── components │ │ │ ├── _tables.scss │ │ │ ├── _alert.scss │ │ │ ├── _simple_form.scss │ │ │ ├── _flashes.scss │ │ │ ├── _fixedsticky.scss │ │ │ ├── _banner.scss │ │ │ ├── _buttons.scss │ │ │ ├── _filter.scss │ │ │ └── _sidenav-list.scss │ │ ├── _elements.scss │ │ ├── presets │ │ │ ├── _utils.scss │ │ │ ├── _colors.scss │ │ │ ├── _variables.scss │ │ │ └── _fonts.scss │ │ ├── _components.scss │ │ ├── layout │ │ │ └── _containers.scss │ │ ├── base │ │ │ ├── _base.scss │ │ │ ├── _grid-settings.scss │ │ │ ├── _tables.scss │ │ │ ├── _lists.scss │ │ │ ├── _buttons.scss │ │ │ ├── _variables.scss │ │ │ ├── _typography.scss │ │ │ └── _forms.scss │ │ └── application.scss │ ├── images │ │ ├── dolores.jpg │ │ ├── logo-18f.png │ │ └── 18F_Logo │ │ │ ├── PNG │ │ │ ├── 18F-Logo-L.png │ │ │ ├── 18F-Logo-M.png │ │ │ ├── 18F-Logo-S.png │ │ │ ├── 18F-Logo-Bright-L.png │ │ │ ├── 18F-Logo-Bright-M.png │ │ │ └── 18F-Logo-Bright-S.png │ │ │ ├── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ └── code.txt │ │ │ └── SVG │ │ │ ├── 18F-Logo-2016-Black.svg │ │ │ └── 18F-Logo-2016-Blue.svg │ ├── javascripts │ │ ├── application.js │ │ ├── jquery-elastic.js │ │ ├── sticky.js │ │ └── flashes.js │ └── fonts │ │ ├── dolores │ │ ├── dolores.eot │ │ ├── dolores.ttf │ │ └── dolores.woff │ │ └── 18F_Nimbus │ │ ├── 18FNimbusSansL-bold.ttf │ │ ├── 18FNimbusSansL-italic.ttf │ │ └── 18FNimbusSansL-regular.ttf ├── models │ ├── user.rb │ ├── application_record.rb │ ├── broadcast_message.rb │ ├── quarterly_message.rb │ ├── onboarding_message.rb │ ├── sent_message.rb │ └── employee.rb ├── helpers │ ├── flashes_helper.rb │ ├── time_zone_display_helper.rb │ └── nav_link_helper.rb ├── services │ ├── employee_updater.rb │ ├── slack_channel_id_finder.rb │ ├── next_working_day_finder.rb │ ├── slack_user_finder.rb │ ├── broadcast_message_sender.rb │ ├── message_formatter.rb │ ├── quarterly_message_sender.rb │ ├── onboarding_message_sender.rb │ ├── slack_username_updater.rb │ ├── slack_api_wrapper.rb │ ├── employee_finder.rb │ ├── quarterly_message_day_verifier.rb │ ├── quarterly_message_employee_matcher.rb │ ├── onboarding_message_employee_matcher.rb │ └── message_sender.rb ├── views │ ├── employees │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _form.html.erb │ │ └── index.html.erb │ ├── onboarding_messages │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── index.html.erb │ │ ├── _onboarding_message.html.erb │ │ └── _form.html.erb │ ├── application │ │ ├── _flashes.html.erb │ │ ├── _javascript.html.erb │ │ └── _header.html.erb │ ├── quarterly_messages │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _disabled_alert.html.erb │ │ ├── _form.html.erb │ │ ├── _quarterly_message.html.erb │ │ └── index.html.erb │ ├── sessions │ │ └── new.html.erb │ ├── users │ │ ├── edit.html.erb │ │ └── index.html.erb │ ├── broadcast_messages │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ └── _broadcast_message.html.erb │ ├── layouts │ │ ├── _admin_sidenav_items.html.erb │ │ └── application.html.erb │ ├── test_messages │ │ └── new.html.erb │ ├── auth │ │ └── forbidden.html.erb │ └── sent_messages │ │ └── index.html.erb └── controllers │ ├── sent_messages_controller.rb │ ├── sessions_controller.rb │ ├── send_broadcast_messages_controller.rb │ ├── users_controller.rb │ ├── auth_controller.rb │ ├── broadcast_messages_controller.rb │ ├── application_controller.rb │ ├── test_messages_controller.rb │ ├── employees_controller.rb │ ├── quarterly_messages_controller.rb │ └── onboarding_messages_controller.rb ├── .rspec ├── config ├── initializers │ ├── assets.rb │ ├── json_encoding.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── slack.rb │ ├── wrap_parameters.rb │ ├── clockwork_config.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── application_controller_renderer.rb │ ├── kaminari_config.rb │ ├── omniauth.rb │ ├── business_time.rb │ ├── backtrace_silencers.rb │ ├── errors.rb │ ├── inflections.rb │ ├── simple_form.rb │ └── new_framework_defaults.rb ├── environment.rb ├── spring.rb ├── boot.rb ├── cable.yml ├── secrets.yml ├── locales │ ├── simple_form.en.yml │ └── en.yml ├── puma.rb ├── business_time.yml ├── clock.rb ├── database.yml ├── application.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb └── routes.rb ├── spec ├── support │ ├── fixtures │ │ ├── message_post_failure.json │ │ ├── im_open.json │ │ ├── message_posted.json │ │ └── users_list.json │ ├── factory_girl.rb │ ├── time_helper.rb │ ├── database_cleaner.rb │ ├── slack_api_helper.rb │ ├── fake_slack_api.rb │ └── oauth_helper.rb ├── models │ ├── broadcast_message_spec.rb │ ├── quarterly_message_spec.rb │ ├── sent_message_spec.rb │ ├── employee_spec.rb │ └── onboarding_message_spec.rb ├── features │ ├── visit_root_path_spec.rb │ ├── create_broadcast_message_spec.rb │ ├── edit_users_spec.rb │ ├── send_broadcast_message_spec.rb │ ├── delete_quarterly_message_spec.rb │ ├── delete_onboarding_message_spec.rb │ ├── view_employees_spec.rb │ ├── delete_employees_spec.rb │ ├── view_sent_messages_spec.rb │ ├── edit_employees_spec.rb │ ├── sign_in_user_spec.rb │ ├── send_onboarding_message_after_test_message_spec.rb │ ├── create_employees_spec.rb │ ├── send_quarterly_messages_across_all_time_zones_spec.rb │ ├── create_quarterly_message_spec.rb │ ├── view_quarterly_messages_spec.rb │ ├── edit_quarterly_message_spec.rb │ ├── edit_onboarding_message_spec.rb │ ├── send_test_message_spec.rb │ └── create_onboarding_message_spec.rb ├── controllers │ ├── send_messages_controller_spec.rb │ └── broadcast_messages_controller_spec.rb ├── services │ ├── slack_username_updater_spec.rb │ ├── slack_channel_id_finder_spec.rb │ ├── slack_user_finder_spec.rb │ ├── message_formatter_spec.rb │ ├── quarterly_message_sender_spec.rb │ ├── next_working_day_finder_spec.rb │ ├── broadcast_message_sender_spec.rb │ ├── employee_finder_spec.rb │ ├── onboarding_message_sender_spec.rb │ ├── quarterly_message_day_verifier_spec.rb │ └── quarterly_message_employee_matcher_spec.rb ├── spec_helper.rb ├── requests │ └── auth_spec.rb ├── factories.rb ├── rails_helper.rb └── helpers │ └── format_time_zone_spec.rb ├── bin ├── rake ├── bundle ├── rails ├── rspec ├── delayed_job ├── deploy.sh └── setup ├── script ├── start └── cssh ├── Procfile ├── PULL_REQUEST_TEMPLATE.md ├── config.ru ├── db ├── migrate │ ├── 20160313182803_add_slack_channel_id_to_employees.rb │ ├── 20160402214945_add_end_date_to_scheduled_messages.rb │ ├── 20160919185207_rename_messages_to_broadcast_messages.rb │ ├── 20160627172503_rename_message_time_frame_to_type.rb │ ├── 20160617101033_add_type_to_scheduled_messages.rb │ ├── 20151021183449_add_time_zone_to_employees.rb │ ├── 20160623213452_remove_null_constraint_on_days_after_start.rb │ ├── 20160430175239_add_slack_user_id_to_employee.rb │ ├── 20151021194020_add_time_of_day_to_scheduled_messages.rb │ ├── 20151021202217_add_sent_at_to_sent_scheduled_messages.rb │ ├── 20160122233938_create_users.rb │ ├── 20150917233323_create_employees.rb │ ├── 20160718213232_create_messages.rb │ ├── 20150918175357_create_scheduled_messages.rb │ ├── 20151006161913_add_missing_taggable_index.acts_as_taggable_on_engine.rb │ ├── 20170310195553_remove_sent_message_employee_uniqueness_constraint.rb │ ├── 20151006161914_change_collation_for_tag_names.acts_as_taggable_on_engine.rb │ ├── 20160121174450_add_deleted_at_to_employees_scheduled_messages_and_sent_scheduled_messages.rb │ ├── 20151006161912_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb │ ├── 20150923224816_create_sent_scheduled_messages.rb │ ├── 20150923224817_create_delayed_jobs.rb │ ├── 20151006161911_add_missing_unique_indices.acts_as_taggable_on_engine.rb │ └── 20151006161910_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb ├── chores │ ├── migration_helper_methods.rb │ ├── migrate_messages_to_scheduled_messages.rb │ └── migrate_scheduled_messages_to_messages.rb └── seeds.rb ├── .codeclimate.yml ├── .sample.env ├── Rakefile ├── lib └── tasks │ ├── bundler_audit.rake │ ├── dev.rake │ └── on_first_instance.rake ├── .gitignore ├── manifest.yml ├── manifest_staging.yml ├── Vagrantfile ├── LICENSE.md ├── Gemfile └── .travis.yml /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.8 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/elements/_forms.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_layouts.scss: -------------------------------------------------------------------------------- 1 | @import "layout/containers"; 2 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.assets.version = "1.0" 2 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | require_relative "application" 2 | Rails.application.initialize! 3 | -------------------------------------------------------------------------------- /config/initializers/json_encoding.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport::JSON::Encoding.time_precision = 0 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_scheduled_messages.scss: -------------------------------------------------------------------------------- 1 | .quarterly-message-fields { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /spec/support/fixtures/message_post_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": false, 3 | "error": "not_authed" 4 | } 5 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.action_dispatch.cookies_serializer = :json 2 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.filter_parameters += [:password] 2 | -------------------------------------------------------------------------------- /config/initializers/slack.rb: -------------------------------------------------------------------------------- 1 | Slack.configure do |config| 2 | config.token = ENV["SLACK_API_TOKEN"] 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryGirl::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/elements/_body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | @include font-nimbus('regular'); 3 | 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/fixtures/im_open.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "channel": { 4 | "id": "123ABC_IM_ID" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/images/dolores.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/dolores.jpg -------------------------------------------------------------------------------- /app/assets/images/logo-18f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/logo-18f.png -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | ActiveSupport.on_load(:action_controller) do 2 | wrap_parameters format: [:json] 3 | end 4 | -------------------------------------------------------------------------------- /script/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -x 5 | 6 | bin/rake cf:on_first_instance db:migrate 7 | foreman start -p $PORT 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require elastic 4 | //= require_tree . 5 | -------------------------------------------------------------------------------- /config/initializers/clockwork_config.rb: -------------------------------------------------------------------------------- 1 | module Clockwork 2 | configure do |config| 3 | config[:tz] = "UTC" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/fonts/dolores/dolores.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/dolores/dolores.eot -------------------------------------------------------------------------------- /app/assets/fonts/dolores/dolores.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/dolores/dolores.ttf -------------------------------------------------------------------------------- /app/assets/fonts/dolores/dolores.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/dolores/dolores.woff -------------------------------------------------------------------------------- /app/assets/stylesheets/elements/_tables.scss: -------------------------------------------------------------------------------- 1 | td, 2 | th { 3 | padding-left: 5px; 4 | padding-right: 5px; 5 | vertical-align: top; 6 | } 7 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -p $PORT -C ./config/puma.rb 2 | worker: bundle exec rake jobs:work 3 | clock: bundle exec clockwork config/clock.rb 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/elements/_links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: $dark-18f; 3 | 4 | &:hover, 5 | &:focus { 6 | color: $medium-18f; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes issue(s) # . 2 | 3 | Changes proposed in this pull request: 4 | 5 | - 6 | - 7 | - 8 | 9 | /cc relevant people 10 | -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-L.png -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-M.png -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-S.png -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/favicons/favicon.ico -------------------------------------------------------------------------------- /app/assets/stylesheets/_core.scss: -------------------------------------------------------------------------------- 1 | @import "bourbon"; 2 | @import "base/base"; 3 | @import "neat"; 4 | 5 | html, 6 | body { 7 | box-sizing: border-box 8 | } 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_presets.scss: -------------------------------------------------------------------------------- 1 | @import "presets/fonts"; 2 | @import "presets/variables"; 3 | @import "presets/colors"; 4 | @import "presets/utils"; 5 | 6 | -------------------------------------------------------------------------------- /app/models/broadcast_message.rb: -------------------------------------------------------------------------------- 1 | class BroadcastMessage < ActiveRecord::Base 2 | validates :title, presence: true 3 | validates :body, presence: true 4 | end 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /app/assets/fonts/18F_Nimbus/18FNimbusSansL-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/18F_Nimbus/18FNimbusSansL-bold.ttf -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-Bright-L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-Bright-L.png -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-Bright-M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-Bright-M.png -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/PNG/18F-Logo-Bright-S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/PNG/18F-Logo-Bright-S.png -------------------------------------------------------------------------------- /app/helpers/flashes_helper.rb: -------------------------------------------------------------------------------- 1 | module FlashesHelper 2 | def user_facing_flashes 3 | flash.to_hash.slice("alert", "error", "notice", "success") 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/fonts/18F_Nimbus/18FNimbusSansL-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/18F_Nimbus/18FNimbusSansL-italic.ttf -------------------------------------------------------------------------------- /app/assets/fonts/18F_Nimbus/18FNimbusSansL-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/fonts/18F_Nimbus/18FNimbusSansL-regular.ttf -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/18F/dolores-landingham-slack-bot/HEAD/app/assets/images/18F_Logo/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /app/services/employee_updater.rb: -------------------------------------------------------------------------------- 1 | class EmployeeUpdater 2 | def run 3 | Employee.find_each do |employee| 4 | SlackUsernameUpdater.new(employee).update 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require 'bundler/setup' 7 | load Gem.bin_path('rspec-core', 'rspec') 8 | -------------------------------------------------------------------------------- /app/views/employees/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'header', :title => 'Update employee' %> 2 | 3 |
4 | <%= render "flashes" -%> 5 | <%= render "form" %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/employees/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'header', :title => 'Add a new employee' %> 2 | 3 |
4 | <%= render "flashes" -%> 5 | <%= render "form" %> 6 |
7 | -------------------------------------------------------------------------------- /app/assets/images/18F_Logo/favicons/code.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/components/_tables.scss: -------------------------------------------------------------------------------- 1 | .column-buttons { 2 | width: 105px; 3 | } 4 | 5 | .column-wide { 6 | width: 125px; 7 | } 8 | 9 | .data-small { 10 | font-size: 0.8em; 11 | } 12 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/jquery-elastic.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var toMakeElastic = ['#scheduled_message_body']; 3 | 4 | $.each(toMakeElastic, function (i, selector) { 5 | $(selector).elastic(); 6 | }); 7 | 8 | }); 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/sticky.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | 3 | var $filterHeight = $( '.filter-area' ).outerHeight(); 4 | 5 | $( '.main-content' ).css({ 6 | 'margin-top': $filterHeight 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_elements.scss: -------------------------------------------------------------------------------- 1 | @import 'elements/body'; 2 | @import 'elements/headings'; 3 | @import 'elements/lists'; 4 | @import 'elements/links'; 5 | @import 'elements/tables'; 6 | @import 'elements/forms'; 7 | -------------------------------------------------------------------------------- /app/views/onboarding_messages/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'header', :title => 'Update onboarding message' %> 2 | 3 |
4 | <%= render "flashes" -%> 5 | <%= render "form" %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/onboarding_messages/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'header', :title => 'Add a new onboarding message' %> 2 | 3 |
4 | <%= render "flashes" -%> 5 | <%= render "form" %> 6 |
7 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store( 4 | :cookie_store, 5 | key: "_dolores_landingham_bot_session", 6 | ) 7 | -------------------------------------------------------------------------------- /db/migrate/20160313182803_add_slack_channel_id_to_employees.rb: -------------------------------------------------------------------------------- 1 | class AddSlackChannelIdToEmployees < ActiveRecord::Migration 2 | def change 3 | add_column :employees, :slack_channel_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160402214945_add_end_date_to_scheduled_messages.rb: -------------------------------------------------------------------------------- 1 | class AddEndDateToScheduledMessages < ActiveRecord::Migration 2 | def change 3 | add_column :scheduled_messages, :end_date, :date 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160919185207_rename_messages_to_broadcast_messages.rb: -------------------------------------------------------------------------------- 1 | class RenameMessagesToBroadcastMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :messages, :broadcast_messages 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /script/cssh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ $# -eq 0 ]]; then 4 | echo "cssh " 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 | 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 |
2 |
3 |

<%= title %> 4 | <% if defined? subtitle %> 5 | <%= subtitle %> 6 | <% end %> 7 |

8 | <%= yield %> 9 |
10 |
11 |

18F Onboarding Bot

12 | <%= image_tag "18F_Logo/PNG/18F-Logo-Bright-L.png", class: "filter-logo" %> 13 |
14 |
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 | 7 | 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 | 8 | 9 | 10 | 11 | 12 | <% @users.each do |user| %> 13 | 14 | 15 | 16 | 22 | 23 | <% end %> 24 |
    EmailAdmin
    <%= user.email %><%= user.admin? %> 17 | <%= link_to edit_user_path(user), class: 'button button-edit' do %> 18 | 19 | Edit 20 | <% end %> 21 |
    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 | 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 | 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 | 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 | 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 | 32 | 33 |
    34 | <%= yield %> 35 |
    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 | 10 |
    11 | <% end %> 12 | <% end %> 13 | 14 |
    15 | <%= render "flashes" -%> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% @sent_messages.each do |sent_message| %> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <% end %> 34 |
    Slack usernameSent onSent atMessage bodyError message
    <%= sent_message.slack_username %><%= sent_message.sent_on %><%= display_local_time_zone(sent_message) %><%= truncate(sent_message.message_body, length: 50, separator: ' ') %><%= sent_message.error_message %>
    35 | <% if @sent_messages.empty? %> 36 | 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 | 7 |
    8 | <% end %> 9 | <% end %> 10 | 11 | 12 |
    13 | <%= render "flashes" -%> 14 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% @employees.each do |employee| %> 25 | 26 | 27 | 28 | 29 | 35 | 42 | 43 | <% end %> 44 |
    Slack usernameStarted onTime Zone
    <%= employee.slack_username %><%= employee.started_on %><%= employee.time_zone %> 30 | <%= link_to edit_employee_path(employee), class: 'button button-edit' do %> 31 | 32 | Edit 33 | <% end %> 34 | 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 |
    45 | <% if @employees.empty? %> 46 | 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 | --------------------------------------------------------------------------------