├── .ruby-version ├── .rspec ├── Procfile ├── backend ├── models │ ├── import.rb │ ├── summary.rb │ ├── category_payment.rb │ ├── category_contribution.rb │ ├── employer_contribution.rb │ ├── whale.rb │ ├── multiple.rb │ ├── payment.rb │ ├── iec.rb │ ├── payment_codes.rb │ ├── lobbyist.rb │ ├── map.rb │ └── contribution.rb ├── fetchers │ ├── category_payments.rb │ ├── multiples.rb │ ├── whales.rb │ ├── payment.rb │ ├── category_contributions.rb │ ├── summary.rb │ ├── contribution.rb │ ├── late_contribution.rb │ ├── loan.rb │ ├── employer_contributions.rb │ ├── base.rb │ └── iec.rb ├── payment_codes.csv ├── downloaders │ ├── socrata_downloader.rb │ └── calaccess_downloader.rb ├── environment.rb ├── load_data.rb ├── schema.rb └── 2014_Lobbyist_Directory.csv ├── config.ru ├── assets ├── js │ ├── config.js │ ├── collections │ │ ├── candidates.js │ │ ├── payments.js │ │ └── contributions.js │ ├── models │ │ ├── payment.js │ │ ├── contribution.js │ │ └── candidate.js │ ├── views │ │ ├── rules.js │ │ ├── faq.js │ │ ├── _candidateTable.js │ │ ├── _multiplesView.js │ │ ├── employees.js │ │ ├── _categoryView.js │ │ ├── _topContributorsView.js │ │ ├── _search.js │ │ ├── _paymentsView.js │ │ ├── iec.js │ │ ├── contributor.js │ │ ├── home.js │ │ ├── committee.js │ │ ├── _paymentCategories.js │ │ ├── _contributorsView.js │ │ ├── about.js │ │ ├── _chartsWrapper.js │ │ ├── candidate.js │ │ └── _dailyContributionsChart.js │ ├── util.js │ ├── templates │ │ ├── _helpers.js │ │ ├── employees.hbs │ │ ├── contributor.hbs │ │ ├── rules.hbs │ │ ├── candidate.hbs │ │ └── about.hbs │ ├── application.js │ ├── vendor │ │ ├── accounting.min.js │ │ ├── topojson.v1.min.js │ │ └── GoogleChart.js │ └── app.js ├── images │ ├── Liu.jpg │ ├── Quan.jpg │ ├── Quan.png │ ├── Ruby.jpg │ ├── Ruby.png │ ├── Tuman.jpg │ ├── Tuman.png │ ├── Houston.jpg │ ├── Kaplan.jpg │ ├── Kaplan.png │ ├── Parker.jpg │ ├── Parker.png │ ├── Schaaf.jpg │ ├── Schaaf.png │ ├── Siegel.jpg │ ├── Siegel.png │ ├── Wilson.jpg │ ├── favicon.ico │ ├── Anderson.jpg │ ├── Karamooz.jpg │ ├── McCullough.jpg │ ├── McCullough.png │ ├── Sidebotham.jpg │ ├── Washington.jpg │ ├── Williams.jpg │ ├── OaklandCityTree.png │ ├── Peter-Liu_thumb.jpg │ ├── cfa_logo_footer.png │ ├── courtney_ruby.jpg │ ├── jean_quan_thumb.jpg │ ├── joe_tuman_thumb.jpg │ ├── openoaklandlogo.png │ ├── squairy_light.png │ ├── Dan_Siegel_thumb.jpg │ ├── Ken-Houston_thumb.jpg │ ├── eric-wilson_thumb.jpg │ ├── footer_logo_plus.png │ ├── squairy_light_@2X.png │ ├── OaklandCityTreeFull.png │ ├── Saied-Karamooz_thumb.jpg │ ├── bryan_parker_thumb.jpg │ ├── libby_schaaf_thumb.jpg │ ├── rebecca-kaplan_thumb.jpg │ ├── Charles-Williams_thumb.jpg │ ├── Nancy-Sidebotham_thumb.jpg │ ├── OpenOakland-Logo-white2.png │ ├── Patrick_McCullough_thumb.jpg │ ├── Sammuel_Washington_thumb.jpg │ ├── Jason-Shake-Anderson_thumb.jpg │ └── logo-Public-Ethics-Commission.png ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff └── css │ ├── application.css.scss │ ├── daily-contributions-chart.css │ ├── charts.css.scss │ └── _sass-enhance.css.scss ├── public ├── pie.jpg ├── robots.txt └── sitemap.xml.gz ├── netfile-etl ├── run_all.sh ├── merge_csvs_across_years.py ├── download_and_unzip_files.py ├── etl.py ├── LICENSE.md └── README.md ├── .gitignore ├── spec ├── factories │ ├── import.rb │ ├── party.rb │ └── contribution.rb ├── backend │ └── fetchers │ │ ├── loan_spec.rb │ │ ├── payment_spec.rb │ │ ├── late_contribution_spec.rb │ │ ├── iec_spec.rb │ │ ├── contribution_spec.rb │ │ └── summary_spec.rb ├── samples │ ├── socrata_summary.json │ ├── socrata_contribution_from_committee.json │ ├── socrata_late_contribution_from_committee.json │ ├── socrata_contribution_valid.json │ ├── socrata_payment.json │ ├── socrata_iec.json │ └── socrata_loan.json ├── app_spec.rb └── spec_helper.rb ├── bin ├── console └── test_amounts ├── .travis.yml ├── Gemfile ├── Rakefile ├── neo4j ├── import.cyp └── README.md ├── pie.html ├── Gemfile.lock ├── installBackEnd.sh ├── README_installation_in_vagrant.md ├── README.md └── Vagrantfile /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.2 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rackup -p $PORT -o 0.0.0.0 2 | -------------------------------------------------------------------------------- /backend/models/import.rb: -------------------------------------------------------------------------------- 1 | class Import < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /backend/models/summary.rb: -------------------------------------------------------------------------------- 1 | class Summary < ActiveRecord::Base 2 | 3 | end 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << '.' 2 | 3 | require 'app' 4 | run OpenDisclosureApp 5 | -------------------------------------------------------------------------------- /assets/js/config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | chartWidth: 700, 3 | chartHeight: 400 4 | }; 5 | -------------------------------------------------------------------------------- /backend/models/category_payment.rb: -------------------------------------------------------------------------------- 1 | class CategoryPayment < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /public/pie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/public/pie.jpg -------------------------------------------------------------------------------- /backend/models/category_contribution.rb: -------------------------------------------------------------------------------- 1 | class CategoryContribution < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /assets/images/Liu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Liu.jpg -------------------------------------------------------------------------------- /assets/images/Quan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Quan.jpg -------------------------------------------------------------------------------- /assets/images/Quan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Quan.png -------------------------------------------------------------------------------- /assets/images/Ruby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Ruby.jpg -------------------------------------------------------------------------------- /assets/images/Ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Ruby.png -------------------------------------------------------------------------------- /assets/images/Tuman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Tuman.jpg -------------------------------------------------------------------------------- /assets/images/Tuman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Tuman.png -------------------------------------------------------------------------------- /backend/models/employer_contribution.rb: -------------------------------------------------------------------------------- 1 | class EmployerContribution < ActiveRecord::Base 2 | 3 | end 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | 4 | Sitemap: http://www.opendisclosure.io/sitemap.xml 5 | -------------------------------------------------------------------------------- /public/sitemap.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/public/sitemap.xml.gz -------------------------------------------------------------------------------- /assets/images/Houston.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Houston.jpg -------------------------------------------------------------------------------- /assets/images/Kaplan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Kaplan.jpg -------------------------------------------------------------------------------- /assets/images/Kaplan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Kaplan.png -------------------------------------------------------------------------------- /assets/images/Parker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Parker.jpg -------------------------------------------------------------------------------- /assets/images/Parker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Parker.png -------------------------------------------------------------------------------- /assets/images/Schaaf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Schaaf.jpg -------------------------------------------------------------------------------- /assets/images/Schaaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Schaaf.png -------------------------------------------------------------------------------- /assets/images/Siegel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Siegel.jpg -------------------------------------------------------------------------------- /assets/images/Siegel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Siegel.png -------------------------------------------------------------------------------- /assets/images/Wilson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Wilson.jpg -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/favicon.ico -------------------------------------------------------------------------------- /assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/images/Anderson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Anderson.jpg -------------------------------------------------------------------------------- /assets/images/Karamooz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Karamooz.jpg -------------------------------------------------------------------------------- /assets/images/McCullough.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/McCullough.jpg -------------------------------------------------------------------------------- /assets/images/McCullough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/McCullough.png -------------------------------------------------------------------------------- /assets/images/Sidebotham.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Sidebotham.jpg -------------------------------------------------------------------------------- /assets/images/Washington.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Washington.jpg -------------------------------------------------------------------------------- /assets/images/Williams.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Williams.jpg -------------------------------------------------------------------------------- /netfile-etl/run_all.sh: -------------------------------------------------------------------------------- 1 | python download_and_unzip_files.py 2 | python etl.py 3 | python merge_csvs_across_years.py 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .bundle 3 | .env 4 | **/*.swp 5 | **/.DS_Store 6 | **/*.sqlite3 7 | .sass-cache 8 | public/assets 9 | -------------------------------------------------------------------------------- /backend/models/whale.rb: -------------------------------------------------------------------------------- 1 | class Whale < ActiveRecord::Base 2 | belongs_to :contributor, class_name: 'Party' 3 | end 4 | -------------------------------------------------------------------------------- /spec/factories/import.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory 'import' do 3 | import_time Time.now 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /assets/images/OaklandCityTree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/OaklandCityTree.png -------------------------------------------------------------------------------- /assets/images/Peter-Liu_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Peter-Liu_thumb.jpg -------------------------------------------------------------------------------- /assets/images/cfa_logo_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/cfa_logo_footer.png -------------------------------------------------------------------------------- /assets/images/courtney_ruby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/courtney_ruby.jpg -------------------------------------------------------------------------------- /assets/images/jean_quan_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/jean_quan_thumb.jpg -------------------------------------------------------------------------------- /assets/images/joe_tuman_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/joe_tuman_thumb.jpg -------------------------------------------------------------------------------- /assets/images/openoaklandlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/openoaklandlogo.png -------------------------------------------------------------------------------- /assets/images/squairy_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/squairy_light.png -------------------------------------------------------------------------------- /backend/models/multiple.rb: -------------------------------------------------------------------------------- 1 | class Multiple < ActiveRecord::Base 2 | belongs_to :contributor, class_name: 'Party' 3 | end 4 | -------------------------------------------------------------------------------- /assets/images/Dan_Siegel_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Dan_Siegel_thumb.jpg -------------------------------------------------------------------------------- /assets/images/Ken-Houston_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Ken-Houston_thumb.jpg -------------------------------------------------------------------------------- /assets/images/eric-wilson_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/eric-wilson_thumb.jpg -------------------------------------------------------------------------------- /assets/images/footer_logo_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/footer_logo_plus.png -------------------------------------------------------------------------------- /assets/images/squairy_light_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/squairy_light_@2X.png -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/images/OaklandCityTreeFull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/OaklandCityTreeFull.png -------------------------------------------------------------------------------- /assets/images/Saied-Karamooz_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Saied-Karamooz_thumb.jpg -------------------------------------------------------------------------------- /assets/images/bryan_parker_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/bryan_parker_thumb.jpg -------------------------------------------------------------------------------- /assets/images/libby_schaaf_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/libby_schaaf_thumb.jpg -------------------------------------------------------------------------------- /assets/images/rebecca-kaplan_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/rebecca-kaplan_thumb.jpg -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH << '.' 4 | require 'backend/environment.rb' 5 | require 'pry' 6 | 7 | binding.pry 8 | -------------------------------------------------------------------------------- /assets/images/Charles-Williams_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Charles-Williams_thumb.jpg -------------------------------------------------------------------------------- /assets/images/Nancy-Sidebotham_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Nancy-Sidebotham_thumb.jpg -------------------------------------------------------------------------------- /assets/js/collections/candidates.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Candidates = Backbone.Collection.extend({ 2 | model: OpenDisclosure.Candidate 3 | }); 4 | -------------------------------------------------------------------------------- /assets/images/OpenOakland-Logo-white2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/OpenOakland-Logo-white2.png -------------------------------------------------------------------------------- /assets/images/Patrick_McCullough_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Patrick_McCullough_thumb.jpg -------------------------------------------------------------------------------- /assets/images/Sammuel_Washington_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Sammuel_Washington_thumb.jpg -------------------------------------------------------------------------------- /assets/images/Jason-Shake-Anderson_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/Jason-Shake-Anderson_thumb.jpg -------------------------------------------------------------------------------- /assets/images/logo-Public-Ethics-Commission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openoakland/opendisclosure/HEAD/assets/images/logo-Public-Ethics-Commission.png -------------------------------------------------------------------------------- /assets/js/models/payment.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Payment = Backbone.Model.extend({ 2 | }); 3 | OpenDisclosure.CategoryPayment = Backbone.Model.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /backend/models/payment.rb: -------------------------------------------------------------------------------- 1 | class Payment < ActiveRecord::Base 2 | belongs_to :payer, class_name: 'Party' 3 | belongs_to :recipient, class_name: 'Party' 4 | end 5 | -------------------------------------------------------------------------------- /assets/css/application.css.scss: -------------------------------------------------------------------------------- 1 | //= require _sass-enhance 2 | //= require font-awesome.min 3 | //= require bootstrap.min 4 | //= require main 5 | //= require charts 6 | //= require daily-contributions-chart 7 | -------------------------------------------------------------------------------- /spec/factories/party.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory 'party/individual' do 3 | name 'Foo Party' 4 | end 5 | 6 | factory 'party/committee' do 7 | name 'Foo Party' 8 | committee_id 1234567 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without production development 3 | env: 4 | - "DATABASE_URL=postgres://localhost/travis_ci_test" 5 | before_script: 6 | - psql -c 'create database travis_ci_test;' -U postgres 7 | script: bundle exec rspec 8 | -------------------------------------------------------------------------------- /spec/factories/contribution.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory 'contribution' do 3 | sequence(:transaction_id) 4 | amount 100 5 | association :contributor, factory: 'party/individual' 6 | association :recipient, factory: 'party/committee' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /assets/js/views/rules.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Rules = Backbone.View.extend({ 2 | template: HandlebarsTemplates['rules'], 3 | 4 | initialize: function(){ 5 | this.render(); 6 | }, 7 | 8 | render: function() { 9 | this.$el.html(this.template()); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /backend/models/iec.rb: -------------------------------------------------------------------------------- 1 | class IEC < ActiveRecord::Base 2 | self.inheritance_column = :_disabled 3 | 4 | belongs_to :contributor, class_name: 'Party', counter_cache: :contributions_count 5 | belongs_to :recipient, class_name: 'Party', counter_cache: :received_contributions_count 6 | 7 | end 8 | -------------------------------------------------------------------------------- /assets/js/views/faq.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Faq = Backbone.View.extend({ 2 | 3 | template: HandlebarsTemplates['faq'], 4 | 5 | initialize: function(options) { 6 | this.render(); 7 | }, 8 | 9 | render: function() { 10 | this.$el.html(this.template()); 11 | } 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /backend/models/payment_codes.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | class PaymentCodes < ActiveRecord::Base 4 | 5 | def self.load_from_file(csv) 6 | CSV.parse(open(csv).read) do |row| 7 | PaymentCodes.create( 8 | code: row[0], 9 | text: row[1] 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /backend/models/lobbyist.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | class Lobbyist < ActiveRecord::Base 4 | 5 | def self.load_from_file(csv) 6 | CSV.parse(open(csv).read, headers: :first_row) do |row| 7 | Lobbyist.create( 8 | id: row['Lobbyist ID'], 9 | name: row['Lobbyist'], 10 | firm: row['Lobbyist_Firm'], 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/backend/fetchers/loan_spec.rb: -------------------------------------------------------------------------------- 1 | describe DataFetcher::Loan do 2 | subject { described_class.new([row]).run! } 3 | 4 | context 'with a normal row' do 5 | let(:row) { JSON.load(File.read('spec/samples/socrata_loan.json')) } 6 | 7 | it 'creates a Loan record' do 8 | expect { subject }.to change { ::Contribution.count }.by(1) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/backend/fetchers/payment_spec.rb: -------------------------------------------------------------------------------- 1 | describe DataFetcher::Payment do 2 | subject { described_class.new([row]).run! } 3 | 4 | describe 'with a valid payment row' do 5 | let(:row) { JSON.load(File.read('spec/samples/socrata_payment.json')) } 6 | 7 | it 'loads a payment' do 8 | expect { subject }.to change { ::Payment.count }.by(1) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /netfile-etl/merge_csvs_across_years.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | csv_files_for_2011 = glob.glob("*2011*.csv") 5 | 6 | for filename in csv_files_for_2011: 7 | filename_wildcard_expression = filename.replace("2011","*") 8 | output_filename = filename.replace("2011","") 9 | os.system("awk 'FNR==1 && NR!=1{next;}{print}' " + filename_wildcard_expression + "> " + output_filename) 10 | 11 | -------------------------------------------------------------------------------- /assets/js/util.js: -------------------------------------------------------------------------------- 1 | var OpenDisclosure = window.OpenDisclosure || {}; 2 | 3 | OpenDisclosure.friendlyMoney = function(number) { 4 | return accounting.formatMoney(number, '$', 0); 5 | }; 6 | 7 | OpenDisclosure.friendlyPct = function(float) { 8 | var result = Math.round(float * 10000) / 100; 9 | result = result || 0; //Turns NaN into 0 for display purposes 10 | result += "%"; 11 | return result; 12 | }; 13 | -------------------------------------------------------------------------------- /assets/js/templates/_helpers.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper('friendlyMoney', function(amount) { 2 | return accounting.formatMoney(amount, '$', 0); 3 | }); 4 | 5 | Handlebars.registerHelper('list', function(items) { 6 | var out = ""; 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /assets/js/collections/payments.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Payments = Backbone.Collection.extend({ 2 | url: function() { 3 | return '/api/payments/' + this.options.candidateId; 4 | }, 5 | model: OpenDisclosure.Payment, 6 | initialize: function(models, options) { 7 | this.options = options; 8 | } 9 | }); 10 | OpenDisclosure.CategoryPayments = Backbone.Collection.extend({ 11 | url: '/api/category_payments', 12 | model: OpenDisclosure.CategoryPayment 13 | }); 14 | -------------------------------------------------------------------------------- /assets/js/templates/employees.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{contribution.attributes.contributor.name}} 5 | {{contribution.attributes.recipient.name}} 6 | {{contribution.friendlyAmount}} 7 | {{contribution.friendlyDate}} 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /backend/models/map.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | class Map < ActiveRecord::Base 4 | self.inheritance_column = 'something_that_isnt_the_word_type' 5 | 6 | def self.load_mappings(csv) 7 | transaction do 8 | CSV.parse(open(csv).read, headers: :first_row) do |row| 9 | Map.create( 10 | id: row['id'], 11 | emp1: row['emp1'], 12 | emp2: row['emp2'], 13 | type: row['type'], 14 | ) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/backend/fetchers/late_contribution_spec.rb: -------------------------------------------------------------------------------- 1 | describe DataFetcher::LateContribution do 2 | describe '.parse_row' do 3 | subject { described_class.new([row]).run! } 4 | 5 | context 'with a valid person-to-committee contribution' do 6 | let(:row) { JSON.load(File.read('spec/samples/socrata_late_contribution_from_committee.json')) } 7 | 8 | it 'parses' do 9 | expect { subject }.to change { Contribution.count }.by(1) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/samples/socrata_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "rpt_date": "2014-06-22T00:00:00", 3 | "committee_type": "CTL", 4 | "filer_id": "1367207", 5 | "rec_type": "SMRY", 6 | "form_type": "F460", 7 | "thru_date": "2014-06-30T00:00:00", 8 | "line_item": "1", 9 | "from_date": "2014-01-01T00:00:00", 10 | "filer_naml": "Charles R. Williams for Mayor of Oakland 2014", 11 | "amount_a": "10635", 12 | "report_num": "2", 13 | "amount_b": "10635", 14 | "elect_date": "2014-11-04T00:00:00" 15 | } 16 | -------------------------------------------------------------------------------- /backend/fetchers/category_payments.rb: -------------------------------------------------------------------------------- 1 | class DataFetcher 2 | class CategoryPayments 3 | def self.run! 4 | ActiveRecord::Base.connection.execute <<-QUERY 5 | INSERT INTO category_payments (payer_id, text, code, amount) 6 | SELECT payer_id, 7 | COALESCE(text, 'Not stated'), p.code, sum(amount) 8 | FROM (payments p left outer join payment_codes c 9 | on p.code = c.code) 10 | GROUP BY payer_id, text, p.code 11 | ORDER BY payer_id, text 12 | QUERY 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.1.2' 3 | 4 | gem 'activerecord' 5 | gem 'dotenv' 6 | gem 'haml' 7 | gem 'pg' 8 | gem 'rake' 9 | gem 'sinatra' 10 | gem 'sinatra-asset-pipeline' 11 | gem 'sinatra-contrib' 12 | gem 'handlebars_assets' 13 | gem 'therubyracer' 14 | 15 | group :development do 16 | gem 'pry' 17 | gem 'rb-readline' 18 | gem 'foreman' 19 | end 20 | 21 | group :test do 22 | gem 'rspec' 23 | gem 'factory_girl' 24 | end 25 | 26 | group :production do 27 | gem 'sitemap_generator' 28 | end 29 | -------------------------------------------------------------------------------- /netfile-etl/download_and_unzip_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | 4 | current_year = datetime.datetime.now().year 5 | years_with_data = range(2011, current_year + 1) 6 | remote_path = "https://ssl.netfile.com/pub2/excel/COAKBrowsable/" 7 | 8 | for year in years_with_data: 9 | print "Downloading " + str(year) + " data..." 10 | filename_for_year = "efile_newest_COAK_" + str(year) + ".zip" 11 | os.system("curl -f -L -O " + remote_path + filename_for_year) 12 | os.system("unzip " + filename_for_year) 13 | os.system("rm " + filename_for_year) 14 | -------------------------------------------------------------------------------- /backend/fetchers/multiples.rb: -------------------------------------------------------------------------------- 1 | class DataFetcher 2 | class Multiples 3 | def self.run! 4 | ActiveRecord::Base.connection.execute <<-QUERY 5 | INSERT into multiples(contributor_id, number) 6 | SELECT contributor_id, count(distinct recipient_id) 7 | FROM contributions, parties r 8 | WHERE r.id = recipient_id AND 9 | r.committee_id in (#{Party::MAYORAL_CANDIDATE_IDS.join ','}) 10 | GROUP BY contributor_id 11 | HAVING count(distinct recipient_id) > 1 12 | ORDER BY count(distinct recipient_id) desc; 13 | QUERY 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /assets/js/templates/contributor.hbs: -------------------------------------------------------------------------------- 1 | {{#each contributors}} 2 |
3 |

{{name}}

