├── 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 |
--------------------------------------------------------------------------------