├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── applied_checklist.rb │ ├── checklist_item.rb │ ├── github_webhook.rb │ ├── identity.rb │ ├── github_repository.rb │ ├── checklist.rb │ └── user.rb ├── assets │ ├── images │ │ └── .keep │ ├── stylesheets │ │ ├── global.sass │ │ ├── github_repositories.sass │ │ ├── static.sass │ │ ├── header.sass │ │ ├── bootstrap_and_overrides.css │ │ ├── application.css │ │ └── checklists.sass │ └── javascripts │ │ ├── global.js │ │ ├── bootstrap.js │ │ ├── static.coffee │ │ ├── keyboard_shortcuts.js │ │ ├── application.js │ │ └── checklists.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_controller.rb │ ├── github_repositories_controller.rb │ ├── webhooks_controller.rb │ ├── application_controller.rb │ ├── checklist_items_controller.rb │ ├── users │ │ └── omniauth_callbacks_controller.rb │ └── checklists_controller.rb ├── helpers │ ├── static_helper.rb │ └── application_helper.rb ├── views │ ├── checklists │ │ ├── show.json.jbuilder │ │ ├── index.json.jbuilder │ │ ├── new.html.haml │ │ ├── edit.html.haml │ │ ├── _checklist_items.html.haml │ │ ├── _checklist_item.html.haml │ │ ├── show.html.haml │ │ ├── _checklist_item_form.html.haml │ │ ├── index.html.haml │ │ └── _form.html.haml │ ├── devise │ │ ├── mailer │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── unlock_instructions.html.erb │ │ │ └── reset_password_instructions.html.erb │ │ ├── unlocks │ │ │ └── new.html.erb │ │ ├── passwords │ │ │ ├── new.html.erb │ │ │ └── edit.html.erb │ │ ├── confirmations │ │ │ └── new.html.erb │ │ ├── sessions │ │ │ └── new.html.erb │ │ ├── registrations │ │ │ ├── new.html.erb │ │ │ └── edit.html.erb │ │ └── shared │ │ │ └── _links.html.erb │ ├── github_repositories │ │ └── show.html.haml │ ├── static │ │ └── index.html.haml │ └── layouts │ │ └── application.html.haml ├── jobs │ └── github_webhook_worker.rb └── lib │ ├── request_forgery_protection_token_verifier.rb │ └── github_client.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .ruby-gemset ├── .ruby-version ├── vendor └── assets │ ├── javascripts │ ├── .keep │ ├── fastclick-1.0.6.js │ └── mousetrap-1.4.6.js │ └── stylesheets │ └── .keep ├── .rspec ├── config ├── initializers │ ├── timeout.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── sprockets.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── inflections.rb │ ├── content_security_policy.rb │ ├── new_framework_defaults_5_2.rb │ └── devise.rb ├── projections.json ├── environment.rb ├── boot.rb ├── database.yml ├── routes.rb ├── locales │ ├── en.bootstrap.yml │ ├── en.yml │ └── devise.en.yml ├── unicorn.rb ├── secrets.yml ├── nginx.conf.erb ├── storage.yml ├── application.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb └── newrelic.yml ├── bin ├── dev ├── rake ├── bundle ├── rails ├── delayed_job ├── yarn ├── update └── setup ├── package.json ├── .buildpacks ├── Procfile ├── spec ├── controllers │ └── static_controller_spec.rb ├── models │ └── user_spec.rb ├── helpers │ └── static_helper_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── docker-web-entrypoint.sh ├── config.ru ├── db ├── migrate │ ├── 20150222001723_add_admin_column_to_users.rb │ ├── 20150222010937_unique_index_on_uid_and_provider.rb │ ├── 20181125230625_add_hook_deleted_at_to_github_webhooks.rb │ ├── 20150303024255_add_when_matching_pattern_to_checklists.rb │ ├── 20150219132122_create_checklists.rb │ ├── 20150220035121_add_created_by_id_to_checklists.rb │ ├── 20150219133348_create_checklist_items.rb │ ├── 20241106203538_change_applied_checklist_pull_request_id_integer_to_big_int.rb │ ├── 20181125224029_add_updater_to_checklists.rb │ ├── 20150220031546_create_identities.rb │ ├── 20150223012124_create_github_webhooks.rb │ ├── 20150223135342_create_applied_checklists.rb │ ├── 20150222225503_create_github_repositories.rb │ ├── 20150222001459_create_delayed_jobs.rb │ └── 20150204150447_devise_create_users.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── Dockerfile ├── docker-compose.yml ├── .gitignore ├── Gemfile ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | preflight -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.8 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /app/helpers/static_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticHelper 2 | end 3 | -------------------------------------------------------------------------------- /config/initializers/timeout.rb: -------------------------------------------------------------------------------- 1 | Rack::Timeout.timeout = 10 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/global.sass: -------------------------------------------------------------------------------- 1 | body 2 | padding-top: 70px 3 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | rails server -p 9600 -b 0.0.0.0 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "12.22.12" 4 | } 5 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/github_repositories.sass: -------------------------------------------------------------------------------- 1 | .repository-checklists 2 | max-width: 400px 3 | -------------------------------------------------------------------------------- /app/controllers/static_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static.sass: -------------------------------------------------------------------------------- 1 | .hero-button 2 | max-width: 400px 3 | margin: 20px 0 20px 4 | -------------------------------------------------------------------------------- /app/views/checklists/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @checklist, :id, :created_at, :updated_at 2 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/ryandotsmith/nginx-buildpack 2 | https://github.com/heroku/heroku-buildpack-ruby.git 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/global.js: -------------------------------------------------------------------------------- 1 | $(document).on("ready page:load", function() { 2 | FastClick.attach(document.body); 3 | }) 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/start-nginx bundle exec unicorn -E $RAILS_ENV -c ./config/unicorn.rb 2 | worker: bundle exec rake jobs:work --trace 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | $(document).on("ready page:load", function() { 2 | $('[data-toggle="tooltip"]').tooltip() 3 | }) 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/header.sass: -------------------------------------------------------------------------------- 1 | .navbar .avatar 2 | padding-top: 10px 3 | padding-bottom: 10px 4 | img 5 | height: 31px 6 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/controllers/static_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe StaticController, type: :controller do 4 | 5 | end 6 | -------------------------------------------------------------------------------- /config/projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "app/workers/*_worker.rb" : { 3 | "command": "worker", 4 | "template": "class %SWorker\nend" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docker-web-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -f tmp/pids/server.pid ]; then 5 | rm tmp/pids/server.pid 6 | fi 7 | 8 | exec bundle exec "$@" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe User, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/checklists/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array!(@checklists) do |checklist| 2 | json.extract! checklist, :id 3 | json.url checklist_url(checklist, format: :json) 4 | end 5 | -------------------------------------------------------------------------------- /app/views/checklists/new.html.haml: -------------------------------------------------------------------------------- 1 | %h2 New checklist 2 | 3 | = render 'form', checklist: checklist 4 | 5 | .row 6 | .col-sm-2 7 | .col-sm-6 8 | = link_to 'Back', checklists_path 9 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_preflight_session' 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def rails_to_bootstrap_flash_key(key) 3 | {notice: :success, alert: :warning, error: :danger}.with_indifferent_access.fetch(key) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150222001723_add_admin_column_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAdminColumnToUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :users, :admin, :boolean, null: false, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150222010937_unique_index_on_uid_and_provider.rb: -------------------------------------------------------------------------------- 1 | class UniqueIndexOnUidAndProvider < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index :identities, [:uid, :provider], unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/checklists/edit.html.haml: -------------------------------------------------------------------------------- 1 | %h2 Editing checklist 2 | 3 | = render 'form' 4 | 5 | .row 6 | .col-sm-2 7 | .col-sm-6 8 | = link_to 'Show', checklist 9 | \| 10 | = link_to 'Back', checklists_path 11 | -------------------------------------------------------------------------------- /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 | # require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /db/migrate/20181125230625_add_hook_deleted_at_to_github_webhooks.rb: -------------------------------------------------------------------------------- 1 | class AddHookDeletedAtToGithubWebhooks < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :github_webhooks, :hook_deleted_at, :datetime 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 | -------------------------------------------------------------------------------- /db/migrate/20150303024255_add_when_matching_pattern_to_checklists.rb: -------------------------------------------------------------------------------- 1 | class AddWhenMatchingPatternToChecklists < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :checklists, :with_file_matching_pattern, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/static.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/models/applied_checklist.rb: -------------------------------------------------------------------------------- 1 | class AppliedChecklist < ApplicationRecord 2 | belongs_to :checklist 3 | 4 | validates :checklist, :github_pull_request_id, presence: true 5 | validates :checklist_id, uniqueness: {scope: :github_pull_request_id} 6 | end 7 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 |

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/models/checklist_item.rb: -------------------------------------------------------------------------------- 1 | class ChecklistItem < ApplicationRecord 2 | belongs_to :checklist 3 | validates :name, :created_by, presence: true 4 | belongs_to :created_by, class_name: 'User' 5 | 6 | def to_markdown 7 | "- [ ] #{name}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20150219132122_create_checklists.rb: -------------------------------------------------------------------------------- 1 | class CreateChecklists < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :checklists do |t| 4 | t.string :name, :null => false 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /db/migrate/20150220035121_add_created_by_id_to_checklists.rb: -------------------------------------------------------------------------------- 1 | class AddCreatedByIdToChecklists < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :checklists, :created_by_id, :integer, null: false 4 | add_column :checklist_items, :created_by_id, :integer, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/bootstrap_and_overrides.css: -------------------------------------------------------------------------------- 1 | /* 2 | =require twitter-bootstrap-static/bootstrap 3 | 4 | Use Font Awesome icons (default) 5 | To use Glyphicons sprites instead of Font Awesome, replace with "require twitter-bootstrap-static/sprites" 6 | =require twitter-bootstrap-static/fontawesome 7 | */ -------------------------------------------------------------------------------- /app/controllers/github_repositories_controller.rb: -------------------------------------------------------------------------------- 1 | class GithubRepositoriesController < ApplicationController 2 | before_action :authenticate_user! 3 | expose(:github_repositories) { current_user.accessible_github_repositories } 4 | expose(:github_repository, scope: -> { github_repositories }) 5 | 6 | def show 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20150219133348_create_checklist_items.rb: -------------------------------------------------------------------------------- 1 | class CreateChecklistItems < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :checklist_items do |t| 4 | t.string :name, null: false 5 | t.belongs_to :checklist, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/checklists/_checklist_items.html.haml: -------------------------------------------------------------------------------- 1 | .checklist-items 2 | .row 3 | .col-xs-10 4 | %h4 Item 5 | - checklist.checklist_items.each do |item| 6 | = render 'checklist_item', item: item, checklist: checklist 7 | .row 8 | = render 'checklist_item_form', checklist: checklist, item: ChecklistItem.new, placeholder: 'New item' 9 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 |

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.5.8 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs 4 | RUN mkdir /preflight 5 | 6 | WORKDIR /preflight 7 | 8 | ADD .ruby-version /preflight/.ruby-version 9 | ADD Gemfile /preflight/Gemfile 10 | ADD Gemfile.lock /preflight/Gemfile.lock 11 | 12 | RUN bundle install 13 | ADD . /preflight -------------------------------------------------------------------------------- /config/initializers/sprockets.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.assets.register_postprocessor 'application/javascript', :close_over_app_javascripts do |context, data| 2 | if context.pathname.to_s.starts_with?(Rails.root.join('app/assets/javascripts').to_s) 3 | "(function () {\n'use strict';\n#{data}\n}).call(this);" 4 | else 5 | data 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/webhooks_controller.rb: -------------------------------------------------------------------------------- 1 | class WebhooksController < ApplicationController 2 | skip_before_action :verify_authenticity_token 3 | 4 | def github 5 | wanted_headers = GithubWebhookWorker.get_headers(request.headers) 6 | Delayed::Job.enqueue(GithubWebhookWorker.new(wanted_headers, request.raw_post)) 7 | 8 | head :ok 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 | -------------------------------------------------------------------------------- /db/migrate/20241106203538_change_applied_checklist_pull_request_id_integer_to_big_int.rb: -------------------------------------------------------------------------------- 1 | class ChangeAppliedChecklistPullRequestIdIntegerToBigInt < ActiveRecord::Migration[5.2] 2 | def up 3 | change_column :applied_checklists, :github_pull_request_id, :bigint 4 | end 5 | 6 | def down 7 | change_column :applied_checklists, :github_pull_request_id, :integer 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/checklists/_checklist_item.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | = render 'checklists/checklist_item_form', checklist: checklist, item: item 3 | .col-xs-2.col-md-5 4 | = link_to '', checklist_checklist_item_path(checklist, item), class: 'destroy-checklist-item btn btn-danger btn-sm', method: :delete, remote: true, :'data-disable' => true, :'data-comfortable-text' => 'Remove', :'data-abbreviated-text' => 'X' 5 | -------------------------------------------------------------------------------- /app/views/checklists/show.html.haml: -------------------------------------------------------------------------------- 1 | %p#notice= notice 2 | 3 | %h3= link_to checklist.github_repository.github_full_name, github_repository_path(checklist.github_repository) 4 | %h3= checklist.name 5 | 6 | .row 7 | .col-xs-10 8 | = link_to 'Edit', edit_checklist_path(checklist) 9 | \| 10 | = link_to 'Back', checklists_path 11 | 12 | = render 'checklist_items', checklist: checklist 13 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | host: <%= ENV.fetch("DB_HOST", "localhost") %> 4 | database: <%= ENV.fetch("POSTGRES_DB", "preflight-dev") %> 5 | username: <%= ENV.fetch("POSTGRES_USER", "") %> 6 | password: <%= ENV.fetch("POSTGRES_PASSWORD", "") %> 7 | pool: 10 8 | timeout: 5000 9 | 10 | test: 11 | adapter: postgresql 12 | database: preflight-test 13 | pool: 10 14 | timeout: 5000 15 | -------------------------------------------------------------------------------- /db/migrate/20181125224029_add_updater_to_checklists.rb: -------------------------------------------------------------------------------- 1 | class AddUpdaterToChecklists < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :checklists, :last_updated_by_id, :integer 4 | execute %(UPDATE checklists SET last_updated_by_id = created_by_id) 5 | change_column :checklists, :last_updated_by_id, :integer, null: false 6 | end 7 | 8 | def down 9 | remove_column :checklists, :last_updated_by_id 10 | end 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 wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Someone has requested a link to change your password. You can do this through the link below.

4 | 5 |

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

6 | 7 |

If you didn't request this, please ignore this email.

8 |

Your password won't change until you access the link above and create a new one.