4 |
5 | 6 |
7 | {{#each contributions}} 8 |
9 |
10 | 11 | {{recipientName}} 12 | {{amount}}{{type}} 13 | {{date}} 14 | 15 |
16 |
17 | {{/each}} 18 | 19 | {{/each}} 20 | -------------------------------------------------------------------------------- /spec/samples/socrata_contribution_from_committee.json: -------------------------------------------------------------------------------- 1 | { 2 | "thru_date": "2012-09-30T00:00:00", 3 | "entity_cd": "COM", 4 | "intr_self": "n", 5 | "rpt_date": "2012-09-30T00:00:00", 6 | "elect_date": "2012-11-06T00:00:00", 7 | "form_type": "A", 8 | "rec_type": "RCPT", 9 | "cmte_id": "1296948", 10 | "tran_id": "C5565486", 11 | "filer_naml": "Friends of Nyeisha Dewitt for City Council 2012", 12 | "committee_type": "CTL", 13 | "tran_amt2": "1300", 14 | "tran_amt1": "1300", 15 | "tran_self": "n", 16 | "from_date": "2012-07-01T00:00:00", 17 | "report_num": "0", 18 | "tran_date": "2012-09-27T00:00:00" 19 | } 20 | -------------------------------------------------------------------------------- /netfile-etl/etl.py: -------------------------------------------------------------------------------- 1 | import xlrd 2 | import os 3 | import glob 4 | 5 | xlsx_files_in_current_dir = glob.glob("*.xlsx") 6 | 7 | for excel_filename in xlsx_files_in_current_dir: 8 | print "Opening " + excel_filename + "..." 9 | workbook = xlrd.open_workbook("./" + excel_filename) 10 | filename_without_extension = excel_filename.split(".")[0] 11 | sheet_names = workbook.sheet_names() 12 | for sheet_name in sheet_names: 13 | csv_filename = filename_without_extension + "_" +sheet_name + ".csv" 14 | print "Writing " + csv_filename 15 | os.system("in2csv " + excel_filename + " --sheet " + "\"" + sheet_name + "\"" + " > " + csv_filename) 16 | -------------------------------------------------------------------------------- /backend/fetchers/whales.rb: -------------------------------------------------------------------------------- 1 | class DataFetcher 2 | class Whales 3 | def self.run! 4 | ActiveRecord::Base.connection.execute <<-QUERY 5 | INSERT into whales(contributor_id, amount) 6 | SELECT contributor_id, sum(amount) 7 | FROM contributions, parties 8 | WHERE amount IS NOT NULL AND recipient_id = parties.id AND committee_id <> 0 AND 9 | ( committee_id in (#{Party::MAYORAL_CANDIDATE_IDS.join ','}) OR 10 | committee_id in (#{Party::CANDIDATE_IDS.join ','})) 11 | GROUP BY contributor_id 12 | ORDER BY sum(amount) desc 13 | LIMIT 20; 14 | QUERY 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /assets/js/views/_candidateTable.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.CandidateTable = Backbone.View.extend({ 2 | template: _.template($('#mayoral-table-template').html()), 3 | 4 | initialize : function() { 5 | if (this.collection.length > 0) { 6 | this.render(); 7 | } 8 | this.listenTo(this.collection, 'sync', this.render); 9 | }, 10 | 11 | render : function() { 12 | var candidates = _.partition(this.collection.models, function(m) { 13 | return m.attributes.summary; 14 | }); 15 | 16 | this.$el.html(this.template({ 17 | candidatesWithData : candidates[0], 18 | candidatesWithoutData : candidates[1] 19 | })); 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'sinatra/asset_pipeline/task' 2 | load 'backend/environment.rb' 3 | require './app' 4 | 5 | namespace :sitemap do 6 | task :generate do 7 | require 'sitemap_generator' 8 | puts 'Be sure you have the latest data and have the latest copy of the code!' 9 | 10 | SitemapGenerator::Sitemap.default_host = 'http://www.opendisclosure.io' 11 | SitemapGenerator::Sitemap.create do 12 | add '/', changefreq: 'daily', priority: 1 13 | add '/rules', changefreq: 'monthly' 14 | 15 | Party::CANDIDATE_INFO.each do |_id, c| 16 | add Party.new(c).link_path, priority: 0.8 17 | end 18 | end 19 | end 20 | end 21 | 22 | Sinatra::AssetPipeline::Task.define! OpenDisclosureApp 23 | -------------------------------------------------------------------------------- /spec/samples/socrata_late_contribution_from_committee.json: -------------------------------------------------------------------------------- 1 | { 2 | "tran_id": "INC3", 3 | "ctrib_emp": "DAVITA", 4 | "rec_type": "S497", 5 | "filer_id": "1361867", 6 | "ctrib_date": "2014-03-18T00:00:00", 7 | "enty_naml": "PARKER", 8 | "rpt_id_num": "P14-JPP-03", 9 | "amount": "20000", 10 | "filer_naml": "BRYAN PARKER FOR A SAFER AND MORE EQUITABLE OAKLAND BALLOT MEASURE COMMITTEE SUPPORTING MEASURE AA", 11 | "entity_cd": "IND", 12 | "enty_st": "CA", 13 | "rpt_date": "2014-03-20T00:00:00", 14 | "committee_type": "RCP", 15 | "enty_zip4": "94611", 16 | "enty_city": "OAKLAND", 17 | "ctrib_occ": "VICE PRESIDENT", 18 | "enty_namf": "BRYAN", 19 | "report_num": "0", 20 | "form_type": "F497P1" 21 | } 22 | -------------------------------------------------------------------------------- /spec/backend/fetchers/iec_spec.rb: -------------------------------------------------------------------------------- 1 | describe DataFetcher::IEC do 2 | subject { DataFetcher::IEC.new([row]) } 3 | 4 | context 'with a valid row' do 5 | let(:row) { JSON.load(File.read('spec/samples/socrata_iec.json')) } 6 | 7 | it 'does nothing without a recipient already' do 8 | expect { subject.run! }.to change { ::Contribution.count }.by(0) 9 | end 10 | 11 | context 'when a recipient is found' do 12 | before do 13 | allow(subject).to receive(:get_recipient).and_return(Party::Committee.create!(name: 'testing')) 14 | end 15 | 16 | it 'creates a contribution' do 17 | expect { subject.run! }.to change { ::Contribution.count }.by(1) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/samples/socrata_contribution_valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "thru_date" : "2011-12-31T00:00:00", 3 | "entity_cd" : "IND", 4 | "intr_self" : "n", 5 | "tran_zip4" : "94710", 6 | "rpt_date" : "2012-01-31T00:00:00", 7 | "tran_state" : "CA", 8 | "form_type" : "A", 9 | "rec_type" : "RCPT", 10 | "tran_id" : "A6", 11 | "filer_naml" : "Derrick H Muhammad for City Council 2012", 12 | "filer_id" : "1342709", 13 | "committee_type" : "RCP", 14 | "tran_occ" : "Longshoreman", 15 | "tran_amt2" : "100", 16 | "tran_amt1" : "100", 17 | "tran_nams" : "Jr", 18 | "tran_city" : "Berkeley", 19 | "tran_namf" : "James", 20 | "tran_emp" : "Pacific Maritime Assn/ILWU", 21 | "tran_self" : "n", 22 | "from_date" : "2011-01-01T00:00:00", 23 | "tran_naml" : "Curtis", 24 | "report_num" : "0", 25 | "tran_date" : "2011-12-07T00:00:00" 26 | } 27 | -------------------------------------------------------------------------------- /bin/test_amounts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load 'backend/environment.rb' 3 | 4 | Party.mayoral_candidates.each do |c| 5 | puts c.short_name 6 | contributions = c.summary.total_contributions_received 7 | expenditures = c.summary.total_expenditures_made 8 | misc = c.summary.total_misc_increases_to_cash 9 | unpaid = c.summary.total_unpaid_bills 10 | actual = c.summary.ending_cash_balance 11 | 12 | total = contributions - expenditures + misc + unpaid 13 | 14 | puts "contributions: $#{contributions}" 15 | puts "expenditures: - $#{expenditures}" 16 | puts "misc: + $#{misc}" 17 | puts "unpaid: + $#{unpaid}" 18 | puts " ====================" 19 | puts " $#{total}" 20 | puts "actual COH: - $#{actual}" 21 | puts " ====================" 22 | puts "difference: $#{total - actual}" 23 | puts '' 24 | puts '' 25 | end 26 | -------------------------------------------------------------------------------- /spec/samples/socrata_payment.json: -------------------------------------------------------------------------------- 1 | { 2 | "payee_city": "Oakland", 3 | "payee_naml": "Reynolds", 4 | "report_num": "0", 5 | "payee_namf": "Jessica", 6 | "from_date": "2013-09-30T00:00:00", 7 | "cum_ytd": "0", 8 | "expn_dscr": "fact sheets", 9 | "committee_type": "BMC", 10 | "filer_id": "1310647", 11 | "bal_juris": "City of Oakland", 12 | "filer_naml": "The Oakland Fund for Keep Oakland Firesafe 2013", 13 | "sup_opp_cd": "S", 14 | "expn_date": "2013-10-21T00:00:00", 15 | "rpt_date": "2013-10-31T00:00:00", 16 | "amount": "256.5", 17 | "entity_cd": "IND", 18 | "payee_zip4": "94610", 19 | "expn_chkno": "1078", 20 | "thru_date": "2013-10-27T00:00:00", 21 | "expn_code": "LIT", 22 | "form_type": "E", 23 | "bal_name": "Wildfire Prevention Assessment District", 24 | "rec_type": "EXPN", 25 | "tran_id": "E100", 26 | "cand_naml": "na", 27 | "payee_state": "CA", 28 | "cand_namf": "na" 29 | } 30 | -------------------------------------------------------------------------------- /spec/samples/socrata_iec.json: -------------------------------------------------------------------------------- 1 | { 2 | "payee_city": "Oakland", 3 | "payee_naml": "US Postmaster", 4 | "report_num": "0", 5 | "from_date": "2012-01-01T00:00:00", 6 | "cum_ytd": "12007.75", 7 | "expn_dscr": "Postage for a mailer in support of Amy Lemley for Oakland City Council, Dist. 1", 8 | "committee_type": "RCP", 9 | "filer_id": "983545", 10 | "filer_naml": "OAKPAC, OAKLAND METROPOLITAN CHAMBER OF COMMERCE", 11 | "office_cd": "CCM", 12 | "dist_no": "1", 13 | "sup_opp_cd": "S", 14 | "expn_date": "2012-10-19T00:00:00", 15 | "rpt_date": "2012-10-23T00:00:00", 16 | "juris_cd": "OTH", 17 | "amount": "6826.31", 18 | "entity_cd": "OTH", 19 | "payee_zip4": "94612", 20 | "thru_date": "2012-10-20T00:00:00", 21 | "juris_dscr": "Oakland, CA", 22 | "elect_date": "2012-11-06T00:00:00", 23 | "expn_code": "IND", 24 | "form_type": "F465P3", 25 | "rec_type": "EXPN", 26 | "tran_id": "PDT19", 27 | "cand_naml": "Amy Lemley", 28 | "payee_state": "CA" 29 | } 30 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../app.rb' 2 | 3 | describe OpenDisclosureApp do 4 | let(:app) { described_class } 5 | 6 | describe '/' do 7 | subject { get '/' } 8 | 9 | # Just add a smoke test for now. Later, we can add some more meat to this 10 | # test suite. 11 | it { should be_ok } 12 | end 13 | 14 | describe '/api/contributor/:id' do 15 | let(:contribution) { create(:contribution) } 16 | 17 | def response; JSON.parse(subject.body); end 18 | 19 | subject { get "/api/contributor/#{contribution.contributor_id}" } 20 | 21 | it 'returns the contributions by that party' do 22 | expect(response.length).to eq(1) 23 | end 24 | 25 | context 'with another contribution to a different Party' do 26 | let!(:other_contribution) { create(:contribution) } 27 | 28 | it 'does not return contributions by the other party' do 29 | expect(response.map { |c| c["id"] }).not_to include(other_contribution.id) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/samples/socrata_loan.json: -------------------------------------------------------------------------------- 1 | { 2 | "loan_occ": "Consultant", 3 | "lndr_namf": "Brenda", 4 | "report_num": "0", 5 | "loan_rate": "0", 6 | "from_date": "2014-10-01T00:00:00", 7 | "loan_date2": "2015-01-31T00:00:00", 8 | "loan_date1": "2014-10-06T00:00:00", 9 | "loan_self": "n", 10 | "loan_zip4": "94618", 11 | "committee_type": "CTL", 12 | "filer_id": "136810", 13 | "lndr_naml": "Roberts", 14 | "filer_naml": "Brenda Roberts for Oakland City Auditor 2014", 15 | "loan_emp": "Accretive Solutions", 16 | "tran_id": "pWa8KIWYRbah", 17 | "loan_amt3": "51000", 18 | "loan_amt4": "0", 19 | "loan_amt1": "22000", 20 | "loan_amt2": "22000", 21 | "rpt_date": "2014-10-20T00:00:00", 22 | "loan_st": "CA", 23 | "entity_cd": "IND", 24 | "thru_date": "2014-10-18T00:00:00", 25 | "loan_amt6": "0", 26 | "loan_amt5": "0", 27 | "loan_amt8": "22000", 28 | "loan_city": "Oakland", 29 | "loan_amt7": "0", 30 | "elect_date": "2014-11-04T00:00:00", 31 | "form_type": "B1", 32 | "rec_type": "LOAN" 33 | } 34 | -------------------------------------------------------------------------------- /backend/payment_codes.csv: -------------------------------------------------------------------------------- 1 | CMP,campaign paraphernalia/misc. 2 | CNS,campaign consultants 3 | CTB,contribution 4 | CVC,civic donations 5 | FIL,candidate filing/ballot fees 6 | FND,fundraising events 7 | IND,independent expenditure supporting/opposing others 8 | LEG,legal defense 9 | LIT,campaign literature and mailings 10 | MBR,member communications 11 | MTG,meetings and appearances 12 | OFC,office expenses 13 | PET,petition circulating 14 | PHO,phone banks 15 | POL,polling and survey research 16 | POS,"postage, delivery and messenger services" 17 | PRO,"professional services (legal, accounting)" 18 | PRT,print ads 19 | RAD,radio airtime and production costs 20 | RFD,returned contributions 21 | SAL,campaign workers' salaries 22 | TEL,t.v. or cable airtime and production costs 23 | TRC,"candidate travel, lodging, and meals" 24 | TRS,"staff/spouse travel, lodging, and meals" 25 | TSF,transfer between committees of the same candidate/sponsor 26 | VOT,voter registration 27 | WEB,"information technology costs (internet, e-mail)" 28 | ,Not Stated 29 | -------------------------------------------------------------------------------- /backend/downloaders/socrata_downloader.rb: -------------------------------------------------------------------------------- 1 | # Utility class to interact with Socrata and download the rows from a 2 | # spreadsheet. 3 | # 4 | # Usage: 5 | # url = 'http://data.oaklandnet.com/resource/3xq4-ermg.json' 6 | # SocrataDownloader.new(url).each do |record| 7 | # puts record # { "key" => "value" } 8 | # end 9 | # 10 | require 'open-uri' 11 | 12 | class SocrataDownloader 13 | include Enumerable 14 | 15 | def initialize(uri) 16 | @uri = URI(uri) 17 | end 18 | 19 | def each(&block) 20 | more = true 21 | offset = 0 22 | while more 23 | url = @uri 24 | url.query = URI.encode_www_form( 25 | '$limit' => 1000, 26 | '$order' => 'thru_date ASC', 27 | '$offset' => offset 28 | ) 29 | 30 | puts ' Downloading: ' + url.to_s 31 | 32 | response = JSON.parse(open(url.to_s).read) 33 | 34 | response.each do |row| 35 | next if row['rpt_date'] > '2014-11-30T00:00:00' 36 | block.call(row) 37 | end 38 | 39 | # preparation for next loop! 40 | more = response.length > 0 41 | offset = offset + 1000 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /backend/downloaders/calaccess_downloader.rb: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # THIS DOES NOT WORK 3 | ################################################################################ 4 | # 5 | # I'm checking it in as a starting point... the problem is that Ruby Zip library 6 | # can't stream input so I'll probably have to go back-to-the-drawing-board and 7 | # figure out a better ETL process for this. 8 | # 9 | require 'net/http' 10 | # require 'zip' 11 | 12 | class CalaccessDownloader 13 | ARCHIVE_URL = URI('http://campaignfinance.cdn.sos.ca.gov/dbwebexport.zip') 14 | 15 | def initialize 16 | @conn = Net::HTTP.start(ARCHIVE_URL.host, ARCHIVE_URL.port) 17 | 18 | IO.pipe do |read_io, write_io| 19 | Thread.new do 20 | @conn.request Net::HTTP::Get.new(ARCHIVE_URL.request_uri) do |request| 21 | request.read_body { puts chunk.length; write_io << chunk } 22 | end 23 | end 24 | 25 | # Zip::File.open_buffer(read_io) do |zipfile| 26 | # zipfile.each do |entry| 27 | # puts entry.name 28 | # end 29 | # end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /assets/js/views/_multiplesView.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.MultiplesView = Backbone.View.extend({ 2 | 3 | template: _.template('\ 4 |
\ 5 | <%= contributor.attributes.name %>\ 6 | <%= number %> candidates \ 7 |
'), 8 | 9 | initialize: function(options) { 10 | this.options = options; 11 | 12 | _.bindAll(this, 'renderContributor'); 13 | 14 | this.listenTo(this.collection, 'sync', this.render); 15 | 16 | if (this.collection.length > 0) { 17 | this.render(); 18 | } 19 | }, 20 | 21 | render: function() { 22 | this.$el.empty(); 23 | $('

Contributors Who Gave to More Than One Mayoral Candidate

').appendTo(this.$el); 24 | 25 | this.$el.append(this.collection.map(this.renderContributor).join(' ')); 26 | }, 27 | 28 | renderContributor: function(c) { 29 | var contributor = new OpenDisclosure.Contributor(c.attributes.contributor); 30 | 31 | return this.template({ 32 | number: c.attributes.number, 33 | contributor: contributor 34 | }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /assets/js/application.js: -------------------------------------------------------------------------------- 1 | //= require vendor/topojson.v1.min 2 | //= require vendor/bootstrap.min 3 | //= require vendor/modernizr-2.6.2-respond-1.1.0.min 4 | //= require vendor/accounting.min 5 | //= require vendor/GoogleChart 6 | //= require vendor/moment 7 | //= require handlebars 8 | 9 | //= require config 10 | 11 | //= require models/candidate 12 | //= require models/contribution 13 | //= require models/payment 14 | 15 | //= require collections/candidates 16 | //= require collections/contributions 17 | //= require collections/payments 18 | 19 | //= require_tree ./templates/ 20 | 21 | //= require views/about 22 | //= require views/candidate 23 | //= require views/committee 24 | //= require views/faq 25 | //= require views/home 26 | //= require views/rules 27 | //= require views/contributor 28 | //= require views/employees 29 | //= require views/iec 30 | //= require views/_candidateTable 31 | //= require views/_chartsWrapper 32 | //= require views/_zipcodeChartView 33 | //= require views/_dailyContributionsChart 34 | //= require views/_contributorsView 35 | //= require views/_topContributorsView 36 | //= require views/_categoryView 37 | //= require views/_paymentCategories 38 | //= require views/_paymentsView 39 | //= require views/_multiplesView 40 | //= require views/_search 41 | 42 | //= require util 43 | //= require app 44 | -------------------------------------------------------------------------------- /assets/js/views/employees.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Employees = Backbone.View.extend({ 2 | 3 | template: HandlebarsTemplates['employees'], 4 | 5 | initialize: function(options) { 6 | _.bindAll(this, 'render', 'renderContribution'); 7 | 8 | this.options = options; 9 | this.collection = new OpenDisclosure.Employees([], { employer_id: this.options.employer_id } ); 10 | this.collection.fetch({ success: this.render }); 11 | 12 | }, 13 | 14 | render: function() { 15 | this.$el.html('\ 16 |

' + this.options.headline + '

\ 17 |
'); 18 | 19 | new OpenDisclosure.Search({ 20 | el : "#search" 21 | }); 22 | 23 | if (this.collection.length > 0) { 24 | this.$('.data').html(this.collection.map(this.renderContribution)); 25 | } else { 26 | this.$('.data').empty(); 27 | } 28 | 29 | }, 30 | 31 | renderContribution: function(c) { 32 | var contribution = new OpenDisclosure.Contribution(c.attributes); 33 | 34 | contribution.friendlyAmount = OpenDisclosure.friendlyMoney(contribution.attributes.amount); 35 | 36 | contribution.friendlyDate = moment(contribution.attributes.date).format("MMM-DD-YY"); 37 | 38 | contribution.calculatedLink = contribution.recipientLinkPath(); 39 | 40 | return this.template({ contribution: contribution }); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, make a 10 | # separate helper file that requires this one and then use it only in the specs 11 | # that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | require 'rack/test' 18 | require 'factory_girl' 19 | require_relative '../backend/environment.rb' 20 | 21 | ENV['RACK_ENV'] = 'test' 22 | 23 | FactoryGirl.find_definitions 24 | 25 | RSpec.configure do |config| 26 | config.include FactoryGirl::Syntax::Methods 27 | config.include Rack::Test::Methods 28 | 29 | config.before(:suite) do 30 | ActiveRecord::Migration.verbose = false 31 | 32 | require_relative '../backend/schema.rb' 33 | 34 | FactoryGirl.create(:import) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /backend/environment.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << '.' 2 | 3 | require 'active_record' 4 | require 'dotenv' 5 | require 'pg' 6 | 7 | Dotenv.load 8 | 9 | ENV['DATABASE_URL'] ||= "postgres://localhost/postgres" 10 | 11 | pg_stopped = `ps | grep postgres | grep -v grep`.empty? 12 | pg_stopped_message = <<-INFO 13 | PostgreSQL appears to not be running... If you installed it with homebrew you 14 | can start it with: 15 | 16 | postgres -D /usr/local/var/postgres 17 | INFO 18 | 19 | begin 20 | ActiveRecord::Base.establish_connection ENV['DATABASE_URL'] 21 | ActiveRecord::Base.connection.verify! 22 | rescue PG::ConnectionBad => ex 23 | raise ex unless pg_stopped 24 | puts pg_stopped_message 25 | 26 | exit 1 27 | rescue ActiveRecord::AdapterNotSpecified 28 | url = "postgres://#{ENV['USER']}:[your password]@localhost/postgres" 29 | default_url = "postgres://#{ENV['USER']}@localhost/postgres" 30 | 31 | begin 32 | conn = ActiveRecord::Base.establish_connection default_url 33 | conn.connection 34 | url = default_url 35 | rescue PG::ConnectionBad => ex 36 | raise ex unless pg_stopped 37 | puts pg_stopped_message 38 | 39 | exit 1 40 | end 41 | 42 | puts "You need to run this command:" 43 | puts "echo DATABASE_URL=\"#{url}\" > .env" 44 | 45 | exit 1 46 | end 47 | 48 | Dir[File.dirname(__FILE__) + '/models/*.rb'].each { |f| require f } 49 | Dir[File.dirname(__FILE__) + '/fetchers/*.rb'].each { |f| require f } 50 | Dir[File.dirname(__FILE__) + '/downloaders/*.rb'].each { |f| require f } 51 | -------------------------------------------------------------------------------- /netfile-etl/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Dave Guarino 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /backend/models/contribution.rb: -------------------------------------------------------------------------------- 1 | class Contribution < ActiveRecord::Base 2 | self.inheritance_column = :_disabled 3 | 4 | belongs_to :contributor, class_name: 'Party', counter_cache: :contributions_count 5 | belongs_to :recipient, class_name: 'Party', counter_cache: :received_contributions_count 6 | 7 | before_save :set_self_contribution 8 | 9 | after_create :increment_oakland_contribution_count 10 | after_create :increment_small_contribution_ammount 11 | after_create :increment_self_contributions_total 12 | 13 | enum type: [:contribution, :loan, :inkind, :independent] 14 | 15 | def increment_oakland_contribution_count 16 | return unless contributor.from_oakland? 17 | recipient.received_contributions_from_oakland += amount 18 | recipient.save 19 | end 20 | 21 | def set_self_contribution 22 | self.self_contribution = contributor.name == recipient.short_name 23 | true 24 | end 25 | 26 | def increment_self_contributions_total 27 | # This is a bit tricky because the name of the person doesn't match the name 28 | # of the campaign, so there will be two Party instances with different IDs. 29 | # So I suppose we're stuck just string-comparing the names. 30 | return unless self_contribution 31 | recipient.self_contributions_total += amount 32 | recipient.save 33 | end 34 | 35 | def increment_small_contribution_ammount 36 | if amount.to_f < 100 && amount.to_f > -100 37 | recipient.small_contributions += amount.to_f 38 | recipient.save 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/backend/fetchers/contribution_spec.rb: -------------------------------------------------------------------------------- 1 | describe DataFetcher::Contribution do 2 | before do 3 | ::Contribution.delete_all 4 | ::Party.delete_all 5 | end 6 | 7 | describe '.parse_row' do 8 | subject { described_class.new([row]).run! } 9 | 10 | context 'with a valid committe-to-committee contribution' do 11 | let(:row) { JSON.load(File.read('spec/samples/socrata_contribution_from_committee.json')) } 12 | 13 | it 'creates a Contribution record' do 14 | expect { subject }.to change { Contribution.count }.by(1) 15 | end 16 | end 17 | 18 | context 'when given a valid personal contribution from Socrata' do 19 | let(:row) { JSON.load(File.read('spec/samples/socrata_contribution_valid.json')) } 20 | 21 | it 'creates a party for the recipient' do 22 | subject 23 | expect(Party.where(committee_id: row['filer_id'])).to be_present 24 | end 25 | 26 | it 'creates a Contribution record with the right values' do 27 | subject 28 | expect(Contribution.first.amount).to equal(row['tran_amt1'].to_i) 29 | expect(Contribution.first.contributor).to be_present 30 | end 31 | 32 | context 'when the recipient exists already' do 33 | it "doesn't create a Party for the recipient" do 34 | ::Party::Committee.create(name: 'foo', committee_id: row['filer_id']) 35 | 36 | expect { subject }.not_to change { Party.where(committee_id: row['filer_id']).count } 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /backend/fetchers/payment.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::Payment < DataFetcher::Base 4 | def parse_row(row) 5 | payer = get_filer(row) 6 | 7 | recipient = 8 | case row['entity_cd'] 9 | when 'COM', 'SCC' 10 | # entity being paid is a Committee and Cmte_ID will be set. Same thing as 11 | # Filer_ID but some names disagree 12 | Party::Committee.where(committee_id: row['cmte_id']) 13 | .first_or_create(name: row['payee_naml']) 14 | 15 | when 'IND' 16 | # entity being paid is an Individual 17 | full_name = row.values_at('payee_naml', 'payee_namf', 'payee_nams') 18 | .join(' ') 19 | .strip 20 | Party::Individual.where(name: full_name, 21 | city: row['payee_city'], 22 | state: row['payee_state'], 23 | zip: row['payee_zip4']) 24 | .first_or_create 25 | when 'OTH' 26 | # payee is "Other" 27 | Party::Other.where(name: row['payee_naml']) 28 | .first_or_create(city: row['payee_city'], 29 | state: row['payee_state'], 30 | zip: row['payee_zip4']) 31 | end 32 | 33 | ::Payment.create(payer: payer, 34 | recipient: recipient, 35 | amount: row['amount'], 36 | code: row['expn_code'], 37 | date: row['expn_date']) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /assets/js/views/_categoryView.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.CategoryView = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.options = options; 4 | this.render(); 5 | }, 6 | 7 | render: function() { 8 | var attributes = this.options.attributes; 9 | this.$el.empty(); 10 | this.$el.append('

Total Contributions by Category

'); 11 | // Create the data table. 12 | var data = new google.visualization.DataTable(); 13 | data.addColumn('string', 'Category'); 14 | data.addColumn('number', 'Amount'); 15 | data.addRow(['Not Itemized', attributes.summary.total_unitemized_contributions]); 16 | _.each(this.collection, function( c ){ 17 | data.addRow([c.attributes.contype, c.attributes.amount]); 18 | }); 19 | 20 | pieChart = new Backbone.GoogleChart({ 21 | chartType: 'PieChart', 22 | options: { 23 | // 'title':'Total Contributions by Category', 24 | // 'titleTextStyle':{'fontSize':30, 'fontName':'Crete Round', 'color':'#555555'}, 25 | 'backgroundColor': '#E9E9E9', 26 | 'chartArea': { 27 | 'width': 300 28 | }, 29 | 'width': 300, 30 | 'height': 250, 31 | 'sliceVisibilityThreshold': 0 32 | }, 33 | dataTable: data, 34 | }); 35 | 36 | pieChart.render(); 37 | this.$el.append($('
').html(pieChart.el)); 38 | this.$el.append("
For more details on the data in this pie chart see the FAQ
"); 39 | }, 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /assets/js/views/_topContributorsView.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.TopContributorsView = Backbone.View.extend({ 2 | template: _.template(' \ 3 |
\ 4 | \ 5 | <%= attributes.contrib %>\ 6 | <%= OpenDisclosure.friendlyMoney(attributes.amount) %> \ 7 | \ 8 |
'), 9 | 10 | initialize: function(options) { 11 | this.options = options; 12 | this.render(); 13 | }, 14 | 15 | render: function() { 16 | this.$el.empty(); 17 | $('

Top Contributors to ' + this.options.candidate + '

\ 18 |

employees grouped with their employer

\ 19 |
\ 20 |
\ 21 |
\ 22 |
') 23 | .appendTo(this.$el); 24 | 25 | var half = Math.round(this.collection.length/2); 26 | var left = this.collection.slice(0,half); 27 | var right = this.collection.slice(half); 28 | 29 | this.$el.find('.leftCol').html( 30 | _.map(left, function(c) { 31 | return this.template(c); 32 | }.bind(this)) 33 | ); 34 | 35 | this.$el.find('.rightCol').html( 36 | _.map(right, function(c) { 37 | return this.template(c); 38 | }.bind(this)) 39 | ); 40 | 41 | this.$el.append("
For more Details on how we group businesses and employers see the FAQ
"); 42 | }, 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /assets/js/models/contribution.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Contribution = Backbone.Model.extend({ 2 | typeName : function() { 3 | if (this.attributes.type == 'contribution') { 4 | return ''; // 'nuff said 5 | } else if (this.attributes.type == 'loan') { 6 | return ' (Loan)'; 7 | } else if (this.attributes.type == 'independent') { 8 | return ' (Independent committee expenditure)'; 9 | } 10 | }, 11 | contributorLinkPath : function() { 12 | // TODO: maybe DRY this up by calling OpenDisclosure.Contributor, but I like 13 | // this here for performance reasons. 14 | return '/contributor/' + this.attributes.contributor.id; 15 | }, 16 | 17 | recipientLinkPath : function() { 18 | var recipient = new OpenDisclosure.Candidate(this.attributes.recipient); 19 | return recipient.linkPath(); 20 | } 21 | }); 22 | 23 | OpenDisclosure.EmployerContribution = Backbone.Model.extend({ 24 | employerLinkPath : function () { 25 | return '/employer/' + this.attributes.contrib + '/' + this.attributes.employer_id; 26 | } 27 | }); 28 | 29 | OpenDisclosure.Employee = Backbone.Model.extend({ 30 | }); 31 | 32 | OpenDisclosure.CategoryContribution = Backbone.Model.extend({ 33 | }); 34 | 35 | OpenDisclosure.Whale = Backbone.Model.extend({ 36 | }); 37 | 38 | OpenDisclosure.Multiple = Backbone.Model.extend({ 39 | }); 40 | 41 | OpenDisclosure.IEC = Backbone.Model.extend({ 42 | }); 43 | 44 | OpenDisclosure.Contributor = Backbone.Model.extend({ 45 | linkPath : function() { 46 | return '/contributor/' + this.attributes.id; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /assets/js/views/_search.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Search = Backbone.View.extend({ 2 | initialize : function(options) { 3 | this.search = options.search || ''; 4 | 5 | this.render(); 6 | }, 7 | 8 | render : function() { 9 | this.$el.append('

Search for Contributors By Name

\ 10 | '); 14 | 15 | this.$('#search').on('submit', this.handleSearch.bind(this)); 16 | }, 17 | 18 | handleSearch : function(e) { 19 | e.preventDefault(); 20 | 21 | window.appNavigate('/search/' + encodeURI(this.$('[name=name]').val()), { trigger: true }); 22 | } 23 | }); 24 | 25 | OpenDisclosure.CommitteeSearch = Backbone.View.extend({ 26 | initialize : function (options) { 27 | this.search = options.committeeName || ''; 28 | this.render(); 29 | }, 30 | render : function() { 31 | this.$el.append('

Search for Committees or Candidates By Name

\ 32 |
\ 33 | \ 34 | \ 35 |
'); 36 | this.$('#searchCommittee').on('submit', this.handleSearch.bind(this)); 37 | }, 38 | handleSearch : function(e) { 39 | e.preventDefault(); 40 | 41 | window.appNavigate('/searchCommittee/' + encodeURI(this.$('[name=name]').val()), { trigger: true }); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /backend/fetchers/category_contributions.rb: -------------------------------------------------------------------------------- 1 | class DataFetcher 2 | class CategoryContributions 3 | def self.run! 4 | ActiveRecord::Base.connection.execute <<-QUERY 5 | INSERT into category_contributions(recipient_id, name, contype, number, amount) 6 | SELECT 7 | r.id, 8 | r.name, 9 | case 10 | when cont.self_contribution then 'Self Funded' 11 | when c.type = 'Party::Other' then 12 | case 13 | when maps.type = 'Union' then 'Union' 14 | when maps.type = 'PAC' then 'Political Action Committee' 15 | when l.firm is not null then 'Lobbyist' 16 | else 'Company' 17 | end 18 | when c.type = 'Party::Individual' AND 19 | l.name is not null OR l.firm is not null then 'Lobbyist' 20 | when c.type = 'Party::Committee' and maps.type = 'Union' then 'Union' 21 | else substring(c.type, 8) 22 | end as ConType, count(*), sum(amount) 23 | FROM 24 | contributions cont, 25 | parties r, 26 | (parties c 27 | left outer join maps on name = emp2 28 | left outer join lobbyists l on 29 | c.name = l.name or c.name = l.firm or c.employer = l.firm) 30 | WHERE 31 | r.committee_id in (#{Party::MAYORAL_CANDIDATE_IDS.join ','}) AND 32 | r.id = recipient_id AND 33 | c.id = contributor_id 34 | GROUP BY 35 | r.id, r.name, ConType 36 | ORDER BY 37 | r.id, r.name, ConType; 38 | QUERY 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /assets/js/views/_paymentsView.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.PaymentsView = Backbone.View.extend({ 2 | template: _.template('\ 3 |
\ 4 |

<%= headline %>

\ 5 |
\ 6 |
\ 7 |
<%= leftPayments %>
\ 8 |
<%= rightPayments %>
\ 9 |
'), 10 | 11 | paymentTemplate: _.template('\ 12 |
\ 13 | <%= payment.attributes.recipient.name %>\ 14 | <%= OpenDisclosure.friendlyMoney(payment.attributes.amount) %>\ 15 | <%= payment.attributes.date ? moment(payment.attributes.date).format("MMM-DD-YY"): "" %>\ 16 |
'), 17 | 18 | initialize: function(options) { 19 | this.headline = options.headline; 20 | this.showDate = options.showDate; 21 | 22 | _.bindAll(this, 'renderPayment'); 23 | 24 | this.render(); 25 | }, 26 | 27 | render: function() { 28 | var half = Math.round(this.collection.length/2); 29 | var left = this.collection.slice(0,half); 30 | var right = this.collection.slice(half); 31 | 32 | this.$el.html(this.template({ 33 | headline : this.headline, 34 | leftPayments : left.map(this.renderPayment).join(''), 35 | rightPayments : right.map(this.renderPayment).join('') 36 | })); 37 | this.$el.append("
Some data does not have dates see FAQ
"); 38 | }, 39 | 40 | renderPayment: function(payment) { 41 | var payment = new OpenDisclosure.Payment(payment.attributes); 42 | var renderedPayments = ''; 43 | renderedPayments = this.paymentTemplate({ 44 | payment: payment 45 | }) 46 | 47 | return renderedPayments; 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /backend/fetchers/summary.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::Summary < DataFetcher::Base 4 | # Hash of: 5 | # Form_Type => { Line_Item => SQL Column name } 6 | SUMMARY_LINES = { 7 | 'F460' => { 8 | '3' => :total_monetary_contributions, 9 | '4' => :total_nonmonetary_contributions, 10 | '5' => :total_contributions_received, 11 | '9' => :total_unpaid_bills, 12 | '11' => :total_expenditures_made, 13 | '14' => :total_misc_increases_to_cash, 14 | '16' => :ending_cash_balance, 15 | }, 16 | 'A' => { 17 | '2' => :total_unitemized_contributions, 18 | }, 19 | }.freeze 20 | 21 | def validate_row(row) 22 | return 'Unused Form Type' unless SUMMARY_LINES.include? row['form_type'] 23 | return 'Unused Line Item' unless SUMMARY_LINES[row['form_type']].include? row['line_item'] 24 | return 'Pending filer_id' if row['filer_id'] == 'Pending' || row['filer_id'].to_i == 0 25 | end 26 | 27 | def parse_row(row) 28 | column = SUMMARY_LINES[row['form_type']][row['line_item']] 29 | value = row['amount_a'] 30 | summary = ::Summary.where(party_id: row['filer_id']) 31 | .first_or_create 32 | 33 | # HACK / Naming convention: 34 | # The "Total" fields (i.e. :total_monetary_contributions) are reported 35 | # on each summary sheet for that period only. This means to calculate a true 36 | # total, we need to add the values from each of the summary sheets. 37 | if column =~ /^total/ 38 | summary.update_attributes( 39 | column => (summary[column] || 0) + value.to_i, 40 | ) 41 | else 42 | summary.update_attributes( 43 | column => value, 44 | ) 45 | end 46 | 47 | ::Party.where(committee_id: row['filer_id']) 48 | .update_all(['last_updated_date = GREATEST(?, last_updated_date)', row['rpt_date']]) 49 | 50 | summary 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /assets/js/models/candidate.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Candidate = Backbone.Model.extend({ 2 | linkPath : function() { 3 | return '/candidate/' + this.attributes.short_name.toLowerCase().replace(/[^a-z0-9]/g, '-'); 4 | }, 5 | 6 | imagePath : function() { 7 | return this.attributes.image; 8 | }, 9 | 10 | totalContributions : function() { 11 | return OpenDisclosure.friendlyMoney(this._totalContributionsRaw()); 12 | }, 13 | 14 | availableBalance : function() { 15 | return OpenDisclosure.friendlyMoney(this.attributes.summary['ending_cash_balance'] - 16 | this.attributes.summary['total_unpaid_bills']); 17 | }, 18 | 19 | pctContributionsFromOakland : function() { 20 | return OpenDisclosure.friendlyPct( 21 | (this.attributes.received_contributions_from_oakland - this.attributes.self_contributions_total)/ (this._totalContributionsRaw() - 22 | (this.attributes.self_contributions_total + this.attributes.summary['total_unitemized_contributions'])) 23 | ); 24 | }, 25 | 26 | pctSmallContributions : function() { 27 | return OpenDisclosure.friendlyPct( 28 | (this.attributes.summary['total_unitemized_contributions'] + 29 | this.attributes.small_contributions) / this._totalContributionsRaw()); 30 | }, 31 | 32 | pctPersonalContributions: function(){ 33 | return OpenDisclosure.friendlyPct(this.get('self_contributions_total') / this.get('summary').total_contributions_received); 34 | }, 35 | 36 | avgContribution : function () { 37 | return OpenDisclosure.friendlyMoney(this._totalContributionsRaw() / this.attributes.received_contributions_count); 38 | }, 39 | 40 | friendlySummaryNumber : function(which) { 41 | return OpenDisclosure.friendlyMoney(this.attributes.summary[which]); 42 | }, 43 | 44 | _totalContributionsRaw : function() { 45 | return this.attributes.summary['total_contributions_received'] + this.attributes.summary['total_misc_increases_to_cash']; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /assets/js/views/iec.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.IECView = Backbone.View.extend({ 2 | 3 | template: _.template('\ 4 |
\ 5 | <%= attributes.date ? moment(attributes.date).format("MMM-DD-YY"): "" %>\ 6 | <%= attributes.support ? "Supporting" : "Opposing" %>\ 7 | \ 8 | <%= attributes.recipient.name %>\ 9 | \ 10 | <%= OpenDisclosure.friendlyMoney(attributes.amount) %>\ 11 |
\ 12 |
\ 13 | \ 14 | <%= attributes.description %>\ 15 |
'), 16 | 17 | header: _.template('\ 18 |
\ 19 |

<%= contributorName %>

\ 20 |
'), 21 | 22 | initialize: function(options) { 23 | this.options = options; 24 | 25 | _.bindAll(this, 'renderExpenditure'); 26 | 27 | if (this.collection.length > 0) { 28 | this.render(); 29 | } 30 | this.listenTo(this.collection, 'sync', this.render); 31 | 32 | }, 33 | 34 | render: function() { 35 | this.$el.empty(); 36 | $('

Independent Commitee Expenditures

').appendTo(this.$el); 37 | $('
Not controlled by candidates. See FAQ.
').appendTo(this.$el); 38 | if (this.collection.length == 0) { 39 | return 40 | } 41 | this.name = ""; 42 | this.$el.append(this.collection.map(this.renderExpenditure).join(' ')); 43 | }, 44 | 45 | renderExpenditure: function(c) { 46 | ret = ""; 47 | if (c.attributes.contributor.name != this.name) { 48 | this.name = c.attributes.contributor.name; 49 | ret = this.header({ contributorName: this.name }); 50 | } 51 | contribution = new OpenDisclosure.Contribution(c.attributes); 52 | return ret + this.template(contribution); 53 | 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /spec/backend/fetchers/summary_spec.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | 3 | describe DataFetcher::Summary do 4 | before do 5 | ::Summary.delete_all 6 | end 7 | 8 | describe '.parse_summary' do 9 | let(:row) do 10 | { 11 | "thru_date" : "2011-12-31T00:00:00", 12 | "line_item" : "1", 13 | "from_date" : "2011-07-01T00:00:00", 14 | "filer_naml" : "Harland For Mayor", 15 | "amount_a" : "0", 16 | "report_num" : "0", 17 | "amount_b" : "5762", 18 | "form_type" : "F460", 19 | "rec_type" : "SMRY", 20 | "filer_id" : "1327636", 21 | "committee_type" : "CTL", 22 | "rpt_date" : "2014-04-01T00:00:00" 23 | } 24 | end 25 | 26 | subject { DataFetcher::Summary.parse_summary(row) } 27 | 28 | context 'when the line item is "total monetary contributions"' do 29 | let(:row) { JSON.load(File.read('spec/samples/socrata_summary.json')) } 30 | let(:rows) { [row] } 31 | let(:thru_date) { Date.parse('2013-06-30') } 32 | let(:amount) { (Random.rand * 100).to_i } 33 | 34 | before do 35 | row['form_type'] = 'F460' 36 | row['line_item'] = '3' 37 | row['amount_a'] = amount 38 | row['thru_date'] = thru_date 39 | end 40 | 41 | it 'sets amount correctly' do 42 | expect(subject.total_monetary_contributions).to eq(amount) 43 | end 44 | 45 | context 'when there are multiple summaries' do 46 | before do 47 | other_row = row.dup 48 | other_row['amount_a'] = other_amount 49 | other_row['thru_date'] = other_thru_date 50 | 51 | rows << other_row 52 | 53 | subject 54 | end 55 | 56 | let(:other_amount) { (Random.rand * 100).to_i } 57 | let(:other_thru_date) { thru_date + 1 } 58 | 59 | it 'aggregates across multiple rows' do 60 | expect(subject.reload.total_monetary_contributions). 61 | to eq(amount + other_amount) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /backend/load_data.rb: -------------------------------------------------------------------------------- 1 | require_relative 'environment.rb' 2 | require_relative 'schema.rb' # wipe the database and start anew 3 | 4 | class DataFetcher 5 | SOCRATA_URLS = [ 6 | [DataFetcher::LateContribution, 'http://data.oaklandnet.com/resource/qact-u8hq.json'], 7 | [DataFetcher::Contribution, 'http://data.oaklandnet.com/resource/3xq4-ermg.json'], 8 | [DataFetcher::Payment, 'http://data.oaklandnet.com/resource/bvfu-nq99.json'], 9 | [DataFetcher::Loan, 'http://data.oaklandnet.com/resource/qaa7-q29f.json'], 10 | [DataFetcher::Summary, 'http://data.oaklandnet.com/resource/rsxe-vvuw.json'], 11 | [DataFetcher::IEC, 'http://data.oaklandnet.com/resource/jkj3-8yq3.json'], 12 | [DataFetcher::IEC, 'http://data.oaklandnet.com/resource/6ejr-39gh.json'], 13 | ] 14 | 15 | def self.load_all_data! 16 | if ENV['DEBUG'] 17 | ActiveRecord::Base.logger = Logger.new(STDOUT) 18 | end 19 | 20 | puts "Loading Payment Codes" 21 | PaymentCodes.load_from_file('backend/payment_codes.csv') 22 | 23 | # This table maps spellings of employers to a common spelling. 24 | # It needs to be updated when a new batch of data is available 25 | # as there is no check on spelling on the forms. 26 | puts "Loading Employer Map" 27 | Map.load_mappings('backend/map.csv') 28 | 29 | puts "Loading Lobbyist data" 30 | Lobbyist.load_from_file('backend/2014_Lobbyist_Directory.csv') 31 | 32 | SOCRATA_URLS.each do |fetcher_class, socrata_url| 33 | puts "Fetching #{fetcher_class} from Socrata:" 34 | downloader = SocrataDownloader.new(socrata_url) 35 | fetcher = fetcher_class.new(downloader) 36 | 37 | fetcher.run! 38 | 39 | puts fetcher.status 40 | end 41 | 42 | puts "Run analysis" 43 | DataFetcher::CategoryContributions.run! 44 | DataFetcher::EmployerContributions.run! 45 | DataFetcher::CategoryPayments.run! 46 | DataFetcher::Multiples.run! 47 | DataFetcher::Whales.run! 48 | 49 | Import.create(import_time: Time.now) 50 | end 51 | end 52 | 53 | if __FILE__ == $0 54 | DataFetcher.load_all_data! 55 | end 56 | -------------------------------------------------------------------------------- /assets/js/views/contributor.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Contributor = Backbone.View.extend({ 2 | 3 | template: HandlebarsTemplates['contributor'], 4 | 5 | initialize: function(options) { 6 | _.bindAll(this, 'render'); 7 | 8 | this.options = options; 9 | 10 | this.collection = new OpenDisclosure.Contributors([], { 11 | contributor: this.options.contributorId, 12 | search: this.options.search 13 | }); 14 | 15 | this.collection.fetch({ success: this.render }); 16 | }, 17 | 18 | contributorName: function() { 19 | return this.collection.at(0).get('contributor').name; 20 | }, 21 | 22 | render: function() { 23 | if (this.collection.length === 0) { 24 | this.$el.empty(); 25 | new OpenDisclosure.Search({ 26 | el: this.$el, 27 | }); 28 | 29 | return; 30 | } 31 | 32 | // group all contributions by contributor 33 | var groupedCollection = this.collection.groupBy(function(m) { 34 | return m.attributes.contributor.name; 35 | }); 36 | 37 | this.$el.html(new OpenDisclosure.Search({ search : this.options.search }).$el); 38 | this.$el.append(this.template({ 39 | // produce a mapping like: 40 | // { 41 | // contributors: [{ 42 | // name: 'foobar baz', 43 | // contributions: [{ ... }, ...], 44 | // }, ...] 45 | // } 46 | contributors: _.map(groupedCollection, function(contributions, name) { 47 | return { 48 | name: name, 49 | contributions: _.map(contributions, function(contribution) { 50 | contribution = new OpenDisclosure.Contribution(contribution.attributes); 51 | 52 | return { 53 | recipientLinkPath: contribution.recipientLinkPath(), 54 | recipientName: contribution.attributes.recipient.name, 55 | amount: OpenDisclosure.friendlyMoney(contribution.get('amount')), 56 | type: contribution.typeName(), 57 | date: moment(contribution.attributes.date).format("MMM-DD-YY") 58 | }; 59 | }) 60 | }; 61 | })} 62 | )); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /assets/js/views/home.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Home = Backbone.View.extend({ 2 | initialize : function() { 3 | this.render(); 4 | }, 5 | 6 | render : function() { 7 | this.$el.html('
\ 8 |
\ 9 | \ 10 | \ 11 |
For information on searching see the FAQ
\ 12 |
\ 13 |
\ 14 |
\ 15 |
\ 16 |
'); 17 | 18 | new OpenDisclosure.CandidateTable({ 19 | el : '#candidateTable', 20 | collection : OpenDisclosure.Data.candidates 21 | }); 22 | 23 | new OpenDisclosure.Search({ 24 | el : '#search' 25 | }); 26 | 27 | new OpenDisclosure.CommitteeSearch({ 28 | el : '#committee' 29 | }); 30 | 31 | OpenDisclosure.Data.zipContributions.done(function(data) { 32 | new OpenDisclosure.ZipcodeChartView({ 33 | el : '#zipcodeChart', 34 | collection : data, 35 | base_height: 480 36 | }); 37 | }); 38 | 39 | 40 | // TODO: This is commented out until it uses the data format returned by 41 | // /api/contributions/by_date and that API endpoint is created. 42 | // 43 | OpenDisclosure.Data.dailyContributions.done(function(data) { 44 | new OpenDisclosure.DailyContributionsChartView({ 45 | el : "#dailyChart", 46 | collection: data, 47 | base_height: 480 48 | }); 49 | }); 50 | 51 | new OpenDisclosure.ContributorsView({ 52 | el : '#topContributions', 53 | collection : OpenDisclosure.Data.whales, 54 | headline :'Top Contributors To All Candidates in This Election', 55 | showDate : false 56 | }); 57 | 58 | new OpenDisclosure.MultiplesView({ 59 | el : '#multiples', 60 | collection: OpenDisclosure.Data.multiples, 61 | headline: 'Contributors To More Than One Mayoral Candidate' 62 | }); 63 | 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /assets/js/collections/contributions.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Contributions = Backbone.Collection.extend({ 2 | url: function() { 3 | return '/api/contributions/candidate/' + this.options.candidateId; 4 | }, 5 | model: OpenDisclosure.Contribution, 6 | initialize: function(models, options) { 7 | this.options = options; 8 | } 9 | }); 10 | OpenDisclosure.CommitteeContributions = Backbone.Collection.extend({ 11 | url: function() { 12 | return '/api/contributions/committee/' + encodeURI(this.options.committeeName); 13 | }, 14 | model: OpenDisclosure.Contribution, 15 | initialize: function(models, options) { 16 | this.options = options; 17 | } 18 | }); 19 | OpenDisclosure.CategoryContributions = Backbone.Collection.extend({ 20 | url: '/api/category_contributions', 21 | model: OpenDisclosure.CategoryContribution 22 | }); 23 | OpenDisclosure.EmployerContributions = Backbone.Collection.extend({ 24 | url: '/api/employer_contributions', 25 | model: OpenDisclosure.EmployerContribution 26 | }); 27 | OpenDisclosure.Whales = Backbone.Collection.extend({ 28 | url: '/api/whales', 29 | model: OpenDisclosure.Whale 30 | }); 31 | OpenDisclosure.Multiples = Backbone.Collection.extend({ 32 | url: '/api/multiples', 33 | model: OpenDisclosure.Multiple 34 | }); 35 | OpenDisclosure.IECs = Backbone.Collection.extend({ 36 | url: '/api/independent', 37 | model: OpenDisclosure.IEC 38 | }); 39 | OpenDisclosure.Contributors = Backbone.Collection.extend({ 40 | model: OpenDisclosure.Contributor, 41 | comparator: function (model) { 42 | return model.get('contributor').name; 43 | }, 44 | url: function() { 45 | if (this.options.contributor) { 46 | return '/api/contributor/' + this.options.contributor; 47 | } else { 48 | return '/api/contributorName/' + encodeURI(this.options.search); 49 | } 50 | 51 | }, 52 | initialize: function(models, options) { 53 | this.options = options; 54 | } 55 | }); 56 | OpenDisclosure.Employees = Backbone.Collection.extend({ 57 | model: OpenDisclosure.Employee, 58 | url: function() { 59 | return '/api/employees/' + this.options.employer_id; 60 | }, 61 | initialize: function(models, options){ 62 | this.options = options; 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /backend/fetchers/contribution.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::Contribution < DataFetcher::Base 4 | def validate_row(row) 5 | return 'Invalid Transaction Amount' unless row['tran_amt1'] 6 | return 'Undefined recipient' if row['cmte_id'].nil? && row['tran_naml'].nil? && row['tran_namf'].nil? 7 | end 8 | 9 | def parse_row(row) 10 | recipient = get_filer(row) 11 | 12 | contributor = 13 | case row['entity_cd'] 14 | when 'COM', 'SCC' 15 | # contributor is a Committee and Cmte_ID is set. Same thing as 16 | # Filer_ID but some names disagree 17 | Party::Committee.where(committee_id: row['cmte_id']) 18 | .first_or_create(name: row['tran_naml'] || 'unknown') 19 | 20 | when 'IND', 'OTH' 21 | # If there is a first name this may have been chatacterized as other 22 | # instead of individual. This happened with the Firefighters Union Pac 23 | if row['entity_cd'] == 'IND' || !row['tran_namf'].nil? then 24 | # contributor is an Individual 25 | full_name = row.values_at('tran_namf', 'tran_naml', 'tran_nams') 26 | .join(' ') 27 | .strip 28 | Party::Individual.where(name: full_name, 29 | city: row['tran_city'], 30 | state: row['tran_state'], 31 | zip: row['tran_zip4']) 32 | .first_or_initialize 33 | .tap { |p| p.update_attributes(employer: row['tran_emp'], occupation: row['tran_occ']) } 34 | else 35 | # contributor is "Other" 36 | Party::Other.where(name: row['tran_naml'] || 'unknown') 37 | .first_or_initialize 38 | .tap { |p| p.update_attributes(city: row['tran_city'], state: row['tran_state'], zip: row['tran_zip4']) } 39 | end 40 | end 41 | 42 | ::Contribution.where(recipient: recipient, 43 | transaction_id: row['tran_id'], 44 | contributor: contributor, 45 | amount: row['tran_amt1'], 46 | date: row['tran_date'], 47 | type: 'contribution' 48 | ).first_or_create 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /assets/js/views/committee.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Committee = Backbone.View.extend({ 2 | template: _.template(' \ 3 |
\ 4 |
'), 9 | 10 | header: _.template('\ 11 |
\ 12 |

<%= recipientName %>

\ 13 |
\ 14 |
'), 15 | 16 | initialize: function(options) { 17 | _.bindAll(this, 'render', 'renderContribution'); 18 | this.options = options; 19 | 20 | this.collection = new OpenDisclosure.CommitteeContributions([], { 21 | committeeName: this.options.committeeName, 22 | }); 23 | this.collection.fetch({ success: this.render }); 24 | }, 25 | 26 | recipientName: function() { 27 | return this.collection.at(0).get('recipient').name; 28 | }, 29 | 30 | render: function() { 31 | this.$el.html('\ 32 |
'); 33 | 34 | new OpenDisclosure.CommitteeSearch({ 35 | el : "#search" 36 | }); 37 | 38 | if (this.collection.length > 0) { 39 | if (this.options.search) { 40 | this.collection.sort(); 41 | } 42 | this.name = this.recipientName(); 43 | this.$('.data').html(this.header({ recipientName: this.name })); 44 | this.$('.contributions').html(this.collection.map(this.renderContribution)); 45 | } else { 46 | this.$('.data').empty(); 47 | } 48 | 49 | }, 50 | 51 | renderContribution: function(c) { 52 | var contribution = new OpenDisclosure.Contribution(c.attributes); 53 | 54 | var ret = ""; 55 | if (this.name != c.attributes.recipient.name) { 56 | this.name = c.attributes.recipient.name; 57 | ret = this.header({ recipientName: this.name }); 58 | } 59 | return ret + this.template({ contribution: contribution }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /assets/js/views/_paymentCategories.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.PaymentCategory = Backbone.View.extend({ 2 | initialize: function(options) { 3 | this.options = options; 4 | _.bindAll(this, 'displayDetails'); 5 | // We don't want to show this section until someone clicks 6 | $(this.options.list).hide(); 7 | this.render(); 8 | }, 9 | 10 | render: function() { 11 | var attributes = this.options.attributes; 12 | this.$el.empty(); 13 | this.$el.append('

Total Payments by Category

'); 14 | // Create the data table. 15 | var data = new google.visualization.DataTable(); 16 | this.data = data; 17 | data.addColumn('string', 'Category'); 18 | data.addColumn('number', 'Amount'); 19 | data.addColumn('string', 'Code'); 20 | _.each(this.collection, function( c ){ 21 | data.addRow([c.attributes.text, c.attributes.amount, c.attributes.code]); 22 | }); 23 | 24 | pieChart = new Backbone.GoogleChart({ 25 | chartType: 'PieChart', 26 | options: { 27 | // 'title':'Total Contributions by Category', 28 | // 'titleTextStyle':{'fontSize':30, 'fontName':'Crete Round', 'color':'#555555'}, 29 | 'backgroundColor': '#E9E9E9', 30 | 'chartArea': { 31 | 'width': 300 32 | }, 33 | 'width': 300, 34 | 'height': 250, 35 | 'sliceVisibilityThreshold': 0 36 | }, 37 | dataTable: data, 38 | }); 39 | 40 | pieChart.render(); 41 | 42 | // Clicking on the chart loads the details 43 | pieChart.on('select', this.displayDetails); 44 | this.$el.append($('
').html(pieChart.el)); 45 | this.$el.append("
Click on chart or labels to drill down to actual payments
"); 46 | }, 47 | 48 | displayDetails : function(chartObject) { 49 | data = this.data; 50 | selection = chartObject.getSelection()[0]; 51 | if (!selection) return; 52 | collection = _.filter(this.options.payments.models, function(c) { 53 | return c.attributes.code == data.getValue(selection.row, 2) 54 | }); 55 | $(this.options.list).show(); 56 | new OpenDisclosure.PaymentsView({ 57 | el: this.options.list, 58 | headline: 'All Payments for ' + data.getValue(selection.row, 0), 59 | collection: collection 60 | }); 61 | } 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /assets/css/daily-contributions-chart.css: -------------------------------------------------------------------------------- 1 | 2 | section#dailyChart path { 3 | stroke-width: 4; 4 | fill: none; 5 | } 6 | 7 | section#dailyChart .axis path, 8 | .axis line { 9 | fill: none; 10 | stroke: #000; 11 | stroke-width: 1; 12 | shape-rendering: crispEdges; 13 | } 14 | 15 | section#dailyChart text { 16 | fill: #555555; 17 | } 18 | 19 | section#dailyChart .axis path,line { 20 | stroke: #555555; 21 | } 22 | 23 | section#dailyChart h3, h4 { 24 | text-align: right; 25 | } 26 | 27 | section#dailyChart .legend { 28 | margin-left: 12%; 29 | } 30 | 31 | section#dailyChart .legend div { 32 | display: block; 33 | float: left; 34 | } 35 | 36 | section#dailyChart .legend .legend-item { 37 | margin: 5px 6px; 38 | width: 148px; 39 | } 40 | 41 | section#dailyChart .legend .color { 42 | width: 1.3em; 43 | height: 1.3em; 44 | margin: 1px 4px 0 4px; 45 | } 46 | 47 | section#dailyChart .q0-12 { 48 | stroke: #000000; 49 | background-color: #000000; 50 | } 51 | section#dailyChart .q1-12 { 52 | stroke: #0087E5; 53 | background-color: #0087E5; 54 | } 55 | section#dailyChart .q2-12 { 56 | stroke: #26D5F5; 57 | background-color: #26D5F5; 58 | } 59 | section#dailyChart .q3-12 { 60 | stroke: #A8E938; 61 | background-color: #A8E938; 62 | } 63 | section#dailyChart .q4-12 { 64 | stroke: #FED35E; 65 | background-color: #FED35E; 66 | } 67 | section#dailyChart .q5-12 { 68 | stroke: #FD2D2D; 69 | background-color: #FD2D2D; 70 | } 71 | section#dailyChart .q6-12 { 72 | stroke: #E1266C; 73 | background-color: #E1266C; 74 | } 75 | section#dailyChart .q7-12 { 76 | stroke: #DA9EE7; 77 | background-color: #DA9EE7; 78 | } 79 | section#dailyChart .q8-12 { 80 | stroke: #0A588f; 81 | background-color: #0A588f; 82 | } 83 | section#dailyChart .q9-12 { 84 | stroke: #8E34F4; 85 | background-color: #8E34F4; 86 | } 87 | section#dailyChart .q10-12 { 88 | stroke: #2FB788; 89 | background-color: #2FB788; 90 | } 91 | section#dailyChart .q11-12 { 92 | stroke: #BA0012; 93 | background-color: #BA0012; 94 | } 95 | section#dailyChart .q12-12 { 96 | stroke: #B9A101; 97 | background-color: #B9A101; 98 | } 99 | section#dailyChart .q13-12 { 100 | stroke: #480A6A; 101 | background-color: #480A6A; 102 | } 103 | -------------------------------------------------------------------------------- /backend/fetchers/late_contribution.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::LateContribution < DataFetcher::Base 4 | def validate_row(row) 5 | return 'Unspecified amount' if row['amount'].nil? 6 | return 'Unnamed entity' if row['enty_naml'].nil? && row['enty_namf'].nil? 7 | return 'Payment from this committee' if row['form_type'] == 'F497P2' 8 | end 9 | 10 | def parse_row(row) 11 | recipient = get_filer(row) 12 | contributor = 13 | case row['entity_cd'] 14 | when 'COM', 'SCC' 15 | # contributor is a Committee and Cmte_ID is set. Same thing as 16 | # Filer_ID but some names disagree 17 | Party::Committee.where(committee_id: row['cmte_id']) 18 | .first_or_create(name: row['enty_naml']) 19 | 20 | when 'IND', 'OTH' 21 | # If there is a first name this may have been chatacterized as other 22 | # instead of individual. This happened with the Firefighters Union Pac 23 | if row['entity_cd'] == 'IND' || !row['tran_namf'].nil? then 24 | # contributor is an Individual 25 | full_name = row.values_at('enty_namf', 'enty_naml', 'enty_nams') 26 | .join(' ') 27 | .strip 28 | Party::Individual.where(name: full_name, 29 | city: row['enty_city'], 30 | state: row['enty_st'], 31 | zip: row['enty_zip4']) 32 | .first_or_initialize 33 | .tap { |p| p.update_attributes(employer: row['ctrib_emp'], occupation: row['ctrib_occ']) } 34 | else 35 | # contributor is "Other" 36 | Party::Other.where(name: row['enty_naml']) 37 | .first_or_initialize 38 | .tap { |p| p.update_attributes(city: row['enty_city'], state: row['enty_st'], zip: row['enty_zip4']) } 39 | end 40 | end 41 | 42 | ::Party.where(committee_id: recipient[:filer_id]) 43 | .update_all(['last_updated_date = GREATEST(?, last_updated_date)', row['rpt_date']]) 44 | 45 | ::Contribution.where(recipient: recipient, 46 | transaction_id: row['tran_id'], 47 | contributor: contributor, 48 | amount: row['amount'], 49 | date: row['ctrib_date'], 50 | type: 'contribution' 51 | ).first_or_create 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /assets/js/templates/rules.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Oakland Campaign Finance Rules

3 |

4 | Any “person” may contribute up to $700* to each candidate per election cycle. “Broad-based political committees” may contribute up to $1400* to each candidate per election cycle ( Oakland Campaign Reform Act (OCRA) 3.12.050). 5 |

6 |

7 | *Candidates may accept this higher contribution limit ONLY IF they accept the OCRA expenditure ceilings by submitting the OCRA Form 301. If the candidate does not accept the expenditure ceilings then the limit for persons is $100 and the limit for broad-based political committees is $250. 8 |

9 |

10 | “Person” means an individual, proprietorship, firm, partnership, joint venture, syndicate, business, trust, company, corporation, association, committee, and any other organization or group of persons acting in concert (OCRA 3.12.040). 11 |

12 |

13 | ”Broad-based political committee” means a committee of persons which has been in existence for more than six months, receives contributions from one hundred (100) or more persons, and acting in concert makes contributions to five or more candidates (OCRA 3.12.040). 14 |

15 |

16 | For more information on Oakland’s campaign finance rules, please review the Oakland Campaign Reform Act (OCRA) and the Public Ethics Commission’s Guide to OCRA. 17 |

18 |

19 | Contact the City of Oakland Public Ethics Commission with any questions at ethicscommission@oaklandnet.com or (510) 238-3593. 20 |

21 |
22 | -------------------------------------------------------------------------------- /backend/fetchers/loan.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::Loan < DataFetcher::Base 4 | def validate_row(row) 5 | return 'No loan amount' if row['loan_amt1'].to_i == 0 && row['loan_amt5'].to_i == 0 && row['loan_amt6'].to_i == 0 6 | return 'No loan name' if row['loan_amt1'].nil? && row['lndr_naml'].nil? && row['lndr_namf'].nil? 7 | end 8 | 9 | def parse_row(row) 10 | recipient = get_filer(row) 11 | 12 | contributor = 13 | case row['entity_cd'] 14 | when 'COM', 'SCC' 15 | # contributor is a Committee and Cmte_ID is set. Same thing as 16 | # Filer_ID but some names disagree 17 | Party::Committee.where(committee_id: row['cmte_id']) 18 | .first_or_create(name: row['lndr_naml']) 19 | 20 | when 'IND', 'OTH' 21 | # If there is a first name this may have been chatacterized as other 22 | # instead of individual. This happened with the Firefighters Union Pac 23 | if row['entity_cd'] == 'IND' || !row['tran_namf'].nil? 24 | # contributor is an Individual 25 | full_name = row.values_at('lndr_namf', 'lndr_naml', 'lndr_nams') 26 | .join(' ') 27 | .strip 28 | Party::Individual.where(name: full_name, 29 | city: row['loan_city'], 30 | state: row['loan_st'], 31 | zip: row['loan_zip4']) 32 | .first_or_create(employer: row['loan_emp'], 33 | occupation: row['loan_occ']) 34 | else 35 | # contributor is "Other" 36 | Party::Other.where(name: row['lndr_naml']) 37 | .first_or_create(city: row['loan_city'], 38 | state: row['loan_st'], 39 | zip: row['loan_zip4']) 40 | end 41 | end 42 | 43 | # "amount received this period less amount paid backand amount forgiven" 44 | loan_amt = row['loan_amt1'].to_i - (row['loan_amt5'].to_i + row['loan_amt6'].to_i) 45 | # There is no date recorded for the rempayment/forgiven date, so just use 46 | # the report date. 47 | if loan_amt > 0 48 | loan_date = row['loan_date1'] 49 | else 50 | loan_date = row['rpt_date'] 51 | end 52 | 53 | ::Contribution.where(recipient: recipient, transaction_id: row['tran_id'], 54 | contributor: contributor, 55 | amount: loan_amt, 56 | date: loan_date, 57 | type: 'loan' 58 | ).first_or_create 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /neo4j/import.cyp: -------------------------------------------------------------------------------- 1 | // Delete everything 2 | MATCH (n) OPTIONAL MATCH (n)-[r]->() DELETE n, r; 3 | 4 | CREATE INDEX ON :Entity(name); 5 | CREATE INDEX ON :City(name); 6 | CREATE INDEX ON :Occupation(name); 7 | 8 | // Load contributions data 9 | LOAD CSV WITH HEADERS FROM "file:///tmp/A-Contributions.csv" AS line 10 | WITH line, 11 | CASE line.Entity_Cd 12 | WHEN "IND" THEN line.Tran_NamF + " " + line.Tran_NamL 13 | ELSE line.Tran_NamL END AS contributorName, 14 | toInt(round(coalesce(toFloat(line.Tran_Amt1), 0.0))) AS amount 15 | MERGE (f:Entity {name: line.Filer_NamL}) 16 | SET f:Recipient, 17 | f.id = line.Filer_ID, 18 | f.committeeType = line.Committee_Type 19 | MERGE (c:Entity {name: contributorName}) 20 | SET c:Contributor, 21 | c.zip = substring(line.Tran_Zip4, 0, 5), 22 | c.occupation = line.Tran_Occ 23 | MERGE (c)-[n:CONTRIBUTED {amount: amount, date: line.Tran_Date}]->(f) 24 | SET 25 | n.desc = line.Tran_Dscr 26 | FOREACH(n IN (CASE line.Tran_Emp WHEN "" THEN [] else [line.Tran_Emp] END) | 27 | MERGE (e:Entity {name: n}) 28 | SET e:Employer 29 | MERGE (c)-[:EMPLOYER]->(e) 30 | ) 31 | FOREACH(n IN (CASE line.Tran_City WHEN "" THEN [] else [upper(line.Tran_City)] END) | 32 | MERGE (p:City {name: n, state: line.Tran_State}) 33 | MERGE (c)-[:LOCATION]->(p) 34 | ) 35 | FOREACH(n IN (CASE line.Tran_Occ WHEN "" THEN [] else [upper(line.Tran_Occ)] END) | 36 | MERGE (o:Occupation {name: n}) 37 | MERGE (c)-[:WORKS_AS]->(o) 38 | ); 39 | 40 | 41 | // Load expenditure data 42 | LOAD CSV WITH HEADERS FROM "file:///tmp/E-Expenditure.csv" AS line 43 | WITH line, 44 | CASE line.Entity_Cd 45 | WHEN "IND" THEN line.Payee_NamF + " " + line.Payee_NamL 46 | ELSE line.Payee_NamL END AS vendorName, 47 | toInt(round(coalesce(toFloat(line.Amount), 0.0))) AS amount 48 | MERGE (f:Entity {name: line.Filer_NamL}) 49 | SET f:Recipient, 50 | f.id = line.Filer_ID, 51 | f.committeeType = line.Committee_Type 52 | MERGE (v:Entity {name: vendorName}) 53 | SET v:Vendor 54 | MERGE (f)-[n:PAYED {amount: amount, date: line.Expn_Date}]->(v) 55 | SET 56 | n.desc = line.Expn_Dscr 57 | FOREACH(n IN (CASE line.Payee_City WHEN "" THEN [] else [upper(line.Payee_City)] END) | 58 | MERGE (p:City {name: n, state: line.Payee_State}) 59 | MERGE (v)-[:LOCATION]->(p) 60 | ); 61 | 62 | 63 | // Label the known Mayoral Candidates with a :Candidate label 64 | MATCH (f:Entity) 65 | WHERE f.name IN [ 66 | "Patrick McCullough Mayor 2014", 67 | "Parker for Oakland Mayor 2014", 68 | "Re-Elect Mayor Quan 2014", 69 | "Libby Schaaf for Oakland Mayor 2014", 70 | "Joe Tuman for Mayor 2014" 71 | ] 72 | SET f:Candidate; 73 | -------------------------------------------------------------------------------- /assets/css/charts.css.scss: -------------------------------------------------------------------------------- 1 | 2 | #zipcodeChart { 3 | /* ------------------- HEADERS -------------------- */ 4 | h3, h4 { 5 | text-align: right; 6 | } 7 | 8 | div.candidate.description { 9 | display: none; 10 | } 11 | 12 | div.candidate.description .return{ 13 | text-decoration: underline; 14 | cursor: pointer; 15 | } 16 | 17 | /* ------------------- TOOLTIP -------------------- */ 18 | #svg-wrapper { 19 | position: relative; 20 | } 21 | 22 | #tooltip { 23 | background-color: white; 24 | position: absolute; 25 | padding: 5px; 26 | font-size: 12px; 27 | } 28 | 29 | #tooltip.hidden { 30 | display: none; 31 | } 32 | 33 | /* ------------------- MAP -------------------- */ 34 | .zip { 35 | fill: white; 36 | stroke: #bfbfbf; 37 | } 38 | 39 | .zip.hover { 40 | fill: #f0f0f0; 41 | } 42 | 43 | .city { 44 | fill: none; 45 | stroke: #4c4c4c; 46 | } 47 | 48 | /* ------------------- LEGEND -------------------- */ 49 | .legend { 50 | cursor: pointer; 51 | } 52 | 53 | .legend text{ 54 | fill: #555555; 55 | text-anchor: end; 56 | } 57 | 58 | .status { 59 | fill: #bfbfbf; 60 | } 61 | 62 | .legend rect.name { 63 | fill-opacity: 0; 64 | } 65 | 66 | .legend.selected rect.name { 67 | fill : #ffffff; 68 | fill-opacity: 1; 69 | } 70 | 71 | .legend rect.name.highlight { 72 | fill : #ffffff; 73 | fill-opacity: 1; 74 | } 75 | 76 | .divider { 77 | fill: white; 78 | } 79 | 80 | /* -- Overview Bar -- */ 81 | 82 | .legend.overview text { 83 | font-family: 'Crete Round', serif; 84 | } 85 | 86 | .legend.overview rect.divider { 87 | display: none; 88 | } 89 | 90 | .legend.overview.selected rect.name{ 91 | fill: black; 92 | fill-opacity: 100%; 93 | } 94 | 95 | .legend.overview.selected text { 96 | fill: white; 97 | } 98 | 99 | .legend.selected text { 100 | fill: black; 101 | } 102 | 103 | /* ------------------- SCALE -------------------- */ 104 | #scale { 105 | display: none; 106 | } 107 | 108 | #scale text.name { 109 | text-anchor: middle; 110 | fill: #555555; 111 | } 112 | 113 | rect.scale { 114 | fill: white; 115 | } 116 | 117 | /* ------------------- COLOR SCALE -------------------- */ 118 | .q0-12{fill: #000000} 119 | .q1-12{fill: #0087E5} 120 | .q2-12{fill: #26D5F5} 121 | .q3-12{fill: #A8E938} 122 | .q4-12{fill: #FED35E} 123 | .q5-12{fill: #FD2D2D} 124 | .q6-12{fill: #E1266C} 125 | .q7-12{fill: #DA9EE7} 126 | .q8-12{fill: #0A588f} 127 | .q9-12{fill: #8E34F4} 128 | .q10-12{fill: #2FB788} 129 | .q11-12{fill: #BA0012} 130 | .q12-12{fill: #B9A101} 131 | .q13-12{fill: #480A6A} 132 | } -------------------------------------------------------------------------------- /backend/fetchers/employer_contributions.rb: -------------------------------------------------------------------------------- 1 | class DataFetcher 2 | class EmployerContributions 3 | def self.run! 4 | ActiveRecord::Base.connection.execute <<-QUERY 5 | INSERT into employers(employer_name) 6 | SELECT DISTINCT s.contrib FROM 7 | ( 8 | SELECT 9 | CASE 10 | WHEN p.Emp1 is NULL THEN 11 | coalesce(c.employer, 'N/A') 12 | WHEN p.Emp1 = 'N/A' THEN 13 | coalesce((SELECT p1.Emp1 14 | FROM maps p1 WHERE p1.Emp2 = c.occupation), c.occupation) 15 | WHEN p.Emp1 = 'Unemployed' THEN 16 | coalesce((SELECT p1.Emp1 17 | FROM maps p1 WHERE p1.Emp2 = c.occupation), 'Unemployed') 18 | ELSE p.Emp1 19 | END AS contrib 20 | FROM contributions b, 21 | parties c LEFT OUTER JOIN maps p ON (c.employer = p.Emp2) 22 | WHERE b.contributor_id = c.id AND c.type = 'Party::Individual' 23 | UNION ALL 24 | SELECT coalesce(p.Emp1, c.name) AS contrib 25 | FROM contributions b, 26 | parties c LEFT OUTER JOIN maps p ON c.name = p.Emp2 27 | WHERE b.contributor_id = c.id AND c.type <> 'Party::Individual' 28 | ) s 29 | QUERY 30 | ActiveRecord::Base.connection.execute <<-QUERY 31 | UPDATE parties c SET employer_id = e.id 32 | FROM employers e, maps p 33 | WHERE CASE 34 | WHEN p.Emp1 = 'N/A' THEN 35 | coalesce((SELECT p1.Emp1 36 | FROM maps p1 WHERE p1.Emp2 = c.occupation), c.occupation) 37 | WHEN p.Emp1 = 'Unemployed' THEN 38 | coalesce((SELECT p1.Emp1 39 | FROM maps p1 WHERE p1.Emp2 = c.occupation), 'Unemployed') 40 | ELSE p.Emp1 41 | END = e.employer_name AND 42 | c.employer = p.Emp2 AND c.type = 'Party::Individual' 43 | QUERY 44 | ActiveRecord::Base.connection.execute <<-QUERY 45 | UPDATE parties c SET employer_id = e.id 46 | FROM employers e, maps p 47 | WHERE p.Emp1 = e.employer_name AND 48 | c.name = p.Emp2 AND c.type <> 'Party::Individual' 49 | QUERY 50 | ActiveRecord::Base.connection.execute <<-QUERY 51 | INSERT into employer_contributions(recipient_id, employer_id, name, contrib, amount) 52 | SELECT r.id, e.id, r.name, e.employer_name, sum(amount) as amount 53 | FROM contributions b, parties r, parties c, employers e 54 | WHERE b.recipient_id = r.id AND b.contributor_id = c.id AND e.id = c.employer_id 55 | AND r.committee_id in (#{Party::MAYORAL_CANDIDATE_IDS.join ','}) 56 | GROUP BY e.id, r.id 57 | QUERY 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /assets/js/views/_contributorsView.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.ContributorsView = Backbone.View.extend({ 2 | template: _.template('\ 3 |
\ 4 |

<%= headline %>

\ 5 |
\ 6 |
\ 7 |
<%= leftContributions %>
\ 8 |
<%= rightContributions %>
\ 9 |
'), 10 | 11 | contributionTemplate: _.template('\ 12 | '), 19 | 20 | contributionTemplateNoDate: _.template('\ 21 | '), 27 | 28 | initialize: function(options) { 29 | this.headline = options.headline; 30 | this.showDate = options.showDate; 31 | 32 | _.bindAll(this, 'renderContribution'); 33 | this.listenTo(this.collection, 'sync', this.render); 34 | 35 | if (this.collection.length > 0) { 36 | this.render(); 37 | } 38 | }, 39 | 40 | render: function() { 41 | var half = Math.round(this.collection.length/2); 42 | var left = this.collection.toArray().slice(0,half); 43 | var right = this.collection.toArray().slice(half); 44 | 45 | this.$el.html(this.template({ 46 | headline : this.headline, 47 | leftContributions : left.map(this.renderContribution).join(''), 48 | rightContributions : right.map(this.renderContribution).join('') 49 | })); 50 | }, 51 | 52 | renderContribution: function(contribution) { 53 | var contribution = new OpenDisclosure.Contribution(contribution.attributes); 54 | var renderedContributions = ''; 55 | if (this.showDate) { 56 | renderedContributions = this.contributionTemplate({ 57 | contribution: contribution 58 | }) 59 | } else { 60 | renderedContributions = this.contributionTemplateNoDate({ 61 | contribution: contribution 62 | }) 63 | } 64 | 65 | return renderedContributions; 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /assets/js/views/about.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.About = Backbone.View.extend({ 2 | team: [ 3 | { 4 | "name" : "Lauren Angius", 5 | "github" : "https://github.com/lla2105" 6 | }, 7 | { 8 | "name" : "Tom Dooner", 9 | "github" : "https://github.com/tdooner" 10 | }, 11 | { 12 | "name" : "Mike Ubell", 13 | "github" : "https://github.com/mikeubell" 14 | }, 15 | { 16 | "name" : "Elina Rubuliak", 17 | "github" : "https://github.com/elinaru" 18 | }, 19 | { 20 | "name" : "Kyle Warneck", 21 | "github" : "https://github.com/KyleW" 22 | }, 23 | { 24 | "name" : "Vivian Brown", 25 | "github" : "https://github.com/vbrown608" 26 | }, 27 | { 28 | "name" : "Ian Root", 29 | "github" : "https://github.com/ianaroot" 30 | }, 31 | { 32 | "name" : "Klein Lieu", 33 | "github" : "https://github.com/kleinlieu" 34 | }, 35 | { 36 | "name" : "Amanda Richardson", 37 | "github" : "https://github.com/amandaric" 38 | }, 39 | { 40 | "name" : "Dave Guarino", 41 | "github" : "https://github.com/daguar" 42 | }, 43 | { 44 | "name" : "John Osborn", 45 | "github" : "https://github.com/bayreporta" 46 | }, 47 | { 48 | "name" : "Phil Wolff", 49 | "github" : "https://github.com/evanwolf" 50 | }, 51 | { 52 | "name" : "Ethan Lang", 53 | "github" : "https://github.com/ethanlang" 54 | }, 55 | { 56 | "name" : "Steve 'Spike' Spiker", 57 | "github" : "https://github.com/spjika" 58 | }, 59 | { 60 | "name" : "Luis Aguilar", 61 | "github" : "https://github.com/munners17" 62 | }, 63 | { 64 | "name" : "Emily Bookstein", 65 | "github" : "https://github.com/bookstein" 66 | }, 67 | { 68 | "name" : "Brian Ferrell", 69 | "github" : "https://github.com/endenizen" 70 | }, 71 | { 72 | "name" : "Edward Breen", 73 | "github" : "https://github.com/tedbreen" 74 | }, 75 | { 76 | "name" : "Timothy Kempf", 77 | "github" : "https://github.com/Fauntleroy" 78 | }, 79 | { 80 | "name" : "Ian Rees", 81 | "github" : "https://github.com/irees" 82 | }, 83 | { 84 | "name" : "Jonathan Wrobel", 85 | "github" : "https://github.com/jwrobes" 86 | }, 87 | { 88 | "name" : "Maggie Shine", 89 | "github" : "https://github.com/magshi" 90 | }, 91 | { 92 | "name" : "Sunny Juneja", 93 | "github" : "https://github.com/whatasunnyday" 94 | } 95 | ], 96 | 97 | template: HandlebarsTemplates['about'], 98 | 99 | initialize: function() { 100 | this.render(); 101 | }, 102 | 103 | render: function() { 104 | this.$el.html(this.template({team: this.team})); 105 | }, 106 | 107 | }); 108 | -------------------------------------------------------------------------------- /assets/js/vendor/accounting.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * accounting.js v0.3.2, copyright 2011 Joss Crowcroft, MIT license, http://josscrowcroft.github.com/accounting.js 3 | */ 4 | (function(p,z){function q(a){return!!(""===a||a&&a.charCodeAt&&a.substr)}function m(a){return u?u(a):"[object Array]"===v.call(a)}function r(a){return"[object Object]"===v.call(a)}function s(a,b){var d,a=a||{},b=b||{};for(d in b)b.hasOwnProperty(d)&&null==a[d]&&(a[d]=b[d]);return a}function j(a,b,d){var c=[],e,h;if(!a)return c;if(w&&a.map===w)return a.map(b,d);for(e=0,h=a.length;ea?"-":"",g=parseInt(y(Math.abs(a||0),h),10)+"",l=3a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal))};c.formatColumn=function(a,b,d,i,e,h){if(!a)return[];var f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format),l=g.pos.indexOf("%s")a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal));if(a.length>k)k=a.length;return a});return j(a,function(a){return q(a)&&a.length 2 |

Campaign Finance for the 2014 Oakland Mayoral Election

3 | 4 | 5 |

{{short_name}}

6 | 7 | {{#if summary}} 8 |
9 |
10 | Total Contributions
11 | {{summary.totalContributions}} 12 | 13 |
14 |
Expenditures
15 | {{summary.totalExpenditures}} 16 | = 17 |
18 |
Available Balance
19 | {{summary.availableBalance}} 20 |
21 |
No. of Contributions
22 | {{received_contributions_count}} 23 |
24 |
25 | {{/if}} 26 | 27 |
28 |
29 | 30 |

{{profession}}

31 |

Party Affiliation: {{party_affiliation}}

32 |

33 | 34 | {{twitter}} 35 |

36 |
37 | 38 |
39 |

{{bio}}

40 |
41 | Sources
42 | {{#each sources}} 43 | {{name}}
44 | {{/each}} 45 |
46 |
47 | 48 |
49 | {{#if summary}} 50 |

Percentage of total contributions that are small contributions*: {{summary.pctSmallContributions}}

51 |

Personal funds loaned and contributed to campaign: {{friendlyMoney self_contributions_total}}

52 |

% of the total amount raised is personal funds: {{summary.pctPersonalContributions}}

53 |

Declared candidacy: {{declared}}

54 |

Data last updated: {{lastUpdatedDate}}

55 |

* Candidates do not need to itemize contributions less than $100 by contributor, but do need to include all contributions in their total reported amount. FAQ

56 | {{else}} 57 |
The candidate had not reported any campaign contributions by the last filing deadline. Candidates are not required to report if they have raised less than $1,000.
58 | {{/if}} 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /pie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Backbone.d3 - Pie charts 4 | 5 | 6 | 7 | 8 | 9 | 69 | 70 | 71 |
72 |

Pie charts

73 |

Backbone-d3 currently supports simple pie charts with transitions. 74 | Changing values in a collection causes the pie chart to update to the 75 | new ratios. Adding a value adds a new segment to the pie chart, and 76 | removing a value removes that segment.

77 |
78 | 79 | 80 | 83 | 86 | 87 |
81 |

Fixed length series

82 |
84 |

Variable length series

85 |
88 | 89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /assets/js/views/_chartsWrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Original author: David Eads (https://github.com/eads) 3 | * 4 | * Wrap D3 charting components in a simple Backbone view interface 5 | * 6 | * Provides a redrawing path, data sync, and fallback for non-d3 browsers. 7 | * 8 | * Views that extend ChartView should implement their own "draw" function and go to work. 9 | * 10 | * var collection = new Backbone.Collection([ ["Maria", 33], ["Heather", 29] ]); 11 | * var view = new MyChartView({ $el: $("#topper-chart"), collection: collection}); 12 | * 13 | **/ 14 | 15 | OpenDisclosure.ChartView = Backbone.View.extend({ 16 | constructor: function(options) { 17 | this.default_options = { 18 | base_width: 700, 19 | aspect: .6, 20 | margin: { 21 | top: 10, 22 | right: 0, 23 | bottom: 15, 24 | left: 0 25 | }, 26 | type: "" 27 | }; 28 | 29 | this.options = $.extend(true, this.default_options, options); 30 | 31 | var breakpoints = _.pairs(this.options.breakpoints); 32 | this.options.breakpoints = _.sortBy(breakpoints, function(item) { 33 | return Number(item[0]); 34 | }); 35 | 36 | // Fallback if d3 is unavailable, add some formatters otherwise. 37 | if (!this.d3) { 38 | this.draw = this.fallback_draw; 39 | } else { 40 | this.formatNumber = d3.format(".lf"); 41 | this.formatCommas = d3.format(","); 42 | this.formatPercent = d3.format("%"); 43 | } 44 | Backbone.View.apply(this, arguments); 45 | }, 46 | initialize: function(options) { 47 | this.get_dimensions(); 48 | this.render(); 49 | 50 | $(window).on("resize", function() { 51 | this.resize(); 52 | }.bind(this)); 53 | }, 54 | resize: function() { 55 | this.get_dimensions(); 56 | this.$el.find('svg').attr("width", this.dimensions.width); 57 | this.$el.find('svg').attr("height", this.dimensions.height); 58 | }, 59 | get_dimensions: function() { 60 | var window_width = $(window).width(); 61 | 62 | var wrapperWidth = this.$el.width(); 63 | var width = wrapperWidth - this.options.margin.left - this.options.margin.right; 64 | var height = wrapperWidth*this.options.aspect - this.options.margin.bottom - this.options.margin.top; 65 | 66 | wrapperHeight = height + this.options.margin.top + this.options.margin.bottom; 67 | 68 | this.dimensions = { 69 | width: width, 70 | height: height, 71 | wrapperWidth: wrapperWidth, 72 | wrapperHeight: wrapperHeight 73 | }; 74 | }, 75 | // The render function wraps drawing with responsivosity 76 | render: function() { 77 | this.$el.empty(); 78 | this.get_dimensions(); 79 | this.draw(this.el); 80 | }, 81 | draw: function() { 82 | console.log("override ChartView's draw function with your d3 code"); 83 | return this; 84 | }, 85 | fallback_draw: function() { 86 | this.$el.html( 87 | '

Warning! You are using an unsupported browser. ' + 88 | 'Please upgrade to Chrome, Firefox, or Internet Explorer version 9 or higher to view ' + 89 | 'charts on this site.

'); 90 | return this; 91 | }, 92 | d3: function() { 93 | return (typeof d3 !== 'undefined'); 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (4.1.0) 5 | activesupport (= 4.1.0) 6 | builder (~> 3.1) 7 | activerecord (4.1.0) 8 | activemodel (= 4.1.0) 9 | activesupport (= 4.1.0) 10 | arel (~> 5.0.0) 11 | activesupport (4.1.0) 12 | i18n (~> 0.6, >= 0.6.9) 13 | json (~> 1.7, >= 1.7.7) 14 | minitest (~> 5.1) 15 | thread_safe (~> 0.1) 16 | tzinfo (~> 1.1) 17 | arel (5.0.1.20140414130214) 18 | backports (3.6.0) 19 | builder (3.2.2) 20 | coderay (1.1.0) 21 | coffee-script (2.3.0) 22 | coffee-script-source 23 | execjs 24 | coffee-script-source (1.7.1) 25 | diff-lcs (1.2.5) 26 | dotenv (0.7.0) 27 | execjs (2.2.1) 28 | factory_girl (4.4.0) 29 | activesupport (>= 3.0.0) 30 | foreman (0.67.0) 31 | dotenv (~> 0.7.0) 32 | thor (~> 0.17.0) 33 | haml (4.0.5) 34 | tilt 35 | handlebars_assets (0.17.1) 36 | execjs (>= 1.2.9) 37 | multi_json 38 | sprockets (>= 2.0.3) 39 | tilt 40 | hike (1.2.3) 41 | i18n (0.6.9) 42 | json (1.8.1) 43 | libv8 (3.16.14.7) 44 | method_source (0.8.2) 45 | minitest (5.3.3) 46 | multi_json (1.9.3) 47 | pg (0.17.1) 48 | pry (0.9.12.6) 49 | coderay (~> 1.0) 50 | method_source (~> 0.8) 51 | slop (~> 3.4) 52 | rack (1.5.2) 53 | rack-protection (1.5.3) 54 | rack 55 | rack-test (0.6.2) 56 | rack (>= 1.0) 57 | rake (10.3.2) 58 | rb-readline (0.5.1) 59 | ref (1.0.5) 60 | rspec (3.0.0) 61 | rspec-core (~> 3.0.0) 62 | rspec-expectations (~> 3.0.0) 63 | rspec-mocks (~> 3.0.0) 64 | rspec-core (3.0.3) 65 | rspec-support (~> 3.0.0) 66 | rspec-expectations (3.0.3) 67 | diff-lcs (>= 1.2.0, < 2.0) 68 | rspec-support (~> 3.0.0) 69 | rspec-mocks (3.0.3) 70 | rspec-support (~> 3.0.0) 71 | rspec-support (3.0.3) 72 | sass (3.3.9) 73 | sinatra (1.4.5) 74 | rack (~> 1.4) 75 | rack-protection (~> 1.4) 76 | tilt (~> 1.3, >= 1.3.4) 77 | sinatra-asset-pipeline (0.4.0) 78 | coffee-script 79 | rake 80 | sass 81 | sinatra 82 | sprockets 83 | sprockets-helpers 84 | sprockets-sass 85 | sinatra-contrib (1.4.2) 86 | backports (>= 2.0) 87 | multi_json 88 | rack-protection 89 | rack-test 90 | sinatra (~> 1.4.0) 91 | tilt (~> 1.3) 92 | sitemap_generator (5.0.5) 93 | builder 94 | slop (3.5.0) 95 | sprockets (2.12.1) 96 | hike (~> 1.2) 97 | multi_json (~> 1.0) 98 | rack (~> 1.0) 99 | tilt (~> 1.1, != 1.3.0) 100 | sprockets-helpers (1.1.0) 101 | sprockets (~> 2.0) 102 | sprockets-sass (1.2.0) 103 | sprockets (~> 2.0) 104 | tilt (~> 1.1) 105 | therubyracer (0.12.1) 106 | libv8 (~> 3.16.14.0) 107 | ref 108 | thor (0.17.0) 109 | thread_safe (0.3.3) 110 | tilt (1.4.1) 111 | tzinfo (1.1.0) 112 | thread_safe (~> 0.1) 113 | 114 | PLATFORMS 115 | ruby 116 | 117 | DEPENDENCIES 118 | activerecord 119 | dotenv 120 | factory_girl 121 | foreman 122 | haml 123 | handlebars_assets 124 | pg 125 | pry 126 | rake 127 | rb-readline 128 | rspec 129 | sinatra 130 | sinatra-asset-pipeline 131 | sinatra-contrib 132 | sitemap_generator 133 | therubyracer 134 | -------------------------------------------------------------------------------- /assets/js/templates/about.hbs: -------------------------------------------------------------------------------- 1 | Fork me on GitHub 2 |
3 |

About the Open Disclosure Project

4 | 5 |

6 | Open Disclosure is a group of volunteer civic hackers passionate about open data, transparency and shining light on the funding that fuels Oakland’s electoral campaigns. 7 |

8 | 9 |

10 | Beginning in 2013 the Oakland Public Ethics Commission partnered with OpenOakland to form the Open Disclosure team for the purpose of ensuring government integrity and transparency in campaign activities by opening up campaign data. Disclosure of campaign finance empowers citizens and strengthens democracy because it keeps government accountable by revealing influential connections, the flow of money in politics, and potential issues with how campaign funds are raised or spent. Open Disclosure’s goal is to introduce a high standard of clarity and transparency to the disclosure of Oakland’s local campaign finance so that the public can understand how local campaigns are financed. 11 |

12 | 13 |

14 | Open Disclosure is a project of OpenOakland, a Code for America citizen brigade that works to improve the lives of Oaklanders by advancing civic innovation and open government through community partnerships, engaged volunteers, and civic technology. OpenDisclosure.io is our team’s site established to easily visualize and simplify campaign finance data for Oakland elections. We want to make this local campaign contribution and spending data easier to find, see, search and understand. We update this data as the local mayoral campaigns submit their latest campaign finance filings to NetFile, Oakland’s campaign finance disclosure database, according to filing deadlines set forth by the California Fair Political Practices Commission (FPPC). We hope Open Disclosure sheds some light on the dynamic financial foundation that fuels the campaigns of Oakland candidates. Please contact our team if you have any questions: oaklandopendisclosure@gmail.com. 15 |

16 | 17 |

18 | All development on OpenDisclosure happens publicly on Github. We invite any questions or suggestions for improvement–just create an issue on Github! 19 |

20 |

Open Disclosure Team

21 | {{#list team}}{{/list}} 22 | {{!-- {{#list team}}
  • {{name}}
  • {{/list}} --}} 23 | {{!-- <% _.each(teammates, function(m) { %> 24 |
  • <%= m.name %>
  • 25 | <% }) %> --}} 26 | ...and thanks to everyone else in the OpenOakland Code for America Brigade for helping us out. 27 |
    28 | -------------------------------------------------------------------------------- /backend/fetchers/iec.rb: -------------------------------------------------------------------------------- 1 | require 'backend/fetchers/base' 2 | 3 | class DataFetcher::IEC < DataFetcher::Base 4 | def validate_row(row) 5 | # the 496 and 465 have differnt column names 6 | row['expn_date'] = row['exp_date'] if row['exp_date'] 7 | 8 | # attempt to find the recipient during validation so we can give good error 9 | # feedback; stick it on the row object so we don't have to do the costly 10 | # queries again during row processing 11 | row['_od_recipient'] = get_recipient(row) 12 | 13 | return 'No recipient found' unless row['_od_recipient'] 14 | return 'Outside jurisdiction' if !row['juris_dscr'].nil? && !/oakland/i.match(row['juris_dscr']) 15 | return 'Outside jurisdiction' if !row['bal_juris'].nil? && !/oakland/i.match(row['bal_juris']) 16 | end 17 | 18 | def parse_row(row) 19 | contributor = get_filer(row) 20 | 21 | # Add the entry into the IEC table. 22 | ::IEC.where(recipient: row['_od_recipient'], contributor: contributor, 23 | transaction_id: row['tran_id'], date: row['expn_date'], 24 | amount: row['amount'], 25 | description: row['expn_dscr'].nil? ? row[',payyee_naml'] : row['expn_dscr'], 26 | support: row['sup_opp_cd'] == "S").first_or_create() 27 | 28 | # If its an opposition expenditure we are done. 29 | return if row['sup_opp_cd'] == "O" 30 | 31 | # Add this as if it were a contribution supporting the issue/candidate. 32 | ::Contribution.where(recipient: row['_od_recipient'], transaction_id: row['tran_id']).first_or_create( 33 | contributor: contributor, 34 | amount: row['amount'], 35 | date: row['expn_date'], 36 | type: 'independent' 37 | ) 38 | end 39 | 40 | private 41 | 42 | def get_recipient(row) 43 | if row['bal_num'].nil? 44 | # The data only has candidate names. 45 | # Sometimes they are only in one of the name fields. 46 | # Try to match first on both names with the year of the campaign as this tends to 47 | # be in the title of the comittee. It would be nice to use the elect_date field 48 | # but this does not seem to be entered so we use the exp_date. 49 | # 50 | # The Kaplan committee does not have her first name in the title 51 | # but an IEC does, avoid having it give money to itself. 52 | if row['cand_naml'] == 'Kaplan' && row['expn_date'][0, 4] == '2014' then 53 | search = 'Kaplan for Oakland Mayor 2014' 54 | else 55 | search = "%" + (row['cand_namf'].nil? ? "" : row['cand_namf']) + 56 | (row['cand_naml'].nil? ? "" : row['cand_naml']) + 57 | "%" + row['expn_date'][0, 4] + "%" 58 | end 59 | 60 | recipient = Party::Committee.where("lower(name) like lower(?)", search).first 61 | return recipient if recipient 62 | 63 | # Can't match with the year, try without. 64 | search = "%" + (row['cand_namf'].nil? ? "" : row['cand_namf']) + 65 | (row['cand_naml'].nil? ? "" : row['cand_naml']) + "%" 66 | recipient = Party::Committee.where("lower(name) like lower(?)", search).first 67 | return recipient if recipient 68 | 69 | search = "%" + row['cand_naml'] + "%" + row['expn_date'][0, 4] + "%" 70 | recipient = Party::Committee.where("lower(name) like lower(?)", search).first 71 | return recipient if recipient 72 | 73 | # Can't match with the year, try without. 74 | search = "%" + row['cand_naml'] + "%" 75 | recipient = Party::Committee.where("lower(name) like lower(?)", search).first 76 | return recipient if recipient 77 | else 78 | Party::Committee 79 | .where(name: "Measure " + row['bal_num'] + " " + row['expn_date'][0, 4]) 80 | .first_or_create(committee_id: -1) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /installBackEnd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "This script will attempt to install the OpenDisclosure back end in a Vagrant box" 4 | 5 | sudo apt-get update 6 | sudo apt-get -y install curl 7 | sudo apt-get -y install ruby-full git 8 | sudo gem install -y bundler 9 | sudo gem install -y foreman 10 | sudo aptitude install -y postgresql 11 | #this line gives an error: 12 | ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents 13 | #ln: failed to create symbolic link ‘/home/vagrant/Library/LaunchAgents’: No such file or directory 14 | #couldn't find ref to "ln" in history from last week... 15 | #checking the week before last week ... same line was used then 16 | #trying install anyway, if it works, erase the line about the ln 17 | sudo ARCHFLAGS="-arch x86_64" gem install pg 18 | #got error here too: 19 | # ERROR: Error installing pg: 20 | # ERROR: Failed to build gem native extension. 21 | 22 | # /usr/bin/ruby1.9.1 extconf.rb 23 | # checking for libpq-fe.h... no 24 | # Can't find the 'libpq-fe.h header 25 | # Can't find the 'libpq-fe.h header 26 | # *** extconf.rb failed *** 27 | # Could not create Makefile due to some reason, probably lack of 28 | # necessary libraries and/or headers. Check the mkmf.log file for more 29 | # details. You may need configuration options. 30 | git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 31 | git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build 32 | echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile 33 | echo 'eval "$(rbenv init -)"' >> ~/.bash_profile 34 | source .bash_profile 35 | #sudo apt-get install libopenssl-devel libreadline-devel 36 | #sudo apt-get install openssl-devel readline-devel 37 | #it appears the names of the preceeding packages are wrong 38 | # Reading package lists... Done 39 | # Building dependency tree 40 | # Reading state information... Done 41 | # E: Unable to locate package libopenssl-devel 42 | # E: Unable to locate package libreadline-devel 43 | echo "installing libssl-dev libreadline-dev g++ libpq-dev ... " 44 | sudo apt-get -y install libssl-dev libreadline-dev g++ libpq-dev 45 | 46 | echo "Running rbenv install 2.1.2" 47 | rbenv install 2.1.2 48 | rbenv global 2.1.2 49 | 50 | echo "installing the pg gem" 51 | gem install pg -v '0.17.1' 52 | 53 | echo "installing bundler" 54 | gem install bundler 55 | 56 | echo "Cloning the opendisclosure git repo..." 57 | 58 | git clone https://github.com/openoakland/opendisclosure.git 59 | 60 | cd opendisclosure/ 61 | source ~/.bash_profile 62 | 63 | echo "Installing the bundle..." 64 | bundle install 65 | #old error here: Your Ruby version is 1.9.3, but your Gemfile specified 2.1.2 66 | 67 | #gem install pg -v '0.17.1' 68 | #did this in one of the preceeding lines 69 | 70 | #sudo -u postgres createuser postgres -P 71 | #the preceeding line likely is not necessarry and creates this error after entering a space as the password 72 | #createuser: creation of new role failed: ERROR: role "postgres" already exists 73 | 74 | echo "Preparing the database" 75 | sudo -u postgres createuser -d -s vagrant 76 | 77 | echo DATABASE_URL="postgres://$USER:password@localhost/vagrant" > .env 78 | 79 | sudo -u postgres psql -c "ALTER USER vagrant WITH PASSWORD 'password';" 80 | sudo service postgresql restart 81 | createdb vagrant 82 | 83 | source ~/.bash_profile 84 | #load the data 85 | echo "Loading the data ... " 86 | bundle exec ruby backend/load_data.rb 87 | 88 | echo "All installation steps complete or attempted." 89 | echo "If all went without error, you should be able to run these commands" 90 | echo "> source ~/.bash_profile" 91 | echo "> cd opendisclosure" 92 | echo "> foreman start" 93 | echo "then enter 127.0.0.1:5000 into the address bar in an internet" 94 | echo "browser on this computer and see the Open Disclosure homepage." 95 | -------------------------------------------------------------------------------- /README_installation_in_vagrant.md: -------------------------------------------------------------------------------- 1 | Open Disclosure backend installation in Vagrant in 6 steps 2 | ================= 3 | 4 | Scripts to build Open Disclosure's back end in a Vagrant virtual machine. 5 | This has been tested on a Mac running Yosemite. From what I understand, Virtual box and Vagrant work in Windows as well, and most of the commands are the same, though, I have not tested it in Windows. 6 | 7 | To install the backend: 8 | 9 | ####1) Install Virtual box and Vagrant. 10 | 11 | See [https://www.virtualbox.org/](https://www.virtualbox.org/) for virtual box. 12 | 13 | See [http://www.vagrantup.com/](http://www.vagrantup.com/) for Vagrant. 14 | 15 | ----------------------- 16 | ###Three important notes on Vagrant usage: 17 | 18 | ######a) Never run Vagrant commands from your home machine as root :) 19 | ie, if you mistakenly run anything with the pattern 20 | 21 | host machine prompt> sudo vagrant xxx 22 | 23 | you will probably need to read [this](http://stackoverflow.com/questions/25652769/should-vagrant-require-sudo-for-each-command) to make everything work right again. 24 | 25 | Running code as root inside of the Vagrant vitual machine, ( ie: after >vagrant ssh and before ctrl-d ), is fine. 26 | 27 | ######b) Starting the Vagrant virtual machine after it is shut down 28 | To restart a virtual box, you must first go to the directory where you first initilized and installed the Vagrant box, then use the 'vagrant up' command. If you type this command: 29 | 30 | host machine prompt> vagrant up 31 | 32 | in the wrong directory it will cause much confusion and problems. 33 | 34 | ######c) Re-installing the virtual box 35 | If you are re-installing the back end and wish to re-install the vagrant instance, you need to first destroy the previously installed Vagrant box using these commands, run from the directory where the original Vagrant box was installed. 36 | 37 | > vagrant destroy 38 | 39 | Then remove the Vagrant config file found in the installation directory (it is named "Vagrantfile", no extension). 40 | 41 | > rm Vagrantfile 42 | 43 | ----------------------- 44 | ####2) Setup the Vagrant folder 45 | 46 | ######a) Copy this git repository to a folder on your computer. 47 | 48 | ######b) Navigate to the git repository's folder; this will be used as the Vagrant install folder. 49 | 50 | ####3) Establish the Virtual box and install the server 51 | At a terminal command prompt run these commands: 52 | 53 | host machine prompt> vagrant up 54 | host machine prompt> vagrant ssh 55 | #note: the previous command will take you into the vagrant virtual machine and give you a guest machine prompt 56 | guest machine prompt> cp /vagrant/installBackEnd.sh ~ 57 | guest machine prompt> sudo chmod 755 ~/installBackEnd.sh 58 | guest machine prompt> ./installBackEnd.sh 59 | 60 | After the installation finishes and the guest machine prompt returns, run this command: 61 | 62 | guest machine prompt> source ~/.bash_profile 63 | 64 | The server should now be installed and the data loaded. 65 | 66 | ####4) Start the server 67 | 68 | guest machine prompt> cd ~/opendisclosure/ 69 | guest machine prompt> foreman start 70 | 71 | ####5) Check the installation 72 | The server should now be running. To check that everything was successfull open an internet browser window and enter this in the address bar: 73 | 74 | 127.0.0.1:5000 75 | 76 | The Open Disclosure site should come up, and should look pretty much just like the current running [Open Disclosure website](http://www.opendisclosure.io/). 77 | 78 | ####6) Shutting everything down 79 | It is advised that you do not leave a virtual machine running when it is not needed, as this can occasionally cause conflicts if other virtual machines are initilized which are located in associated directories or use the same ports. 80 | 81 | ######To stop the server: 82 | At the command prompt where the command '> foreman start command' was entered, press 'control + C'. 83 | 84 | ######To stop Vagrant: 85 | 86 | Enter 87 | 88 | 'control + D' 89 | 90 | to return to the host machine, then enter this command at the command line: 91 | 92 | host machine prompt> vagrant halt 93 | 94 | -------------------------------------------------------------------------------- /netfile-etl/README.md: -------------------------------------------------------------------------------- 1 | # Netfile ETL 2 | 3 | A simple ETL system to process .zip files from the [Netfile](https://www.netfile.com/) campaign finance filing system into individual CSVs per form. 4 | 5 | ## What It Does 6 | 7 | This is a simple set of Python scripts which does a few things: 8 | 9 | 1. Downloads and unzips the .zip files containing the campaign finance data from the folder Netfile set up for Oakland ([download_and_unzip_files.py](download_and_unzip_files.py)) 10 | 11 | 2. Extracts the individual sheets from each Excel workbook as standalone CSVs ([etl.py](etl.py)) 12 | 13 | 3. Combines the sheets for each "form" (for example, "A-Contributions") across all years, yielding a single CSV with all years' data for each individual form ([merge_csvs_across_years.py](merge_csvs_across_years.py)) 14 | 15 | These scripts replicate a similar process used by the San Francisco Ethics Commission to consolidate the campaign finance data and get it into a shape where it can be loaded onto its open data portal. 16 | 17 | ## Notes on Reuse 18 | 19 | These scripts can pretty easily be modified for your own use. You will need to do a few things: 20 | 21 | 1. Ask Netfile to set up a public web directory with the raw .zip files usually served via their form. 22 | 2. In [download_and_unzip_files.py](download_and_unzip_files.py), edit `remote_path` to reflect your own directory URL from Netfile. 23 | 3. Edit that same file, changing the 2011 in `years_with_data` if you want to start from a year other than 2011. 24 | 25 | ## Getting Started 26 | 27 | This is a set of Unix shell scripts intended to run on a \*nixy system like Linux or Mac OSX. It has been tested and works on both OSX 10.7 and Ubuntu 13.02. 28 | 29 | ### Installing Dependencies 30 | 31 | #### System Dependencies 32 | 33 | There are two system dependencies: `curl` and `unzip`. 34 | 35 | Use your local package management system to install these. For example, on Ubuntu, do this by running: 36 | 37 | ``` 38 | apt-get install curl 39 | apt-get install unzip 40 | ``` 41 | 42 | (On OSX using Homebrew is recommended.) 43 | 44 | #### Python Dependencies 45 | 46 | These are Python scripts and so require that language to be installed (it has been tested with 2.7.) 47 | 48 | Additionally, you will need to install `csvkit`. The easiest way to do this is using the `pip` Python package management software. 49 | 50 | On a fresh Ubuntu system, you can do this by running: 51 | 52 | ``` 53 | apt-get install python-pip 54 | pip install csvkit 55 | ``` 56 | 57 | ### Running 58 | 59 | You can run the scripts by running the `run_all.sh` script. 60 | 61 | ### Cron Note 62 | 63 | When running as a cron job, we encountered an issue of cron being unable to find the `in2csv` program. We fixed this by modifying that part of the script, changing "in2csv" on line 15 to the full path of where it was installed, such as "/opt/local/in2csv". (You can find the full path by running `which in2csv`.) 64 | 65 | ### Running on Windows with Vagrant 66 | 67 | You can run these scripts on a Windows computer by using the excellent Vagrant virtual machine stack. For instructions, [click here](https://github.com/daguar/netfile-etl/issues/2). 68 | 69 | ## Oakland's Setup 70 | 71 | The current (alpha) setup in Oakland is:1 72 | 73 | 1. The scripts are run nightly via a cron job on an Ubuntu virtual machine, running on a city staffer's Windows desktop using the wonderful Vagrant (see above) 74 | 2. The scripts dump the data to a folder shared with the Windows host machine (via the `/vagrant` folder in the virtual machine) 75 | 3. We use [Socrata DataSync](http://support.socrata.com/entries/24241271-Setting-up-a-basic-DataSync-job) to upload the CSVs from that local folder to the Socrata open data portal 76 | 4. Windows Task Scheduler is used (see [tutorial here](http://support.socrata.com/entries/24234461-Scheduling-a-DataSync-job-using-Windows-Task-Scheduler)) to automatically re-upload new files every day 77 | 78 | ## Copyright & License 79 | 80 | Copyright Dave Guarino, 2014 81 | BSD License 82 | 83 | This code was written by Dave (OpenOakland) in partnership with the City of Oakland, CA's Public Ethics Commission. Thanks go to the PEC's Lauren Angius for being willing to try an open source approach to this problem. 84 | -------------------------------------------------------------------------------- /assets/css/_sass-enhance.css.scss: -------------------------------------------------------------------------------- 1 | // To configure your breakpoints, set the $breakpoint-max-widths variable 2 | // before importing sass-enhance. 3 | // 4 | // This variable is a comma separated list of breakpoint names and max-width 5 | // pairs. You can choose whatever names and widths you prefer. 6 | $breakpoint-max-widths: extra-small 767px, 7 | small 991px, 8 | medium 1199px, 9 | large 99999px !default; 10 | 11 | // Helper method to find the width of the named breakpoint for the given edge 12 | // (i.e. min-width or max-width) 13 | @function _breakpoint-width($breakpoint, $edge: min) { 14 | // Because we are only listing breakpoints and their max-widths, we need to 15 | // store the width of the previous breakpoint + 1 here. Since we haven't 16 | // started running through the breakpoints yet, we want it to start at 0px. 17 | $min-width: 0px; 18 | @each $breakpoint-max-width in $breakpoint-max-widths { 19 | $breakpoint-name: nth($breakpoint-max-width, 1); 20 | $breakpoint-px: nth($breakpoint-max-width, 2); 21 | @if $breakpoint-name == $breakpoint { 22 | // We found the requested breakpoint in our list of breakpoints and 23 | // max-widths 24 | @if ($edge == min) { 25 | @return $min-width; 26 | } @else { 27 | @return $breakpoint-px; 28 | } 29 | } @else { 30 | // We have not found the requested breakpoint in our list of breakpoints 31 | // and max-widths yet, so we need to store the width of the current 32 | // breakpoint + 1px as the min-width of the next breakpoint in our 33 | // iteration. 34 | $min-width: $breakpoint-px + 1px; 35 | } 36 | } 37 | // The requested breakpoint was not in our list of breakpoints and 38 | // max-widths, so we want to return the breakpoint as-is. This allows us to 39 | // enhance or degrade for arbitrary viewport widths. 40 | @return $breakpoint; 41 | } 42 | 43 | // Convenience method that returns the min-width of the named breakpoint 44 | @function _breakpoint-min-width($breakpoint-name) { 45 | @return _breakpoint-width($breakpoint-name, min); 46 | } 47 | 48 | // Convenience method that returns the max-width of the named breakpoint 49 | @function _breakpoint-max-width($breakpoint-name) { 50 | @return _breakpoint-width($breakpoint-name, max); 51 | } 52 | 53 | // Returns a parsed version of the breakpoint provided to enhance() or 54 | // degrade() 55 | @function _parse-breakpoint($breakpoint) { 56 | @if length($breakpoint) == 3 and nth($breakpoint, 2) == 'until' { 57 | // ranged breakpoint in the form of "breakpoint-a until breakpoint-b" 58 | @return (nth($breakpoint, 1) nth($breakpoint, 3)); 59 | } @else { 60 | // non-ranged breakpoint 61 | @return $breakpoint; 62 | } 63 | } 64 | 65 | // Accepts a breakpoint (non-ranged or ranged) and a block of styles. Wraps the 66 | // block of styles in a media query that applies the styles as the viewport 67 | // gets wider. This can be used to progressively enhance a page. 68 | @mixin enhance($breakpoint) { 69 | $breakpoint: _parse-breakpoint($breakpoint); 70 | @if length($breakpoint) == 1 { 71 | // non-ranged breakpoint 72 | @media only screen and (min-width: _breakpoint-min-width($breakpoint)) { 73 | @content; 74 | } 75 | } @else { 76 | // ranged breakpoint 77 | $from: nth($breakpoint, 1); 78 | $until: nth($breakpoint, 2); 79 | @media only screen and 80 | (min-width: _breakpoint-min-width($from)) and 81 | (max-width: _breakpoint-min-width($until) - 1) { 82 | @content; 83 | } 84 | } 85 | } 86 | 87 | // Accepts a breakpoint (non-ranged or ranged) and a block of styles. Wraps the 88 | // block of styles in a media query that applies the styles as the viewport 89 | // gets narrower. This can be used to gracefully degrade a page. 90 | @mixin degrade($breakpoint) { 91 | $breakpoint: _parse-breakpoint($breakpoint); 92 | @if length($breakpoint) == 1 { 93 | // non-ranged breakpoint 94 | @media only screen and (max-width: _breakpoint-max-width($breakpoint)) { 95 | @content; 96 | } 97 | } @else { 98 | // ranged breakpoint 99 | $from: nth($breakpoint, 1); 100 | $until: nth($breakpoint, 2); 101 | @media only screen and 102 | (max-width: _breakpoint-max-width($from)) and 103 | (min-width: _breakpoint-max-width($until) + 1) { 104 | @content; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.App = Backbone.Router.extend({ 2 | routes: { 3 | '': 'home', 4 | 'about': 'about', 5 | 'candidate/:name': 'candidate', 6 | 'faq':'faq', 7 | 'rules': 'rules', 8 | 'iec': 'iec', 9 | 'contributor/:id': 'contributor', 10 | 'employer/:employer_name/:employer_id': 'employer', 11 | 'search/:name': 'search', 12 | 'searchCommittee/:name': 'searchCommittee' 13 | }, 14 | 15 | initialize : function() { 16 | // Store all the data globally, as a convenience. 17 | // 18 | // We should try to minimize the amount of data we need to fetch here, 19 | // since each fetch makes an HTTP request. 20 | OpenDisclosure.Data = { 21 | employerContributions: new OpenDisclosure.EmployerContributions(), 22 | categoryContributions: new OpenDisclosure.CategoryContributions(), 23 | categoryPayments: new OpenDisclosure.CategoryPayments(), 24 | whales: new OpenDisclosure.Whales(), 25 | multiples: new OpenDisclosure.Multiples(), 26 | independentExpends: new OpenDisclosure.IECs(), 27 | zipContributions: $.getJSON("/api/contributions/zip"), 28 | dailyContributions: $.getJSON("/api/contributions/over_time") 29 | }; 30 | 31 | // Call fetch on each Backbone.Collection 32 | for (var dataset in OpenDisclosure.Data) { 33 | if (typeof OpenDisclosure.Data[dataset].fetch === "function") { 34 | OpenDisclosure.Data[dataset].fetch(); 35 | } 36 | } 37 | 38 | OpenDisclosure.Data.candidates = new OpenDisclosure.Candidates(OpenDisclosure.BootstrappedData.candidates); 39 | 40 | Backbone.history.start({ pushState: true }); 41 | }, 42 | 43 | home: function(){ 44 | $(window).scrollTop(0); 45 | new OpenDisclosure.Views.Home({ 46 | el: '.main' 47 | }); 48 | }, 49 | 50 | about: function () { 51 | $(window).scrollTop(0); 52 | new OpenDisclosure.Views.About({ 53 | el: '.main' 54 | }); 55 | }, 56 | 57 | candidate: function(name){ 58 | $(window).scrollTop(0); 59 | new OpenDisclosure.Views.Candidate({ 60 | el: '.main', 61 | candidateName: name 62 | }); 63 | }, 64 | 65 | rules: function () { 66 | $(window).scrollTop(0); 67 | new OpenDisclosure.Views.Rules({ 68 | el: '.main' 69 | }); 70 | }, 71 | 72 | iec: function () { 73 | $(window).scrollTop(0); 74 | new OpenDisclosure.Views.IECView({ 75 | el: '.main', 76 | collection: OpenDisclosure.Data.independentExpends 77 | }); 78 | }, 79 | 80 | contributor : function(id) { 81 | $(window).scrollTop(0); 82 | new OpenDisclosure.Views.Contributor({ 83 | el: '.main', 84 | contributorId: id 85 | }); 86 | }, 87 | 88 | employer : function(employer_name, employer_id) { 89 | $(window).scrollTop(0); 90 | new OpenDisclosure.Views.Employees({ 91 | el: '.main', 92 | employer_id: employer_id, 93 | headline: employer_name 94 | }); 95 | }, 96 | 97 | search : function(name) { 98 | $(window).scrollTop(0); 99 | new OpenDisclosure.Views.Contributor({ 100 | el: '.main', 101 | search: name 102 | }); 103 | }, 104 | 105 | searchCommittee: function(name){ 106 | $(window).scrollTop(0); 107 | new OpenDisclosure.Views.Committee({ 108 | el: '.main', 109 | committeeName: name 110 | }); 111 | }, 112 | 113 | faq : function() { 114 | $(window).scrollTop(0); 115 | new OpenDisclosure.Views.Faq({ 116 | el: '.main' 117 | }); 118 | // For some reason the FAQ page does not go to the hash. 119 | // This code makes it do it. 120 | var tmp = location.hash; 121 | location.hash = ""; 122 | location.hash = tmp; 123 | }, 124 | 125 | handleLinkClicked: function(e) { 126 | var $link = $(e.target).closest('a'); 127 | 128 | if ($link.length) { 129 | var linkUrl = $link.attr('href'), 130 | externalUrl = linkUrl.indexOf('http') === 0, 131 | dontIntercept = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; 132 | 133 | if (externalUrl || dontIntercept) { 134 | return; 135 | } else { 136 | e.preventDefault(); 137 | } 138 | 139 | window.ga('send', 'pageview', linkUrl); 140 | 141 | this.navigate(linkUrl.replace(/^\//,''), { trigger: true }); 142 | } 143 | } 144 | }); 145 | 146 | $(function() { 147 | var app = new OpenDisclosure.App(); 148 | window.appNavigate = app.navigate; 149 | $(document).click(app.handleLinkClicked.bind(app)); 150 | }); 151 | -------------------------------------------------------------------------------- /backend/schema.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | 3 | ActiveRecord::Schema.define do 4 | ActiveRecord::Base.connection.tables.each do |table_to_drop| 5 | drop_table table_to_drop 6 | end 7 | 8 | create_table :parties do |t| 9 | t.string :type, null: false # Individual, Other, Committee 10 | t.string :name, null: false 11 | t.string :city 12 | t.string :state 13 | t.integer :zip 14 | t.integer :committee_id # 0 = pending 15 | t.string :employer 16 | t.integer :employer_id 17 | t.string :occupation 18 | 19 | t.integer :received_contributions_count, null: false, default: 0 20 | t.integer :received_contributions_from_oakland, null: false, default: 0 21 | t.integer :small_contributions, null: false, default: 0 22 | t.integer :contributions_count, null: false, default: 0 23 | t.integer :self_contributions_total, null: false, default: 0 24 | t.date :last_updated_date 25 | 26 | t.index [:committee_id, :type] 27 | t.index [:type, :name, :city, :state] 28 | t.index :last_updated_date 29 | end 30 | 31 | create_table :committee_maps do |t| 32 | t.string :filer_id 33 | t.string :name 34 | t.integer :committee_id 35 | 36 | t.index [:committee_id] 37 | end 38 | 39 | create_table :iecs do |t| 40 | t.string :transaction_id, null: false 41 | t.integer :contributor_id, null: false 42 | t.integer :recipient_id, null: false 43 | t.boolean :support 44 | t.integer :amount 45 | t.date :date 46 | t.string :description 47 | 48 | t.index [:recipient_id, :transaction_id] 49 | end 50 | 51 | create_table :employers do |t| 52 | t.string :employer_name, null: false 53 | 54 | t.index :employer_name 55 | end 56 | 57 | create_table :contributions do |t| 58 | t.string :transaction_id, null: false 59 | t.integer :contributor_id, null: false 60 | t.integer :recipient_id, null: false 61 | t.integer :amount 62 | t.date :date 63 | t.integer :type 64 | 65 | t.index :recipient_id 66 | t.index :contributor_id 67 | t.index [:recipient_id, :transaction_id] 68 | 69 | t.boolean :self_contribution, null: false, default: false 70 | end 71 | 72 | create_table :payments do |t| 73 | t.integer :payer_id, null: false 74 | t.integer :recipient_id, null: false 75 | t.integer :amount 76 | t.string :code 77 | t.date :date 78 | 79 | t.index :payer_id 80 | t.index :recipient_id 81 | end 82 | 83 | create_table :category_payments do |t| 84 | t.integer :payer_id 85 | t.string :text 86 | t.string :code 87 | t.integer :amount 88 | 89 | t.index :payer_id 90 | end 91 | 92 | create_table :summaries do |t| 93 | t.integer :party_id, null: false 94 | 95 | # From Summary sheet: 96 | t.integer :total_monetary_contributions 97 | t.integer :total_nonmonetary_contributions 98 | t.integer :total_contributions_received 99 | t.integer :total_unpaid_bills 100 | t.integer :total_expenditures_made 101 | t.integer :total_misc_increases_to_cash 102 | t.integer :ending_cash_balance 103 | 104 | # From Schedule A summary: 105 | t.integer :total_unitemized_contributions 106 | 107 | t.index :party_id, unique: true 108 | end 109 | 110 | create_table :maps do |t| 111 | t.string :emp1, null: false 112 | t.string :emp2, null: false 113 | t.string :type, null: true 114 | 115 | t.index [:emp2 ], unique: true 116 | end 117 | create_table :category_contributions do |t| 118 | t.integer :recipient_id, null: false 119 | t.string :name, null: false 120 | t.string :contype, null: false 121 | t.integer :number, null: false 122 | t.integer :amount, null: false 123 | 124 | t.index [:recipient_id] 125 | end 126 | create_table :employer_contributions do |t| 127 | t.integer :recipient_id, null: false 128 | t.integer :employer_id 129 | t.string :name, null: false 130 | t.string :contrib, null: false 131 | t.integer :amount, null: false 132 | 133 | t.index [:recipient_id] 134 | end 135 | create_table :whales do |t| 136 | t.integer :contributor_id, null: false 137 | t.integer :amount 138 | end 139 | create_table :multiples do |t| 140 | t.integer :contributor_id, null: false 141 | t.integer :number 142 | end 143 | 144 | create_table :lobbyists do |t| 145 | t.string :name 146 | t.string :firm 147 | 148 | t.index [:name], unique: true 149 | t.index [:firm] 150 | end 151 | 152 | create_table :payment_codes do |t| 153 | t.string :code 154 | t.string :text 155 | 156 | t.index [:code], unique: true 157 | end 158 | 159 | create_table :imports do |t| 160 | t.datetime :import_time 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /assets/js/views/candidate.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.Views.Candidate = Backbone.View.extend({ 2 | 3 | template: HandlebarsTemplates['candidate'], 4 | 5 | initialize: function(options) { 6 | this.candidateName = options.candidateName; 7 | 8 | if (OpenDisclosure.Data.candidates.length > 0) { 9 | this.findCandidateAndRender(); 10 | } 11 | 12 | this.listenTo(OpenDisclosure.Data.candidates, 'sync', this.findCandidateAndRender); 13 | }, 14 | 15 | findCandidateAndRender: function() { 16 | var shortNameMatches = function(c) { 17 | return c.linkPath().indexOf(this.candidateName) >= 0; 18 | }.bind(this); 19 | 20 | var candidate = OpenDisclosure.Data.candidates.find(shortNameMatches); 21 | 22 | if (candidate) { 23 | this.model = candidate; 24 | this.contributions = new OpenDisclosure.Contributions([], { candidateId: candidate.attributes.id }); 25 | this.contributions.fetch(); 26 | this.payments = new OpenDisclosure.Payments([], { candidateId: candidate.attributes.id }); 27 | this.payments.fetch(); 28 | this.render(); 29 | } else { 30 | location.assign(location.href.replace("candidate/", "searchCommittee/")); 31 | } 32 | }, 33 | 34 | render: function(){ 35 | this.$el.html(this.template(this.templateContext())); 36 | 37 | if (this.model.get('summary') !== null){ 38 | //Render Subviews 39 | if (OpenDisclosure.Data.categoryContributions.length > 0) { 40 | this.renderCategoryChart(); 41 | } 42 | if (OpenDisclosure.Data.categoryPayments.length > 0) { 43 | this.renderPaymentCategoryChart(); 44 | } 45 | 46 | if (OpenDisclosure.Data.employerContributions.length > 0) { 47 | this.renderTopContributors(); 48 | } 49 | 50 | if (this.contributions.length > 0) { 51 | this.renderAllContributions(); 52 | } 53 | } else { 54 | $('#category').hide(); 55 | $('#payment').hide(); 56 | $('#topContributors').hide(); 57 | $('#contributors').hide(); 58 | } 59 | 60 | 61 | //Listen for new data 62 | this.listenTo(OpenDisclosure.Data.categoryContributions, 'sync', this.renderCategoryChart); 63 | this.listenTo(OpenDisclosure.Data.categoryPayments, 'sync', this.renderPaymentCategoryChart); 64 | this.listenTo(OpenDisclosure.Data.employerContributions, 'sync', this.renderTopContributors); 65 | this.listenTo(this.contributions, 'sync', this.renderAllContributions); 66 | }, 67 | 68 | renderCategoryChart: function() { 69 | var candidateId = this.model.attributes.id; 70 | this.categories = _.filter(OpenDisclosure.Data.categoryContributions.models, function(c) { 71 | return c.attributes.recipient_id == candidateId; 72 | }); 73 | 74 | new OpenDisclosure.CategoryView({ 75 | el: '#category', 76 | collection: this.categories, 77 | attributes: this.model.attributes 78 | }); 79 | }, 80 | 81 | renderPaymentCategoryChart: function() { 82 | var candidateId = this.model.attributes.id; 83 | this.categories = _.filter(OpenDisclosure.Data.categoryPayments.models, function(c) { 84 | return c.attributes.payer_id == candidateId; 85 | }); 86 | 87 | new OpenDisclosure.Views.PaymentCategory({ 88 | el: '#payment', 89 | list: '#paymentList', 90 | collection: this.categories, 91 | payments: this.payments, 92 | attributes: this.model.attributes 93 | }); 94 | }, 95 | 96 | renderTopContributors: function(){ 97 | // Filter contributors based on cadidateId 98 | var count = 0; 99 | var candidateId = this.model.attributes.id; 100 | 101 | this.topContributions = _.filter(OpenDisclosure.Data.employerContributions.models, function(c) { 102 | return c.attributes.recipient_id == candidateId; 103 | }).sort(function(a, b){return b.attributes.amount - a.attributes.amount}); 104 | 105 | // Create a new subview 106 | new OpenDisclosure.TopContributorsView({ 107 | el: "#topContributors", 108 | collection: this.topContributions.slice(0, 10), 109 | candidate: this.model.get('short_name') 110 | }); 111 | }, 112 | 113 | renderAllContributions: function(){ 114 | var candidateId = this.model.attributes.id; 115 | 116 | new OpenDisclosure.ContributorsView({ 117 | el: "#contributors", 118 | collection: this.contributions, 119 | headline: 'All Contributions to ' + this.model.get('short_name'), 120 | showDate: true 121 | }); 122 | }, 123 | 124 | templateContext: function() { 125 | var context = _.clone(this.model.attributes); 126 | 127 | context.imagePath = this.model.imagePath(); 128 | context.lastUpdatedDate = this.model.get('last_updated_date'); 129 | 130 | if (this.model.get('summary') !== null) { 131 | context.summary.totalContributions = this.model.totalContributions(); 132 | context.summary.availableBalance = this.model.availableBalance(); 133 | context.summary.totalExpenditures = this.model.friendlySummaryNumber('total_expenditures_made'); 134 | context.summary.pctPersonalContributions = this.model.pctPersonalContributions(); 135 | context.summary.pctSmallContributions = this.model.pctSmallContributions(); 136 | } 137 | 138 | return context; 139 | } 140 | }); 141 | -------------------------------------------------------------------------------- /assets/js/views/_dailyContributionsChart.js: -------------------------------------------------------------------------------- 1 | OpenDisclosure.DailyContributionsChartView = OpenDisclosure.ChartView.extend({ 2 | draw: function(el){ 3 | var chart = this; 4 | chart.data = this.collection; 5 | chart.candidates = _.pluck(OpenDisclosure.BootstrappedData.candidates, "short_name"); 6 | 7 | var margin = {top: 0, right: 0, bottom: 30, left: 100}, 8 | svgWidth = chart.dimensions.width, 9 | svgHeight = chart.dimensions.height, 10 | chartWidth = svgWidth - margin.left - margin.right, 11 | chartHeight = svgHeight - margin.top - margin.bottom; 12 | 13 | chart.svg = d3.select(el).append("svg") 14 | .attr("width", svgWidth) 15 | .attr("height", svgHeight) 16 | .attr("viewBox", "0 0 " + svgWidth + " " + svgHeight) 17 | .attr("preserveAspectRatio", "xMidYMid") 18 | .append("g") 19 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 20 | 21 | // Color scale - takes a candidate name and returns a CSS class name. 22 | chart.color = d3.scale.ordinal() 23 | .domain(chart.candidates) 24 | .range(d3.range(chart.candidates.length).map(function(i) { 25 | return "q" + (i + 1) + "-12"; 26 | })); 27 | 28 | var date_format = d3.time.format("%Y-%m-%d"); 29 | var parse_date = date_format.parse; 30 | 31 | var x = d3.time.scale() 32 | .range([0, chartWidth]); 33 | 34 | var y = d3.scale.linear() 35 | .range([chartHeight, 0]); 36 | 37 | // For each candidate, add a starting point at $0 38 | // and an ending point for today. 39 | for (var candidate in chart.data) { 40 | var contributions = chart.data[candidate]; 41 | contributions.unshift({ 42 | amount: 0, 43 | date: contributions[0].date 44 | }); 45 | contributions.push({ 46 | amount: contributions[contributions.length - 1].amount, 47 | date: date_format(new Date()) 48 | }) 49 | } 50 | 51 | // Find the maximum contribution so we can set the range. 52 | var y_max = 4500 + _.reduce(chart.data, function(maximum, candidate) { //the 4500 is added to prevent clipping at the top of the graph. 53 | var candidate_max = candidate[candidate.length - 1].amount; 54 | return Math.max(maximum, candidate_max); 55 | }, 0); 56 | 57 | var today = new Date() 58 | var electionDate = parse_date( "2014-11-04" ) 59 | var endDate = ( today < electionDate ? today : electionDate ) 60 | var dateOfFirstContribution = parse_date("2013-04-09") 61 | 62 | x.domain([ dateOfFirstContribution, endDate ]); 63 | y.domain([0, y_max]); 64 | 65 | // Format x axis labels 66 | var date_tick_format = d3.time.format.multi([ 67 | ["%b '%y", function(d) { return (d.getMonth() == 0 && d.getYear() != 113) } ], 68 | ["%b '%y", function(d) { return (d.getMonth() == 4 && d.getYear() == 113) } ], 69 | ["%b", function() { return true; } ] 70 | ]); 71 | 72 | var xAxis = d3.svg.axis() 73 | .scale(x) 74 | .orient("bottom") 75 | .ticks(d3.time.months, 1) 76 | .tickFormat(date_tick_format); 77 | 78 | var yAxis = d3.svg.axis() 79 | .scale(y) 80 | .tickFormat(d3.format("$,d")) 81 | .orient("left"); 82 | 83 | var line = d3.svg.line() 84 | .x(function(d) { 85 | return x(parse_date(d.date)); 86 | }) 87 | .y(function(d) { 88 | return y(d.amount); 89 | }); 90 | 91 | // Plot each line. 92 | for (var candidate in chart.data){ 93 | chart.svg.append("path") 94 | .datum(chart.data[candidate]) 95 | .attr("class", "line") 96 | .attr("id", candidate) 97 | .attr("d", line) 98 | .attr("class", chart.color(candidate)); 99 | } 100 | 101 | chart.svg.append("g") 102 | .attr("class", "x axis") 103 | .attr("transform", "translate(0," + (chartHeight) + ")") 104 | .call(xAxis); 105 | 106 | chart.svg.append("g") 107 | .attr("class", "y axis") 108 | .call(yAxis) 109 | .append("text") 110 | .attr("transform", "rotate(-90)") 111 | .attr("y", 6) 112 | .attr("dy", ".71em") 113 | .style("text-anchor", "end") 114 | .text("Total raised"); 115 | 116 | chart.drawLegend(); 117 | chart.drawTitle(); 118 | 119 | var font_size = chart.dimensions.width / 62; 120 | chart.svg.selectAll('text') 121 | .attr("font-size", font_size); 122 | }, 123 | 124 | drawLegend: function() { 125 | var candidates = _.keys(this.data); 126 | this.$el.append("
    "); 127 | for (var i = 0; i < candidates.length; i++){ 128 | var candidate = candidates[i]; 129 | this.$el.find('.legend').append("
    " + 130 | "
    " + 131 | "
    " + candidate + "
    " + 132 | "
    "); 133 | } 134 | }, 135 | 136 | drawTitle: function() { 137 | this.$el.prepend("

    Cumulative itemized campaign contributions

    "); 138 | this.$el.append("
    The numbers in this graph are calculated from a different data set than the contributions table above. For more details, please check the FAQ.
    "); 139 | } 140 | }) 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS VERSION IS DEPRECATED. SEE: https://github.com/caciviclab/odca-jekyll 2 | 3 | [![Stories in Ready](https://badge.waffle.io/openoakland/opendisclosure.png?label=ready&title=Ready)](https://waffle.io/openoakland/opendisclosure) 4 | opendisclosure 5 | ============== 6 | 7 | ## Overview 8 | 9 | The goal of the project is to produce useful visualizations and statistics for Oakland's campaign finance data, starting with the November 2014 mayoral race. 10 | 11 | Meeting notes can be found in [this Google Doc](https://docs.google.com/document/d/11xji54-RiszyFBQnSBOI5Ylmzn2vC9glwAoU6A8CM_0/edit?pli=1#). 12 | 13 | ## To install the backend in a Vagrant virtual box, follow the instructions here: 14 | [Instructions for installing backend in Vagrant](/README_installation_in_vagrant.md) 15 | 16 | ## Running Locally 17 | 18 | To start, you'll need ruby installed. 19 | 20 | brew install rbenv 21 | brew install ruby-build 22 | rbenv install 2.1.2 23 | 24 | Then install bundler and foreman: 25 | 26 | gem install bundler 27 | gem install foreman 28 | 29 | Install postgres: 30 | 31 | ```bash 32 | brew install postgres 33 | 34 | # choose one: 35 | # A) to start postgres on startup: 36 | ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents 37 | launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist 38 | 39 | # B) or, to run postgres in a terminal (you will have to leave it running) 40 | postgres -D /usr/local/var/postgres 41 | 42 | ARCHFLAGS="-arch x86_64" gem install pg 43 | ``` 44 | 45 | Now you can install the other dependencies with: 46 | 47 | bundle install 48 | 49 | Create your postgresql user: (may be unnecessary, depending on how postgres is 50 | installed): 51 | 52 | sudo -upostgres createuser $USER -P 53 | # enter a password you're not terribly worried to share 54 | echo DATABASE_URL="postgres://$USER:[your password]@localhost/postgres" > .env 55 | 56 | You should be all set. Run the app like this: 57 | 58 | foreman start 59 | 60 | Then, to get a local copy of all the data: 61 | 62 | bundle exec ruby backend/load_data.rb 63 | 64 | ## Data Source 65 | 66 | The raw, original, separated-by-year data can be found on Oakland's "NetFile" 67 | site here: http://ssl.netfile.com/pub2/Default.aspx?aid=COAK 68 | 69 | We process that data in a nightly ETL process. Every day (or so) [this 70 | dataset][1] is updated with the latest version of the data. **There is a [data 71 | dictionary of what all the columns mean here][2].** 72 | 73 | ## Name mapping 74 | 75 | When we aggregate to find top contributors by company and employee, we use a mapping table to correct for spelling errors and different ways of representing the same entity. This is stored in backend/map.csv and gets loaded into the maps table during the data load process. 76 | 77 | Since there is no easy way to calculate when two entities are the same updating the maps table requires human intervention. Here are the steps to update the data: 78 | 79 | 1. load the most recent data (see above). 80 | 2. In your favorite Postgres interface run this query and export it: 81 | SELECT DISTINCT * FROM ( 82 | SELECT 0, name, name FROM parties c, contributions 83 | WHERE contributor_id = c.id AND c.type <> 'Party::Individual' 84 | AND NOT name =ANY (SELECT Emp2 FROM maps) 85 | UNION ALL SELECT 0, employer, employer FROM parties c, contributions 86 | WHERE contributor_id = c.id AND c.type ='Party::Individual' 87 | AND NOT employer =ANY (SELECT Emp2 FROM maps) 88 | ) s 89 | 4. load map.csv and this new data into your favorite column oriented data processing tool 90 | e.g. Excel 91 | 5. sort on the Emp1 column 92 | 6. Search for rows that have 0 in the first column and see if they are equivalent 93 | to any near by entity. If they are, copy the value of Emp1 from that row 94 | to this one. If the entity is a union but "Union" in the type column. 95 | In some cases an equivalent entity might not sort near by, e.g: 96 | San Fransisco Bay Area Rapid Transit District : BART 97 | City of Oakland : Oakland, city of 98 | California Senate : State of CA Senate 99 | 7. Renumber the first column so all are unique. In Excel or equivalent you can 100 | set the first row to 1 and the second row to =A1+1 and copy that forumla to 101 | all the other rows. 102 | 103 | ## Deploying 104 | 105 | In order to deploy to production ([opendisclosure.io]) you will need a couple things: 106 | 107 | 1. A public-private SSH keypair (use the `ssh-keygen` command to make one) 108 | 2. A [Heroku](https://heroku.com) account. Make sure to associate it with your 109 | public key (`~/.ssh/id_rsa.pub`) 110 | 3. Permission for your Heroku account to deploy. You can get this from the 111 | current OpenDisclosure maintainers. 112 | 113 | Then, you can deploy via git: 114 | 115 | # first time setup: 116 | git remote add heroku git@heroku.com:opendisclosure.git 117 | 118 | # to deploy: 119 | git checkout master 120 | # highly recommended: run `git log` so you know what will be deployed. When 121 | # ready to deploy, run: 122 | git push heroku master 123 | 124 | Make sure to push changes back to this repository as well, so that heroku and 125 | this repository's master branch stay in-sync! 126 | 127 | [1]: https://data.oaklandnet.com/dataset/Campaign-Finance-FPPC-Form-460-Schedule-A-Monetary/3xq4-ermg 128 | [2]: https://data.sfgov.org/Ethics/Campaign-Finance-Data-Key/wygs-cc76 129 | -------------------------------------------------------------------------------- /assets/js/vendor/topojson.v1.min.js: -------------------------------------------------------------------------------- 1 | !function(){function e(e,n){function t(n){var t=e.arcs[n],r=t[0],o=[0,0];return t.forEach(function(e){o[0]+=e[0],o[1]+=e[1]}),[r,o]}var r={},o={};n.forEach(function(e){var n,a,i=t(e),u=i[0],c=i[1];if(n=o[u])if(delete o[n.end],n.push(e),n.end=c,a=r[c]){delete r[a.start];var f=a===n?n:n.concat(a);r[f.start=n.start]=o[f.end=a.end]=f}else if(a=o[c]){delete r[a.start],delete o[a.end];var f=n.concat(a.map(function(e){return~e}).reverse());r[f.start=n.start]=o[f.end=a.start]=f}else r[n.start]=o[n.end]=n;else if(n=r[c])if(delete r[n.start],n.unshift(e),n.start=u,a=o[u]){delete o[a.end];var s=a===n?n:a.concat(n);r[s.start=a.start]=o[s.end=n.end]=s}else if(a=r[u]){delete r[a.start],delete o[a.end];var s=a.map(function(e){return~e}).reverse().concat(n);r[s.start=a.end]=o[s.end=n.end]=s}else r[n.start]=o[n.end]=n;else if(n=r[u])if(delete r[n.start],n.unshift(~e),n.start=c,a=o[c]){delete o[a.end];var s=a===n?n:a.concat(n);r[s.start=a.start]=o[s.end=n.end]=s}else if(a=r[c]){delete r[a.start],delete o[a.end];var s=a.map(function(e){return~e}).reverse().concat(n);r[s.start=a.end]=o[s.end=n.end]=s}else r[n.start]=o[n.end]=n;else if(n=o[c])if(delete o[n.end],n.push(~e),n.end=u,a=o[u]){delete r[a.start];var f=a===n?n:n.concat(a);r[f.start=n.start]=o[f.end=a.end]=f}else if(a=r[u]){delete r[a.start],delete o[a.end];var f=n.concat(a.map(function(e){return~e}).reverse());r[f.start=n.start]=o[f.end=a.start]=f}else r[n.start]=o[n.end]=n;else n=[e],r[n.start=u]=o[n.end=c]=n});var a=[];for(var i in o)a.push(o[i]);return a}function n(n,t,r){function a(e){0>e&&(e=~e),(l[e]||(l[e]=[])).push(s)}function i(e){e.forEach(a)}function u(e){e.forEach(i)}function c(e){"GeometryCollection"===e.type?e.geometries.forEach(c):e.type in d&&(s=e,d[e.type](e.arcs))}var f=[];if(arguments.length>1){var s,l=[],d={LineString:i,MultiLineString:u,Polygon:u,MultiPolygon:function(e){e.forEach(u)}};c(t),l.forEach(arguments.length<3?function(e,n){f.push(n)}:function(e,n){r(e[0],e[e.length-1])&&f.push(n)})}else for(var p=0,v=n.arcs.length;v>p;++p)f.push(p);return o(n,{type:"MultiLineString",arcs:e(n,f)})}function t(e,n){return"GeometryCollection"===n.type?{type:"FeatureCollection",features:n.geometries.map(function(n){return r(e,n)})}:r(e,n)}function r(e,n){var t={type:"Feature",id:n.id,properties:n.properties||{},geometry:o(e,n)};return null==n.id&&delete t.id,t}function o(e,n){function t(e,n){n.length&&n.pop();for(var t,r=s[0>e?~e:e],o=0,i=r.length;i>o;++o)n.push(t=r[o].slice()),f(t,o);0>e&&a(n,i)}function r(e){return e=e.slice(),f(e,0),e}function o(e){for(var n=[],r=0,o=e.length;o>r;++r)t(e[r],n);return n.length<2&&n.push(n[0].slice()),n}function i(e){for(var n=o(e);n.length<4;)n.push(n[0].slice());return n}function u(e){return e.map(i)}function c(e){var n=e.type;return"GeometryCollection"===n?{type:n,geometries:e.geometries.map(c)}:n in l?{type:n,coordinates:l[n](e)}:null}var f=d(e.transform),s=e.arcs,l={Point:function(e){return r(e.coordinates)},MultiPoint:function(e){return e.coordinates.map(r)},LineString:function(e){return o(e.arcs)},MultiLineString:function(e){return e.arcs.map(o)},Polygon:function(e){return u(e.arcs)},MultiPolygon:function(e){return e.arcs.map(u)}};return c(n)}function a(e,n){for(var t,r=e.length,o=r-n;o<--r;)t=e[o],e[o++]=e[r],e[r]=t}function i(e,n){for(var t=0,r=e.length;r>t;){var o=t+r>>>1;e[o]e&&(e=~e);var t=o[e];t?t.push(n):o[e]=[n]})}function t(e,t){e.forEach(function(e){n(e,t)})}function r(e,n){"GeometryCollection"===e.type?e.geometries.forEach(function(e){r(e,n)}):e.type in u&&u[e.type](e.arcs,n)}var o={},a=e.map(function(){return[]}),u={LineString:n,MultiLineString:t,Polygon:t,MultiPolygon:function(e,n){e.forEach(function(e){t(e,n)})}};e.forEach(r);for(var c in o)for(var f=o[c],s=f.length,l=0;s>l;++l)for(var d=l+1;s>d;++d){var p,v=f[l],h=f[d];(p=a[v])[c=i(p,h)]!==h&&p.splice(c,0,h),(p=a[h])[c=i(p,v)]!==v&&p.splice(c,0,v)}return a}function c(e,n){function t(e){i.remove(e),e[1][2]=n(e),i.push(e)}var r,o=d(e.transform),a=p(e.transform),i=l(s),u=0;for(n||(n=f),e.arcs.forEach(function(e){var t=[];e.forEach(o);for(var a=1,u=e.length-1;u>a;++a)r=e.slice(a-1,a+2),r[1][2]=n(r),t.push(r),i.push(r);e[0][2]=e[u][2]=1/0;for(var a=0,u=t.length;u>a;++a)r=t[a],r.previous=t[a-1],r.next=t[a+1]});r=i.pop();){var c=r.previous,v=r.next;r[1][2]0;){var r=(n+1>>1)-1,a=o[r];if(e(t,a)>=0)break;o[a.index=n]=a,o[t.index=n=r]=t}}function t(n){for(var t=o[n];;){var r=n+1<<1,a=r-1,i=n,u=o[i];if(ae;++e){var r=arguments[e];n(r.index=o.push(r)-1)}return o.length},r.pop=function(){var e=o[0],n=o.pop();return o.length&&(o[n.index=0]=n,t(0)),e},r.remove=function(r){var a=r.index,i=o.pop();return a!==o.length&&(o[i.index=a]=i,(e(i,r)<0?n:t)(a)),a},r}function d(e){if(!e)return v;var n,t,r=e.scale[0],o=e.scale[1],a=e.translate[0],i=e.translate[1];return function(e,u){u||(n=t=0),e[0]=(n+=e[0])*r+a,e[1]=(t+=e[1])*o+i}}function p(e){if(!e)return v;var n,t,r=e.scale[0],o=e.scale[1],a=e.translate[0],i=e.translate[1];return function(e,u){u||(n=t=0);var c=0|(e[0]-a)/r,f=0|(e[1]-i)/o;e[0]=c-n,e[1]=f-t,n=c,t=f}}function v(){}var h={version:"1.5.1",mesh:n,feature:t,neighbors:u,presimplify:c};"function"==typeof define&&define.amd?define(h):"object"==typeof module&&module.exports?module.exports=h:this.topojson=h}(); -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # All Vagrant configuration is done here. The most common configuration 9 | # options are documented and commented below. For a complete reference, 10 | # please see the online documentation at vagrantup.com. 11 | 12 | # Every Vagrant virtual environment requires a box to build off of. 13 | config.vm.box = "https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-i386-vagrant-disk1.box" 14 | 15 | # Disable automatic box update checking. If you disable this, then 16 | # boxes will only be checked for updates when the user runs 17 | # `vagrant box outdated`. This is not recommended. 18 | # config.vm.box_check_update = false 19 | 20 | # Create a forwarded port mapping which allows access to a specific port 21 | # within the machine from a port on the host machine. In the example below, 22 | # accessing "localhost:8080" will access port 80 on the guest machine. 23 | # config.vm.network "forwarded_port", guest: 80, host: 8080 24 | 25 | # Create a private network, which allows host-only access to the machine 26 | # using a specific IP. 27 | # config.vm.network "private_network", ip: "192.168.33.10" 28 | 29 | # Create a public network, which generally matched to bridged network. 30 | # Bridged networks make the machine appear as another physical device on 31 | # your network. 32 | # config.vm.network "public_network" 33 | 34 | # If true, then any SSH connections made will enable agent forwarding. 35 | # Default value: false 36 | # config.ssh.forward_agent = true 37 | 38 | # Share an additional folder to the guest VM. The first argument is 39 | # the path on the host to the actual folder. The second argument is 40 | # the path on the guest to mount the folder. And the optional third 41 | # argument is a set of non-required options. 42 | # config.vm.synced_folder "../data", "/vagrant_data" 43 | 44 | # Provider-specific configuration so you can fine-tune various 45 | # backing providers for Vagrant. These expose provider-specific options. 46 | # Example for VirtualBox: 47 | # 48 | # config.vm.provider "virtualbox" do |vb| 49 | # # Don't boot with headless mode 50 | # vb.gui = true 51 | # 52 | # # Use VBoxManage to customize the VM. For example to change memory: 53 | # vb.customize ["modifyvm", :id, "--memory", "1024"] 54 | # end 55 | # 56 | # View the documentation for the provider you're using for more 57 | # information on available options. 58 | 59 | # Enable provisioning with CFEngine. CFEngine Community packages are 60 | # automatically installed. For example, configure the host as a 61 | # policy server and optionally a policy file to run: 62 | # 63 | # config.vm.provision "cfengine" do |cf| 64 | # cf.am_policy_hub = true 65 | # # cf.run_file = "motd.cf" 66 | # end 67 | # 68 | # You can also configure and bootstrap a client to an existing 69 | # policy server: 70 | # 71 | # config.vm.provision "cfengine" do |cf| 72 | # cf.policy_server_address = "10.0.2.15" 73 | # end 74 | 75 | # Enable provisioning with Puppet stand alone. Puppet manifests 76 | # are contained in a directory path relative to this Vagrantfile. 77 | # You will need to create the manifests directory and a manifest in 78 | # the file default.pp in the manifests_path directory. 79 | # 80 | # config.vm.provision "puppet" do |puppet| 81 | # puppet.manifests_path = "manifests" 82 | # puppet.manifest_file = "site.pp" 83 | # end 84 | 85 | # Enable provisioning with chef solo, specifying a cookbooks path, roles 86 | # path, and data_bags path (all relative to this Vagrantfile), and adding 87 | # some recipes and/or roles. 88 | # 89 | # config.vm.provision "chef_solo" do |chef| 90 | # chef.cookbooks_path = "../my-recipes/cookbooks" 91 | # chef.roles_path = "../my-recipes/roles" 92 | # chef.data_bags_path = "../my-recipes/data_bags" 93 | # chef.add_recipe "mysql" 94 | # chef.add_role "web" 95 | # 96 | # # You may also specify custom JSON attributes: 97 | # chef.json = { mysql_password: "foo" } 98 | # end 99 | 100 | # Enable provisioning with chef server, specifying the chef server URL, 101 | # and the path to the validation key (relative to this Vagrantfile). 102 | # 103 | # The Opscode Platform uses HTTPS. Substitute your organization for 104 | # ORGNAME in the URL and validation key. 105 | # 106 | # If you have your own Chef Server, use the appropriate URL, which may be 107 | # HTTP instead of HTTPS depending on your configuration. Also change the 108 | # validation key to validation.pem. 109 | # 110 | # config.vm.provision "chef_client" do |chef| 111 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 112 | # chef.validation_key_path = "ORGNAME-validator.pem" 113 | # end 114 | # 115 | # If you're using the Opscode platform, your validator client is 116 | # ORGNAME-validator, replacing ORGNAME with your organization name. 117 | # 118 | # If you have your own Chef Server, the default validation client name is 119 | # chef-validator, unless you changed the configuration. 120 | # 121 | # chef.validation_client_name = "ORGNAME-validator" 122 | 123 | config.vm.network :forwarded_port, host: 5000, guest: 5000 124 | config.vm.provider :virtualbox do |virtualbox| 125 | # allocate 1024 mb RAM 126 | virtualbox.customize ["modifyvm", :id, "--memory", "2024"] 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /backend/2014_Lobbyist_Directory.csv: -------------------------------------------------------------------------------- 1 | Lobbyist ID,Lobbyist,Lobbyist_Firm,Lobbyist_Phone ,Lobbyist_Email,Lobbyist_Business_Address,Lobbyist_City,Lobbyist_State,Lobbyist_Zip,Client 1,Client 2,Client 3,Client 4,Client 5,Client 6,Client 7 2 | 1,"Aroner, Dion",AJE Partners,510-849-4811,diona@ajepartners.com,"1803 6th St, #B",Berkeley,CA,94710,Safeway,Waste Management of Alameda County,Redflex Traffic Systems (California) Inc.,,,, 3 | 2,"Chen, Serena",,510-982-3191,serena.chen@lung.org,424 Pendleton Way,Oakland,CA,94621,American Lung Association in California,,,,,, 4 | 3,"Clemens, Alex",,415-364-0000,clemens@barcoast.com,38 Mason St ,San Francisco,CA,94102,"Car2Go N.A., LLC",,,,,, 5 | 4,"Coleman, Michon",,510-752-2004,michon.a.coleman@kp.org,"4501 Broadway, 2nd Floor",Oakland,CA,94611,"Kaiser Foundation Health Plan, Inc. ",,,,,, 6 | 5,"Crowley, Michael",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,, 7 | 6,"Custar, Kristin",The Jordan Company L.P.,212-572-0800,info@olsonhagel.com,"399 Park Avenue, 30th Floor",New York,NY,10022,,,,,,, 8 | 7,"Dada, Suhail",,949-720-4518,suhail.dada@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,, 9 | 8,"De Luca, Niccolo",,510-835-9050,noeluca@townsenopa.com,"300 Frank Ogawa Plaza, Suite 204",Oakland,CA,94612,Oakland Museum,Oakland Zoo,,,,, 10 | 9,"Edwards, Merlin",Edwards Consulting,510-635-4669,mek011@pacbell.net,P.O. Box 1072,El Cerrito,CA,94530,Mass Mutual,Urban Core LLC,,,,, 11 | 10,"Gonzalez, Javier",,408-416-6344,jgonzalez@calrest.org,"621 Capitol Mall, Suite 2000 ",Sacramento,CA,95814,California Restaurant Association ,,,,,, 12 | 11,"Gray, Kevin",,949-720-4871,kevin.gray@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,, 13 | 12,"Guarino, Tom",,510-437-2552,tgg3@pge.com,"1330 Broadway, Suite 1535",Oakland,CA,94612,Pacific Gas and Electric Company,,,,,, 14 | 13,"Horning, Ryan",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,, 15 | 14,"Jewel, Elisabeth",AJE Partners,510-849-4811,elisabeth@ajepartners.com,"1803 6th St, #B",Berkeley,CA,94710,AT&T,Safeway,Redflex Traffic Systems (California) Inc.,,,, 16 | 15,"Kingsley, Daniel",SKS Investments,415-421-8200,dkingsley@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,, 17 | 16,"Kraetesch, Neil",,510-638-4900,nkraetsch@athletics.com,700 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,, 18 | 17,"Linney, Douglas",The Next Generation,510-444-4710 x309,dklinney@nextgeneration.org,"1814 Franklin St, Suite 510",Oakland,CA,94612,American Promotional Events West,,,,,, 19 | 18,"Madrid, Michael",Grass Roots Lab,916-704-4569,madrid@grassrootslab.com,"1029 J Street, Suite 100",Sacramento,CA,95814,ecoATM,,,,,, 20 | 19,"McClure, Mark",California Capital & Investment Group,510-463-6338,mmcclure@californiagroup.com,"300 Frank Ogawa Plaza, Suite 340",Oakland,CA,94612,Foster Media,California Capital & Investment Group,Oakland Global Rail Enterprise,,,, 21 | 20,"McConnell, Gregory",The McConnell Group,510-834-0400,gmc@themcconnellgroup.com,"350 Frank Ogawa Plaza, #703",Oakland,CA,94612,Jobs and Housing Coalition,LAZ Parking,Feld Entertainment,PG&E,Xerox,, 22 | 21,"McPoyle, Nancy",,415-389-6800,nancy.mcpoyle@oracle.com,500 Oracle Parkway,Redwood Shores,CA,94070,"Oracle America, Inc.",,,,,, 23 | 22,"Mersten, David",,858-766-7650,dmersten@ecoatm.com,10515 Vista Sorrento Parkway,San Diego,CA,92121,ecoATM,,,,,, 24 | 23,"Moffatt, John",Nielsen Merksamer,916-446-6752,jmoffatt@nmgovlaw.com,"1415 L St., Suite 1200",Sacramento,CA,95814,ecoATM,,,,,, 25 | 24,"Muehlethaler, Jeff",,213-739-3720,jeff.muehlethaler@pimco.com,1633 Broadway,New York,NY,10019,Pacific Investment Management Company LLC,,,,,, 26 | 25,"Neal, Kathy",,510-430-1252,kathy@kneal.com,6114 La Salle Ave #641 ,Oakland,CA,94611,"Kneal Resource System, Inc ",Dailey-Wells,,,,, 27 | 26,"Revell, Dennis",Revell Communications,916-443-3816,DCR@revellcommunications.com,"1 Capitol Mall, Suite 210",Sacramento,CA,95814,"American Promotional Events, Inc.",,,,,, 28 | 27,"Reynolds, Jessica",Reynolds Strategies,510-450-0295,jessica@reynolds-strategies.com,645 Mariposa Ave,Oakland,CA,94610,Oakland Association of Realtors,,,,,, 29 | 28,"Romano, Mark",,949-720-6010,mark.romano@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,, 30 | 29,"Spees, Richard",,510-227-5600,rlspees@msn.com,2431 Mariner Sq Dr.,Alamaeda,CA,94501,St. John's Church,,,,,, 31 | 30,"Stein, Paul",SKS Investments,415-421-8200,pstein@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,, 32 | 31,"Tucker, David",,510-613-2142,dtucker2@wm.com,172 98th Ave.,Oakland,CA,94603,Waste Management of Alameda County,,,,,, 33 | 32,"Turner, Ronnie",Turner Development Resource Group,510-395-2766,rtdevelops@comcast.net,"4100 Redwood Rd, #170",Oakland,CA,94619,"UrbanCore Development, LLC",,,,,, 34 | 33,"Urbina, Geoff",,206-684-6259,geoff.urbina@key.com,"1301 5th Avenue, 25th Floor",Seattle,WA,98101,KeyBanc Capital markets Inc.,,,,,, 35 | 34,"Villegas, Peter",,213-621-8406,peter.villegas@jpmchase.com,"300 S. Grand Ave., Suite 400",Los Angeles,CA,90071,JPMorgan Chase & Co.,,,,,, 36 | 35,"Wallace, Juan Carlos", SKS Investments,415-421-8200,jcwallace@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,, 37 | 36,"Welch, Sean",Nielsen Merksamer,415-389-6800,swelch@nmgovlaw.com,"2350 Kerner Blvd., Suite 250",San Rafael,CA,94901,ecoATM,,,,,, 38 | 37,"Wolff, Lewis",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,, 39 | 38,"Wolmark, Steven",SKS Investments,415-421-8200,swolmark@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,, 40 | -------------------------------------------------------------------------------- /neo4j/README.md: -------------------------------------------------------------------------------- 1 | Disclosure Data in Neo4j 2 | ======================== 3 | 4 | ## Warning! 5 | 6 | The Neo4j part of the project, while cool and promising, is not currently part 7 | of the project nor something the OpenDisclosure team is currently developing. 8 | This folder is here for historical purposes and in case anyone happens upon 9 | this project who is passionate about graph databases and wants to play around 10 | with this stuff. 11 | 12 | ## Overview 13 | 14 | Importing in the [Neo4j Graph Database](http://neo4j.org) allows for analysis and querying of this connected data. Neo4j can then be used to answer questions of interest, and provide data for visulatizations. 15 | 16 | ## Importing data 17 | 18 | The raw data from Oakland's "NetFile" site (http://ssl.netfile.com/pub2/Default.aspx?aid=COAK) needs the A-Contributions and E-Expenditures sheets saved to CSV. The [import.cyp](import.cyp) will then load these into Neo4j (it assumes they are saved in /tmp). 19 | 20 | ``` 21 | curl -b /dev/null -L -o - "https://docs.google.com/spreadsheet/ccc?key=0AgAZooSCCSN0dGUwVXdHQzlXamwxSEVtcTVzMHNZN1E&output=csv&gid=0" > /tmp/A-Contributions.csv 22 | curl -b /dev/null -L -o - "https://docs.google.com/spreadsheet/ccc?key=0AgAZooSCCSN0dGUwVXdHQzlXamwxSEVtcTVzMHNZN1E&output=csv&gid=8" > /tmp/E-Expenditure.csv 23 | 24 | wget http://dist.neo4j.org/neo4j-community-2.1.0-M01-unix.tar.gz 25 | tar xzf neo4j-community-2.1.0-M01-unix.tar.gz 26 | cd neo4j-community-2.1.0-M01 27 | ./bin/neo4j start 28 | ./bin/neo4j-shell -file import.cyp 29 | ``` 30 | 31 | ## Visualizing 32 | 33 | Using the Neo4j browser (http://localhost:7474), data can be visualized by running queries in the [Cypher query language](http://cypherlang.org). 34 | 35 | ``` 36 | // Candidates and contributors who provided over $700 37 | MATCH (f:Candidate) // Find all the candidates 38 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c) WHERE n.amount > 700 // Find the contributors giving > $700 39 | OPTIONAL MATCH (c)-[:LOCATION|EMPLOYER|WORKS_AS]->(l) // Match additional data abount contributors for visualization 40 | RETURN f, c, l 41 | ``` 42 | 43 | ![Screen Shot](http://cl.ly/image/0v2u0A1e3e0q/Screen%20Shot%202014-02-23%20at%2012.33.32%20AM.png) 44 | 45 | ## Querying data 46 | 47 | Using the [Cypher query language](http://cypherlang.org), the following questions can be answered: 48 | 49 | [1. Who are the top 5-10 contributors to each campaign? (people or company)](https://github.com/openoakland/opendisclosure/issues/3) 50 | 51 | ``` 52 | MATCH (f:Candidate) 53 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c) 54 | WITH f, c, sum(n.amount) AS amount ORDER BY amount DESC 55 | RETURN f.name AS candidate, collect({name: c.name, amount: amount})[0..10] AS contributors 56 | ``` 57 | | candidate | contributors 58 | |-----------------------------|----------------------------------------- 59 | |Patrick McCullough Mayor 2014| [{name:"Patrick McCullough",amount:100}] 60 | |Re-Elect Mayor Quan 2014 | [{name:"Sprinkler Fitters & Apprentices Local 483 PAC, id#1298012",amount:1400},{name:"IBEW PAC Educational Fund",amount:1400},{name:"IUPAT-Political Action Together-Political Committee",amount:1400},{name:"Electrical Workers Local 595 PAC",amount:1300},{name:"Steven Douglas",amount:700},{name:"ARCALA Land Company",amount:700},{name:"Conway Jones, Jr.",amount:700},{name:"Annie Tsai",amount:700},{name:"Ronald Herron",amount:700},{name:"Lailan Huen",amount:700}]" 61 | |Parker for Oakland Mayor 2014| [{name:"Scott Taylor",amount:700},{name:"Larry Williams",amount:700},{name:"Terrence McGrath",amount:700},{name:"Joseph Whitehouse",amount:700},{name:"Pamela Lathan",amount:700},{name:"Rob Davenport",amount:700},{name:"Ed Page",amount:700},{name:"Nneka Rimmer",amount:700},{name:"John Lewis",amount:700},{name:"Mobile Connectory LLC",amount:700}] 62 | Libby Schaaf for Oakland Mayor 2014|[{name:"Sal Fahey",amount:1000},{name:"John Protopappas",amount:700},{name:"Joan Story",amount:700},{name:"Richard Schaaf",amount:700},{name:"Antioch Street Limited",amount:700},{name:"Lang Scoble",amount:700},{name:"Paul Weinstein",amount:700},{name:"Bradley Brownlow",amount:700},{name:"Jerrold Kram",amount:700},{name:"Julian Beasley",amount:700}] 63 | |Joe Tuman for Mayor 2014 | [{name:"James and Darcy Diamantine",amount:1400},{name:"Tod & Jen Vedock",amount:1400},{name:"John and Alanna Dittoe",amount:1400},{name:"Bill/Warrine Young/Coffey",amount:1400},{name:"Mr and Mrs EM Edward Downer",amount:1400},{name:"Mark & Susan Stutzman",amount:1400},{name:"Leonard & Silvia Silvani",amount:1400},{name:"Bradford & Barbara Dickason",amount:1400},{name:"Robert & Ann Spears",amount:1400},{name:"Patricia & Tony Theophilos",amount:1400}] 64 | 65 | [2. Which industries support each candidate? (top 5 industries, aggregate amount given from this industry to each candidate, percentage that this contribution makes in the committee’s entire fundraising efforts for this reporting period)](https://github.com/openoakland/opendisclosure/issues/4) 66 | 67 | _The import data does not yet contain industry information (although there is Occupation?)_ 68 | 69 | [3. Bar graph showing how much campaign committee has raised so far versus how much that committee has spent in expenditures on the campaign.](https://github.com/openoakland/opendisclosure/issues/5) 70 | 71 | ``` 72 | MATCH (f:Candidate) 73 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-() 74 | WITH f, sum(n.amount) AS received 75 | OPTIONAL MATCH (f)-[n:PAYED]->() 76 | WITH f, received, sum(n.amount) AS spent 77 | RETURN f.name AS candidate, spent, received, received - spent AS balance ORDER BY balance DESC 78 | ``` 79 | candidate|spent|received|balance 80 | ---------|-----|--------|------- 81 | Parker for Oakland Mayor 2014|33783|166884|133101 82 | Joe Tuman for Mayor 2014|19119|140100|120981 83 | Libby Schaaf for Oakland Mayor 2014|4293|117795|113502 84 | Re-Elect Mayor Quan 2014|39215|121522|82307 85 | Patrick McCullough Mayor 2014|0|100|100 86 | 87 | [4. What percentage of campaign contributions to each mayoral candidate are made from Oakland residents vs. others?](https://github.com/openoakland/opendisclosure/issues/6) 88 | 89 | ``` 90 | MATCH (f:Candidate) 91 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c) 92 | WHERE (c)-[:LOCATION]->({name:'OAKLAND', state:'CA'}) 93 | WITH f, sum(n.amount) AS oakContributions 94 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c) 95 | WITH f, oakContributions, sum(n.amount) as total 96 | RETURN f.name AS candidate, round((toFloat(oakContributions) / total) * 100) AS percentage ORDER BY percentage DESC 97 | ``` 98 | 99 | candidate|percentage 100 | ---------|---------- 101 | Patrick McCullough Mayor 2014|100 102 | Joe Tuman for Mayor 2014|67 103 | Libby Schaaf for Oakland Mayor 2014|53 104 | Re-Elect Mayor Quan 2014|51 105 | Parker for Oakland Mayor 2014|35 106 | 107 | [5. Evaluate any overlap between corporations and industries that employ and register a lobbyist with the City of Oakland and campaign contribution and expenditure data.](https://github.com/openoakland/opendisclosure/issues/7) 108 | 109 | _The import data does not yet contain industry information_ 110 | -------------------------------------------------------------------------------- /assets/js/vendor/GoogleChart.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | Backbone.GoogleChart = (function(_super) { 8 | __extends(GoogleChart, _super); 9 | 10 | function GoogleChart() { 11 | this._listers = __bind(this._listers, this); 12 | this._removeGoogleListener = __bind(this._removeGoogleListener, this); 13 | this._addGoogleListener = __bind(this._addGoogleListener, this); 14 | this.unbind = __bind(this.unbind, this); 15 | this.bind = __bind(this.bind, this); 16 | this.render = __bind(this.render, this); 17 | this.id = __bind(this.id, this); 18 | this.onGoogleLoad = __bind(this.onGoogleLoad, this); 19 | return GoogleChart.__super__.constructor.apply(this, arguments); 20 | } 21 | 22 | 23 | /* 24 | * Initialize a new GoogleChart object 25 | * 26 | * Example: 27 | * lineChart = new Backbone.GoogleChart({ 28 | * chartType: 'ColumnChart', 29 | * dataTable: [['Germany', 'USA', 'Brazil', 'Canada', 'France', 'RU'], 30 | * [700, 300, 400, 500, 600, 800]], 31 | * options: {'title': 'Countries'}, 32 | * }); 33 | * 34 | * $('body').append( lineChart.render().el ); 35 | * 36 | * For the complete list of options please checkout 37 | * https://developers.google.com/chart/interactive/docs/reference#chartwrapperobject 38 | * 39 | */ 40 | 41 | GoogleChart.prototype.initialize = function(options) { 42 | var chartOptions; 43 | chartOptions = _.extend({}, options); 44 | _(['el', 'id', 'attributes', 'className', 'tagName']).map(function(k) { 45 | return delete chartOptions[k]; 46 | }); 47 | google.load('visualization', '1', { 48 | packages: ['corechart'], 49 | callback: (function(_this) { 50 | return function() { 51 | return _this.onGoogleLoad("loaded"); 52 | }; 53 | })(this) 54 | }); 55 | return this.onGoogleLoad((function(_this) { 56 | return function() { 57 | var formatter, _i, _ref, _results; 58 | _this.google = google.visualization; 59 | if (chartOptions.dataTable instanceof Array) { 60 | chartOptions.dataTable = _this.google.arrayToDataTable(chartOptions.dataTable); 61 | } 62 | if (typeof chartOptions.beforeDraw === "function") { 63 | chartOptions.beforeDraw(_this, chartOptions); 64 | } 65 | if (formatter = chartOptions.formatter) { 66 | _((function() { 67 | _results = []; 68 | for (var _i = 0, _ref = chartOptions.dataTable.getNumberOfRows() - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); } 69 | return _results; 70 | }).apply(this)).map(function(index) { 71 | return _(formatter.columns).map(function(column) { 72 | return chartOptions.dataTable.setFormattedValue(index, column, formatter.callback(chartOptions.dataTable.getValue(index, column))); 73 | }); 74 | }); 75 | } 76 | return _this.wrapper = new _this.google.ChartWrapper(chartOptions); 77 | }; 78 | })(this)); 79 | }; 80 | 81 | 82 | /* 83 | * Execute a callback once google visualization fully loaded 84 | */ 85 | 86 | GoogleChart.prototype.onGoogleLoad = function(callback) { 87 | if (callback === "loaded") { 88 | this.googleLoaded = true; 89 | return _(this.onGoogleLoadItems).map(function(fn) { 90 | return fn(); 91 | }); 92 | } else { 93 | if (this.googleLoaded) { 94 | return callback(); 95 | } else { 96 | return (this.onGoogleLoadItems || (this.onGoogleLoadItems = [])).push(callback); 97 | } 98 | } 99 | }; 100 | 101 | 102 | /* 103 | * Execute a callback once a given element ID appears in DOM ( mini livequery ). 104 | * 105 | * We need it because GoogleChart object only draw itself on DOM elements 106 | * so we first need to wait for our element to be added to the DOM before 107 | * we call GoogleChart.draw(). 108 | * 109 | * Usage: 110 | * Backbone.GoogleChart.watch("#myid", function() { console.log("I'm in") }); 111 | * $("body").append("
    "); // 'I"m in' should be printed to console 112 | * 113 | */ 114 | 115 | GoogleChart.watch = function(id, fn) { 116 | var func, timeout; 117 | (GoogleChart._list || (GoogleChart._list = {}))[id] = fn; 118 | if (GoogleChart._watching) { 119 | return; 120 | } 121 | GoogleChart._watching = true; 122 | timeout = 10; 123 | return (func = function() { 124 | _(GoogleChart._list).map(function(fn, id) { 125 | if ($(id)[0]) { 126 | return fn() & delete GoogleChart._list[id]; 127 | } 128 | }); 129 | if (_(GoogleChart._list).isEmpty()) { 130 | return GoogleChart._watching = false; 131 | } else { 132 | return setTimeout(func, timeout += 10); 133 | } 134 | })(); 135 | }; 136 | 137 | 138 | /* 139 | * Returns the wrapping element id 140 | * if no id was specified on initialization a random one will be returned 141 | */ 142 | 143 | GoogleChart.prototype.id = function() { 144 | var _ref; 145 | return ((_ref = this.el) != null ? _ref.id : void 0) || this.randomID(); 146 | }; 147 | 148 | 149 | /* 150 | * "Instruct" the current graph instance to draw itself once its visiable on DOM 151 | * return the current instance 152 | */ 153 | 154 | GoogleChart.prototype.render = function() { 155 | this.onGoogleLoad((function(_this) { 156 | return function() { 157 | return _this.constructor.watch("#" + _this.el.id, function() { 158 | return _this.wrapper.draw(_this.el.id); 159 | }); 160 | }; 161 | })(this)); 162 | return this; 163 | }; 164 | 165 | 166 | /* 167 | * Register for ChartWrapper events 168 | * For the complete event list please look at the events section under 169 | * https://developers.google.com/chart/interactive/docs/reference#chartwrapperobject 170 | * 171 | * graph = new Backbone.GoogleChart({chartOptions: options}); 172 | * graph.on("select",function(graph) { console.log("Someone click on me!") }) 173 | * graph.on("error",function(graph) { console.log("Oops") }) 174 | * graph.on("ready",function(graph) { console.log("I'm ready!") }) 175 | * 176 | */ 177 | 178 | GoogleChart.prototype.bind = function(event, callback) { 179 | var _base; 180 | (_base = this._listers())[event] || (_base[event] = this._addGoogleListener(event)); 181 | return GoogleChart.__super__.bind.call(this, event, callback); 182 | }; 183 | 184 | 185 | /* 186 | * alias of @bind 187 | */ 188 | 189 | GoogleChart.prototype.on = GoogleChart.prototype.bind; 190 | 191 | 192 | /* 193 | * Unbind events, please look at Backbone.js docs for the API 194 | */ 195 | 196 | GoogleChart.prototype.unbind = function(event, callback, context) { 197 | if (event) { 198 | this._removeGoogleListener(event); 199 | } else if (callback) { 200 | _(_(this._listers()).pairs()).map((function(_this) { 201 | return function(pair) { 202 | if (pair[1] === callback) { 203 | return _this._removeGoogleListener(pair[0]); 204 | } 205 | }; 206 | })(this)); 207 | } else { 208 | _(_(this._listers()).values()).map(this._removeGoogleListener); 209 | } 210 | return GoogleChart.__super__.unbind.call(this, event, callback, context); 211 | }; 212 | 213 | 214 | /* 215 | * alias of @unbind 216 | */ 217 | 218 | GoogleChart.prototype.off = GoogleChart.prototype.unbind; 219 | 220 | GoogleChart.prototype._addGoogleListener = function(event) { 221 | return this.onGoogleLoad((function(_this) { 222 | return function() { 223 | return _this.google.events.addListener(_this.wrapper, event, function() { 224 | return _this.trigger(event, _this.wrapper.getChart()); 225 | }); 226 | }; 227 | })(this)); 228 | }; 229 | 230 | GoogleChart.prototype._removeGoogleListener = function(event) { 231 | return this.onGoogleLoad((function(_this) { 232 | return function() { 233 | _this.google.events.removeListener(_this._listers()[event]); 234 | return delete _this._listers()[event]; 235 | }; 236 | })(this)); 237 | }; 238 | 239 | GoogleChart.prototype._listers = function() { 240 | return this._listersObj || (this._listersObj = {}); 241 | }; 242 | 243 | 244 | /* 245 | * Generate a random ID, gc_XXX 246 | */ 247 | 248 | GoogleChart.prototype.randomID = function() { 249 | return _.uniqueId("gc_"); 250 | }; 251 | 252 | return GoogleChart; 253 | 254 | })(Backbone.View); 255 | 256 | }).call(this); 257 | --------------------------------------------------------------------------------