├── log └── .keep ├── db └── migrations │ ├── .keep │ ├── 20161019_1231_rename_ursus_id_to_incident_id.rb │ ├── 20161111_1415_involved_person_copy_changes.rb │ ├── 20161111_1324_remove_contracting_ori_field.rb │ ├── 20161026_1154_add_secondary_index_to_users.rb │ └── 20161026_1148_add_secondary_index_to_incidents.rb ├── .rspec ├── app ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ ├── doj.html.erb │ │ ├── partials │ │ │ ├── _alerts.html.erb │ │ │ ├── _notices.html.erb │ │ │ ├── _footer.html.erb │ │ │ └── _head.html.erb │ │ ├── devise.html.erb │ │ ├── application.html.erb │ │ ├── dashboard.html.erb │ │ └── error.html.erb │ ├── errors │ │ ├── internal_server_error.html.erb │ │ ├── bad_request.html.erb │ │ └── not_found.html.erb │ ├── doj │ │ ├── analysis.html.erb │ │ ├── _doj_dashboard_sidenav.html.erb │ │ ├── overview.html.erb │ │ ├── window.html.erb │ │ ├── incidents.html.erb │ │ └── whosubmitted.html.erb │ ├── incidents │ │ ├── _dashboard_sidenav.html.erb │ │ ├── _dashboard_sidenav_item.html.erb │ │ ├── index.html.erb │ │ ├── _past_submissions_pane.html.erb │ │ └── upload.html.erb │ ├── devise │ │ ├── mailer │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── unlock_instructions.html.erb │ │ │ └── reset_password_instructions.html.erb │ │ ├── passwords │ │ │ ├── new.html.erb │ │ │ └── edit.html.erb │ │ ├── unlocks │ │ │ └── new.html.erb │ │ ├── sessions │ │ │ └── new.html.erb │ │ ├── confirmations │ │ │ └── new.html.erb │ │ ├── registrations │ │ │ ├── new.html.erb │ │ │ ├── _user_account_fields.erb │ │ │ └── edit.html.erb │ │ └── shared │ │ │ └── _links.html.erb │ ├── feedback │ │ ├── thank_you.html.erb │ │ └── new.html.erb │ ├── application │ │ ├── _errors.html.erb │ │ ├── _audit_entry.html.erb │ │ ├── _review_table.html.erb │ │ ├── _form_save_buttons.html.erb │ │ ├── _edit_person_headers.html.erb │ │ ├── _controls.html.erb │ │ ├── _display_item.html.erb │ │ ├── _yes_no_question.html.erb │ │ ├── _incident_id.html.erb │ │ ├── _breadcrumbs.html.erb │ │ ├── _incidents_table.html.erb │ │ ├── _received_force.html.erb │ │ └── siteminder_auth_fail.html.erb │ ├── bridge_mailer │ │ ├── feedback_email.txt │ │ └── feedback_email.html.erb │ ├── pages │ │ ├── maintenance.html.erb │ │ └── splash_whitelabel.html.erb │ └── screeners │ │ └── forms_not_necessary.html.erb ├── assets │ ├── images │ │ ├── favicon.ico │ │ ├── ursus_logo.png │ │ ├── ag_seal_gray.png │ │ ├── favicon_base.png │ │ ├── fbi-bw-large.png │ │ ├── bayes_logo_name.png │ │ ├── demo-watermark.png │ │ ├── bayes_logo_white.png │ │ ├── bayes_bridge_1600px.png │ │ ├── bayes_logo_triangle.png │ │ ├── bayes_bridge_uof_1600px.png │ │ ├── bayes_bridge_uof_600px.png │ │ ├── bayes_bridge_title_1600px.png │ │ ├── bayes_bridge_title_400px.png │ │ ├── bayes_bridge_title_600px.png │ │ ├── bayes_bridge_title_800px.png │ │ └── ursus_logo_cropped_notext.png │ ├── javascripts │ │ ├── splash.js │ │ ├── doj.js │ │ ├── incidents.unsaved.js │ │ ├── upload.js │ │ ├── analytics.js │ │ ├── demo.js │ │ ├── override │ │ │ └── ie8.js │ │ ├── util.js │ │ ├── incidents.address.js │ │ ├── application.js │ │ ├── controls.js │ │ ├── incidents.js │ │ ├── incidents.charges.js │ │ └── lib │ │ │ └── jquery.flexverticalcenter.js │ └── stylesheets │ │ ├── general_info.scss │ │ ├── _alerts.scss │ │ ├── override │ │ └── ie.css │ │ ├── tool_tip.scss │ │ ├── analytics.scss │ │ ├── screener.scss │ │ ├── _controls.scss │ │ ├── _footer.scss │ │ ├── upload.scss │ │ ├── doj_dashboard.scss │ │ ├── _breadcrumbs.scss │ │ ├── splash_one_col.scss │ │ ├── _dashboard_sidenav.scss │ │ ├── devise.scss │ │ ├── _style_reset.scss │ │ └── review.scss ├── helpers │ ├── application_helper.rb │ ├── doj_helper.rb │ ├── path_helper.rb │ ├── incident_helper.rb │ └── form_controls_helper.rb ├── controllers │ ├── involved_civilians_controller.rb │ ├── involved_officers_controller.rb │ ├── monitoring_controller.rb │ ├── splash_controller.rb │ ├── feedback_controller.rb │ ├── users │ │ └── sessions_controller.rb │ ├── screeners_controller.rb │ ├── steps_base_controller.rb │ └── doj_controller.rb ├── models │ ├── feedback.rb │ ├── event.rb │ ├── concerns │ │ ├── validates_uniqueness.rb │ │ ├── allows_partial_save.rb │ │ ├── incident_authorization.rb │ │ ├── can_lookup_fields.rb │ │ └── has_only_one_instance.rb │ ├── constants │ │ ├── user.rb │ │ ├── constants.rb │ │ ├── involved_officer.rb │ │ ├── general_info.rb │ │ └── involved_person.rb │ ├── audit_entry.rb │ ├── visit.rb │ ├── agency_status.rb │ ├── incident_id.rb │ ├── involved_officer.rb │ └── screener.rb ├── validators │ ├── incident_time_validator.rb │ ├── ori_validator.rb │ ├── civilian_mental_status_validator.rb │ ├── subset_validator.rb │ ├── uniqueness_validator.rb │ └── incident_date_validator.rb ├── queries │ ├── query.rb │ ├── employee_draft_counts_query.rb │ ├── past_submissions_query.rb │ ├── get_all_incidents_query.rb │ ├── dashboard_incidents_query.rb │ └── compute_analytics_query.rb ├── mailers │ └── bridge_mailer.rb └── services │ ├── bulk_import_service.rb │ └── incident_stats_service.rb ├── notebooks ├── requirements-notebooks.py └── Dockerfile.notebooks ├── public ├── favicon.ico └── robots.txt ├── config ├── initializers │ ├── gems │ │ ├── gaffe.rb │ │ └── dynamo_db.rb │ ├── rails │ │ ├── cookies_serializer.rb │ │ ├── session_store.rb │ │ ├── filter_parameter_logging.rb │ │ ├── wrap_parameters.rb │ │ └── assets.rb │ └── custom │ │ ├── lib.rb │ │ ├── mail.rb │ │ ├── ori.rb │ │ ├── branding.rb │ │ ├── aws.rb │ │ └── views.rb ├── boot.rb ├── environment.rb ├── aws.yml ├── secrets.yml ├── application.rb ├── routes.rb └── environments │ ├── development.rb │ └── test.rb ├── .dockerignore ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── config.ru ├── spec ├── factories │ ├── screener.rb │ ├── user.rb │ ├── involved_officer.rb │ ├── general_info.rb │ ├── involved_civilian.rb │ └── incident.rb ├── mailers │ ├── previews │ │ └── bridge_mailer_preview.rb │ └── bridge_mailer_spec.rb ├── requests │ ├── monitoring_spec.rb │ ├── maintenance_spec.rb │ ├── splash_page_spec.rb │ ├── feedback_spec.rb │ ├── visit_tracking_spec.rb │ ├── login_with_devise_spec.rb │ ├── incident_id_spec.rb │ └── devise_signup_spec.rb ├── controllers │ └── application_controller_spec.rb └── models │ └── agency_status_spec.rb ├── lib ├── core_extensions │ ├── time.rb │ ├── array.rb │ ├── string.rb │ └── integer.rb ├── tasks │ ├── maintenance.rake │ ├── migrations.rake │ └── siteminder_cookie_handling.rake ├── bridge_exceptions.rb └── siteminder.rb ├── data ├── ori.csv.example ├── README.md ├── scripts │ ├── cleaning_helpers.py │ ├── generate_js_from_csv.py │ ├── generate_ori_js.py │ ├── generate_ori_rb.py │ ├── clean_qualifiers.py │ ├── generate_ori_contracts_rb.py │ └── cleanup_csv.py └── raw │ └── vcco_county.csv ├── precompile_and_serve.sh ├── Rakefile ├── Dockerfile.data ├── Dockerfile.mailcatcher ├── .gitattributes ├── Dockerfile.test ├── Dockerfile.test-devise ├── CONTRIBUTING.md ├── .rubocop.metrics.yml ├── LICENSE ├── should_run_ci.sh ├── Dockerfile ├── .gitignore ├── .rubocop.yml ├── Gemfile └── circle.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /notebooks/requirements-notebooks.py: -------------------------------------------------------------------------------- 1 | xlrd 2 | boto3 3 | -------------------------------------------------------------------------------- /app/views/errors/internal_server_error.html.erb: -------------------------------------------------------------------------------- 1 | Something went wrong. 2 | -------------------------------------------------------------------------------- /app/views/errors/bad_request.html.erb: -------------------------------------------------------------------------------- 1 | You are not authorized to view that page. 2 | -------------------------------------------------------------------------------- /app/views/errors/not_found.html.erb: -------------------------------------------------------------------------------- 1 | You tried to go to a page that doesn't exist. 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /config/initializers/gems/gaffe.rb: -------------------------------------------------------------------------------- 1 | # Gaffe-related configuration goes here. 2 | 3 | Gaffe.enable! 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | log 2 | tmp 3 | 4 | .bundle 5 | .git 6 | *.sublime* 7 | *.tar 8 | dynamodb 9 | 10 | -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/views/doj/analysis.html.erb: -------------------------------------------------------------------------------- 1 |