9 | -------------------------------------------------------------------------------- /db/migrate/20150220031546_create_identities.rb: -------------------------------------------------------------------------------- 1 | class CreateIdentities < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :identities do |t| 4 | t.string :provider, null: false 5 | t.belongs_to :user, null: false 6 | t.string :uid, null: false 7 | t.text :omniauth_data, null: false 8 | 9 | t.timestamps null: false 10 | end 11 | 12 | add_index :identities, [:user_id, :provider], unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/keyboard_shortcuts.js: -------------------------------------------------------------------------------- 1 | var oldStopCallback = Mousetrap.stopCallback 2 | Mousetrap.stopCallback = function(e, element, combo) { 3 | if (!oldStopCallback.call(Mousetrap, e, element, combo)) { 4 | return false 5 | } else { 6 | return combo != 'command+enter' 7 | } 8 | } 9 | 10 | Mousetrap.bind('command+enter', function(e) { 11 | var $target = $(e.target) 12 | if ($target.is('input')) { 13 | $target.closest('form').submit() 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /db/migrate/20150223012124_create_github_webhooks.rb: -------------------------------------------------------------------------------- 1 | class CreateGithubWebhooks < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :github_webhooks do |t| 4 | t.integer :github_id, null: false 5 | t.belongs_to :github_repository, null: false 6 | t.belongs_to :created_by, null: false 7 | 8 | t.timestamps null: false 9 | end 10 | 11 | add_index :github_webhooks, :github_id 12 | add_index :github_webhooks, :github_repository_id, unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/devise/unlocks/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend unlock instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 |
12 | <%= f.submit "Resend unlock instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /spec/helpers/static_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # Specs in this file have access to a helper object that includes 4 | # the StaticHelper. For example: 5 | # 6 | # describe StaticHelper do 7 | # describe "string concat" do 8 | # it "concats two strings with spaces" do 9 | # expect(helper.concat_strings("this","that")).to eq("this that") 10 | # end 11 | # end 12 | # end 13 | RSpec.describe StaticHelper, type: :helper do 14 | pending "add some examples to (or delete) #{__FILE__}" 15 | end 16 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Forgot your password?

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 |
12 | <%= f.submit "Send me reset password instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres:14.12 5 | ports: 6 | - "5432:5432" 7 | environment: 8 | POSTGRES_USER: 9 | POSTGRES_PASSWORD: 10 | POSTGRES_DB: 11 | 12 | 13 | web: 14 | build: . 15 | entrypoint: ./docker-web-entrypoint.sh 16 | command: bundle exec rails s -p 3000 -b '0.0.0.0' 17 | volumes: 18 | - .:/preflight 19 | ports: 20 | - "3000:3000" 21 | depends_on: 22 | - db 23 | env_file: 24 | - .env 25 | -------------------------------------------------------------------------------- /app/views/checklists/_checklist_item_form.html.haml: -------------------------------------------------------------------------------- 1 | - placeholder ||= nil 2 | = form_for [checklist, item], remote: true do |f| 3 | .col-xs-6.col-md-4 4 | - unless item.new_record? 5 | .checklist-item-name{'data-edit-prompt' => 'Edit'} 6 | %span.name= item.name 7 | = f.text_field :name, placeholder: placeholder, class: 'form-control', :'data-edit-control' => true 8 | .col-xs-2.col-md-1 9 | = f.submit 'Save', :'data-disable-with' => 'Saving..', class: 'btn btn-primary btn-sm', :'data-edit-control' => true 10 | -------------------------------------------------------------------------------- /app/views/devise/confirmations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 |
12 | <%= f.submit "Resend confirmation instructions" %> 13 |
14 | <% end %> 15 | 16 | <%= render "devise/shared/links" %> 17 | -------------------------------------------------------------------------------- /db/migrate/20150223135342_create_applied_checklists.rb: -------------------------------------------------------------------------------- 1 | class CreateAppliedChecklists < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :applied_checklists do |t| 4 | t.belongs_to :checklist, null: false 5 | t.integer :github_pull_request_id, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | 10 | add_index :applied_checklists, [:github_pull_request_id, :checklist_id], unique: true, name: 'one_checklist_application_per_pull' 11 | add_index :applied_checklists, :checklist_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | !/log/.keep 17 | /tmp 18 | .env 19 | keys 20 | -------------------------------------------------------------------------------- /app/views/github_repositories/show.html.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | = github_repository.github_full_name 3 | = '(' 4 | = link_to 'Github', github_repository.github_url, target: '_blank' 5 | = ')' 6 | 7 | - if github_repository.checklists.any? 8 | %table.table.repository-checklists 9 | %thead 10 | %tr 11 | %th Checklist 12 | %tbody 13 | - github_repository.checklists.each do |c| 14 | %tr 15 | %td= link_to c.name, checklist_url(c) 16 | 17 | = link_to '[ + ] New Checklist', new_checklist_path(github_repository_id: github_repository.id) 18 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | if ENV['REQUIRE_HTTPS'] 3 | before_action :require_https 4 | end 5 | 6 | protect_from_forgery with: :exception 7 | 8 | def default_url_options 9 | ActionMailer::Base.default_url_options 10 | end 11 | 12 | def after_sign_in_path_for(user) 13 | checklists_path 14 | end 15 | 16 | private 17 | 18 | def require_https 19 | unless request.scheme == 'https' 20 | redirect_to %{https://#{request.host_with_port}#{request.path}#{request.query_string}} 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/checklists/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 My checklists 2 | 3 | %table.table 4 | %tr 5 | %th Name 6 | %th Repository 7 | %th 8 | %th 9 | 10 | - checklists.each do |checklist| 11 | %tr 12 | %td= link_to checklist.name, checklist 13 | %th= link_to checklist.github_repository.github_full_name, github_repository_path(checklist.github_repository) 14 | %td= link_to 'Edit', edit_checklist_path(checklist) 15 | %td= link_to 'Destroy', checklist, :method => :delete, :data => { :confirm => 'Are you sure?' } 16 | 17 | %br 18 | 19 | = link_to '[ + ] New Checklist', new_checklist_path 20 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: 'static#index' 3 | devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" } 4 | 5 | resources :checklists do 6 | resources :checklist_items, only: [:create, :update, :destroy] 7 | end 8 | 9 | resources :github_repositories, only: :show 10 | 11 | post '/github/webhook', to: 'webhooks#github', as: :github_webhook 12 | 13 | constraints ->(request) { request.env['warden'].authenticate? && request.env['warden'].user.admin? } do 14 | match "/delayed_job" => DelayedJobWeb, :anchor => false, via: [:get, :post] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/github_webhook.rb: -------------------------------------------------------------------------------- 1 | class GithubWebhook < ApplicationRecord 2 | belongs_to :github_repository 3 | belongs_to :created_by, class_name: 'User' 4 | 5 | validates :created_by, presence: true 6 | validates :github_id, :github_repository, presence: true, uniqueness: true 7 | 8 | def self.unhook_all_hooked_by_old_oauth_integration! 9 | where(hook_deleted_at: nil).find_each do |hook| 10 | hook.transaction do 11 | hook.hook_deleted_at = Time.now 12 | 13 | hook.created_by.github_client.remove_hook(hook.github_repository.github_full_name, hook.github_id) 14 | 15 | hook.save! 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/locales/en.bootstrap.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | breadcrumbs: 6 | application: 7 | root: "Index" 8 | pages: 9 | pages: "Pages" 10 | helpers: 11 | actions: "Actions" 12 | links: 13 | back: "Back" 14 | cancel: "Cancel" 15 | confirm: "Are you sure?" 16 | destroy: "Delete" 17 | new: "New" 18 | edit: "Edit" 19 | titles: 20 | edit: "Edit %{model}" 21 | save: "Save %{model}" 22 | new: "New %{model}" 23 | delete: "Delete %{model}" 24 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20150222225503_create_github_repositories.rb: -------------------------------------------------------------------------------- 1 | class CreateGithubRepositories < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :github_repositories do |t| 4 | t.integer :github_id, null: false 5 | t.string :github_full_name, null: false 6 | t.string :github_owner_type, null: false 7 | t.string :github_url, null: false 8 | 9 | t.timestamps null: false 10 | end 11 | 12 | add_index :github_repositories, :github_id, unique: true 13 | 14 | add_column :users, :accessible_github_repository_ids, :text, array: true, default: [] 15 | 16 | add_column :checklists, :github_repository_id, :integer, null: false 17 | add_index :checklists, :github_repository_id 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Log in

2 | 3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 4 |
5 | <%= f.label :email %>
6 | <%= f.email_field :email, autofocus: true %> 7 |
8 | 9 |
10 | <%= f.label :password %>
11 | <%= f.password_field :password, autocomplete: "off" %> 12 |
13 | 14 | <% if devise_mapping.rememberable? -%> 15 |
16 | <%= f.check_box :remember_me %> 17 | <%= f.label :remember_me %> 18 |
19 | <% end -%> 20 | 21 |
22 | <%= f.submit "Log in" %> 23 |
24 | <% end %> 25 | 26 | <%= render "devise/shared/links" %> 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | worker_processes Integer(ENV['WEB_CONCURRENCY'] || 3) 2 | timeout 15 3 | preload_app true 4 | 5 | LISTEN = ['staging', 'production'].include?(ENV['RAILS_ENV']) ? '/tmp/nginx.socket' : ENV.fetch('PORT').to_i 6 | listen LISTEN 7 | 8 | before_fork do |server, worker| 9 | FileUtils.touch('/tmp/app-initialized') 10 | 11 | Signal.trap 'TERM' do 12 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead' 13 | Process.kill 'QUIT', Process.pid 14 | end 15 | 16 | ActiveRecord::Base.connection.disconnect! 17 | end 18 | 19 | after_fork do |server, worker| 20 | Signal.trap 'TERM' do 21 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT' 22 | end 23 | 24 | ActiveRecord::Base.establish_connection 25 | end 26 | -------------------------------------------------------------------------------- /app/views/devise/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Change your password

2 | 3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= devise_error_messages! %> 5 | <%= f.hidden_field :reset_password_token %> 6 | 7 |
8 | <%= f.label :password, "New password" %>
9 | <%= f.password_field :password, autofocus: true, autocomplete: "off" %> 10 |
11 | 12 |
13 | <%= f.label :password_confirmation, "Confirm new password" %>
14 | <%= f.password_field :password_confirmation, autocomplete: "off" %> 15 |
16 | 17 |
18 | <%= f.submit "Change my password" %> 19 |
20 | <% end %> 21 | 22 | <%= render "devise/shared/links" %> 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require twitter/bootstrap 16 | //= require turbolinks 17 | //= require mousetrap-1.4.6 18 | //= require fastclick-1.0.6 19 | //= require_tree . 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/checklists.sass: -------------------------------------------------------------------------------- 1 | .checklist-items .row 2 | padding: 10px 0 10px 3 | .btn 4 | vertical-align: middle 5 | 6 | .edit_checklist_item 7 | [data-edit-control] 8 | display: none 9 | 10 | &[data-edit-mode] 11 | [data-edit-control] 12 | display: block 13 | .name 14 | display: none 15 | 16 | &:not([data-edit-mode]) 17 | .checklist-item-name 18 | &:after 19 | content: attr(data-edit-prompt) 20 | margin-left: 5px 21 | color: grey 22 | 23 | @media (min-width: 768px) 24 | &:after 25 | display: none 26 | &:hover:after 27 | display: inline 28 | 29 | [data-comfortable-text] 30 | @media (max-width: 767px) 31 | &:after 32 | content: attr(data-abbreviated-text) 33 | 34 | @media (min-width: 768px) 35 | &:after 36 | content: attr(data-comfortable-text) 37 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign up

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 |
12 | <%= f.label :password %> 13 | <% if @validatable %> 14 | (<%= @minimum_password_length %> characters minimum) 15 | <% end %>
16 | <%= f.password_field :password, autocomplete: "off" %> 17 |
18 | 19 |
20 | <%= f.label :password_confirmation %>
21 | <%= f.password_field :password_confirmation, autocomplete: "off" %> 22 |
23 | 24 |
25 | <%= f.submit "Sign up" %> 26 |
27 | <% end %> 28 | 29 | <%= render "devise/shared/links" %> 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ruby File.read('./.ruby-version').chomp 2 | source 'https://rubygems.org' 3 | 4 | gem "decent_exposure" 5 | gem 'delayed_job' 6 | gem 'delayed_job_active_record' 7 | gem 'delayed_job_web' 8 | gem "devise" 9 | gem 'dotenv' 10 | gem 'foreman' 11 | gem "haml" 12 | gem "haml-rails" 13 | gem 'listen' 14 | gem 'newrelic_rpm' 15 | gem 'nokogiri', '~> 1.11.4' 16 | gem "omniauth" 17 | gem 'omniauth-github' 18 | gem 'octokit' 19 | gem "pg" 20 | gem "pry" 21 | gem "pry-rails" 22 | gem "twitter-bootstrap-rails" 23 | gem 'coffee-rails' 24 | gem 'jquery-rails' 25 | gem 'rack-timeout' 26 | gem 'rails', '~> 5.2.4.3' 27 | gem 'rake', '< 13.0' 28 | gem 'sass-rails' 29 | gem 'thin' 30 | gem 'turbolinks' 31 | gem 'uglifier', '>= 1.3.0' 32 | gem 'unicorn' 33 | 34 | group :test, :development do 35 | gem "factory_girl" 36 | gem "fivemat" 37 | gem "rspec-rails" 38 | gem "rspec" 39 | end 40 | 41 | group :test do 42 | gem "shoulda-matchers" 43 | end 44 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 95f0e896e28742c36993e3a739222881847bea4cf1152ef8779fcb231fd328714cd029fdfb5641a9fbc2eb84aca97ab2fe3432e6406b75ea5452a5194e47b236 15 | 16 | test: 17 | secret_key_base: dd6015f9863006a9a941662189ea6dae2c7c9c8cd235cd6111e63acf1cb54bb00a49c2703ea030549c715f8fc9e646ea409022ad6b05039a055eb6653e56eb59 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/checklist_items_controller.rb: -------------------------------------------------------------------------------- 1 | class ChecklistItemsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | expose(:checklists) { current_user.accessible_checklists } 5 | expose(:checklist, scope: -> { checklists }) 6 | expose(:checklist_items, from: :checklist) 7 | expose(:checklist_item, scope: -> { checklist_items }) 8 | 9 | def create 10 | checklist_item.created_by = current_user 11 | save_and_render_checklist 12 | end 13 | 14 | def update 15 | checklist_item.attributes = checklist_item_params 16 | save_and_render_checklist 17 | end 18 | 19 | def destroy 20 | checklist_item.destroy 21 | head :ok 22 | end 23 | 24 | private 25 | 26 | def save_and_render_checklist 27 | if checklist_item.save 28 | render partial: 'checklists/checklist_item', locals: {item: checklist_item, checklist: checklist} 29 | else 30 | render body: checklist_item.errors.full_messages.join("\n"), status: :bad_request 31 | end 32 | end 33 | 34 | def checklist_item_params 35 | params.require(:checklist_item).permit(:name) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/users/omniauth_callbacks_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::OmniauthCallbacksController < ApplicationController 2 | before_action :check_team_membership 3 | 4 | def github 5 | if user_signed_in? 6 | current_user.update_from_omniauth!(omniauth) 7 | redirect_to :root, :notice => "Done!" 8 | else 9 | user = User.find_or_create_from_omniauth!(omniauth) 10 | 11 | flash[:notice] = after_sign_in_notice_for(user) 12 | sign_in_and_redirect(user) 13 | end 14 | end 15 | 16 | private 17 | 18 | def omniauth 19 | request.env['omniauth.auth'] 20 | end 21 | 22 | def after_sign_in_notice_for(user) 23 | if user.just_created? 24 | "Signed up with github!" 25 | else 26 | "Logged in with github" 27 | end 28 | end 29 | 30 | def check_team_membership 31 | login = omniauth.info.nickname 32 | 33 | unless client.team_member?(ENV.fetch('GITHUB_TEAM_ID'), login) 34 | render :status => :forbidden, :text => "Sorry!" 35 | end 36 | end 37 | 38 | def client 39 | GithubClient.new(:access_token => omniauth.credentials.token) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/checklists/_form.html.haml: -------------------------------------------------------------------------------- 1 | = form_for checklist, html: {class: 'form-horizontal'} do |f| 2 | - if checklist.errors.any? 3 | #error_explanation 4 | %h2= "#{pluralize(checklist.errors.count, "error")} prohibited this checklist from being saved:" 5 | %ul 6 | - checklist.errors.full_messages.each do |msg| 7 | %li= msg 8 | .form-group 9 | = f.label :github_repository_id, class: 'control-label col-sm-2' 10 | .col-sm-6 11 | = f.select :github_repository_id, current_user.accessible_github_repositories.select_options, {include_blank: true}, class: 'form-control' 12 | .form-group 13 | = f.label :name, class: 'control-label col-sm-2' 14 | .col-sm-6 15 | = f.text_field :name, class: 'form-control' 16 | .form-group 17 | = f.label :with_file_matching_pattern, "Only when at least one changed file path matches this pattern (regexp)", class: 'control-label col-sm-2' 18 | .col-sm-6 19 | = f.text_field :with_file_matching_pattern, class: 'form-control' 20 | .form-group 21 | .col-sm-2 22 | .col-sm-6 23 | .actions 24 | = f.submit 'Save', class: 'btn btn-primary btn-sm' 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/checklists.js: -------------------------------------------------------------------------------- 1 | $(document).on('ajax:success', '.destroy-checklist-item', function() { 2 | $(this).closest('.row').slideUp('slow', function() { $(this).remove() }) 3 | }) 4 | 5 | $(document).on('ajax:success', '.new_checklist_item', function(e, data) { 6 | var $form = $(this) 7 | $form[0].reset() 8 | $(data).insertBefore($form.closest('.row')) 9 | }).on('ajax:error', '.new_checklist_item, .edit_checklist_item', function(e, xhr) { 10 | alert(xhr.responseText) 11 | }) 12 | 13 | $(document).on('click', '.edit_checklist_item:not([data-edit-mode]) .checklist-item-name', function() { 14 | var $form = $(this).closest('.edit_checklist_item') 15 | $form.attr('data-edit-mode', true). 16 | find('input[type="text"]').focus().end(). 17 | closest('.checklist-items').find('.edit_checklist_item').not($form).removeAttr('data-edit-mode') 18 | return false 19 | }).on('click', function(e) { 20 | if (!$(e.target).is('.edit_checklist_item input[type="text"]')) { 21 | $('.edit_checklist_item').removeAttr('data-edit-mode') 22 | } 23 | }) 24 | 25 | $(document).on('ajax:success', '.edit_checklist_item', function(e, data) { 26 | $(this).closest('.row').replaceWith(data) 27 | }) 28 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /config/nginx.conf.erb: -------------------------------------------------------------------------------- 1 | daemon off; 2 | #Heroku dynos have at least 4 cores. 3 | worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>; 4 | 5 | events { 6 | use epoll; 7 | accept_mutex on; 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | gzip on; 13 | gzip_comp_level 2; 14 | gzip_min_length 512; 15 | 16 | server_tokens off; 17 | 18 | log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; 19 | access_log logs/nginx/access.log l2met; 20 | error_log logs/nginx/error.log; 21 | 22 | include mime.types; 23 | default_type application/octet-stream; 24 | sendfile on; 25 | 26 | #Must read the body in 5 seconds. 27 | client_body_timeout 5; 28 | 29 | upstream app_server { 30 | server unix:/tmp/nginx.socket fail_timeout=0; 31 | } 32 | 33 | server { 34 | listen <%= ENV["PORT"] %>; 35 | server_name _; 36 | keepalive_timeout 5; 37 | root /app/public; 38 | try_files $uri/index.html $uri.html $uri @rails; 39 | 40 | location @rails { 41 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 42 | proxy_set_header Host $http_host; 43 | proxy_redirect off; 44 | proxy_pass http://app_server; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/models/identity.rb: -------------------------------------------------------------------------------- 1 | class Identity < ApplicationRecord 2 | belongs_to :user, inverse_of: :identities 3 | validates :provider, presence: true, :inclusion => { in: %w(github) } 4 | validates :omniauth_data, presence: true 5 | validates :uid, uniqueness: {scope: [:provider]} 6 | validates :provider, uniqueness: {scope: [:user_id]} 7 | 8 | serialize :omniauth_data, Hash 9 | 10 | def self.link!(omniauth) 11 | create! do |a| 12 | a.provider = omniauth.provider 13 | a.uid = omniauth.uid 14 | a.omniauth_data = omniauth 15 | end 16 | end 17 | 18 | def self.find_by_omniauth(omniauth) 19 | find_by_uid_and_provider(omniauth.uid, omniauth.provider) 20 | end 21 | 22 | def self.find_by_omniauth!(omniauth) 23 | find_by_omniauth(omniauth) or raise ActiveRecord::RecordNotFound 24 | end 25 | 26 | def update_from_omniauth!(omniauth) 27 | self.omniauth_data = omniauth 28 | save! 29 | end 30 | 31 | def image_url 32 | omniauth_data.info.image 33 | end 34 | 35 | def profile_url 36 | omniauth_data.extra.raw_info.html_url 37 | end 38 | 39 | delegate :credentials, to: :omniauth_data 40 | 41 | def client 42 | GithubClient.new(:access_token => credentials.token) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.html.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end -%> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end -%> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end -%> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end -%> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end -%> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
24 | <% end -%> 25 | <% end -%> 26 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | require "action_mailer/railtie" 11 | require "action_view/railtie" 12 | # require "action_cable/engine" 13 | require "sprockets/railtie" 14 | require 'delegate' 15 | require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | Dotenv.load! if Rails.env.test? || Rails.env.development? 22 | 23 | module Preflight 24 | class Application < Rails::Application 25 | # Initialize configuration defaults for originally generated Rails version. 26 | config.load_defaults 5.0 27 | 28 | config.active_job.queue_adapter = :delayed_job 29 | 30 | # Settings in config/environments/* take precedence over those specified here. 31 | # Application configuration can go into files in config/initializers 32 | # -- all .rb files in that directory are automatically loaded after loading 33 | # the framework and any gems in your application. 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /db/migrate/20150222001459_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration[4.2] 2 | def self.up 3 | create_table :delayed_jobs, force: true do |table| 4 | table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. 6 | table.text :handler, null: false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 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 self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit <%= resource_name.to_s.humanize %>

2 | 3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> 4 | <%= devise_error_messages! %> 5 | 6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %> 9 |
10 | 11 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %> 14 | 15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "off" %> 18 |
19 | 20 |
21 | <%= f.label :password_confirmation %>
22 | <%= f.password_field :password_confirmation, autocomplete: "off" %> 23 |
24 | 25 |
26 | <%= f.label :current_password %> (we need your current password to confirm your changes)
27 | <%= f.password_field :current_password, autocomplete: "off" %> 28 |
29 | 30 |
31 | <%= f.submit "Update" %> 32 |
33 | <% end %> 34 | 35 |

Cancel my account

36 | 37 |

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

38 | 39 | <%= link_to "Back", :back %> 40 | -------------------------------------------------------------------------------- /db/migrate/20150204150447_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table(:users) do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.inet :current_sign_in_ip 20 | t.inet :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps 35 | end 36 | 37 | add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/checklists_controller.rb: -------------------------------------------------------------------------------- 1 | class ChecklistsController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | expose(:checklists) { current_user.accessible_checklists } 5 | expose(:checklist, scope: -> { checklists }) 6 | 7 | expose(:github_repositories) { current_user.accessible_github_repositories } 8 | expose(:github_repository, scope: -> { github_repositories }) 9 | 10 | def index 11 | checklists.includes(:github_repository) 12 | end 13 | 14 | def show 15 | end 16 | 17 | def new 18 | if github_repository 19 | checklist.github_repository = github_repository 20 | end 21 | end 22 | 23 | def edit 24 | end 25 | 26 | def create 27 | checklist.github_repository = github_repositories.find_by_id(checklist_params[:github_repository_id]) 28 | checklist.updater = current_user 29 | 30 | if checklist.save 31 | redirect_to checklist, notice: 'Checklist was successfully created.' 32 | else 33 | render action: 'new' 34 | end 35 | end 36 | 37 | def update 38 | checklist.updater = current_user 39 | 40 | if checklist.update(checklist_params) 41 | redirect_to checklist, notice: 'Checklist was successfully updated.' 42 | else 43 | render action: 'edit' 44 | end 45 | end 46 | 47 | def destroy 48 | checklist.destroy 49 | redirect_to checklists_url, notice: 'Checklist was successfully destroyed.' 50 | end 51 | 52 | private 53 | 54 | def checklist_params 55 | params.require(:checklist).permit(:name, :github_repository_id, :with_file_matching_pattern) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/jobs/github_webhook_worker.rb: -------------------------------------------------------------------------------- 1 | class GithubWebhookWorker < Struct.new(:headers, :body) 2 | HUB_SIGNATURE = 'X-Hub-Signature' 3 | WANTED_HEADERS = %w(X-GitHub-Event X-GitHub-Delivery) << HUB_SIGNATURE 4 | InvalidSignature = Class.new(StandardError) 5 | 6 | HANDLED_ACTIONS = %w(opened synchronize) 7 | 8 | def self.get_headers(headers) 9 | WANTED_HEADERS.each_with_object({}) do |header, hash| 10 | hash[header] = headers[header] 11 | end 12 | end 13 | 14 | def perform 15 | verify_signature! 16 | 17 | return unless HANDLED_ACTIONS.include?(action) 18 | return unless repo = GithubRepository.find_by_github_id(repository_id) 19 | return unless installation_id.present? 20 | 21 | repo.apply_checklists_for_pull!(installation_id, pull_id, number) 22 | end 23 | 24 | def verify_signature! 25 | hub_signature = headers[HUB_SIGNATURE].to_s.split('=', 2).last 26 | signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV.fetch('GITHUB_WEBHOOK_SECRET'), body) 27 | 28 | raise InvalidSignature unless hub_signature == signature 29 | end 30 | 31 | private 32 | 33 | def parsed_body 34 | @parsed_body ||= JSON.parse(body) 35 | end 36 | 37 | def pull_id 38 | parsed_body.fetch('pull_request').fetch('id') 39 | end 40 | 41 | def number 42 | parsed_body.fetch('number') 43 | end 44 | 45 | def repository_id 46 | parsed_body.fetch('repository').fetch('id') 47 | end 48 | 49 | def action 50 | parsed_body['action'] 51 | end 52 | 53 | def installation_id 54 | parsed_body.fetch('installation', {})['id'] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/lib/request_forgery_protection_token_verifier.rb: -------------------------------------------------------------------------------- 1 | # copied from @sikachu https://github.com/omniauth/omniauth/pull/809#issuecomment-497173593 2 | # Provides a callable method that verifies Cross-Site Request Forgery protection 3 | # token. This class includes `ActionController::RequestForgeryProtection` 4 | # directly and utilizes `verified_request?` method to match the way Rails 5 | # performs token verification in Rails controllers. 6 | # 7 | # If you like to learn more about how Rails generate and verify authenticity 8 | # token, you can find the source code at 9 | # https://github.com/rails/rails/blob/v5.2.2/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L217-L240. 10 | class RequestForgeryProtectionTokenVerifier 11 | include ActiveSupport::Configurable 12 | include ActionController::RequestForgeryProtection 13 | 14 | # `ActionController::RequestForgeryProtection` contains a few configurable 15 | # options. As we want to make sure that our configuration is the same as what 16 | # being set in `ActionController::Base`, we should make all out configuration 17 | # methods to delegate to `ActionController::Base`. 18 | config.each_key do |configuration_name| 19 | define_method configuration_name do 20 | ActionController::Base.config[configuration_name] 21 | end 22 | end 23 | 24 | def call(env) 25 | @request = ActionDispatch::Request.new(env) 26 | 27 | unless verified_request? 28 | raise ActionController::InvalidAuthenticityToken 29 | end 30 | end 31 | 32 | private 33 | 34 | attr_reader :request 35 | delegate :params, :session, to: :request 36 | end 37 | -------------------------------------------------------------------------------- /app/models/github_repository.rb: -------------------------------------------------------------------------------- 1 | class GithubRepository < ApplicationRecord 2 | has_many :checklists, dependent: :destroy 3 | 4 | validates :github_id, :github_full_name, :github_owner_type, :github_url, presence: true 5 | validates :github_owner_type, inclusion: %w(Organization User) 6 | 7 | def self.find_or_create_all(repositories) 8 | repositories.each do |r| 9 | where(github_id: r.id).first_or_create do |new_repo| 10 | new_repo.github_full_name = r.full_name 11 | new_repo.github_owner_type = r.owner.type 12 | new_repo.github_url = r.html_url 13 | end 14 | end 15 | end 16 | 17 | def self.select_options 18 | pluck(:github_full_name, :id) 19 | end 20 | 21 | def apply_checklists_for_pull!(installation_id, pull_id, number) 22 | transaction do 23 | return unless candidates = checklists.includes(:checklist_items).presence 24 | 25 | client = GithubClient.for_installation(installation_id) 26 | files = client.pull_request_files(github_full_name, number) 27 | 28 | to_apply = candidates.select do |c| 29 | c.apply_to_pull_with_files?(files) 30 | end.each_with_object([]) do |c, o| 31 | c.applied_checklists.where(github_pull_request_id: pull_id).first_or_create! do |_| 32 | o << c 33 | end 34 | end 35 | 36 | return unless to_apply.present? 37 | 38 | body = client.pull_request(github_full_name, number)['body'] 39 | new_body = (to_apply.map(&:to_markdown).prepend(body)).join("\n\n--------\n") 40 | 41 | client.update_pull_request( 42 | github_full_name, 43 | number, 44 | body: new_body 45 | ) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /app/lib/github_client.rb: -------------------------------------------------------------------------------- 1 | class GithubClient < SimpleDelegator 2 | def self.for_installation(installation_id) 3 | token = app_client.create_app_installation_access_token(installation_id)[:token] 4 | 5 | new(bearer_token: token) 6 | end 7 | 8 | def self.app_client 9 | new(bearer_token: bearer_token) 10 | end 11 | 12 | def self.bearer_token 13 | private_key = OpenSSL::PKey::RSA.new(Base64.decode64(ENV.fetch('GITHUB_APP_PRIVATE_KEY'))) 14 | 15 | payload = { 16 | iat: Time.now.to_i, 17 | exp: Time.now.to_i + (10 * 60) - 10, 18 | iss: ENV.fetch('GITHUB_APP_ID') 19 | } 20 | 21 | JWT.encode(payload, private_key, "RS256") 22 | end 23 | 24 | def initialize(options) 25 | super(Octokit::Client.new(options)) 26 | end 27 | 28 | def repositories_with_write_access 29 | repos = [] 30 | 31 | find_user_installations.installations.flat_map do |installation| 32 | page = 1 33 | 34 | begin 35 | repos += find_installation_repositories_for_user(installation.id, page: page, per_page: 100).repositories 36 | page += 1 37 | end while last_response.rels[:next].present? 38 | end 39 | 40 | with_write_access(repos) 41 | end 42 | 43 | def create_default_hook(repo) 44 | create_hook( 45 | repo.github_full_name, 'web', 46 | {url: github_webhook_url, content_type: 'json', secret: ENV.fetch('GITHUB_WEBHOOK_SECRET') }, 47 | {:events => ['push', 'pull_request']} 48 | ) 49 | end 50 | 51 | private 52 | 53 | def with_write_access(repos_to_check) 54 | repos_to_check.select { |r| r.permissions.push } 55 | end 56 | 57 | def github_webhook_url 58 | Rails.application.routes.url_helpers.github_webhook_url(ActionMailer::Base.default_url_options) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/views/static/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Welcome to Preflight! 2 | 3 | - unless user_signed_in? 4 | %div 5 | = link_to "Auth GitHub to get started", user_github_omniauth_authorize_path, method: :post, class: 'hero-button btn btn-lg btn-block btn-primary' 6 | 7 | %p.lead 8 | As software projects and teams grow, it gets more and more difficult for engineers to remember the things they should consider when making changes to the code. This can be partially mitigated by a comprehensive test suite, which will (in many cases) tell engineers when they've broken existing functionality, but a test suite falls short of capturing (at least) the following problems: introduction of security holes, performance regressions, visual bugs and regressions, UX issues, failure to mobile optimize certain pages, and more. 9 | 10 | %p.lead 11 | Preflight brings to software development what preflight checklists provided for airline safety: a list of things that every engineer has to think about before marking a pull request as "ready for review". With preflight, you can create checklists for repositories, and, when engineers create new pull requests, the checklists are automatically added to the pull request description. 12 | 13 | %p.lead 14 | Unlike testing, preflight checklists won't "break" if someone doesn't look at them. Instead, they are ways of helping engineers avoid common mistakes and regressions. How you use them is up to you and what works best for your team. For certain checklists, you might want to require that engineers "check off" each item to verify that they've done it, like you might want a mechanic to "check off" that they've verified the engine on a plane. For other checklists, you might simply want to list a bunch of things for the engineer to consider, like a list of supported browsers, and leave it up to the engineer's discretion on what he/she thinks is worth checking. 15 | -------------------------------------------------------------------------------- /app/models/checklist.rb: -------------------------------------------------------------------------------- 1 | class Checklist < ApplicationRecord 2 | validates :name, :created_by, :github_repository, presence: true 3 | has_many :checklist_items, dependent: :destroy 4 | has_many :applied_checklists, dependent: :destroy 5 | belongs_to :created_by, class_name: 'User' 6 | belongs_to :last_updated_by, class_name: 'User' 7 | belongs_to :github_repository 8 | 9 | validate :user_can_access_repository 10 | validate :matching_pattern_is_valid_regexp 11 | 12 | after_save :clear_updater 13 | 14 | attr_reader :updater 15 | 16 | def self.for_repositories(github_repositories) 17 | where("github_repository_id IN (#{github_repositories.select(:id).to_sql})") 18 | end 19 | 20 | def to_markdown 21 | "#### #{name}\n" + checklist_items.map(&:to_markdown).join("\n") 22 | end 23 | 24 | delegate :github_client, to: :created_by 25 | 26 | def apply_to_pull_with_files?(files) 27 | return true unless with_file_matching_pattern.present? 28 | re = Regexp.new(with_file_matching_pattern) 29 | 30 | files.any? { |f| f.filename =~ re } 31 | end 32 | 33 | def updater=(user) 34 | @updater = user 35 | 36 | self.last_updated_by = user 37 | self.created_by = user if new_record? 38 | end 39 | 40 | private 41 | 42 | def user_can_access_repository 43 | return unless github_repository.present? && updater.present? 44 | 45 | unless updater.can_access_repository?(github_repository) 46 | errors.add(:base, "You do not have access to this repository") 47 | end 48 | end 49 | 50 | def matching_pattern_is_valid_regexp 51 | return unless with_file_matching_pattern.present? 52 | Regexp.new(with_file_matching_pattern) 53 | rescue RegexpError 54 | errors.add(:with_file_matching_pattern, 'is not a valid regular expression') 55 | end 56 | 57 | def clear_updater 58 | @updater = nil 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults_5_2.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.2 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 8 | 9 | # Make Active Record use stable #cache_key alongside new #cache_version method. 10 | # This is needed for recyclable cache keys. 11 | # Rails.application.config.active_record.cache_versioning = true 12 | 13 | # Use AES-256-GCM authenticated encryption for encrypted cookies. 14 | # Also, embed cookie expiry in signed or encrypted cookies for increased security. 15 | # 16 | # This option is not backwards compatible with earlier Rails versions. 17 | # It's best enabled when your entire app is migrated and stable on 5.2. 18 | # 19 | # Existing cookies will be converted on read then written with the new scheme. 20 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true 21 | 22 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages 23 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. 24 | # Rails.application.config.active_support.use_authenticated_message_encryption = true 25 | 26 | # Add default protection from forgery to ActionController::Base instead of in 27 | # ApplicationController. 28 | # Rails.application.config.action_controller.default_protect_from_forgery = true 29 | 30 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and 31 | # 'f' after migrating old data. 32 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true 33 | 34 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. 35 | # Rails.application.config.active_support.use_sha1_digests = true 36 | 37 | # Make `form_with` generate id attributes for any generated HTML tags. 38 | # Rails.application.config.action_view.form_with_generates_ids = true 39 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | - application_name = Rails.application.class.to_s.split('::').first 3 | %html(lang="en-US" class="#{controller_name}-#{action_name}") 4 | %head 5 | %title 6 | - if content_for?(:title) 7 | = yield (:title) 8 | - else 9 | = application_name 10 | = stylesheet_link_tag "application", :media => "all" 11 | = javascript_include_tag "application" 12 | = csrf_meta_tags 13 | %meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'} 14 | %body 15 | %header 16 | %nav.navbar.navbar-default.navbar-fixed-top{role: 'navigation'} 17 | .container-fluid 18 | .navbar-header 19 | %button.navbar-toggle{:'data-toggle' => 'collapse', :'data-target' => '#collapsed-nav'} 20 | %span.icon-bar 21 | %span.icon-bar 22 | %span.icon-bar 23 | %a.navbar-brand{href: '/'}= application_name 24 | .collapse.navbar-collapse#collapsed-nav 25 | %ul.nav.navbar-nav 26 | - if user_signed_in? 27 | %li= link_to 'Checklists', checklists_path 28 | - else 29 | %li= link_to 'Log in with GitHub', user_github_omniauth_authorize_path, method: :post 30 | 31 | .navbar-right 32 | - if user_signed_in? 33 | %ul.nav.navbar-nav 34 | %li 35 | = link_to current_user.github_profile_url, target: '_blank', class: 'avatar' do 36 | = image_tag current_user.avatar_url, :'data-toggle' => 'tooltip', :'data-placement' => 'bottom', title: current_user.email 37 | %li= link_to 'Sign Out', destroy_user_session_path, method: :delete 38 | 39 | .container-fluid 40 | .row 41 | .col-md-9.col-md-offset-1{role: 'main'} 42 | - flash.keys.each do |key| 43 | .alert{ class: "alert-#{rails_to_bootstrap_flash_key(key)}" } 44 | %a.close{ href: '#', "data-dismiss" => "alert" } x 45 | %h4.alert-heading= key.capitalize 46 | - if flash[key].respond_to?(:each) 47 | - flash[key].each do |msg| 48 | = msg 49 | %br 50 | - else 51 | = flash[key] 52 | - flash.delete(key) 53 | = yield 54 | = yield(:page_javascript) if content_for? :page_javascript 55 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. Others available are: 3 | # :confirmable, :lockable, :timeoutable and :omniauthable 4 | devise :database_authenticatable, 5 | :recoverable, :rememberable, :trackable, :validatable, 6 | :omniauthable, :omniauth_providers => [:github] 7 | 8 | attribute_method_suffix '_previously_changed?' 9 | 10 | has_many :identities, dependent: :destroy 11 | has_many :checklists, foreign_key: :created_by_id 12 | has_many :checklist_items, foreign_key: :created_by_id 13 | 14 | def just_created? 15 | id_previously_changed? 16 | end 17 | 18 | def self.find_or_create_from_omniauth!(omniauth) 19 | if identity = Identity.find_by_omniauth(omniauth) 20 | identity.update_from_omniauth!(omniauth) 21 | identity.user 22 | else 23 | transaction do 24 | create! do |u| 25 | u.email = omniauth.info.email 26 | u.password = SecureRandom.hex(64) 27 | end.tap do |u| 28 | u.identities.link!(omniauth) 29 | end 30 | end 31 | end.tap do |u| 32 | u.sync_accessible_repositories 33 | end 34 | end 35 | 36 | def update_from_omniauth!(omniauth) 37 | identities.find_by_omniauth!(omniauth).update_from_omniauth!(omniauth) 38 | end 39 | 40 | def github_client 41 | github_identity.client 42 | end 43 | 44 | def sync_accessible_repositories 45 | repos = github_client.repositories_with_write_access 46 | GithubRepository.find_or_create_all(repos) 47 | update!(accessible_github_repository_ids: repos.map(&:id)) 48 | end 49 | handle_asynchronously :sync_accessible_repositories 50 | 51 | def accessible_github_repositories 52 | GithubRepository.where(github_id: accessible_github_repository_ids) 53 | end 54 | 55 | def accessible_checklists 56 | Checklist.for_repositories(accessible_github_repositories) 57 | end 58 | 59 | def can_access_repository?(github_repository) 60 | accessible_github_repository_ids.map(&:to_s).include?(github_repository.github_id.to_s) 61 | end 62 | 63 | def avatar_url 64 | github_identity.image_url 65 | end 66 | 67 | def github_profile_url 68 | github_identity.profile_url 69 | end 70 | 71 | private 72 | 73 | def github_identity 74 | identities.find_by_provider!('github') 75 | end 76 | 77 | def attribute_previously_changed?(attr) 78 | previous_changes.keys.include?(attr.to_s) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV['RAILS_ENV'] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path('../../config/environment', __FILE__) 5 | require 'rspec/rails' 6 | # Add additional requires below this line. Rails is not loaded until this point! 7 | 8 | # Requires supporting ruby files with custom matchers and macros, etc, in 9 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 10 | # run as spec files by default. This means that files in spec/support that end 11 | # in _spec.rb will both be required and run as specs, causing the specs to be 12 | # run twice. It is recommended that you do not name files matching this glob to 13 | # end with _spec.rb. You can configure this pattern with the --pattern 14 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 15 | # 16 | # The following line is provided for convenience purposes. It has the downside 17 | # of increasing the boot-up time by auto-requiring all files in the support 18 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 19 | # require only the support files necessary. 20 | # 21 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 22 | 23 | # Checks for pending migrations before tests are run. 24 | # If you are not using ActiveRecord, you can remove this line. 25 | ActiveRecord::Migration.maintain_test_schema! 26 | 27 | RSpec.configure do |config| 28 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 29 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 30 | 31 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 32 | # examples within a transaction, remove the following line or assign false 33 | # instead of true. 34 | config.use_transactional_fixtures = true 35 | 36 | # RSpec Rails can automatically mix in different behaviours to your tests 37 | # based on their file location, for example enabling you to call `get` and 38 | # `post` in specs under `spec/controllers`. 39 | # 40 | # You can disable this behaviour by removing the line below, and instead 41 | # explicitly tag your specs with their type, e.g.: 42 | # 43 | # RSpec.describe UsersController, :type => :controller do 44 | # # ... 45 | # end 46 | # 47 | # The different available types are documented in the features, such as in 48 | # https://relishapp.com/rspec/rspec-rails/docs 49 | config.infer_spec_type_from_file_location! 50 | end 51 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | config.action_mailer.default_url_options = { host: ENV.fetch('CANONICAL_DOMAIN'), protocol: ENV.fetch('URL_PROTOCOL') } 3 | # Settings specified here will take precedence over those in config/application.rb. 4 | 5 | # In the development environment your application's code is reloaded on 6 | # every request. This slows down response time but is perfect for development 7 | # since you don't have to restart the web server when you make code changes. 8 | config.cache_classes = false 9 | 10 | # Do not eager load code on boot. 11 | config.eager_load = false 12 | 13 | # Show full error reports. 14 | config.consider_all_requests_local = true 15 | 16 | # Enable/disable caching. By default caching is disabled. 17 | # Run rails dev:cache to toggle caching. 18 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 19 | config.action_controller.perform_caching = true 20 | 21 | config.cache_store = :memory_store 22 | config.public_file_server.headers = { 23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 24 | } 25 | else 26 | config.action_controller.perform_caching = false 27 | 28 | config.cache_store = :null_store 29 | end 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options) 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | config.action_mailer.perform_caching = false 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise an error on page load if there are pending migrations. 43 | config.active_record.migration_error = :page_load 44 | 45 | # Highlight code that triggered database queries in logs. 46 | config.active_record.verbose_query_logs = true 47 | 48 | # Debug mode disables concatenation and preprocessing of assets. 49 | # This option may cause significant delays in view rendering with a large 50 | # number of complex assets. 51 | config.assets.debug = true 52 | 53 | # Suppress logger output for asset requests. 54 | config.assets.quiet = true 55 | 56 | # Raises error for missing translations 57 | # config.action_view.raise_on_missing_translations = true 58 | 59 | # Use an evented file watcher to asynchronously detect changes in source code, 60 | # routes, locales, etc. This feature depends on the listen gem. 61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 62 | end 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preflight 2 | 3 | As software projects and teams grow, it gets more and more difficult for engineers to remember the things they should consider when making changes to the code. This can be partially mitigated by a comprehensive test suite, which will (in many cases) tell engineers when they've broken existing functionality, but a test suite falls short of capturing (at least) the following problems: introduction of security holes, performance regressions, visual bugs and regressions, UX issues, failure to mobile optimize certain pages, and more. 4 | 5 | Preflight brings to software development what preflight checklists provided for airline safety: a list of things that every engineer has to think about before marking a pull request as "ready for review". With preflight, you can create checklists for repositories, and, when engineers create new pull requests, the checklists are automatically added to the pull request description. 6 | 7 | Unlike testing, preflight checklists won't "break" if someone doesn't look at them. Instead, they are ways of helping engineers avoid common mistakes and regressions. How you use them is up to you and what works best for your team. For certain checklists, you might want to require that engineers "check off" each item to verify that they've done it, like you might want a mechanic to "check off" that they've verified the engine on a plane. For other checklists, you might simply want to list a bunch of things for the engineer to consider, like a list of supported browsers, and leave it up to the engineer's discretion on what he/she thinks is worth checking. 8 | 9 | ### LICENSE 10 | 11 | Copyright (c) 2015 Andrew Warner 12 | 13 | MIT License 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining 16 | a copy of this software and associated documentation files (the 17 | "Software"), to deal in the Software without restriction, including 18 | without limitation the rights to use, copy, modify, merge, publish, 19 | distribute, sublicense, and/or sell copies of the Software, and to 20 | permit persons to whom the Software is furnished to do so, subject to 21 | the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be 24 | included in all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 30 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 31 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 32 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | 35 | #### Running using DOCKER 36 | 37 | ##### Create an .env file 38 | ``` 39 | CANONICAL_DOMAIN=localhost:3000 40 | URL_PROTOCOL=http 41 | DB_HOST=db 42 | POSTGRES_USER=* 43 | POSTGRES_PASSWORD=* 44 | POSTGRES_DB=* 45 | NEW_RELIC_ENABLED=false 46 | ``` 47 | 48 | ##### Build the containers 49 | `docker compose build` 50 | 51 | ##### Run the migrations 52 | `docker compose run web bin/rake db:migrate` 53 | 54 | ##### Generate the static files 55 | `docker compose run web bin/rails generate bootstrap:install static --no-coffeescript` 56 | 57 | 58 | ##### Start the server 59 | `docker compose up` -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | config.action_mailer.default_url_options = { host: ENV.fetch('CANONICAL_DOMAIN'), protocol: ENV.fetch('URL_PROTOCOL') } 3 | # Settings specified here will take precedence over those in config/application.rb. 4 | 5 | # Code is not reloaded between requests. 6 | config.cache_classes = true 7 | 8 | # Eager load code on boot. This eager loads most of Rails and 9 | # your application in memory, allowing both threaded web servers 10 | # and those relying on copy on write to perform better. 11 | # Rake tasks automatically ignore this option for performance. 12 | config.eager_load = true 13 | 14 | # Full error reports are disabled and caching is turned on. 15 | config.consider_all_requests_local = false 16 | config.action_controller.perform_caching = true 17 | 18 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 19 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 20 | # config.require_master_key = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # Compress JavaScripts and CSS. 27 | config.assets.js_compressor = :uglifier 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.action_controller.asset_host = 'http://assets.example.com' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 41 | 42 | # Store uploaded files on the local file system (see config/storage.yml for options) 43 | config.active_storage.service = :local 44 | 45 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 46 | # config.force_ssl = true 47 | 48 | # Use the lowest log level to ensure availability of diagnostic information 49 | # when problems arise. 50 | config.log_level = :debug 51 | 52 | # Prepend all log lines with the following tags. 53 | config.log_tags = [ :request_id ] 54 | 55 | # Use a different cache store in production. 56 | # config.cache_store = :mem_cache_store 57 | 58 | # Use a real queuing backend for Active Job (and separate queues per environment) 59 | # config.active_job.queue_adapter = :resque 60 | # config.active_job.queue_name_prefix = "preflight_#{Rails.env}" 61 | 62 | config.action_mailer.perform_caching = false 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation cannot be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Use default logging formatter so that PID and timestamp are not suppressed. 76 | config.log_formatter = ::Logger::Formatter.new 77 | 78 | # Use a different logger for distributed setups. 79 | # require 'syslog/logger' 80 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 81 | 82 | if ENV["RAILS_LOG_TO_STDOUT"].present? 83 | logger = ActiveSupport::Logger.new(STDOUT) 84 | logger.formatter = config.log_formatter 85 | config.logger = ActiveSupport::TaggedLogging.new(logger) 86 | end 87 | 88 | # Do not dump schema after migrations. 89 | config.active_record.dump_schema_after_migration = false 90 | end 91 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | omniauth_callbacks: 27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 28 | success: "Successfully authenticated from %{kind} account." 29 | passwords: 30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 33 | updated: "Your password has been changed successfully. You are now signed in." 34 | updated_not_active: "Your password has been changed successfully." 35 | registrations: 36 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 37 | signed_up: "Welcome! You have signed up successfully." 38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 42 | updated: "Your account has been updated successfully." 43 | sessions: 44 | signed_in: "Signed in successfully." 45 | signed_out: "Signed out successfully." 46 | already_signed_out: "Signed out successfully." 47 | unlocks: 48 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 49 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 50 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 51 | errors: 52 | messages: 53 | already_confirmed: "was already confirmed, please try signing in" 54 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 55 | expired: "has expired, please request a new one" 56 | not_found: "not found" 57 | not_locked: "was not locked" 58 | not_saved: 59 | one: "1 error prohibited this %{resource} from being saved:" 60 | other: "%{count} errors prohibited this %{resource} from being saved:" 61 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # The settings below are suggested to provide a good initial experience 44 | # with RSpec, but feel free to customize to your heart's content. 45 | =begin 46 | # These two settings work together to allow you to limit a spec run 47 | # to individual examples or groups you care about by tagging them with 48 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 49 | # get run. 50 | config.filter_run :focus 51 | config.run_all_when_everything_filtered = true 52 | 53 | # Limits the available syntax to the non-monkey patched syntax that is 54 | # recommended. For more details, see: 55 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 56 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 57 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 58 | config.disable_monkey_patching! 59 | 60 | # Many RSpec users commonly either run the entire suite or an individual 61 | # file, and it's useful to allow more verbose output when running an 62 | # individual spec file. 63 | if config.files_to_run.one? 64 | # Use the documentation formatter for detailed output, 65 | # unless a formatter has already been configured 66 | # (e.g. via a command-line flag). 67 | config.default_formatter = 'doc' 68 | end 69 | 70 | # Print the 10 slowest examples and example groups at the 71 | # end of the spec run, to help surface which specs are running 72 | # particularly slow. 73 | config.profile_examples = 10 74 | 75 | # Run specs in random order to surface order dependencies. If you find an 76 | # order dependency and want to debug it, you can fix the order by providing 77 | # the seed, which is printed after each run. 78 | # --seed 1234 79 | config.order = :random 80 | 81 | # Seed global randomization in this process using the `--seed` CLI option. 82 | # Setting this allows you to use `--seed` to deterministically reproduce 83 | # test failures related to randomization by passing the same `--seed` value 84 | # as the one that triggered the failure. 85 | Kernel.srand config.seed 86 | =end 87 | end 88 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2024_11_06_203538) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "applied_checklists", id: :serial, force: :cascade do |t| 19 | t.integer "checklist_id", null: false 20 | t.bigint "github_pull_request_id", null: false 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | t.index ["checklist_id"], name: "index_applied_checklists_on_checklist_id" 24 | t.index ["github_pull_request_id", "checklist_id"], name: "one_checklist_application_per_pull", unique: true 25 | end 26 | 27 | create_table "checklist_items", id: :serial, force: :cascade do |t| 28 | t.string "name", null: false 29 | t.integer "checklist_id", null: false 30 | t.datetime "created_at", null: false 31 | t.datetime "updated_at", null: false 32 | t.integer "created_by_id", null: false 33 | end 34 | 35 | create_table "checklists", id: :serial, force: :cascade do |t| 36 | t.string "name", null: false 37 | t.datetime "created_at", null: false 38 | t.datetime "updated_at", null: false 39 | t.integer "created_by_id", null: false 40 | t.integer "github_repository_id", null: false 41 | t.string "with_file_matching_pattern" 42 | t.integer "last_updated_by_id", null: false 43 | t.index ["github_repository_id"], name: "index_checklists_on_github_repository_id" 44 | end 45 | 46 | create_table "delayed_jobs", id: :serial, force: :cascade do |t| 47 | t.integer "priority", default: 0, null: false 48 | t.integer "attempts", default: 0, null: false 49 | t.text "handler", null: false 50 | t.text "last_error" 51 | t.datetime "run_at" 52 | t.datetime "locked_at" 53 | t.datetime "failed_at" 54 | t.string "locked_by" 55 | t.string "queue" 56 | t.datetime "created_at" 57 | t.datetime "updated_at" 58 | t.index ["priority", "run_at"], name: "delayed_jobs_priority" 59 | end 60 | 61 | create_table "github_repositories", id: :serial, force: :cascade do |t| 62 | t.integer "github_id", null: false 63 | t.string "github_full_name", null: false 64 | t.string "github_owner_type", null: false 65 | t.string "github_url", null: false 66 | t.datetime "created_at", null: false 67 | t.datetime "updated_at", null: false 68 | t.index ["github_id"], name: "index_github_repositories_on_github_id", unique: true 69 | end 70 | 71 | create_table "github_webhooks", id: :serial, force: :cascade do |t| 72 | t.integer "github_id", null: false 73 | t.integer "github_repository_id", null: false 74 | t.integer "created_by_id", null: false 75 | t.datetime "created_at", null: false 76 | t.datetime "updated_at", null: false 77 | t.datetime "hook_deleted_at" 78 | t.index ["github_id"], name: "index_github_webhooks_on_github_id" 79 | t.index ["github_repository_id"], name: "index_github_webhooks_on_github_repository_id", unique: true 80 | end 81 | 82 | create_table "identities", id: :serial, force: :cascade do |t| 83 | t.string "provider", null: false 84 | t.integer "user_id", null: false 85 | t.string "uid", null: false 86 | t.text "omniauth_data", null: false 87 | t.datetime "created_at", null: false 88 | t.datetime "updated_at", null: false 89 | t.index ["uid", "provider"], name: "index_identities_on_uid_and_provider", unique: true 90 | t.index ["user_id", "provider"], name: "index_identities_on_user_id_and_provider", unique: true 91 | end 92 | 93 | create_table "users", id: :serial, force: :cascade do |t| 94 | t.string "email", default: "", null: false 95 | t.string "encrypted_password", default: "", null: false 96 | t.string "reset_password_token" 97 | t.datetime "reset_password_sent_at" 98 | t.datetime "remember_created_at" 99 | t.integer "sign_in_count", default: 0, null: false 100 | t.datetime "current_sign_in_at" 101 | t.datetime "last_sign_in_at" 102 | t.inet "current_sign_in_ip" 103 | t.inet "last_sign_in_ip" 104 | t.datetime "created_at" 105 | t.datetime "updated_at" 106 | t.boolean "admin", default: false, null: false 107 | t.text "accessible_github_repository_ids", default: [], array: true 108 | t.index ["email"], name: "index_users_on_email", unique: true 109 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.2.4.3) 5 | actionpack (= 5.2.4.3) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | actionmailer (5.2.4.3) 9 | actionpack (= 5.2.4.3) 10 | actionview (= 5.2.4.3) 11 | activejob (= 5.2.4.3) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.2.4.3) 15 | actionview (= 5.2.4.3) 16 | activesupport (= 5.2.4.3) 17 | rack (~> 2.0, >= 2.0.8) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.2.4.3) 22 | activesupport (= 5.2.4.3) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.2.4.3) 28 | activesupport (= 5.2.4.3) 29 | globalid (>= 0.3.6) 30 | activemodel (5.2.4.3) 31 | activesupport (= 5.2.4.3) 32 | activerecord (5.2.4.3) 33 | activemodel (= 5.2.4.3) 34 | activesupport (= 5.2.4.3) 35 | arel (>= 9.0) 36 | activestorage (5.2.4.3) 37 | actionpack (= 5.2.4.3) 38 | activerecord (= 5.2.4.3) 39 | marcel (~> 0.3.1) 40 | activesupport (5.2.4.3) 41 | concurrent-ruby (~> 1.0, >= 1.0.2) 42 | i18n (>= 0.7, < 2) 43 | minitest (~> 5.1) 44 | tzinfo (~> 1.1) 45 | addressable (2.5.2) 46 | public_suffix (>= 2.0.2, < 4.0) 47 | arel (9.0.0) 48 | bcrypt (3.1.13) 49 | builder (3.2.4) 50 | coderay (1.1.3) 51 | coffee-rails (4.2.2) 52 | coffee-script (>= 2.2.0) 53 | railties (>= 4.0.0) 54 | coffee-script (2.4.1) 55 | coffee-script-source 56 | execjs 57 | coffee-script-source (1.12.2) 58 | commonjs (0.2.7) 59 | concurrent-ruby (1.1.7) 60 | crass (1.0.6) 61 | daemons (1.3.1) 62 | decent_exposure (3.0.2) 63 | activesupport (>= 4.0) 64 | delayed_job (4.1.8) 65 | activesupport (>= 3.0, < 6.1) 66 | delayed_job_active_record (4.1.4) 67 | activerecord (>= 3.0, < 6.1) 68 | delayed_job (>= 3.0, < 5) 69 | delayed_job_web (1.4.3) 70 | activerecord (> 3.0.0) 71 | delayed_job (> 2.0.3) 72 | rack-protection (>= 1.5.5) 73 | sinatra (>= 1.4.4) 74 | devise (4.7.1) 75 | bcrypt (~> 3.0) 76 | orm_adapter (~> 0.1) 77 | railties (>= 4.1.0) 78 | responders 79 | warden (~> 1.2.3) 80 | diff-lcs (1.4.4) 81 | dotenv (1.0.2) 82 | erubi (1.9.0) 83 | erubis (2.7.0) 84 | eventmachine (1.2.7) 85 | execjs (2.7.0) 86 | factory_girl (4.5.0) 87 | activesupport (>= 3.0.0) 88 | faraday (0.17.3) 89 | multipart-post (>= 1.2, < 3) 90 | ffi (1.9.25) 91 | fivemat (1.3.1) 92 | foreman (0.77.0) 93 | dotenv (~> 1.0.2) 94 | thor (~> 0.19.1) 95 | globalid (0.4.2) 96 | activesupport (>= 4.2.0) 97 | grease (0.3.1) 98 | haml (5.0.4) 99 | temple (>= 0.8.0) 100 | tilt 101 | haml-rails (1.0.0) 102 | actionpack (>= 4.0.1) 103 | activesupport (>= 4.0.1) 104 | haml (>= 4.0.6, < 6.0) 105 | html2haml (>= 1.0.1) 106 | railties (>= 4.0.1) 107 | hashie (4.1.0) 108 | html2haml (2.2.0) 109 | erubis (~> 2.7.0) 110 | haml (>= 4.0, < 6) 111 | nokogiri (>= 1.6.0) 112 | ruby_parser (~> 3.5) 113 | i18n (1.8.5) 114 | concurrent-ruby (~> 1.0) 115 | jquery-rails (4.3.1) 116 | rails-dom-testing (>= 1, < 3) 117 | railties (>= 4.2.0) 118 | thor (>= 0.14, < 2.0) 119 | json (2.3.1) 120 | jwt (2.2.2) 121 | kgio (2.9.3) 122 | less (2.6.0) 123 | commonjs (~> 0.2.7) 124 | less-rails (3.0.0) 125 | actionpack (>= 4.0) 126 | grease 127 | less (~> 2.6.0) 128 | sprockets (> 2, < 4) 129 | tilt 130 | listen (3.2.1) 131 | rb-fsevent (~> 0.10, >= 0.10.3) 132 | rb-inotify (~> 0.9, >= 0.9.10) 133 | loofah (2.6.0) 134 | crass (~> 1.0.2) 135 | nokogiri (>= 1.5.9) 136 | mail (2.7.1) 137 | mini_mime (>= 0.1.1) 138 | marcel (0.3.3) 139 | mimemagic (~> 0.3.2) 140 | method_source (1.0.0) 141 | mimemagic (0.3.10) 142 | nokogiri (~> 1) 143 | rake 144 | mini_mime (1.0.2) 145 | mini_portile2 (2.5.1) 146 | minitest (5.14.1) 147 | multi_json (1.15.0) 148 | multi_xml (0.6.0) 149 | multipart-post (2.1.1) 150 | mustermann (1.1.1) 151 | ruby2_keywords (~> 0.0.1) 152 | newrelic_rpm (6.12.0.367) 153 | nio4r (2.5.2) 154 | nokogiri (1.11.4) 155 | mini_portile2 (~> 2.5.0) 156 | racc (~> 1.4) 157 | oauth2 (1.4.4) 158 | faraday (>= 0.8, < 2.0) 159 | jwt (>= 1.0, < 3.0) 160 | multi_json (~> 1.3) 161 | multi_xml (~> 0.5) 162 | rack (>= 1.2, < 3) 163 | octokit (4.13.0) 164 | sawyer (~> 0.8.0, >= 0.5.3) 165 | omniauth (1.9.1) 166 | hashie (>= 3.4.6) 167 | rack (>= 1.6.2, < 3) 168 | omniauth-github (1.4.0) 169 | omniauth (~> 1.5) 170 | omniauth-oauth2 (>= 1.4.0, < 2.0) 171 | omniauth-oauth2 (1.7.0) 172 | oauth2 (~> 1.4) 173 | omniauth (~> 1.9) 174 | orm_adapter (0.5.0) 175 | pg (0.18.1) 176 | pry (0.13.1) 177 | coderay (~> 1.1) 178 | method_source (~> 1.0) 179 | pry-rails (0.3.9) 180 | pry (>= 0.10.4) 181 | public_suffix (3.0.3) 182 | racc (1.5.2) 183 | rack (2.2.3) 184 | rack-protection (2.0.8.1) 185 | rack 186 | rack-test (1.1.0) 187 | rack (>= 1.0, < 3) 188 | rack-timeout (0.2.1) 189 | rails (5.2.4.3) 190 | actioncable (= 5.2.4.3) 191 | actionmailer (= 5.2.4.3) 192 | actionpack (= 5.2.4.3) 193 | actionview (= 5.2.4.3) 194 | activejob (= 5.2.4.3) 195 | activemodel (= 5.2.4.3) 196 | activerecord (= 5.2.4.3) 197 | activestorage (= 5.2.4.3) 198 | activesupport (= 5.2.4.3) 199 | bundler (>= 1.3.0) 200 | railties (= 5.2.4.3) 201 | sprockets-rails (>= 2.0.0) 202 | rails-dom-testing (2.0.3) 203 | activesupport (>= 4.2.0) 204 | nokogiri (>= 1.6) 205 | rails-html-sanitizer (1.3.0) 206 | loofah (~> 2.3) 207 | railties (5.2.4.3) 208 | actionpack (= 5.2.4.3) 209 | activesupport (= 5.2.4.3) 210 | method_source 211 | rake (>= 0.8.7) 212 | thor (>= 0.19.0, < 2.0) 213 | raindrops (0.13.0) 214 | rake (12.3.3) 215 | rb-fsevent (0.10.3) 216 | rb-inotify (0.9.10) 217 | ffi (>= 0.5.0, < 2) 218 | responders (2.4.1) 219 | actionpack (>= 4.2.0, < 6.0) 220 | railties (>= 4.2.0, < 6.0) 221 | rspec (3.2.0) 222 | rspec-core (~> 3.2.0) 223 | rspec-expectations (~> 3.2.0) 224 | rspec-mocks (~> 3.2.0) 225 | rspec-core (3.2.3) 226 | rspec-support (~> 3.2.0) 227 | rspec-expectations (3.2.1) 228 | diff-lcs (>= 1.2.0, < 2.0) 229 | rspec-support (~> 3.2.0) 230 | rspec-mocks (3.2.1) 231 | diff-lcs (>= 1.2.0, < 2.0) 232 | rspec-support (~> 3.2.0) 233 | rspec-rails (1.3.2) 234 | rack (>= 1.0.0) 235 | rspec (>= 1.3.0) 236 | rspec-support (3.2.2) 237 | ruby2_keywords (0.0.2) 238 | ruby_parser (3.11.0) 239 | sexp_processor (~> 4.9) 240 | sass (3.5.6) 241 | sass-listen (~> 4.0.0) 242 | sass-listen (4.0.0) 243 | rb-fsevent (~> 0.9, >= 0.9.4) 244 | rb-inotify (~> 0.9, >= 0.9.7) 245 | sass-rails (5.0.7) 246 | railties (>= 4.0.0, < 6) 247 | sass (~> 3.1) 248 | sprockets (>= 2.8, < 4.0) 249 | sprockets-rails (>= 2.0, < 4.0) 250 | tilt (>= 1.1, < 3) 251 | sawyer (0.8.1) 252 | addressable (>= 2.3.5, < 2.6) 253 | faraday (~> 0.8, < 1.0) 254 | sexp_processor (4.10.1) 255 | shoulda-matchers (2.8.0) 256 | activesupport (>= 3.0.0) 257 | sinatra (2.0.8.1) 258 | mustermann (~> 1.0) 259 | rack (~> 2.0) 260 | rack-protection (= 2.0.8.1) 261 | tilt (~> 2.0) 262 | sprockets (3.7.2) 263 | concurrent-ruby (~> 1.0) 264 | rack (> 1, < 3) 265 | sprockets-rails (3.2.1) 266 | actionpack (>= 4.0) 267 | activesupport (>= 4.0) 268 | sprockets (>= 3.0.0) 269 | temple (0.8.0) 270 | thin (1.7.2) 271 | daemons (~> 1.0, >= 1.0.9) 272 | eventmachine (~> 1.0, >= 1.0.4) 273 | rack (>= 1, < 3) 274 | thor (0.19.4) 275 | thread_safe (0.3.6) 276 | tilt (2.0.10) 277 | turbolinks (5.2.1) 278 | turbolinks-source (~> 5.2) 279 | turbolinks-source (5.2.0) 280 | twitter-bootstrap-rails (3.2.2) 281 | actionpack (>= 3.1) 282 | execjs (>= 2.2.2, >= 2.2) 283 | less-rails (>= 2.5.0) 284 | railties (>= 3.1) 285 | tzinfo (1.2.7) 286 | thread_safe (~> 0.1) 287 | uglifier (2.7.0) 288 | execjs (>= 0.3.0) 289 | json (>= 1.8.0) 290 | unicorn (4.8.3) 291 | kgio (~> 2.6) 292 | rack 293 | raindrops (~> 0.7) 294 | warden (1.2.7) 295 | rack (>= 1.0) 296 | websocket-driver (0.7.3) 297 | websocket-extensions (>= 0.1.0) 298 | websocket-extensions (0.1.5) 299 | 300 | PLATFORMS 301 | ruby 302 | 303 | DEPENDENCIES 304 | coffee-rails 305 | decent_exposure 306 | delayed_job 307 | delayed_job_active_record 308 | delayed_job_web 309 | devise 310 | dotenv 311 | factory_girl 312 | fivemat 313 | foreman 314 | haml 315 | haml-rails 316 | jquery-rails 317 | listen 318 | newrelic_rpm 319 | nokogiri (~> 1.11.4) 320 | octokit 321 | omniauth 322 | omniauth-github 323 | pg 324 | pry 325 | pry-rails 326 | rack-timeout 327 | rails (~> 5.2.4.3) 328 | rake (< 13.0) 329 | rspec 330 | rspec-rails 331 | sass-rails 332 | shoulda-matchers 333 | thin 334 | turbolinks 335 | twitter-bootstrap-rails 336 | uglifier (>= 1.3.0) 337 | unicorn 338 | 339 | RUBY VERSION 340 | ruby 2.5.8p224 341 | 342 | BUNDLED WITH 343 | 1.17.3 344 | -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python and Node applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated March 17, 2015 7 | # 8 | # This configuration file is custom generated for app34618591@heroku.com 9 | 10 | 11 | # Here are the settings that are common to all environments 12 | common: &default_settings 13 | # ============================== LICENSE KEY =============================== 14 | 15 | # You must specify the license key associated with your New Relic 16 | # account. This key binds your Agent's data to your account in the 17 | # New Relic service. 18 | license_key: '<%= ENV["NEW_RELIC_LICENSE_KEY"] %>' 19 | 20 | # Agent Enabled (Ruby/Rails Only) 21 | # Use this setting to force the agent to run or not run. 22 | # Default is 'auto' which means the agent will install and run only 23 | # if a valid dispatcher such as Mongrel is running. This prevents 24 | # it from running with Rake or the console. Set to false to 25 | # completely turn the agent off regardless of the other settings. 26 | # Valid values are true, false and auto. 27 | # 28 | agent_enabled: <%= ENV.fetch("NEW_RELIC_ENABLED", auto) %> 29 | 30 | # Application Name Set this to be the name of your application as 31 | # you'd like it show up in New Relic. The service will then auto-map 32 | # instances of your application into an "application" on your 33 | # dashboard page. If you want to map this instance into multiple 34 | # apps, like "AJAX Requests" and "All UI" then specify a semicolon 35 | # separated list of up to three distinct names, or a yaml list. 36 | # Defaults to the capitalized RAILS_ENV or RACK_ENV (i.e., 37 | # Production, Staging, etc) 38 | # 39 | # Example: 40 | # 41 | # app_name: 42 | # - Ajax Service 43 | # - All Services 44 | # 45 | # Caution: If you change this name, a new application will appear in the New 46 | # Relic user interface with the new name, and data will stop reporting to the 47 | # app with the old name. 48 | # 49 | # See https://newrelic.com/docs/site/renaming-applications for more details 50 | # on renaming your New Relic applications. 51 | # 52 | app_name: Preflight 53 | 54 | # When "true", the agent collects performance data about your 55 | # application and reports this data to the New Relic service at 56 | # newrelic.com. This global switch is normally overridden for each 57 | # environment below. (formerly called 'enabled') 58 | monitor_mode: true 59 | 60 | # Developer mode should be off in every environment but 61 | # development as it has very high overhead in memory. 62 | developer_mode: false 63 | 64 | # The newrelic agent generates its own log file to keep its logging 65 | # information separate from that of your application. Specify its 66 | # log level here. 67 | log_level: info 68 | 69 | # Optionally set the path to the log file This is expanded from the 70 | # root directory (may be relative or absolute, e.g. 'log/' or 71 | # '/var/log/') The agent will attempt to create this directory if it 72 | # does not exist. 73 | # log_file_path: 'log' 74 | 75 | # Optionally set the name of the log file, defaults to 'newrelic_agent.log' 76 | # log_file_name: 'newrelic_agent.log' 77 | 78 | # The newrelic agent communicates with the service via https by default. This 79 | # prevents eavesdropping on the performance metrics transmitted by the agent. 80 | # The encryption required by SSL introduces a nominal amount of CPU overhead, 81 | # which is performed asynchronously in a background thread. If you'd prefer 82 | # to send your metrics over http uncomment the following line. 83 | # ssl: false 84 | 85 | #============================== Browser Monitoring =============================== 86 | # New Relic Real User Monitoring gives you insight into the performance real users are 87 | # experiencing with your website. This is accomplished by measuring the time it takes for 88 | # your users' browsers to download and render your web pages by injecting a small amount 89 | # of JavaScript code into the header and footer of each page. 90 | browser_monitoring: 91 | # By default the agent automatically injects the monitoring JavaScript 92 | # into web pages. Set this attribute to false to turn off this behavior. 93 | auto_instrument: true 94 | 95 | # Proxy settings for connecting to the New Relic server. 96 | # 97 | # If a proxy is used, the host setting is required. Other settings 98 | # are optional. Default port is 8080. 99 | # 100 | # proxy_host: hostname 101 | # proxy_port: 8080 102 | # proxy_user: 103 | # proxy_pass: 104 | 105 | # The agent can optionally log all data it sends to New Relic servers to a 106 | # separate log file for human inspection and auditing purposes. To enable this 107 | # feature, change 'enabled' below to true. 108 | # See: https://newrelic.com/docs/ruby/audit-log 109 | audit_log: 110 | enabled: false 111 | 112 | # Tells transaction tracer and error collector (when enabled) 113 | # whether or not to capture HTTP params. When true, frameworks can 114 | # exclude HTTP parameters from being captured. 115 | # Rails: the RoR filter_parameter_logging excludes parameters 116 | # Java: create a config setting called "ignored_params" and set it to 117 | # a comma separated list of HTTP parameter names. 118 | # ex: ignored_params: credit_card, ssn, password 119 | capture_params: false 120 | 121 | # Transaction tracer captures deep information about slow 122 | # transactions and sends this to the New Relic service once a 123 | # minute. Included in the transaction is the exact call sequence of 124 | # the transactions including any SQL statements issued. 125 | transaction_tracer: 126 | 127 | # Transaction tracer is enabled by default. Set this to false to 128 | # turn it off. This feature is only available at the Professional 129 | # and above product levels. 130 | enabled: true 131 | 132 | # Threshold in seconds for when to collect a transaction 133 | # trace. When the response time of a controller action exceeds 134 | # this threshold, a transaction trace will be recorded and sent to 135 | # New Relic. Valid values are any float value, or (default) "apdex_f", 136 | # which will use the threshold for an dissatisfying Apdex 137 | # controller action - four times the Apdex T value. 138 | transaction_threshold: apdex_f 139 | 140 | # When transaction tracer is on, SQL statements can optionally be 141 | # recorded. The recorder has three modes, "off" which sends no 142 | # SQL, "raw" which sends the SQL statement in its original form, 143 | # and "obfuscated", which strips out numeric and string literals. 144 | record_sql: obfuscated 145 | 146 | # Threshold in seconds for when to collect stack trace for a SQL 147 | # call. In other words, when SQL statements exceed this threshold, 148 | # then capture and send to New Relic the current stack trace. This is 149 | # helpful for pinpointing where long SQL calls originate from. 150 | stack_trace_threshold: 0.500 151 | 152 | # Determines whether the agent will capture query plans for slow 153 | # SQL queries. Only supported in mysql and postgres. Should be 154 | # set to false when using other adapters. 155 | # explain_enabled: true 156 | 157 | # Threshold for query execution time below which query plans will 158 | # not be captured. Relevant only when `explain_enabled` is true. 159 | # explain_threshold: 0.5 160 | 161 | # Error collector captures information about uncaught exceptions and 162 | # sends them to New Relic for viewing 163 | error_collector: 164 | 165 | # Error collector is enabled by default. Set this to false to turn 166 | # it off. This feature is only available at the Professional and above 167 | # product levels. 168 | enabled: true 169 | 170 | # To stop specific errors from reporting to New Relic, set this property 171 | # to comma-separated values. Default is to ignore routing errors, 172 | # which are how 404's get triggered. 173 | ignore_errors: "ActionController::RoutingError,Sinatra::NotFound" 174 | 175 | # If you're interested in capturing memcache keys as though they 176 | # were SQL uncomment this flag. Note that this does increase 177 | # overhead slightly on every memcached call, and can have security 178 | # implications if your memcached keys are sensitive 179 | # capture_memcache_keys: true 180 | 181 | # Application Environments 182 | # ------------------------------------------ 183 | # Environment-specific settings are in this section. 184 | # For Rails applications, RAILS_ENV is used to determine the environment. 185 | # For Java applications, pass -Dnewrelic.environment to set 186 | # the environment. 187 | 188 | # NOTE if your application has other named environments, you should 189 | # provide newrelic configuration settings for these environments here. 190 | 191 | development: 192 | <<: *default_settings 193 | # Turn on communication to New Relic service in development mode 194 | monitor_mode: true 195 | app_name: My Application (Development) 196 | 197 | # Rails Only - when running in Developer Mode, the New Relic Agent will 198 | # present performance information on the last 100 transactions you have 199 | # executed since starting the mongrel. 200 | # NOTE: There is substantial overhead when running in developer mode. 201 | # Do not use for production or load testing. 202 | developer_mode: true 203 | 204 | test: 205 | <<: *default_settings 206 | # It almost never makes sense to turn on the agent when running 207 | # unit, functional or integration tests or the like. 208 | monitor_mode: false 209 | 210 | # Turn on the agent in production for 24x7 monitoring. NewRelic 211 | # testing shows an average performance impact of < 5 ms per 212 | # transaction, you can leave this on all the time without 213 | # incurring any user-visible performance degradation. 214 | production: 215 | <<: *default_settings 216 | monitor_mode: true 217 | 218 | # Many applications have a staging environment which behaves 219 | # identically to production. Support for that environment is provided 220 | # here. By default, the staging environment has the agent turned on. 221 | staging: 222 | <<: *default_settings 223 | monitor_mode: true 224 | app_name: My Application (Staging) 225 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | OmniAuth.config.allowed_request_methods = [:post] 2 | OmniAuth.config.before_request_phase = RequestForgeryProtectionTokenVerifier.new 3 | 4 | # Use this hook to configure devise mailer, warden hooks and so forth. 5 | # Many of these configuration options can be set straight in your model. 6 | Devise.setup do |config| 7 | # The secret key used by Devise. Devise uses this key to generate 8 | # random tokens. Changing this key will render invalid all existing 9 | # confirmation, reset password and unlock tokens in the database. 10 | # config.secret_key = 'a7c11422fdc5273df6241f996fdab3a889b4288806743848366995d5cf99ab33ec932dc7298ae1f5712891577bd8a5b501f3c165a68646ecb178b97529dd69bd' 11 | 12 | # ==> Mailer Configuration 13 | # Configure the e-mail address which will be shown in Devise::Mailer, 14 | # note that it will be overwritten if you use your own mailer class 15 | # with default "from" parameter. 16 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 17 | 18 | # Configure the class responsible to send e-mails. 19 | # config.mailer = 'Devise::Mailer' 20 | 21 | # ==> ORM configuration 22 | # Load and configure the ORM. Supports :active_record (default) and 23 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 24 | # available as additional gems. 25 | require 'devise/orm/active_record' 26 | 27 | # ==> Configuration for any authentication mechanism 28 | # Configure which keys are used when authenticating a user. The default is 29 | # just :email. You can configure it to use [:username, :subdomain], so for 30 | # authenticating a user, both parameters are required. Remember that those 31 | # parameters are used only when authenticating and not when retrieving from 32 | # session. If you need permissions, you should implement that in a before filter. 33 | # You can also supply a hash where the value is a boolean determining whether 34 | # or not authentication should be aborted when the value is not present. 35 | # config.authentication_keys = [ :email ] 36 | 37 | # Configure parameters from the request object used for authentication. Each entry 38 | # given should be a request method and it will automatically be passed to the 39 | # find_for_authentication method and considered in your model lookup. For instance, 40 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 41 | # The same considerations mentioned for authentication_keys also apply to request_keys. 42 | # config.request_keys = [] 43 | 44 | # Configure which authentication keys should be case-insensitive. 45 | # These keys will be downcased upon creating or modifying a user and when used 46 | # to authenticate or find a user. Default is :email. 47 | config.case_insensitive_keys = [ :email ] 48 | 49 | # Configure which authentication keys should have whitespace stripped. 50 | # These keys will have whitespace before and after removed upon creating or 51 | # modifying a user and when used to authenticate or find a user. Default is :email. 52 | config.strip_whitespace_keys = [ :email ] 53 | 54 | # Tell if authentication through request.params is enabled. True by default. 55 | # It can be set to an array that will enable params authentication only for the 56 | # given strategies, for example, `config.params_authenticatable = [:database]` will 57 | # enable it only for database (email + password) authentication. 58 | # config.params_authenticatable = true 59 | 60 | # Tell if authentication through HTTP Auth is enabled. False by default. 61 | # It can be set to an array that will enable http authentication only for the 62 | # given strategies, for example, `config.http_authenticatable = [:database]` will 63 | # enable it only for database authentication. The supported strategies are: 64 | # :database = Support basic authentication with authentication key + password 65 | # config.http_authenticatable = false 66 | 67 | # If 401 status code should be returned for AJAX requests. True by default. 68 | # config.http_authenticatable_on_xhr = true 69 | 70 | # The realm used in Http Basic Authentication. 'Application' by default. 71 | # config.http_authentication_realm = 'Application' 72 | 73 | # It will change confirmation, password recovery and other workflows 74 | # to behave the same regardless if the e-mail provided was right or wrong. 75 | # Does not affect registerable. 76 | # config.paranoid = true 77 | 78 | # By default Devise will store the user in session. You can skip storage for 79 | # particular strategies by setting this option. 80 | # Notice that if you are skipping storage for all authentication paths, you 81 | # may want to disable generating routes to Devise's sessions controller by 82 | # passing skip: :sessions to `devise_for` in your config/routes.rb 83 | config.skip_session_storage = [:http_auth] 84 | 85 | # By default, Devise cleans up the CSRF token on authentication to 86 | # avoid CSRF token fixation attacks. This means that, when using AJAX 87 | # requests for sign in and sign up, you need to get a new CSRF token 88 | # from the server. You can disable this option at your own risk. 89 | # config.clean_up_csrf_token_on_authentication = true 90 | 91 | # ==> Configuration for :database_authenticatable 92 | # For bcrypt, this is the cost for hashing the password and defaults to 10. If 93 | # using other encryptors, it sets how many times you want the password re-encrypted. 94 | # 95 | # Limiting the stretches to just one in testing will increase the performance of 96 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 97 | # a value less than 10 in other environments. Note that, for bcrypt (the default 98 | # encryptor), the cost increases exponentially with the number of stretches (e.g. 99 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 100 | config.stretches = Rails.env.test? ? 1 : 10 101 | 102 | # Setup a pepper to generate the encrypted password. 103 | # config.pepper = '75c4bff768ce8daf5031d05ba1e7f0dea2f0e7fd2f48fb51ee6e98366e501f8edda72ccdf9d78c9dd55e54031438c6e2d858cb45fa85eb22138d706e6d7c1fea' 104 | 105 | # ==> Configuration for :confirmable 106 | # A period that the user is allowed to access the website even without 107 | # confirming their account. For instance, if set to 2.days, the user will be 108 | # able to access the website for two days without confirming their account, 109 | # access will be blocked just in the third day. Default is 0.days, meaning 110 | # the user cannot access the website without confirming their account. 111 | # config.allow_unconfirmed_access_for = 2.days 112 | 113 | # A period that the user is allowed to confirm their account before their 114 | # token becomes invalid. For example, if set to 3.days, the user can confirm 115 | # their account within 3 days after the mail was sent, but on the fourth day 116 | # their account can't be confirmed with the token any more. 117 | # Default is nil, meaning there is no restriction on how long a user can take 118 | # before confirming their account. 119 | # config.confirm_within = 3.days 120 | 121 | # If true, requires any email changes to be confirmed (exactly the same way as 122 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 123 | # db field (see migrations). Until confirmed, new email is stored in 124 | # unconfirmed_email column, and copied to email column on successful confirmation. 125 | config.reconfirmable = true 126 | 127 | # Defines which key will be used when confirming an account 128 | # config.confirmation_keys = [ :email ] 129 | 130 | # ==> Configuration for :rememberable 131 | # The time the user will be remembered without asking for credentials again. 132 | # config.remember_for = 2.weeks 133 | 134 | # Invalidates all the remember me tokens when the user signs out. 135 | config.expire_all_remember_me_on_sign_out = true 136 | 137 | # If true, extends the user's remember period when remembered via cookie. 138 | # config.extend_remember_period = false 139 | 140 | # Options to be passed to the created cookie. For instance, you can set 141 | # secure: true in order to force SSL only cookies. 142 | # config.rememberable_options = {} 143 | 144 | # ==> Configuration for :validatable 145 | # Range for password length. 146 | config.password_length = 8..128 147 | 148 | # Email regex used to validate email formats. It simply asserts that 149 | # one (and only one) @ exists in the given string. This is mainly 150 | # to give user feedback and not to assert the e-mail validity. 151 | # config.email_regexp = /\A[^@]+@[^@]+\z/ 152 | 153 | # ==> Configuration for :timeoutable 154 | # The time you want to timeout the user session without activity. After this 155 | # time the user will be asked for credentials again. Default is 30 minutes. 156 | # config.timeout_in = 30.minutes 157 | 158 | # If true, expires auth token on session timeout. 159 | # config.expire_auth_token_on_timeout = false 160 | 161 | # ==> Configuration for :lockable 162 | # Defines which strategy will be used to lock an account. 163 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 164 | # :none = No lock strategy. You should handle locking by yourself. 165 | # config.lock_strategy = :failed_attempts 166 | 167 | # Defines which key will be used when locking and unlocking an account 168 | # config.unlock_keys = [ :email ] 169 | 170 | # Defines which strategy will be used to unlock an account. 171 | # :email = Sends an unlock link to the user email 172 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 173 | # :both = Enables both strategies 174 | # :none = No unlock strategy. You should handle unlocking by yourself. 175 | # config.unlock_strategy = :both 176 | 177 | # Number of authentication tries before locking an account if lock_strategy 178 | # is failed attempts. 179 | # config.maximum_attempts = 20 180 | 181 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 182 | # config.unlock_in = 1.hour 183 | 184 | # Warn on the last attempt before the account is locked. 185 | # config.last_attempt_warning = true 186 | 187 | # ==> Configuration for :recoverable 188 | # 189 | # Defines which key will be used when recovering the password for an account 190 | # config.reset_password_keys = [ :email ] 191 | 192 | # Time interval you can reset your password with a reset password key. 193 | # Don't put a too small interval or your users won't have the time to 194 | # change their passwords. 195 | config.reset_password_within = 6.hours 196 | 197 | # ==> Configuration for :encryptable 198 | # Allow you to use another encryption algorithm besides bcrypt (default). You can use 199 | # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, 200 | # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) 201 | # and :restful_authentication_sha1 (then you should set stretches to 10, and copy 202 | # REST_AUTH_SITE_KEY to pepper). 203 | # 204 | # Require the `devise-encryptable` gem when using anything other than bcrypt 205 | # config.encryptor = :sha512 206 | 207 | # ==> Scopes configuration 208 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 209 | # "users/sessions/new". It's turned off by default because it's slower if you 210 | # are using only default views. 211 | # config.scoped_views = false 212 | 213 | # Configure the default scope given to Warden. By default it's the first 214 | # devise role declared in your routes (usually :user). 215 | # config.default_scope = :user 216 | 217 | # Set this configuration to false if you want /users/sign_out to sign out 218 | # only the current scope. By default, Devise signs out all scopes. 219 | # config.sign_out_all_scopes = true 220 | 221 | # ==> Navigation configuration 222 | # Lists the formats that should be treated as navigational. Formats like 223 | # :html, should redirect to the sign in page when the user does not have 224 | # access, but formats like :xml or :json, should return 401. 225 | # 226 | # If you have any extra navigational formats, like :iphone or :mobile, you 227 | # should add them to the navigational formats lists. 228 | # 229 | # The "*/*" below is required to match Internet Explorer requests. 230 | # config.navigational_formats = ['*/*', :html] 231 | 232 | # The default HTTP method used to sign out a resource. Default is :delete. 233 | config.sign_out_via = :delete 234 | 235 | # ==> OmniAuth 236 | # Add a new OmniAuth provider. Check the wiki for more information on setting 237 | # up on your models and hooks. 238 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 239 | 240 | config.omniauth :github, ENV['GITHUB_CLIENT_ID'], ENV['GITHUB_CLIENT_SECRET'], :scope => 'user,repo,public_repo' 241 | 242 | # ==> Warden configuration 243 | # If you want to use other strategies, that are not supported by Devise, or 244 | # change the failure app, you can configure them inside the config.warden block. 245 | # 246 | # config.warden do |manager| 247 | # manager.intercept_401 = false 248 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 249 | # end 250 | 251 | # ==> Mountable engine configurations 252 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 253 | # is mountable, there are some extra configurations to be taken into account. 254 | # The following options are available, assuming the engine is mounted as: 255 | # 256 | # mount MyEngine, at: '/my_engine' 257 | # 258 | # The router that invoked `devise_for`, in the example above, would be: 259 | # config.router_name = :my_engine 260 | # 261 | # When using omniauth, Devise cannot automatically set Omniauth path, 262 | # so you need to do it manually. For the users scope, it would be: 263 | # config.omniauth_path_prefix = '/my_engine/users/auth' 264 | end 265 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/fastclick-1.0.6.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict'; 3 | 4 | /** 5 | * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. 6 | * 7 | * @codingstandard ftlabs-jsv2 8 | * @copyright The Financial Times Limited [All Rights Reserved] 9 | * @license MIT License (see LICENSE.txt) 10 | */ 11 | 12 | /*jslint browser:true, node:true*/ 13 | /*global define, Event, Node*/ 14 | 15 | 16 | /** 17 | * Instantiate fast-clicking listeners on the specified layer. 18 | * 19 | * @constructor 20 | * @param {Element} layer The layer to listen on 21 | * @param {Object} [options={}] The options to override the defaults 22 | */ 23 | function FastClick(layer, options) { 24 | var oldOnClick; 25 | 26 | options = options || {}; 27 | 28 | /** 29 | * Whether a click is currently being tracked. 30 | * 31 | * @type boolean 32 | */ 33 | this.trackingClick = false; 34 | 35 | 36 | /** 37 | * Timestamp for when click tracking started. 38 | * 39 | * @type number 40 | */ 41 | this.trackingClickStart = 0; 42 | 43 | 44 | /** 45 | * The element being tracked for a click. 46 | * 47 | * @type EventTarget 48 | */ 49 | this.targetElement = null; 50 | 51 | 52 | /** 53 | * X-coordinate of touch start event. 54 | * 55 | * @type number 56 | */ 57 | this.touchStartX = 0; 58 | 59 | 60 | /** 61 | * Y-coordinate of touch start event. 62 | * 63 | * @type number 64 | */ 65 | this.touchStartY = 0; 66 | 67 | 68 | /** 69 | * ID of the last touch, retrieved from Touch.identifier. 70 | * 71 | * @type number 72 | */ 73 | this.lastTouchIdentifier = 0; 74 | 75 | 76 | /** 77 | * Touchmove boundary, beyond which a click will be cancelled. 78 | * 79 | * @type number 80 | */ 81 | this.touchBoundary = options.touchBoundary || 10; 82 | 83 | 84 | /** 85 | * The FastClick layer. 86 | * 87 | * @type Element 88 | */ 89 | this.layer = layer; 90 | 91 | /** 92 | * The minimum time between tap(touchstart and touchend) events 93 | * 94 | * @type number 95 | */ 96 | this.tapDelay = options.tapDelay || 200; 97 | 98 | /** 99 | * The maximum time for a tap 100 | * 101 | * @type number 102 | */ 103 | this.tapTimeout = options.tapTimeout || 700; 104 | 105 | if (FastClick.notNeeded(layer)) { 106 | return; 107 | } 108 | 109 | // Some old versions of Android don't have Function.prototype.bind 110 | function bind(method, context) { 111 | return function() { return method.apply(context, arguments); }; 112 | } 113 | 114 | 115 | var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; 116 | var context = this; 117 | for (var i = 0, l = methods.length; i < l; i++) { 118 | context[methods[i]] = bind(context[methods[i]], context); 119 | } 120 | 121 | // Set up event handlers as required 122 | if (deviceIsAndroid) { 123 | layer.addEventListener('mouseover', this.onMouse, true); 124 | layer.addEventListener('mousedown', this.onMouse, true); 125 | layer.addEventListener('mouseup', this.onMouse, true); 126 | } 127 | 128 | layer.addEventListener('click', this.onClick, true); 129 | layer.addEventListener('touchstart', this.onTouchStart, false); 130 | layer.addEventListener('touchmove', this.onTouchMove, false); 131 | layer.addEventListener('touchend', this.onTouchEnd, false); 132 | layer.addEventListener('touchcancel', this.onTouchCancel, false); 133 | 134 | // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) 135 | // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick 136 | // layer when they are cancelled. 137 | if (!Event.prototype.stopImmediatePropagation) { 138 | layer.removeEventListener = function(type, callback, capture) { 139 | var rmv = Node.prototype.removeEventListener; 140 | if (type === 'click') { 141 | rmv.call(layer, type, callback.hijacked || callback, capture); 142 | } else { 143 | rmv.call(layer, type, callback, capture); 144 | } 145 | }; 146 | 147 | layer.addEventListener = function(type, callback, capture) { 148 | var adv = Node.prototype.addEventListener; 149 | if (type === 'click') { 150 | adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { 151 | if (!event.propagationStopped) { 152 | callback(event); 153 | } 154 | }), capture); 155 | } else { 156 | adv.call(layer, type, callback, capture); 157 | } 158 | }; 159 | } 160 | 161 | // If a handler is already declared in the element's onclick attribute, it will be fired before 162 | // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and 163 | // adding it as listener. 164 | if (typeof layer.onclick === 'function') { 165 | 166 | // Android browser on at least 3.2 requires a new reference to the function in layer.onclick 167 | // - the old one won't work if passed to addEventListener directly. 168 | oldOnClick = layer.onclick; 169 | layer.addEventListener('click', function(event) { 170 | oldOnClick(event); 171 | }, false); 172 | layer.onclick = null; 173 | } 174 | } 175 | 176 | /** 177 | * Windows Phone 8.1 fakes user agent string to look like Android and iPhone. 178 | * 179 | * @type boolean 180 | */ 181 | var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0; 182 | 183 | /** 184 | * Android requires exceptions. 185 | * 186 | * @type boolean 187 | */ 188 | var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone; 189 | 190 | 191 | /** 192 | * iOS requires exceptions. 193 | * 194 | * @type boolean 195 | */ 196 | var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone; 197 | 198 | 199 | /** 200 | * iOS 4 requires an exception for select elements. 201 | * 202 | * @type boolean 203 | */ 204 | var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); 205 | 206 | 207 | /** 208 | * iOS 6.0-7.* requires the target element to be manually derived 209 | * 210 | * @type boolean 211 | */ 212 | var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent); 213 | 214 | /** 215 | * BlackBerry requires exceptions. 216 | * 217 | * @type boolean 218 | */ 219 | var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0; 220 | 221 | /** 222 | * Determine whether a given element requires a native click. 223 | * 224 | * @param {EventTarget|Element} target Target DOM element 225 | * @returns {boolean} Returns true if the element needs a native click 226 | */ 227 | FastClick.prototype.needsClick = function(target) { 228 | switch (target.nodeName.toLowerCase()) { 229 | 230 | // Don't send a synthetic click to disabled inputs (issue #62) 231 | case 'button': 232 | case 'select': 233 | case 'textarea': 234 | if (target.disabled) { 235 | return true; 236 | } 237 | 238 | break; 239 | case 'input': 240 | 241 | // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) 242 | if ((deviceIsIOS && target.type === 'file') || target.disabled) { 243 | return true; 244 | } 245 | 246 | break; 247 | case 'label': 248 | case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames 249 | case 'video': 250 | return true; 251 | } 252 | 253 | return (/\bneedsclick\b/).test(target.className); 254 | }; 255 | 256 | 257 | /** 258 | * Determine whether a given element requires a call to focus to simulate click into element. 259 | * 260 | * @param {EventTarget|Element} target Target DOM element 261 | * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. 262 | */ 263 | FastClick.prototype.needsFocus = function(target) { 264 | switch (target.nodeName.toLowerCase()) { 265 | case 'textarea': 266 | return true; 267 | case 'select': 268 | return !deviceIsAndroid; 269 | case 'input': 270 | switch (target.type) { 271 | case 'button': 272 | case 'checkbox': 273 | case 'file': 274 | case 'image': 275 | case 'radio': 276 | case 'submit': 277 | return false; 278 | } 279 | 280 | // No point in attempting to focus disabled inputs 281 | return !target.disabled && !target.readOnly; 282 | default: 283 | return (/\bneedsfocus\b/).test(target.className); 284 | } 285 | }; 286 | 287 | 288 | /** 289 | * Send a click event to the specified element. 290 | * 291 | * @param {EventTarget|Element} targetElement 292 | * @param {Event} event 293 | */ 294 | FastClick.prototype.sendClick = function(targetElement, event) { 295 | var clickEvent, touch; 296 | 297 | // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) 298 | if (document.activeElement && document.activeElement !== targetElement) { 299 | document.activeElement.blur(); 300 | } 301 | 302 | touch = event.changedTouches[0]; 303 | 304 | // Synthesise a click event, with an extra attribute so it can be tracked 305 | clickEvent = document.createEvent('MouseEvents'); 306 | clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); 307 | clickEvent.forwardedTouchEvent = true; 308 | targetElement.dispatchEvent(clickEvent); 309 | }; 310 | 311 | FastClick.prototype.determineEventType = function(targetElement) { 312 | 313 | //Issue #159: Android Chrome Select Box does not open with a synthetic click event 314 | if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { 315 | return 'mousedown'; 316 | } 317 | 318 | return 'click'; 319 | }; 320 | 321 | 322 | /** 323 | * @param {EventTarget|Element} targetElement 324 | */ 325 | FastClick.prototype.focus = function(targetElement) { 326 | var length; 327 | 328 | // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. 329 | if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') { 330 | length = targetElement.value.length; 331 | targetElement.setSelectionRange(length, length); 332 | } else { 333 | targetElement.focus(); 334 | } 335 | }; 336 | 337 | 338 | /** 339 | * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. 340 | * 341 | * @param {EventTarget|Element} targetElement 342 | */ 343 | FastClick.prototype.updateScrollParent = function(targetElement) { 344 | var scrollParent, parentElement; 345 | 346 | scrollParent = targetElement.fastClickScrollParent; 347 | 348 | // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the 349 | // target element was moved to another parent. 350 | if (!scrollParent || !scrollParent.contains(targetElement)) { 351 | parentElement = targetElement; 352 | do { 353 | if (parentElement.scrollHeight > parentElement.offsetHeight) { 354 | scrollParent = parentElement; 355 | targetElement.fastClickScrollParent = parentElement; 356 | break; 357 | } 358 | 359 | parentElement = parentElement.parentElement; 360 | } while (parentElement); 361 | } 362 | 363 | // Always update the scroll top tracker if possible. 364 | if (scrollParent) { 365 | scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; 366 | } 367 | }; 368 | 369 | 370 | /** 371 | * @param {EventTarget} targetElement 372 | * @returns {Element|EventTarget} 373 | */ 374 | FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { 375 | 376 | // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. 377 | if (eventTarget.nodeType === Node.TEXT_NODE) { 378 | return eventTarget.parentNode; 379 | } 380 | 381 | return eventTarget; 382 | }; 383 | 384 | 385 | /** 386 | * On touch start, record the position and scroll offset. 387 | * 388 | * @param {Event} event 389 | * @returns {boolean} 390 | */ 391 | FastClick.prototype.onTouchStart = function(event) { 392 | var targetElement, touch, selection; 393 | 394 | // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). 395 | if (event.targetTouches.length > 1) { 396 | return true; 397 | } 398 | 399 | targetElement = this.getTargetElementFromEventTarget(event.target); 400 | touch = event.targetTouches[0]; 401 | 402 | if (deviceIsIOS) { 403 | 404 | // Only trusted events will deselect text on iOS (issue #49) 405 | selection = window.getSelection(); 406 | if (selection.rangeCount && !selection.isCollapsed) { 407 | return true; 408 | } 409 | 410 | if (!deviceIsIOS4) { 411 | 412 | // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): 413 | // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched 414 | // with the same identifier as the touch event that previously triggered the click that triggered the alert. 415 | // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an 416 | // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. 417 | // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string, 418 | // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long, 419 | // random integers, it's safe to to continue if the identifier is 0 here. 420 | if (touch.identifier && touch.identifier === this.lastTouchIdentifier) { 421 | event.preventDefault(); 422 | return false; 423 | } 424 | 425 | this.lastTouchIdentifier = touch.identifier; 426 | 427 | // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: 428 | // 1) the user does a fling scroll on the scrollable layer 429 | // 2) the user stops the fling scroll with another tap 430 | // then the event.target of the last 'touchend' event will be the element that was under the user's finger 431 | // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check 432 | // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). 433 | this.updateScrollParent(targetElement); 434 | } 435 | } 436 | 437 | this.trackingClick = true; 438 | this.trackingClickStart = event.timeStamp; 439 | this.targetElement = targetElement; 440 | 441 | this.touchStartX = touch.pageX; 442 | this.touchStartY = touch.pageY; 443 | 444 | // Prevent phantom clicks on fast double-tap (issue #36) 445 | if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { 446 | event.preventDefault(); 447 | } 448 | 449 | return true; 450 | }; 451 | 452 | 453 | /** 454 | * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. 455 | * 456 | * @param {Event} event 457 | * @returns {boolean} 458 | */ 459 | FastClick.prototype.touchHasMoved = function(event) { 460 | var touch = event.changedTouches[0], boundary = this.touchBoundary; 461 | 462 | if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { 463 | return true; 464 | } 465 | 466 | return false; 467 | }; 468 | 469 | 470 | /** 471 | * Update the last position. 472 | * 473 | * @param {Event} event 474 | * @returns {boolean} 475 | */ 476 | FastClick.prototype.onTouchMove = function(event) { 477 | if (!this.trackingClick) { 478 | return true; 479 | } 480 | 481 | // If the touch has moved, cancel the click tracking 482 | if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { 483 | this.trackingClick = false; 484 | this.targetElement = null; 485 | } 486 | 487 | return true; 488 | }; 489 | 490 | 491 | /** 492 | * Attempt to find the labelled control for the given label element. 493 | * 494 | * @param {EventTarget|HTMLLabelElement} labelElement 495 | * @returns {Element|null} 496 | */ 497 | FastClick.prototype.findControl = function(labelElement) { 498 | 499 | // Fast path for newer browsers supporting the HTML5 control attribute 500 | if (labelElement.control !== undefined) { 501 | return labelElement.control; 502 | } 503 | 504 | // All browsers under test that support touch events also support the HTML5 htmlFor attribute 505 | if (labelElement.htmlFor) { 506 | return document.getElementById(labelElement.htmlFor); 507 | } 508 | 509 | // If no for attribute exists, attempt to retrieve the first labellable descendant element 510 | // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label 511 | return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); 512 | }; 513 | 514 | 515 | /** 516 | * On touch end, determine whether to send a click event at once. 517 | * 518 | * @param {Event} event 519 | * @returns {boolean} 520 | */ 521 | FastClick.prototype.onTouchEnd = function(event) { 522 | var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; 523 | 524 | if (!this.trackingClick) { 525 | return true; 526 | } 527 | 528 | // Prevent phantom clicks on fast double-tap (issue #36) 529 | if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { 530 | this.cancelNextClick = true; 531 | return true; 532 | } 533 | 534 | if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { 535 | return true; 536 | } 537 | 538 | // Reset to prevent wrong click cancel on input (issue #156). 539 | this.cancelNextClick = false; 540 | 541 | this.lastClickTime = event.timeStamp; 542 | 543 | trackingClickStart = this.trackingClickStart; 544 | this.trackingClick = false; 545 | this.trackingClickStart = 0; 546 | 547 | // On some iOS devices, the targetElement supplied with the event is invalid if the layer 548 | // is performing a transition or scroll, and has to be re-detected manually. Note that 549 | // for this to function correctly, it must be called *after* the event target is checked! 550 | // See issue #57; also filed as rdar://13048589 . 551 | if (deviceIsIOSWithBadTarget) { 552 | touch = event.changedTouches[0]; 553 | 554 | // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null 555 | targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; 556 | targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; 557 | } 558 | 559 | targetTagName = targetElement.tagName.toLowerCase(); 560 | if (targetTagName === 'label') { 561 | forElement = this.findControl(targetElement); 562 | if (forElement) { 563 | this.focus(targetElement); 564 | if (deviceIsAndroid) { 565 | return false; 566 | } 567 | 568 | targetElement = forElement; 569 | } 570 | } else if (this.needsFocus(targetElement)) { 571 | 572 | // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. 573 | // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). 574 | if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { 575 | this.targetElement = null; 576 | return false; 577 | } 578 | 579 | this.focus(targetElement); 580 | this.sendClick(targetElement, event); 581 | 582 | // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. 583 | // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) 584 | if (!deviceIsIOS || targetTagName !== 'select') { 585 | this.targetElement = null; 586 | event.preventDefault(); 587 | } 588 | 589 | return false; 590 | } 591 | 592 | if (deviceIsIOS && !deviceIsIOS4) { 593 | 594 | // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled 595 | // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). 596 | scrollParent = targetElement.fastClickScrollParent; 597 | if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { 598 | return true; 599 | } 600 | } 601 | 602 | // Prevent the actual click from going though - unless the target node is marked as requiring 603 | // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. 604 | if (!this.needsClick(targetElement)) { 605 | event.preventDefault(); 606 | this.sendClick(targetElement, event); 607 | } 608 | 609 | return false; 610 | }; 611 | 612 | 613 | /** 614 | * On touch cancel, stop tracking the click. 615 | * 616 | * @returns {void} 617 | */ 618 | FastClick.prototype.onTouchCancel = function() { 619 | this.trackingClick = false; 620 | this.targetElement = null; 621 | }; 622 | 623 | 624 | /** 625 | * Determine mouse events which should be permitted. 626 | * 627 | * @param {Event} event 628 | * @returns {boolean} 629 | */ 630 | FastClick.prototype.onMouse = function(event) { 631 | 632 | // If a target element was never set (because a touch event was never fired) allow the event 633 | if (!this.targetElement) { 634 | return true; 635 | } 636 | 637 | if (event.forwardedTouchEvent) { 638 | return true; 639 | } 640 | 641 | // Programmatically generated events targeting a specific element should be permitted 642 | if (!event.cancelable) { 643 | return true; 644 | } 645 | 646 | // Derive and check the target element to see whether the mouse event needs to be permitted; 647 | // unless explicitly enabled, prevent non-touch click events from triggering actions, 648 | // to prevent ghost/doubleclicks. 649 | if (!this.needsClick(this.targetElement) || this.cancelNextClick) { 650 | 651 | // Prevent any user-added listeners declared on FastClick element from being fired. 652 | if (event.stopImmediatePropagation) { 653 | event.stopImmediatePropagation(); 654 | } else { 655 | 656 | // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) 657 | event.propagationStopped = true; 658 | } 659 | 660 | // Cancel the event 661 | event.stopPropagation(); 662 | event.preventDefault(); 663 | 664 | return false; 665 | } 666 | 667 | // If the mouse event is permitted, return true for the action to go through. 668 | return true; 669 | }; 670 | 671 | 672 | /** 673 | * On actual clicks, determine whether this is a touch-generated click, a click action occurring 674 | * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or 675 | * an actual click which should be permitted. 676 | * 677 | * @param {Event} event 678 | * @returns {boolean} 679 | */ 680 | FastClick.prototype.onClick = function(event) { 681 | var permitted; 682 | 683 | // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. 684 | if (this.trackingClick) { 685 | this.targetElement = null; 686 | this.trackingClick = false; 687 | return true; 688 | } 689 | 690 | // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. 691 | if (event.target.type === 'submit' && event.detail === 0) { 692 | return true; 693 | } 694 | 695 | permitted = this.onMouse(event); 696 | 697 | // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. 698 | if (!permitted) { 699 | this.targetElement = null; 700 | } 701 | 702 | // If clicks are permitted, return true for the action to go through. 703 | return permitted; 704 | }; 705 | 706 | 707 | /** 708 | * Remove all FastClick's event listeners. 709 | * 710 | * @returns {void} 711 | */ 712 | FastClick.prototype.destroy = function() { 713 | var layer = this.layer; 714 | 715 | if (deviceIsAndroid) { 716 | layer.removeEventListener('mouseover', this.onMouse, true); 717 | layer.removeEventListener('mousedown', this.onMouse, true); 718 | layer.removeEventListener('mouseup', this.onMouse, true); 719 | } 720 | 721 | layer.removeEventListener('click', this.onClick, true); 722 | layer.removeEventListener('touchstart', this.onTouchStart, false); 723 | layer.removeEventListener('touchmove', this.onTouchMove, false); 724 | layer.removeEventListener('touchend', this.onTouchEnd, false); 725 | layer.removeEventListener('touchcancel', this.onTouchCancel, false); 726 | }; 727 | 728 | 729 | /** 730 | * Check whether FastClick is needed. 731 | * 732 | * @param {Element} layer The layer to listen on 733 | */ 734 | FastClick.notNeeded = function(layer) { 735 | var metaViewport; 736 | var chromeVersion; 737 | var blackberryVersion; 738 | var firefoxVersion; 739 | 740 | // Devices that don't support touch don't need FastClick 741 | if (typeof window.ontouchstart === 'undefined') { 742 | return true; 743 | } 744 | 745 | // Chrome version - zero for other browsers 746 | chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; 747 | 748 | if (chromeVersion) { 749 | 750 | if (deviceIsAndroid) { 751 | metaViewport = document.querySelector('meta[name=viewport]'); 752 | 753 | if (metaViewport) { 754 | // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) 755 | if (metaViewport.content.indexOf('user-scalable=no') !== -1) { 756 | return true; 757 | } 758 | // Chrome 32 and above with width=device-width or less don't need FastClick 759 | if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { 760 | return true; 761 | } 762 | } 763 | 764 | // Chrome desktop doesn't need FastClick (issue #15) 765 | } else { 766 | return true; 767 | } 768 | } 769 | 770 | if (deviceIsBlackBerry10) { 771 | blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/); 772 | 773 | // BlackBerry 10.3+ does not require Fastclick library. 774 | // https://github.com/ftlabs/fastclick/issues/251 775 | if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) { 776 | metaViewport = document.querySelector('meta[name=viewport]'); 777 | 778 | if (metaViewport) { 779 | // user-scalable=no eliminates click delay. 780 | if (metaViewport.content.indexOf('user-scalable=no') !== -1) { 781 | return true; 782 | } 783 | // width=device-width (or less than device-width) eliminates click delay. 784 | if (document.documentElement.scrollWidth <= window.outerWidth) { 785 | return true; 786 | } 787 | } 788 | } 789 | } 790 | 791 | // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97) 792 | if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') { 793 | return true; 794 | } 795 | 796 | // Firefox version - zero for other browsers 797 | firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; 798 | 799 | if (firefoxVersion >= 27) { 800 | // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896 801 | 802 | metaViewport = document.querySelector('meta[name=viewport]'); 803 | if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) { 804 | return true; 805 | } 806 | } 807 | 808 | // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version 809 | // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx 810 | if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') { 811 | return true; 812 | } 813 | 814 | return false; 815 | }; 816 | 817 | 818 | /** 819 | * Factory method for creating a FastClick object 820 | * 821 | * @param {Element} layer The layer to listen on 822 | * @param {Object} [options={}] The options to override the defaults 823 | */ 824 | FastClick.attach = function(layer, options) { 825 | return new FastClick(layer, options); 826 | }; 827 | 828 | 829 | if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 830 | 831 | // AMD. Register as an anonymous module. 832 | define(function() { 833 | return FastClick; 834 | }); 835 | } else if (typeof module !== 'undefined' && module.exports) { 836 | module.exports = FastClick.attach; 837 | module.exports.FastClick = FastClick; 838 | } else { 839 | window.FastClick = FastClick; 840 | } 841 | }()); 842 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/mousetrap-1.4.6.js: -------------------------------------------------------------------------------- 1 | /*global define:false */ 2 | /** 3 | * Copyright 2013 Craig Campbell 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | * Mousetrap is a simple keyboard shortcut library for Javascript with 18 | * no external dependencies 19 | * 20 | * @version 1.4.6 21 | * @url craig.is/killing/mice 22 | */ 23 | (function(window, document, undefined) { 24 | 25 | /** 26 | * mapping of special keycodes to their corresponding keys 27 | * 28 | * everything in this dictionary cannot use keypress events 29 | * so it has to be here to map to the correct keycodes for 30 | * keyup/keydown events 31 | * 32 | * @type {Object} 33 | */ 34 | var _MAP = { 35 | 8: 'backspace', 36 | 9: 'tab', 37 | 13: 'enter', 38 | 16: 'shift', 39 | 17: 'ctrl', 40 | 18: 'alt', 41 | 20: 'capslock', 42 | 27: 'esc', 43 | 32: 'space', 44 | 33: 'pageup', 45 | 34: 'pagedown', 46 | 35: 'end', 47 | 36: 'home', 48 | 37: 'left', 49 | 38: 'up', 50 | 39: 'right', 51 | 40: 'down', 52 | 45: 'ins', 53 | 46: 'del', 54 | 91: 'meta', 55 | 93: 'meta', 56 | 224: 'meta' 57 | }, 58 | 59 | /** 60 | * mapping for special characters so they can support 61 | * 62 | * this dictionary is only used incase you want to bind a 63 | * keyup or keydown event to one of these keys 64 | * 65 | * @type {Object} 66 | */ 67 | _KEYCODE_MAP = { 68 | 106: '*', 69 | 107: '+', 70 | 109: '-', 71 | 110: '.', 72 | 111 : '/', 73 | 186: ';', 74 | 187: '=', 75 | 188: ',', 76 | 189: '-', 77 | 190: '.', 78 | 191: '/', 79 | 192: '`', 80 | 219: '[', 81 | 220: '\\', 82 | 221: ']', 83 | 222: '\'' 84 | }, 85 | 86 | /** 87 | * this is a mapping of keys that require shift on a US keypad 88 | * back to the non shift equivelents 89 | * 90 | * this is so you can use keyup events with these keys 91 | * 92 | * note that this will only work reliably on US keyboards 93 | * 94 | * @type {Object} 95 | */ 96 | _SHIFT_MAP = { 97 | '~': '`', 98 | '!': '1', 99 | '@': '2', 100 | '#': '3', 101 | '$': '4', 102 | '%': '5', 103 | '^': '6', 104 | '&': '7', 105 | '*': '8', 106 | '(': '9', 107 | ')': '0', 108 | '_': '-', 109 | '+': '=', 110 | ':': ';', 111 | '\"': '\'', 112 | '<': ',', 113 | '>': '.', 114 | '?': '/', 115 | '|': '\\' 116 | }, 117 | 118 | /** 119 | * this is a list of special strings you can use to map 120 | * to modifier keys when you specify your keyboard shortcuts 121 | * 122 | * @type {Object} 123 | */ 124 | _SPECIAL_ALIASES = { 125 | 'option': 'alt', 126 | 'command': 'meta', 127 | 'return': 'enter', 128 | 'escape': 'esc', 129 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 130 | }, 131 | 132 | /** 133 | * variable to store the flipped version of _MAP from above 134 | * needed to check if we should use keypress or not when no action 135 | * is specified 136 | * 137 | * @type {Object|undefined} 138 | */ 139 | _REVERSE_MAP, 140 | 141 | /** 142 | * a list of all the callbacks setup via Mousetrap.bind() 143 | * 144 | * @type {Object} 145 | */ 146 | _callbacks = {}, 147 | 148 | /** 149 | * direct map of string combinations to callbacks used for trigger() 150 | * 151 | * @type {Object} 152 | */ 153 | _directMap = {}, 154 | 155 | /** 156 | * keeps track of what level each sequence is at since multiple 157 | * sequences can start out with the same sequence 158 | * 159 | * @type {Object} 160 | */ 161 | _sequenceLevels = {}, 162 | 163 | /** 164 | * variable to store the setTimeout call 165 | * 166 | * @type {null|number} 167 | */ 168 | _resetTimer, 169 | 170 | /** 171 | * temporary state where we will ignore the next keyup 172 | * 173 | * @type {boolean|string} 174 | */ 175 | _ignoreNextKeyup = false, 176 | 177 | /** 178 | * temporary state where we will ignore the next keypress 179 | * 180 | * @type {boolean} 181 | */ 182 | _ignoreNextKeypress = false, 183 | 184 | /** 185 | * are we currently inside of a sequence? 186 | * type of action ("keyup" or "keydown" or "keypress") or false 187 | * 188 | * @type {boolean|string} 189 | */ 190 | _nextExpectedAction = false; 191 | 192 | /** 193 | * loop through the f keys, f1 to f19 and add them to the map 194 | * programatically 195 | */ 196 | for (var i = 1; i < 20; ++i) { 197 | _MAP[111 + i] = 'f' + i; 198 | } 199 | 200 | /** 201 | * loop through to map numbers on the numeric keypad 202 | */ 203 | for (i = 0; i <= 9; ++i) { 204 | _MAP[i + 96] = i; 205 | } 206 | 207 | /** 208 | * cross browser add event method 209 | * 210 | * @param {Element|HTMLDocument} object 211 | * @param {string} type 212 | * @param {Function} callback 213 | * @returns void 214 | */ 215 | function _addEvent(object, type, callback) { 216 | if (object.addEventListener) { 217 | object.addEventListener(type, callback, false); 218 | return; 219 | } 220 | 221 | object.attachEvent('on' + type, callback); 222 | } 223 | 224 | /** 225 | * takes the event and returns the key character 226 | * 227 | * @param {Event} e 228 | * @return {string} 229 | */ 230 | function _characterFromEvent(e) { 231 | 232 | // for keypress events we should return the character as is 233 | if (e.type == 'keypress') { 234 | var character = String.fromCharCode(e.which); 235 | 236 | // if the shift key is not pressed then it is safe to assume 237 | // that we want the character to be lowercase. this means if 238 | // you accidentally have caps lock on then your key bindings 239 | // will continue to work 240 | // 241 | // the only side effect that might not be desired is if you 242 | // bind something like 'A' cause you want to trigger an 243 | // event when capital A is pressed caps lock will no longer 244 | // trigger the event. shift+a will though. 245 | if (!e.shiftKey) { 246 | character = character.toLowerCase(); 247 | } 248 | 249 | return character; 250 | } 251 | 252 | // for non keypress events the special maps are needed 253 | if (_MAP[e.which]) { 254 | return _MAP[e.which]; 255 | } 256 | 257 | if (_KEYCODE_MAP[e.which]) { 258 | return _KEYCODE_MAP[e.which]; 259 | } 260 | 261 | // if it is not in the special map 262 | 263 | // with keydown and keyup events the character seems to always 264 | // come in as an uppercase character whether you are pressing shift 265 | // or not. we should make sure it is always lowercase for comparisons 266 | return String.fromCharCode(e.which).toLowerCase(); 267 | } 268 | 269 | /** 270 | * checks if two arrays are equal 271 | * 272 | * @param {Array} modifiers1 273 | * @param {Array} modifiers2 274 | * @returns {boolean} 275 | */ 276 | function _modifiersMatch(modifiers1, modifiers2) { 277 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 278 | } 279 | 280 | /** 281 | * resets all sequence counters except for the ones passed in 282 | * 283 | * @param {Object} doNotReset 284 | * @returns void 285 | */ 286 | function _resetSequences(doNotReset) { 287 | doNotReset = doNotReset || {}; 288 | 289 | var activeSequences = false, 290 | key; 291 | 292 | for (key in _sequenceLevels) { 293 | if (doNotReset[key]) { 294 | activeSequences = true; 295 | continue; 296 | } 297 | _sequenceLevels[key] = 0; 298 | } 299 | 300 | if (!activeSequences) { 301 | _nextExpectedAction = false; 302 | } 303 | } 304 | 305 | /** 306 | * finds all callbacks that match based on the keycode, modifiers, 307 | * and action 308 | * 309 | * @param {string} character 310 | * @param {Array} modifiers 311 | * @param {Event|Object} e 312 | * @param {string=} sequenceName - name of the sequence we are looking for 313 | * @param {string=} combination 314 | * @param {number=} level 315 | * @returns {Array} 316 | */ 317 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 318 | var i, 319 | callback, 320 | matches = [], 321 | action = e.type; 322 | 323 | // if there are no events related to this keycode 324 | if (!_callbacks[character]) { 325 | return []; 326 | } 327 | 328 | // if a modifier key is coming up on its own we should allow it 329 | if (action == 'keyup' && _isModifier(character)) { 330 | modifiers = [character]; 331 | } 332 | 333 | // loop through all callbacks for the key that was pressed 334 | // and see if any of them match 335 | for (i = 0; i < _callbacks[character].length; ++i) { 336 | callback = _callbacks[character][i]; 337 | 338 | // if a sequence name is not specified, but this is a sequence at 339 | // the wrong level then move onto the next match 340 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 341 | continue; 342 | } 343 | 344 | // if the action we are looking for doesn't match the action we got 345 | // then we should keep going 346 | if (action != callback.action) { 347 | continue; 348 | } 349 | 350 | // if this is a keypress event and the meta key and control key 351 | // are not pressed that means that we need to only look at the 352 | // character, otherwise check the modifiers as well 353 | // 354 | // chrome will not fire a keypress if meta or control is down 355 | // safari will fire a keypress if meta or meta+shift is down 356 | // firefox will fire a keypress if meta or control is down 357 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 358 | 359 | // when you bind a combination or sequence a second time it 360 | // should overwrite the first one. if a sequenceName or 361 | // combination is specified in this call it does just that 362 | // 363 | // @todo make deleting its own method? 364 | var deleteCombo = !sequenceName && callback.combo == combination; 365 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 366 | if (deleteCombo || deleteSequence) { 367 | _callbacks[character].splice(i, 1); 368 | } 369 | 370 | matches.push(callback); 371 | } 372 | } 373 | 374 | return matches; 375 | } 376 | 377 | /** 378 | * takes a key event and figures out what the modifiers are 379 | * 380 | * @param {Event} e 381 | * @returns {Array} 382 | */ 383 | function _eventModifiers(e) { 384 | var modifiers = []; 385 | 386 | if (e.shiftKey) { 387 | modifiers.push('shift'); 388 | } 389 | 390 | if (e.altKey) { 391 | modifiers.push('alt'); 392 | } 393 | 394 | if (e.ctrlKey) { 395 | modifiers.push('ctrl'); 396 | } 397 | 398 | if (e.metaKey) { 399 | modifiers.push('meta'); 400 | } 401 | 402 | return modifiers; 403 | } 404 | 405 | /** 406 | * prevents default for this event 407 | * 408 | * @param {Event} e 409 | * @returns void 410 | */ 411 | function _preventDefault(e) { 412 | if (e.preventDefault) { 413 | e.preventDefault(); 414 | return; 415 | } 416 | 417 | e.returnValue = false; 418 | } 419 | 420 | /** 421 | * stops propogation for this event 422 | * 423 | * @param {Event} e 424 | * @returns void 425 | */ 426 | function _stopPropagation(e) { 427 | if (e.stopPropagation) { 428 | e.stopPropagation(); 429 | return; 430 | } 431 | 432 | e.cancelBubble = true; 433 | } 434 | 435 | /** 436 | * actually calls the callback function 437 | * 438 | * if your callback function returns false this will use the jquery 439 | * convention - prevent default and stop propogation on the event 440 | * 441 | * @param {Function} callback 442 | * @param {Event} e 443 | * @returns void 444 | */ 445 | function _fireCallback(callback, e, combo, sequence) { 446 | 447 | // if this event should not happen stop here 448 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 449 | return; 450 | } 451 | 452 | if (callback(e, combo) === false) { 453 | _preventDefault(e); 454 | _stopPropagation(e); 455 | } 456 | } 457 | 458 | /** 459 | * handles a character key event 460 | * 461 | * @param {string} character 462 | * @param {Array} modifiers 463 | * @param {Event} e 464 | * @returns void 465 | */ 466 | function _handleKey(character, modifiers, e) { 467 | var callbacks = _getMatches(character, modifiers, e), 468 | i, 469 | doNotReset = {}, 470 | maxLevel = 0, 471 | processedSequenceCallback = false; 472 | 473 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 474 | for (i = 0; i < callbacks.length; ++i) { 475 | if (callbacks[i].seq) { 476 | maxLevel = Math.max(maxLevel, callbacks[i].level); 477 | } 478 | } 479 | 480 | // loop through matching callbacks for this key event 481 | for (i = 0; i < callbacks.length; ++i) { 482 | 483 | // fire for all sequence callbacks 484 | // this is because if for example you have multiple sequences 485 | // bound such as "g i" and "g t" they both need to fire the 486 | // callback for matching g cause otherwise you can only ever 487 | // match the first one 488 | if (callbacks[i].seq) { 489 | 490 | // only fire callbacks for the maxLevel to prevent 491 | // subsequences from also firing 492 | // 493 | // for example 'a option b' should not cause 'option b' to fire 494 | // even though 'option b' is part of the other sequence 495 | // 496 | // any sequences that do not match here will be discarded 497 | // below by the _resetSequences call 498 | if (callbacks[i].level != maxLevel) { 499 | continue; 500 | } 501 | 502 | processedSequenceCallback = true; 503 | 504 | // keep a list of which sequences were matches for later 505 | doNotReset[callbacks[i].seq] = 1; 506 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq); 507 | continue; 508 | } 509 | 510 | // if there were no sequence matches but we are still here 511 | // that means this is a regular match so we should fire that 512 | if (!processedSequenceCallback) { 513 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 514 | } 515 | } 516 | 517 | // if the key you pressed matches the type of sequence without 518 | // being a modifier (ie "keyup" or "keypress") then we should 519 | // reset all sequences that were not matched by this event 520 | // 521 | // this is so, for example, if you have the sequence "h a t" and you 522 | // type "h e a r t" it does not match. in this case the "e" will 523 | // cause the sequence to reset 524 | // 525 | // modifier keys are ignored because you can have a sequence 526 | // that contains modifiers such as "enter ctrl+space" and in most 527 | // cases the modifier key will be pressed before the next key 528 | // 529 | // also if you have a sequence such as "ctrl+b a" then pressing the 530 | // "b" key will trigger a "keypress" and a "keydown" 531 | // 532 | // the "keydown" is expected when there is a modifier, but the 533 | // "keypress" ends up matching the _nextExpectedAction since it occurs 534 | // after and that causes the sequence to reset 535 | // 536 | // we ignore keypresses in a sequence that directly follow a keydown 537 | // for the same character 538 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 539 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 540 | _resetSequences(doNotReset); 541 | } 542 | 543 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 544 | } 545 | 546 | /** 547 | * handles a keydown event 548 | * 549 | * @param {Event} e 550 | * @returns void 551 | */ 552 | function _handleKeyEvent(e) { 553 | 554 | // normalize e.which for key events 555 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 556 | if (typeof e.which !== 'number') { 557 | e.which = e.keyCode; 558 | } 559 | 560 | var character = _characterFromEvent(e); 561 | 562 | // no character found then stop 563 | if (!character) { 564 | return; 565 | } 566 | 567 | // need to use === for the character check because the character can be 0 568 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 569 | _ignoreNextKeyup = false; 570 | return; 571 | } 572 | 573 | Mousetrap.handleKey(character, _eventModifiers(e), e); 574 | } 575 | 576 | /** 577 | * determines if the keycode specified is a modifier key or not 578 | * 579 | * @param {string} key 580 | * @returns {boolean} 581 | */ 582 | function _isModifier(key) { 583 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 584 | } 585 | 586 | /** 587 | * called to set a 1 second timeout on the specified sequence 588 | * 589 | * this is so after each key press in the sequence you have 1 second 590 | * to press the next key before you have to start over 591 | * 592 | * @returns void 593 | */ 594 | function _resetSequenceTimer() { 595 | clearTimeout(_resetTimer); 596 | _resetTimer = setTimeout(_resetSequences, 1000); 597 | } 598 | 599 | /** 600 | * reverses the map lookup so that we can look for specific keys 601 | * to see what can and can't use keypress 602 | * 603 | * @return {Object} 604 | */ 605 | function _getReverseMap() { 606 | if (!_REVERSE_MAP) { 607 | _REVERSE_MAP = {}; 608 | for (var key in _MAP) { 609 | 610 | // pull out the numeric keypad from here cause keypress should 611 | // be able to detect the keys from the character 612 | if (key > 95 && key < 112) { 613 | continue; 614 | } 615 | 616 | if (_MAP.hasOwnProperty(key)) { 617 | _REVERSE_MAP[_MAP[key]] = key; 618 | } 619 | } 620 | } 621 | return _REVERSE_MAP; 622 | } 623 | 624 | /** 625 | * picks the best action based on the key combination 626 | * 627 | * @param {string} key - character for key 628 | * @param {Array} modifiers 629 | * @param {string=} action passed in 630 | */ 631 | function _pickBestAction(key, modifiers, action) { 632 | 633 | // if no action was picked in we should try to pick the one 634 | // that we think would work best for this key 635 | if (!action) { 636 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 637 | } 638 | 639 | // modifier keys don't work as expected with keypress, 640 | // switch to keydown 641 | if (action == 'keypress' && modifiers.length) { 642 | action = 'keydown'; 643 | } 644 | 645 | return action; 646 | } 647 | 648 | /** 649 | * binds a key sequence to an event 650 | * 651 | * @param {string} combo - combo specified in bind call 652 | * @param {Array} keys 653 | * @param {Function} callback 654 | * @param {string=} action 655 | * @returns void 656 | */ 657 | function _bindSequence(combo, keys, callback, action) { 658 | 659 | // start off by adding a sequence level record for this combination 660 | // and setting the level to 0 661 | _sequenceLevels[combo] = 0; 662 | 663 | /** 664 | * callback to increase the sequence level for this sequence and reset 665 | * all other sequences that were active 666 | * 667 | * @param {string} nextAction 668 | * @returns {Function} 669 | */ 670 | function _increaseSequence(nextAction) { 671 | return function() { 672 | _nextExpectedAction = nextAction; 673 | ++_sequenceLevels[combo]; 674 | _resetSequenceTimer(); 675 | }; 676 | } 677 | 678 | /** 679 | * wraps the specified callback inside of another function in order 680 | * to reset all sequence counters as soon as this sequence is done 681 | * 682 | * @param {Event} e 683 | * @returns void 684 | */ 685 | function _callbackAndReset(e) { 686 | _fireCallback(callback, e, combo); 687 | 688 | // we should ignore the next key up if the action is key down 689 | // or keypress. this is so if you finish a sequence and 690 | // release the key the final key will not trigger a keyup 691 | if (action !== 'keyup') { 692 | _ignoreNextKeyup = _characterFromEvent(e); 693 | } 694 | 695 | // weird race condition if a sequence ends with the key 696 | // another sequence begins with 697 | setTimeout(_resetSequences, 10); 698 | } 699 | 700 | // loop through keys one at a time and bind the appropriate callback 701 | // function. for any key leading up to the final one it should 702 | // increase the sequence. after the final, it should reset all sequences 703 | // 704 | // if an action is specified in the original bind call then that will 705 | // be used throughout. otherwise we will pass the action that the 706 | // next key in the sequence should match. this allows a sequence 707 | // to mix and match keypress and keydown events depending on which 708 | // ones are better suited to the key provided 709 | for (var i = 0; i < keys.length; ++i) { 710 | var isFinal = i + 1 === keys.length; 711 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 712 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 713 | } 714 | } 715 | 716 | /** 717 | * Converts from a string key combination to an array 718 | * 719 | * @param {string} combination like "command+shift+l" 720 | * @return {Array} 721 | */ 722 | function _keysFromString(combination) { 723 | if (combination === '+') { 724 | return ['+']; 725 | } 726 | 727 | return combination.split('+'); 728 | } 729 | 730 | /** 731 | * Gets info for a specific key combination 732 | * 733 | * @param {string} combination key combination ("command+s" or "a" or "*") 734 | * @param {string=} action 735 | * @returns {Object} 736 | */ 737 | function _getKeyInfo(combination, action) { 738 | var keys, 739 | key, 740 | i, 741 | modifiers = []; 742 | 743 | // take the keys from this pattern and figure out what the actual 744 | // pattern is all about 745 | keys = _keysFromString(combination); 746 | 747 | for (i = 0; i < keys.length; ++i) { 748 | key = keys[i]; 749 | 750 | // normalize key names 751 | if (_SPECIAL_ALIASES[key]) { 752 | key = _SPECIAL_ALIASES[key]; 753 | } 754 | 755 | // if this is not a keypress event then we should 756 | // be smart about using shift keys 757 | // this will only work for US keyboards however 758 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 759 | key = _SHIFT_MAP[key]; 760 | modifiers.push('shift'); 761 | } 762 | 763 | // if this key is a modifier then add it to the list of modifiers 764 | if (_isModifier(key)) { 765 | modifiers.push(key); 766 | } 767 | } 768 | 769 | // depending on what the key combination is 770 | // we will try to pick the best event for it 771 | action = _pickBestAction(key, modifiers, action); 772 | 773 | return { 774 | key: key, 775 | modifiers: modifiers, 776 | action: action 777 | }; 778 | } 779 | 780 | /** 781 | * binds a single keyboard combination 782 | * 783 | * @param {string} combination 784 | * @param {Function} callback 785 | * @param {string=} action 786 | * @param {string=} sequenceName - name of sequence if part of sequence 787 | * @param {number=} level - what part of the sequence the command is 788 | * @returns void 789 | */ 790 | function _bindSingle(combination, callback, action, sequenceName, level) { 791 | 792 | // store a direct mapped reference for use with Mousetrap.trigger 793 | _directMap[combination + ':' + action] = callback; 794 | 795 | // make sure multiple spaces in a row become a single space 796 | combination = combination.replace(/\s+/g, ' '); 797 | 798 | var sequence = combination.split(' '), 799 | info; 800 | 801 | // if this pattern is a sequence of keys then run through this method 802 | // to reprocess each pattern one key at a time 803 | if (sequence.length > 1) { 804 | _bindSequence(combination, sequence, callback, action); 805 | return; 806 | } 807 | 808 | info = _getKeyInfo(combination, action); 809 | 810 | // make sure to initialize array if this is the first time 811 | // a callback is added for this key 812 | _callbacks[info.key] = _callbacks[info.key] || []; 813 | 814 | // remove an existing match if there is one 815 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 816 | 817 | // add this call back to the array 818 | // if it is a sequence put it at the beginning 819 | // if not put it at the end 820 | // 821 | // this is important because the way these are processed expects 822 | // the sequence ones to come first 823 | _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 824 | callback: callback, 825 | modifiers: info.modifiers, 826 | action: info.action, 827 | seq: sequenceName, 828 | level: level, 829 | combo: combination 830 | }); 831 | } 832 | 833 | /** 834 | * binds multiple combinations to the same callback 835 | * 836 | * @param {Array} combinations 837 | * @param {Function} callback 838 | * @param {string|undefined} action 839 | * @returns void 840 | */ 841 | function _bindMultiple(combinations, callback, action) { 842 | for (var i = 0; i < combinations.length; ++i) { 843 | _bindSingle(combinations[i], callback, action); 844 | } 845 | } 846 | 847 | // start! 848 | _addEvent(document, 'keypress', _handleKeyEvent); 849 | _addEvent(document, 'keydown', _handleKeyEvent); 850 | _addEvent(document, 'keyup', _handleKeyEvent); 851 | 852 | var Mousetrap = { 853 | 854 | /** 855 | * binds an event to mousetrap 856 | * 857 | * can be a single key, a combination of keys separated with +, 858 | * an array of keys, or a sequence of keys separated by spaces 859 | * 860 | * be sure to list the modifier keys first to make sure that the 861 | * correct key ends up getting bound (the last key in the pattern) 862 | * 863 | * @param {string|Array} keys 864 | * @param {Function} callback 865 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 866 | * @returns void 867 | */ 868 | bind: function(keys, callback, action) { 869 | keys = keys instanceof Array ? keys : [keys]; 870 | _bindMultiple(keys, callback, action); 871 | return this; 872 | }, 873 | 874 | /** 875 | * unbinds an event to mousetrap 876 | * 877 | * the unbinding sets the callback function of the specified key combo 878 | * to an empty function and deletes the corresponding key in the 879 | * _directMap dict. 880 | * 881 | * TODO: actually remove this from the _callbacks dictionary instead 882 | * of binding an empty function 883 | * 884 | * the keycombo+action has to be exactly the same as 885 | * it was defined in the bind method 886 | * 887 | * @param {string|Array} keys 888 | * @param {string} action 889 | * @returns void 890 | */ 891 | unbind: function(keys, action) { 892 | return Mousetrap.bind(keys, function() {}, action); 893 | }, 894 | 895 | /** 896 | * triggers an event that has already been bound 897 | * 898 | * @param {string} keys 899 | * @param {string=} action 900 | * @returns void 901 | */ 902 | trigger: function(keys, action) { 903 | if (_directMap[keys + ':' + action]) { 904 | _directMap[keys + ':' + action]({}, keys); 905 | } 906 | return this; 907 | }, 908 | 909 | /** 910 | * resets the library back to its initial state. this is useful 911 | * if you want to clear out the current keyboard shortcuts and bind 912 | * new ones - for example if you switch to another page 913 | * 914 | * @returns void 915 | */ 916 | reset: function() { 917 | _callbacks = {}; 918 | _directMap = {}; 919 | return this; 920 | }, 921 | 922 | /** 923 | * should we stop this event before firing off callbacks 924 | * 925 | * @param {Event} e 926 | * @param {Element} element 927 | * @return {boolean} 928 | */ 929 | stopCallback: function(e, element) { 930 | 931 | // if the element has the class "mousetrap" then no need to stop 932 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 933 | return false; 934 | } 935 | 936 | // stop for input, select, and textarea 937 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; 938 | }, 939 | 940 | /** 941 | * exposes _handleKey publicly so it can be overwritten by extensions 942 | */ 943 | handleKey: _handleKey 944 | }; 945 | 946 | // expose mousetrap to the global object 947 | window.Mousetrap = Mousetrap; 948 | 949 | // expose mousetrap as an AMD module 950 | if (typeof define === 'function' && define.amd) { 951 | define(Mousetrap); 952 | } 953 | }) (window, document); 954 | --------------------------------------------------------------------------------