<%= f.rich_text_area :comment, :class => "tinymce", :rows => 40, :cols => 120 %>
18 |
19 |
20 |
21 |
22 |
23 | -
24 |
25 | <%= f.submit "Save", :class => "action" %> <%= link_to "Cancel", extranet_rules_path %>
26 |
27 |
28 |
29 |
30 |
31 | <% end %>
32 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | require "active_storage/engine"
9 | require "action_controller/railtie"
10 | require "action_mailer/railtie"
11 | require "action_mailbox/engine"
12 | require "action_text/engine"
13 | require "action_view/railtie"
14 | # require "action_cable/engine"
15 | require "sprockets/railtie"
16 | require "rails/test_unit/railtie"
17 | require "font-awesome-rails"
18 |
19 | # Require the gems listed in Gemfile, including any gems
20 | # you've limited to :test, :development, or :production.
21 | Bundler.require(*Rails.groups)
22 |
23 | module Policeboard
24 | class Application < Rails::Application
25 | # Initialize configuration defaults for originally generated Rails version.
26 | config.load_defaults 5.0
27 |
28 | # Settings in config/environments/* take precedence over those specified here.
29 | # Application configuration can go into files in config/initializers
30 | # -- all .rb files in that directory are automatically loaded after loading
31 | # the framework and any gems in your application.
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/views/rakes/shared/_links.html.erb:
--------------------------------------------------------------------------------
1 | <%- if controller_name != 'sessions' %>
2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end -%>
4 |
5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end -%>
8 |
9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end -%>
12 |
13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end -%>
16 |
17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end -%>
20 |
21 | <%- if devise_mapping.omniauthable? %>
22 | <%- resource_class.omniauth_providers.each do |provider| %>
23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
24 | <% end -%>
25 | <% end -%>
26 |
--------------------------------------------------------------------------------
/app/views/extranet/board_members/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% title "Board" %>
3 |
4 |
Board
5 |
6 |
<%= link_to "Add board member", [:new, :extranet, :board_member], :class => "action" %>
7 |
8 |
9 |
10 |
11 | | Name |
12 | Status |
13 | Last modified |
14 | |
15 |
16 |
17 |
18 | <% @board_members.each do |board| %>
19 |
20 | | <%= link_to board.first_name + " " + board.last_name, [:edit, :extranet, board] %> |
21 | <%= board.active ? "Current" : "Past" %> |
22 | <%= board.updated_at %> |
23 | <%= link_to "X", [:extranet, board], :confirm => 'Are you sure?', :method => :delete, :title => "Delete" %> |
24 |
25 | <% end %>
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/controllers/cases_controller.rb:
--------------------------------------------------------------------------------
1 | class CasesController < ApplicationController
2 | def index
3 | if params[:search]
4 | @header = "Search results"
5 | @cases = Case.search(params[:search])
6 | @counter = "Matching cases: #{@cases.count}"
7 | else
8 | @header = "Recent cases"
9 | @cases = Case.where(is_active: true).where.not(defendant_id: nil)
10 | @counter = "Total cases: #{@cases.count}"
11 | end
12 |
13 | @cases = @cases.order(Arel.sql('date_initiated IS NULL, date_initiated DESC')).paginate(:page => params[:page])
14 |
15 | @cases_per_year = Case.where('date_initiated IS NOT NULL')
16 | .order(Arel.sql('EXTRACT(YEAR from date_initiated)'))
17 | .group(Arel.sql('EXTRACT(YEAR from date_initiated)'))
18 | .count
19 | @case_trend = []
20 | (@cases_per_year.keys.first.to_i..@cases_per_year.keys.last.to_i).each do |year|
21 | @case_trend << (@cases_per_year[year.to_f] || 0)
22 | end
23 | end
24 |
25 | def show
26 | @case = Case.find(params[:id])
27 | if !@case.is_active
28 | redirect_to cases_path, :notice =>"Case not found"
29 | end
30 | #@files = Dir.glob("**/public/case_files/" + @case.number + "_*.pdf").map{|path| path.gsub("public/","/") }
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/app/views/devise/shared/_links.html.erb:
--------------------------------------------------------------------------------
1 | <%- if controller_name != 'sessions' %>
2 | <%= link_to "Sign in", new_session_path(resource_name) %>
3 | <% end -%>
4 |
5 | <%
6 | =begin %>
7 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
8 | <%= link_to "Sign up", new_registration_path(resource_name) %>
9 | <% end -%>
10 |
11 | <%
12 | =end%>
13 |
14 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
15 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
16 | <% end -%>
17 |
18 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
19 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
20 | <% end -%>
21 |
22 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
23 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
24 | <% end -%>
25 |
26 | <%- if devise_mapping.omniauthable? %>
27 | <%- resource_class.omniauth_providers.each do |provider| %>
28 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
29 | <% end -%>
30 | <% end -%>
31 |
--------------------------------------------------------------------------------
/app/uploaders/image_uploader.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | class ImageUploader < CarrierWave::Uploader::Base
4 | include CarrierWave::MiniMagick
5 |
6 | # Override the directory where uploaded files will be stored.
7 | # This is a sensible default for uploaders that are meant to be mounted:
8 | def store_dir
9 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
10 | end
11 |
12 | # Provide a default URL as a default if there hasn't been a file uploaded:
13 | def default_url(*args)
14 | ActionController::Base.helpers.asset_path("fallback/" + [version_name, "nil_board_image.jpg"].compact.join('_'))
15 | end
16 |
17 | # Process files as they are uploaded:
18 | # process :scale => [200, 300]
19 |
20 | # def scale(width, height)
21 | # # do something
22 | # end
23 |
24 | process resize_to_fit: [800, 800]
25 |
26 | # Create different versions of your uploaded files:
27 | version :thumb do
28 | process :resize_to_limit => [200, 200]
29 | end
30 |
31 | # Add a white list of extensions which are allowed to be uploaded.
32 | # For images you might use something like this:
33 | # def extension_white_list
34 | # %w(jpg jpeg gif png)
35 | # end
36 |
37 | # Override the filename of the uploaded files:
38 | # Avoid using model.id or version_name here, see uploader/store.rb for details.
39 | # def filename
40 | # "something.jpg" if original_filename
41 | # end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/reset.css:
--------------------------------------------------------------------------------
1 | /*------------------------------------+
2 | | Part: Reset browser default styles |
3 | +------------------------------------*/
4 |
5 | /* Imports
6 | =====================================================================*/
7 | @import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:300,300italic,400,400italic,600,600italic,700,700italic);
8 |
9 | /*@import "fontawesome/fontawesome.scss";*/
10 | /*(@import url(//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css);*/
11 |
12 | a, abbr, acronym, address, applet, big, blockquote, body, caption, cite, code,
13 | dd, del, dfn, div, dl, dt, em, fieldset, font, form, h1, h2, h3, h4, h5, h6,
14 | html, iframe, img, ins, kbd, label, legend, li, object, ol, p, pre, q, s, samp,
15 | small, span, strike, strong, sub, sup, table, tbody, td tfoot, th, thead, tr,
16 | tt, ul, var {
17 | border: 0;
18 | font-family: inherit;
19 | font-size: 100%;
20 | font-style: inherit;
21 | font-weight: inherit;
22 | margin: 0;
23 | outline: 0;
24 | padding: 0;
25 | vertical-align: baseline;
26 | }
27 |
28 | :focus { outline: 0; }
29 | blockquote, q { quotes: "" ""; }
30 | blockquote:before, blockquote:after, q:before, q:after { content: ""; }
31 | body { background: #fff; line-height: 1; color: #000; }
32 | caption, th, td { text-align: left; font-weight: normal; }
33 | ol, ul { list-style: none; }
34 | table { border-collapse: separate; border-spacing: 0; }
--------------------------------------------------------------------------------
/app/views/devise/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 |
Edit <%= resource_name.to_s.humanize %>
2 |
3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %>
9 |
10 |
11 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %>
14 |
15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "off" %>
18 |
19 |
20 |
21 | <%= f.label :password_confirmation %>
22 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
23 |
24 |
25 |
26 | <%= f.label :current_password %> (we need your current password to confirm your changes)
27 | <%= f.password_field :current_password, autocomplete: "off" %>
28 |
29 |
30 |
31 | <%= f.submit "Update" %>
32 |
33 | <% end %>
34 |
35 |
Cancel my account
36 |
37 |
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
38 |
39 | <%= link_to "Back", :back %>
40 |
--------------------------------------------------------------------------------
/app/views/rakes/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 |
Edit <%= resource_name.to_s.humanize %>
2 |
3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true %>
9 |
10 |
11 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
12 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
13 | <% end %>
14 |
15 |
16 | <%= f.label :password %> (leave blank if you don't want to change it)
17 | <%= f.password_field :password, autocomplete: "off" %>
18 |
19 |
20 |
21 | <%= f.label :password_confirmation %>
22 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
23 |
24 |
25 |
26 | <%= f.label :current_password %> (we need your current password to confirm your changes)
27 | <%= f.password_field :current_password, autocomplete: "off" %>
28 |
29 |
30 |
31 | <%= f.submit "Update" %>
32 |
33 | <% end %>
34 |
35 |
Cancel my account
36 |
37 |
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
38 |
39 | <%= link_to "Back", :back %>
40 |
--------------------------------------------------------------------------------
/app/controllers/extranet/superintendents_controller.rb:
--------------------------------------------------------------------------------
1 | module Extranet
2 | class SuperintendentsController < Extranet::ApplicationController
3 | before_action :populate_superintendent, only: [:edit, :update]
4 |
5 | def index
6 | @superintendents = Superintendent.order(start_of_term: :desc)
7 | end
8 |
9 | def new
10 | @superintendent = Superintendent.new
11 | end
12 |
13 | def create
14 | @superintendent = Superintendent.new(superintendent_params)
15 | if @superintendent.save
16 | redirect_to extranet_superintendents_path, :notice => "New superintendent successfully added"
17 | else
18 | flash[:alert] = "Oops, there was an error adding a new superintendent."
19 | render :action => 'new'
20 | end
21 | end
22 |
23 | def update
24 | if @superintendent.update(superintendent_params)
25 | redirect_to extranet_superintendents_path, :notice => "Superintendent successfully updated"
26 | else
27 | flash[:alert] = "Oops, there was an error updating superintendent information."
28 | render :action => 'edit'
29 | end
30 | end
31 |
32 | private
33 | def superintendent_params
34 | params.require(:superintendent).permit(:first_name, :last_name, :start_of_term, :end_of_term)
35 | end
36 |
37 | def populate_superintendent
38 | @superintendent = Superintendent.find(params[:id])
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/db/migrate/20211127153204_convert_to_rich_text.rb:
--------------------------------------------------------------------------------
1 | class ConvertToRichText < ActiveRecord::Migration[6.1]
2 | include ActionView::Helpers::TextHelper
3 | def change
4 | rename_column :board_member_votes, :dissent_description, :dissent_description_old
5 | BoardMemberVote.all.each do |boardMemberVote|
6 | boardMemberVote.update_attribute(:dissent_description, simple_format(boardMemberVote.dissent_description_old))
7 | end
8 | remove_column :board_member_votes, :dissent_description_old
9 |
10 | rename_column :cases, :majority_decision, :majority_decision_old
11 | Case.all.each do |theCase|
12 | theCase.update_attribute(:majority_decision, simple_format(theCase.majority_decision_old))
13 | end
14 | remove_column :cases, :majority_decision_old
15 |
16 | rename_column :minority_opinions, :opinion_text, :opinion_text_old
17 | MinorityOpinion.all.each do |minorityOpinion|
18 | minorityOpinion.update_attribute(:opinion_text, simple_format(minorityOpinion.opinion_text_old))
19 | end
20 | remove_column :minority_opinions, :opinion_text_old
21 |
22 | rename_column :rules, :description, :description_old
23 | rename_column :rules, :comment, :comment_old
24 | Rule.all.each do |rule|
25 | rule.update_attribute(:description, simple_format(rule.description_old))
26 | rule.update_attribute(:comment, simple_format(rule.comment_old))
27 | end
28 | remove_column :rules, :description_old
29 | remove_column :rules, :comment_old
30 |
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/views/board/responsibilities.html.erb:
--------------------------------------------------------------------------------
1 | <% title "Board responsibilities" %>
2 |
3 |
4 |
Board responsibilities
5 |
The Police Board's primary powers and responsibilities include the following:
6 |
The Board decides disciplinary cases when the Superintendent of Police files charges to discharge or suspend a police officer for more than thirty days.
7 |
The Board reviews, upon the request of police officers, disciplinary suspensions of six through thirty days.
8 |
The Board decides matters in which the Chief Administrator of the Independent Police Review Authority and the Superintendent do not concur regarding discipline of a police officer.
9 |
The Board holds monthly public meetings that provide an opportunity for all members of the public to present questions and comments to the Board, the Superintendent of Police, and the Chief Administrator of the Independent Police Review Authority.
10 |
When there is a vacancy in the position of Superintendent of Police, the Board reviews applications, conducts interviews, and submits to the Mayor a list of three candidates; the Mayor must choose from the list or request another list from the Board.
11 |
The Board adopts the rules and regulations governing the Police Department.
12 |
<%= link_to raw("See board members and their voting history »"), board_index_path %>
13 |
--------------------------------------------------------------------------------
/db/migrate/20180413200531_update_minority_opnions_associations.rb:
--------------------------------------------------------------------------------
1 | class UpdateMinorityOpnionsAssociations < ActiveRecord::Migration[4.2]
2 | def change
3 | MinorityOpinion.where("board_member_one = ''").update_all(board_member_one: nil)
4 | MinorityOpinion.where("board_member_two = ''").update_all(board_member_two: nil)
5 | MinorityOpinion.where("board_member_three = ''").update_all(board_member_three: nil)
6 | MinorityOpinion.where("board_member_four = ''").update_all(board_member_four: nil)
7 |
8 | change_column :minority_opinions, :board_member_one, 'integer USING CAST(board_member_one AS integer)'
9 | change_column :minority_opinions, :board_member_two, 'integer USING CAST(board_member_two AS integer)'
10 | change_column :minority_opinions, :board_member_three, 'integer USING CAST(board_member_three AS integer)'
11 | change_column :minority_opinions, :board_member_four, 'integer USING CAST(board_member_four AS integer)'
12 |
13 | rename_column :minority_opinions, :board_member_one, :board_member_one_id
14 | rename_column :minority_opinions, :board_member_two, :board_member_two_id
15 | rename_column :minority_opinions, :board_member_three, :board_member_three_id
16 | rename_column :minority_opinions, :board_member_four, :board_member_four_id
17 |
18 | add_index :minority_opinions, :board_member_one_id
19 | add_index :minority_opinions, :board_member_two_id
20 | add_index :minority_opinions, :board_member_three_id
21 | add_index :minority_opinions, :board_member_four_id
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/uploaders/case_file_uploader.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | class CaseFileUploader < CarrierWave::Uploader::Base
4 | # Override the directory where uploaded files will be stored.
5 | # This is a sensible default for uploaders that are meant to be mounted:
6 | def store_dir
7 | #"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
8 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}"
9 | end
10 |
11 | # Provide a default URL as a default if there hasn't been a file uploaded:
12 | # def default_url
13 | # # For Rails 3.1+ asset pipeline compatibility:
14 | # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
15 | #
16 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_')
17 | # end
18 |
19 | # Process files as they are uploaded:
20 | # process :scale => [200, 300]
21 | #
22 | # def scale(width, height)
23 | # # do something
24 | # end
25 |
26 | # Create different versions of your uploaded files:
27 | # version :thumb do
28 | # process :resize_to_fit => [50, 50]
29 | # end
30 |
31 | # Add a white list of extensions which are allowed to be uploaded.
32 | # For images you might use something like this:
33 | def extension_white_list
34 | %w(pdf doc docx)
35 | end
36 |
37 | # Override the filename of the uploaded files:
38 | # Avoid using model.id or version_name here, see uploader/store.rb for details.
39 | # def filename
40 | # "something.jpg" if original_filename
41 | # end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 | # # If you are using webpack-dev-server then specify webpack-dev-server host
15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/db/migrate/20210625201618_create_active_storage_tables.active_storage.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from active_storage (originally 20170806125915)
2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
3 | def change
4 | create_table :active_storage_blobs do |t|
5 | t.string :key, null: false
6 | t.string :filename, null: false
7 | t.string :content_type
8 | t.text :metadata
9 | t.string :service_name, null: false
10 | t.bigint :byte_size, null: false
11 | t.string :checksum, null: false
12 | t.datetime :created_at, null: false
13 |
14 | t.index [ :key ], unique: true
15 | end
16 |
17 | create_table :active_storage_attachments do |t|
18 | t.string :name, null: false
19 | t.references :record, null: false, polymorphic: true, index: false
20 | t.references :blob, null: false
21 |
22 | t.datetime :created_at, null: false
23 |
24 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
25 | t.foreign_key :active_storage_blobs, column: :blob_id
26 | end
27 |
28 | create_table :active_storage_variant_records do |t|
29 | t.belongs_to :blob, null: false, index: false
30 | t.string :variation_digest, null: false
31 |
32 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
33 | t.foreign_key :active_storage_blobs, column: :blob_id
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/db/migrate/20160210211917_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | class DeviseCreateUsers < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table(:users) do |t|
4 | ## Database authenticatable
5 | t.string :email, null: false, default: ""
6 | t.string :encrypted_password, null: false, default: ""
7 |
8 | ## Recoverable
9 | t.string :reset_password_token
10 | t.datetime :reset_password_sent_at
11 |
12 | ## Rememberable
13 | t.datetime :remember_created_at
14 |
15 | ## Trackable
16 | t.integer :sign_in_count, default: 0, null: false
17 | t.datetime :current_sign_in_at
18 | t.datetime :last_sign_in_at
19 | t.inet :current_sign_in_ip
20 | t.inet :last_sign_in_ip
21 |
22 | ## Confirmable
23 | # t.string :confirmation_token
24 | # t.datetime :confirmed_at
25 | # t.datetime :confirmation_sent_at
26 | # t.string :unconfirmed_email # Only if using reconfirmable
27 |
28 | ## Lockable
29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
30 | # t.string :unlock_token # Only if unlock strategy is :email or :both
31 | # t.datetime :locked_at
32 |
33 |
34 | t.timestamps null: false
35 | end
36 |
37 | add_index :users, :email, unique: true
38 | add_index :users, :reset_password_token, unique: true
39 | # add_index :users, :confirmation_token, unique: true
40 | # add_index :users, :unlock_token, unique: true
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/controllers/analytics_controller.rb:
--------------------------------------------------------------------------------
1 | class AnalyticsController < ApplicationController
2 |
3 | def index
4 | start_year = (params[:start_year] || Date.today.year - 4).to_i
5 | end_year = (params[:end_year] || Date.today.year).to_i
6 |
7 | @cases_filed_by_year_range = (start_year..end_year).to_a
8 | total_cases = []
9 | decided_cases = []
10 |
11 |
12 | recommended_outcome_ids = Outcome.where(name: ["Suspension", "Termination"]).map(&:id)
13 | decided_outcome_ids = Outcome.where(name: ["Returned to Duty", "Settlement", "Suspension", "Termination"]).map(&:id)
14 | rank_ids = Rank.where(name: ["Police Officer", "Detective", "Gang Crimes Specialist", "Lieutenant", "Sargeant"]).map(&:id)
15 |
16 |
17 | @cases_filed_by_year_range.each do |year|
18 | cases_per_year = Case.includes(:defendant).where('extract(year from date_initiated) = ?', year).where(recommended_outcome_id: recommended_outcome_ids).where(:defendants => {rank_id: rank_ids})
19 | decisions_per_year = Case.includes(:defendant).where('extract(year from date_decided) = ?', year).where(decided_outcome_id: decided_outcome_ids).where(:defendants => {rank_id: rank_ids})
20 |
21 | total_cases.push(cases_per_year.count)
22 | decided_cases.push(decisions_per_year.count)
23 | end
24 |
25 | @cases_filed_by_year_series = [
26 | {
27 | name: 'Total cases',
28 | data: total_cases
29 | },
30 | {
31 | name: 'Decided cases',
32 | data: decided_cases
33 | }
34 | ]
35 |
36 | end
37 |
38 | end
39 |
40 |
--------------------------------------------------------------------------------
/app/views/board/show.html.erb:
--------------------------------------------------------------------------------
1 | <% title @member.full_name %>
2 | <% body_id "board-detail" %>
3 |
4 |
5 |
30 |
31 |
32 | <%= render partial: "member_cases", locals: { cases: @member.cases } %>
33 |
--------------------------------------------------------------------------------
/app/controllers/extranet/rules_controller.rb:
--------------------------------------------------------------------------------
1 | module Extranet
2 | class RulesController < Extranet::ApplicationController
3 | def index
4 | @rules = Rule.all
5 | @rules.inspect
6 | end
7 |
8 | def new
9 | @rule = Rule.new
10 | end
11 |
12 | def create
13 | @rule = Rule.create
14 | if @rule.save
15 | redirect_to extranet_rules_path, :notice => "Rule successfully added"
16 | else
17 | flash[:error] = "Oops, there was an error creating a new rule."
18 | render :action => 'new'
19 | end
20 | end
21 |
22 | def edit
23 | @rule = Rule.find(params[:id])
24 | end
25 |
26 | def update
27 | @rule = Rule.find(params[:id])
28 | if @rule.update(rule_params)
29 | redirect_to extranet_rules_path, :notice => "rule successfully saved"
30 | else
31 | render :action => 'edit'
32 | end
33 | end
34 |
35 | def show
36 | end
37 |
38 | def destroy
39 | @rule = Rule.find(params[:id])
40 | @rule.destroy
41 | flash[:notice] = "Rule deleted"
42 | redirect_to extranet_rules_path
43 | end
44 |
45 | private
46 | def rule_params
47 | params.require(:rule).permit(:code, :description, :comment)
48 | #params.require(:case).permit(:number, :date_initiated, :date_decided, :recommended_outcome_id, :decided_outcome_id,
49 | # defendant_attributes: [:first_name, :last_name, :rank_id, :number],
50 | # :case_rules_attributes => [[:id, :_destroy]]
51 | #)
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/app/views/extranet/cases/index.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% title "Cases" %>
4 |
5 |
Cases
6 |
<%= link_to "Download CSV", extranet_cases_path(format: :csv) %>
7 |
8 |
<%= link_to "Add case", [:new, :extranet, :case], :class => "action" %>
9 |
10 |
11 |
12 |
13 | | <%= sort_link("number", "Case #") %> |
14 | <%= sort_link("defendants.first_name", "Name") %> |
15 | <%= sort_link("date_decided", "Judgement date") %> |
16 | <%= sort_link("outcomes.name", "Outcome") %> |
17 | <%= sort_link("updated_at", "Last modified") %> |
18 | <%= sort_link("id", "Last created") %> |
19 | |
20 |
21 |
22 |
23 | <% @cases.each do |c| %>
24 |
25 | | <%= link_to c.number, [:edit, :extranet, c] %> |
26 | <%= c.defendant.present? ? c.defendant.full_name : '' %> |
27 | <%= c.date_decided %> |
28 | <%= c.decided_outcome.present? ? c.decided_outcome.name : '' %> |
29 | <%= c.updated_at %> |
30 | <%= c.created_at %> |
31 | <%= link_to "X", [:extranet, c], :confirm => 'Are you sure?', :method => :delete, :title => "Delete" %> |
32 |
33 | <% end %>
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | def body_id(body_id)
3 | content_for (:body_id) { body_id }
4 | end
5 |
6 | def navClass(controller)
7 | "current" if params[:controller] == controller || request.path.starts_with?('/' + controller)
8 | end
9 |
10 | def title(page_title)
11 | content_for (:title) { page_title }
12 | end
13 |
14 | def link_to_add_fields(name, f, association)
15 | new_object = f.object.class.reflect_on_association(association).klass.new
16 | fields = f.fields_for(association, new_object, :child_index => "new_#{association}") do |builder|
17 | render(association.to_s.singularize + "_fields", :f => builder)
18 | end
19 |
20 | link_to(name,'#', class: "add_fields", data: {id: 0, fields: fields.gsub("\n",""), association: association})
21 | end
22 |
23 | def sort_link(column, title = nil)
24 | title ||= column.titleize
25 | direction = column == sort_column && sort_direction == "asc" ? "desc" : "asc"
26 | icon = sort_direction == "asc" ? "▲" : "▼"
27 | icon = column == sort_column ? icon : ""
28 | link_to "#{title}
#{icon}".html_safe,
29 | params.permit(:per_page, :search, :sort, :direction).merge(sort: column, direction: direction)
30 | end
31 |
32 | def share_on_facebook
33 | link_to image_tag("facebook.png"), 'https://www.facebook.com/sharer.php?u='+request.original_url, target: :blank
34 | end
35 |
36 | def share_on_twitter(title)
37 | link_to image_tag("twitter.png"), 'https://twitter.com/share?text='+title+'&url='+request.original_url, target: :blank
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/.ebextensions/03_files.config:
--------------------------------------------------------------------------------
1 | files:
2 | "/opt/elasticbeanstalk/support/conf/webapp_healthd.conf":
3 | mode: "000644"
4 | owner: root
5 | group: root
6 | content: |
7 | upstream my_app {
8 | server unix:///var/run/puma/my_app.sock;
9 | }
10 |
11 | log_format healthd '$msec"$uri"'
12 | '$status"$request_time"$upstream_response_time"'
13 | '$http_x_forwarded_for';
14 |
15 | server {
16 | listen 80;
17 | server_name _ localhost; # need to listen to localhost for worker tier
18 |
19 | if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
20 | set $year $1;
21 | set $month $2;
22 | set $day $3;
23 | set $hour $4;
24 | }
25 |
26 | access_log /var/log/nginx/access.log main;
27 | access_log /var/log/nginx/healthd/application.log.$year-$month-$day-$hour healthd;
28 |
29 | client_max_body_size 20M;
30 |
31 | location / {
32 | proxy_pass http://my_app; # match the name of upstream directive which is defined above
33 | proxy_set_header Host $host;
34 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35 | }
36 |
37 | location /assets {
38 | alias /var/app/current/public/assets;
39 | gzip_static on;
40 | gzip on;
41 | expires max;
42 | add_header Cache-Control public;
43 | }
44 |
45 | location /public {
46 | alias /var/app/current/public;
47 | gzip_static on;
48 | gzip on;
49 | expires max;
50 | add_header Cache-Control public;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
12 | #
13 | port ENV.fetch("PORT") { 3000 }
14 |
15 | # Specifies the `environment` that Puma will run in.
16 | #
17 | environment ENV.fetch("RAILS_ENV") { "development" }
18 |
19 | # Specifies the `pidfile` that Puma will use.
20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
21 |
22 | # Specifies the number of `workers` to boot in clustered mode.
23 | # Workers are forked web server processes. If using threads and workers together
24 | # the concurrency of the application would be max `threads` * `workers`.
25 | # Workers do not work on JRuby or Windows (both of which do not support
26 | # processes).
27 | #
28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
29 |
30 | # Use the `preload_app!` method when specifying a `workers` number.
31 | # This directive tells Puma to first boot the application and load code
32 | # before forking the application. This takes advantage of Copy On Write
33 | # process behavior so workers use less memory.
34 | #
35 | # preload_app!
36 |
37 | # Allow puma to be restarted by `rails restart` command.
38 | plugin :tmp_restart
39 |
--------------------------------------------------------------------------------
/rails-vagrant-provision.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | sudo locale-gen en_US.UTF-8
4 | sudo update-locale LANG=en_US.UTF-8
5 | sudo update-locale LC_ALL=en_US.UTF-8
6 |
7 | sudo apt-get update
8 | sudo apt-get install -y build-essential git curl libxslt1-dev libxml2-dev libssl-dev
9 |
10 | # postgres
11 | echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main " | sudo tee -a /etc/apt/sources.list.d/pgdg.list
12 | sudo wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
13 | sudo apt-get update
14 | sudo apt-get install -y postgresql-9.3 libpq-dev
15 | echo '# "local" is for Unix domain socket connections only
16 | local all all trust
17 | # IPv4 local connections:
18 | host all all 0.0.0.0/0 trust
19 | # IPv6 local connections:
20 | host all all ::/0 trust' | sudo tee /etc/postgresql/9.3/main/pg_hba.conf
21 | sudo sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/g" /etc/postgresql/9.3/main/postgresql.conf
22 | sudo /etc/init.d/postgresql restart
23 | sudo su - postgres -c 'createuser -s vagrant'
24 |
25 | # redis
26 | sudo apt-get install -y python-software-properties
27 | sudo add-apt-repository -y ppa:rwky/redis
28 | sudo apt-get update
29 | sudo apt-get install -y redis-server
30 |
31 | # rvm and ruby
32 | su - vagrant -c 'curl -sSL https://get.rvm.io | bash -s stable --ruby'
33 | su - vagrant -c 'rvm rvmrc warning ignore allGemfiles'
34 |
35 | # node
36 | su - vagrant -c 'curl https://raw.githubusercontent.com/creationix/nvm/v0.14.0/install.sh | sh'
37 | su - vagrant -c 'nvm install 0.10'
38 | su - vagrant -c 'nvm alias default 0.10'
39 |
40 | echo "All done installing!
--------------------------------------------------------------------------------
/app/controllers/board_controller.rb:
--------------------------------------------------------------------------------
1 | class BoardController < ApplicationController
2 | def index
3 | board_members = BoardMember.order(board_position: :asc, last_name: :asc)
4 | @current_board_members = board_members.select{ |bm| bm.active == true }
5 | @past_board_members = board_members.select{ |bm| bm.active == false }
6 | end
7 |
8 | def show
9 | @member = BoardMember.find(params[:id])
10 |
11 | @vote_bar_chart = [
12 | { :class => "agree", :size => @member.votes_agree_rate },
13 | { :class => "disagree", :size => @member.votes_dissent_rate },
14 | { :class => "novote", :size => @member.votes_abstain_rate}]
15 |
16 | if @member.votes_agree_rate > 10
17 | @vote_bar_chart[0].merge!(:symbol => "fa-check-circle", :display => "#{@member.votes_agree_rate}%")
18 | end
19 | if @member.votes_dissent_rate > 10
20 | @vote_bar_chart[1].merge!(:symbol => "fa-times-circle", :display => "#{@member.votes_dissent_rate}%")
21 | end
22 | if @member.votes_abstain_rate > 10
23 | @vote_bar_chart[2].merge!(:symbol => "fa-ban", :display => "#{@member.votes_abstain_rate}%")
24 | end
25 | end
26 |
27 | def new
28 | @member = BoardMember.new()
29 | end
30 |
31 | def create
32 | @member = BoardMember.new(entry_params)
33 | if @member.save
34 | redirect_to board_path(@board), notice: 'Board successfully created'
35 | else
36 | render :new
37 | end
38 | end
39 |
40 | def update
41 | @member = BoardMember.find(params[:id])
42 | if @member.update(params)
43 | redirect_to board_path(@board), notice: 'Board successfully updated'
44 | else
45 | render :edit
46 | end
47 | end
48 |
49 | def responsibilities
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/views/extranet/superintendents/_form.html.erb:
--------------------------------------------------------------------------------
1 |
Superintendent details
2 | <%= form_for [:extranet, @superintendent], :html => {:multipart => true} do |f| %>
3 |
4 | -
5 |
<%= f.label :first_name, "Name" %>
6 |
12 |
13 | -
14 |
<%= f.label :start_of_term, "Start of term" %>
15 |
20 |
21 | -
22 |
<%= f.label :end_of_term, "End of term" %>
23 |
28 |
29 |
30 |
31 |
32 | -
33 |
34 | <%= f.submit "Save", :class => "action" %>
35 | <%= link_to "Cancel", extranet_superintendents_path %>
36 |
37 |
38 |
39 | <% end %>
40 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/models/board_member.rb:
--------------------------------------------------------------------------------
1 | class BoardMember < ActiveRecord::Base
2 | has_many :board_member_votes
3 | has_many :terms
4 | has_many :cases, through: :board_member_votes
5 | mount_uploader :image, ImageUploader
6 |
7 | accepts_nested_attributes_for :terms, :allow_destroy => true
8 |
9 | def active
10 | self.terms.each do |term|
11 | if term.end && term.end >= DateTime.now.to_date
12 | return true
13 | end
14 | end
15 | return false
16 | end
17 |
18 | def photo
19 | board_dir = "board-members/"
20 | photo = [board_dir, full_name.downcase.gsub(' ','-').gsub('.',''), ".jpg"].join
21 | File.exists?([Rails.root, "/app/assets/images/", photo].join) ? photo : "#{board_dir}placeholder.jpg"
22 | end
23 |
24 | def full_name
25 | [first_name, last_name].join(' ')
26 | end
27 |
28 | def votes_total_count
29 | total = BoardMemberVote.where(board_member_id: id).count
30 | total == 0 ? 1 : total
31 | end
32 |
33 | def votes_abstain_count
34 | BoardMemberVote.where(board_member_id: id, vote_id: Vote.ABSTAIN).count
35 | end
36 |
37 | def votes_abstain_rate
38 | (votes_abstain_count.to_f / votes_total_count * 100).round
39 | end
40 |
41 | def votes_dissent_count
42 | BoardMemberVote.where(board_member_id: id, vote_id: Vote.DISSENT).count
43 | end
44 |
45 | def votes_dissent_rate
46 | (votes_dissent_count.to_f / votes_total_count * 100).round
47 | end
48 |
49 | def votes_agree_count
50 | agree_vote_id = Vote.find_by_name("Agree")
51 | BoardMemberVote.where(board_member_id: id, vote_id: Vote.AGREE).count
52 | end
53 |
54 | def votes_agree_rate
55 | (votes_agree_count.to_f / votes_total_count * 100).round
56 | end
57 |
58 | def status
59 | if active
60 | return 'current'
61 | else
62 | return 'past'
63 | end
64 |
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/controllers/extranet/board_members_controller.rb:
--------------------------------------------------------------------------------
1 | module Extranet
2 | class BoardMembersController < Extranet::ApplicationController
3 | def index
4 | @board_members = BoardMember.all
5 | end
6 |
7 | def new
8 | @board_member = BoardMember.new
9 | 1.times do
10 | term = @board_member.terms.build
11 | end
12 | end
13 |
14 | def create
15 | @board_member = BoardMember.new(board_member_params)
16 | if @board_member.save
17 | redirect_to extranet_board_members_path, :notice => "Board member successfully added"
18 | else
19 | flash[:error] = "Oops there was an error."
20 | render 'new'
21 | end
22 | end
23 |
24 | def edit
25 | @board_member = BoardMember.find(params[:id])
26 | end
27 |
28 | def update
29 | #render :text => @some_object.inspect
30 | #raise board_member_params.inspect
31 | #debug.inspect
32 |
33 | @board_member = BoardMember.find(params[:id])
34 | if @board_member.update(board_member_params)
35 | redirect_to extranet_board_members_path, :notice => "Board member successfully updated"
36 | else
37 | render :action => 'edit'
38 | end
39 |
40 | end
41 |
42 | def show
43 | @board_member = BoardMember.find(params[:id])
44 | end
45 |
46 | def destroy
47 | @board_member = BoardMember.find(params[:id])
48 | @board_member.destroy
49 | flash[:notice] = "Board member deleted"
50 | redirect_to extranet_board_members_path
51 | end
52 |
53 | private
54 | def board_member_params
55 | params.require(:board_member).permit(:first_name, :last_name, :image, :remove_image, :board_position, :job_title, :organization,
56 | terms_attributes: [:id, :board_member_id, :start, :end] )
57 |
58 | #params.require(:board_member).permit!
59 |
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | var validEnv = ['development', 'test', 'production']
3 | var currentEnv = api.env()
4 | var isDevelopmentEnv = api.env('development')
5 | var isProductionEnv = api.env('production')
6 | var isTestEnv = api.env('test')
7 |
8 | if (!validEnv.includes(currentEnv)) {
9 | throw new Error(
10 | 'Please specify a valid `NODE_ENV` or ' +
11 | '`BABEL_ENV` environment variables. Valid values are "development", ' +
12 | '"test", and "production". Instead, received: ' +
13 | JSON.stringify(currentEnv) +
14 | '.'
15 | )
16 | }
17 |
18 | return {
19 | presets: [
20 | isTestEnv && [
21 | '@babel/preset-env',
22 | {
23 | targets: {
24 | node: 'current'
25 | }
26 | }
27 | ],
28 | (isProductionEnv || isDevelopmentEnv) && [
29 | '@babel/preset-env',
30 | {
31 | forceAllTransforms: true,
32 | useBuiltIns: 'entry',
33 | corejs: 3,
34 | modules: false,
35 | exclude: ['transform-typeof-symbol']
36 | }
37 | ]
38 | ].filter(Boolean),
39 | plugins: [
40 | 'babel-plugin-macros',
41 | '@babel/plugin-syntax-dynamic-import',
42 | isTestEnv && 'babel-plugin-dynamic-import-node',
43 | '@babel/plugin-transform-destructuring',
44 | [
45 | '@babel/plugin-proposal-class-properties',
46 | {
47 | loose: true
48 | }
49 | ],
50 | [
51 | '@babel/plugin-proposal-object-rest-spread',
52 | {
53 | useBuiltIns: true
54 | }
55 | ],
56 | [
57 | '@babel/plugin-transform-runtime',
58 | {
59 | helpers: false
60 | }
61 | ],
62 | [
63 | '@babel/plugin-transform-regenerator',
64 | {
65 | async: false
66 | }
67 | ]
68 | ].filter(Boolean)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'simple_form'
4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5 | gem 'rails', '~> 6.0'
6 | # Use postgresql as the database for Active Record
7 | gem 'pg'
8 | # Use SCSS for stylesheets
9 | gem 'sass-rails', '~> 5.0'
10 | # Use Uglifier as compressor for JavaScript assets
11 | gem 'uglifier', '>= 1.3.0'
12 | # Use CoffeeScript for .js.coffee assets and views
13 | gem 'coffee-rails', '~> 5.0'
14 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes
15 | # gem 'therubyracer', platforms: :ruby
16 |
17 |
18 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
19 | gem 'jbuilder', '~> 2.0'
20 | # bundle exec rake doc:rails generates the API under doc/api.
21 | #gem 'sdoc', '~> 0.4.0', group: :doc
22 |
23 | gem 'twitter'
24 |
25 | group :development do
26 | gem 'foreman'
27 | gem 'htmlbeautifier'
28 | gem 'spring'
29 | gem 'listen'
30 | end
31 |
32 | # Use ActiveModel has_secure_password
33 | # gem 'bcrypt', '~> 3.1.7'
34 |
35 | # Use unicorn as the app server
36 | # gem 'unicorn'
37 |
38 | # Use Capistrano for deployment
39 | # gem 'capistrano-rails', group: :development
40 |
41 | # Use debugger
42 | # gem 'debugger', group: [:development, :test]
43 |
44 | # use devise for authentication
45 | gem 'devise'
46 |
47 |
48 | # For Heroku
49 | #gem 'rails_12factor', group: :production
50 | gem 'puma'
51 |
52 | ruby '~> 3.0'
53 |
54 | gem 'font-awesome-rails', '~> 4.7.0.7'
55 | #gem 'font-awesome-sass'
56 | gem 'roo', '~> 2.0.0'
57 | gem 'will_paginate'
58 |
59 | gem 'mini_magick'
60 | gem 'carrierwave', '~> 1.0'
61 |
62 | gem 'fog-aws'
63 |
64 | gem 'tinymce-rails'
65 |
66 | #for managing environment variables
67 | gem 'figaro'
68 |
69 | gem 'aws-sdk'
70 |
71 | #for debugging
72 | gem 'byebug', group: [:development, :test]
73 |
74 |
75 | #for notification banners
76 | gem 'izitoast'
77 |
78 | gem 'bootsnap'
79 |
80 | gem 'webpacker', '~> 5.4'
81 |
82 |
83 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults_5_2.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 5.2 upgrade.
4 | #
5 | # Once upgraded flip defaults one by one to migrate to the new default.
6 | #
7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option.
8 |
9 | # Make Active Record use stable #cache_key alongside new #cache_version method.
10 | # This is needed for recyclable cache keys.
11 | # Rails.application.config.active_record.cache_versioning = true
12 |
13 | # Use AES-256-GCM authenticated encryption for encrypted cookies.
14 | # Also, embed cookie expiry in signed or encrypted cookies for increased security.
15 | #
16 | # This option is not backwards compatible with earlier Rails versions.
17 | # It's best enabled when your entire app is migrated and stable on 5.2.
18 | #
19 | # Existing cookies will be converted on read then written with the new scheme.
20 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
21 |
22 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
23 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
24 | # Rails.application.config.active_support.use_authenticated_message_encryption = true
25 |
26 | # Add default protection from forgery to ActionController::Base instead of in
27 | # ApplicationController.
28 | # Rails.application.config.action_controller.default_protect_from_forgery = true
29 |
30 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
31 | # 'f' after migrating old data.
32 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
33 |
34 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
35 | # Rails.application.config.active_support.use_sha1_digests = true
36 |
37 | # Make `form_with` generate id attributes for any generated HTML tags.
38 | # Rails.application.config.action_view.form_with_generates_ids = true
39 |
--------------------------------------------------------------------------------
/app/views/rules/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title "Rules of conduct" %>
2 | <% body_id "rules-index" %>
3 |
4 |
5 |
Rules of conduct
6 |
7 |
From the Rules and Regulations of the Chicago Police Department adopted and published by the Police Board.
8 |
9 |
10 |
11 |
12 | | Hidden rule # |
13 | Rule |
14 | Description |
15 | Cases |
16 | Hidden cases |
17 | |
18 |
19 |
20 |
21 | <% @rules.each do |rule| %>
22 |
23 | | <%= rule.code %> |
24 | Rule <%= rule.code %> |
25 |
26 | <%= rule.description %>
27 | <% if rule.comment && raw(rule.comment).length>45 %>
28 |
29 |
30 | <% else %>
31 |
32 | <% end %>
33 | |
34 | <% cases = CaseRule.where(rule_id: rule.id).count %>
35 | <%= cases %> (<%= (cases.to_f / Case.count * 100).round %>%) |
36 | |
37 | |
38 |
39 | <% end %>
40 |
41 |
42 |
43 |
44 |
45 |
46 | <%= javascript_pack_tag "rules" %>
47 |
--------------------------------------------------------------------------------
/app/views/welcome/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title "Home" %>
2 | <% body_id "home" %>
3 |
4 |
5 |
6 |
7 |
8 |
Case outcomes
9 |
How does the superintendent's recommended discipline compare to the final police board's judgement?
10 |
11 |
12 |
13 |
14 | |
15 | Recommendation Decision |
16 | Cases |
17 | Annual trend |
18 | |
19 |
20 |
21 |
22 | <% @case_outcomes.each do |outcomes, count| %>
23 |
24 | | <%= count %> |
25 | <%= Outcome.find(outcomes[0]).name %> <%= Outcome.find(outcomes[1]).name %> |
26 | <%= count %> (<%= (count.to_f / @cases.count * 100).round %>%) |
27 | <%= Case.count_per_year_for_outcome(outcomes[0], outcomes[1]).join(',') %> |
28 | |
29 |
30 | <% end %>
31 |
32 |
33 |
34 |
35 | <%= render partial: "board/list", locals: { board_members: @board_members, title: '
Board member voting history
', table_id: 'board-list' } %>
36 |
37 |
38 |
39 |
40 | <%= javascript_pack_tag "board-members-voting-history" %>
41 | <%= javascript_pack_tag "home" %>
42 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 |
6 | Rails.application.configure do
7 | # Settings specified here will take precedence over those in config/application.rb.
8 |
9 | config.cache_classes = true
10 |
11 | # Do not eager load code on boot. This avoids loading your whole application
12 | # just for the purpose of running a single test. If you are using a tool that
13 | # preloads Rails for running tests, you may have to set it to true.
14 | config.eager_load = false
15 |
16 | # Configure public file server for tests with Cache-Control for performance.
17 | config.public_file_server.enabled = true
18 | config.public_file_server.headers = {
19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
20 | }
21 |
22 | # Show full error reports and disable caching.
23 | config.consider_all_requests_local = true
24 | config.action_controller.perform_caching = false
25 | config.cache_store = :null_store
26 |
27 | # Raise exceptions instead of rendering exception templates.
28 | config.action_dispatch.show_exceptions = false
29 |
30 | # Disable request forgery protection in test environment.
31 | config.action_controller.allow_forgery_protection = false
32 |
33 | # Store uploaded files on the local file system in a temporary directory.
34 | config.active_storage.service = :test
35 |
36 | config.action_mailer.perform_caching = false
37 |
38 | # Tell Action Mailer not to deliver emails to the real world.
39 | # The :test delivery method accumulates sent emails in the
40 | # ActionMailer::Base.deliveries array.
41 | config.action_mailer.delivery_method = :test
42 |
43 | # Print deprecation notices to the stderr.
44 | config.active_support.deprecation = :stderr
45 |
46 | # Raises error for missing translations.
47 | # config.action_view.raise_on_missing_translations = true
48 | end
49 |
--------------------------------------------------------------------------------
/app/views/analytics/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title "Analytics" %>
2 | <% body_id "analytics-index" %>
3 |
4 |
Case Analytics
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | <%= form_tag({url: analytics_path}, {method: :get}) do %>
13 | <%= label_tag :start_year %>
14 | <%= text_field_tag :start_year, params[:start_year]|| Date.today.year - 4 %>
15 |
16 | <%= label_tag :end_year %>
17 | <%= text_field_tag :end_year, params[:end_year] || Date.today.year %>
18 |
19 | <%= submit_tag "Update" %>
20 | <% end %>
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/config/webpacker.yml:
--------------------------------------------------------------------------------
1 | # Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | default: &default
4 | source_path: app/javascript
5 | source_entry_path: packs
6 | public_root_path: public
7 | public_output_path: packs
8 | cache_path: tmp/cache/webpacker
9 | webpack_compile_output: true
10 |
11 | # Additional paths webpack should lookup modules
12 | # ['app/assets', 'engine/foo/app/assets']
13 | additional_paths: ['app/assets','node_modules']
14 |
15 | # Reload manifest.json on all requests so we reload latest compiled packs
16 | cache_manifest: false
17 |
18 | # Extract and emit a css file
19 | extract_css: false
20 |
21 | static_assets_extensions:
22 | - .jpg
23 | - .jpeg
24 | - .png
25 | - .gif
26 | - .tiff
27 | - .ico
28 | - .svg
29 | - .eot
30 | - .otf
31 | - .ttf
32 | - .woff
33 | - .woff2
34 |
35 | extensions:
36 | - .mjs
37 | - .js
38 | - .sass
39 | - .scss
40 | - .css
41 | - .module.sass
42 | - .module.scss
43 | - .module.css
44 | - .png
45 | - .svg
46 | - .gif
47 | - .jpeg
48 | - .jpg
49 |
50 | development:
51 | <<: *default
52 | compile: true
53 |
54 | # Reference: https://webpack.js.org/configuration/dev-server/
55 | dev_server:
56 | https: false
57 | host: localhost
58 | port: 3035
59 | public: localhost:3035
60 | hmr: false
61 | # Inline should be set to true if using HMR
62 | inline: true
63 | overlay: true
64 | compress: true
65 | disable_host_check: true
66 | use_local_ip: false
67 | quiet: false
68 | pretty: false
69 | headers:
70 | 'Access-Control-Allow-Origin': '*'
71 | watch_options:
72 | ignored: '**/node_modules/**'
73 |
74 |
75 | test:
76 | <<: *default
77 | compile: true
78 |
79 | # Compile test packs to a separate directory
80 | public_output_path: packs-test
81 |
82 | production:
83 | <<: *default
84 |
85 | # Production depends on precompilation of packs prior to booting for performance.
86 | compile: false
87 |
88 | # Extract and emit a css file
89 | extract_css: true
90 |
91 | # Cache manifest.json for performance
92 | cache_manifest: true
93 |
--------------------------------------------------------------------------------
/app/views/board/_list.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= raw(title) %>
3 |
4 |
>
5 |
6 |
7 | |
8 | |
9 | |
10 | How often a member agreed with the final decision of the board
11 | |
12 | |
13 |
14 |
15 | |
16 | |
17 | Board member |
18 | Agreed |
19 | Disagreed |
20 | Did not vote |
21 | Voting chart |
22 |
23 |
24 |
25 | <% board_members.each do |bm| %>
26 |
27 | | <%= bm.last_name %> |
28 | <%= image_tag bm.image_url(:thumb), alt: [bm.full_name, bm.board_position].reject(&:blank?).join(', '), size: '70' %> |
29 |
30 |
31 | <%= link_to [bm.full_name, bm.board_position].reject(&:blank?).join(', ') , board_path(bm) %>
32 |
33 |
34 | <%= [bm.job_title, bm.organization].reject(&:blank?).join(', ') %>
35 |
36 | |
37 | <%= bm.votes_agree_rate %>% |
38 | <%= bm.votes_dissent_rate %>% |
39 | <%= bm.votes_abstain_rate %>% |
40 |
41 |
44 | |
45 |
46 | <% end %>
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults_6_0.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 6.0 upgrade.
4 | #
5 | # Once upgraded flip defaults one by one to migrate to the new default.
6 | #
7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option.
8 |
9 | # Don't force requests from old versions of IE to be UTF-8 encoded.
10 | # Rails.application.config.action_view.default_enforce_utf8 = false
11 |
12 | # Embed purpose and expiry metadata inside signed and encrypted
13 | # cookies for increased security.
14 | #
15 | # This option is not backwards compatible with earlier Rails versions.
16 | # It's best enabled when your entire app is migrated and stable on 6.0.
17 | # Rails.application.config.action_dispatch.use_cookies_with_metadata = true
18 |
19 | # Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification.
20 | # Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false
21 |
22 | # Return false instead of self when enqueuing is aborted from a callback.
23 | # Rails.application.config.active_job.return_false_on_aborted_enqueue = true
24 |
25 | # Send Active Storage analysis and purge jobs to dedicated queues.
26 | # Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
27 | # Rails.application.config.active_storage.queues.purge = :active_storage_purge
28 |
29 | # When assigning to a collection of attachments declared via `has_many_attached`, replace existing
30 | # attachments instead of appending. Use #attach to add new attachments without replacing existing ones.
31 | # Rails.application.config.active_storage.replace_on_assign_to_many = true
32 |
33 | # Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
34 | #
35 | # The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
36 | # will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
37 | # If you send mail in the background, job workers need to have a copy of
38 | # MailDeliveryJob to ensure all delivery jobs are processed properly.
39 | # Make sure your entire app is migrated and stable on 6.0 before using this setting.
40 | # Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
41 |
42 | # Enable the same cache key to be reused when the object being cached of type
43 | # `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count)
44 | # of the relation's cache key into the cache version to support recycling cache key.
45 | # Rails.application.config.active_record.collection_cache_versioning = true
46 |
--------------------------------------------------------------------------------
/app/views/cases/index.html.erb:
--------------------------------------------------------------------------------
1 | <% title "Cases" %>
2 |
3 |
4 |
Cases
5 |
6 |
7 |
8 | <%= form_tag(cases_path, :method => "get", id: "case-search-content") do %>
9 | <%= text_field_tag :search, params[:search], class: "txt", placeholder: "Search cases..." %>
10 | <%= button_tag(type: 'submit', class: "action") do %>
11 |
12 | <% end %>
13 | <% end %>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
<%= @header %>
21 |
22 |
30 |
<%= link_to raw('View all cases »'), cases_path if params[:search]%>
31 |
32 |
33 |
34 |
<%= @counter %>
35 |
>
36 |
37 |
38 |
39 |
40 |
41 | <%= javascript_include_tag "//code.highcharts.com/highcharts.js" %>
42 |
95 |
96 | <%= javascript_pack_tag "case" %>
--------------------------------------------------------------------------------
/app/views/extranet/board_members/_form.html.erb:
--------------------------------------------------------------------------------
1 |
Board details
2 | <%= form_for [:extranet, @board_member], :html => {:multipart => true} do |f| %>
3 |
4 | -
5 |
<%= f.label :first_name, "Name" %>
6 |
12 |
13 | -
14 |
<%= f.label :image %>
15 | <%= image_tag @board_member.image_url(:thumb).to_s %>
16 |
<%= f.check_box :remove_image %>Remove image
17 |
<%= f.file_field :image %>
18 |
19 |
-
20 |
<%= f.label :board_position %>
21 | <%= f.text_field :board_position, :class => "txt-med" %>
22 |
23 |
-
24 |
<%= f.label :organization %>
25 | <%= f.text_field :organization, :class => "txt-med" %>
26 |
27 |
-
28 |
<%= f.label :job_title %>
29 | <%= f.text_field :job_title, :class => "txt-med" %>
30 |
31 |
32 | <%= f.fields_for :terms do |builder| %>
33 |
-
34 |
<%= builder.label :start, "Appointed" %>
35 |
44 |
45 | <% end %>
46 |
47 |
48 |
49 |
50 | -
51 |
52 | <%= f.submit "Save", :class => "action" %> <%= link_to "Cancel", extranet_board_members_path %>
53 |
54 |
55 |
56 |
57 |
58 | <% end %>
59 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | config.log_level = :debug
3 | # Verifies that versions and hashed value of the package contents in the project's package.json
4 | config.webpacker.check_yarn_integrity = true
5 | # Settings specified here will take precedence over those in config/application.rb.
6 |
7 | # In the development environment your application's code is reloaded on
8 | # every request. This slows down response time but is perfect for development
9 | # since you don't have to restart the web server when you make code changes.
10 | config.cache_classes = false
11 |
12 | # Do not eager load code on boot.
13 | config.eager_load = false
14 |
15 | # Show full error reports.
16 | config.consider_all_requests_local = true
17 |
18 | # Enable/disable caching. By default caching is disabled.
19 | # Run rails dev:cache to toggle caching.
20 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
21 | config.action_controller.perform_caching = true
22 | config.action_controller.enable_fragment_cache_logging = true
23 |
24 | config.cache_store = :memory_store
25 | config.public_file_server.headers = {
26 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
27 | }
28 | else
29 | config.action_controller.perform_caching = false
30 |
31 | config.cache_store = :null_store
32 | end
33 |
34 | # Store uploaded files on the local file system (see config/storage.yml for options).
35 | config.active_storage.service = :local
36 |
37 | # Don't care if the mailer can't send.
38 | config.action_mailer.raise_delivery_errors = false
39 |
40 | config.action_mailer.perform_caching = false
41 |
42 | # Print deprecation notices to the Rails logger.
43 | config.active_support.deprecation = :log
44 |
45 | # Raise an error on page load if there are pending migrations.
46 | config.active_record.migration_error = :page_load
47 |
48 | # Highlight code that triggered database queries in logs.
49 | config.active_record.verbose_query_logs = true
50 |
51 | # Debug mode disables concatenation and preprocessing of assets.
52 | # This option may cause significant delays in view rendering with a large
53 | # number of complex assets.
54 | config.assets.debug = true
55 |
56 | # Suppress logger output for asset requests.
57 | config.assets.quiet = true
58 |
59 | # Raises error for missing translations.
60 | # config.action_view.raise_on_missing_translations = true
61 |
62 | # Use an evented file watcher to asynchronously detect changes in source code,
63 | # routes, locales, etc. This feature depends on the listen gem.
64 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
65 |
66 | config.hosts << ENV["APP_DOMAIN"] unless ENV["APP_DOMAIN"].nil?
67 | if (gitpod_workspace_url = ENV["GITPOD_WORKSPACE_URL"])
68 | config.hosts << /.*#{URI.parse(gitpod_workspace_url).host}/
69 | end
70 | config.app_domain = ENV["APP_DOMAIN"] || "localhost:3000"
71 |
72 | end
73 |
--------------------------------------------------------------------------------
/app/javascript/packs/responsive-init.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | $().ready(function () {
3 |
4 | //this script relies on responsive.js
5 | var _Responsive = Responsive ||
6 | // ensure no js errors if not included
7 | { init: function () { alert('Method not implemented; please include responsive.js script in page head!'); } },
8 |
9 |
10 | //flags
11 | _menu_created,
12 | _menu_open,
13 |
14 |
15 | //method to create the custom sidebar menu for smaller window sizes
16 | _createMenu = function () {
17 | if ($("#nav").size() == 0) return;
18 |
19 | var nav = $("#nav").html();
20 |
21 | $("body").append("");
22 | $("#header .content").prepend("
Menu");
23 | $("#menu .fa-home").after(" Home");
24 |
25 | $("#menu").height($(window).height());
26 | $("#nav").hide();
27 |
28 | $("#nav-toggle").click(function (event) {
29 | $("#menu").show().animate({ right: "0" }, 150);
30 | $("#wrapper").css({ position: "fixed", top: 0, width: $("#wrapper").width() }).animate({ right: "250px" }, 150);
31 | _menu_open = true;
32 | event.stopPropagation();
33 | return false;
34 | });
35 |
36 | $("#nav-toggle").click(function (event) {
37 | $("#menu").show().animate({ right: "0" }, 150);
38 | $("#wrapper").css({ position: "fixed", top: 0, width: $("#wrapper").width() }).animate({ right: "250px" }, 150);
39 | _menu_open = true;
40 | event.stopPropagation();
41 | return false;
42 | });
43 |
44 | $("html").on("click touchstart", function (e) {
45 | if (_menu_open && $("#menu").has(e.target).length === 0) {
46 | $("#menu").animate({ right: "-400px" }, 150, function () {
47 | $(this).hide();
48 | });
49 | $("#wrapper").animate({ right: 0 }, 150, function () {
50 | $(this).css({ position: "static", top: "auto", width: "auto" });
51 | });
52 | _menu_open = false;
53 | e.stopPropagation();
54 | return false;
55 | }
56 | });
57 | },
58 |
59 | //method to remove the custom sidebar menu
60 | _removeMenu = function () {
61 | $("#menu, #nav-toggle").remove();
62 | $("#wrapper").css({ position: "static", top: "auto", width: "auto" });
63 | $("#nav").show();
64 | };
65 |
66 |
67 | // initialize responsive layouts
68 | _Responsive.init({
69 | layouts: {
70 | MEDIUM: { maxWidth: 900, stylesheet: false },
71 | NARROW: { maxWidth: 400, stylesheet: false }
72 | },
73 | //listen for responsive layout change; will also be fired once on page load
74 | onLayoutChange: function (layout) {
75 | if (layout == _Responsive.LayoutType.MEDIUM || layout == _Responsive.LayoutType.NARROW) {
76 | if (!_menu_created)
77 | _createMenu();
78 | _menu_created = true;
79 | }
80 | else {
81 | if (_menu_created)
82 | _removeMenu();
83 | _menu_created = false;
84 | }
85 | }
86 | });
87 |
88 | });
89 |
--------------------------------------------------------------------------------