Analysis coming soon    

2 | -------------------------------------------------------------------------------- /app/assets/images/ursus_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/ursus_logo.png -------------------------------------------------------------------------------- /app/assets/images/ag_seal_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/ag_seal_gray.png -------------------------------------------------------------------------------- /app/assets/images/favicon_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/favicon_base.png -------------------------------------------------------------------------------- /app/assets/images/fbi-bw-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/fbi-bw-large.png -------------------------------------------------------------------------------- /app/assets/images/bayes_logo_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_logo_name.png -------------------------------------------------------------------------------- /app/assets/images/demo-watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/demo-watermark.png -------------------------------------------------------------------------------- /notebooks/Dockerfile.notebooks: -------------------------------------------------------------------------------- 1 | FROM jupyter/scipy-notebook:latest 2 | COPY * ./ 3 | RUN pip3 install -r requirements-notebooks.py 4 | -------------------------------------------------------------------------------- /app/assets/images/bayes_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_logo_white.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_1600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_1600px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_logo_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_logo_triangle.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_uof_1600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_uof_1600px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_uof_600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_uof_600px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_title_1600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_title_1600px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_title_400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_title_400px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_title_600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_title_600px.png -------------------------------------------------------------------------------- /app/assets/images/bayes_bridge_title_800px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/bayes_bridge_title_800px.png -------------------------------------------------------------------------------- /app/assets/images/ursus_logo_cropped_notext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayesimpact/bridge-uof/HEAD/app/assets/images/ursus_logo_cropped_notext.png -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /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/assets/javascripts/splash.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('#splash .inner').flexVerticalCenter(); 3 | $('#splash-one-col .inner').flexVerticalCenter(); 4 | }); 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general_info.scss: -------------------------------------------------------------------------------- 1 | .ui-datepicker-current-day a { 2 | background-color: #FDD; 3 | color: red; 4 | border-radius: 2px; 5 | font-weight: bold; 6 | } 7 | -------------------------------------------------------------------------------- /config/initializers/rails/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/factories/screener.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :screener do 3 | id { SecureRandom.uuid } 4 | multiple_agencies false 5 | shots_fired true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/rails/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: '_ab71_session' 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_alerts.scss: -------------------------------------------------------------------------------- 1 | .alert-dismissable { 2 | position: relative; 3 | } 4 | 5 | .alert-dismissable > .close { 6 | position: absolute; 7 | top: 0; 8 | right: 6px; 9 | } 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= yield %> 7 | 8 | 9 | -------------------------------------------------------------------------------- /config/initializers/custom/lib.rb: -------------------------------------------------------------------------------- 1 | # Load all of our lib code. 2 | Dir[File.join(Rails.root, "lib", "*.rb")].each { |l| require l } 3 | Dir[File.join(Rails.root, "lib", "core_extensions", "*.rb")].each { |l| require l } 4 | -------------------------------------------------------------------------------- /lib/core_extensions/time.rb: -------------------------------------------------------------------------------- 1 | # Monkey-patches to the Time class go here. 2 | class Time 3 | def self.this_year 4 | current.year 5 | end 6 | 7 | def self.last_year 8 | this_year - 1 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Generic helper methods that don't belong in other Helper modules go here. 2 | module ApplicationHelper 3 | def title(page_title) 4 | content_for(:title) { page_title } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /data/ori.csv.example: -------------------------------------------------------------------------------- 1 | AGENCY_NAME,COUNTY,ORI,CONTRACTS_TO_ORI 2 | Bayes Impact Police Department,SAN FRANCISCO,CA0012345, 3 | Rubin Sheriff's Office,ORANGE,CA0027777, 4 | Thomas City Police Department,ORANGE,CA0028888,CA0027777 5 | -------------------------------------------------------------------------------- /app/controllers/involved_civilians_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for Involved Civilians form. 2 | class InvolvedCiviliansController < InvolvedPersonsController 3 | private 4 | 5 | def person_type 6 | :civilian 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/involved_officers_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for Involved Officers form. 2 | class InvolvedOfficersController < InvolvedPersonsController 3 | private 4 | 5 | def person_type 6 | :officer 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /precompile_and_serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on first error. 4 | set -e 5 | 6 | # Precompile assets. 7 | if [ "${RAILS_ENV}" == "production" ]; then 8 | rake assets:precompile 9 | fi 10 | 11 | # Serve. 12 | rails server $@ 13 | -------------------------------------------------------------------------------- /app/views/layouts/doj.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :sidenav do %> 2 | <%= render partial: 'doj_dashboard_sidenav' %> 3 | <% end %> 4 | 5 | <% content_for :panel do %> 6 | <%= yield %> 7 | <% end %> 8 | <%= render template: "layouts/dashboard" %> 9 | -------------------------------------------------------------------------------- /config/initializers/rails/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 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /app/views/incidents/_dashboard_sidenav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% @status_texts.each do |status, text| %> 3 | <%= render partial: "dashboard_sidenav_item", locals: {text: text, status: status ? status.to_sym : nil} %> 4 | <% end %> 5 |
6 | -------------------------------------------------------------------------------- /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/feedback.rb: -------------------------------------------------------------------------------- 1 | # A piece of user feedback. 2 | class Feedback 3 | include Dynamoid::Document 4 | include CanLookupFields 5 | 6 | belongs_to :user 7 | 8 | field :source, :string 9 | field :content, :string 10 | 11 | validates :content, presence: true 12 | end 13 | -------------------------------------------------------------------------------- /app/views/feedback/thank_you.html.erb: -------------------------------------------------------------------------------- 1 | <%= title "Thank you" %> 2 | 3 |
4 | 5 |

Thank you for your feedback, we'll get back to you soon!

6 | 7 | <%= link_to 'Back to Dashboard', '/', class: 'btn-bayes' %> 8 | 9 |
10 | -------------------------------------------------------------------------------- /app/views/layouts/partials/_alerts.html.erb: -------------------------------------------------------------------------------- 1 | <% if alert %> 2 |

<%= alert %> 3 | 6 |

7 | <% end %> -------------------------------------------------------------------------------- /app/views/layouts/partials/_notices.html.erb: -------------------------------------------------------------------------------- 1 | <% if notice %> 2 |

<%= notice %> 3 | 6 |

7 | <% end %> -------------------------------------------------------------------------------- /app/views/incidents/_dashboard_sidenav_item.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /config/aws.yml: -------------------------------------------------------------------------------- 1 | # AWS configuration. 2 | development: 3 | access_key_id: AKIA1111111111111UA 4 | secret_access_key: secret 5 | endpoint: http://localhost:8000 6 | 7 | test: 8 | access_key_id: AKIA1111111111111UA 9 | secret_access_key: secret 10 | endpoint: http://localhost:8001 11 | -------------------------------------------------------------------------------- /Dockerfile.data: -------------------------------------------------------------------------------- 1 | FROM tailordev/pandas:0.17.1 2 | 3 | RUN mkdir -p /code/data 4 | WORKDIR /code 5 | COPY data/raw data/raw 6 | COPY data/scripts data/scripts 7 | COPY app app 8 | COPY Makefile . 9 | 10 | RUN /usr/bin/make clean 11 | RUN /usr/local/bin/pip install xlrd 12 | 13 | CMD ["make", "all"] 14 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../../config/application', __FILE__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/doj.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | // Add autocomplete for agency ori search bar 3 | $('#doj_incidents_ori_box').autocomplete({ 4 | source: window.AGENCY_ORI, 5 | select: function (event, ui) { 6 | window.location.href = '/doj/incidents/' + ui.item.label; 7 | } 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /lib/tasks/maintenance.rake: -------------------------------------------------------------------------------- 1 | namespace :maintenance do 2 | desc "Start maintenance mode" 3 | task start: :environment do 4 | GlobalState.start_maintenance_mode! 5 | end 6 | 7 | desc "Stop maintenance mode" 8 | task stop: :environment do 9 | GlobalState.stop_maintenance_mode! 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/layouts/devise.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render "layouts/partials/head" %> 4 | 5 | <%= render "layouts/partials/nav" %> 6 |
7 |
8 |
9 | <%= render "layouts/partials/alerts" %> 10 | <%= yield %> 11 |
12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/controllers/monitoring_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for monitoring endpoints. 2 | # Note this controller inherits from ActionController::Base, 3 | # NOT ApplicationController, so it doesn't perform use authentication, 4 | # tracking, etc. 5 | class MonitoringController < ActionController::Base 6 | def ping 7 | render plain: "pong" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/event.rb: -------------------------------------------------------------------------------- 1 | # An single controller action, tracked by Ahoy. 2 | # Each individual user visit will likely consist of many events. 3 | class Event 4 | include Dynamoid::Document 5 | 6 | # associations 7 | belongs_to :visit 8 | belongs_to :user 9 | 10 | # fields 11 | field :name 12 | field :properties 13 | field :time, :datetime 14 | end 15 | -------------------------------------------------------------------------------- /app/validators/incident_time_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates that a time is in the appropriate format. 2 | class IncidentTimeValidator < ActiveModel::EachValidator 3 | def validate_each(record, attribute, value) 4 | unless value =~ /^(([01][0-9])|(2[0-3]))[0-5][0-9]$/i 5 | record.errors[attribute] << "must be 4 digits, between 0000 and 2359" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/validators/ori_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates that the incident ORI is allowed. 2 | class OriValidator < ActiveModel::EachValidator 3 | def validate_each(record, attribute, value) 4 | user = record.incident.target.try(:user) 5 | 6 | if user && user.allowed_oris.exclude?(value) 7 | record.errors[attribute] << "invalid ORI: #{value}" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render "layouts/partials/head" %> 4 | 5 |
6 | <%= render "layouts/partials/nav" %> 7 |
8 | <%= content_for?(:body) ? yield(:body) : yield %> 9 |
10 | <%= render "layouts/partials/footer" %> 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/override/ie.css: -------------------------------------------------------------------------------- 1 | /* IE(8,9)-specific CSS hacks. */ 2 | 3 | input:not([type="checkbox"]) { 4 | /* Prevents text being cut off in text input fields on IE9. */ 5 | line-height: 1; 6 | } 7 | 8 | table.table-bordered { 9 | /* Without this, borders don't always render on bootstrap tables due to IE weirdness. */ 10 | border-collapse: separate !important; 11 | } 12 | -------------------------------------------------------------------------------- /app/views/application/_errors.html.erb: -------------------------------------------------------------------------------- 1 | <% record.errors.clear if record['partial'] %> 2 | <% if (record.is_a? Array and record.map { |e| e.errors.any? }.any?) or (!record.is_a? Array and record.errors.any?) %> 3 |
4 |

There were problems with your form. Scroll down to see highlighted errors.

5 |
6 | <% end %> -------------------------------------------------------------------------------- /Dockerfile.mailcatcher: -------------------------------------------------------------------------------- 1 | FROM ruby:2.2.3 2 | 3 | MAINTAINER everett.wetchler@bayesimpact.org 4 | 5 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev 6 | RUN gem install mailcatcher 7 | 8 | EXPOSE 1025 9 | EXPOSE 1080 10 | #CMD ["/usr/local/bundle/bin/mailcatcher", "-f", "--ip", "0.0.0.0", "--http-ip", "0.0.0.0"] 11 | CMD ["/usr/local/bundle/bin/mailcatcher", "-f"] 12 | -------------------------------------------------------------------------------- /app/helpers/doj_helper.rb: -------------------------------------------------------------------------------- 1 | # Helper methods pertaining to the DOJ dashboard. 2 | module DojHelper 3 | def submission_window_button_text 4 | if GlobalState.submission_open? 5 | "Close the window now" 6 | elsif GlobalState.submission_not_yet_open? 7 | "Open the window now" 8 | else 9 | "Re-open the window for #{Time.current.year - 1}" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/concerns/validates_uniqueness.rb: -------------------------------------------------------------------------------- 1 | # Adds validates_uniqueness_of() to models (see validators/uniqueness_validator.rb). 2 | module ValidatesUniqueness 3 | extend ActiveSupport::Concern 4 | 5 | # [Class methods.] 6 | module ClassMethods 7 | def validates_uniqueness_of(*atts) 8 | validates_with(UniquenessValidator, _merge_attributes(atts)) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :dummy_user, class: User do 3 | first_name 'Dummy' 4 | last_name 'User' 5 | email 'test@example.com' 6 | password 'password' if Rails.configuration.x.login.use_devise? 7 | ori 'ORI01234' 8 | role Rails.configuration.x.roles.admin 9 | department 'Foo Police Department' 10 | user_id 'some_user_id_123' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/mailers/previews/bridge_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/ursus_mailer 2 | class BridgeMailerPreview < ActionMailer::Preview 3 | def feedback_email 4 | feedback = Feedback.new(source: "Some part of the page (test)", 5 | content: "This was confusing (test)") 6 | BridgeMailer.feedback_email(feedback, User.all.first) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | app/assets/javascripts/lib/* linguist-vendored 2 | app/assets/stylesheets/lib/* linguist-vendored 3 | 4 | app/assets/javascripts/crimes.js linguist-vendored 5 | app/assets/javascripts/crime_qualifiers.js linguist-vendored 6 | app/assets/javascripts/police_agency_oris.js linguist-vendored 7 | app/models/constants/department_by_ori.rb linguist-vendored 8 | app/models/constants/contracting_oris.rb linguist-vendored 9 | -------------------------------------------------------------------------------- /app/controllers/splash_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for splash pages. 2 | class SplashController < ApplicationController 3 | skip_before_action :consider_splash 4 | 5 | def splash_show 6 | render "pages/splash_#{Rails.configuration.x.branding.name}", layout: nil 7 | end 8 | 9 | def splash_dismiss 10 | @current_user.update_attributes(splash_complete: true) 11 | redirect_to dashboard_path 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/application/_audit_entry.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= audit_entry_description(entry) %> 4 | <% if show_changed_fields_for_audit_entry?(entry) %> 5 | 10 | <% end %> 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/views/bridge_mailer/feedback_email.txt: -------------------------------------------------------------------------------- 1 | URSUS feedback from <%= @user.full_name %> 2 | Source: 3 | 4 | <%= @feedback.source %> 5 | 6 | Content: 7 | 8 | <%= @feedback.content %> 9 | 10 | User info: 11 | 12 | Name: <%= @user.full_name %> 13 | ORI: <%= @user.ori %> 14 | Department: <%= @user.department %> 15 | Role: <%= @user.role %> 16 | Email: <%= @user.email %> 17 | 18 | Sent at <%= @feedback.created_at %> 19 | -------------------------------------------------------------------------------- /lib/core_extensions/array.rb: -------------------------------------------------------------------------------- 1 | # Monkey-patches to the Array class go here. 2 | class Array 3 | # (Used in FactoryGirl factories.) 4 | # Usually samples a single element from an array, but sometimes samples two. 5 | def sample_one_or_two_elements 6 | sample(rand(5).zero? ? 2 : 1) 7 | end 8 | 9 | # (Used in tests.) 10 | # Check if an array is sorted. 11 | def sorted? 12 | self == sort 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/validators/civilian_mental_status_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates that a mental status list doesn't include both 'None' and something else. 2 | class CivilianMentalStatusValidator < ActiveModel::EachValidator 3 | def validate_each(record, attribute, value) 4 | if value.present? && value.include?('None') && value.length >= 2 5 | record.errors[attribute] << "cannot specify both 'None' and another value" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tasks/migrations.rake: -------------------------------------------------------------------------------- 1 | require 'dynamodb/migration' 2 | 3 | namespace :db do 4 | desc "Run all DynamoDB migrations" 5 | task migrate: :environment do 6 | aws_client = Dynamoid.adapter.adapter.client 7 | DynamoDB::Migration.run_all_migrations( 8 | client: aws_client, 9 | path: Rails.root.join('db/migrations'), 10 | migration_table_name: "#{Dynamoid.config.namespace}_migrations" 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/rails/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] if respond_to?(:wrap_parameters) 9 | end 10 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | ## `data/` Contents 2 | 3 | - `data/raw` contains raw files received from our California DoJ partners, roughly 2016-03-14. 4 | - `data/scripts` contains Python scripts that are run by the Makefile to process the raw data. 5 | - `ori.csv` **must be provided** for the application to run. If you don't have a list of police agencies in your state, you can copy over the provided example file: 6 | ``` 7 | cp data/ori.csv.example data/ori.csv 8 | ``` 9 | -------------------------------------------------------------------------------- /spec/requests/monitoring_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe '[Monitoring endpoints, no login]', type: :request do 4 | it '/ping.html renders "pong"' do 5 | visit '/ping.html' 6 | expect(page.status_code).to eq(200) 7 | expect(page.body).to eq("pong") 8 | end 9 | 10 | it '/ping renders "pong"' do 11 | visit 'ping' 12 | expect(page.status_code).to eq(200) 13 | expect(page.body).to eq("pong") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/rails/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 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | Rails.application.config.assets.precompile += %w(fastforward.js override/ie8.js override/ie.css) 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/tool_tip.scss: -------------------------------------------------------------------------------- 1 | .help-tip { 2 | margin-left: 6px; 3 | text-align: center; 4 | background-color: #BCDBEA; 5 | border-radius: 50%; 6 | width: 16px; 7 | height: 16px; 8 | font-size: 16px; 9 | line-height: 16px; 10 | display: inline-block; 11 | cursor: pointer; 12 | 13 | &:hover { 14 | background-color: #666; 15 | } 16 | 17 | &:before{ 18 | content: '?'; 19 | font-weight: bold; 20 | color: #fff; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/incidents.unsaved.js: -------------------------------------------------------------------------------- 1 | var unsavedChanges = false; 2 | 3 | $(function () { 4 | $('#incidentStep').on('change keyup keydown', 'input, textarea, select', function (e) { 5 | unsavedChanges = true; 6 | }); 7 | 8 | $(document).submit(function() { 9 | unsavedChanges = false; 10 | }); 11 | 12 | $(window).on('beforeunload', function () { 13 | if (unsavedChanges) { 14 | return 'You have unsaved changes.'; 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /app/models/constants/user.rb: -------------------------------------------------------------------------------- 1 | module Constants 2 | # Constants related to the User model. 3 | module User 4 | SITEMINDER_KEY_MAPPING = { 5 | 'givenname' => :first_name, 6 | 'sn' => :last_name, 7 | 'mail' => :email, 8 | 'dojORI' => :ori, 9 | 'dojagencyName' => :department, 10 | 'SM_USERGROUPS' => :role, 11 | 'SM_USERLOGINNAME' => :user_id 12 | }.freeze 13 | USER_FIELDS = SITEMINDER_KEY_MAPPING.values.freeze 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/queries/query.rb: -------------------------------------------------------------------------------- 1 | # Base class for queries. Subclasses must implement #perform_query. 2 | class Query 3 | def run(params) 4 | # Request-level memoization. 5 | key = params.merge(query: self.class.name).to_s 6 | result = RequestStore.store[key] ||= perform_query(params) 7 | result.clone # (Just in case another query tries to mutate the result.) 8 | end 9 | 10 | private 11 | 12 | def perform_query 13 | raise UnimplementedError.new 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Forgotten 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.email_field :email, autofocus: true, placeholder: "Email" %> 8 |
9 | 10 |
11 | <%= f.submit "Send instructions" %> 12 |
13 | <% end %> 14 | 15 | <%= render "devise/shared/links" %> 16 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM bayesimpact/ruby-2.2.3-phantomjs 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs 4 | RUN mkdir /bridge-uof 5 | WORKDIR /bridge-uof 6 | COPY Gemfile /bridge-uof/Gemfile 7 | COPY Gemfile.lock /bridge-uof/Gemfile.lock 8 | RUN bundle install --with=test 9 | 10 | COPY . /bridge-uof 11 | RUN sed -i -e "s/localhost:/testdb:/" /bridge-uof/config/aws.yml 12 | ENTRYPOINT ["bundle", "exec", "rspec", "--profile", "15", "--format", "documentation"] 13 | -------------------------------------------------------------------------------- /Dockerfile.test-devise: -------------------------------------------------------------------------------- 1 | FROM bayesimpact/ruby-2.2.3-phantomjs 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs 4 | RUN mkdir /bridge-uof 5 | WORKDIR /bridge-uof 6 | COPY Gemfile /bridge-uof/Gemfile 7 | COPY Gemfile.lock /bridge-uof/Gemfile.lock 8 | RUN bundle install --with=test 9 | 10 | COPY . /bridge-uof 11 | RUN sed -i -e "s/localhost:8001/testdb-devise:8002/" /bridge-uof/config/aws.yml 12 | ENTRYPOINT ["bundle", "exec", "rspec", "--format", "documentation"] 13 | -------------------------------------------------------------------------------- /app/assets/javascripts/upload.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('#showHideSchema').toggleLinkFor($('#schema')); 3 | 4 | $('#xmlSchema').hide(); 5 | $('.toggle-schema').click(function () { 6 | $('#jsonSchema').toggle(); 7 | $('#xmlSchema').toggle(); 8 | }); 9 | 10 | $('#upload-control').submit(function (event) { 11 | var fileButton = $(".btn-file"); 12 | $('.btn-file').css({'opacity': 0.5, 'background-color': 'gray'}); 13 | $("#fileButtonText").html("Uploading...."); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/views/doj/_doj_dashboard_sidenav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% [ 3 | ["Overview", "overview"], 4 | ["Submission window", "window"], 5 | ["Who has submitted?", "whosubmitted"], 6 | ["View incidents", "incidents"], 7 | ["Analysis", "analysis"] 8 | ].each do |item| %> 9 |
  • 10 | <%= link_to item[0], url_for(action: item[1], controller: 'doj') %> 11 |
  • 12 | <% end %> 13 |
    14 | -------------------------------------------------------------------------------- /app/views/doj/overview.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Welcome, <%= @current_user.full_name %>

    3 |

    Department of Justice

    4 |

    You are a DOJ superuser. Use the menu on the left to 5 | perform your duties. You can 6 | open and close the submission window, 7 | see which agencies have submitted, 8 | view and edit incidents submitted by police agencies, 9 | and analyze the incident submissions in aggregate. 10 |

    11 |
    12 | -------------------------------------------------------------------------------- /app/views/application/_review_table.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% for key in display_fields %> 3 | 4 | <%= render partial: "display_item", locals: { 5 | key: labels[key] || key.to_s.custom_humanize, 6 | value: (data.send(key) rescue nil), 7 | is_fbi_field: fbi_fields.include?(key), 8 | partial: local_assigns[:partials].try { |p| p[key.to_sym] } 9 | } %> 10 | 11 | <% end %> 12 |
    13 | -------------------------------------------------------------------------------- /app/models/audit_entry.rb: -------------------------------------------------------------------------------- 1 | # An entry in the audit log. 2 | class AuditEntry 3 | include Dynamoid::Document 4 | 5 | has_one :user 6 | has_many :changed_fields 7 | field :custom_text, :string 8 | field :page, :string 9 | field :is_new, :boolean 10 | end 11 | 12 | # A field that has been changed within an AuditEntry. 13 | class ChangedField 14 | include Dynamoid::Document 15 | 16 | belongs_to :audit_entry 17 | field :key, :string 18 | field :old_value, :string 19 | field :new_value, :string 20 | end 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/custom/mail.rb: -------------------------------------------------------------------------------- 1 | # Load and validate email configuration. 2 | 3 | mail_config = Rails.application.config.x.mail 4 | 5 | unless ENV['MAIL_FROM'].present? 6 | raise 'Must set MAIL_FROM to an email address for the app\'s outgoing emails.' 7 | end 8 | mail_config.from_address = ENV['MAIL_FROM'] 9 | 10 | unless ENV['FEEDBACK_MAIL_TO'].present? 11 | raise 'Must set FEEDBACK_MAIL_TO to an email address for the app to send user feedback.' 12 | end 13 | mail_config.feedback_to_address = ENV['FEEDBACK_MAIL_TO'] 14 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Login

    2 | 3 | <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> 4 |
    5 | <%= f.email_field :email, autofocus: true, placeholder: "Email" %> 6 |
    7 | 8 |
    9 | <%= f.password_field :password, autocomplete: "off", placeholder: "Password" %> 10 |
    11 | 12 |
    13 | <%= f.submit "Login" %> 14 |
    15 | <% end %> 16 | 17 | <%= render "devise/shared/links" %> 18 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/bridge_mailer/feedback_email.html.erb: -------------------------------------------------------------------------------- 1 |

    URSUS feedback from <%= @user.full_name %>

    2 |

    Source:

    3 |

    4 | <%= @feedback.source %> 5 |

    6 |

    Content:

    7 |

    8 | <%= @feedback.content %> 9 |

    10 |

    User info:

    11 | 18 |

    Sent at <%= @feedback.created_at %>

    19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | We welcome contributions to Bridge. 4 | 5 | By submitting a pull request, you represent that you have the right to license your contribution to Bayes Impact and the community, and agree by submitting the patch that your contributions are licensed under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0). 6 | 7 | Before submitting the pull request, please make sure you have tested your changes 8 | and that they follow our code quality guidelines (a good way to check is to run the 9 | `rubocop` linter). 10 | -------------------------------------------------------------------------------- /app/controllers/feedback_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for feedback form. 2 | class FeedbackController < ApplicationController 3 | def new 4 | @feedback = Feedback.new 5 | end 6 | 7 | def create 8 | @feedback = Feedback.new(get_formatted_params(params, Feedback)) 9 | if @feedback.save 10 | @current_user.feedbacks << @feedback 11 | BridgeMailer.feedback_email(@feedback, @current_user).deliver_now 12 | redirect_to :thank_you 13 | else 14 | render :new 15 | end 16 | end 17 | 18 | def thank_you 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /.rubocop.metrics.yml: -------------------------------------------------------------------------------- 1 | # To assess code quality metrics with rubocop (not computed normally), run: 2 | # rubocop -c .rubocop.metrics.yml --only Metrics app config lib vendor 3 | 4 | Metrics/AbcSize: 5 | Max: 9999 # ABC, CyclomaticComplexity, and PerceivedComplexity are very similar metrics, so disable all but the last. 6 | 7 | Metrics/CyclomaticComplexity: 8 | Max: 9999 # ABC, CyclomaticComplexity, and PerceivedComplexity are very similar metrics, so disable all but the last. 9 | 10 | Metrics/LineLength: 11 | Max: 120 12 | 13 | Metrics/MethodLength: 14 | Max: 15 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/analytics.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | // Selecting a new year automatically redirects to that page. 3 | $('#analytics-year').change(function () { 4 | window.location.href = $(this).val(); 5 | }); 6 | }); 7 | 8 | function renderPivotTable(year) { 9 | var jsonPath = year ? "/incidents.json?year=" + year : "/incidents.json"; 10 | $.getJSON(jsonPath, function(data) { 11 | $("#pivot-table").pivotUI(data, { 12 | rows: ["Officer Force Used"], 13 | cols: ["Civilian Armed?"], 14 | rendererName: "Table" 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /app/assets/javascripts/demo.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | // #hideDemoWarning button immediately hides the demo warning and sets a cookie. 3 | $('#hideDemoWarning').click(function () { 4 | $('#demoWarning').hide(); 5 | positionControls(); // (see controls.js) 6 | 7 | document.cookie = 'hide_demo_msg=true'; 8 | }); 9 | $('#generateFakeIncidentsButton').click(function (event) { 10 | $("#dashboardColumn div.alert").css("opacity", "0.5"); 11 | $(this).val("Generating....").prop("disabled", true); 12 | $(this).parents('form').submit(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /app/queries/employee_draft_counts_query.rb: -------------------------------------------------------------------------------- 1 | # Returns the number of drafts still being filled out by each employee in the user's ORI. 2 | class EmployeeDraftCountsQuery < Query 3 | private 4 | 5 | def perform_query(params) 6 | raise ActionController::BadRequest.new unless params[:user].admin? 7 | 8 | all_incidents = GetAllIncidentsQuery.new.run(user: params[:user]) 9 | 10 | all_incidents.each_with_object(Hash.new(0)) do |i, counts| 11 | counts[i.user.full_name] += 1 if i.draft? && (i.user != params[:user]) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/requests/maintenance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe '[Maintenance mode]', type: :request do 4 | it 'Maintenance mode can be switched on and off' do 5 | login(dont_handle_splash: true) 6 | 7 | visit root_path 8 | expect(page).to have_content('Welcome') 9 | 10 | GlobalState.start_maintenance_mode! 11 | 12 | visit root_path 13 | expect(page).to have_content('Down for Maintenance') 14 | 15 | GlobalState.stop_maintenance_mode! 16 | 17 | visit root_path 18 | expect(page).to have_content('Welcome') 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/mailers/bridge_mailer.rb: -------------------------------------------------------------------------------- 1 | # Mailer class that sends out feedback emails. 2 | class BridgeMailer < ActionMailer::Base 3 | default from: Rails.configuration.x.mail.from_address 4 | layout 'mailer' 5 | 6 | def feedback_email(feedback, user) 7 | @feedback = feedback 8 | @user = user 9 | 10 | mail(to: Rails.configuration.x.mail.feedback_to_address, 11 | cc: (user.email unless Rails.configuration.x.login.use_demo?), 12 | subject: "#{Rails.configuration.x.branding.ursus? ? 'URSUS feedback' : 'Feedback'} from #{@user.full_name}") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/core_extensions/string.rb: -------------------------------------------------------------------------------- 1 | # Monkey-patches to the String class go here. 2 | class String 3 | # http://stackoverflow.com/questions/1235863/test-if-a-string-is-basically-an-integer-in-quotes-using-ruby 4 | def integer? 5 | to_i.to_s == self 6 | end 7 | 8 | # Convert the string to an integer only if it is actually an integer, return nil otherwise. 9 | def to_i_safe 10 | integer? ? to_i : nil 11 | end 12 | 13 | # Like #humanize but handles *_id strings better. 14 | def custom_humanize 15 | end_with?('_id') ? (humanize + " ID") : humanize 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/validators/subset_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates that a record is a subset of a given list. 2 | class SubsetValidator < ActiveModel::EachValidator 3 | include ActiveModel::Validations::Clusivity 4 | 5 | def validate_each(record, attribute, values) 6 | return if values.nil? 7 | 8 | values.uniq.each do |value| 9 | unless include?(record, value) 10 | record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value)) 11 | end 12 | end 13 | 14 | record.errors.add(attribute, :duplicate) unless values.uniq.length == values.length 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Bridge is (C) 2016 Bayes Impact 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /app/helpers/path_helper.rb: -------------------------------------------------------------------------------- 1 | # Helpers related to paths. 2 | # Also included in ApplicationController. 3 | module PathHelper 4 | def edit_person_path(type, incident, num) 5 | send("edit_incident_involved_#{type}_path", incident, num) 6 | end 7 | 8 | def new_person_path(type, incident) 9 | send("new_incident_involved_#{type}_path", incident) 10 | end 11 | 12 | def analytics_path(year) 13 | dashboard_path(status: 'analytics', year: year) 14 | end 15 | 16 | def incident_base_url(incident_url) 17 | parts = incident_url.split("/") 18 | "/#{parts[1]}/#{parts[2]}" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /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.email_field :email, autofocus: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), placeholder: "Email" %> 8 |
    9 | 10 |
    11 | <%= f.submit "Resend confirmation instructions" %> 12 |
    13 | <% end %> 14 | 15 | <%= render "devise/shared/links" %> 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/analytics.scss: -------------------------------------------------------------------------------- 1 | .visualization { 2 | position: relative; 3 | } 4 | 5 | // We add a persistent watermark to charts in the demo app, so all screenshots 6 | // will clearly show that the data is not live. 7 | .demo-watermark { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | height: 100%; 12 | width: 100%; 13 | opacity: 0.5; 14 | background-image: url(asset-path('demo-watermark.png')); 15 | background-repeat: repeat; 16 | z-index: 99; 17 | pointer-events: none; 18 | } 19 | 20 | #pivotInstructions { 21 | font-size: 16px; 22 | } 23 | 24 | #notes { 25 | margin-top: 1em; 26 | } 27 | -------------------------------------------------------------------------------- /app/views/layouts/dashboard.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :body do %> 2 | <%= render partial: "controls" %> 3 |
    4 |
    5 |
    6 | <%= yield :sidenav %> 7 |
    8 |
    9 | <% if flash[:notice] %> 10 |
    <%= flash[:notice] %>
    11 | <% end %> 12 | <%= yield :panel %> 13 |
    14 |
    15 |
    16 | 17 | <% end %> 18 | <%= render template: "layouts/application" %> -------------------------------------------------------------------------------- /app/views/application/_form_save_buttons.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 12 | <% unless local_assigns[:no_partial_save] %> 13 |     or     14 | <%= link_to "I'm not done yet, save my progress so far", {}, id: "save_and_return" %> 15 | <% end %> -------------------------------------------------------------------------------- /should_run_ci.sh: -------------------------------------------------------------------------------- 1 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 2 | 3 | # Always run CI in master. 4 | if [ "$BRANCH" != "master" ]; then 5 | # Do not run CI if the branch doesn't exist anymore. 6 | if [ -z "$(git ls-remote --heads origin "$BRANCH")" ]; then 7 | echo "Branch doesn't exist anymore - skipping tests ..." 8 | touch skip-tests 9 | exit 10 | fi 11 | 12 | # Do not run CI if the branch has already been updated. 13 | if [ "$(git ls-remote --heads origin "$BRANCH" | cut -f1)" != "$(git rev-parse HEAD)" ]; then 14 | echo "Branch is no longer up-to-date - skipping tests ..." 15 | touch skip-tests 16 | exit 17 | fi 18 | fi 19 | -------------------------------------------------------------------------------- /lib/core_extensions/integer.rb: -------------------------------------------------------------------------------- 1 | # Monkey-patches to the Integer class go here. 2 | class Integer 3 | # Allows ActionView::Helpers::TextHelper#pluralize to be used outside of views. 4 | def pluralize(word) 5 | ActionController::Base.helpers.pluralize(self, word) 6 | end 7 | 8 | # Run a block a given number of times, sleeping and retrying when 9 | # an exception of the given type is caught. 10 | def tries(catching: StandardError) 11 | retries ||= 0 12 | yield 13 | rescue catching 14 | if (retries += 1) <= self 15 | sleep 1 << retries # (Sleep with exponential backoff.) 16 | retry 17 | end 18 | raise 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/factories/involved_officer.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :involved_officer do 3 | id { SecureRandom.uuid } 4 | officer_used_force false 5 | officer_used_force_reason ['To effect arrest or take into custody'] 6 | injured false 7 | received_force true 8 | received_force_type ['Blunt / impact weapon'] 9 | received_force_location ['Head'] 10 | on_duty true 11 | dress 'Tactical' 12 | age '21-25' 13 | race { InvolvedPerson::RACES.sample_one_or_two_elements } 14 | asian_race { race.include?(InvolvedPerson::ASIAN_RACE_STR) ? InvolvedPerson::ASIAN_RACES.sample_one_or_two_elements : nil } 15 | gender 'Male' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/concerns/allows_partial_save.rb: -------------------------------------------------------------------------------- 1 | # Gives Documents the ability to be saved without validation. 2 | # 3 | # Note that partial=true renders a document invalid, so if a 4 | # document has previously been partially saved, you must give 5 | # it partial=false before you can save it without validation. 6 | module AllowsPartialSave 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | field :partial, :boolean 11 | validates :partial, acceptance: { accept: false } 12 | end 13 | 14 | def partial_save(attributes) 15 | attributes.each { |attribute, value| self[attribute] = value } 16 | self["partial"] = true 17 | save(validate: false) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/queries/past_submissions_query.rb: -------------------------------------------------------------------------------- 1 | # Returns previously-submitted incidents, organized by year. 2 | class PastSubmissionsQuery < Query 3 | private 4 | 5 | def perform_query(params) 6 | # raise ActionController::BadRequest.new unless params[:user].admin? 7 | 8 | submitted_incidents = GetAllIncidentsQuery.new.run(user: params[:user]).select(&:submitted?) 9 | 10 | agency_status = AgencyStatus.find_by_ori(params[:user].ori) 11 | submitted_years = agency_status ? agency_status.complete_submission_years : [] 12 | 13 | Hash[submitted_years.map do |year| 14 | [year, submitted_incidents.select { |i| i.year == year }] 15 | end] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/pages/maintenance.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render "layouts/partials/head" %> 4 | 5 |
    6 | <% if Rails.configuration.x.branding.ursus? %> 7 | 8 | <% else %> 9 | 10 | <% end %> 11 |

    Down for Maintenance

    12 |

    <%= Rails.configuration.x.branding.ursus? ? 'URSUS' : 'Bridge UOF' %> is temporarily down for routine maintenance.

    13 |

    Please come back in an hour.

    14 |
    15 | 16 | 17 | -------------------------------------------------------------------------------- /app/controllers/users/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | module Users 2 | class SessionsController < Devise::SessionsController 3 | # before_filter :configure_sign_in_params, only: [:create] 4 | 5 | # GET /resource/sign_in 6 | # def new 7 | # super 8 | # end 9 | 10 | # POST /resource/sign_in 11 | # def create 12 | # super 13 | # end 14 | 15 | # DELETE /resource/sign_out 16 | # def destroy 17 | # super 18 | # end 19 | 20 | # protected 21 | 22 | # If you have extra params to permit, append them to the sanitizer. 23 | # def configure_sign_in_params 24 | # devise_parameter_sanitizer.for(:sign_in) << :attribute 25 | # end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build this Container by running from this dir: 2 | # docker build -t bayesimpact/bridge-uof . 3 | FROM ruby:2.2.3 4 | 5 | MAINTAINER everett.wetchler@bayesimpact.org 6 | 7 | RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs 8 | RUN mkdir /bridge-uof 9 | WORKDIR /bridge-uof 10 | COPY Gemfile /bridge-uof/Gemfile 11 | COPY Gemfile.lock /bridge-uof/Gemfile.lock 12 | RUN bundle install 13 | 14 | COPY . /bridge-uof 15 | RUN sed -i -e "s/localhost:/db:/" /bridge-uof/config/aws.yml 16 | EXPOSE 80 17 | CMD ["./precompile_and_serve.sh", "-b", "0.0.0.0", "-p", "80"] 18 | 19 | # Label the image with the git commit. 20 | ARG GIT_SHA1=non-git 21 | LABEL org.bayesimpact.git=$GIT_SHA1 22 | -------------------------------------------------------------------------------- /spec/controllers/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ApplicationController, type: :controller do 4 | it "replace_url_host" do 5 | c = ApplicationController.new 6 | expect(c.replace_url_host('http://foo.bar.baz/qux', 'something')).to eq('something/qux') 7 | expect(c.replace_url_host('http://foo.bar.baz/qux', 'https://something')).to eq('https://something/qux') 8 | expect(c.replace_url_host('https://foo.bar.baz/qux', 'https://something')).to eq('https://something/qux') 9 | expect(c.replace_url_host('http://foo.bar.baz/', 'something')).to eq('something/') 10 | expect(c.replace_url_host('http://foo.bar.baz', 'something')).to eq('something') 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/stylesheets/screener.scss: -------------------------------------------------------------------------------- 1 | .agency-table-group { 2 | display: inline-block; 3 | background-color: #EEE; 4 | margin: 10px; 5 | padding: 14px; 6 | border: 1px solid #CCC; 7 | font-size: 90%; 8 | text-align: center; 9 | min-width: 250px; 10 | table { 11 | border-collapse: collapse; 12 | margin: 0px auto 5px auto; 13 | color: #555; 14 | border-radius: 3px; 15 | th, td { 16 | padding: 2px 5px; 17 | } 18 | th { 19 | text-align: right; 20 | font-weight: bold; 21 | } 22 | } 23 | } 24 | 25 | .important-help-text { 26 | color: $color-secondary-darkest-18f; 27 | font-style: italic; 28 | } 29 | 30 | .you-are-admin { 31 | text-align: center; 32 | } 33 | -------------------------------------------------------------------------------- /config/initializers/custom/ori.rb: -------------------------------------------------------------------------------- 1 | # App should fail to start if ORI data isn't loaded. 2 | 3 | # Skip this check in test env, because right now CircleCI doesn't 4 | # generate the ORI files and we don't want to add an extra step 5 | # to our build. 6 | unless Rails.env.test? 7 | Rails.configuration.after_initialize do 8 | begin 9 | Constants::CONTRACTING_ORIS 10 | rescue LoadError 11 | raise "Constants::CONTRACTING_ORIS not specified! Have you run `docker-compose run data`?" 12 | end 13 | 14 | begin 15 | Constants::DEPARTMENT_BY_ORI 16 | rescue LoadError 17 | raise "Constants::DEPARTMENT_BY_ORI not specified! Have you run `docker-compose run data`?" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/initializers/custom/branding.rb: -------------------------------------------------------------------------------- 1 | # Branding-specific configuration code goes here. 2 | 3 | branding = Rails.configuration.x.branding 4 | 5 | branding.name = ENV["BRANDING"] 6 | branding.incident_id_prefix = ENV["INCIDENT_ID_PREFIX"] 7 | 8 | def branding.ursus? 9 | name == Constants::BRANDING_URSUS 10 | end 11 | 12 | def branding.whitelabel? 13 | name == Constants::BRANDING_WHITELABEL 14 | end 15 | 16 | # Validate presence of env vars. 17 | 18 | if branding.incident_id_prefix.blank? 19 | raise 'Must set INCIDENT_ID_PREFIX env var.' 20 | end 21 | 22 | if Constants::AVAILABLE_BRANDINGS.exclude? branding.name 23 | raise "Must set BRANDING env variable to one of #{Constants::AVAILABLE_BRANDINGS} (currently it is '#{ENV['BRANDING']}')" 24 | end 25 | -------------------------------------------------------------------------------- /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 | <%= render partial: "user_account_fields", locals: {f: f} %> 7 | 8 |
    9 | <% "Password" %> 10 | <%= f.password_field :password, autocomplete: "off", 11 | placeholder: @pwd_placeholder %> 12 |
    13 | 14 |
    15 | <%= f.password_field :password_confirmation, autocomplete: "off", placeholder: "Confirm password" %> 16 |
    17 | 18 |
    19 | <%= f.submit "Sign up" %> 20 |
    21 | <% end %> 22 | 23 | <%= link_to "Have an account? Log in", new_user_session_path %> 24 | -------------------------------------------------------------------------------- /app/models/constants/constants.rb: -------------------------------------------------------------------------------- 1 | # Generic constants that pertain to more than one model go here. 2 | module Constants 3 | NONE = "None".freeze 4 | UNCONFIRMED_FLED = 'Unconfirmed (Fled)'.freeze 5 | MULTIPLE = "Multiple".freeze 6 | VARIOUS = 'Various*'.freeze 7 | 8 | ERROR_BLANK_FIELD = "Can't be blank".freeze 9 | 10 | SBI_DEFINITION = "a bodily injury that involves a substantial risk of death, unconsciousness," \ 11 | " protracted and obvious disfigurement, or protracted loss or impairment of" \ 12 | " the function of a bodily member or organ".freeze 13 | 14 | BRANDING_WHITELABEL = 'whitelabel'.freeze 15 | BRANDING_URSUS = 'ursus'.freeze 16 | AVAILABLE_BRANDINGS = [BRANDING_WHITELABEL, BRANDING_URSUS].freeze 17 | end 18 | -------------------------------------------------------------------------------- /app/views/devise/registrations/_user_account_fields.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= f.text_field :first_name, autofocus: true, placeholder: "First Name" %> 3 |
    4 | 5 |
    6 | <%= f.text_field :last_name, placeholder: "Last Name" %> 7 |
    8 | 9 |
    10 | <%= f.email_field :email, placeholder: "Email" %> 11 |
    12 | 13 |
    14 | <%= f.text_field :ori, placeholder: "ORI" %> 15 |
    16 | 17 |
    18 | <%= f.text_field :department, placeholder: "Department" %> 19 |
    20 | 21 |
    22 | <%= f.text_field :role, placeholder: "Role" %> 23 |
    24 | 25 |
    26 | <%= f.text_field :user_id, placeholder: "User ID (from ECARS)" %> 27 |
    28 | -------------------------------------------------------------------------------- /config/initializers/custom/aws.rb: -------------------------------------------------------------------------------- 1 | # AWS-related configuration goes here. 2 | 3 | Aws.config[:region] = ENV['AWS_REGION'] || 'us-west-1' 4 | 5 | if ENV['USE_DEVELOPMENT_AWS_KEYS'] == 'true' && Rails.env != 'test' 6 | # Allow overriding of key settings, in case we need to run local dynamodb. 7 | aws_env_key = 'development' 8 | else 9 | aws_env_key = Rails.env 10 | end 11 | 12 | creds = YAML.load(File.read(Rails.root.join('config', 'aws.yml'))) 13 | if creds.key?(aws_env_key) 14 | creds = creds[aws_env_key] 15 | Aws.config[:credentials] = Aws::Credentials.new(creds['access_key_id'], creds['secret_access_key']) 16 | Aws.config[:endpoint] = creds['endpoint'] 17 | else 18 | Aws.config[:credentials] = Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']) 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/gems/dynamo_db.rb: -------------------------------------------------------------------------------- 1 | # DynamoDB configuration goes here. 2 | 3 | Dynamoid.configure do |config| 4 | config.namespace = ENV['DYNAMO_TABLE_PREFIX'] || "ursus_#{Rails.env}" 5 | config.write_capacity = 1 6 | config.read_capacity = 1 7 | end 8 | 9 | MODELS = [ 10 | User, Incident, Screener, GeneralInfo, InvolvedCivilian, InvolvedOfficer, 11 | Feedback, AuditEntry, ChangedField, AgencyStatus, GlobalState, Visit, Event 12 | ].freeze 13 | 14 | Rails.configuration.after_initialize do 15 | # Initialize tables, if necessary. Do this after initialization so that 16 | # we are sure that Devise has been loaded. 17 | MODELS.each do |model| 18 | 3.tries(catching: Aws::DynamoDB::Errors::LimitExceededException) do 19 | model.create_table 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/queries/get_all_incidents_query.rb: -------------------------------------------------------------------------------- 1 | # Returns all incidents potentially visible to a user. 2 | # This is a "sub-query" returning a raw incident set that is 3 | # then used by user-facing queries such as DashboardIncidentsQuery. 4 | class GetAllIncidentsQuery < Query 5 | private 6 | 7 | def perform_query(params) 8 | if params[:user].admin? 9 | # Admins oversee incidents of all users in their ORI and contracting ORIs. 10 | params[:user].allowed_oris.flat_map do |ori| 11 | User.find_all_by_secondary_index(ori: ori).flat_map do |user| 12 | user.incidents.records 13 | end 14 | end 15 | else 16 | # Other users only oversee their own incidents. 17 | params[:user].incidents.records 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/factories/general_info.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :general_info do 3 | id { SecureRandom.uuid } 4 | # Generate a random date in the past year. 5 | incident_date_str { rand(Date.civil(Time.current.year - 1, 1, 1)..Date.civil(Time.current.year - 1, 12, 31)).strftime('%m/%d/%Y') } 6 | incident_time_str '1400' 7 | sequence(:address) { |n| "123#{n} Main Street" } 8 | city 'San Francisco' 9 | state 'CA' 10 | zip_code '94123' 11 | county 'San Francisco County' 12 | multiple_locations false 13 | on_k12_campus false 14 | arrest_made false 15 | crime_report_filed false 16 | contact_reason GeneralInfo::CONTACT_REASONS[0] 17 | num_involved_civilians 1 18 | num_involved_officers 1 19 | ori { build(:dummy_user).ori } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/application/_edit_person_headers.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | <%= @type.capitalize %> 3 | <% if @num_involved_persons > 1 %> 4 | <% @num_involved_persons.times do |i| %> 5 | <% if @current_person_index == i %> 6 | <%= link_to (i+1).to_s, '#', class: "person-link person-link-current" %> 7 | <% elsif i == @involved_persons.length and i <= @disabled_beyond %> 8 | <%= link_to (i+1).to_s, new_person_path(@type, @incident), class: "person-link"%> 9 | <% elsif i > @disabled_beyond %> 10 | <%= (i+1).to_s %> 11 | <% else %> 12 | <%= link_to (i+1).to_s, edit_person_path(@type, @incident, i), class: "person-link" %> 13 | <% end %> 14 | <% end %> 15 | <% end %> 16 |

    17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_controls.scss: -------------------------------------------------------------------------------- 1 | #controls { 2 | position: absolute; 3 | top: $nav-height; 4 | bottom: 0; 5 | right: 0; 6 | width: 84px; 7 | 8 | margin-left: 10px; 9 | font-size: 46px; 10 | background-color: #f3f3f3; 11 | 12 | .fa { 13 | width: 100%; 14 | padding: 24px 5px; 15 | color: $grayish-color; 16 | cursor: pointer; 17 | text-decoration: none !important; 18 | border-bottom: 1px solid #ccc; 19 | 20 | &:hover { 21 | color: #777; 22 | } 23 | 24 | .button-label { 25 | display: block; 26 | padding-top: 4px; 27 | font-size: 12px; 28 | font-family: 'Source Sans Pro', sans-serif; 29 | font-weight: bold; 30 | } 31 | } 32 | } 33 | 34 | @media screen and (max-width: 768px) { 35 | #controls { 36 | display: none; 37 | } 38 | } -------------------------------------------------------------------------------- /spec/requests/splash_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe '[Splash page]', type: :request do 4 | it 'Renders the splash page for first-time visitors' do 5 | login(dont_handle_splash: true) 6 | visit root_path 7 | expect(current_path).to end_with('/welcome') 8 | 9 | # Until the user clicks 'ENTER' on the splash page, they'll keep getting 10 | # redirected there 11 | visit root_path 12 | expect(current_path).to end_with('/welcome') 13 | 14 | click_button('ENTER') 15 | expect(current_path).not_to end_with('/welcome') # Sent away from splash 16 | visit root_path 17 | expect(current_path).not_to end_with('/welcome') # Doesn't get splash again 18 | visit welcome_path 19 | expect(current_path).to end_with('/welcome') # Can still explicitly visit 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/queries/dashboard_incidents_query.rb: -------------------------------------------------------------------------------- 1 | # Returns incidents to display on the dashboard. 2 | class DashboardIncidentsQuery < Query 3 | private 4 | 5 | def perform_query(params) 6 | agency_last_submission_year = AgencyStatus.get_agency_last_submission_year(params[:user].ori) 7 | 8 | GetAllIncidentsQuery.new.run(user: params[:user]) 9 | .reject(&:deleted?) # Don't include deleted incidents. 10 | .select { |i| i.year.nil? || i.year > agency_last_submission_year } # Ignore submitted incidents and old drafts. 11 | .select { |i| i.authorized_to_view? params[:user] } # Only show incidents user is authorized to see. 12 | .sort_by { |i| i.created_at.to_i }.reverse! # Display reverse chronologically. 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/screeners_controller.rb: -------------------------------------------------------------------------------- 1 | # Controller for Screener form. 2 | class ScreenersController < ApplicationController 3 | before_action :set_screener 4 | 5 | def create 6 | formatted_params = get_formatted_params(params, Screener) 7 | if @screener.update_attributes(formatted_params) 8 | if @screener.forms_necessary? 9 | @incident = Incident.create(user: @current_user, screener: @screener) 10 | @incident.audit_entries << AuditEntry.new(user: @current_user, custom_text: 'filled out the screener') 11 | redirect_to edit_incident_general_info_path(@incident) 12 | else 13 | render 'forms_not_necessary' 14 | end 15 | else 16 | render :new 17 | end 18 | end 19 | 20 | private 21 | 22 | def set_screener 23 | @screener = Screener.new 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/override/ie8.js: -------------------------------------------------------------------------------- 1 | /* IE<=8-specific JS hacks. */ 2 | 3 | $(function () { 4 | // Hide #breadcrumbs-ursus-id because for some reason it makes the page not load. 5 | $('#breadcrumbs-ursus-id').hide(); 6 | 7 | // Analytics don't work for a few reasons. Just don't display them. 8 | $('.visualization').text( 9 | "Your web browser, Internet Explorer 8, does not support this chart. " + 10 | "If you'd like to see this visualization, try any newer version of Internet Explorer (9, 10, etc) " + 11 | "or another major browser (Chrome, Firefox, etc)." 12 | ); 13 | 14 | // Call positionControls() (event handler on window resize) a little later to make 15 | // it correctly calculate the top margin of #controls. [see controls.js]. 16 | setTimeout(function () { 17 | $(window).resize(); 18 | }, 1000); 19 | }); 20 | -------------------------------------------------------------------------------- /app/validators/uniqueness_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates that an attribute of the given object is unique - 2 | # that is, no other instance of this model has that attribute value. 3 | # Avoids a SCAN operation if possible by attempting a secondary index query. 4 | class UniquenessValidator < ActiveModel::EachValidator 5 | def validate_each(document, attribute, value) 6 | records = begin 7 | if document.class.indexed_hash_keys.include? attribute.to_s 8 | document.class.find_all_by_secondary_index(attribute => value) 9 | else 10 | document.class.where(attribute => value).all 11 | end 12 | end 13 | 14 | if records.size > 1 || (records.size == 1 && records[0].hash_key != document.hash_key) 15 | document.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(value: value)) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /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 | <% if @minimum_password_length %> 9 | (<%= @minimum_password_length %> characters minimum)
    10 | <% end %> 11 | <%= f.password_field :password, autofocus: true, autocomplete: "off", placeholder: "New password" %> 12 |
    13 | 14 |
    15 | <%= f.password_field :password_confirmation, autocomplete: "off", placeholder: "Confirm new password" %> 16 |
    17 | 18 |
    19 | <%= f.submit "Change my password" %> 20 |
    21 | <% end %> 22 | 23 | <%= render "devise/shared/links" %> 24 | -------------------------------------------------------------------------------- /app/views/layouts/partials/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 3 | <% if Rails.configuration.x.branding.ursus? %> 4 | 23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/application/_controls.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | <% if local_assigns[:show_print_button] %> 7 | 8 | <% end %> 9 |
    10 | -------------------------------------------------------------------------------- /app/views/application/_display_item.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= key %> 3 | <% if is_fbi_field %> 4 | 8 | <% end %> 9 | 10 | 11 | <% if partial %> 12 | <%= render partial: partial, locals: { value: value } %> 13 | <% elsif value.instance_of?(Array) %> 14 | <%= value.join(', ') %> 15 | <% elsif value == nil || value == '' %> 16 | n/a 17 | <% elsif value.instance_of?(DateTime) %> 18 | <%= value.strftime("%a %b %e %H:%M %Y") %> 19 | <% else %> 20 | <%= (value == !!value) ? (value ? "Yes" : "No") : value.to_s %> 21 | <% end %> 22 | 23 | -------------------------------------------------------------------------------- /app/helpers/incident_helper.rb: -------------------------------------------------------------------------------- 1 | # Helper methods pertaining to displaying components of incidents. 2 | module IncidentHelper 3 | def display_ori(ori) 4 | "#{Constants::DEPARTMENT_BY_ORI[ori]} (#{ori})" 5 | end 6 | 7 | def render_incident_id(incident) 8 | render partial: "incident_id", locals: { value: incident.incident_id } 9 | end 10 | 11 | def audit_entry_description(entry) 12 | name = entry.user.full_name 13 | date = entry.created_at.strftime("%a %b %-d %Y at %H%M") 14 | 15 | if entry.custom_text 16 | "#{name} #{entry.custom_text} on #{date}." 17 | elsif entry.is_new 18 | "#{name} filled out the #{entry.page} page on #{date}." 19 | else 20 | "#{name} edited the #{entry.page} page on #{date} and updated the following fields:" 21 | end 22 | end 23 | 24 | def show_changed_fields_for_audit_entry?(entry) 25 | !entry.custom_text && !entry.is_new 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /app/views/incidents/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= title @status_text %> 2 | 3 | <% content_for :sidenav do %> 4 | <%= render partial: 'dashboard_sidenav' %> 5 | <% end %> 6 | <% content_for :panel do %> 7 | <%= dashboard_page_explanation(@current_user, @status, @incidents.length) %> 8 | 9 | <% if @status == 'state_submission' %> 10 | <%= render partial: 'state_submit_pane' %> 11 | <% elsif @status == 'past_submissions' %> 12 | <%= render partial: 'past_submissions_pane' %> 13 | <% elsif @status == 'analytics' %> 14 | <%= render partial: 'analytics_pane' %> 15 | <% elsif @status.present? && Incident::STATUS_TYPES.include?(@status.to_sym) %> 16 | <% if @incidents.present? %> 17 | <%= render partial: 'incidents_table', locals: {incidents: @incidents} %> 18 | <% end %> 19 | <% else %> 20 | <%= render partial: 'overview_pane' %> 21 | <% end %> 22 | <% end %> 23 | <%= render template: "layouts/dashboard" %> 24 | -------------------------------------------------------------------------------- /lib/bridge_exceptions.rb: -------------------------------------------------------------------------------- 1 | # Custom exceptions go here. 2 | module BridgeExceptions 3 | class BulkUploadError < StandardError; end 4 | class DeserializationError < StandardError; end 5 | class IncidentIdCollisionError < StandardError; end 6 | class SiteminderAuthenticationError < StandardError; end 7 | class SiteminderCookieNotFoundError < StandardError; end 8 | class UnableToSubmitError < StandardError; end 9 | class UnathorizedToView < ActionController::BadRequest; end 10 | class UnimplementedError < StandardError; end 11 | 12 | # Custom exceptions with default messages go below. 13 | 14 | # Thrown when an invalid login mechanism is detected. 15 | class InvalidLoginMechanismError < StandardError 16 | def initialize(msg = "Need to set LOGIN_MECHANISM env variable to DEVISE, SITEMINDER, or DEMO " \ 17 | "(currently it is '#{ENV['LOGIN_MECHANISM']}')") 18 | super 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /data/scripts/cleaning_helpers.py: -------------------------------------------------------------------------------- 1 | """Helper functions for data cleaning scripts.""" 2 | 3 | 4 | def extract_code_number(code): 5 | """For natural sorting, extract (for example) '123' from '123 ABC'. 6 | 7 | Lexicographical sorting of code strings does not result in a natural ordering. 8 | Return a tuple of all numerical sections. E.g. "123.4 ABC" would return (123, 4) 9 | so that it naturally sorts before "123.10" and after "50.4". 10 | """ 11 | current = '' 12 | parts = [] 13 | for i, ch in enumerate(code): 14 | if ch.isdigit(): 15 | current += ch 16 | elif ch == '.': 17 | if not current: 18 | # Break if we get something weird like '123...4' 19 | break 20 | parts.append(int(current)) 21 | current = '' 22 | else: 23 | break 24 | if current: 25 | parts.append(int(current)) 26 | return tuple(parts) 27 | -------------------------------------------------------------------------------- /app/views/application/_yes_no_question.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%= f.label field, label, class: label_class %> 4 | <% unless help_content.nil? %> 5 |
    7 | <% end %> 8 |
    9 | 10 | 11 | 19 | 27 | 28 |
    29 | -------------------------------------------------------------------------------- /app/views/application/_incident_id.html.erb: -------------------------------------------------------------------------------- 1 | <% if value.present? %> 2 | <%= value.prefix %>-<%= value.county %>-<%= value.agency %>-<%= value.year %>-<%= value.code %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /spec/requests/feedback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe '[Feedback and feedback email tests]', type: :request do 4 | before :each do 5 | login 6 | end 7 | 8 | it 'Loads the feedback page when the help button is clicked' do 9 | click_link 'HELP' 10 | expect(current_path).to eq(feedback_path) 11 | end 12 | 13 | it 'Saves feedback and sends it in an email', driver: :poltergeist do 14 | expect(Feedback.count).to eq(0) 15 | visit feedback_path 16 | find('a', text: 'Click here for a feedback form').click 17 | fill_in 'Which part gave you difficulty', with: "Foo" 18 | fill_in 'Please explain', with: "Bar" 19 | expect { find('button[type=submit]').click } 20 | .to change { ActionMailer::Base.deliveries.count }.by(1) 21 | expect(current_path).to eq(thank_you_path) 22 | 23 | expect(Feedback.count).to eq(1) 24 | f = Feedback.first 25 | expect(f.source).to eq("Foo") 26 | expect(f.content).to eq("Bar") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/requests/visit_tracking_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe '[Visit and event tracking]', type: :request do 4 | let(:user) { User.first } 5 | 6 | before :each do 7 | login 8 | 9 | # We don't care about the events that were triggered by login. 10 | Event.all.each(&:destroy) 11 | end 12 | 13 | it 'tracks visits and events' do 14 | # Visiting one page should create a visit and one event. 15 | 16 | visit dashboard_path 17 | 18 | expect(Visit.count).to eq(1) 19 | expect(Visit.first.user).to eq(user) 20 | 21 | expect(Event.count).to eq(1) 22 | expect(Event.first.name).to eq('incidents#index') 23 | expect(Event.first.visit.id).to eq(Visit.first.id) 24 | expect(Event.first.user).to eq(user) 25 | 26 | # Visiting another page should create new events, but the visit should stay the same. 27 | 28 | visit new_incident_path 29 | 30 | expect(Visit.count).to eq(1) 31 | expect(Event.count).to be > 1 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | height: $footer-height; 6 | background-color: $grayish-color; 7 | padding: 18px; 8 | 9 | img { 10 | display: inline-block; 11 | height: 48px; 12 | width: 48px; 13 | } 14 | 15 | div { 16 | float: left; 17 | } 18 | 19 | #agency-name { 20 | padding: 0px; 21 | padding-top: 0px; 22 | margin: auto 0px auto 10px; 23 | font-size: 16px; 24 | font-weight: bold; 25 | color: white; 26 | height: 100%; 27 | } 28 | 29 | p { 30 | margin: 0; 31 | } 32 | 33 | a { 34 | color: white; 35 | &:hover { 36 | color: white; 37 | text-decoration: underline; 38 | } 39 | } 40 | } 41 | 42 | @media screen and (max-width: 768px) { 43 | footer { 44 | position: static; 45 | height: auto; 46 | padding: 24px; 47 | padding-bottom: 0; 48 | text-align: center; 49 | 50 | img, div { 51 | float: none; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/views/layouts/error.html.erb: -------------------------------------------------------------------------------- 1 | <%= title "Oops!" %> 2 | 3 | <% content_for :body do %> 4 | <%= render partial: "controls" %> 5 |
    6 |
    7 |

    Oops!

    8 |
    9 |

    <%= yield %>

    10 |

    If you have a moment, please <%= link_to "let us know", feedback_path %> what you were doing before you got this message, so we can track down the issue more easily.

    11 |

    <%= link_to "Return to your dashboard", dashboard_path %>

    12 |
    13 | 14 | <% if Rails.env.development? || ENV["SHOW_EXCEPTIONS_ANYWAY"] == "true" %> 15 |
    16 |

    Original exception (only displayed in development and test environment):

    17 |
    <%= @exception.inspect %>
    18 |
    <%= @exception.backtrace.join("\n") %>
    19 | <% end %> 20 |
    21 |
    22 | <% end %> 23 | <%= render template: "layouts/application" %> 24 | -------------------------------------------------------------------------------- /app/views/incidents/_past_submissions_pane.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <% if @past_incidents_by_year.empty? %> 3 |
    4 | <% if @current_user.admin? %> 5 | Your agency has not yet made 6 | <% if Rails.configuration.x.branding.ursus? %>an URSUS submission<% else %>a submission<% end %> 7 | to the state. 8 | <% else %> 9 | None of your incidents have been submitted to the state yet. 10 | <% end %> 11 |
    12 | <% else %> 13 |

    Past State Submissions

    14 | <% @past_incidents_by_year.keys.sort.reverse.each do |year| %> 15 |

    <%= year %> <%= "(#{pluralize(@past_incidents_by_year[year].length, 'incident')})" %>

    16 | <% if @past_incidents_by_year[year].length > 0 %> 17 | <%= render partial: 'incidents_table', locals: {incidents: @past_incidents_by_year[year]} %> 18 | <% end %> 19 | <% end %> 20 | <% end %> 21 |
    22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/upload.scss: -------------------------------------------------------------------------------- 1 | #bulk-upload { 2 | .block { 3 | overflow: scroll; 4 | width: 80%; 5 | background-color: #f5f5f5; 6 | border: 1px solid #ccc; 7 | margin-bottom: 1em; 8 | 9 | pre { 10 | border: none; 11 | } 12 | 13 | h3 { 14 | padding-left: 10px; 15 | } 16 | } 17 | 18 | #schema { 19 | display: none; 20 | } 21 | 22 | #upload-control { 23 | margin: 20px 0; 24 | 25 | .btn-file { 26 | position: relative; 27 | overflow: hidden; 28 | 29 | input[type=file] { 30 | position: absolute; 31 | top: 0; 32 | right: 0; 33 | min-width: 100%; 34 | min-height: 100%; 35 | font-size: 100px; 36 | text-align: right; 37 | filter: alpha(opacity=0); 38 | opacity: 0; 39 | outline: none; 40 | background: white; 41 | cursor: inherit; 42 | display: block; 43 | } 44 | } 45 | } 46 | 47 | #showHideSchema, .toggle-schema { 48 | cursor: pointer; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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: b714509650504eaa786e181ad638b81863e3ddd85d338615fc97d5971c1f29b787b860e357add56cd3e9f2270c6af5987b2f5124d3efd3dfc68dd7c52c85958a 15 | 16 | test: 17 | secret_key_base: 3b5db199004c4490259a6d4156c5173402f8ae45f3357ff4f98abb4779f4517e57906c9f30bae8164782dd0456f2a8691c7c3ad7e77c7c93ff66cf74869c0d6d 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 | -------------------------------------------------------------------------------- /spec/factories/involved_civilian.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :involved_civilian do 3 | id { SecureRandom.uuid } 4 | assaulted_officer false 5 | perceived_armed false 6 | confirmed_armed { [true, false].sample } 7 | confirmed_armed_weapon { confirmed_armed ? InvolvedCivilian::CONFIRMED_WEAPONS.sample_one_or_two_elements : nil } 8 | firearm_type { confirmed_armed_weapon ? InvolvedCivilian::FIREARM_TYPES.sample_one_or_two_elements : nil } 9 | resisted false 10 | received_force true 11 | received_force_type { InvolvedCivilian::RECEIVED_FORCE_TYPES.sample_one_or_two_elements } 12 | received_force_location ['Head'] 13 | injured false 14 | custody_status 'In custody (W&I section 5150)' 15 | mental_status ['None'] 16 | highest_charge 'Some charge' 17 | age '21-25' 18 | race { InvolvedPerson::RACES.sample_one_or_two_elements } 19 | asian_race { race.include?(InvolvedPerson::ASIAN_RACE_STR) ? InvolvedPerson::ASIAN_RACES.sample_one_or_two_elements : nil } 20 | gender 'Male' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /config/initializers/custom/views.rb: -------------------------------------------------------------------------------- 1 | # ActionView override configuration goes here. 2 | 3 | # Override ActionView's field_error_proc to wrap form fields with errors 4 | # in a .has-errors div (this is a boostrap class to highlight fields with errors). 5 | 6 | ActionView::Base.field_error_proc = proc do |html_tag, instance| 7 | # We need to use .html_safe at the end due to field_error_proc handling, 8 | # and it's not a big deal anyway because there's no user content to worry about. 9 | # rubocop:disable Rails/OutputSafety 10 | if html_tag =~ /^